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
74 changes: 74 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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"
13 changes: 10 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
35 changes: 32 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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.
Expand All @@ -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
Expand All @@ -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)
39 changes: 31 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,41 @@
</div>

> [!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-<version>-win-x64.zip` from [Releases](https://github.com/Rovey/gpu-auto-optimizer/releases), check it and unzip it anywhere:

```powershell
(Get-FileHash .\GpuAutoOptimizer-<version>-win-x64.zip -Algorithm SHA256).Hash # compare with the .sha256 file
gh attestation verify .\GpuAutoOptimizer-<version>-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
Expand All @@ -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.
Expand All @@ -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
Expand All @@ -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)
```

Expand Down Expand Up @@ -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

Expand Down
Loading