Files
ObsidianDragon/src/ui/windows/xmrig_download_dialog.h
T
DanSandClaude Opus 5.5 575abd4bcc perf(ui): render frames on demand instead of continuously
While theme effects were on (the default) the main loop drew every
frame it could. Where vsync is off (offscreen, some drivers, remote
sessions) an idle wallet spun at ~3,000 fps, 88% of a core; with vsync
it still drew every refresh. Frames are now paced by what needs them:

- full rate while there is input, a held key, a window appearing or
  settling, a smooth scroll or the UI sweep;
- ambient (30 fps focused, 10 fps unfocused) for theme effects, the
  acrylic backdrop, avatar animations and notifications;
- progress (10 fps focused, 4 fps unfocused) for UI that changes
  without input. The updater and bootstrap downloads, console
  commands, lite-wallet operations, seed migration, the loading
  overlay and the lock-screen countdowns ask for it with
  material::RequestProgressFrame();
- otherwise idle, as before: wait for events, with a housekeeping wake
  every 2 s (5 s unfocused).

A software frame cap (0.9x the display refresh) bounds the loop where
vsync is off, and hidden, occluded or minimized windows get no ambient
frames. Reduce Motion freezes the theme-effect clock
(ThemeEffects::setMotionFrozen), so it needs no ambient frames either.

The displayed-balance roll goes through one clamped StepDisplayLerp
(balance_lerp.h). An idle-length frame pushed dt*speed past 1, so the
roll overshot, and under Reduce Motion's speed of 999 it diverged to
inf/NaN. The classic layout had its own copy of the old code.

Idle CPU, Lite build, offscreen (no vsync): 88% -> 5% of a core with
effects on, and 91% -> 3% with effects saved off (with the previous
commit).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 19:52:09 -05:00

448 lines
21 KiB
C++

// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// Modal dialog to download / update the DRG-XMRig miner from the project Gitea, driven by
// util::XmrigUpdater. Two-pane layout (mirrors the node updater / Manage-Portfolio editor): the list
// of miner versions on the LEFT, the selected version's detail + install action on the RIGHT.
// States: Listing -> ReleaseList -> Downloading/Verifying/Extracting -> Done / Failed. Header-only.
// The installed release tag is persisted to settings on success. Installs are refused while the
// miner is running (you can't replace a running binary).
#pragma once
#include <algorithm>
#include <cctype>
#include <memory>
#include <sstream>
#include <string>
#include <vector>
#include "../../app.h"
#include "../../config/settings.h"
#include "../../resources/embedded_resources.h" // resources::getDaemonDirectory()
#include "../../util/i18n.h"
#include "../../util/xmrig_updater.h"
#include "../material/colors.h"
#include "../material/draw_helpers.h"
#include "../material/markdown.h"
#include "../material/type.h"
#include "../theme.h"
#include "release_list_view.h"
#include "imgui.h"
namespace dragonx {
namespace ui {
class XmrigDownloadDialog {
public:
using Progress = util::XmrigUpdater::Progress;
using St = util::XmrigUpdater::State;
static void show(App* app) {
if (!app) return;
s_app = app;
s_open = true;
s_persisted = false;
s_sel = 0;
s_sweep = false;
s_installed_tag = app->settings() ? app->settings()->getXmrigVersion() : std::string();
s_rows.clear();
s_releases.clear();
s_updater = std::make_unique<util::XmrigUpdater>();
s_updater->startListReleases(); // fetch every version up front for the two-pane picker
}
static bool isOpen() { return s_open; }
// Sweep-only: seed fake releases + a forced state so the offline UI sweep can render the modal.
static void sweepSeed(App* app, std::vector<util::XmrigRelease> releases, St state,
const std::string& installedTag) {
s_app = app;
s_open = true;
s_persisted = false;
s_installed_tag = installedTag;
s_sel = 0;
s_sweep = true;
s_releases = std::move(releases);
s_rows.clear();
s_updater.reset();
s_sweepProgress = Progress{};
s_sweepProgress.state = state;
if (!s_releases.empty()) s_sweepProgress.latest_tag = s_releases.front().tag;
s_sweepProgress.installed_tag = installedTag;
s_sweepProgress.percent = 62.0f;
s_sweepProgress.status_text = "drg-xmrig-linux-x64.zip";
}
static void sweepClose() { s_open = false; s_sweep = false; s_releases.clear(); s_rows.clear(); }
static void render() {
if (!s_open || !s_app) {
if (!s_open) s_updater.reset();
return;
}
using namespace material;
const float dp = Layout::dpiScale();
const Progress p = s_sweep ? s_sweepProgress
: (s_updater ? s_updater->getProgress() : Progress{});
const bool active = (p.state == St::Downloading || p.state == St::Verifying || p.state == St::Extracting);
// The updater works on its own thread: keep frames coming while it checks, lists, downloads or
// installs, so progress and the next screen show promptly even when the app would otherwise idle.
if (p.state != St::Unavailable && p.state != St::ReleaseList &&
p.state != St::Done && p.state != St::Failed) {
RequestProgressFrame();
}
OverlayDialogSpec ov;
ov.title = TR("xmrig_update_title");
ov.p_open = active ? nullptr : &s_open;
ov.style = OverlayStyle::BlurFloat;
ov.cardWidth = 880.0f;
ov.cardHeight = std::min(760.0f, ImGui::GetMainViewport()->Size.y * 0.84f / dp);
ov.idSuffix = "xmrigupd";
if (BeginOverlayDialog(ov)) {
switch (p.state) {
case St::Idle:
case St::Checking:
case St::UpToDate:
case St::UpdateAvailable:
case St::Listing: renderCentered(TR("xmrig_loading_releases")); break;
case St::Unavailable: renderUnavailable(p); break;
case St::Failed:
if (s_releases.empty()) { renderListFailed(p); break; }
renderTwoPane(dp, p, active); break;
default: renderTwoPane(dp, p, active); break;
}
EndOverlayDialog();
}
if (!s_open) { s_updater.reset(); s_sweep = false; }
}
private:
static float fullW() { return ImGui::GetContentRegionAvail().x; }
static void renderCentered(const char* msg) {
using namespace material;
ImVec2 av = ImGui::GetContentRegionAvail();
ImFont* f = Type().body2();
ImVec2 ts = f->CalcTextSizeA(f->LegacySize, FLT_MAX, 0, msg);
ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPosX() + std::max(0.0f, (av.x - ts.x) * 0.5f),
ImGui::GetCursorPosY() + av.y * 0.45f));
Type().text(TypeStyle::Body2, msg);
}
static void renderUnavailable(const Progress& p) {
using namespace material;
Type().text(TypeStyle::Subtitle2, TR("xmrig_unavailable_title"));
ImGui::Spacing();
ImGui::TextWrapped("%s", p.status_text.empty() ? TR("xmrig_unavailable_body")
: p.status_text.c_str());
ImGui::Spacing();
if (TactileButton(TR("close"), ImVec2(std::min(fullW(), 200.0f * Layout::dpiScale()), 0)))
s_open = false;
}
static void renderListFailed(const Progress& p) {
using namespace material;
const float bw = std::min(fullW(), 220.0f * Layout::dpiScale());
Type().textColored(TypeStyle::Subtitle2, Error(), TR("xmrig_update_failed"));
ImGui::Spacing();
ImGui::TextWrapped("%s", p.error.empty() ? TR("xmrig_unknown_error") : p.error.c_str());
ImGui::Spacing();
if (TactileButton(TR("retry"), ImVec2(bw, 0)) && s_updater) {
s_releases.clear();
s_rows.clear();
s_updater->startListReleases();
}
ImGui::Spacing();
if (TactileButton(TR("close"), ImVec2(bw, 0))) s_open = false;
}
static void buildRows() {
if (!s_rows.empty() || s_releases.empty()) return;
const std::string token = util::currentXmrigPlatformToken();
s_rows.reserve(s_releases.size());
for (const auto& r : s_releases) {
ReleaseRow row;
row.tag = r.tag;
row.title = r.name;
row.date = r.publishedAt.size() >= 10 ? r.publishedAt.substr(0, 10) : r.publishedAt;
row.prerelease = r.prerelease;
row.hasAsset = util::selectXmrigAsset(r, token) >= 0;
row.installed = !s_installed_tag.empty() && r.tag == s_installed_tag;
s_rows.push_back(std::move(row));
}
}
static std::string cleanNotes(const std::string& body) {
std::istringstream ss(body);
std::string line, out;
while (std::getline(ss, line)) {
size_t a = line.find_first_not_of(" \t");
std::string lt = (a == std::string::npos) ? std::string() : line.substr(a);
std::string low = lt;
std::transform(low.begin(), low.end(), low.begin(),
[](unsigned char c) { return (char)std::tolower(c); });
if (!lt.empty() && lt[0] == '|') break;
if (low.rfind("## checksum", 0) == 0 || low.rfind("#### checksum", 0) == 0 ||
low.rfind("checksums", 0) == 0 || low.rfind("### sha", 0) == 0) break;
out += line;
out += '\n';
}
while (!out.empty() && (out.back() == '\n' || out.back() == '\r' ||
out.back() == ' ' || out.back() == '\t')) out.pop_back();
return out;
}
static void startInstallSelected() {
if (!s_updater || s_app->isPoolMinerRunning()) return; // can't replace a running miner
if (s_sel < 0 || s_sel >= (int)s_releases.size()) return;
const util::XmrigRelease rel = s_releases[s_sel];
s_updater->startInstallRelease(resources::getDaemonDirectory(), rel);
}
// ================= Two-pane: version list (left) + detail/progress (right) =================
static void renderTwoPane(float dp, const Progress& p, bool active) {
using namespace material;
if (!s_sweep && s_releases.empty() && s_updater) s_releases = s_updater->getReleases();
buildRows();
if (s_sel >= (int)s_rows.size()) s_sel = s_rows.empty() ? 0 : (int)s_rows.size() - 1;
ImFont* bodyF = Type().body2();
ImFont* capF = Type().caption();
const bool showFooter = (p.state == St::ReleaseList);
const float footerH = showFooter
? (ImGui::GetFrameHeight() * 1.25f + ImGui::GetStyle().ItemSpacing.y * 2.0f + Layout::spacingSm())
: 0.0f;
const float bodyH = std::max(120.0f * dp, ImGui::GetContentRegionAvail().y - footerH);
const float contentW = ImGui::GetContentRegionAvail().x;
const float gap = Layout::spacingLg();
const float masterW = std::min(std::max(contentW * 0.34f, 240.0f * dp), 340.0f * dp);
const float detailW = contentW - masterW - gap;
// ---- LEFT: borderless list of tactile version cards ----
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
ImGui::BeginChild("##xmrigVersions", ImVec2(masterW, bodyH), false);
{
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, Layout::spacingSm()));
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0, 0, 0, 0));
ImDrawList* mdl = ImGui::GetWindowDrawList();
ImFont* icoF = Type().iconSmall();
const float rowH = Layout::spacingMd() * 2.0f + bodyF->LegacySize + Layout::spacingXs() + capF->LegacySize;
const float rowW = ImGui::GetContentRegionAvail().x;
const float round = 8.0f * dp;
for (int i = 0; i < (int)s_rows.size(); ++i) {
const ReleaseRow& r = s_rows[i];
ImGui::PushID(i);
bool sel = (s_sel == i);
ImVec2 rmn = ImGui::GetCursorScreenPos();
if (ImGui::Selectable("##ver", sel, ImGuiSelectableFlags_SpanAvailWidth, ImVec2(0, rowH))) {
if (!active) s_sel = i;
}
bool hov = ImGui::IsItemHovered();
bool held = ImGui::IsItemActive();
ImVec2 rmx(rmn.x + rowW, rmn.y + rowH);
ImU32 fill = sel ? WithAlpha(Primary(), held ? 55 : 42)
: held ? WithAlpha(OnSurface(), 34)
: hov ? WithAlpha(OnSurface(), 24)
: WithAlpha(OnSurface(), 12);
mdl->AddRectFilled(rmn, rmx, fill, round);
if (sel) mdl->AddRect(rmn, rmx, WithAlpha(Primary(), 150), round, 0, 1.6f * dp);
if (hov || held) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
const char* ico = nullptr; ImU32 statusC = 0; const char* tt = nullptr; const char* lbl = nullptr;
if (r.installed) { ico = ICON_MD_CHECK_CIRCLE; statusC = Success(); tt = TR("xmrig_active_tt"); lbl = TR("upd_installed_badge"); }
else if (r.prerelease) { ico = ICON_MD_SCIENCE; statusC = Warning(); tt = TR("xmrig_prerelease_tt"); lbl = TR("upd_prerelease"); }
else if (i == 0) { ico = ICON_MD_NEW_RELEASES; statusC = Primary(); tt = TR("xmrig_latest_tt"); lbl = TR("xmrig_latest_badge"); }
float rightLimit = rmx.x - Layout::spacingMd();
if (ico) {
float cs = bodyF->LegacySize * 1.3f;
ImVec2 cp(rmx.x - Layout::spacingMd() - cs, (rmn.y + rmx.y) * 0.5f - cs * 0.5f);
mdl->AddText(icoF, cs, cp, statusC, ico);
if (tt && hov && ImGui::IsMouseHoveringRect(cp, ImVec2(cp.x + cs, cp.y + cs)))
Tooltip("%s", tt);
rightLimit = cp.x - Layout::spacingSm();
}
float x = rmn.x + Layout::spacingMd();
float tagY = rmn.y + Layout::spacingMd();
mdl->AddText(bodyF, bodyF->LegacySize, ImVec2(x, tagY),
r.hasAsset ? OnSurface() : OnSurfaceDisabled(), r.tag.c_str());
if (lbl) {
float bx = x + bodyF->CalcTextSizeA(bodyF->LegacySize, FLT_MAX, 0, r.tag.c_str()).x + Layout::spacingSm();
float by = tagY + (bodyF->LegacySize - capF->LegacySize) * 0.5f;
if (bx + capF->CalcTextSizeA(capF->LegacySize, FLT_MAX, 0, lbl).x <= rightLimit)
mdl->AddText(capF, capF->LegacySize, ImVec2(bx, by), statusC, lbl);
}
if (!r.date.empty())
mdl->AddText(capF, capF->LegacySize,
ImVec2(x, rmx.y - Layout::spacingMd() - capF->LegacySize),
OnSurfaceMedium(), r.date.c_str());
ImGui::PopID();
}
ImGui::PopStyleColor(3);
ImGui::PopStyleVar();
}
ImGui::EndChild();
ImGui::PopStyleVar();
ImGui::SameLine(0.0f, gap);
// ---- RIGHT: detail / progress / done / failed for the selected version ----
ImGui::BeginChild("##xmrigDetail", ImVec2(detailW, bodyH), false);
switch (p.state) {
case St::ReleaseList: renderDetail(dp, p); break;
case St::Downloading:
case St::Verifying:
case St::Extracting: renderProgress(dp, p); break;
case St::Done: renderDone(dp, p); break;
case St::Failed: renderFailed(dp, p); break;
default: break;
}
ImGui::EndChild();
if (showFooter) {
ImGui::Spacing();
const float bw = 170.0f * dp, bh = ImGui::GetFrameHeight() * 1.25f;
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, ImGui::GetContentRegionAvail().x - bw));
if (TactileButton(TR("close"), ImVec2(bw, bh))) s_open = false;
}
}
static void renderDetail(float dp, const Progress&) {
using namespace material;
if (s_sel < 0 || s_sel >= (int)s_releases.size() || s_sel >= (int)s_rows.size()) return;
const util::XmrigRelease& rel = s_releases[s_sel];
const ReleaseRow& row = s_rows[s_sel];
const bool haveInstalled = !s_installed_tag.empty();
const bool downgrade = !row.installed && s_sel != 0 && haveInstalled;
const bool mining = s_app->isPoolMinerRunning();
const char* noteStr = mining ? TR("xmrig_stop_mining_first")
: !row.hasAsset ? TR("upd_no_build_platform")
: downgrade ? TR("xmrig_downgrade_note")
: TR("xmrig_verify_note");
const char* label = row.installed ? TR("xmrig_reinstall")
: (s_sel == 0) ? TR("xmrig_download_install")
: TR("xmrig_install_this");
const float btnH = ImGui::GetFrameHeight() * 1.15f;
const float noteH = ImGui::CalcTextSize(noteStr, nullptr, false, ImGui::GetContentRegionAvail().x).y;
const float reserve = noteH + btnH + Layout::spacingMd() * 3.0f;
const float infoH = std::max(120.0f * dp, ImGui::GetContentRegionAvail().y - reserve);
// ---- Info card: ONE rounded Material surface (version header + release notes markdown) ----
ImGui::PushStyleColor(ImGuiCol_ChildBg, WithAlpha(OnSurface(), 22));
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 10.0f * dp);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(Layout::spacingLg(), Layout::spacingLg()));
ImGui::BeginChild("##xmrigInfo", ImVec2(0, infoH), ImGuiChildFlags_AlwaysUseWindowPadding);
{
Type().text(TypeStyle::H6, rel.tag.c_str());
if (row.installed)
Type().textColored(TypeStyle::Subtitle2, Success(), TR("xmrig_status_current"));
else if (s_sel == 0)
Type().textColored(TypeStyle::Subtitle2, Primary(),
haveInstalled ? TR("xmrig_update_available") : TR("xmrig_status_latest"));
else if (haveInstalled)
Type().textColored(TypeStyle::Subtitle2, Warning(), TR("xmrig_status_older"));
else
Type().text(TypeStyle::Subtitle2, rel.name.empty() ? rel.tag.c_str() : rel.name.c_str());
const std::string installed = s_installed_tag.empty() ? std::string(TR("xmrig_none"))
: s_installed_tag;
ImGui::Spacing();
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(),
(std::string(TR("xmrig_installed")) + " " + installed).c_str());
if (!row.date.empty())
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(),
(std::string(TR("xmrig_released")) + " " + row.date).c_str());
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
const std::string notes = cleanNotes(rel.body);
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0));
ImGui::BeginChild("##xmrigNotes", ImVec2(0, std::max(0.0f, ImGui::GetContentRegionAvail().y)), false);
if (notes.empty())
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("xmrig_no_notes"));
else
RenderMarkdown(notes);
ImGui::EndChild();
ImGui::PopStyleColor();
}
ImGui::EndChild();
ImGui::PopStyleVar(2);
ImGui::PopStyleColor();
// ---- Below the info card: verify / stop-mining note + install button (centered, text-fit) ----
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x);
Type().textColored(TypeStyle::Caption, (mining || downgrade) ? Warning() : OnSurfaceMedium(), noteStr);
ImGui::PopTextWrapPos();
ImGui::Spacing();
const float bw = ImGui::CalcTextSize(label).x + ImGui::GetStyle().FramePadding.x * 2.0f + Layout::spacingLg();
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, (ImGui::GetContentRegionAvail().x - bw) * 0.5f));
ImGui::BeginDisabled(!row.hasAsset || mining);
if (TactileButton(label, ImVec2(bw, btnH))) startInstallSelected();
ImGui::EndDisabled();
}
static void renderProgress(float dp, const Progress& p) {
using namespace material;
const char* title = p.state == St::Downloading ? TR("xmrig_downloading")
: p.state == St::Verifying ? TR("xmrig_verifying")
: TR("xmrig_installing");
Type().text(TypeStyle::Subtitle2, title);
ImGui::Spacing();
material::ProgressBar(fullW(), 8.0f * dp, p.percent / 100.0f);
ImGui::Spacing();
ImGui::TextWrapped("%s", p.status_text.c_str());
ImGui::Spacing();
if (TactileButton(TR("cancel"), ImVec2(fullW(), 0))) {
if (s_updater) s_updater->cancel();
s_open = false;
}
}
static void renderDone(float, const Progress& p) {
using namespace material;
if (!s_persisted) {
if (s_app->settings() && !p.latest_tag.empty()) {
s_app->settings()->setXmrigVersion(p.latest_tag);
s_app->settings()->save();
}
s_persisted = true;
}
Type().textColored(TypeStyle::Subtitle2, Success(), TR("xmrig_installed_ok"));
ImGui::Spacing();
ImGui::Text("%s %s", TR("xmrig_version"), p.latest_tag.c_str());
ImGui::Spacing();
if (TactileButton(TR("close"), ImVec2(fullW(), 0))) s_open = false;
}
static void renderFailed(float, const Progress& p) {
using namespace material;
Type().textColored(TypeStyle::Subtitle2, Error(), TR("xmrig_update_failed"));
ImGui::Spacing();
ImGui::TextWrapped("%s", p.error.empty() ? TR("xmrig_unknown_error") : p.error.c_str());
ImGui::Spacing();
ImGui::BeginDisabled(s_app->isPoolMinerRunning());
if (TactileButton(TR("retry"), ImVec2(fullW(), 0))) startInstallSelected();
ImGui::EndDisabled();
ImGui::Spacing();
if (TactileButton(TR("close"), ImVec2(fullW(), 0))) s_open = false;
}
static inline bool s_open = false;
static inline bool s_persisted = false;
static inline bool s_sweep = false;
static inline int s_sel = 0;
static inline std::string s_installed_tag;
static inline std::vector<ReleaseRow> s_rows; // built once per listing (UI rows)
static inline std::vector<util::XmrigRelease> s_releases; // matching releases (install by index)
static inline Progress s_sweepProgress;
static inline App* s_app = nullptr;
static inline std::unique_ptr<util::XmrigUpdater> s_updater;
};
} // namespace ui
} // namespace dragonx