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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ What else is in it:

- **Five more profiles** for when you want one thing only: Best of my GPU, Cool & efficient, Quiet, Max performance and Undervolt.
- **You see what it does:** during a run the window draws the card's voltage/frequency curve as the run writes it, over the card's own, says which step the run is at, and counts every long one down.
- **You keep seeing what it gave:** the dashboard shows the saved tune against the card at stock, as measured when the tune was made: speed, power, fan speed under load and peak temperature.
- **Fan curves:** the measured one, Silent, Normal, Cool or Aggressive, or your own; fans stop at idle without cycling on and off, the app learns the lowest speed your fans can actually hold, and the fans go back to the driver on exit, sleep, logoff and crashes.
- **Native and small:** two C++ programs, under 2 MB together, with no installer, no runtime and no driver or service.

Expand Down Expand Up @@ -204,6 +205,7 @@ flowchart LR
| `gao --probe` | Live telemetry: clocks, temperature, fan, power, clock-offset ranges |
| `gao --curve` | Prints the voltage/frequency curve as the driver reports it; changes nothing |
| `gao --stress <seconds>` | Runs the stress test alone and prints its verdict; changes nothing |
| `gao --stress <seconds> --duty <pct>` | A light load instead: the card works that share of the time and idles in between, for checking the fans' idle rule; changes nothing |
| `gao --bandwidth [--after-load <sec>]` | Measures the current memory bandwidth and shows every run of the measurement; with `--after-load`, right after that many seconds of the stress load, as a run measures it |
| `gao --busy` | Shows the other programs that use the card right now, as a run looks for them before it starts; changes nothing |
| `gao --update` | Installs the latest release into the folder `gao.exe` is in, if there is a newer one |
Expand Down Expand Up @@ -252,7 +254,7 @@ Yes. The edge of a card is found by crossing it. A probe that computes a wrong v

A search may reset the driver once: the screen goes black for a moment. The run then reconnects, sets the card to stock, rests for 20 seconds, checks that the card computes normally again, and stops exploring that clock: it tries no new values for it, and confirms a little below the last value that passed. The memory search and the core search each get one, so a run can reset the driver twice. One more ends the run at stock with nothing saved, and so does a card that does not come back. Nothing about a reset is remembered, so the next run on such a card crosses the edge again. Results on real cards are in [docs/hardware-checks.md](docs/hardware-checks.md), checks 51 and 52.

A driver reset also reaches the other programs on the desktop. In a test during development, with an earlier build that put the load back on the card right after a forced reset, no application window could be opened or seen afterwards and one application crashed; the user had to sign out and in. The rest after a reset was added since; whether it prevents this has not been tested. Save your work before a run.
A driver reset also reaches the other programs on the desktop. On an RTX 5070, one run with the app's window open left a desktop where only that window was drawn, and one other program crashed; the owner had to sign out and in. A second run, with the window closed, left the desktop usable, and the window is now drawn by the processor instead of the graphics card. Whether that is enough on every card is not known. When it happens, the app says so: sign out and in, or restart the computer. Save your work before a run.

