diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..f0a379f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,74 @@ +name: Release + +# A version tag (v1.2.3) builds, tests and packages the app and creates a +# DRAFT GitHub release with the zip, its SHA-256 and a build provenance +# attestation. Publishing the draft is a manual step. Run it by hand +# (workflow_dispatch) for a dry run that only uploads the zip as an artifact. +on: + push: + tags: ['v*'] + workflow_dispatch: + +permissions: + contents: read + +jobs: + package: + name: Build and package (x64) + runs-on: windows-2025 + timeout-minutes: 30 + permissions: + contents: write # create the draft release + id-token: write # sign the provenance attestation + attestations: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Build and test + run: cmake --workflow --preset ci + + # A tag must match the version compiled into the executables. + - name: Check the version + if: startsWith(github.ref, 'refs/tags/') + shell: pwsh + env: + TAG: ${{ github.ref_name }} + run: | + $built = (& .\build\Release\gao.exe --version).Split(' ')[-1] + if ("v$built" -ne $env:TAG) { throw "tag $env:TAG does not match the built version $built" } + + - name: Package + shell: pwsh + env: + NAME: ${{ github.ref_type == 'tag' && github.ref_name || 'dev' }} + run: | + $stage = New-Item -ItemType Directory "dist/GpuAutoOptimizer" + Copy-Item build/Release/GpuAutoOptimizer.exe, build/Release/gao.exe, README.md, LICENSE $stage + $zip = "dist/GpuAutoOptimizer-$env:NAME-win-x64.zip" + Compress-Archive -Path "$stage/*" -DestinationPath $zip + Get-FileHash $zip -Algorithm SHA256 | ForEach-Object { "$($_.Hash.ToLower()) $(Split-Path $zip -Leaf)" } | + Set-Content -Encoding ascii "$zip.sha256" + Remove-Item $stage -Recurse + + - name: Upload (dry run) + if: github.event_name == 'workflow_dispatch' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: GpuAutoOptimizer-dev + path: dist/ + + - name: Attest build provenance + if: startsWith(github.ref, 'refs/tags/') + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 + with: + subject-path: dist/*.zip + + - name: Create draft release + if: startsWith(github.ref, 'refs/tags/') + shell: pwsh + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ github.ref_name }} + run: gh release create $env:TAG (Get-ChildItem dist/*).FullName --draft --verify-tag --generate-notes --title "GPU Auto Optimizer $env:TAG" diff --git a/CLAUDE.md b/CLAUDE.md index 70ba985..e82a138 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,11 +13,18 @@ the user is in another language — translate, never paste non-English text into ```powershell $bin = "C:\Program Files\Microsoft Visual Studio\18\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin" -& "$bin\cmake.exe" -S . -B build -A x64 -& "$bin\cmake.exe" --build build --config Release -& "$bin\ctest.exe" --test-dir build -C Release --output-on-failure +& "$bin\cmake.exe" --workflow --preset ci # configure, build Release, test +& "$bin\cmake.exe" --build --preset debug; & "$bin\ctest.exe" --preset debug ``` +Everything we compile is `/W4 /WX`; fix warnings, do not suppress them. + +## Layout + +Two executables on one engine: `gao.exe` (CLI, `src/app/main.cpp`) and +`GpuAutoOptimizer.exe` (window + tray + watchdog, `src/app/gui/`). Logic both +need lives in `src/app/common.*`, never in either entry point. + ## Rules the design depends on - `src/core/` is pure logic: no `windows.h`, NVML, NVAPI or D3D headers. It reaches hardware diff --git a/CMakeLists.txt b/CMakeLists.txt index 5627ab5..46fb93a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -58,6 +58,8 @@ add_library(core STATIC src/core/boot.cpp src/core/task_xml.hpp src/core/task_xml.cpp + src/core/watchdog.hpp + src/core/watchdog.cpp ) target_include_directories(core PUBLIC src) target_include_directories(core SYSTEM PRIVATE third_party) @@ -75,8 +77,12 @@ add_library(hw STATIC target_include_directories(hw PUBLIC src) target_link_libraries(hw PUBLIC core d3d11 dxgi d3dcompiler shell32 ole32 PRIVATE gao_warnings gao_hardening) -add_executable(gao src/app/main.cpp src/app/gao.manifest) -target_link_libraries(gao PRIVATE core hw gao_warnings gao_hardening) +# UI-independent application logic shared by both executables. +add_library(app STATIC src/app/common.hpp src/app/common.cpp) +target_link_libraries(app PUBLIC core hw PRIVATE gao_warnings gao_hardening) + +add_executable(gao src/app/main.cpp src/app/gao.manifest src/app/app.rc) +target_link_libraries(gao PRIVATE app gao_warnings gao_hardening) # d3dcompiler_47.dll, d3d11.dll and dxgi.dll are not KnownDLLs: delay-load # them so they resolve after SetDefaultDllDirectories(SYSTEM32) in main(), # never from the exe's folder. @@ -87,6 +93,28 @@ target_link_options(gao PRIVATE /DELAYLOAD:d3dcompiler_47.dll /DELAYLOAD:d3d11.d # compatibility. (/DYNAMICBASE and /HIGHENTROPYVA are already x64 defaults.) target_link_options(gao PRIVATE /GUARD:CF /CETCOMPAT) +# Dear ImGui (vendored, unmodified) as its own library, compiled with the +# compiler's defaults rather than our warning set. +add_library(imgui STATIC + third_party/imgui/imgui.cpp third_party/imgui/imgui_draw.cpp + third_party/imgui/imgui_tables.cpp third_party/imgui/imgui_widgets.cpp + third_party/imgui/backends/imgui_impl_win32.cpp third_party/imgui/backends/imgui_impl_dx11.cpp +) +target_include_directories(imgui SYSTEM PUBLIC third_party/imgui) +target_link_libraries(imgui PUBLIC d3d11 dwmapi PRIVATE gao_hardening) + +# The window and tray executable (Windows subsystem: no console). +add_executable(GpuAutoOptimizer WIN32 + src/app/gui/main_gui.cpp + src/app/gui/ui.hpp src/app/gui/ui.cpp + src/app/gui/worker.hpp src/app/gui/worker.cpp + src/app/gao.manifest + src/app/app.rc +) +target_link_libraries(GpuAutoOptimizer PRIVATE app imgui gao_warnings gao_hardening delayimp) +target_link_options(GpuAutoOptimizer PRIVATE /DELAYLOAD:d3dcompiler_47.dll /DELAYLOAD:d3d11.dll /DELAYLOAD:dxgi.dll + /DELAYLOAD:dwmapi.dll /DEPENDENTLOADFLAG:0x800 /GUARD:CF /CETCOMPAT) + enable_testing() add_executable(core_tests tests/test_main.cpp @@ -100,8 +128,9 @@ add_executable(core_tests tests/test_search.cpp tests/test_boot.cpp tests/test_task_xml.cpp + tests/test_watchdog.cpp ) target_link_libraries(core_tests PRIVATE core doctest gao_warnings) add_test(NAME core_tests COMMAND core_tests) -set_target_properties(core hw gao core_tests PROPERTIES COMPILE_WARNING_AS_ERROR ON) +set_target_properties(core hw app gao GpuAutoOptimizer core_tests PROPERTIES COMPILE_WARNING_AS_ERROR ON) diff --git a/README.md b/README.md index f014c24..bfbb793 100644 --- a/README.md +++ b/README.md @@ -12,20 +12,41 @@ > [!IMPORTANT] -> `main` is being rewritten from Python to C++ (see [the design spec](docs/superpowers/specs/2026-09-20-cpp-rewrite-design.md)). What's here today is the repository skeleton, the hardware layer and the stress load: it can read live telemetry, write clock offsets and fan speed verified against the driver, and run a DX11 load that checks every value it computes and ends in a stability verdict. It can also tune: `--optimize` searches power limit, core and memory offsets against that verdict, saves the result, and `--boot on` re-applies it at every logon. **There is no GUI yet** -- that is a later phase. v1.0 will be the finished C++ application. The previous, working Python version (search engine, risk profiles, tray GUI) is preserved at the `v0.9-python` git tag and still works as it always did; it just isn't what lives on `main` anymore. +> `main` is being rewritten from Python to C++ (see [the design spec](docs/superpowers/specs/2026-09-20-cpp-rewrite-design.md)). What's here today is the repository skeleton, the hardware layer and the stress load: it can read live telemetry, write clock offsets and fan speed verified against the driver, and run a DX11 load that checks every value it computes and ends in a stability verdict. It can also tune: `--optimize` searches power limit, core and memory offsets against that verdict, saves the result, and `--boot on` re-applies it at every logon. `GpuAutoOptimizer.exe` is the window and tray app on top of the same engine; its tray process keeps the tune applied for the whole session. v1.0 will be the finished C++ application. The previous, working Python version (search engine, risk profiles, tray GUI) is preserved at the `v0.9-python` git tag and still works as it always did; it just isn't what lives on `main` anymore. > [!WARNING] > The commands below that write to the GPU (`--set-core`, `--set-mem`, `--set-fan`, `--reset`, `--optimize`, `--apply`, `--boot`) change clocks, power limit and fan behavior directly through the driver. Every write is verified by reading the value back rather than trusting the driver's return code, but a wrong offset can still make a card unstable. Use at your own risk. ## Quick start +### From a release + +Download `GpuAutoOptimizer--win-x64.zip` from [Releases](https://github.com/Rovey/gpu-auto-optimizer/releases), check it and unzip it anywhere: + +```powershell +(Get-FileHash .\GpuAutoOptimizer--win-x64.zip -Algorithm SHA256).Hash # compare with the .sha256 file +gh attestation verify .\GpuAutoOptimizer--win-x64.zip --repo Rovey/gpu-auto-optimizer # optional: proves CI built it from this repo +``` + +The executables are not code-signed, so Windows SmartScreen may say it "protected your PC" on the first start: choose **More info**, then **Run anyway**. Turning on apply-at-logon copies both executables to `%ProgramFiles%\GpuAutoOptimizer\`, so the unzipped folder can be deleted afterwards. + +### From source + Requires an x64 Windows machine with Visual Studio 2026 (C++ and CMake workload), whose bundled CMake (4.2 or newer) knows the VS 2026 generator. From a Developer PowerShell, in the repo root: ```powershell cmake --workflow --preset ci # configure, build Release, run the tests ``` -That builds `.\build\Release\gao.exe` -- a command-line tool; there is no GUI yet. +That builds two programs in `.\build\Release\`: `GpuAutoOptimizer.exe`, the window and tray app, and `gao.exe`, the command-line tool. Both use the same engine and the same saved state. + +### The app + +Start `GpuAutoOptimizer.exe`. The window shows the GPU, live telemetry, the saved tune and what is applied right now; pick a preset and press **Optimize** (it offers to restart as administrator first, because tuning writes clocks and power limits). The run screen shows temperature, power and every probe as it happens; **Abort** restores stock. The results screen compares before and after and can turn on **apply at every logon**. + +With apply-at-logon on, Windows starts the app in the tray at every logon. It applies the saved tune and then keeps it applied: every 30 seconds it reads back what the driver reports. If the tune was reset (a driver reset or TDR), it applies it again; if that happens four times within an hour it stops and tells you the tune is probably not stable; if another program such as MSI Afterburner changed the settings, it leaves them alone and tells you; after a driver update it does not apply the old tune and tells you to optimize again. While it keeps a tune applied (or a run is in progress), closing the window keeps it in the tray and **Exit** in the tray menu quits; otherwise closing the window quits. **Revert to stock** in the tray menu, or `gao --reset` from a shell, puts the card at stock and the watchdog leaves it there until you apply the tune again. Only one optimize runs at a time, from the window or the command line. + +### The command line ```powershell .\build\Release\gao.exe --version @@ -40,7 +61,7 @@ That builds `.\build\Release\gao.exe` -- a command-line tool; there is no GUI ye `--optimize` runs baseline → power → core → memory → 60 s soak (~10 minutes), leaves the result applied until reboot, and keeps a crash journal so a setting that froze the machine is never tried again. Ctrl+C restores stock. The core and memory edges it finds are each re-checked with a 30 s probe before the safety margin is applied, and the memory search stops at the **bandwidth peak** rather than at the highest offset that does not produce errors: GDDR6X and GDDR6 retry failed transfers, so an excessive memory clock costs speed long before it produces a wrong result. `--bandwidth` prints the current memory bandwidth. -All state (`gao.json`, the crash journal, `boot.log`) lives in `%ProgramData%\GpuAutoOptimizer`, which users can read but only administrators can write. `--boot on` copies `gao.exe` to `%ProgramFiles%\GpuAutoOptimizer\` and registers a logon task (`\GpuAutoOptimizer\BootApply`) that runs that copy and re-applies the saved profile; `--status` says whether the installed copy matches your build; if the machine crashes within 2 minutes of three logons in a row, or the NVIDIA driver version changes, boot-apply stops and `--status` / `boot.log` say why. A profile only applies to the card it was tuned on (NVML UUID). +All state (`gao.json`, the crash journal, `boot.log`) lives in `%ProgramData%\GpuAutoOptimizer`, which users can read but only administrators can write. `--boot on` copies both executables to `%ProgramFiles%\GpuAutoOptimizer\` and registers a logon task (`\GpuAutoOptimizer\BootApply`) that starts that copy of the app in the tray, which re-applies the saved profile and keeps it applied; `--status` says whether the installed copies match your build; if the machine crashes within 2 minutes of three logons in a row, or the NVIDIA driver version changes, boot-apply stops and `--status` / `boot.log` say why. A profile only applies to the card it was tuned on (NVML UUID). > [!NOTE] > The logon task runs with administrator rights, so everything it touches is admin-only: the exe in Program Files and the state folder in ProgramData (a folder someone else created there first, or a link in its place, is refused). Both locations come from the registry, not from environment variables. The CRT is linked statically and every non-system DLL (`d3d11`, `dxgi`, `d3dcompiler_47`) is delay-loaded from System32, so nothing placed next to the exe is ever loaded. Profile values outside the search range are refused. @@ -52,7 +73,7 @@ All state (`gao.json`, the crash journal, `boot.log`) lives in `%ProgramData%\Gp ### Requirements - Windows 10/11, 64-bit -- MSVC (Visual Studio) with the C++ and CMake components, or CMake >= 3.28 with MSVC on `PATH` +- To build: Visual Studio 2026 with the C++ and CMake components (CMake 4.2 or newer) - An NVIDIA GPU with a recent driver, for `--probe` and the write commands (NVML/NVAPI are loaded at runtime; nothing in the build itself needs a GPU, and CI builds the full executable on a GPU-less runner) ## Project structure @@ -74,9 +95,11 @@ src/hw/ the only code that touches hardware: NVML, NVAPI and D3D11. stress.* DX11 compute stress load; the GPU checks every value it computes app_files.* gao.json (atomic writes), the crash journal and boot.log, all flushed to disk boot_task.* the logon task, via schtasks.exe -src/app/ entry point -- currently a command-line probe (gao.exe); an ImGui UI is a later phase. +src/app/ common.*: application logic both programs share (optimize, boot-apply, logon task) + main.cpp gao.exe, the command line + gui/ GpuAutoOptimizer.exe: window, tray, optimize worker thread, watchdog loop tests/ doctest, core only. -third_party/ doctest, nlohmann/json -- vendored as source, no package manager. +third_party/ doctest, nlohmann/json, Dear ImGui -- vendored as source, no package manager. docs/hardware-checks.md the manual checklist for what CI can't test (the runner has no GPU) ``` @@ -105,13 +128,13 @@ cmake --build --preset asan ctest --preset asan # needs the MSVC bin\Hostx64\x64 folder on PATH ``` -Our code builds at `/W4 /permissive-` with warnings as errors; `third_party/` is a system include. All of it is compiled with Control Flow Guard and SDL checks; `gao.exe` is linked CET-compatible and carries an application manifest. +Our code builds at `/W4 /permissive-` with warnings as errors; `third_party/` is a system include. All of it is compiled with Control Flow Guard and SDL checks; both executables are linked CET-compatible and carry an application manifest. `core_tests` is the only test binary. It covers `src/core/` -- presets, config round-trip, and the `GpuControl` seam that lets tests drive fake hardware through lambdas -- and needs no GPU, which is why CI runs it on a GitHub-hosted `windows-2025` runner, once normally and once under AddressSanitizer. `src/hw/` (NVML, NVAPI) is verified manually on real hardware instead, through [`docs/hardware-checks.md`](docs/hardware-checks.md); CI still builds the full `gao.exe` so a link error in the hardware layer is caught there rather than on a developer's machine. ## Status and roadmap -Phases 0-4 of the rewrite are done: repository migration, the CMake/CI skeleton, the hardware layer (NVML telemetry and power limit, NVAPI clock offsets, NVAPI fan control), the DX11 stress load with its stability verdict, the search with its crash-safe journal, and persistence with boot-apply. The phases after this one -- the ImGui screens and tray, and finally the release workflow -- are not started. See [the design spec](docs/superpowers/specs/2026-09-20-cpp-rewrite-design.md) for the full phase table and the reasoning behind each decision (why NVAPI + NVML directly, why the Afterburner route was abandoned, why undervolting ships opt-in). +Phases 0-5 of the rewrite are done: repository migration, the CMake/CI skeleton, the hardware layer (NVML telemetry and power limit, NVAPI clock offsets, NVAPI fan control), the DX11 stress load with its stability verdict, the search with its crash-safe journal, persistence with boot-apply, and the window and tray app with its tune watchdog. Phase 6 is under way: the executables carry an icon and version information, and a `v*` tag builds a draft release (zip, SHA-256, build provenance attestation); the first release is still to be tagged. See [the design spec](docs/superpowers/specs/2026-09-20-cpp-rewrite-design.md) for the full phase table and the reasoning behind each decision (why NVAPI + NVML directly, why the Afterburner route was abandoned, why undervolting ships opt-in). ## License diff --git a/docs/hardware-checks.md b/docs/hardware-checks.md index 1b1c035..130965a 100644 --- a/docs/hardware-checks.md +++ b/docs/hardware-checks.md @@ -52,6 +52,15 @@ root; adjust the path for a Debug build. | 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 | | | 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`. | | 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. | +| 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. | +| 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 | | +| 31 | Abort from the window | Abort during the core search | yes | the run ends with `aborted`, card at stock | | +| 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 | | +| 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 | | +| 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 | | +| 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. | +| 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); the second optimize refuses with `another optimize is already running` | | +| 37 | Closing the window | close the window with no tune applied, then again with a tune kept applied | no / yes | without a tune the app exits; with one it hides to the tray | Pass without a tune (2026-09-27): the app exits on close. | ## Notes diff --git a/docs/superpowers/specs/2026-09-27-p5-ui-tray-watchdog-design.md b/docs/superpowers/specs/2026-09-27-p5-ui-tray-watchdog-design.md new file mode 100644 index 0000000..391fca0 --- /dev/null +++ b/docs/superpowers/specs/2026-09-27-p5-ui-tray-watchdog-design.md @@ -0,0 +1,72 @@ +# P5 — Window, Tray and Tune Watchdog + +Date: 2026-09-27 +Status: approved (design) under the user's overnight mandate; not yet implemented +Parents: `2026-09-20-cpp-rewrite-design.md` §8, `2026-09-26-p4-persistence-and-boot-apply-design.md`, +`2026-09-27-p4b-measurement-and-hardening-design.md`. Research: ImGui/tray/crash notes of 2026-09-27. + +## 1. Goal + +A desktop app next to the CLI: pick a preset, press Optimize, watch it run, see the result — +and a tray process that re-applies the tune at logon and keeps it applied for the whole +session. The user asked for the last part explicitly: a tune that silently disappeared (TDR, +driver reset, another tool) must not cost a whole game session. + +## 2. Decisions + +| Decision | Choice | Why | +|---|---|---| +| Executables | `gao.exe` (CLI, console subsystem, unchanged) + **`GpuAutoOptimizer.exe`** (window + tray, Windows subsystem), both linking `core` and `hw` | One console-subsystem exe flashes a console for the GUI; one Windows-subsystem exe breaks CLI output and exit codes in a shell. Two small exes is the least fragile. | +| Logon | The logon task runs `GpuAutoOptimizer.exe --tray` (elevated) instead of `gao.exe --boot-apply`. It does the boot decision (strikes, driver, GPU), applies, clears the strike after 2 minutes, and **stays resident** in the tray with the watchdog | One resident elevated process instead of a one-shot plus a second tray process. No console flash at logon. Task time limit becomes unlimited (`PT0S`). `gao --boot-apply` remains for compatibility. | +| Watchdog | Every 30 s: read back the applied core/mem offsets and power limit, compare with the profile | The user's requirement. | +| Watchdog cases | matches → nothing; **all stock, same driver/GPU** → re-apply (a TDR or driver reset), but after 3 resets within an hour stop re-applying and notify ("keeps getting reset — likely unstable, re-optimize"); **neither stock nor ours** → another tool (Afterburner, NVIDIA App) owns the GPU: back off, notify once, do not fight; **driver or GPU changed** → do not apply the old tune, notify "driver changed — re-optimize?" once | Evidence from the research: fighting another tuner causes write wars; repeated resets are an instability signal. | +| Screens | Home (GPU, live telemetry, profile/boot status, four preset cards, Optimize), Run (phase log, live score/temperature graph, Abort), Results (before/after, Apply at boot toggle) | Parent spec §8. The three "advanced sliders" are dropped (YAGNI; presets cover the objectives). | +| ImGui | Dear ImGui **v1.92.9b** (tagged master, not docking), vendored under `third_party/imgui` with the Win32 + DX11 backends; no ImPlot (`ImGui::PlotLines` is enough for two series) | Research: docking/multi-viewport are overkill; 1.92 brings dynamic fonts and `FontScaleDpi`. | +| Rendering | Own D3D11 device for the UI (separate from the stress device); render only when needed: block in `MsgWaitForMultipleObjectsEx`, ~4 Hz on Home, ~10 Hz on Run, a few frames after input, nothing while hidden in the tray | The tool measures the GPU; its own UI must not load it. DXGI occlusion is unreliable on Windows 11 — track visibility ourselves. | +| Work | Optimize runs on a `std::jthread`; the UI reads a snapshot under a mutex; Abort sets the same abort flag Ctrl+C sets | A 10-minute run must never block the window. | +| Elevation | Unelevated launch works (telemetry, status); Optimize / Apply / Boot toggle show "Restart as administrator" (`ShellExecuteExW` with `runas`) | Elevation stays the user's choice (manifest `asInvoker`). | +| Tray | `Shell_NotifyIcon` with hWnd+uID (no GUID: the exe is unsigned and may move), `NOTIFYICON_VERSION_4` + `NIF_SHOWTIP`, tooltip = temp/clock, menu Open / Re-apply / Revert to stock / Exit; `TaskbarCreated` re-adds the icon; `ChangeWindowMessageFilterEx` lets Explorer's messages reach the elevated window; balloons with `NIIF_RESPECT_QUIET_TIME` | Research, Microsoft Learn. | +| Single instance | Named mutex; a second launch activates the first window and exits | Two watchdogs would fight each other. | +| Crash dumps | `SetUnhandledExceptionFilter` → `MiniDumpWriteDump` into the state folder (`crash-