Skip to content

Commit 83f2e64

Browse files
authored
Merge pull request #26 from Rovey/start-menu-entry
fix: the installed app is in the Start menu, and an update of the copy in Program Files asks for administrator rights
2 parents 9036862 + a5739f1 commit 83f2e64

9 files changed

Lines changed: 93 additions & 5 deletions

File tree

‎docs/development.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,13 @@ The applied offset is always at least one step below the confirmed edge. Profile
101101
| 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. |
102102
| 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. |
103103
| 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. |
104+
| The installed copy has a Start menu entry, and an update that cannot write asks for the rights | Without an entry Windows' search does not find the app, and a copy that is not running cannot be started except from its folder. The entry is made with the logon task, for all users, removed with it, and made by the copy that starts after an update when it is missing (installs of 0.4.1 and older have none). A copy started from that entry is not elevated and cannot replace its own files in Program Files: 0.4.0 failed there with `error 5`. The click on the update now starts the copy again elevated with `--update-now`, and that copy installs it (hardware check 85). |
104105
| A newer copy takes over from an older one | The app is single-instance, and the copy in the tray usually runs elevated. A copy that is started later reads the running one's version from a window property (builds before 0.3.1 have none and count as older) and, when it is newer itself, offers to replace it: it starts again elevated with `--replace`, asks the old copy to exit through its own tray-menu command (which hands the fans back and stops a run cleanly) and ends it only when it does not go and no optimize run is under way. The logon copy follows when apply-at-logon is on and that copy is not newer (`update_logon_copy`), because a profile saved by a new build must be one the logon copy accepts; but only after something the user did with the new build: an update, taking over from an older copy, or a run that saved a profile. A copy that was merely started installs nothing and says in its log that the logon copy is older. Until 0.4.0 the copy installed itself the moment it started elevated, and the task was created by starting `schtasks.exe`: on 2026-10-10 Windows Defender judged exactly that sequence in a build nobody had seen before (the app restarting itself elevated, copying itself to Program Files and creating a logon task with the highest rights, all within a second) as `Behavior:Win32/Execution.A!ml`, and removed the build, the logon copy and the task (hardware check 83). The task is now registered through the Task Scheduler's own interface (`ITaskFolder::RegisterTask`), and nothing is started for it. |
105106
| A logon apply that ends cleanly is no strike | Strikes count logons that may have crashed the machine. An apply that was refused before any write, or failed and reset the card to stock, did not: the strike it recorded up front is taken back. Only a failed reset keeps it. Before 0.3.1 three refused logons read as `disabled after 3 crashes`. |
106107
| An undervolt is a flat top on the curve, found like a core offset | The driver lets a program offset the frequency of each point of the voltage/frequency curve; voltages cannot be written. To run a clock on less voltage, a lower point (the anchor) is raised to that clock and no point above it runs more, so the card has no reason to ask for more voltage. The points below keep the built-in curve, so idle clocks stay as they are. No voltage lock is set: the lock call hard-froze the reference RTX 4070 in the Python version. The write (`apply_flat_top`) uses the units the reference card stores (kHz, one to one; hardware check 61) and estimates nothing. What protects a card that stores them differently is the order: the first round raises a point only half of the way, so a card that moves twice as far lands on the target and not above it, and a card that did not move the anchor by what was written (within one step of its clock grid) is refused before the full step; each later round adds a point's own remaining gap as read back, which also follows a curve that shifts with temperature; no offset is ever written beyond what the plan asks plus two grid steps; and a card that has not arrived after four rounds is refused and cleared. (A first version estimated the units from the read-back; a review showed that estimate could push the anchor far above the target, and it was removed before any write ran on hardware.) A reset to stock zeroes a shape on the curve first and judges it by reading back; a plain core offset, which is the same offset on every point, still goes with the offset reset alone. The search (`find_undervolt`) keeps the clock the card runs under load at stock and moves the anchor one point down at a time. What a candidate asks of the card is its raise, the anchor's frequency above its built-in one, which is a core offset seen at one point; so the core search's rules carry over unchanged: first failure ends the descent, 30 s confirm, the same margin on the raise, 300 s soak, journal entry (`"uv"`) before the write, any failure ends at stock. The core offset and the curve are one table (hardware check 61), so a profile will carry one or the other. First version: `gao --undervolt` only, not saved, not in the window; no write has been run on hardware yet (checks 62 to 65). The curve calls are undocumented NVAPI, as the offsets are; the write procedure and the search are this project's own, with Green Curve (aufkrawall/green-curve, MIT) read as a worked example of what goes wrong on real cards. |
107108
| Dear ImGui on DX11 | One small binary with no runtime, and the D3D11 device is in the process anyway for the stress load. |
108109

