Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
00fea70
feat: the undervolt is saved as the profile
Rovey Oct 8, 2026
0cd4b36
feat: a saved undervolt is applied by `gao --apply` and at logon
Rovey Oct 8, 2026
ae2e6bc
feat: the watchdog keeps a saved undervolt applied and gives up at stock
Rovey Oct 8, 2026
86a0d4c
feat(gui): the undervolt in the window
Rovey Oct 8, 2026
e072da1
feat(gui): a run draws the curve; the result says how long each side …
Rovey Oct 8, 2026
ff328bc
feat: the undervolt run measures stock for five minutes, and counts i…
Rovey Oct 8, 2026
d8f68b1
feat: the points above the anchor are stored lower, so a warm card st…
Rovey Oct 8, 2026
0c3ff71
docs: the undervolt result as a card in the README
Rovey Oct 8, 2026
d0bbf4a
feat(gui): a drawn fan icon, and profile cards with an icon badge ins…
Rovey Oct 8, 2026
c07677f
fix(gui): the value of a telemetry tile sits right under its label
Rovey Oct 8, 2026
e292768
feat: a profile can carry an overclock and an undervolt together
Rovey Oct 8, 2026
e0e347c
feat: the undervolt can be searched on top of the saved overclock
Rovey Oct 8, 2026
339c256
feat(core): the quietest fan speed that holds a target temperature, b…
Rovey Oct 8, 2026
dd2b84a
feat: All in one: overclock, an undervolt on top of it, the fan tune …
Rovey Oct 8, 2026
92ccddb
docs: All in one from the window, with clean numbers, in the README a…
Rovey Oct 8, 2026
a4da898
fix(gui): the fan tile says who has the fans during a run
Rovey Oct 8, 2026
846b48d
fix(gui): the fan label during a fan tune is "(tuning)", and a tile n…
Rovey Oct 8, 2026
6883a39
feat: one click is all of it: a saved run keeps itself applied at log…
Rovey Oct 8, 2026
58592c1
fix(gui): the page scrolls with the mouse on any card; version 0.4.0
Rovey Oct 8, 2026
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 CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ add_library(core STATIC
src/core/vf_curve.cpp
src/core/undervolt.hpp
src/core/undervolt.cpp
src/core/fan_tune.hpp
src/core/fan_tune.cpp
)
target_include_directories(core PUBLIC src)
target_include_directories(core SYSTEM PRIVATE third_party)
Expand Down Expand Up @@ -147,6 +149,7 @@ add_executable(core_tests
tests/test_update.cpp
tests/test_vf_curve.cpp
tests/test_undervolt.cpp
tests/test_fan_tune.cpp
)
target_link_libraries(core_tests PRIVATE core doctest gao_warnings)
add_test(NAME core_tests COMMAND core_tests)
Expand Down
117 changes: 98 additions & 19 deletions README.md

Large diffs are not rendered by default.

