diff --git a/README.md b/README.md index b08be8b..1912058 100644 --- a/README.md +++ b/README.md @@ -42,9 +42,9 @@ That builds two programs in `.\build\Release\`: `GpuAutoOptimizer.exe`, the wind ### 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**. +Start `GpuAutoOptimizer.exe`. The **Dashboard** shows the GPU with live telemetry, what is applied right now next to the saved tune (with **Apply saved tune**, the **Apply at logon** switch and **Revert to stock**), the four profiles and a log of what happened. On the **Optimize** page, pick a profile and press **Optimize GPU** (it offers to restart as administrator first, because tuning writes clocks and power limits). The run shows temperature, power and every probe as it happens; **Abort** restores stock. The results compare 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 (window, tray menu or `gao --apply`). Only one optimize runs at a time, from the window or the command line. +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. Closing the window keeps the app in the tray; **Exit** in the tray menu 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 (window, tray menu or `gao --apply`). Only one optimize runs at a time, from the window or the command line. ### The command line diff --git a/docs/hardware-checks.md b/docs/hardware-checks.md index b4e2887..fedd76e 100644 --- a/docs/hardware-checks.md +++ b/docs/hardware-checks.md @@ -60,7 +60,7 @@ root; adjust the path for a Debug build. | 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); after `gao --apply` the tray keeps the tune applied again; the second optimize refuses with `another optimize is already running` | Pass for reset and apply (2026-09-27): after `gao --reset` the card stayed at stock for 45 s with the tray running (no watchdog line); `gao --apply` applied the tune again. Two optimizes at once not yet tried. | -| 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. | +| 37 | Closing the window | close the window, then choose Exit in the tray menu | no | closing hides the window to the tray (a balloon says so the first time); Exit quits | | ## Notes diff --git a/src/app/gui/main_gui.cpp b/src/app/gui/main_gui.cpp index eab0db5..52ea7aa 100644 --- a/src/app/gui/main_gui.cpp +++ b/src/app/gui/main_gui.cpp @@ -80,6 +80,7 @@ struct App { bool watch = false; // keep the saved tune applied (off after a revert or a reset by choice) bool worker_was_running = false; bool strike_pending = false; // a logon apply whose 2-minute grace has not passed yet + bool told_about_tray = false; // the "still running in the tray" balloon, once per session // Crash dumps are written by a thread created up front (a crashing thread // may have no stack or heap left to do it itself). @@ -163,17 +164,6 @@ void recover_device() { if (!g.device_lost) ImGui_ImplDX11_Init(g.device.Get(), g.ctx.Get()); } -void apply_dpi(float scale) { - ImGuiStyle& style = ImGui::GetStyle(); - style = ImGuiStyle(); - ImGui::StyleColorsDark(); - style.WindowRounding = 0; - style.FrameRounding = 4; - style.ChildRounding = 6; - style.ScaleAllSizes(scale); - style.FontScaleDpi = scale; -} - // ---------------------------------------------------------------- tray std::wstring widen(const std::string& s) { @@ -211,8 +201,16 @@ void tray_icon(DWORD message, const wchar_t* tip = nullptr, const wchar_t* ballo } } -void notify(const std::string& text) { - g.ui.watchdog_note = gao::app::now_text() + " " + text; +// A line in the window's log for this session. +void note(const std::string& text, bool warn = false) { + g.ui.notes.push_back({gao::app::now_text(), text, warn}); + if (g.ui.notes.size() > 100) g.ui.notes.erase(g.ui.notes.begin()); +} + +// A tray balloon. Pass log=false when the event is already in boot.log, +// which the window's log shows too. +void notify(const std::string& text, bool log = true) { + if (log) note(text, true); tray_icon(NIM_MODIFY, nullptr, widen(text).c_str()); } @@ -256,14 +254,16 @@ void refresh_status(bool with_task) { } if (g.gpu.read_applied) g.ui.applied = g.gpu.read_applied(); const auto log = gao::read_lines(gao::boot_log_path()); - g.ui.last_boot = log && !log->empty() ? log->back() : std::string(); + g.ui.boot_log.clear(); + if (log) g.ui.boot_log.assign(log->size() > 200 ? log->end() - 200 : log->begin(), log->end()); if (with_task) g.ui.boot_on = gao::boot_task_exists(); } // ---------------------------------------------------------------- actions +void hw_lost(); // below, with the other hardware helpers + void act_optimize(gao::Preset preset) { - g.ui.message.clear(); g.worker->start(preset); // the watchdog skips while it runs } @@ -285,48 +285,49 @@ void act_restart_elevated() { g.exit_requested = true; } else { g.instance_mutex = CreateMutexW(nullptr, TRUE, kInstanceMutex); - g.ui.message = "Elevation was cancelled."; + note("Elevation was cancelled."); } } bool refuse_while_tuning() { if (g.worker->running()) return true; if (!gao::app::tuning_in_progress()) return false; - g.ui.message = "An optimize is running on the command line; wait for it to finish."; + note("An optimize is running on the command line; wait for it to finish.", true); return true; } void act_apply() { if (refuse_while_tuning()) return; - if (!g.nvml_ok || !g.nvapi_ok) { g.ui.message = "The NVIDIA driver is not available right now."; return; } + if (!g.nvml_ok || !g.nvapi_ok) { note("The NVIDIA driver is not available right now.", true); return; } std::string why; - if (!gao::app::prepare_state(&why)) { g.ui.message = why; return; } + if (!gao::app::prepare_state(&why)) { note(why, true); return; } gao::Config cfg = gao::app::load_config(); cfg.boot_strikes = 0; // strikes only gate the logon apply const std::string driver = g.nvml_ok ? g.nvml->DriverVersion() : std::string(); const auto d = gao::decide_boot(cfg, driver, g.nvml_ok ? g.nvml->GpuUuid(kGpu) : std::string()); - if (d != gao::BootDecision::Apply) { g.ui.message = "Not applied: " + gao::app::decision_text(d, cfg, driver); return; } - if (!gao::apply_profile(g.gpu, *cfg.profile, &why)) { g.ui.message = "Not applied: " + why; return; } + if (d != gao::BootDecision::Apply) { note("Not applied: " + gao::app::decision_text(d, cfg, driver), true); return; } + if (!gao::apply_profile(g.gpu, *cfg.profile, &why)) { note("Not applied: " + why, true); return; } g.watch = true; g.watchdog = gao::Watchdog(); - g.ui.message = "Applied " + gao::app::profile_text(*cfg.profile); + note("Applied " + gao::app::profile_text(*cfg.profile)); refresh_status(false); } void act_revert() { if (refuse_while_tuning()) return; - if (!g.nvml_ok || !g.nvapi_ok) { g.ui.message = "The NVIDIA driver is not available right now."; return; } + if (!g.nvml_ok || !g.nvapi_ok) { note("The NVIDIA driver is not available right now.", true); return; } const bool ok = g.gpu.reset_to_stock && g.gpu.reset_to_stock(); g.watch = false; // stock by choice: the watchdog must not undo it - g.ui.message = ok ? "Back at stock. The saved tune is not re-applied until you apply it again." - : "Reset to stock FAILED; try gao --reset."; + note(ok ? "Back at stock. The saved tune is not re-applied until you apply it again." + : "Reset to stock FAILED; try gao --reset.", + !ok); refresh_status(false); } void act_boot(bool on) { std::string message; - on ? gao::app::enable_boot(&message) : gao::app::disable_boot(&message); - g.ui.message = message; + const bool ok = on ? gao::app::enable_boot(&message) : gao::app::disable_boot(&message); + note(message, !ok); refresh_status(true); } @@ -341,6 +342,11 @@ void render() { act.apply_profile = act_apply; act.revert_to_stock = act_revert; act.set_boot = act_boot; + act.detect_gpu = [] { // e.g. after a driver update: re-create NVML and NVAPI now + if (refuse_while_tuning()) return; + hw_lost(); + g.hw_retry_at = 0; + }; gao::gui::draw_ui(g.ui, g.worker->snapshot(), act); ImGui::Render(); const float clear[4] = {0.08f, 0.08f, 0.10f, 1.0f}; @@ -460,21 +466,22 @@ void on_watchdog() { std::string why; if (gao::apply_profile(g.gpu, *cfg.profile, &why)) { gao::app::boot_log("watchdog: the tune had been reset (driver reset or TDR); re-applied"); - notify("The tune had been reset (driver reset or TDR) and was re-applied."); + notify("The tune had been reset (driver reset or TDR) and was re-applied.", false); } else { gao::app::boot_log("watchdog: re-apply failed: " + why); - notify("The tune had been reset and could not be re-applied: " + why); + notify("The tune had been reset and could not be re-applied: " + why, false); } break; } case gao::WatchAction::GiveUpUnstable: gao::app::boot_log("watchdog: reset 4 times within an hour; stopped re-applying"); notify("The tune keeps getting reset (4 times within an hour), which usually means it is not stable. " - "Stopped re-applying it; optimize again."); + "Stopped re-applying it; optimize again.", + false); break; case gao::WatchAction::BackOffForeign: gao::app::boot_log("watchdog: another program changed the GPU settings; leaving them alone"); - notify("Another program (Afterburner, NVIDIA App...) changed the GPU settings. Leaving them alone."); + notify("Another program (Afterburner, NVIDIA App...) changed the GPU settings. Leaving them alone.", false); break; case gao::WatchAction::NotifyDriverChanged: notify("The NVIDIA driver or the card changed since the tune was made. Optimize again to tune for it."); @@ -504,10 +511,10 @@ LRESULT CALLBACK wnd_proc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { if (wp == static_cast(gao::app::TrayNotice::TuneApplied)) { g.watch = true; g.watchdog = gao::Watchdog(); - g.ui.watchdog_note = gao::app::now_text() + " Applied from the command line; kept applied from now on."; + note("Applied from the command line; kept applied from now on."); } else { g.watch = false; - g.ui.watchdog_note = gao::app::now_text() + " Set to stock from the command line; not re-applied until you apply it again."; + note("Set to stock from the command line; not re-applied until you apply it again.", true); } refresh_status(false); return 0; @@ -527,19 +534,19 @@ LRESULT CALLBACK wnd_proc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { } return 0; case WM_DPICHANGED: { - apply_dpi(HIWORD(wp) / 96.0f); + gao::gui::apply_style(HIWORD(wp) / 96.0f); const RECT* r = reinterpret_cast(lp); SetWindowPos(hwnd, nullptr, r->left, r->top, r->right - r->left, r->bottom - r->top, SWP_NOZORDER | SWP_NOACTIVATE); return 0; } case WM_CLOSE: - // With a tune to keep applied, or a run in progress, the window - // hides and the tray stays. Otherwise there is nothing to stay for. - if (g.watch || g.worker->running()) { - ShowWindow(hwnd, SW_HIDE); - g.visible = false; - } else { - g.exit_requested = true; + // Closing hides to the tray, like other tray apps; Exit in the tray + // menu quits. Say so the first time, or it looks like a quit. + ShowWindow(hwnd, SW_HIDE); + g.visible = false; + if (!g.told_about_tray) { + g.told_about_tray = true; + tray_icon(NIM_MODIFY, nullptr, L"Still running in the tray. Right-click the icon and choose Exit to quit."); } return 0; case WM_TIMER: @@ -627,11 +634,11 @@ int WINAPI wWinMain(HINSTANCE inst, HINSTANCE, PWSTR cmdline, int) { wc.hCursor = LoadCursorW(nullptr, MAKEINTRESOURCEW(32512)); // IDC_ARROW wc.lpszClassName = kWindowClass; RegisterClassExW(&wc); - g.hwnd = CreateWindowW(kWindowClass, L"GPU Auto Optimizer", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 920, 640, + g.hwnd = CreateWindowW(kWindowClass, L"GPU Auto Optimizer", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 1180, 860, nullptr, nullptr, inst, nullptr); if (!g.hwnd || !create_device()) return 1; const float scale = GetDpiForWindow(g.hwnd) / 96.0f; // the monitor the window actually opened on - SetWindowPos(g.hwnd, nullptr, 0, 0, static_cast(920 * scale), static_cast(640 * scale), + SetWindowPos(g.hwnd, nullptr, 0, 0, static_cast(1180 * scale), static_cast(860 * scale), SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE); const BOOL dark = TRUE; // a dark title bar to match the dark window DwmSetWindowAttribute(g.hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, &dark, sizeof(dark)); @@ -651,9 +658,8 @@ int WINAPI wWinMain(HINSTANCE inst, HINSTANCE, PWSTR cmdline, int) { io.IniFilename = nullptr; // nothing to persist; the layout is fixed wchar_t windows[MAX_PATH]; const UINT wn = GetWindowsDirectoryW(windows, MAX_PATH); - const std::string font = wn && wn < MAX_PATH ? (std::filesystem::path(windows) / L"Fonts" / L"segoeui.ttf").string() : ""; - if (font.empty() || !io.Fonts->AddFontFromFileTTF(font.c_str(), 17.0f)) io.Fonts->AddFontDefault(); - apply_dpi(scale); + gao::gui::load_fonts(wn && wn < MAX_PATH ? (std::filesystem::path(windows) / L"Fonts").string() : std::string()); + gao::gui::apply_style(scale); ImGui_ImplWin32_Init(g.hwnd); ImGui_ImplDX11_Init(g.device.Get(), g.ctx.Get()); @@ -672,7 +678,7 @@ int WINAPI wWinMain(HINSTANCE inst, HINSTANCE, PWSTR cmdline, int) { g.strike_pending = true; SetTimer(g.hwnd, kTimerStrike, 2 * 60 * 1000, nullptr); } else if (logon.decision != gao::BootDecision::NoProfile) { - notify(logon.message); + notify(logon.message, false); // apply_at_logon wrote boot.log } } refresh_status(false); diff --git a/src/app/gui/ui.cpp b/src/app/gui/ui.cpp index 4c9810d..9a20af9 100644 --- a/src/app/gui/ui.cpp +++ b/src/app/gui/ui.cpp @@ -1,5 +1,7 @@ #include "app/gui/ui.hpp" #include "core/boot.hpp" +#include "core/objectives.hpp" +#include "core/version.hpp" #include "imgui.h" #include #include @@ -10,196 +12,631 @@ namespace gao::gui { namespace { constexpr size_t kHistory = 60; -const ImVec4 kGood{0.45f, 0.85f, 0.45f, 1.0f}; -const ImVec4 kWarn{0.95f, 0.75f, 0.30f, 1.0f}; -const ImVec4 kBad{0.95f, 0.40f, 0.40f, 1.0f}; -const ImVec4 kDim{0.60f, 0.60f, 0.65f, 1.0f}; +constexpr size_t kLogShort = 6; // log lines on the dashboard before "View full log" + +ImFont* g_regular = nullptr; +ImFont* g_bold = nullptr; +bool g_full_log = false; + +const ImVec4 kBg{0.067f, 0.075f, 0.090f, 1}; +const ImVec4 kNav{0.050f, 0.057f, 0.069f, 1}; +const ImVec4 kCard{0.090f, 0.102f, 0.122f, 1}; +const ImVec4 kInner{0.110f, 0.125f, 0.149f, 1}; +const ImVec4 kInnerHover{0.140f, 0.158f, 0.188f, 1}; +const ImVec4 kBorder{0.170f, 0.192f, 0.227f, 1}; +const ImVec4 kAccent{0.180f, 0.435f, 0.835f, 1}; +const ImVec4 kAccentHover{0.231f, 0.502f, 0.906f, 1}; +const ImVec4 kAccentBright{0.290f, 0.580f, 1.000f, 1}; +const ImVec4 kSelectedBg{0.098f, 0.145f, 0.220f, 1}; +const ImVec4 kText{0.900f, 0.910f, 0.930f, 1}; +const ImVec4 kDim{0.600f, 0.640f, 0.690f, 1}; +const ImVec4 kGood{0.360f, 0.780f, 0.500f, 1}; +const ImVec4 kWarn{0.910f, 0.710f, 0.230f, 1}; +const ImVec4 kBad{0.930f, 0.400f, 0.400f, 1}; + +// Segoe Fluent Icons (Windows 11) / Segoe MDL2 Assets (Windows 10) code points. +enum Icon : unsigned { + kIconHome = 0xE80F, kIconPulse = 0xE9D9, kIconInfo = 0xE946, kIconChip = 0xEEA1, kIconMemory = 0xE964, + kIconTemp = 0xE9CA, kIconPower = 0xE945, kIconFan = 0xF16A, kIconRefresh = 0xE72C, kIconUndo = 0xE777, + kIconPlay = 0xF5B0, kIconStop = 0xE71A, kIconClock = 0xE823, kIconStar = 0xE734, kIconQuiet = 0xE992, + kIconLeaf = 0xEC0A, kIconGauge = 0xEC4A, kIconShield = 0xEA18, kIconList = 0xE8FD, kIconBack = 0xE72B, +}; + +// UTF-8 for a code point in the Basic Multilingual Plane's private use area. +std::string ic(unsigned cp) { + const char s[4] = {static_cast(0xE0 | (cp >> 12)), static_cast(0x80 | ((cp >> 6) & 0x3F)), + static_cast(0x80 | (cp & 0x3F)), 0}; + return s; +} +std::string with_icon(unsigned cp, const char* text) { return ic(cp) + " " + text; } + +float em() { return ImGui::GetFontSize(); } +float base() { return ImGui::GetStyle().FontSizeBase; } struct PresetInfo { Preset preset; + Icon icon; const char* title; const char* summary; }; constexpr PresetInfo kPresets[] = { - {Preset::BestOfMyGpu, "Best of my GPU", - "Highest confirmed clocks with a safety margin and the most power the card allows, up to 75 C."}, - {Preset::Quiet, "Quiet", - "Milder clocks and the lowest power limit that costs under 2 % speed, up to 80 C."}, - {Preset::CoolAndEfficient, "Cool & efficient", - "Power only: the lowest power limit that costs under 2 % speed, clocks left stock, up to 65 C."}, - {Preset::MaxPerformance, "Max performance", - "Highest confirmed clocks minus one step and the highest power limit, up to 83 C."}, + {Preset::BestOfMyGpu, kIconStar, "Best of my GPU", + "Highest confirmed clocks with a safety margin and the most power the card allows."}, + {Preset::CoolAndEfficient, kIconLeaf, "Cool & efficient", + "Power only: the lowest power limit that costs under 2 % speed, clocks left stock."}, + {Preset::Quiet, kIconQuiet, "Quiet", "Milder clocks and the lowest power limit that costs under 2 % speed."}, + {Preset::MaxPerformance, kIconGauge, "Max performance", + "Highest confirmed clocks minus one step and the highest power limit."}, }; +const char* title_of(Preset preset) { + for (const PresetInfo& p : kPresets) + if (p.preset == preset) return p.title; + return preset_name(preset); +} + std::string field(int v, const char* unit) { if (v < 0) return "n/a"; return std::to_string(v) + unit; } +std::string offset(int mhz) { return (mhz >= 0 ? "+" : "") + std::to_string(mhz) + " MHz"; } -void plot(const char* label, const std::deque& h, float lo, float hi, const char* overlay) { - std::vector v(h.begin(), h.end()); - if (v.empty()) v.push_back(0); - ImGui::PlotLines(label, v.data(), static_cast(v.size()), 0, overlay, lo, hi, ImVec2(-1, 70)); +// ------------------------------------------------------------------ widgets + +void text_bold(const char* text, float scale = 1.0f) { + ImGui::PushFont(g_bold, base() * scale); + ImGui::TextUnformatted(text); + ImGui::PopFont(); +} +void heading(const char* text) { text_bold(text, 1.2f); } +void dim(const char* text) { + ImGui::PushStyleColor(ImGuiCol_Text, kDim); + ImGui::TextUnformatted(text); + ImGui::PopStyleColor(); +} +void wrapped(const ImVec4& color, const std::string& text) { + ImGui::PushStyleColor(ImGuiCol_Text, color); + ImGui::PushTextWrapPos(0); + ImGui::TextUnformatted(text.c_str()); + ImGui::PopTextWrapPos(); + ImGui::PopStyleColor(); } -void telemetry_row(const UiState& s) { - const Telemetry& t = s.telemetry; - ImGui::Text("Core %s Memory %s Temperature %s Power %s / %d W Fan %s", field(t.core_mhz, " MHz").c_str(), - field(t.mem_mhz, " MHz").c_str(), field(t.temp_c, " C").c_str(), field(t.power_w, "").c_str(), - t.power_limit_w, field(t.fan_pct, " %").c_str()); +// A rounded panel. height 0: as tall as its contents. Always pair with end_card(). +void begin_card(const char* id, float height = 0, const ImVec4& bg = kCard, float width = 0) { + ImGui::PushStyleColor(ImGuiCol_ChildBg, bg); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(em() * 0.9f, em() * 0.75f)); + ImGui::BeginChild(id, ImVec2(width, height), + ImGuiChildFlags_Borders | ImGuiChildFlags_AlwaysUseWindowPadding | + (height == 0 ? ImGuiChildFlags_AutoResizeY : ImGuiChildFlags_None), + ImGuiWindowFlags_NoScrollbar); + ImGui::PopStyleVar(); + ImGui::PopStyleColor(); } +void end_card() { ImGui::EndChild(); } -void status_block(const UiState& s, const UiActions& act) { - ImGui::SeparatorText("Saved tune"); - if (!s.profile) { - ImGui::TextColored(kDim, "None yet. Pick a preset and press Optimize."); - } else { - const Profile& p = *s.profile; - ImGui::Text("%s: power %d %%, core +%d MHz, memory +%d MHz (saved %s)", preset_name(p.preset), p.power_pct, - p.core_mhz, p.mem_mhz, p.saved_at.c_str()); - if (!s.profile_driver_ok || !s.profile_gpu_ok) - ImGui::TextColored(kWarn, "Tuned on driver %s%s. Optimize again before relying on it.", p.driver.c_str(), - s.profile_gpu_ok ? "" : " and another card"); - } - if (s.applied) { - const AppliedState& a = *s.applied; - const bool stock = a.core_mhz == 0 && a.mem_mhz == 0 && a.power_pct >= 99 && a.power_pct <= 101; - const bool ours = s.profile && a.core_mhz == s.profile->core_mhz && a.mem_mhz == s.profile->mem_mhz && - a.power_pct >= s.profile->power_pct - 1 && a.power_pct <= s.profile->power_pct + 1; - ImGui::Text("Applied now: power %d %%, core %+d MHz, memory %+d MHz", a.power_pct, a.core_mhz, a.mem_mhz); - ImGui::SameLine(); - if (ours) ImGui::TextColored(kGood, "(the saved tune)"); - else if (stock) ImGui::TextColored(kDim, "(stock)"); - else ImGui::TextColored(kWarn, "(set by something else)"); - } - ImGui::Text("Apply at logon: %s", s.boot_on ? "on" : "off"); - if (s.strikes > 0) { - ImGui::SameLine(); - ImGui::TextColored(kWarn, " %d of %d crash strikes", s.strikes, kMaxBootStrikes); - } - if (!s.last_boot.empty()) ImGui::TextColored(kDim, "Last logon: %s", s.last_boot.c_str()); - if (!s.watchdog_note.empty()) ImGui::TextColored(kWarn, "%s", s.watchdog_note.c_str()); +// Puts the next item at the right edge of the current line, `width` wide. +void align_right(float width) { + ImGui::SameLine(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x - width); +} - if (s.elevated) { - ImGui::BeginDisabled(!s.profile); - if (ImGui::Button("Apply saved tune")) act.apply_profile(); - ImGui::EndDisabled(); - ImGui::SameLine(); - ImGui::BeginDisabled(!s.profile && !s.boot_on); // turning it off never needs a profile - if (ImGui::Button(s.boot_on ? "Turn off apply at logon" : "Apply at every logon")) act.set_boot(!s.boot_on); - ImGui::EndDisabled(); - ImGui::SameLine(); - if (ImGui::Button("Revert to stock")) act.revert_to_stock(); - } +float button_width(const std::string& label) { + return ImGui::CalcTextSize(label.c_str()).x + ImGui::GetStyle().FramePadding.x * 2; +} + +bool primary_button(const std::string& label, ImVec2 size = ImVec2(0, 0)) { + ImGui::PushStyleColor(ImGuiCol_Button, kAccent); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, kAccentHover); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, kAccentBright); + ImGui::PushStyleColor(ImGuiCol_Border, kAccent); + const bool pressed = ImGui::Button(label.c_str(), size); + ImGui::PopStyleColor(4); + return pressed; } -void home(UiState& s, const UiActions& act) { - ImGui::Text("%s", s.gpu_name.empty() ? "No NVIDIA GPU found" : s.gpu_name.c_str()); +// An on/off switch; returns true when clicked. +bool toggle(const char* id, bool on) { + const float h = em() * 1.25f, w = h * 1.9f; + const ImVec2 p = ImGui::GetCursorScreenPos(); + const bool clicked = ImGui::InvisibleButton(id, ImVec2(w, h)); + ImDrawList* dl = ImGui::GetWindowDrawList(); + const ImVec4 track = on ? kAccent : (ImGui::IsItemHovered() ? kInnerHover : kInner); + dl->AddRectFilled(p, ImVec2(p.x + w, p.y + h), ImGui::GetColorU32(track), h / 2); + dl->AddRect(p, ImVec2(p.x + w, p.y + h), ImGui::GetColorU32(on ? kAccent : kBorder), h / 2); + const float r = h / 2 - h * 0.15f; + dl->AddCircleFilled(ImVec2(on ? p.x + w - h / 2 : p.x + h / 2, p.y + h / 2), r, ImGui::GetColorU32(kText)); + return clicked; +} + +// A telemetry tile: icon, label, value. +void tile(const char* id, Icon icon, const char* label, const std::string& value, float w, float h) { + ImGui::PushStyleColor(ImGuiCol_ChildBg, kInner); + ImGui::BeginChild(id, ImVec2(w, h), ImGuiChildFlags_Borders, ImGuiWindowFlags_NoScrollbar); + ImGui::PopStyleColor(); + const float content_h = em() * 2.45f; + ImGui::SetCursorPos(ImVec2(em() * 0.9f, (h - content_h) / 2 + em() * 0.3f)); + ImGui::PushFont(nullptr, base() * 1.8f); + ImGui::PushStyleColor(ImGuiCol_Text, kAccentBright); + ImGui::TextUnformatted(ic(icon).c_str()); + ImGui::PopStyleColor(); + ImGui::PopFont(); + ImGui::SameLine(0, em() * 0.8f); + ImGui::SetCursorPosY((h - content_h) / 2); + ImGui::BeginGroup(); + dim(label); + text_bold(value.c_str(), 1.3f); + ImGui::EndGroup(); + ImGui::EndChild(); +} + +void telemetry_tiles(const UiState& s) { + const Telemetry& t = s.telemetry; + const float gap = ImGui::GetStyle().ItemSpacing.x; + const float w = (ImGui::GetContentRegionAvail().x - gap * 4) / 5, h = em() * 4.2f; + tile("core", kIconChip, "Core clock", field(t.core_mhz, " MHz"), w, h); + ImGui::SameLine(); + tile("mem", kIconMemory, "Memory clock", field(t.mem_mhz, " MHz"), w, h); + ImGui::SameLine(); + tile("temp", kIconTemp, "Temperature", field(t.temp_c, " \xC2\xB0""C"), w, h); + ImGui::SameLine(); + tile("power", kIconPower, "Power", field(t.power_w, "") + " / " + field(t.power_limit_w, " W"), w, h); ImGui::SameLine(); - ImGui::TextColored(kDim, " driver %s", s.driver.empty() ? "unknown" : s.driver.c_str()); - telemetry_row(s); - status_block(s, act); + tile("fan", kIconFan, "Fan speed", field(t.fan_pct, " %"), w, h); +} - ImGui::SeparatorText("Optimize"); - const float w = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) / 2; +// The four presets side by side; clicking one selects it. +void preset_cards(UiState& s) { + const float gap = ImGui::GetStyle().ItemSpacing.x; + const float w = (ImGui::GetContentRegionAvail().x - gap * 3) / 4, h = em() * 7.6f; int i = 0; for (const PresetInfo& p : kPresets) { ImGui::PushID(i); const bool selected = s.preset == p.preset; - ImGui::PushStyleColor(ImGuiCol_Border, selected ? kGood : ImGui::GetStyleColorVec4(ImGuiCol_Border)); - ImGui::BeginChild("preset", ImVec2(w, 0), ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY); - if (ImGui::RadioButton(p.title, selected)) s.preset = p.preset; - ImGui::PushTextWrapPos(0); - ImGui::TextColored(kDim, "%s", p.summary); - ImGui::PopTextWrapPos(); - ImGui::EndChild(); + ImGui::PushStyleColor(ImGuiCol_ChildBg, selected ? kSelectedBg : kInner); + ImGui::PushStyleColor(ImGuiCol_Border, selected ? kAccentBright : kBorder); + ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, selected ? 2.0f : 1.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(em() * 0.8f, em() * 0.7f)); + ImGui::BeginChild("preset", ImVec2(w, h), ImGuiChildFlags_Borders | ImGuiChildFlags_AlwaysUseWindowPadding, + ImGuiWindowFlags_NoScrollbar); + ImGui::PopStyleVar(2); + ImGui::PopStyleColor(2); + if (ImGui::RadioButton("##pick", selected)) s.preset = p.preset; + ImGui::SameLine(); + ImGui::PushStyleColor(ImGuiCol_Text, selected ? kAccentBright : kDim); + ImGui::TextUnformatted(ic(p.icon).c_str()); ImGui::PopStyleColor(); - if (i % 2 == 0) ImGui::SameLine(); + ImGui::SameLine(); + text_bold(p.title, 1.05f); + wrapped(kDim, std::string(p.summary) + " Up to " + std::to_string(objectives_for(p.preset).max_temp_c) + + " \xC2\xB0""C."); + ImGui::EndChild(); + if (ImGui::IsItemClicked()) s.preset = p.preset; + if (i < 3) ImGui::SameLine(); ImGui::PopID(); ++i; } +} + +// ------------------------------------------------------------------ dashboard + +void gpu_card(const UiState& s, const UiActions& act) { + begin_card("gpu"); + const float top = ImGui::GetCursorPosY(); + ImGui::BeginGroup(); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(ImGui::GetStyle().ItemSpacing.x, em() * 0.15f)); + text_bold(s.gpu_name.empty() ? "No NVIDIA GPU found" : s.gpu_name.c_str(), 1.6f); + dim(("Driver " + (s.driver.empty() ? std::string("unknown") : s.driver)).c_str()); + ImGui::PopStyleVar(); + ImGui::EndGroup(); + const std::string detect = with_icon(kIconRefresh, "Detect GPU"); + align_right(button_width(detect)); + ImGui::SetCursorPosY(top); + if (ImGui::Button(detect.c_str())) act.detect_gpu(); ImGui::Spacing(); + telemetry_tiles(s); + end_card(); +} + +void tuning_row(const char* label, const std::string& value, const ImVec4& color = kText) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + dim(label); + ImGui::TableNextColumn(); + ImGui::PushStyleColor(ImGuiCol_Text, color); + ImGui::PushTextWrapPos(0); + ImGui::TextUnformatted(value.c_str()); + ImGui::PopTextWrapPos(); + ImGui::PopStyleColor(); +} + +void tuning_card(const UiState& s, const UiActions& act) { + begin_card("tuning"); + heading("Current tuning"); + const float gap = ImGui::GetStyle().ItemSpacing.x; + int rows = s.applied ? 5 : 2; + if (s.profile && (!s.profile_driver_ok || !s.profile_gpu_ok)) ++rows; + if (s.strikes > 0) ++rows; + const float row_h = em() + ImGui::GetStyle().CellPadding.y * 2; + const float h = std::max(em() * 9.6f, rows * row_h + em() * 1.8f); + const float right_w = std::max(em() * 17, ImGui::GetContentRegionAvail().x * 0.36f); + + begin_card("values", h, kInner, ImGui::GetContentRegionAvail().x - right_w - gap); + if (ImGui::BeginTable("rows", 2, ImGuiTableFlags_SizingFixedFit)) { + ImGui::TableSetupColumn("label", ImGuiTableColumnFlags_WidthFixed, em() * 10); + ImGui::TableSetupColumn("value", ImGuiTableColumnFlags_WidthStretch); + if (s.applied) { + const AppliedState& a = *s.applied; + const bool stock = a.core_mhz == 0 && a.mem_mhz == 0 && a.power_pct >= 99 && a.power_pct <= 101; + const bool ours = s.profile && a.core_mhz == s.profile->core_mhz && a.mem_mhz == s.profile->mem_mhz && + a.power_pct >= s.profile->power_pct - 1 && a.power_pct <= s.profile->power_pct + 1; + if (ours) tuning_row("Applied now", std::string(title_of(s.profile->preset)) + " (the saved tune)", kGood); + else if (stock) tuning_row("Applied now", "Stock"); + else tuning_row("Applied now", "Set by another program", kWarn); + tuning_row("Core clock offset", offset(a.core_mhz)); + tuning_row("Memory clock offset", offset(a.mem_mhz)); + tuning_row("Power limit", std::to_string(a.power_pct) + " %"); + } else { + tuning_row("Applied now", "Unknown (the driver does not report it)", kDim); + } + if (s.profile) { + const Profile& p = *s.profile; + tuning_row("Saved tune", std::string(title_of(p.preset)) + ": " + std::to_string(p.power_pct) + " %, core " + + offset(p.core_mhz) + ", memory " + offset(p.mem_mhz) + " (" + p.saved_at + ")"); + if (!s.profile_driver_ok || !s.profile_gpu_ok) + tuning_row("", "Tuned on driver " + p.driver + (s.profile_gpu_ok ? "" : " and another card") + + ". Optimize again before relying on it.", + kWarn); + } else { + tuning_row("Saved tune", "None yet. Pick a profile and optimize.", kDim); + } + if (s.strikes > 0) + tuning_row("Crash strikes", std::to_string(s.strikes) + " of " + std::to_string(kMaxBootStrikes) + + " (apply at logon stops at " + std::to_string(kMaxBootStrikes) + ")", + kWarn); + ImGui::EndTable(); + } + end_card(); + + ImGui::SameLine(); + ImGui::PushStyleColor(ImGuiCol_ChildBg, kInner); + ImGui::BeginChild("actions", ImVec2(0, h), ImGuiChildFlags_Borders | ImGuiChildFlags_AlwaysUseWindowPadding); + ImGui::PopStyleColor(); + const float bw = ImGui::GetContentRegionAvail().x, bh = em() * 2.3f; if (s.elevated) { - if (ImGui::Button("Optimize", ImVec2(160, 0))) act.optimize(s.preset); - ImGui::SameLine(); - ImGui::TextColored(kDim, "About 10 minutes of full GPU load. Abort restores stock."); + ImGui::BeginDisabled(!s.profile); + if (primary_button("Apply saved tune", ImVec2(bw, bh))) act.apply_profile(); + ImGui::EndDisabled(); } else { - if (ImGui::Button("Restart as administrator", ImVec2(220, 0))) act.restart_elevated(); - ImGui::SameLine(); - ImGui::TextColored(kDim, "Optimizing and applying change clocks and power limits."); + if (primary_button("Restart as administrator", ImVec2(bw, bh))) act.restart_elevated(); } + // Apply at logon: a framed row with a switch. + ImGui::PushStyleColor(ImGuiCol_ChildBg, kCard); + ImGui::BeginChild("logon", ImVec2(bw, bh), ImGuiChildFlags_Borders, ImGuiWindowFlags_NoScrollbar); + ImGui::PopStyleColor(); + ImGui::SetCursorPos(ImVec2(em() * 0.8f, (bh - em()) / 2 - 1)); + ImGui::TextUnformatted("Apply at logon"); + ImGui::SameLine(bw - em() * 3.4f); + ImGui::SetCursorPosY((bh - em() * 1.25f) / 2 - 1); + // Turning it off never needs a profile; turning it on does. + ImGui::BeginDisabled(!s.elevated || (!s.profile && !s.boot_on)); + if (toggle("##logon", s.boot_on)) act.set_boot(!s.boot_on); + ImGui::EndDisabled(); + ImGui::EndChild(); + ImGui::BeginDisabled(!s.elevated); + if (ImGui::Button(with_icon(kIconUndo, "Revert to stock").c_str(), ImVec2(bw, bh))) act.revert_to_stock(); + ImGui::EndDisabled(); + ImGui::EndChild(); + end_card(); +} + +void profiles_card(UiState& s) { + begin_card("profiles"); + heading("Optimization profiles"); + const std::string go = with_icon(kIconPulse, "Optimize..."); + align_right(button_width(go)); + if (ImGui::Button(go.c_str())) s.page = Page::Optimize; + ImGui::Spacing(); + preset_cards(s); + end_card(); +} + +struct LogLine { + std::string time, text; + bool warn; +}; + +// boot.log lines and this session's notes, newest first. +std::vector log_lines(const UiState& s) { + std::vector out; + for (const std::string& line : s.boot_log) { + const size_t sep = line.find(" "); + LogLine l{sep == std::string::npos ? "" : line.substr(0, sep), sep == std::string::npos ? line : line.substr(sep + 2), + false}; + l.warn = l.text.find("not applied") != std::string::npos || l.text.find("failed") != std::string::npos || + l.text.find("stopped") != std::string::npos || l.text.find("another program") != std::string::npos; + out.push_back(std::move(l)); + } + for (const Note& n : s.notes) out.push_back({n.time, n.text, n.warn}); + // Same-minute entries keep their order: boot.log first, then notes. + std::stable_sort(out.begin(), out.end(), [](const LogLine& a, const LogLine& b) { return a.time > b.time; }); + return out; +} + +void log_card(const UiState& s) { + begin_card("log"); + heading("Log"); + const std::string more = with_icon(kIconList, g_full_log ? "Show recent" : "View full log"); + align_right(button_width(more)); + if (ImGui::Button(more.c_str())) g_full_log = !g_full_log; + const auto lines = log_lines(s); + if (lines.empty()) { + dim("Nothing yet."); + } else if (ImGui::BeginTable("log", 2, ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerH | ImGuiTableFlags_BordersOuter | + ImGuiTableFlags_PadOuterX)) { + ImGui::TableSetupColumn("time", ImGuiTableColumnFlags_WidthFixed, em() * 8.5f); + ImGui::TableSetupColumn("text", ImGuiTableColumnFlags_WidthStretch); + const size_t n = g_full_log ? lines.size() : std::min(lines.size(), kLogShort); + for (size_t i = 0; i < n; ++i) { + ImGui::TableNextRow(ImGuiTableRowFlags_None, em() * 1.7f); + ImGui::TableNextColumn(); + dim(lines[i].time.c_str()); + ImGui::TableNextColumn(); + wrapped(lines[i].warn ? kWarn : kText, lines[i].text); + } + ImGui::EndTable(); + } + end_card(); +} + +void dashboard(UiState& s, const UiActions& act) { + gpu_card(s, act); + tuning_card(s, act); + profiles_card(s); + log_card(s); +} + +// ------------------------------------------------------------------ optimize + +void plot(const char* label, const std::deque& h, float lo, float hi, const char* overlay, float w) { + std::vector v(h.begin(), h.end()); + if (v.empty()) v.push_back(0); + ImGui::PlotLines(label, v.data(), static_cast(v.size()), 0, overlay, lo, hi, ImVec2(w, em() * 5)); +} + +void choose(UiState& s, const UiActions& act) { + begin_card("choose"); + heading("Choose a profile"); + dim("Each profile searches for the highest stable settings and applies a safety margin."); + ImGui::Spacing(); + preset_cards(s); + ImGui::Spacing(); + const ImVec2 big(em() * 13, em() * 2.6f); + ImGui::PushFont(g_bold, base() * 1.15f); + const bool go = s.elevated ? primary_button(with_icon(kIconPlay, "Optimize GPU"), big) + : primary_button("Restart as administrator", big); + ImGui::PopFont(); + if (go) s.elevated ? act.optimize(s.preset) : act.restart_elevated(); + ImGui::SameLine(0, em() * 1.5f); + ImGui::BeginGroup(); + ImGui::TextUnformatted((ic(kIconClock) + " About 10 minutes of full GPU load.").c_str()); + dim("Abort restores stock at any time. A setting that crashes the machine is never tried again."); + ImGui::EndGroup(); + end_card(); } void run_screen(UiState& s, const OptimizeWorker::Snapshot& run, const UiActions& act) { - ImGui::Text("Optimizing: %s", preset_name(s.preset)); - telemetry_row(s); + begin_card("run"); + text_bold((std::string("Optimizing: ") + title_of(s.preset)).c_str(), 1.4f); + if (run.running) { + const std::string abort = with_icon(kIconStop, "Abort"); + align_right(button_width(abort) + em()); + if (ImGui::Button(abort.c_str(), ImVec2(button_width(abort) + em(), 0))) act.abort(); + } + dim("Aborting finishes the current probe, then restores stock."); + ImGui::Spacing(); + telemetry_tiles(s); + ImGui::Spacing(); + const float w = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) / 2; char overlay[64]; std::snprintf(overlay, sizeof(overlay), "temperature %s", field(s.telemetry.temp_c, " C").c_str()); - plot("##temp", s.temp_history, 30, 90, overlay); + plot("##temp", s.temp_history, 30, 90, overlay, w); + ImGui::SameLine(); std::snprintf(overlay, sizeof(overlay), "power %s W", field(s.telemetry.power_w, "").c_str()); - plot("##power", s.power_history, 0, static_cast(std::max(s.telemetry.power_limit_w, 1)), overlay); - if (run.running) { - if (ImGui::Button("Abort", ImVec2(120, 0))) act.abort(); - ImGui::SameLine(); - ImGui::TextColored(kDim, "Aborting finishes the current probe, then restores stock."); - } - ImGui::SeparatorText("Log"); - ImGui::BeginChild("log", ImVec2(0, 0), ImGuiChildFlags_Borders); + plot("##power", s.power_history, 0, static_cast(std::max(s.telemetry.power_limit_w, 1)), overlay, w); + end_card(); + + begin_card("probes"); + heading("Probes"); + ImGui::PushStyleColor(ImGuiCol_ChildBg, kInner); + ImGui::BeginChild("lines", ImVec2(0, std::max(em() * 10, ImGui::GetContentRegionAvail().y - em())), + ImGuiChildFlags_Borders); + ImGui::PopStyleColor(); for (const std::string& line : run.log) ImGui::TextUnformatted(line.c_str()); if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) ImGui::SetScrollHereY(1.0f); ImGui::EndChild(); + end_card(); if (!run.running && run.outcome) s.screen = Screen::Results; } void results(UiState& s, const OptimizeWorker::Snapshot& run, const UiActions& act) { if (!run.outcome) { - s.screen = Screen::Home; + s.screen = Screen::Choose; return; } const app::OptimizeOutcome& o = *run.outcome; + begin_card("results"); if (!o.ran) { - ImGui::TextColored(kBad, "Not started: %s", o.error.c_str()); + heading("Not started"); + wrapped(kBad, o.error); } else if (!o.result.ok) { - ImGui::TextColored(kBad, "Not applied: %s", o.result.reason.c_str()); - ImGui::Text("%s", o.result.stock_restored ? "The card is back at stock." : "Reset to stock FAILED -- run `gao --reset`."); + heading("Not applied"); + wrapped(kBad, o.result.reason); + wrapped(kText, o.result.stock_restored ? "The card is back at stock." : "Reset to stock FAILED -- run `gao --reset`."); } else { const OptimizeResult& r = o.result; - ImGui::TextColored(kGood, "Applied: power %d %%, core +%d MHz, memory +%d MHz", r.power_pct, r.core_mhz, r.mem_mhz); - ImGui::TextColored(kDim, "Confirmed edges: core +%d MHz, memory +%d MHz", r.core_confirmed, r.mem_confirmed); - if (ImGui::BeginTable("cmp", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingStretchSame)) { + heading("Done"); + char line[160]; + std::snprintf(line, sizeof(line), "Applied: power %d %%, core +%d MHz, memory +%d MHz", r.power_pct, r.core_mhz, + r.mem_mhz); + wrapped(kGood, line); + std::snprintf(line, sizeof(line), "Confirmed edges: core +%d MHz, memory +%d MHz. The difference is the safety margin.", + r.core_confirmed, r.mem_confirmed); + wrapped(kDim, line); + ImGui::Spacing(); + if (ImGui::BeginTable("cmp", 3, ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerH | ImGuiTableFlags_BordersOuter | + ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_PadOuterX)) { ImGui::TableSetupColumn(""); ImGui::TableSetupColumn("Before (stock)"); ImGui::TableSetupColumn("After"); ImGui::TableHeadersRow(); auto row = [](const char* name, const std::string& a, const std::string& b) { - ImGui::TableNextRow(); - ImGui::TableNextColumn(); ImGui::TextUnformatted(name); + ImGui::TableNextRow(ImGuiTableRowFlags_None, em() * 1.6f); + ImGui::TableNextColumn(); dim(name); ImGui::TableNextColumn(); ImGui::TextUnformatted(a.c_str()); ImGui::TableNextColumn(); ImGui::TextUnformatted(b.c_str()); }; const double gain = r.baseline.score > 0 ? (r.soak.score / r.baseline.score - 1) * 100 : 0; - char sb[32], sa[32]; + char sb[32], sa[48]; std::snprintf(sb, sizeof(sb), "%.0f it/s", r.baseline.score); std::snprintf(sa, sizeof(sa), "%.0f it/s (%+.1f %%)", r.soak.score, gain); row("Score", sb, sa); row("Core clock", field(r.baseline.avg_core_mhz, " MHz"), field(r.soak.avg_core_mhz, " MHz")); row("Memory clock", field(r.baseline.avg_mem_mhz, " MHz"), field(r.soak.avg_mem_mhz, " MHz")); - row("Peak temperature", field(r.baseline.peak_temp_c, " C"), field(r.soak.peak_temp_c, " C")); + row("Peak temperature", field(r.baseline.peak_temp_c, " \xC2\xB0""C"), field(r.soak.peak_temp_c, " \xC2\xB0""C")); row("Power", field(r.baseline.avg_power_w, " W"), field(r.soak.avg_power_w, " W")); ImGui::EndTable(); } - if (o.saved) ImGui::Text("Saved. It stays applied until reboot; turn on apply at logon to keep it."); - else ImGui::TextColored(kWarn, "Not saved: %s", o.save_note.c_str()); - if (o.saved && ImGui::Button(s.boot_on ? "Apply at logon is on" : "Apply at every logon")) { - if (!s.boot_on) act.set_boot(true); - } + ImGui::Spacing(); + if (o.saved) wrapped(kText, "Saved. It stays applied until reboot; turn on apply at logon to keep it."); + else wrapped(kWarn, "Not saved: " + o.save_note); + if (o.saved && !s.boot_on && primary_button("Apply at every logon")) act.set_boot(true); + if (o.saved && !s.boot_on) ImGui::SameLine(); + if (ImGui::Button(with_icon(kIconUndo, "Revert to stock").c_str())) act.revert_to_stock(); ImGui::SameLine(); - if (ImGui::Button("Revert to stock")) act.revert_to_stock(); } + if (ImGui::Button(with_icon(kIconBack, "Back").c_str())) s.screen = Screen::Choose; + end_card(); +} + +void optimize_page(UiState& s, const OptimizeWorker::Snapshot& run, const UiActions& act) { + if (run.running) s.screen = Screen::Run; + switch (s.screen) { + case Screen::Choose: choose(s, act); break; + case Screen::Run: run_screen(s, run, act); break; + case Screen::Results: results(s, run, act); break; + } +} + +// ------------------------------------------------------------------ about + +void about() { + begin_card("about"); + text_bold("GPU Auto Optimizer", 1.6f); + dim(("Version " + std::string(kVersion)).c_str()); + ImGui::Spacing(); + wrapped(kText, "Finds the power limit and the core and memory clock offsets your NVIDIA card runs stably at, and keeps them " + "applied. Every setting it writes is read back and checked, a failed apply ends at stock, and a crash journal " + "makes sure a setting that froze the machine is never tried again."); ImGui::Spacing(); - if (ImGui::Button("Back")) s.screen = Screen::Home; + wrapped(kDim, "State: %ProgramData%\\GpuAutoOptimizer (gao.json, journal.jsonl, boot.log)"); + wrapped(kDim, "Command line: gao.exe --help"); + wrapped(kDim, "Source: github.com/Rovey/gpu-auto-optimizer. MIT License."); + end_card(); } +// ------------------------------------------------------------------ frame + +void nav_item(UiState& s, Page page, Icon icon, const char* label) { + const bool selected = s.page == page; + const ImVec2 size(ImGui::GetContentRegionAvail().x, em() * 2.4f); + const ImVec2 p = ImGui::GetCursorScreenPos(); + if (ImGui::InvisibleButton(label, size)) s.page = page; + const bool hovered = ImGui::IsItemHovered(); + ImDrawList* dl = ImGui::GetWindowDrawList(); + if (selected || hovered) + dl->AddRectFilled(p, ImVec2(p.x + size.x, p.y + size.y), ImGui::GetColorU32(selected ? kAccent : kInner), em() * 0.35f); + const float y = p.y + (size.y - em()) / 2; + dl->AddText(ImVec2(p.x + em() * 0.9f, y), ImGui::GetColorU32(kText), ic(icon).c_str()); + dl->AddText(ImVec2(p.x + em() * 2.6f, y), ImGui::GetColorU32(kText), label); +} + +void sidebar(UiState& s) { + ImGui::PushStyleColor(ImGuiCol_ChildBg, kNav); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(em() * 0.6f, em() * 0.8f)); + ImGui::BeginChild("nav", ImVec2(em() * 13, 0), ImGuiChildFlags_AlwaysUseWindowPadding, ImGuiWindowFlags_NoScrollbar); + ImGui::PopStyleVar(); + ImGui::PopStyleColor(); + nav_item(s, Page::Dashboard, kIconHome, "Dashboard"); + nav_item(s, Page::Optimize, kIconPulse, "Optimize"); + nav_item(s, Page::About, kIconInfo, "About"); + ImGui::SetCursorPosY(ImGui::GetWindowHeight() - em() * 2.2f); + ImGui::PushStyleColor(ImGuiCol_Text, s.elevated ? kGood : kDim); + ImGui::TextUnformatted((ic(kIconShield) + (s.elevated ? " Administrator" : " Read-only")).c_str()); + ImGui::PopStyleColor(); + ImGui::EndChild(); +} + +} + +void load_fonts(const std::string& fonts_dir) { + ImGuiIO& io = ImGui::GetIO(); + auto add_icons = [&] { + ImFontConfig cfg; + cfg.MergeMode = true; + cfg.GlyphOffset.y = 3.0f; // the icon fonts sit higher than Segoe UI's baseline + if (!io.Fonts->AddFontFromFileTTF((fonts_dir + "\\SegoeIcons.ttf").c_str(), 17.0f, &cfg)) + io.Fonts->AddFontFromFileTTF((fonts_dir + "\\segmdl2.ttf").c_str(), 17.0f, &cfg); // Windows 10 + }; + g_regular = fonts_dir.empty() ? nullptr : io.Fonts->AddFontFromFileTTF((fonts_dir + "\\segoeui.ttf").c_str(), 17.0f); + if (!g_regular) { + g_regular = g_bold = io.Fonts->AddFontDefault(); + return; + } + add_icons(); + g_bold = io.Fonts->AddFontFromFileTTF((fonts_dir + "\\seguisb.ttf").c_str(), 17.0f); + if (g_bold) add_icons(); + else g_bold = g_regular; +} + +void apply_style(float scale) { + ImGuiStyle& st = ImGui::GetStyle(); + st = ImGuiStyle(); + ImGui::StyleColorsDark(); + ImVec4* c = st.Colors; + c[ImGuiCol_WindowBg] = kBg; + c[ImGuiCol_ChildBg] = ImVec4(0, 0, 0, 0); + c[ImGuiCol_PopupBg] = kCard; + c[ImGuiCol_Border] = kBorder; + c[ImGuiCol_Text] = kText; + c[ImGuiCol_TextDisabled] = kDim; + c[ImGuiCol_FrameBg] = kInner; + c[ImGuiCol_FrameBgHovered] = kInnerHover; + c[ImGuiCol_FrameBgActive] = kInnerHover; + c[ImGuiCol_Button] = kInner; + c[ImGuiCol_ButtonHovered] = kInnerHover; + c[ImGuiCol_ButtonActive] = kSelectedBg; + c[ImGuiCol_CheckMark] = kAccentBright; + c[ImGuiCol_Header] = kInner; + c[ImGuiCol_HeaderHovered] = kInnerHover; + c[ImGuiCol_HeaderActive] = kSelectedBg; + c[ImGuiCol_TableHeaderBg] = kInner; + c[ImGuiCol_TableBorderStrong] = kBorder; + c[ImGuiCol_TableBorderLight] = kBorder; + c[ImGuiCol_TableRowBg] = kInner; + c[ImGuiCol_TableRowBgAlt] = ImVec4(0.100f, 0.114f, 0.137f, 1); + c[ImGuiCol_PlotLines] = kAccentBright; + c[ImGuiCol_ScrollbarBg] = ImVec4(0, 0, 0, 0); + st.WindowRounding = 0; + st.ChildRounding = 8; + st.FrameRounding = 6; + st.GrabRounding = 6; + st.FrameBorderSize = 1; + st.ChildBorderSize = 1; + st.FramePadding = ImVec2(12, 7); + st.ItemSpacing = ImVec2(12, 12); + st.CellPadding = ImVec2(10, 4); + st.ScaleAllSizes(scale); + st.FontScaleDpi = scale; } void push_history(std::deque& h, float v) { @@ -211,19 +648,22 @@ void draw_ui(UiState& s, const OptimizeWorker::Snapshot& run, const UiActions& a const ImGuiViewport* vp = ImGui::GetMainViewport(); ImGui::SetNextWindowPos(vp->WorkPos); ImGui::SetNextWindowSize(vp->WorkSize); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); ImGui::Begin("GPU Auto Optimizer", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoBringToFrontOnFocus); - if (run.running && s.screen != Screen::Run) s.screen = Screen::Run; - switch (s.screen) { - case Screen::Home: home(s, act); break; - case Screen::Run: run_screen(s, run, act); break; - case Screen::Results: results(s, run, act); break; - } - if (!s.message.empty()) { - ImGui::Separator(); - ImGui::TextWrapped("%s", s.message.c_str()); + ImGui::PopStyleVar(); + sidebar(s); + ImGui::SameLine(0, 0); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(em() * 0.9f, em() * 0.9f)); + ImGui::BeginChild("content", ImVec2(0, 0), ImGuiChildFlags_AlwaysUseWindowPadding); + ImGui::PopStyleVar(); + switch (s.page) { + case Page::Dashboard: dashboard(s, act); break; + case Page::Optimize: optimize_page(s, run, act); break; + case Page::About: about(); break; } + ImGui::EndChild(); ImGui::End(); } diff --git a/src/app/gui/ui.hpp b/src/app/gui/ui.hpp index 4b71ce3..230f8eb 100644 --- a/src/app/gui/ui.hpp +++ b/src/app/gui/ui.hpp @@ -6,14 +6,25 @@ #include #include #include +#include namespace gao::gui { -enum class Screen { Home, Run, Results }; +enum class Page { Dashboard, Optimize, About }; +enum class Screen { Choose, Run, Results }; // the Optimize page's state + +// Something the app did or was told this session, shown in the log with the +// boot.log lines. +struct Note { + std::string time; // "YYYY-MM-DD HH:MM", as in boot.log + std::string text; + bool warn = false; +}; // Everything the window shows, refreshed by the main loop about once a second. struct UiState { - Screen screen = Screen::Home; + Page page = Page::Dashboard; + Screen screen = Screen::Choose; Preset preset = Preset::BestOfMyGpu; bool elevated = false; @@ -29,9 +40,8 @@ struct UiState { std::optional applied; bool boot_on = false; int strikes = 0; - std::string last_boot; - std::string watchdog_note; // what the tune watchdog last did, if anything - std::string message; // result of the last button press + std::vector boot_log; // the last lines of boot.log, oldest first + std::vector notes; // this session, oldest first }; struct UiActions { @@ -41,8 +51,14 @@ struct UiActions { std::function apply_profile; std::function revert_to_stock; std::function set_boot; + std::function detect_gpu; }; +// Fonts (Segoe UI with Segoe Fluent Icons merged in) and the colour scheme. +// Call load_fonts() once before the first frame, apply_style() on every DPI change. +void load_fonts(const std::string& fonts_dir); +void apply_style(float dpi_scale); + void push_history(std::deque& h, float v); void draw_ui(UiState& s, const OptimizeWorker::Snapshot& run, const UiActions& act);