109-
**Not yet verified on hardware.** The handling of a driver reset during an optimize run (the rows above on the reconnect, the in-probe `STALLED`, the reset budget, the rest and health check, and the memory-first single-step search) describes what the code does. It is implemented and unit-tested. On hardware only this much has been seen, in round A of [hardware check 51](hardware-checks.md), on a build that reconnected but took no rest: `gao` survived a forced reset, reconnected and left the journal clean, and the desktop was unusable afterwards. Whether the rest keeps the desktop usable is not known.
110+
**Partly verified on hardware.** The handling of a driver reset during an optimize run (the rows above on the reconnect, the in-probe `STALLED`, the reset budget, the rest and health check, and the memory-first single-step search) describes what the code does. It is implemented and unit-tested. Seen on hardware, in [hardware check 84](hardware-checks.md) (c): a Best of my GPU run met a driver reset in a memory candidate, reconnected, rested 20 s, passed the health check, confirmed the memory clock lower and went on to its result, with its window drawing throughout. Not seen on hardware: the in-probe `STALLED`, a run that uses up its reset budget, and a reset in the core climb. Round A of hardware check 51, on a build that reconnected but took no rest, left the desktop unusable after a forced reset.
110111

111112
## Rules the design depends on
112113

‎docs/hardware-checks.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ root; adjust the path for a Debug build.
112112
| 82 | The clock profiles measure stock for five minutes | Elevated, nothing else on the card: start this build's `GpuAutoOptimizer.exe`, choose **Best of my GPU** and run it to the end | yes | the run screen first counts down **Measuring the card at stock** from five minutes, with the fan tile on `(driver)`; the search starts after it with its own 30 s baseline; the result table says **Before (stock, 5 min)**, has no line about a stock run that was too short, and its stock temperature and fan speed are those of a card heated through (on the reference card about 67 C and 72 %, as in checks 76 and 77) | Pass (2026-10-10 22:48, reference RTX 4070, driver 617.42, a test build of 2f5ad7d that starts the run by itself and opens the Optimize page; pictures of the app's own window every 30 to 60 s): at 0:45 the run screen says **Measuring the card at stock**, `4:14 left of 5 min`, fan tile `66 % (driver)`; the log then has `stock: STABLE score=5705 it/s clock=2789 MHz peak=67 C fan=72 % power=195 W`, `baseline: 30 s at stock` and `baseline: STABLE score=5662 it/s`. The run ended after 960 s: `Applied: power 105 %, core +135 MHz, memory +800 MHz`, `Confirmed edges: core +195 MHz, memory +1150 MHz`; the table has **Before (stock, 5 min)** 5705 it/s, 2789 MHz, 10494 MHz, 67 C, 72 %, 195 W and **After (5 min)** 5944 it/s (+4.2 %), 2908 MHz, 11300 MHz, 70 C, 65 %, 202 W, and no line about a stock run that was too short. The heading said `Optimizing: All in one`: the test build starts the run past the page's own choice. The memory search stopped at +1300 MHz this time, where the runs before it went on to +2500 MHz; why was not looked at. The run of 22:23 measured stock the same (67 C, `72 % (driver)`, 198 W at 4:45) and ended at a driver reset: check 81 (c). |
113113
| 83 | The logon task through the Task Scheduler interface, and no install at start | (a) Elevated shell, this build: `gao --boot on`, `gao --status`, `Get-ScheduledTask -TaskPath '\GpuAutoOptimizer\' -TaskName BootApply`, `gao --boot off`, `gao --status`, `gao --boot on`; then look at Windows Security's protection history. (b) Start this build's `GpuAutoOptimizer.exe` without administrator rights, click **Restart as administrator**, wait a minute, then switch **Apply at logon** off and on; look at the protection history again | yes | (a) the task exists with action `C:\Program Files\GpuAutoOptimizer\GpuAutoOptimizer.exe --tray`, the user's own account, highest run level, a logon trigger and no time limit; `boot copy: up to date`; after `--boot off` the task and the copy are gone; nothing in the protection history. (b) the app stays up after restarting itself elevated; switching the logon start off and on removes and installs the copy and the task; nothing in the protection history | (a) Pass (2026-10-10 19:00, reference RTX 4070 PC, Windows 11 26200, Defender engine 1.1.26080.3, definitions 1.459.655.0, run from an elevated PowerShell script): `boot-apply on: ... --tray runs at every logon`; the scheduler shows action `C:\Program Files\GpuAutoOptimizer\GpuAutoOptimizer.exe --tray`, the principal is the user's own account, run level Highest, logon type Interactive, `MSFT_TaskLogonTrigger`, limit `PT0S`, state Ready; `boot copy: up to date`; `--boot off`: `task removed, installed copy removed`, task present False; on again: exit 0; no detection in the 20 s after it. (b) Pass, on one run (2026-10-10 21:44, same PC, engine and definitions): the user reported no Defender message. Read afterwards: the task file and `gao.json` were rewritten and the copy in Program Files was created anew at 21:44:28, which only an elevated process can do; Defender logged no event and no detection between 21:00 and 21:46; at 21:46 the build, the copy and the task were still there and the build's `GpuAutoOptimizer.exe` was running elevated. The logs do not show the elevated copy the app started with its button: the process running at 21:46 had started at 21:44:44, 16 s after the switch, with Explorer as its parent. The user started it that second time, as administrator, after going through the steps. One clean run does not show that the change is what made the difference: the verdict comes from a cloud model. What it was for: at 18:48 the same day the build before this change was started without administrator rights and restarted itself elevated with its button. 1.07 s after the elevated copy started, Defender got a cloud verdict (`FastPath`) `Behavior:Win32/Execution.A!ml` on it, quarantined the build, and a minute later removed the copy it had installed in Program Files and the logon task. In that second the copy had run `schtasks /Query`, copied itself to Program Files and run `schtasks /Create`. Defender's log does not say which step it was. Starts that did only part of it were not detected: the 0.3.1 zip restarting itself elevated on 2026-10-09 (it installed nothing: the logon copy was newer), a build installing itself when started from an elevated PowerShell on 2026-10-08, and test builds that install nothing on 2026-10-10. |
114114
| 84 | NVML across a driver reset: nothing faults, and the app and a run go on | Elevated. A driver reset is made as a search makes one: `gao --set-mem 2800` (2700 when that resets at once), then `gao --stress 20`; each reset is one `nvlddmkm` event 14 in the System log. (a) A standalone probe, not part of the project: one process that calls `nvmlDeviceGetHandleByIndex_v2`, `nvmlDeviceGetClockInfo` (core and memory), `nvmlDeviceGetTemperature`, `nvmlDeviceGetPowerUsage`, `nvmlDeviceGetPowerManagementLimit` and `nvmlDeviceGetFanSpeed` every 100 ms, each under its own guard, with a second thread every second, and once in a loop without a pause; first as it is, then with the rule of this build. (b) This build's `GpuAutoOptimizer.exe --tray` with a saved tune, and three resets; watch the process, `boot.log` and `gao --status`, then open the window. (c) A Best of my GPU run that reaches a reset at the memory edge: a test build whose memory climb starts at +2750 MHz. (d) A Best of my GPU run of this build with nothing done to it | yes | (a) as it is: the readings fault after a reset; with the rule: none does, and the readings come back. (b) the same process goes on, `boot.log` has the watchdog's line for every reset and no line about a fault, the tune is applied again and the window shows readings. (c) the run says the driver was reset, reconnects, goes on and ends at its result. (d) the run ends at its result | Pass (2026-10-11, reference RTX 4070, driver 617.42, `nvml.dll` 8.17.16.1742). (a) As it is, 4 resets in 2 processes: from each process's first reset on, both clock calls, the temperature and the power usage faulted at every call until the process ended (1,319 and 1,177 times each), always a read of the same block of memory at `nvml.dll+0xf3ea0`; the power limit and the fan speed answered 999; the handle call went on answering 0; `nvmlShutdown` at the end did not return within 5 s. The faulting instruction reads memory the driver shares with the process, in a retry loop on a sequence number. With the power limit alone asked first: 3 resets at 100 ms, no fault, 3 new sessions; in the loop without a pause 1 fault in 2 resets (power limit 0, fan speed 999, then the readings faulted, all in one round). With both asked first: 4 resets in the loop without a pause, 800,000 rounds, no fault; the new session took between 15 ms and 8 s to start, and once longer than 8 s. The fan speed call sometimes answers 27 (not ready) for seconds before a reset; in the passes before this one the readings still worked then. In this last pass the probe read nothing in such a round either, which this build does not do: that happened before one of its four resets. A reset forced through `D3DKMT_ESCAPE_TDRDBGCTRL` is refused on this Windows (`0xC00000BB`), and these resets are not logged as Display 4101. (b) Three resets at +2800: the same process throughout; `boot.log` has `watchdog: the tune had been reset (driver reset or TDR); re-applied` three times and nothing else; `applied:` is the saved tune after each; the window afterwards shows 660 MHz, 5001 MHz, 35 C, 32 of 210 W and `0 % (driver)`; the app exits when asked. (c) The stock measurement and the baseline as in check 82; at 420 s the driver reset in the candidate at +2800 (journal: `DEVICE LOST`); the log: `mem: the driver reset at +2800; using +2750, the last value that passed`, `the driver was reset; reconnecting`, `resting 20 s before the card is loaded again`, `checking that the card is back: 5 s at stock`, `health: STABLE`; then +2550 confirmed, the core climbed to +195, and the run ended after 861 s with `best: power 105 %, core +135 MHz, mem +1750 MHz` saved and applied; nothing new in `boot.log`. This is the reset the run of 2026-10-10 22:23 ended on (check 81 (c)). The result was not kept: memory was not searched by bandwidth in this test build. A run of the same build that started at +2400 met no reset: +2600 was a wrong result, and so were the three confirm tries below it. (d) 926 s, no reset: `best: power 105 %, core +135 MHz, mem +650 MHz`, nothing new in `boot.log`. Not covered: another card or driver; a card that goes between the two calls and the readings, a matter of microseconds, after which the fault is caught as in check 80. During a reset the thread that starts the new session waits for the library, the window's thread as well; before this build a reading waited there as long. |
115+
| 85 | The installed app is in the Start menu, and an update of the copy in Program Files asks for administrator rights | With a test build of this branch numbered 0.4.0, so that the update to 0.4.1 is on offer. (a) Elevated: `gao --boot on` and look in the Start menu's Programs folder for all users; delete the entry by hand and start the installed `GpuAutoOptimizer.exe --resume --hidden`, as the copy after an update starts; `gao --boot off`; `gao --boot on` again. (b) Not elevated: open Start, type `gpu auto`, start the app; click **Update available** | (a) yes, (b) no | (a) `GPU Auto Optimizer.lnk` is there and points at the installed `GpuAutoOptimizer.exe`; the copy started with `--resume` makes it again; after `--boot off` it is gone. (b) Windows' search finds the app; its window opens without administrator rights; the click asks for them, and the elevated copy downloads the update, installs it and starts as the new version | Pass (2026-10-11, reference RTX 4070 PC, Windows 11 26200). (a) 10:27, from an elevated script: before, no entry; after `--boot on`, `entry -> C:\Program Files\GpuAutoOptimizer\GpuAutoOptimizer.exe`; removed by hand and there again 8 s after the start with `--resume`; after `--boot off`: `task removed, installed copy removed`, no entry; after `--boot on` again the entry is back. No Defender detection in the 15 s after it. (b) The user went through the three steps and ticked them off without a remark. Found afterwards: both executables in Program Files are byte for byte those of the v0.4.1 release zip, the app runs elevated since 10:43:37, the Start menu entry is there, and Defender logged nothing since 10:20. The prompt and the window themselves were seen by the user only. What it is for: earlier that morning 0.4.0, started from Program Files without administrator rights, failed its update with `could not copy ...\GpuAutoOptimizer-update\files\gao.exe to C:\Program Files\GpuAutoOptimizer (error 5)`, and Windows' search found no app: the only entry on this PC pointed at a predecessor of this program. The copy this leaves installed is the released 0.4.1, which has neither change. |
115116

116117
## Notes
117118

0 commit comments

Comments
 (0)