13 changes: 12 additions & 1 deletion docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,18 @@ The applied offset is always at least one step below the confirmed edge. Profile
| Known limits of the health check | After a reset the stress load is rebuilt once, after the rest and before the health probe, outside its timing: it creates a new D3D device and recompiles its shader, so the probe's score no longer includes that. A rebuild that fails counts as a failed try of the health check (the rest and the check are tried up to three times) and its reason is logged (`the stress load could not be rebuilt: ...`); a fault during the rebuild ends the run through the crashed path (journal entry closed, card reset to stock). The load is rebuilt even when it did not notice the reset itself. Not yet measured on hardware. Still unknown: how long the rebuild takes after a reset (`gao --stress-recreate` measures the rebuild on a healthy driver only), whether the first health probe then passes at once, and whether the rest keeps the desktop usable. |
| Nothing about a reset is kept for later runs | A reset says nothing certain about the candidate, and a stored one would let a single invalid probe lower the bound for good. The cost: a run on a card it can push past its edge crosses that edge again every time. Only a freeze is remembered, through the crash journal. |
| An entry at stock clocks never becomes a ceiling | A ceiling of zero would mean "never search this clock", which no freeze at stock can justify. The soak and the +0 sample of the memory scan do not journal a clock that is at stock, and an open entry with an offset of 0 is reported as a freeze but lowers nothing. |
| No undervolting | Locking a voltage point hard-froze the reference RTX 4070, and reshaping the curve gave no measurable gain on a power-limited card. |
| No voltage lock | Locking a voltage point hard-froze the reference RTX 4070. An undervolt is a shape on the curve instead (see below). |
| An undervolt is a profile, in place of an overclock | `gao --undervolt` and the Undervolt card save a profile with the preset `undervolt`, stock clocks and power, and the flat top: the anchor's voltage (the card's own number; a slot is the driver's), the clock, and the raise it was tested with. A core offset and a flat top are one table on the card (hardware check 65), so `apply_profile` refuses a profile that has both, and saving one replaces the other. Applying looks the anchor up by voltage and refuses, at stock, when the curve has no such point or the point would have to be raised more than it was tested with plus what the built-in curve moves with temperature. A profile whose undervolt cannot be read whole is no profile: read without it, it would apply as plain stock and look applied. A build from before this reads the unknown preset as no profile too. |
| A flat top is judged by the curve, not by the offset read-out | The offset read-out reports core +0 with a flat top on the card (hardware check 66). `curve_state` tells stock, a flat top at the saved voltage and anything else apart by the raw offsets alone: frequencies move with temperature. The watchdog, `gao --status` and the dashboard use it. Every apply removes a flat top first, because an offset write that asks for what the driver already reports may change nothing. |
| The watchdog counts resets, and gives up at stock | A driver reset removes a core offset and a flat top alike (hardware check 68), and the watchdog re-applies. A reset that leaves the tune applied counts the same: the tray app sees driver resets through its display device and passes them on, so a tune the driver kept across its resets is still found unstable. A search's own resets are not counted. On the fourth within an hour the card is set to stock, not left with whatever the driver kept. |
| The points above the anchor are stored lower than it | The built-in curve moves with temperature, each point by its own step. A top written exactly flat ran in two ways on the warm reference card with the same offsets: on the anchor's voltage one clock step lower, or on a higher voltage holding the clock, for 12 to 14 W more (hardware check 72). The points above the anchor are therefore stored `kVfTailDropKhz` (45 MHz, three steps of the clock grid) below it, which makes the anchor the highest point whatever the temperature does. The card reports for every point the most that it or any point below it runs, so those points keep reading the anchor's frequency and cannot be checked by it: `apply_flat_top` writes the top flat first and reads it back there, as before, then lowers them in one write and checks that the offsets were stored, that the anchor still is on the target and that nothing reads above it. Measured: 155 W instead of 166 W in the soak, for a clock that is one step lower on the warm card. `gao --curve-flatten <mV> <MHz> 0` still writes the exactly flat top, for comparisons. |
| An overclock and an undervolt are two ends of one line | An overclock is the same offset on every point of the curve: at every frequency the card then runs on less voltage than it did, which is what an undervolt is. With the same margin on the raise there is nothing left to add: on the reference card the undervolt searched on top of core +135 saved 5 W of 201 (hardware check 74). So the two do not stack. What can be chosen is where on the line the card runs: the overclocked clock, the stock clock, or a clock in between. |
| A profile can carry both, as one shape | The core offset and the curve are one table on the card (hardware check 65), so an undervolt next to an overclock is one curve: the core offset on every point below the anchor, the anchor at the target, the points above it lower. `apply_flat_top` takes the offset as `base_raw`, levels the curve to it and keeps it below the anchor; `apply_profile` sets power, core and memory and writes the flat top over the core offset; `curve_state` tells that shape from the bare offset and from stock. Every raise is counted over the built-in curve, offset included: in the journal, for the margin, and in the profile's `raise_khz`. The offset read-out keeps reporting the core offset under the flat top (hardware check 73). |
| All in one takes the middle of the line | One run for a card nobody has tuned: five minutes at stock; the overclock of Best of my GPU; an undervolt on top of it at a clock half-way between what the card ran at stock and what it runs with the overclock (`UndervoltBase::clock_khz`); the fan tune; five minutes with everything applied. The half-way clock is a choice, not a measurement: it is the point where the result is better than stock on both counts, speed and power, which is what one button for everything should give. Wanting all of the speed is Best of my GPU or Max performance; wanting all of the saving is Undervolt. When the margin allows no raise beyond the overclock, the overclock's curve cut off at that clock is the result: at a lower clock the cut alone already means a lower voltage. A step that fails keeps what the steps before it found; a stop request ends at stock. The steps are the existing runs called in a row under one tuning lock, not a new search. |
| The fans are tuned by measuring, last | `tune_fan`: under the load of the finished tune the fans are set by hand, the temperature is given time to settle (within 1 C over three looks 15 s apart, three minutes at most), and the speed goes down 5 % at a time until the card settles above the target, 5 C under the profile's limit. The lowest speed that held is one point of the saved curve; full speed sits at the limit. It runs after the clocks because the heat to carry away is the tune's. The final five minutes then run on that curve: a tune that only held with louder fans does not get the quiet ones, and keeps the profile's own curve. |
| One click is all of it | A run from the window that ends saved switches apply-at-logon on itself (`UiState::keep_at_logon`, a ticked box on the Optimize page while it is still off): the promise is that the user presses one button and is done, and a second switch to find afterwards breaks it. It is a box and not a silent default because it copies the app to Program Files and registers a logon task. The command line does not do it: `gao --boot on` stays its own command. |
| The run draws the curve | During a run from the window the telemetry tick reads the curve back once a second and the run screen draws it over the last read at stock: the search's own writes, not a picture of the plan. It is a read from the window's own driver connection, like the applied-state read that was there already. |
| The undervolt run measures stock for as long as it soaks | Five minutes each (`kUvBaselineS = kUvSoakS`). A card is not heated through after 30 s: on the reference card the short stock run read 59 to 61 C and 185 W where five minutes gave 67 C at 72 % fan and 196 W, so a result compared with it looked no cooler than stock. The run takes four and a half minutes longer for it. It also puts the clock to keep and the built-in curve on a warm card, as the result will run. The long measurements (stock, confirm, soak) are announced through `UndervoltIo::measuring`, and the run screen counts them down. The result table names the length of both runs and shows the fan speed each ended on next to its peak temperature. The optimize run of the clock profiles still measures stock for 30 s (its score is the reference of the power search); its table says so under it. |
| Fans through NVML, stop zone via the driver | NVIDIA's legacy NVAPI fan API is gone on RTX 20-series and newer; NVML's `nvmlDeviceSetFanSpeed_v2` is public and verified by reading the target back. Below the stop threshold the driver owns the fans, so no failure of this app can leave them stopped. |
| Tray app plus logon task | Driver settings are volatile: a reboot or driver reset clears them. The logon task starts the tray app, whose watchdog keeps the tune applied; three crashing logons in a row switch it off. |
| An update is one click, and never more than that | The app asks GitHub for the latest release (`core/update`) and shows one line in the sidebar; nothing is downloaded until it is clicked. The zip must come from this project's releases over HTTPS and match the SHA-256 the GitHub API publishes for it, and both executables in it must carry the version the release claims. The order of these steps and every refusal are `install_release` in `core/update`, which gets its steps as callbacks and is tested with a fake source; a release without a usable SHA-256 is refused there too, not only by the parser, because an empty expectation would equal the hash of a file that cannot be read. Elevated, the download is unpacked under `%ProgramData%\GpuAutoOptimizer\update`, which only administrators can write, so nothing can be swapped between the check and the copy. The executables are replaced where they run from (a running one is renamed aside) and the app starts again with `--resume`, which keeps a tune that is applied under the watchdog. The trust this rests on is the GitHub account: the executables are not code-signed. |
Expand Down
Loading