If the machine freezes instead, the crash journal makes sure the next run stays below that setting.
</details>
Expand Down
22 changes: 11 additions & 11 deletions docs/hardware-checks.md

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion src/app/cli_commands.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ template <class Run> int putting_back(Run run) {
const int code = run();
if (g_abort && code == 1) {
gao::app::OptimizeHooks hooks;
hooks.log = gao::app::logged([](const std::string& msg) { std::printf(" %s\n", msg.c_str()); });
// The end of the run that was stopped: no header line of its own.
hooks.log = gao::app::logged([](const std::string& msg) { std::printf(" %s\n", msg.c_str()); }, false);
gao::app::put_back_after_stop(before, hooks);
}
return code;
Expand Down
9 changes: 5 additions & 4 deletions src/app/common.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ void boot_log(const std::string& msg) {

bool prepare_state(std::string* why) { return ensure_app_dir(why); }

std::function<void(const std::string&)> logged_to(const std::filesystem::path& file, std::function<void(const std::string&)> show) {
std::function<void(const std::string&)> logged_to(const std::filesystem::path& file, std::function<void(const std::string&)> show,
bool header) {
std::error_code ec;
if (std::filesystem::file_size(file, ec) > kRunLogMaxBytes && !ec) {
std::filesystem::path old = file;
Expand All @@ -95,19 +96,19 @@ std::function<void(const std::string&)> logged_to(const std::filesystem::path& f
std::snprintf(stamp, sizeof(stamp), "%04d-%02d-%02d %02d:%02d:%02d ", t.wYear, t.wMonth, t.wDay, t.wHour, t.wMinute, t.wSecond);
append_line_durable(file, stamp + line);
};
write("=== " + std::string(kProductName) + " " + std::string(kVersion));
if (header) write("=== " + std::string(kProductName) + " " + std::string(kVersion));
return [write, show = std::move(show)](const std::string& line) {
write(line); // on disk before anything acts on it
if (show) show(line);
};
}

std::function<void(const std::string&)> logged(std::function<void(const std::string&)> show) {
std::function<void(const std::string&)> logged(std::function<void(const std::string&)> show, bool header) {
// Only into a folder that was verified: an elevated process does not
// append to a file somebody else may have put there.
std::string why;
if (!is_elevated() || run_log_path().empty() || !prepare_state(&why)) return show;
return logged_to(run_log_path(), std::move(show));
return logged_to(run_log_path(), std::move(show), header);
}

std::string profile_text(const Profile& p) {
Expand Down
7 changes: 5 additions & 2 deletions src/app/common.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,14 @@ void boot_log(const std::string& msg);
// that freezes in a run takes the window and its log with it; this is what is
// left. Elevated only (the folder is the administrators'); otherwise `show`
// is returned as it is.
std::function<void(const std::string&)> logged(std::function<void(const std::string&)> show);
// header: the file gets a `=== <product> <version>` line first, which marks the
// start of a run; false for lines that belong to the run before it.
std::function<void(const std::string&)> logged(std::function<void(const std::string&)> show, bool header = true);
// The same, to a file of the caller's choosing. A file over kRunLogMaxBytes
// is moved aside first (one older file is kept, as <file>.old).
inline constexpr std::uintmax_t kRunLogMaxBytes = 1024 * 1024;
std::function<void(const std::string&)> logged_to(const std::filesystem::path& file, std::function<void(const std::string&)> show);
std::function<void(const std::string&)> logged_to(const std::filesystem::path& file, std::function<void(const std::string&)> show,
bool header = true);
// Creates, or verifies and re-secures, the admin-only state folder. Every
// elevated command that reads or writes state calls this first.
bool prepare_state(std::string* why);
Expand Down
8 changes: 8 additions & 0 deletions src/app/common_run_steps.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,14 @@ StockOutcome measure_stock(const OptimizeHooks& hooks) {
auto load = std::make_unique<Stress>();
if (!load->Init()) return fail("stress init failed: " + load->Error());
const GpuControl& gpu = hw.control();
// The emergency handlers for the whole measurement, as during the search:
// without them Ctrl+C on the command line ends the process here, and the
// tune the run found on the card is not put back.
if (hooks.active_gpu) hooks.active_gpu(&gpu);
struct Unregister {
const OptimizeHooks& h;
~Unregister() { if (h.active_gpu) h.active_gpu(nullptr); }
} unregister{hooks};
if (!gpu.reset_to_stock || !gpu.reset_to_stock()) return fail("could not set the card to stock");
const KeepAwake keep_awake;
auto log = [&](const std::string& m) { if (hooks.log) hooks.log(m); };
Expand Down
71 changes: 71 additions & 0 deletions tests/test_config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -279,3 +279,74 @@ TEST_CASE("a measurement against stock that cannot be read is absent, and the pr
CHECK_FALSE(c.profile->against_stock);
}
}

// Who updates from 0.4.2 (the latest release before 0.4.8) keeps the gao.json
// that version wrote: no temperature ceiling and no measurement against stock
// in the profile. The tune must load whole and apply at logon as before.
TEST_CASE("a gao.json written by 0.4.2 keeps its tune and applies at logon") {
// As 0.4.2 saved it on the reference card (keys sorted, as its JSON writer does).
const Config best = from_json(R"({
"boot_strikes": 0,
"fan_control": true,
"fan_min_gpu": "GPU-a41e21d6",
"fan_min_pct": 52,
"profile": {
"core_mhz": 135,
"driver": "617.42",
"fan_curve": {"points": [[50, 35], [60, 45], [70, 65], [75, 100]], "stop_below_c": 50},
"gpu": "GPU-a41e21d6",
"mem_mhz": 650,
"power_pct": 105,
"preset": "best",
"saved_at": "2026-10-11 09:24"
},
"update_check": true
})");
REQUIRE(best.profile);
CHECK(best.profile->preset == Preset::BestOfMyGpu);
CHECK(best.profile->power_pct == 105);
CHECK(best.profile->core_mhz == 135);
CHECK(best.profile->mem_mhz == 650);
CHECK_FALSE(best.profile->undervolt);
CHECK_FALSE(best.profile->temp_ceiling_c);
CHECK_FALSE(best.profile->against_stock);
CHECK(best.fan_control);
CHECK(best.update_check);
CHECK(best.fan_min_pct == 52);
CHECK(decide_boot(best, "617.42", "GPU-a41e21d6") == BootDecision::Apply);
REQUIRE(active_fan_curve(best));
CHECK(*active_fan_curve(best) == *best.profile->fan_curve);
CHECK(profile_ceiling(*best.profile) == 75);

// An All in one tune of 0.4.2: a flat top over a core offset.
const Config all = from_json(R"({
"boot_strikes": 1,
"fan_control": true,
"profile": {
"core_mhz": 135,
"driver": "617.42",
"fan_curve": {"points": [[50, 52], [70, 52], [75, 100]], "stop_below_c": 50},
"gpu": "GPU-a41e21d6",
"mem_mhz": 1400,
"power_pct": 105,
"preset": "all",
"saved_at": "2026-10-11 13:09",
"undervolt": {"freq_khz": 2850000, "raise_khz": 165000, "volt_uv": 1025000}
},
"update_check": false
})");
REQUIRE(all.profile);
CHECK(all.profile->preset == Preset::AllInOne);
REQUIRE(all.profile->undervolt);
CHECK(all.profile->undervolt->volt_uv == 1025000);
CHECK(all.profile->undervolt->freq_khz == 2850000);
CHECK(all.profile->undervolt->raise_khz == 165000);
CHECK(all.boot_strikes == 1);
CHECK_FALSE(all.update_check);
CHECK(decide_boot(all, "617.42", "GPU-a41e21d6") == BootDecision::Apply);
// Saved again by this version, it still reads back the same tune.
const Config again = from_json(to_json(all));
REQUIRE(again.profile);
CHECK(again.profile->undervolt->volt_uv == 1025000);
CHECK(again.profile->mem_mhz == 1400);
}
11 changes: 11 additions & 0 deletions tests/test_run_log.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,17 @@ TEST_CASE("a second run adds to the log of the first") {
CHECK(lines->size() == 4); // two headers, two lines
}

TEST_CASE("what a stopped run does afterwards is part of that run in the log, under no header of its own") {
const TempDir dir;
const auto file = dir.path / "run.log";
gao::app::logged_to(file, {})("stock: ABORTED");
gao::app::logged_to(file, {}, false)("stopped: the tune that was on the card before the run is back");
const auto lines = gao::read_lines(file);
REQUIRE(lines);
REQUIRE(lines->size() == 3); // one header, two lines
CHECK((*lines)[2].find("stopped: the tune") != std::string::npos);
}

TEST_CASE("a run log that has grown large is moved aside, and one older file is kept") {
const TempDir dir;
const auto file = dir.path / "run.log";
Expand Down