From 4276b6efeda7025f6774f77914da00d1f51fda75 Mon Sep 17 00:00:00 2001 From: Rovey Date: Sun, 11 Oct 2026 10:52:31 +0200 Subject: [PATCH 1/2] feat(gui): the run says which step it is at; the search's baseline is the stock measurement; the fan chart shows the curve as set A run only counted down its long measurements; a clock search said nothing but log lines. The search now names every step, with a countdown for the ones that last. It no longer spends 30 s at stock right after the five minutes every profile starts with: that measurement is its baseline. In the fan chart the curve as it was set is drawn thin under the line the fans follow, so a point a guard rail overrides no longer looks ignored. Co-Authored-By: Claude Opus 5.5 --- README.md | 5 ++-- docs/development.md | 3 ++ src/app/common.cpp | 6 ++++ src/app/gui/ui.cpp | 64 ++++++++++++++++++++++++++----------------- src/core/search.cpp | 36 ++++++++++++++++++------ src/core/search.hpp | 10 +++++-- tests/test_search.cpp | 48 ++++++++++++++++++++++++++++++++ 7 files changed, 133 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 096a6d7..9ca4284 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ The one time it asks for you again is after a driver update: a tune belongs to t 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, and counts every long measurement down. +- **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. - **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. @@ -167,8 +167,7 @@ The undervolt is saved in place of an overclock, not next to it: on the card a c ```mermaid flowchart LR - S[Stock
300 s, to compare with] --> A[Baseline
30 s at stock] - A --> B[Power limit] + S[Stock
300 s: the "before",
and what probes are judged by] --> B[Power limit] B --> C[Memory offset
bandwidth peak, confirmed,
safety margin] C --> D[Core offset
single steps, confirmed,
safety margin] D --> G[Soak
300 s] diff --git a/docs/development.md b/docs/development.md index 69beb2c..beccb8e 100644 --- a/docs/development.md +++ b/docs/development.md @@ -115,6 +115,9 @@ The applied offset is always at least one step below the confirmed edge. Profile | With its reference handed over, the undervolt search warms its load up before the first candidate | A search that measures its own reference has five minutes of load behind it when the first 3 s candidate runs. One that is handed the reference (the measurement at stock, or the overclock's soak in All in one) starts its load with that candidate, and a load that has not run yet is slower in its first seconds than the card is: it sizes its batches and the card clocks up from idle. The candidate then averages a clock far below the target and reads `SLOW`, and a descent ends at its first failure. Seen three times on the reference card: `1070 mV (+15 MHz): SLOW ... clock=2323 MHz` and `1050 mV: SLOW ... clock=2411 MHz` in hardware check 92, where the undervolt profile then said "the card held no undervolt", and `1025 mV (+150 MHz): SLOW ... clock=2380 MHz` in an All in one run of 2026-10-08. That last one means the undervolt step of All in one has kept the overclock's curve cut off at its chosen clock and searched nothing below it, in every release that has it; its numbers in the README were measured that way. Now the load runs `kUvWarmUpS` (5 s) at the reference's settings first; only whether the card computes is taken from it. The score threshold was not loosened: a candidate that is slow on a load that is running is still `SLOW`. | | A flat top goes back on a card that is cooler than when it was found | The built-in curve moves with temperature: a point reads up to three steps (45 MHz) more on a cool card than on a warm one. A curve that was only cut off (All in one, when the margin leaves no raise above the overclock) is anchored on the point that ran the target by itself while the search had the card warm. Seconds later, in the next step, the same point read one step more, and `apply_flat_top` refused to put the tune back: "the point already runs more than the target" (hardware check 94). The warm-up of the row above is what made the search read its curve on a warm card; before it, the cut-off was anchored on a card that had idled, and the fault did not show. `apply_flat_top` now takes an anchor that runs up to `kVfTailDropKhz` more than the target and holds it on the target, at the voltage the tune was tested on; more than that is still refused, as not this tune's curve. `curve_state` knows such an anchor (stored below the base offset by no more than that, with the points above cut off) as this program's flat top, so the dashboard and the watchdog do not take it for another program's work. The other direction was always there: a top written on a cool card runs a step lower once it is warm (`kUvClockSlackKhz`). | | The undervolt search keeps the fans on a curve | It left them to the driver and still stopped every probe at the profile's limit of 75 C. That holds on the reference card, where the driver's fans keep an undervolted card far below it, and cannot hold on a card the driver lets run at 85 C: every probe would end `TOO HOT` and read as "the card held no undervolt". The search now drives the fans as `run_optimize` does: the curve in use (the overclock's own, in an all-in-one run) or the preset's, 100 % at the limit. The undervolt is then tested with the fans it will run with, so the run turns the app's fan control on, as every other tune does. Not verified on a hot card: whether 100 % keeps a 3080 Ti below 75 C at stock power is not known, and the undervolt has no power limit to lower. | +| The clock search takes the five minutes at stock as its baseline | The search judges every probe by a score at stock: below a quarter of it a probe is `STALLED`, a card is back after a reset at nine tenths of it, and the efficient profiles look for the lowest power limit within 2 % of it. It used to take that score in 30 s of its own, right after the five minutes at stock that every profile now starts with (`optimize(..., stock)`); a stock measurement that was not stable is not used, and without one the search still takes its 30 s. The five-minute score is a little higher than a 30 s one on a card that is already hot (5705 against 5662 it/s on the reference card), which makes the 2 % of the efficient profiles slightly stricter. Not yet measured on hardware. | +| The search says which step it is at | `OptimizeIo::measuring`, as the undervolt and the fan tune have it: the step, and its length when it lasts (a confirm probe, the final test, the rest after a reset). A search probe lasts three seconds and gets no countdown, only its name. The window shows one row for all of it, as high with a countdown as without, so the page does not jump. | +| The fan chart draws the curve as set under the line the fans follow | A point below the card's minimum, in the fan-stop zone or past the profile's limit is overridden, and the line leaves it. Without the set curve, thin and dimmed through the points, that read as the line ignoring them. | | The undervolt run measures stock for as long as it soaks | Five minutes each (`kUvBaselineS = kUvSoakS`; since the row above the measurement at stock is `measure_stock`'s, which lasts as long). 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 measures stock the same way before its search (`measure_stock`, the fans left to the driver) and shows that as the "before"; the search's own 30 s baseline stays, as the reference of the power search and of the stall and health thresholds. The all-in-one run hands its own stock measurement to the overclock step, so nothing is measured twice. | | 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. | diff --git a/src/app/common.cpp b/src/app/common.cpp index 2847950..179f2c1 100644 --- a/src/app/common.cpp +++ b/src/app/common.cpp @@ -412,6 +412,12 @@ OptimizeOutcome run_optimize(Preset preset, const OptimizeHooks& hooks, const st log("the stress load could not be rebuilt: " + load->Error()); return false; }; + io.measuring = hooks.measuring; + // Whatever step the search announced last is over when it returns, however it returns. + struct StepDone { + const OptimizeHooks& hooks; + ~StepDone() { if (hooks.measuring) hooks.measuring("", 0); } + } step_done{hooks}; if (hooks.active_gpu) hooks.active_gpu(&gpu); // The last line of defence: a fault that escapes the per-call guards (in // the stress load's D3D calls, say) must not kill the process with a diff --git a/src/app/gui/ui.cpp b/src/app/gui/ui.cpp index 69bf886..5bfc3a8 100644 --- a/src/app/gui/ui.cpp +++ b/src/app/gui/ui.cpp @@ -584,26 +584,33 @@ bool curve_editor(FanCurve& c, int min_pct, int max_temp_c, int now_temp_c, int* const ImVec2 n = to_screen(static_cast(std::min(now_temp_c, 100)), 0); dl->AddLine(ImVec2(n.x, a.y), ImVec2(n.x, b.y), ImGui::GetColorU32(kText), 1.0f); } + // The curve as it was set: straight from point to point, flat outside them. + auto as_set = [&](float t) { + const auto& pts = c.points; + if (t <= static_cast(pts.front().temp_c)) return static_cast(pts.front().pct); + for (size_t k = 1; k < pts.size(); ++k) { + if (t > static_cast(pts[k].temp_c)) continue; + const float f = (t - static_cast(pts[k - 1].temp_c)) / static_cast(pts[k].temp_c - pts[k - 1].temp_c); + return static_cast(pts[k - 1].pct) + f * static_cast(pts[k].pct - pts[k - 1].pct); + } + return static_cast(pts.back().pct); + }; // What the fans actually do, guard rails included: exact, not rounded to // whole percents, so straight segments stay straight and the jumps at the // stop threshold and the limit are vertical. auto effective = [&](float t) { if (t >= static_cast(max_temp_c)) return 100.0f; if (c.stop_below_c && t < static_cast(*c.stop_below_c)) return 0.0f; - const auto& pts = c.points; - float v = static_cast(pts.back().pct); - if (t <= static_cast(pts.front().temp_c)) { - v = static_cast(pts.front().pct); - } else { - for (size_t k = 1; k < pts.size(); ++k) { - if (t > static_cast(pts[k].temp_c)) continue; - const float f = (t - static_cast(pts[k - 1].temp_c)) / static_cast(pts[k].temp_c - pts[k - 1].temp_c); - v = static_cast(pts[k - 1].pct) + f * static_cast(pts[k].pct - pts[k - 1].pct); - break; - } - } - return std::max(v, static_cast(min_pct)); + return std::max(as_set(t), static_cast(min_pct)); }; + // Thin and dimmed through the points, under the line the fans follow: where + // the two part, a guard rail is why, and the points there are not ignored + // by mistake. + std::vector set_line{to_screen(20, as_set(20))}; + for (const FanPoint& pt : c.points) set_line.push_back(to_screen(static_cast(pt.temp_c), static_cast(pt.pct))); + set_line.push_back(to_screen(100, as_set(100))); + dl->AddPolyline(set_line.data(), static_cast(set_line.size()), + ImGui::GetColorU32(ImVec4(kAccentBright.x, kAccentBright.y, kAccentBright.z, 0.40f)), ImDrawFlags_None, 1.2f); std::vector line; for (int i = 0; i <= 80 * 8; ++i) { const float t = 20 + static_cast(i) / 8; @@ -928,19 +935,26 @@ void run_screen(UiState& s, const OptimizeWorker::Snapshot& run, const UiActions if (ImGui::Button("Cancel")) act.answer_busy(BusyAnswer::Stop); ImGui::Spacing(); } - // A long measurement: what it is and how long it still takes. - if (run.running && !run.measuring.empty() && run.measuring_seconds > 0) { - const double left = std::max(0.0, run.measuring_seconds - run.measuring_spent); - char time[48]; - std::snprintf(time, sizeof(time), "%d:%02d left of %s", static_cast(left) / 60, static_cast(left) % 60, - duration_text(run.measuring_seconds).c_str()); + // The step the run is at. One that lasts counts down; one that takes + // seconds, or as long as it takes, only says what it is. The row is as + // high either way, so the page does not jump from step to step. + if (run.running && !run.measuring.empty()) { text_bold((ic(kIconClock) + " " + run.measuring).c_str(), 1.1f); - ImGui::SameLine(0, em()); - ImGui::TextUnformatted(time); - ImGui::PushStyleColor(ImGuiCol_PlotHistogram, kAccentBright); - ImGui::PushStyleColor(ImGuiCol_FrameBg, kInner); - ImGui::ProgressBar(static_cast(std::min(1.0, run.measuring_spent / run.measuring_seconds)), ImVec2(-1, em() * 0.5f), ""); - ImGui::PopStyleColor(2); + const ImVec2 bar(-1, em() * 0.5f); + if (run.measuring_seconds > 0) { + const double left = std::max(0.0, run.measuring_seconds - run.measuring_spent); + char time[48]; + std::snprintf(time, sizeof(time), "%d:%02d left of %s", static_cast(left) / 60, static_cast(left) % 60, + duration_text(run.measuring_seconds).c_str()); + ImGui::SameLine(0, em()); + ImGui::TextUnformatted(time); + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, kAccentBright); + ImGui::PushStyleColor(ImGuiCol_FrameBg, kInner); + ImGui::ProgressBar(static_cast(std::min(1.0, run.measuring_spent / run.measuring_seconds)), bar, ""); + ImGui::PopStyleColor(2); + } else { + ImGui::Dummy(ImVec2(1, bar.y)); + } ImGui::Spacing(); } telemetry_tiles(s); diff --git a/src/core/search.cpp b/src/core/search.cpp index 135532b..3bd426d 100644 --- a/src/core/search.cpp +++ b/src/core/search.cpp @@ -177,6 +177,7 @@ OptimizeResult optimize(const GpuControl& gpu, const Objectives& obj, Journal& j OptimizeResult r; std::string stopped; // non-empty once the run must end; later probes become no-ops auto log = [&](const std::string& m) { if (io.log) io.log(m); }; + auto step = [&](const std::string& what, double seconds) { if (io.measuring) io.measuring(what, seconds); }; bool hw_suspect = false; // the connections may be dead: a probe, a write or a measurement ended in a way a driver reset explains bool reconnect_futile = false; // a reconnect failed, or did not help: the way out does not try another // The reset budget. These are only ever set with gpu.recover: without a @@ -264,6 +265,7 @@ OptimizeResult optimize(const GpuControl& gpu, const Objectives& obj, Journal& j if (io.aborted && io.aborted()) return false; if (io.rest) { log("resting " + std::to_string(static_cast(kRestAfterResetS)) + " s before the card is loaded again"); + step("The driver was reset: resting before the card is loaded again", kRestAfterResetS); if (!io.rest(kRestAfterResetS)) return false; } return !(io.aborted && io.aborted()); @@ -309,6 +311,7 @@ OptimizeResult optimize(const GpuControl& gpu, const Objectives& obj, Journal& j if (!load_ready) { log("the stress load is not ready yet"); continue; } } log("checking that the card is back: " + std::to_string(static_cast(kHealthProbeS)) + " s at stock"); + step("Checking that the card is back", kHealthProbeS); // Called directly, not through `probe`, which would count a reset // verdict as an event of its own. No stall floor: the load was // prepared just before, and five seconds are bounded already. @@ -417,15 +420,23 @@ OptimizeResult optimize(const GpuControl& gpu, const Objectives& obj, Journal& j }; if (obj.core_oc) log(range_text("core", bounds.core_max_mhz, bounds.core_from_card)); if (obj.mem_oc) log(range_text("mem", bounds.mem_max_mhz, bounds.mem_from_card)); - log("baseline: 30 s at stock"); - const auto base = probe(kBaselineS, kSafetyTempC); - if (!base) return finish_fail(stopped); - r.baseline = *base; - log("baseline: " + describe(*base)); - // The baseline runs at stock clocks: a reset there is nothing the search - // can work around. - if (reset_in_step) return finish_fail(at_stock_clocks()); - if (base->verdict != Verdict::Stable) return finish_fail(std::string("stock is not stable (") + verdict_name(base->verdict) + ")"); + if (stock && stock->verdict == Verdict::Stable && stock->score > 0) { + // Measured a moment ago, for longer and with the same load: another + // half minute at stock would say nothing new. + r.baseline = *stock; + log("baseline: the measurement at stock, " + describe(*stock)); + } else { + log("baseline: 30 s at stock"); + step("Baseline at stock", kBaselineS); + const auto base = probe(kBaselineS, kSafetyTempC); + if (!base) return finish_fail(stopped); + r.baseline = *base; + log("baseline: " + describe(*base)); + // The baseline runs at stock clocks: a reset there is nothing the + // search can work around. + if (reset_in_step) return finish_fail(at_stock_clocks()); + if (base->verdict != Verdict::Stable) return finish_fail(std::string("stock is not stable (") + verdict_name(base->verdict) + ")"); + } // Power. int power_floor = 100; // lowest power the soak may fall back to @@ -456,6 +467,7 @@ OptimizeResult optimize(const GpuControl& gpu, const Objectives& obj, Journal& j if (stopped.empty()) stopped = at_stock_clocks(); return std::nullopt; } + step("Power limit " + std::to_string(pct) + " %", kPowerProbeS); const auto s = probe(kPowerProbeS, kSafetyTempC); if (s) { seen[pct] = *s; log("power " + std::to_string(pct) + " %: " + describe(*s)); } if (reset_in_step && stopped.empty()) stopped = at_stock_clocks(); @@ -507,6 +519,11 @@ OptimizeResult optimize(const GpuControl& gpu, const Objectives& obj, Journal& j if (reset_in_step && stock_clocks && stopped.empty()) stopped = at_stock_clocks(); return std::nullopt; } + // A search probe lasts seconds and has no countdown; a confirm probe has. + const bool confirm = seconds == kConfirmProbeS; + step(std::string(confirm ? "Confirming " : "Trying ") + + (core_j ? "core clock +" + std::to_string(core) : "memory clock +" + std::to_string(mem)) + " MHz", + confirm ? seconds : 0); const auto s = probe(seconds, obj.max_temp_c); if (s && s->verdict == Verdict::Stable && extra) extra(); if (!journal.complete(*id, closed_as(s))) { @@ -702,6 +719,7 @@ OptimizeResult optimize(const GpuControl& gpu, const Objectives& obj, Journal& j } log("soak: " + std::to_string(static_cast(kSoakS)) + " s at power " + std::to_string(r.power_pct) + " %, core +" + std::to_string(r.core_mhz) + ", mem +" + std::to_string(r.mem_mhz)); + step("Final test of the result", kSoakS); const auto s = probe(kSoakS, obj.max_temp_c); if (!journal.complete(*id, closed_as(s))) return finish_fail("could not write the journal"); diff --git a/src/core/search.hpp b/src/core/search.hpp index 959d618..e971d8d 100644 --- a/src/core/search.hpp +++ b/src/core/search.hpp @@ -165,6 +165,10 @@ struct OptimizeIo { // Makes the stress load ready for a probe after a driver reset (a fresh // device), outside any timing. False: not ready yet. Empty: nothing to do. std::function prepare_load; + // What the search does now, for whoever shows it: the step, and how long + // it lasts when that is known (0: a few seconds, or as long as it takes). + // Optional. + std::function measuring; }; struct OptimizeResult { @@ -201,8 +205,10 @@ struct OptimizeResult { // card. inline constexpr double kNotSlowerScore = 0.99; -// stock: the measurement at stock that went before the run, when there was -// one: what a profile that promises speed must not end slower than. +// stock: the card measured at stock just before, by the caller and with the +// same load. When it was stable it is the baseline, and the search takes none +// of its own. Stable or stopped by the temperature, it is what a profile that +// promises speed must not end slower than. OptimizeResult optimize(const GpuControl& gpu, const Objectives& obj, Journal& journal, const OptimizeIo& io, const std::optional& stock = {}); diff --git a/tests/test_search.cpp b/tests/test_search.cpp index 04ac963..92f1333 100644 --- a/tests/test_search.cpp +++ b/tests/test_search.cpp @@ -290,6 +290,7 @@ struct Run { int abort_after_probes = -1; bool no_rest = false; // the application gives the search no way to wait std::optional stock; // the measurement at stock that went before the run, when there was one + std::vector> steps; // what the search said it was doing, and for how long OptimizeResult go(Preset preset, GpuControl gpu) { Journal j(card.journal, [this](const std::string& l) { if (l.find("\"complete\"") != std::string::npos && card.completes_ok >= 0 && card.completes_ok-- == 0) @@ -334,9 +335,13 @@ struct Run { return card.bw_curve(card.mem); }; if (card.bw_curve) io.bandwidth_unsettled = [this] { return card.bw_unsettled; }; + io.measuring = [this](const std::string& what, double seconds) { steps.emplace_back(what, seconds); }; return optimize(gpu, objectives_for(preset), j, io, stock); } OptimizeResult go(Preset preset) { return go(preset, card.gpu()); } + bool said(const std::string& what, double seconds) const { + return std::find(steps.begin(), steps.end(), std::make_pair(what, seconds)) != steps.end(); + } bool logged(const std::string& what) const { return count_logged(what) > 0; } int count_logged(const std::string& what) const { int n = 0; @@ -437,6 +442,49 @@ TEST_CASE("best preset converges to the card's edges and applies the margin") { CHECK(r.mem_confirmed == 800); } +TEST_CASE("a stock measurement the caller made is the baseline: the search takes none of its own") { + Run plain; + const auto without = plain.go(Preset::BestOfMyGpu); + REQUIRE(without.ok); + CHECK(plain.logged("baseline: 30 s at stock")); + + Run run; + run.stock = StabilityResult{}; + run.stock->score = without.baseline.score; + const auto r = run.go(Preset::BestOfMyGpu); + REQUIRE(r.ok); + CHECK_FALSE(run.logged("baseline: 30 s at stock")); + CHECK(run.logged("baseline: the measurement at stock")); + CHECK(run.card.probes == plain.card.probes - 1); // the half minute at stock, and nothing else + CHECK(r.baseline.score == without.baseline.score); + CHECK(r.power_pct == without.power_pct); + CHECK(r.core_mhz == without.core_mhz); + CHECK(r.mem_mhz == without.mem_mhz); +} + +TEST_CASE("a stock measurement that was not stable is no baseline") { + Run run; + run.stock = StabilityResult{}; + run.stock->verdict = Verdict::TooHot; + run.stock->score = 1000; + REQUIRE(run.go(Preset::BestOfMyGpu).ok); + CHECK(run.logged("baseline: 30 s at stock")); +} + +TEST_CASE("the search says which step it is at, and how long the long ones last") { + Run run; + const auto r = run.go(Preset::BestOfMyGpu); + REQUIRE(r.ok); + REQUIRE_FALSE(run.steps.empty()); + CHECK(run.steps.front() == std::make_pair(std::string("Baseline at stock"), 30.0)); + CHECK(run.said("Power limit 115 %", 20)); + CHECK(run.said("Trying memory clock +800 MHz", 0)); // a search probe: seconds, no countdown + CHECK(run.said("Confirming memory clock +800 MHz", 30)); + CHECK(run.said("Trying core clock +150 MHz", 0)); + CHECK(run.said("Confirming core clock +150 MHz", 30)); + CHECK(run.steps.back() == std::make_pair(std::string("Final test of the result"), 300.0)); +} + TEST_CASE("quiet preset picks the lowest power within 2 % of the reference score") { Run run; const auto r = run.go(Preset::Quiet); From 0aaaf5515ceb4707fe5f6fcc403f481a84820ccc Mon Sep 17 00:00:00 2001 From: Rovey Date: Sun, 11 Oct 2026 18:48:34 +0200 Subject: [PATCH 2/2] feat(gui): the countdown of a step stands at the right edge, and its bar fills evenly The time left stood right behind the step's name and moved with its length. It is at the right edge now, over the end of the bar. While a step counts down the window draws about thirty times a second instead of ten: the bar of a twenty-second step moved in jumps of several pixels. Co-Authored-By: Claude Opus 5.5 --- src/app/gui/main_gui.cpp | 10 ++++++++-- src/app/gui/ui.cpp | 4 +++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/app/gui/main_gui.cpp b/src/app/gui/main_gui.cpp index e93ac38..af02d61 100644 --- a/src/app/gui/main_gui.cpp +++ b/src/app/gui/main_gui.cpp @@ -1322,13 +1322,19 @@ int WINAPI wWinMain(HINSTANCE inst, HINSTANCE, PWSTR cmdline, int) { // Render only when needed: nothing while hidden, a few frames after input, // ~10 Hz during a run and ~4 Hz otherwise. The tool measures the GPU; its - // own window must not load it. + // own window must not load it. The one exception is a step that counts + // down: at 10 Hz the bar of a 20 s step moves in jumps of several pixels, + // so it gets ~30 Hz while it is on screen. The steps that measure + // bandwidth have no countdown and stay at 10. for (;;) { // While the device is lost nothing is drawn either: wait for a message // (the telemetry timer rebuilds the device, when the backoff lets it) // instead of polling. const DWORD timeout = - !g.visible || g.device_lost ? INFINITE : g.input_frames > 0 ? 0 : g.worker->running() ? 100 : 250; + !g.visible || g.device_lost ? INFINITE + : g.input_frames > 0 ? 0 + : !g.worker->running() ? 250 + : g.worker->snapshot().measuring_seconds > 0 ? 33 : 100; MsgWaitForMultipleObjectsEx(0, nullptr, timeout, QS_ALLINPUT, MWMO_INPUTAVAILABLE); MSG msg; bool quit = false; diff --git a/src/app/gui/ui.cpp b/src/app/gui/ui.cpp index 5bfc3a8..aa2c611 100644 --- a/src/app/gui/ui.cpp +++ b/src/app/gui/ui.cpp @@ -946,7 +946,9 @@ void run_screen(UiState& s, const OptimizeWorker::Snapshot& run, const UiActions char time[48]; std::snprintf(time, sizeof(time), "%d:%02d left of %s", static_cast(left) / 60, static_cast(left) % 60, duration_text(run.measuring_seconds).c_str()); - ImGui::SameLine(0, em()); + // At the right edge, over the end of the bar it belongs to: the + // step's name changes length from step to step, the time does not. + align_right(ImGui::CalcTextSize(time).x); ImGui::TextUnformatted(time); ImGui::PushStyleColor(ImGuiCol_PlotHistogram, kAccentBright); ImGui::PushStyleColor(ImGuiCol_FrameBg, kInner);