From 280a71e9735554d27bace7b0b258f1b3f5669dc7 Mon Sep 17 00:00:00 2001 From: DanS Date: Thu, 2 Jul 2026 16:40:40 -0500 Subject: [PATCH] =?UTF-8?q?refactor(audit):=20batch=203=20=E2=80=94=20deco?= =?UTF-8?q?mpose=20App::update()=20+=20collapse=20acrylic=20fork?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three P1 structural refactors from the audit; no behavior change. 1. Extract the ~180-line daemon-stdout rescan/witness parser out of the ~790-line App::update() into a pure, static, unit-testable NetworkRefreshService::parseDaemonRescanOutput() returning a DaemonRescanScan result struct (sits next to the existing static parse* siblings). The scanning half moved verbatim; App::update() keeps the state-application half, now reading scan.* fields. Adds testParseDaemonRescanOutput() covering all six daemon signals + edge cases against fixture log snippets — previously untestable without a live daemon. 2. Extract the ~95-line global keyboard-shortcut block (Ctrl+, / theme cycle / F5 / low-spec / effects / gradient / wizard) out of App::update() into App::handleGlobalShortcuts(). 3. Collapse the ImGuiAcrylic frontend fork: the GLAD and DX11 namespaces were ~375 lines of semantically-identical code (the frontend makes zero direct GL/DX calls — all backend work delegates to AcrylicMaterial, which has a backend per API). Extended the single frontend's guard to #if defined(DRAGONX_HAS_GLAD) || defined(DRAGONX_USE_DX11) and deleted the DX11 duplicate, leaving the no-backend stub. Kills the Windows/DX11 drift hazard (acrylic frontend changes now made once). Verified: full-node + Lite build clean; ctest 1/1 (incl. the new parser tests); source-hygiene clean; and the collapsed acrylic path was compile-verified under DX11 via the mingw-w64 Windows cross-build (ObsidianDragon.exe links). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/app.cpp | 224 ++----------- src/app.h | 3 + src/services/network_refresh_service.cpp | 185 ++++++++++ src/services/network_refresh_service.h | 17 + src/ui/effects/imgui_acrylic.cpp | 410 +---------------------- tests/test_phase4.cpp | 94 ++++++ 6 files changed, 331 insertions(+), 602 deletions(-) diff --git a/src/app.cpp b/src/app.cpp index 080abd0..4b54642 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -854,195 +854,18 @@ void App::update() // be blocked for minutes on a getrescaninfo call (it waits on cs_main while the daemon // holds it for a witness rebuild). On the worker, the parse queued behind that and the // rescan/witness progress froze. Inline, progress updates every frame regardless. - const std::string output = std::move(newOutput); - bool foundRescan = false; - bool finished = false; - float rescanPct = 0.0f; - std::string lastStatus; - - // Sapling witness rebuild progress. Two daemon signals: - // "Setting Initial Sapling Witness for tx , of " (the common one — - // one per wallet tx; is the stable per-tx key, the total tx count) - // "Building Witnesses for block complete, remaining" (rarer) - bool foundWitness = false; - float witnessPct = -1.0f; // -1 = no fraction parsed this batch - int witnessRemaining = -1; - int witnessReadTotal = -1; // new daemon: the in " / " - bool witnessRebuilt = false; // new daemon's completion line seen this batch - std::vector witnessTxids; // distinct-tx keys seen this batch - int witnessTotalTxs = -1; // max N seen this batch - - size_t pos = 0; - while (pos < output.size()) { - size_t eol = output.find('\n', pos); - if (eol == std::string::npos) eol = output.size(); - std::string line = output.substr(pos, eol - pos); - pos = eol + 1; - - if (line.find("Rescanning from height") != std::string::npos || - line.find("Rescanning last") != std::string::npos) { - foundRescan = true; - lastStatus = line; - } - - auto stillIdx = line.find("Still rescanning"); - if (stillIdx != std::string::npos) { - foundRescan = true; - auto progIdx = line.find("Progress="); - if (progIdx != std::string::npos) { - size_t numStart = progIdx + 9; - size_t numEnd = numStart; - while (numEnd < line.size() && (std::isdigit(line[numEnd]) || line[numEnd] == '.')) { - numEnd++; - } - if (numEnd > numStart) { - try { rescanPct = std::stof(line.substr(numStart, numEnd - numStart)) * 100.0f; } catch (...) {} - } - } - lastStatus = line; - } - - auto rescIdx = line.find("Rescanning..."); - if (rescIdx != std::string::npos) { - foundRescan = true; - auto pctIdx = line.find('%'); - if (pctIdx != std::string::npos && pctIdx > 0) { - size_t numEnd = pctIdx; - size_t numStart = numEnd; - while (numStart > 0 && (std::isdigit(line[numStart - 1]) || line[numStart - 1] == '.')) { - numStart--; - } - if (numStart < numEnd) { - try { rescanPct = std::stof(line.substr(numStart, numEnd - numStart)); } catch (...) {} - } - } - lastStatus = line; - } - - if (line.find("Done rescanning") != std::string::npos || - line.find("Rescan complete") != std::string::npos) { - finished = true; - } - - // This daemon prints no "Done rescanning" line; instead it logs the rescan - // benchmark timing exactly when the scan finishes, e.g.: - // "... rescan 16760577ms" - // Match the lowercase " rescan " bench category ending in "ms" (the - // progress lines are "Still rescanning"/"Rescanning...", which never end in ms). - if (line.find(" rescan ") != std::string::npos) { - std::string t = line; - while (!t.empty() && (t.back() == '\r' || t.back() == '\n' || t.back() == ' ')) - t.pop_back(); - if (t.size() >= 3 && t.compare(t.size() - 2, 2, "ms") == 0 && - std::isdigit(static_cast(t[t.size() - 3]))) { - finished = true; - } - } - - // Sapling note witness rebuild: "Building Witnesses for block - // complete, remaining". The float before "complete" is a 0..1 fraction. - auto witIdx = line.find("Building Witnesses for block"); - if (witIdx != std::string::npos) { - foundWitness = true; - auto compIdx = line.find(" complete", witIdx); - if (compIdx != std::string::npos) { - size_t numEnd = compIdx, numStart = numEnd; - while (numStart > 0 && (std::isdigit((unsigned char)line[numStart - 1]) || - line[numStart - 1] == '.')) numStart--; - if (numStart < numEnd) { - try { witnessPct = std::stof(line.substr(numStart, numEnd - numStart)) * 100.0f; } catch (...) {} - } - } - auto remIdx = line.find(" remaining", witIdx); - if (remIdx != std::string::npos) { - size_t numEnd = remIdx, numStart = numEnd; - while (numStart > 0 && std::isdigit((unsigned char)line[numStart - 1])) numStart--; - if (numStart < numEnd) { - try { witnessRemaining = std::stoi(line.substr(numStart, numEnd - numStart)); } catch (...) {} - } - } - lastStatus = line; - } - - // Newer multi-threaded daemon: "Reading blocks for witness rebuild: / ". - // This replaced the per-block "Building Witnesses" line and is an exact done/total - // count, so capture BOTH (total drives the denominator directly — no peak anchoring). - auto rdIdx = line.find("Reading blocks for witness rebuild:"); - if (rdIdx != std::string::npos) { - foundWitness = true; - auto slash = line.find('/', rdIdx); - if (slash != std::string::npos) { - size_t dEnd = slash; - while (dEnd > rdIdx && !std::isdigit((unsigned char)line[dEnd - 1])) dEnd--; - size_t dStart = dEnd; - while (dStart > rdIdx && std::isdigit((unsigned char)line[dStart - 1])) dStart--; - size_t tStart = slash + 1; - while (tStart < line.size() && !std::isdigit((unsigned char)line[tStart])) tStart++; - size_t tEnd = tStart; - while (tEnd < line.size() && std::isdigit((unsigned char)line[tEnd])) tEnd++; - if (dStart < dEnd && tStart < tEnd) { - try { - long done = std::stol(line.substr(dStart, dEnd - dStart)); - long total = std::stol(line.substr(tStart, tEnd - tStart)); - if (total > 0 && done >= 0 && done <= total) { - witnessRemaining = static_cast(total - done); - witnessReadTotal = static_cast(total); - } - } catch (...) {} - } - } - lastStatus = line; - } - - // New daemon's witness-rebuild completion line: - // "... rebuilt note witness cache(s) to height in ms using thread(s)". - // The parallel rebuild logs no progress, so on completion snap the bar to 100%. - if (line.find("note witness cache") != std::string::npos && - line.find("thread(s)") != std::string::npos) { - foundWitness = true; - witnessRebuilt = true; - lastStatus = line; - } - - // Primary signal: "Setting Initial Sapling Witness for tx , of ". - // is the tx's slot in an unordered map (bounces — useless as progress), so - // we key on (one per tx) and count distinct txs against . Extract the - // hash between "for tx " and the following comma, and N after " of ". - { - auto setIdx = line.find("Setting Initial Sapling Witness for tx "); - if (setIdx != std::string::npos) { - foundWitness = true; - size_t hStart = setIdx + std::string("Setting Initial Sapling Witness for tx ").size(); - size_t comma = line.find(',', hStart); - if (comma != std::string::npos && comma > hStart) { - witnessTxids.push_back(line.substr(hStart, comma - hStart)); - auto ofIdx = line.find(" of ", comma); - if (ofIdx != std::string::npos) { - size_t nStart = ofIdx + 4, nEnd = nStart; - while (nEnd < line.size() && std::isdigit((unsigned char)line[nEnd])) nEnd++; - if (nEnd > nStart) { - try { - int n = std::stoi(line.substr(nStart, nEnd - nStart)); - if (n > witnessTotalTxs) witnessTotalTxs = n; - } catch (...) {} - } - } - } - lastStatus = line; - } - } - } + const auto scan = services::NetworkRefreshService::parseDaemonRescanOutput(std::move(newOutput)); // Apply results directly — we are already on the main thread. - const std::string& status = lastStatus; - if (finished) { + const std::string& status = scan.lastStatus; + if (scan.finished) { if (state_.sync.rescanning) { ui::Notifications::instance().success("Blockchain rescan complete"); } // Witness rebuild finishes with the rescan it's part of. resetWitnessRescanProgress(); state_.sync.rescan_progress = 1.0f; - } else if (foundWitness) { + } else if (scan.foundWitness) { // Witness rebuild is the tail phase of a rescan — keep rescanning set so // the broader gating holds, but surface the witness-specific progress. state_.sync.rescanning = true; @@ -1056,8 +879,8 @@ void App::update() // never drops back to the initial pass. Otherwise an interleaved initial // line would flip the phase back and reset the bar every batch (thrash). int phase = state_.sync.witness_phase; - if (witnessRemaining >= 0 || witnessRebuilt) phase = 2; - else if (!witnessTxids.empty() && phase < 2) phase = 1; + if (scan.witnessRemaining >= 0 || scan.witnessRebuilt) phase = 2; + else if (!scan.witnessTxids.empty() && phase < 2) phase = 1; // Entering a higher sub-phase → reset its progress + accumulators so the // bar restarts from 0 (the two phases have different scales). Upgrade-only, @@ -1072,24 +895,24 @@ void App::update() } if (phase == 2) { - state_.sync.witness_remaining = witnessRemaining; + state_.sync.witness_remaining = scan.witnessRemaining; float p = -1.0f; - if (witnessRebuilt) { + if (scan.witnessRebuilt) { // Parallel rebuild finished (it logs no progress) — snap to 100%. p = 1.0f; state_.sync.witness_remaining = 0; - } else if (witnessReadTotal > 0) { + } else if (scan.witnessReadTotal > 0) { // New daemon: exact done/total. Use the reported total as the // denominator directly (no peak anchoring) for a smooth 0..1 sweep. - int done = witnessReadTotal - (witnessRemaining >= 0 ? witnessRemaining : 0); - p = static_cast(done) / static_cast(witnessReadTotal); - } else if (witnessRemaining >= 0) { + int done = scan.witnessReadTotal - (scan.witnessRemaining >= 0 ? scan.witnessRemaining : 0); + p = static_cast(done) / static_cast(scan.witnessReadTotal); + } else if (scan.witnessRemaining >= 0) { // Older daemon: bare " remaining" with no total — anchor the % to // the largest remaining seen (its first/largest value ≈ the full span). - if (witnessRemaining > witness_rebuild_total_blocks_) - witness_rebuild_total_blocks_ = witnessRemaining; + if (scan.witnessRemaining > witness_rebuild_total_blocks_) + witness_rebuild_total_blocks_ = scan.witnessRemaining; if (witness_rebuild_total_blocks_ > 0) - p = 1.0f - static_cast(witnessRemaining) / + p = 1.0f - static_cast(scan.witnessRemaining) / static_cast(witness_rebuild_total_blocks_); } if (p >= 0.0f) { @@ -1101,8 +924,8 @@ void App::update() // Initial pass: count DISTINCT witnessed txs / total. The set only // grows (dedups the daemon's double-prints), so it's monotonic; the // raw "" can't be used — it's an unordered-map slot that bounces. - if (witnessTotalTxs > witness_total_txs_) witness_total_txs_ = witnessTotalTxs; - for (const auto& txid : witnessTxids) witness_seen_txids_.insert(txid); + if (scan.witnessTotalTxs > witness_total_txs_) witness_total_txs_ = scan.witnessTotalTxs; + for (const auto& txid : scan.witnessTxids) witness_seen_txids_.insert(txid); if (witness_total_txs_ > 0 && !witness_seen_txids_.empty()) { float p = static_cast(witness_seen_txids_.size()) / static_cast(witness_total_txs_); @@ -1114,14 +937,14 @@ void App::update() if (!status.empty()) { state_.sync.rescan_status = status; } - } else if (foundRescan) { + } else if (scan.foundRescan) { state_.sync.rescanning = true; // Reading "Still rescanning" straight from the daemon log is hard proof the // rescan is genuinely running — confirm it so the getrescaninfo poll's // completion check can fire even if it never caught a warmup error. rescan_confirmed_active_ = true; - if (rescanPct > 0.0f) { - state_.sync.rescan_progress = rescanPct / 100.0f; + if (scan.rescanPct > 0.0f) { + state_.sync.rescan_progress = scan.rescanPct / 100.0f; } if (!status.empty()) { state_.sync.rescan_status = status; @@ -1290,6 +1113,13 @@ void App::update() } } + handleGlobalShortcuts(); +} + +void App::handleGlobalShortcuts() +{ + ImGuiIO& io = ImGui::GetIO(); + // Keyboard shortcut: Ctrl+, to open Settings page if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_Comma)) { setCurrentPage(ui::NavPage::Settings); diff --git a/src/app.h b/src/app.h index ab7dbf8..6c75bee 100644 --- a/src/app.h +++ b/src/app.h @@ -443,6 +443,9 @@ private: friend class AppDaemonLifecycleRuntime; friend class AppDaemonLifecycleTaskContext; + // Global keyboard-shortcut handling, dispatched once per frame from update(). + void handleGlobalShortcuts(); + bool sendStopCommandSafely(rpc::RPCClient& client, const char* context); void maybeFinishTransactionSendProgress(); void upsertPendingSendTransaction(const std::string& opid, diff --git a/src/services/network_refresh_service.cpp b/src/services/network_refresh_service.cpp index fe94507..2865107 100644 --- a/src/services/network_refresh_service.cpp +++ b/src/services/network_refresh_service.cpp @@ -2,6 +2,7 @@ #include "../util/logger.h" #include +#include #include #include #include @@ -1336,6 +1337,190 @@ void NetworkRefreshService::resetJobs() } } +NetworkRefreshService::DaemonRescanScan NetworkRefreshService::parseDaemonRescanOutput(const std::string& output) +{ + DaemonRescanScan scan; + bool& foundRescan = scan.foundRescan; + bool& finished = scan.finished; + float& rescanPct = scan.rescanPct; + std::string& lastStatus = scan.lastStatus; + + // Sapling witness rebuild progress. Two daemon signals: + // "Setting Initial Sapling Witness for tx , of " (the common one — + // one per wallet tx; is the stable per-tx key, the total tx count) + // "Building Witnesses for block complete, remaining" (rarer) + bool& foundWitness = scan.foundWitness; + float& witnessPct = scan.witnessPct; // -1 = no fraction parsed this batch + int& witnessRemaining = scan.witnessRemaining; + int& witnessReadTotal = scan.witnessReadTotal; // new daemon: the in " / " + bool& witnessRebuilt = scan.witnessRebuilt; // new daemon's completion line seen this batch + std::vector& witnessTxids = scan.witnessTxids; // distinct-tx keys seen this batch + int& witnessTotalTxs = scan.witnessTotalTxs; // max N seen this batch + + size_t pos = 0; + while (pos < output.size()) { + size_t eol = output.find('\n', pos); + if (eol == std::string::npos) eol = output.size(); + std::string line = output.substr(pos, eol - pos); + pos = eol + 1; + + if (line.find("Rescanning from height") != std::string::npos || + line.find("Rescanning last") != std::string::npos) { + foundRescan = true; + lastStatus = line; + } + + auto stillIdx = line.find("Still rescanning"); + if (stillIdx != std::string::npos) { + foundRescan = true; + auto progIdx = line.find("Progress="); + if (progIdx != std::string::npos) { + size_t numStart = progIdx + 9; + size_t numEnd = numStart; + while (numEnd < line.size() && (std::isdigit(line[numEnd]) || line[numEnd] == '.')) { + numEnd++; + } + if (numEnd > numStart) { + try { rescanPct = std::stof(line.substr(numStart, numEnd - numStart)) * 100.0f; } catch (...) {} + } + } + lastStatus = line; + } + + auto rescIdx = line.find("Rescanning..."); + if (rescIdx != std::string::npos) { + foundRescan = true; + auto pctIdx = line.find('%'); + if (pctIdx != std::string::npos && pctIdx > 0) { + size_t numEnd = pctIdx; + size_t numStart = numEnd; + while (numStart > 0 && (std::isdigit(line[numStart - 1]) || line[numStart - 1] == '.')) { + numStart--; + } + if (numStart < numEnd) { + try { rescanPct = std::stof(line.substr(numStart, numEnd - numStart)); } catch (...) {} + } + } + lastStatus = line; + } + + if (line.find("Done rescanning") != std::string::npos || + line.find("Rescan complete") != std::string::npos) { + finished = true; + } + + // This daemon prints no "Done rescanning" line; instead it logs the rescan + // benchmark timing exactly when the scan finishes, e.g.: + // "... rescan 16760577ms" + // Match the lowercase " rescan " bench category ending in "ms" (the + // progress lines are "Still rescanning"/"Rescanning...", which never end in ms). + if (line.find(" rescan ") != std::string::npos) { + std::string t = line; + while (!t.empty() && (t.back() == '\r' || t.back() == '\n' || t.back() == ' ')) + t.pop_back(); + if (t.size() >= 3 && t.compare(t.size() - 2, 2, "ms") == 0 && + std::isdigit(static_cast(t[t.size() - 3]))) { + finished = true; + } + } + + // Sapling note witness rebuild: "Building Witnesses for block + // complete, remaining". The float before "complete" is a 0..1 fraction. + auto witIdx = line.find("Building Witnesses for block"); + if (witIdx != std::string::npos) { + foundWitness = true; + auto compIdx = line.find(" complete", witIdx); + if (compIdx != std::string::npos) { + size_t numEnd = compIdx, numStart = numEnd; + while (numStart > 0 && (std::isdigit((unsigned char)line[numStart - 1]) || + line[numStart - 1] == '.')) numStart--; + if (numStart < numEnd) { + try { witnessPct = std::stof(line.substr(numStart, numEnd - numStart)) * 100.0f; } catch (...) {} + } + } + auto remIdx = line.find(" remaining", witIdx); + if (remIdx != std::string::npos) { + size_t numEnd = remIdx, numStart = numEnd; + while (numStart > 0 && std::isdigit((unsigned char)line[numStart - 1])) numStart--; + if (numStart < numEnd) { + try { witnessRemaining = std::stoi(line.substr(numStart, numEnd - numStart)); } catch (...) {} + } + } + lastStatus = line; + } + + // Newer multi-threaded daemon: "Reading blocks for witness rebuild: / ". + // This replaced the per-block "Building Witnesses" line and is an exact done/total + // count, so capture BOTH (total drives the denominator directly — no peak anchoring). + auto rdIdx = line.find("Reading blocks for witness rebuild:"); + if (rdIdx != std::string::npos) { + foundWitness = true; + auto slash = line.find('/', rdIdx); + if (slash != std::string::npos) { + size_t dEnd = slash; + while (dEnd > rdIdx && !std::isdigit((unsigned char)line[dEnd - 1])) dEnd--; + size_t dStart = dEnd; + while (dStart > rdIdx && std::isdigit((unsigned char)line[dStart - 1])) dStart--; + size_t tStart = slash + 1; + while (tStart < line.size() && !std::isdigit((unsigned char)line[tStart])) tStart++; + size_t tEnd = tStart; + while (tEnd < line.size() && std::isdigit((unsigned char)line[tEnd])) tEnd++; + if (dStart < dEnd && tStart < tEnd) { + try { + long done = std::stol(line.substr(dStart, dEnd - dStart)); + long total = std::stol(line.substr(tStart, tEnd - tStart)); + if (total > 0 && done >= 0 && done <= total) { + witnessRemaining = static_cast(total - done); + witnessReadTotal = static_cast(total); + } + } catch (...) {} + } + } + lastStatus = line; + } + + // New daemon's witness-rebuild completion line: + // "... rebuilt note witness cache(s) to height in ms using thread(s)". + // The parallel rebuild logs no progress, so on completion snap the bar to 100%. + if (line.find("note witness cache") != std::string::npos && + line.find("thread(s)") != std::string::npos) { + foundWitness = true; + witnessRebuilt = true; + lastStatus = line; + } + + // Primary signal: "Setting Initial Sapling Witness for tx , of ". + // is the tx's slot in an unordered map (bounces — useless as progress), so + // we key on (one per tx) and count distinct txs against . Extract the + // hash between "for tx " and the following comma, and N after " of ". + { + auto setIdx = line.find("Setting Initial Sapling Witness for tx "); + if (setIdx != std::string::npos) { + foundWitness = true; + size_t hStart = setIdx + std::string("Setting Initial Sapling Witness for tx ").size(); + size_t comma = line.find(',', hStart); + if (comma != std::string::npos && comma > hStart) { + witnessTxids.push_back(line.substr(hStart, comma - hStart)); + auto ofIdx = line.find(" of ", comma); + if (ofIdx != std::string::npos) { + size_t nStart = ofIdx + 4, nEnd = nStart; + while (nEnd < line.size() && std::isdigit((unsigned char)line[nEnd])) nEnd++; + if (nEnd > nStart) { + try { + int n = std::stoi(line.substr(nStart, nEnd - nStart)); + if (n > witnessTotalTxs) witnessTotalTxs = n; + } catch (...) {} + } + } + } + lastStatus = line; + } + } + } + + return scan; +} + NetworkRefreshService::DispatchTicket NetworkRefreshService::beginDispatch( Job job, std::size_t queuedWork, std::size_t maxQueuedWork) { diff --git a/src/services/network_refresh_service.h b/src/services/network_refresh_service.h index d57041d..fd779e4 100644 --- a/src/services/network_refresh_service.h +++ b/src/services/network_refresh_service.h @@ -293,6 +293,23 @@ public: static OperationStatusPollResult parseOperationStatusPoll(const nlohmann::json& result, const std::vector& requestedOpids); + struct DaemonRescanScan { + bool foundRescan = false; + bool finished = false; + float rescanPct = 0.0f; + std::string lastStatus; + bool foundWitness = false; + float witnessPct = -1.0f; + int witnessRemaining = -1; + int witnessReadTotal = -1; + bool witnessRebuilt = false; + std::vector witnessTxids; + int witnessTotalTxs = -1; + }; + // Pure line-by-line scan of newly-appended dragonxd stdout for rescan + Sapling + // witness-rebuild progress. No I/O, no state — unit-tested against fixture log snippets. + static DaemonRescanScan parseDaemonRescanOutput(const std::string& output); + static void applyConnectionInfoResult(WalletState& state, const ConnectionInfoResult& result); static void applyWalletEncryptionResult(WalletState& state, const WalletEncryptionResult& result); static void applyConnectionInitResult(WalletState& state, const ConnectionInitResult& result); diff --git a/src/ui/effects/imgui_acrylic.cpp b/src/ui/effects/imgui_acrylic.cpp index db64a90..1ab77e1 100644 --- a/src/ui/effects/imgui_acrylic.cpp +++ b/src/ui/effects/imgui_acrylic.cpp @@ -107,7 +107,10 @@ int MatchPreset(AcrylicQuality q, float blur) } } } } // namespace dragonx::ui::effects::ImGuiAcrylic -#ifdef DRAGONX_HAS_GLAD +// Single ImGuiAcrylic frontend for BOTH GL (GLAD) and DX11: the frontend makes no +// direct GL/DX calls — every backend op is delegated to AcrylicMaterial, which has a +// backend for each API — so it must not fork per-backend. +#if defined(DRAGONX_HAS_GLAD) || defined(DRAGONX_USE_DX11) namespace dragonx { namespace ui { @@ -638,408 +641,6 @@ void ApplyBlurAmount(float blur) } // namespace ui } // namespace dragonx -#endif // DRAGONX_HAS_GLAD - -// ============================================================================ -// Stub Implementations (No GLAD) -// ============================================================================ - -#ifndef DRAGONX_HAS_GLAD - -#ifdef DRAGONX_USE_DX11 - -// ============================================================================ -// DX11 Implementation — mirrors the GLAD version above, delegating to -// AcrylicMaterial which now has a full DX11 backend. -// ============================================================================ - -namespace dragonx { -namespace ui { -namespace effects { - -namespace ImGuiAcrylic { - -static bool s_initialized = false; -static int s_viewportWidth = 0; -static int s_viewportHeight = 0; -static bool s_backgroundCaptured = false; - -struct AcrylicWindowState { - ImVec2 windowPos; - ImVec2 windowSize; - AcrylicParams params; - bool active; -}; -static AcrylicWindowState s_currentWindow = {}; - -bool Init() -{ - if (s_initialized) return true; - - AcrylicMaterial& acrylic = getAcrylicMaterial(); - if (!acrylic.init()) { - DEBUG_LOGF("ImGuiAcrylic DX11: Failed to initialize acrylic material\n"); - return false; - } - - s_initialized = true; - DEBUG_LOGF("ImGuiAcrylic DX11: Initialized successfully\n"); - return true; -} - -void Shutdown() -{ - if (!s_initialized) return; - getAcrylicMaterial().shutdown(); - s_initialized = false; -} - -bool IsAvailable() -{ - return s_initialized && getAcrylicMaterial().isInitialized(); -} - -void BeginFrame(int width, int height) -{ - if (!s_initialized) return; - - if (width != s_viewportWidth || height != s_viewportHeight) { - s_viewportWidth = width; - s_viewportHeight = height; - getAcrylicMaterial().resize(width, height); - } - - // Capture the first few frames to ensure a clean initial blur, - // then stop. Resize / theme change already sets dirtyFrames_ via markBackgroundDirty(). - static int s_warmupFrames = 3; - if (s_warmupFrames > 0) { - getAcrylicMaterial().markBackgroundDirty(); - --s_warmupFrames; - } - - s_backgroundCaptured = false; -} - -void CaptureBackground() -{ - if (!s_initialized || s_backgroundCaptured) return; - getAcrylicMaterial().captureBackground(); - s_backgroundCaptured = true; -} - -void InvalidateCapture() -{ - if (!s_initialized) return; - getAcrylicMaterial().markBackgroundDirty(); -} - -static void BackgroundCaptureCallback(const ImDrawList*, const ImDrawCmd*) -{ - if (!s_initialized) return; - getAcrylicMaterial().captureBackgroundDirect(); - s_backgroundCaptured = true; -} - -ImDrawCallback GetBackgroundCaptureCallback() -{ - return BackgroundCaptureCallback; -} - -void DrawAcrylicRect(ImDrawList* drawList, - const ImVec2& pMin, const ImVec2& pMax, - const AcrylicParams& params, - float rounding) -{ - if (!drawList) return; - - if (!IsAvailable() || !IsEnabled() || !params.enabled) { - drawList->AddRectFilled(pMin, pMax, - ImGui::ColorConvertFloat4ToU32(params.fallbackColor), rounding); - return; - } - - getAcrylicMaterial().drawRect(drawList, pMin, pMax, params, rounding); -} - -void DrawAcrylicRect(ImDrawList* drawList, - const ImVec2& pMin, const ImVec2& pMax, - float rounding) -{ - DrawAcrylicRect(drawList, pMin, pMax, AcrylicMaterial::getDarkPreset(), rounding); -} - -bool BeginAcrylicWindow(const char* name, bool* p_open, - ImGuiWindowFlags flags, - const AcrylicParams& params) -{ - s_currentWindow.params = params; - s_currentWindow.active = true; - - if (!(flags & ImGuiWindowFlags_NoBringToFrontOnFocus)) - ImGui::SetNextWindowFocus(); - - ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0, 0, 0, 0)); - - bool result = ImGui::Begin(name, p_open, flags); - - if (result) { - s_currentWindow.windowPos = ImGui::GetWindowPos(); - s_currentWindow.windowSize = ImGui::GetWindowSize(); - - ImDrawList* drawList = ImGui::GetWindowDrawList(); - ImVec2 pMin = s_currentWindow.windowPos; - ImVec2 pMax = ImVec2(pMin.x + s_currentWindow.windowSize.x, - pMin.y + s_currentWindow.windowSize.y); - - float rounding = ImGui::GetStyle().WindowRounding; - DrawAcrylicRect(drawList, pMin, pMax, params, rounding); - } - - return result; -} - -void EndAcrylicWindow() -{ - ImGui::End(); - ImGui::PopStyleColor(); - s_currentWindow.active = false; -} - -bool BeginAcrylicChild(const char* str_id, - const ImVec2& size, - const AcrylicParams& params, - ImGuiChildFlags child_flags, - ImGuiWindowFlags window_flags) -{ - ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0)); - bool result = ImGui::BeginChild(str_id, size, child_flags, window_flags); - if (result) { - ImVec2 childPos = ImGui::GetWindowPos(); - ImVec2 childSize = ImGui::GetWindowSize(); - ImDrawList* drawList = ImGui::GetWindowDrawList(); - ImVec2 pMin = childPos; - ImVec2 pMax = ImVec2(pMin.x + childSize.x, pMin.y + childSize.y); - float rounding = ImGui::GetStyle().ChildRounding; - DrawAcrylicRect(drawList, pMin, pMax, params, rounding); - } - return result; -} - -void EndAcrylicChild() -{ - ImGui::EndChild(); - ImGui::PopStyleColor(); -} - -bool BeginAcrylicPopup(const char* str_id, - ImGuiWindowFlags flags, - const AcrylicParams& params) -{ - ImGui::PushStyleColor(ImGuiCol_PopupBg, ImVec4(0, 0, 0, 0)); - bool result = ImGui::BeginPopup(str_id, flags); - if (result) { - ImVec2 popupPos = ImGui::GetWindowPos(); - ImVec2 popupSize = ImGui::GetWindowSize(); - ImDrawList* drawList = ImGui::GetWindowDrawList(); - ImVec2 pMin = popupPos; - ImVec2 pMax = ImVec2(pMin.x + popupSize.x, pMin.y + popupSize.y); - float rounding = ImGui::GetStyle().PopupRounding; - DrawAcrylicRect(drawList, pMin, pMax, params, rounding); - } else { - ImGui::PopStyleColor(); - } - return result; -} - -void EndAcrylicPopup() -{ - ImGui::EndPopup(); - ImGui::PopStyleColor(); -} - -bool BeginAcrylicPopupModal(const char* name, bool* p_open, - ImGuiWindowFlags flags, - const AcrylicParams& params) -{ - ImGui::PushStyleColor(ImGuiCol_ModalWindowDimBg, ImVec4(0, 0, 0, 0)); - ImGui::PushStyleColor(ImGuiCol_PopupBg, ImVec4(0, 0, 0, 0)); - bool result = ImGui::BeginPopupModal(name, p_open, flags); - ImGui::PopStyleColor(); // ModalWindowDimBg - - if (result) { - ImDrawList* drawList = ImGui::GetWindowDrawList(); - DrawFrostedScrim(drawList); - ImVec2 popupPos = ImGui::GetWindowPos(); - ImVec2 popupSize = ImGui::GetWindowSize(); - ImVec2 pMin = popupPos; - ImVec2 pMax = ImVec2(pMin.x + popupSize.x, pMin.y + popupSize.y); - float rounding = ImGui::GetStyle().PopupRounding; - DrawAcrylicRect(drawList, pMin, pMax, params, rounding); - } else { - ImGui::PopStyleColor(); - } - - return result; -} - -bool BeginAcrylicContextItem(const char* str_id, - ImGuiPopupFlags popup_flags, - const AcrylicParams& params) -{ - ImGui::PushStyleColor(ImGuiCol_PopupBg, ImVec4(0, 0, 0, 0)); - bool result = ImGui::BeginPopupContextItem(str_id, popup_flags); - if (result) { - ImVec2 popupPos = ImGui::GetWindowPos(); - ImVec2 popupSize = ImGui::GetWindowSize(); - ImDrawList* drawList = ImGui::GetWindowDrawList(); - ImVec2 pMin = popupPos; - ImVec2 pMax = ImVec2(pMin.x + popupSize.x, pMin.y + popupSize.y); - float rounding = ImGui::GetStyle().PopupRounding; - DrawAcrylicRect(drawList, pMin, pMax, params, rounding); - } else { - ImGui::PopStyleColor(); - } - return result; -} - -bool BeginAcrylicContextWindow(const char* str_id, - ImGuiPopupFlags popup_flags, - const AcrylicParams& params) -{ - ImGui::PushStyleColor(ImGuiCol_PopupBg, ImVec4(0, 0, 0, 0)); - bool result = ImGui::BeginPopupContextWindow(str_id, popup_flags); - if (result) { - ImVec2 popupPos = ImGui::GetWindowPos(); - ImVec2 popupSize = ImGui::GetWindowSize(); - ImDrawList* drawList = ImGui::GetWindowDrawList(); - ImVec2 pMin = popupPos; - ImVec2 pMax = ImVec2(pMin.x + popupSize.x, pMin.y + popupSize.y); - float rounding = ImGui::GetStyle().PopupRounding; - DrawAcrylicRect(drawList, pMin, pMax, params, rounding); - } else { - ImGui::PopStyleColor(); - } - return result; -} - -void DrawSidebarBackground(float width, const AcrylicParams& params) -{ - ImGuiViewport* viewport = ImGui::GetMainViewport(); - float menuBarHeight = ImGui::GetFrameHeight(); - ImVec2 pMin = ImVec2(viewport->WorkPos.x, viewport->WorkPos.y + menuBarHeight); - ImVec2 pMax = ImVec2(pMin.x + width, viewport->WorkPos.y + viewport->WorkSize.y); - ImDrawList* drawList = ImGui::GetBackgroundDrawList(); - DrawAcrylicRect(drawList, pMin, pMax, params, 0.0f); -} - -void SetQuality(AcrylicQuality quality) -{ - if (s_initialized) getAcrylicMaterial().setQuality(quality); -} - -AcrylicQuality GetQuality() -{ - if (s_initialized) return getAcrylicMaterial().getQuality(); - return AcrylicQuality::Off; -} - -void SetEnabled(bool enabled) -{ - if (s_initialized) getAcrylicMaterial().setEnabled(enabled); -} - -bool IsEnabled() -{ - if (s_initialized) return getAcrylicMaterial().isEnabled(); - return false; -} - -void SetBlurMultiplier(float multiplier) -{ - if (s_initialized) getAcrylicMaterial().setBlurMultiplier(multiplier); -} - -float GetBlurMultiplier() -{ - if (s_initialized) return getAcrylicMaterial().getBlurMultiplier(); - return 1.0f; -} - -void SetReducedTransparency(bool reduced) -{ - if (s_initialized) getAcrylicMaterial().setReducedTransparency(reduced); -} - -bool GetReducedTransparency() -{ - if (s_initialized) return getAcrylicMaterial().getReducedTransparency(); - return false; -} - -void SetUIOpacity(float opacity) -{ - if (s_initialized) getAcrylicMaterial().setUIOpacity(opacity); -} - -float GetUIOpacity() -{ - if (s_initialized) return getAcrylicMaterial().getUIOpacity(); - return 1.0f; -} - -void SetNoiseOpacity(float multiplier) -{ - if (s_initialized) getAcrylicMaterial().setNoiseOpacityMultiplier(multiplier); -} - -float GetNoiseOpacity() -{ - if (s_initialized) return getAcrylicMaterial().getNoiseOpacityMultiplier(); - return 1.0f; -} - -const AcrylicSettings& GetSettings() -{ - return getAcrylicMaterial().getSettings(); -} - -void SetSettings(const AcrylicSettings& settings) -{ - if (s_initialized) getAcrylicMaterial().setSettings(settings); -} - -void SetPreset(int preset) -{ - if (preset < 0 || preset >= kPresetCount) preset = 3; - SetEnabled(preset != 0); - SetQuality(PresetQuality(preset)); - SetBlurMultiplier(PresetBlur(preset)); -} - -int GetPreset() -{ - return MatchPreset(GetQuality(), GetBlurMultiplier()); -} - -void ApplyBlurAmount(float blur) -{ - if (blur < 0.001f) { - SetEnabled(false); - SetQuality(AcrylicQuality::Off); - SetBlurMultiplier(0.0f); - } else { - SetEnabled(true); - SetQuality(AcrylicQuality::Low); - SetBlurMultiplier(blur); - } -} - -} // namespace ImGuiAcrylic - -} // namespace effects -} // namespace ui -} // namespace dragonx - #else // neither GLAD nor DX11 namespace dragonx { @@ -1173,6 +774,5 @@ void ApplyBlurAmount(float) {} } // namespace ui } // namespace dragonx -#endif // DRAGONX_USE_DX11 -#endif // !DRAGONX_HAS_GLAD +#endif // DRAGONX_HAS_GLAD || DRAGONX_USE_DX11 diff --git a/tests/test_phase4.cpp b/tests/test_phase4.cpp index ef770ce..7a4588e 100644 --- a/tests/test_phase4.cpp +++ b/tests/test_phase4.cpp @@ -1051,6 +1051,99 @@ void testNetworkRefreshSnapshotHelpers() EXPECT_EQ(snapshot.sendTxids.count("pending-send"), static_cast(1)); } +void testParseDaemonRescanOutput() +{ + using dragonx::services::NetworkRefreshService; + + // "Still rescanning ... Progress=" — fraction scaled to a percentage. + { + auto scan = NetworkRefreshService::parseDaemonRescanOutput( + "2026-07-01 Still rescanning at height 500000. Progress=0.42 tip=600000\n"); + EXPECT_TRUE(scan.foundRescan); + EXPECT_NEAR(scan.rescanPct, 42.0f, 0.01f); + } + + // "Rescanning... %" — percentage read directly (already 0..100). + { + auto scan = NetworkRefreshService::parseDaemonRescanOutput( + "Rescanning... 37% done\n"); + EXPECT_TRUE(scan.foundRescan); + EXPECT_NEAR(scan.rescanPct, 37.0f, 0.01f); + } + + // Explicit completion line marks finished. + { + auto scan = NetworkRefreshService::parseDaemonRescanOutput("Done rescanning\n"); + EXPECT_TRUE(scan.finished); + } + + // The daemon that prints no "Done rescanning" logs a rescan benchmark ending in "ms". + { + auto scan = NetworkRefreshService::parseDaemonRescanOutput( + " rescan 16760577ms\n"); + EXPECT_TRUE(scan.finished); + } + + // "Building Witnesses for block complete, remaining". + { + auto scan = NetworkRefreshService::parseDaemonRescanOutput( + "Building Witnesses for block 123 0.5 complete, 40 remaining\n"); + EXPECT_TRUE(scan.foundWitness); + EXPECT_NEAR(scan.witnessPct, 50.0f, 0.01f); + EXPECT_EQ(scan.witnessRemaining, 40); + } + + // "Reading blocks for witness rebuild: / " — captures total and remaining. + { + auto scan = NetworkRefreshService::parseDaemonRescanOutput( + "Reading blocks for witness rebuild: 10 / 200\n"); + EXPECT_TRUE(scan.foundWitness); + EXPECT_EQ(scan.witnessReadTotal, 200); + EXPECT_EQ(scan.witnessRemaining, 190); // total - done + } + + // Parallel-rebuild completion line snaps witnessRebuilt=true. + { + auto scan = NetworkRefreshService::parseDaemonRescanOutput( + "rebuilt 3 note witness cache(s) to height 99 in 12ms using 4 thread(s)\n"); + EXPECT_TRUE(scan.foundWitness); + EXPECT_TRUE(scan.witnessRebuilt); + } + + // "Setting Initial Sapling Witness for tx , of " — distinct hashes, max N. + { + auto scan = NetworkRefreshService::parseDaemonRescanOutput( + "Setting Initial Sapling Witness for tx abc123, 2 of 7\n" + "Setting Initial Sapling Witness for tx def456, 3 of 7\n" + "Setting Initial Sapling Witness for tx abc123, 5 of 7\n"); + EXPECT_TRUE(scan.foundWitness); + // The parser collects every occurrence (dedup happens in the apply step's set), so the + // vector has all three keys but only two distinct values. + EXPECT_EQ(scan.witnessTxids.size(), static_cast(3)); + std::set distinct(scan.witnessTxids.begin(), scan.witnessTxids.end()); + EXPECT_EQ(distinct.size(), static_cast(2)); + EXPECT_EQ(distinct.count("abc123"), static_cast(1)); + EXPECT_EQ(distinct.count("def456"), static_cast(1)); + EXPECT_EQ(scan.witnessTotalTxs, 7); + } + + // Empty input yields all defaults. + { + auto scan = NetworkRefreshService::parseDaemonRescanOutput(""); + EXPECT_FALSE(scan.foundRescan); + EXPECT_FALSE(scan.finished); + EXPECT_NEAR(scan.rescanPct, 0.0f, 0.0001f); + EXPECT_TRUE(scan.lastStatus.empty()); + EXPECT_FALSE(scan.foundWitness); + EXPECT_NEAR(scan.witnessPct, -1.0f, 0.0001f); + EXPECT_EQ(scan.witnessRemaining, -1); + EXPECT_EQ(scan.witnessReadTotal, -1); + EXPECT_FALSE(scan.witnessRebuilt); + EXPECT_TRUE(scan.witnessTxids.empty()); + EXPECT_EQ(scan.witnessTotalTxs, -1); + } +} + void testNetworkRefreshRpcCollectors() { using Refresh = dragonx::services::NetworkRefreshService; @@ -5381,6 +5474,7 @@ int main() testRefreshScheduler(); testNetworkRefreshService(); testNetworkRefreshSnapshotHelpers(); + testParseDaemonRescanOutput(); testNetworkRefreshRpcCollectors(); testNetworkRefreshResultModels(); testOperationStatusPollParsing();