From 09f287fbc5321103c4625312eb228f0c434cdcb7 Mon Sep 17 00:00:00 2001 From: dan_s Date: Wed, 1 Apr 2026 17:06:05 -0500 Subject: [PATCH] feat: thread benchmark, GPU-aware idle mining, thread scaling fix - Add pool mining thread benchmark: cycles through thread counts with 20s warmup + 10s measurement to find optimal setting for CPU - Add GPU-aware idle detection: GPU utilization >= 10% (video, games) treats system as active; toggle in mining tab header (default: on) Supports AMD sysfs, NVIDIA nvidia-smi, Intel freq ratio; -1 on macOS - Fix idle thread scaling: use getRequestedThreads() for immediate thread count instead of xmrig API threads_active which lags on restart - Apply active thread count on initial mining start when user is active - Skip idle mining adjustments while benchmark is running - Disable thread grid drag-to-select during benchmark - Add idle_gpu_aware setting with JSON persistence (default: true) - Add 7 i18n English strings for benchmark and GPU-aware tooltips --- src/app.h | 3 + src/app_security.cpp | 35 +++- src/config/settings.cpp | 2 + src/config/settings.h | 3 + src/daemon/xmrig_manager.h | 4 + src/ui/windows/mining_tab.cpp | 348 +++++++++++++++++++++++++++++++++- src/ui/windows/mining_tab.h | 6 + src/util/i18n.cpp | 7 + src/util/platform.cpp | 101 ++++++++++ src/util/platform.h | 8 + 10 files changed, 508 insertions(+), 9 deletions(-) diff --git a/src/app.h b/src/app.h index a37a779..f3b78e3 100644 --- a/src/app.h +++ b/src/app.h @@ -173,6 +173,9 @@ public: // Pool mining (xmrig) void startPoolMining(int threads); void stopPoolMining(); + int getXmrigRequestedThreads() const { + return xmrig_manager_ ? xmrig_manager_->getRequestedThreads() : 0; + } // Mine-when-idle state query bool isIdleMiningActive() const { return idle_mining_active_; } diff --git a/src/app_security.cpp b/src/app_security.cpp index f57c3b9..4162c06 100644 --- a/src/app_security.cpp +++ b/src/app_security.cpp @@ -19,6 +19,7 @@ #include "ui/schema/ui_schema.h" #include "ui/theme.h" #include "ui/effects/imgui_acrylic.h" +#include "ui/windows/mining_tab.h" #include "util/platform.h" #include "util/secure_vault.h" #include "util/perf_log.h" @@ -439,12 +440,25 @@ void App::checkIdleMining() { return; } + // Skip idle mining adjustments while thread benchmark is running + if (ui::IsMiningBenchmarkActive()) return; + int idleSec = util::Platform::getSystemIdleSeconds(); int delay = settings_->getMineIdleDelay(); bool isPool = settings_->getPoolMode(); bool threadScaling = settings_->getIdleThreadScaling(); int maxThreads = std::max(1, (int)std::thread::hardware_concurrency()); + // GPU-aware idle detection: if enabled, treat GPU utilization >= 10% + // as "user active" (e.g. watching a video). Disabled = unrestricted + // mode that only looks at keyboard/mouse input. + bool gpuBusy = false; + if (settings_->getIdleGpuAware()) { + int gpuUtil = util::Platform::getGpuUtilization(); + gpuBusy = (gpuUtil >= 10); + } + bool systemIdle = (idleSec >= delay) && !gpuBusy; + // Check if mining is already running (manually started by user) bool miningActive = isPool ? (xmrig_manager_ && xmrig_manager_->isRunning()) @@ -461,7 +475,7 @@ void App::checkIdleMining() { if (activeThreads <= 0) activeThreads = std::max(1, maxThreads / 2); if (idleThreads <= 0) idleThreads = maxThreads; - if (idleSec >= delay) { + if (systemIdle) { // System is idle — scale up to idle thread count if (!idle_scaled_to_idle_) { idle_scaled_to_idle_ = true; @@ -474,7 +488,7 @@ void App::checkIdleMining() { DEBUG_LOGF("[App] Idle thread scaling: %d -> %d threads (idle)\n", activeThreads, idleThreads); } } else { - // User is active — scale down to active thread count + // User is active (or GPU busy) — scale down to active thread count if (idle_scaled_to_idle_) { idle_scaled_to_idle_ = false; if (isPool) { @@ -484,11 +498,26 @@ void App::checkIdleMining() { startMining(activeThreads); } DEBUG_LOGF("[App] Idle thread scaling: %d -> %d threads (active)\n", idleThreads, activeThreads); + } else { + // Mining just started while user is active — ensure active + // thread count is applied (grid selection may differ). + int currentThreads = isPool + ? xmrig_manager_->getStats().threads_active + : state_.mining.genproclimit; + if (currentThreads > 0 && currentThreads != activeThreads) { + if (isPool) { + stopPoolMining(); + startPoolMining(activeThreads); + } else { + startMining(activeThreads); + } + DEBUG_LOGF("[App] Idle thread scaling: initial %d -> %d threads (active)\n", currentThreads, activeThreads); + } } } } else { // --- Start/Stop mode (original behavior) --- - if (idleSec >= delay) { + if (systemIdle) { // System is idle — start mining if not already running if (!miningActive && !idle_mining_active_ && !mining_toggle_in_progress_.load()) { // For solo mining, need daemon connected and synced diff --git a/src/config/settings.cpp b/src/config/settings.cpp index 11c71e9..2db3397 100644 --- a/src/config/settings.cpp +++ b/src/config/settings.cpp @@ -153,6 +153,7 @@ bool Settings::load(const std::string& path) if (j.contains("idle_thread_scaling")) idle_thread_scaling_ = j["idle_thread_scaling"].get(); if (j.contains("idle_threads_active")) idle_threads_active_ = j["idle_threads_active"].get(); if (j.contains("idle_threads_idle")) idle_threads_idle_ = j["idle_threads_idle"].get(); + if (j.contains("idle_gpu_aware")) idle_gpu_aware_ = j["idle_gpu_aware"].get(); if (j.contains("saved_pool_urls") && j["saved_pool_urls"].is_array()) { saved_pool_urls_.clear(); for (const auto& u : j["saved_pool_urls"]) @@ -250,6 +251,7 @@ bool Settings::save(const std::string& path) j["idle_thread_scaling"] = idle_thread_scaling_; j["idle_threads_active"] = idle_threads_active_; j["idle_threads_idle"] = idle_threads_idle_; + j["idle_gpu_aware"] = idle_gpu_aware_; j["saved_pool_urls"] = json::array(); for (const auto& u : saved_pool_urls_) j["saved_pool_urls"].push_back(u); diff --git a/src/config/settings.h b/src/config/settings.h index 6aa4587..b83f9d0 100644 --- a/src/config/settings.h +++ b/src/config/settings.h @@ -221,6 +221,8 @@ public: void setIdleThreadsActive(int v) { idle_threads_active_ = std::max(0, v); } int getIdleThreadsIdle() const { return idle_threads_idle_; } void setIdleThreadsIdle(int v) { idle_threads_idle_ = std::max(0, v); } + bool getIdleGpuAware() const { return idle_gpu_aware_; } + void setIdleGpuAware(bool v) { idle_gpu_aware_ = v; } // Saved pool URLs (user-managed favorites dropdown) const std::vector& getSavedPoolUrls() const { return saved_pool_urls_; } @@ -317,6 +319,7 @@ private: bool idle_thread_scaling_ = false; // scale threads instead of start/stop int idle_threads_active_ = 0; // threads when user active (0 = auto) int idle_threads_idle_ = 0; // threads when idle (0 = auto = all) + bool idle_gpu_aware_ = true; // treat GPU activity as non-idle std::vector saved_pool_urls_; // user-saved pool URL favorites std::vector saved_pool_workers_; // user-saved worker address favorites diff --git a/src/daemon/xmrig_manager.h b/src/daemon/xmrig_manager.h index 969c3fc..5aeffcf 100644 --- a/src/daemon/xmrig_manager.h +++ b/src/daemon/xmrig_manager.h @@ -88,6 +88,10 @@ public: const PoolStats& getStats() const { return stats_; } const std::string& getLastError() const { return last_error_; } + /// Thread count requested at start() — available immediately, unlike + /// PoolStats::threads_active which requires an API response. + int getRequestedThreads() const { return threads_; } + /** * @brief Get last N lines of xmrig stdout (thread-safe snapshot). */ diff --git a/src/ui/windows/mining_tab.cpp b/src/ui/windows/mining_tab.cpp index c32272f..b149d68 100644 --- a/src/ui/windows/mining_tab.cpp +++ b/src/ui/windows/mining_tab.cpp @@ -43,6 +43,81 @@ static int s_drag_anchor_thread = 0; // thread# where drag started // Earnings filter: 0 = All, 1 = Solo, 2 = Pool static int s_earnings_filter = 0; +// Thread benchmark state +struct ThreadBenchmark { + enum class Phase { Idle, Starting, WarmingUp, Measuring, Advancing, Done }; + Phase phase = Phase::Idle; + + std::vector candidates; + int current_index = 0; + + struct Result { + int threads; + double hashrate; + }; + std::vector results; + + float phase_timer = 0.0f; + static constexpr float WARMUP_SECS = 20.0f; + static constexpr float MEASURE_SECS = 10.0f; + double best_sample = 0.0; // best hashrate_10s during current measurement window + int sample_count = 0; // number of non-zero hashrate samples collected + + int optimal_threads = 0; + double optimal_hashrate = 0.0; + bool was_pool_running = false; + int prev_threads = 0; + + void reset() { + phase = Phase::Idle; + candidates.clear(); + current_index = 0; + results.clear(); + phase_timer = 0.0f; + best_sample = 0.0; + sample_count = 0; + optimal_threads = 0; + optimal_hashrate = 0.0; + was_pool_running = false; + prev_threads = 0; + } + + void buildCandidates(int max_threads) { + candidates.clear(); + if (max_threads <= 16) { + for (int t = 1; t <= max_threads; t++) + candidates.push_back(t); + } else { + // Sample: 1, then every ceil(max/10) step, always including max + int step = std::max(1, (max_threads + 9) / 10); + for (int t = 1; t <= max_threads; t += step) + candidates.push_back(t); + if (candidates.back() != max_threads) + candidates.push_back(max_threads); + } + } + + float totalEstimatedSecs() const { + return (float)candidates.size() * (WARMUP_SECS + MEASURE_SECS); + } + + float elapsedSecs() const { + float completed = (float)current_index * (WARMUP_SECS + MEASURE_SECS); + return completed + phase_timer; + } + + float progress() const { + float total = totalEstimatedSecs(); + return (total > 0.0f) ? std::min(1.0f, elapsedSecs() / total) : 0.0f; + } +}; +static ThreadBenchmark s_benchmark; + +bool IsMiningBenchmarkActive() { + return s_benchmark.phase != ThreadBenchmark::Phase::Idle && + s_benchmark.phase != ThreadBenchmark::Phase::Done; +} + // Pool mode state static bool s_pool_mode = false; static char s_pool_url[256] = "pool.dragonx.is:3433"; @@ -162,9 +237,16 @@ static void RenderMiningTabContent(App* app) } // Sync thread grid with actual count when idle thread scaling adjusts threads - if (app->settings()->getMineWhenIdle() && app->settings()->getIdleThreadScaling() && !s_drag_active) { - if (s_pool_mode && state.pool_mining.xmrig_running && state.pool_mining.threads_active > 0) { - s_selected_threads = std::min(state.pool_mining.threads_active, max_threads); + // Skip during benchmark — the benchmark controls thread counts directly + if (app->settings()->getMineWhenIdle() && app->settings()->getIdleThreadScaling() && !s_drag_active && !IsMiningBenchmarkActive()) { + if (s_pool_mode && state.pool_mining.xmrig_running) { + // Use the requested thread count (available immediately) rather + // than threads_active from the xmrig API which lags during restarts. + int reqThreads = app->getXmrigRequestedThreads(); + if (reqThreads > 0) + s_selected_threads = std::min(reqThreads, max_threads); + else if (state.pool_mining.threads_active > 0) + s_selected_threads = std::min(state.pool_mining.threads_active, max_threads); } else if (mining.generate && mining.genproclimit > 0) { s_selected_threads = std::min(mining.genproclimit, max_threads); } @@ -239,6 +321,84 @@ static void RenderMiningTabContent(App* app) ? state.pool_mining.xmrig_running : (mining.generate || state.pool_mining.xmrig_running); + // ================================================================ + // Thread Benchmark state machine — runs pool mining at each candidate + // thread count to find the optimal setting for this CPU. + // ================================================================ + if (s_benchmark.phase != ThreadBenchmark::Phase::Idle && + s_benchmark.phase != ThreadBenchmark::Phase::Done) { + float dt = ImGui::GetIO().DeltaTime; + s_benchmark.phase_timer += dt; + + switch (s_benchmark.phase) { + case ThreadBenchmark::Phase::Starting: + // Start pool mining at current candidate + if (s_benchmark.current_index < (int)s_benchmark.candidates.size()) { + int t = s_benchmark.candidates[s_benchmark.current_index]; + app->stopPoolMining(); + app->startPoolMining(t); + s_benchmark.phase = ThreadBenchmark::Phase::WarmingUp; + s_benchmark.phase_timer = 0.0f; + s_benchmark.best_sample = 0.0; + s_benchmark.sample_count = 0; + } else { + s_benchmark.phase = ThreadBenchmark::Phase::Done; + } + break; + + case ThreadBenchmark::Phase::WarmingUp: + if (s_benchmark.phase_timer >= ThreadBenchmark::WARMUP_SECS) { + s_benchmark.phase = ThreadBenchmark::Phase::Measuring; + s_benchmark.phase_timer = 0.0f; + s_benchmark.best_sample = 0.0; + s_benchmark.sample_count = 0; + } + break; + + case ThreadBenchmark::Phase::Measuring: + // Sample hashrate during measurement window + if (state.pool_mining.hashrate_10s > 0.0) { + s_benchmark.sample_count++; + if (state.pool_mining.hashrate_10s > s_benchmark.best_sample) + s_benchmark.best_sample = state.pool_mining.hashrate_10s; + } + if (s_benchmark.phase_timer >= ThreadBenchmark::MEASURE_SECS) { + int t = s_benchmark.candidates[s_benchmark.current_index]; + s_benchmark.results.push_back({t, s_benchmark.best_sample}); + if (s_benchmark.best_sample > s_benchmark.optimal_hashrate) { + s_benchmark.optimal_hashrate = s_benchmark.best_sample; + s_benchmark.optimal_threads = t; + } + s_benchmark.phase = ThreadBenchmark::Phase::Advancing; + s_benchmark.phase_timer = 0.0f; + } + break; + + case ThreadBenchmark::Phase::Advancing: + app->stopPoolMining(); + s_benchmark.current_index++; + if (s_benchmark.current_index < (int)s_benchmark.candidates.size()) { + s_benchmark.phase = ThreadBenchmark::Phase::Starting; + } else { + // Done — apply optimal thread count + s_benchmark.phase = ThreadBenchmark::Phase::Done; + if (s_benchmark.optimal_threads > 0) { + s_selected_threads = s_benchmark.optimal_threads; + app->settings()->setPoolThreads(s_selected_threads); + app->settings()->save(); + } + // Restart mining if it was running before, using optimal count + if (s_benchmark.was_pool_running && s_benchmark.optimal_threads > 0) { + app->startPoolMining(s_benchmark.optimal_threads); + } + } + break; + + default: + break; + } + } + // ================================================================ // Proportional section budget — ensures all content fits without // scrolling at the minimum window size (1024×775). @@ -936,6 +1096,41 @@ static void RenderMiningTabContent(App* app) idleRightEdge = sBtnX - 4.0f * dp; } + // GPU-aware idle toggle (to the left, when idle is on) + // When ON (default): GPU utilization >= 10% counts as "not idle" + // When OFF: unrestricted mode, only keyboard/mouse input matters + if (idleOn) { + bool gpuAware = app->settings()->getIdleGpuAware(); + const char* gpuIcon = gpuAware ? ICON_MD_MONITOR : ICON_MD_MONITOR; + float gBtnX = idleRightEdge - btnSz; + float gBtnY = btnY; + + if (gpuAware) { + dl->AddRectFilled(ImVec2(gBtnX, gBtnY), ImVec2(gBtnX + btnSz, gBtnY + btnSz), + WithAlpha(Primary(), 40), btnSz * 0.5f); + } + + ImVec2 gIcoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, gpuIcon); + ImU32 gIcoCol = gpuAware ? Primary() : OnSurfaceDisabled(); + dl->AddText(icoFont, icoFont->LegacySize, + ImVec2(gBtnX + (btnSz - gIcoSz.x) * 0.5f, gBtnY + (btnSz - gIcoSz.y) * 0.5f), + gIcoCol, gpuIcon); + + ImGui::SetCursorScreenPos(ImVec2(gBtnX, gBtnY)); + ImGui::InvisibleButton("##IdleGpuAware", ImVec2(btnSz, btnSz)); + if (ImGui::IsItemClicked()) { + app->settings()->setIdleGpuAware(!gpuAware); + app->settings()->save(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + ImGui::SetTooltip("%s", gpuAware + ? TR("mining_idle_gpu_on_tooltip") + : TR("mining_idle_gpu_off_tooltip")); + } + idleRightEdge = gBtnX - 4.0f * dp; + } + // Idle delay combo (to the left, when idle is enabled and NOT in thread scaling mode) if (idleOn && !threadScaling) { struct DelayOption { int seconds; const char* label; }; @@ -1076,6 +1271,145 @@ static void RenderMiningTabContent(App* app) ImGui::SetCursorScreenPos(savedCur); } + // --- Thread Benchmark button / progress (left of idle toggle) --- + { + ImVec2 benchSavedCur = ImGui::GetCursorScreenPos(); + bool benchRunning = s_benchmark.phase != ThreadBenchmark::Phase::Idle && + s_benchmark.phase != ThreadBenchmark::Phase::Done; + bool benchDone = s_benchmark.phase == ThreadBenchmark::Phase::Done; + ImFont* icoFont = Type().iconSmall(); + + if (benchRunning) { + // Show progress bar + current test info + float barW = std::min(180.0f * hs, idleRightEdge - (cardMin.x + pad) - 10.0f * dp); + float barH = 4.0f * dp; + float barX = idleRightEdge - barW; + float barY = curY + headerH - barH - 2.0f * dp; + + // Progress bar track + dl->AddRectFilled(ImVec2(barX, barY), ImVec2(barX + barW, barY + barH), + WithAlpha(OnSurface(), 30), barH * 0.5f); + // Progress bar fill + float pct = s_benchmark.progress(); + dl->AddRectFilled(ImVec2(barX, barY), ImVec2(barX + barW * pct, barY + barH), + Primary(), barH * 0.5f); + + // Status text above bar + int ct = s_benchmark.current_index < (int)s_benchmark.candidates.size() + ? s_benchmark.candidates[s_benchmark.current_index] : 0; + snprintf(buf, sizeof(buf), "%s %d/%d (%dt)", + TR("mining_benchmark_testing"), + s_benchmark.current_index + 1, + (int)s_benchmark.candidates.size(), ct); + ImVec2 txtSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf); + dl->AddText(capFont, capFont->LegacySize, + ImVec2(barX + (barW - txtSz.x) * 0.5f, barY - txtSz.y - 2.0f * dp), + OnSurfaceMedium(), buf); + + // Cancel button (small X) + float cancelSz = icoFont->LegacySize + 4.0f * dp; + float cancelX = barX - cancelSz - 4.0f * dp; + float cancelY = curY + (headerH - cancelSz) * 0.5f; + ImGui::SetCursorScreenPos(ImVec2(cancelX, cancelY)); + ImGui::InvisibleButton("##BenchCancel", ImVec2(cancelSz, cancelSz)); + if (ImGui::IsItemClicked()) { + app->stopPoolMining(); + if (s_benchmark.was_pool_running) + app->startPoolMining(s_benchmark.prev_threads); + s_benchmark.reset(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + ImGui::SetTooltip("%s", TR("mining_benchmark_cancel")); + } + const char* cancelIcon = ICON_MD_CLOSE; + ImVec2 cIcoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, cancelIcon); + dl->AddText(icoFont, icoFont->LegacySize, + ImVec2(cancelX + (cancelSz - cIcoSz.x) * 0.5f, + cancelY + (cancelSz - cIcoSz.y) * 0.5f), + OnSurfaceMedium(), cancelIcon); + + idleRightEdge = cancelX - 4.0f * dp; + + } else if (benchDone && s_benchmark.optimal_threads > 0) { + // Show result briefly, then reset on next click + snprintf(buf, sizeof(buf), "%s: %dt (%.1f H/s)", + TR("mining_benchmark_result"), + s_benchmark.optimal_threads, s_benchmark.optimal_hashrate); + ImVec2 txtSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf); + float txtX = idleRightEdge - txtSz.x; + dl->AddText(capFont, capFont->LegacySize, + ImVec2(txtX, curY + (headerH - txtSz.y) * 0.5f), + WithAlpha(Success(), 220), buf); + + // Dismiss button + float dismissSz = icoFont->LegacySize + 4.0f * dp; + float dismissX = txtX - dismissSz - 4.0f * dp; + float dismissY = curY + (headerH - dismissSz) * 0.5f; + ImGui::SetCursorScreenPos(ImVec2(dismissX, dismissY)); + ImGui::InvisibleButton("##BenchDismiss", ImVec2(dismissSz, dismissSz)); + if (ImGui::IsItemClicked()) + s_benchmark.reset(); + if (ImGui::IsItemHovered()) { + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + ImGui::SetTooltip("%s", TR("mining_benchmark_dismiss")); + } + const char* okIcon = ICON_MD_CHECK; + ImVec2 oIcoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, okIcon); + dl->AddText(icoFont, icoFont->LegacySize, + ImVec2(dismissX + (dismissSz - oIcoSz.x) * 0.5f, + dismissY + (dismissSz - oIcoSz.y) * 0.5f), + WithAlpha(Success(), 200), okIcon); + + idleRightEdge = dismissX - 4.0f * dp; + + } else if (s_pool_mode) { + // Show benchmark button (only in pool mode) + float btnSz = icoFont->LegacySize + 8.0f * dp; + float btnX = idleRightEdge - btnSz; + float btnY = curY + (headerH - btnSz) * 0.5f; + + ImGui::SetCursorScreenPos(ImVec2(btnX, btnY)); + ImGui::InvisibleButton("##BenchStart", ImVec2(btnSz, btnSz)); + bool benchHovered = ImGui::IsItemHovered(); + bool benchClicked = ImGui::IsItemClicked(); + + // Hover highlight + if (benchHovered) { + dl->AddRectFilled(ImVec2(btnX, btnY), ImVec2(btnX + btnSz, btnY + btnSz), + StateHover(), btnSz * 0.5f); + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + ImGui::SetTooltip("%s", TR("mining_benchmark_tooltip")); + } + + const char* benchIcon = ICON_MD_SPEED; + ImVec2 bIcoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, benchIcon); + dl->AddText(icoFont, icoFont->LegacySize, + ImVec2(btnX + (btnSz - bIcoSz.x) * 0.5f, + btnY + (btnSz - bIcoSz.y) * 0.5f), + OnSurfaceMedium(), benchIcon); + + if (benchClicked) { + // Require a wallet address for pool mining + std::string worker(s_pool_worker); + if (!worker.empty()) { + s_benchmark.reset(); + s_benchmark.was_pool_running = state.pool_mining.xmrig_running; + s_benchmark.prev_threads = s_selected_threads; + s_benchmark.buildCandidates(max_threads); + s_benchmark.phase = ThreadBenchmark::Phase::Starting; + // Stop any active solo mining first + if (mining.generate) + app->stopMining(); + } + } + + idleRightEdge = btnX - 4.0f * dp; + } + + ImGui::SetCursorScreenPos(benchSavedCur); + } + // Active mining indicator (left of idle toggle) if (mining.generate) { float pulse = effects::isLowSpecMode() @@ -1115,11 +1449,13 @@ static void RenderMiningTabContent(App* app) } // Show pointer cursor when hovering the thread grid - if (hovered_thread > 0) + bool benchActive = s_benchmark.phase != ThreadBenchmark::Phase::Idle && + s_benchmark.phase != ThreadBenchmark::Phase::Done; + if (hovered_thread > 0 && !benchActive) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); - // Drag-to-select logic - if (ImGui::IsMouseClicked(0) && hovered_thread > 0) { + // Drag-to-select logic (disabled during benchmark) + if (!benchActive && ImGui::IsMouseClicked(0) && hovered_thread > 0) { // Begin drag s_drag_active = true; s_drag_anchor_thread = hovered_thread; diff --git a/src/ui/windows/mining_tab.h b/src/ui/windows/mining_tab.h index c8a6ef5..2f55762 100644 --- a/src/ui/windows/mining_tab.h +++ b/src/ui/windows/mining_tab.h @@ -15,5 +15,11 @@ namespace ui { */ void RenderMiningTab(App* app); +/** + * @brief Returns true when the thread benchmark is actively running. + * Used by idle mining to avoid interfering with measurements. + */ +bool IsMiningBenchmarkActive(); + } // namespace ui } // namespace dragonx diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index fc66c27..8203499 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -845,6 +845,8 @@ void I18n::loadBuiltinEnglish() strings_["mining_idle_on_tooltip"] = "Disable idle mining"; strings_["mining_idle_scale_on_tooltip"] = "Thread scaling: ON\nClick to switch to start/stop mode"; strings_["mining_idle_scale_off_tooltip"] = "Start/stop mode: ON\nClick to switch to thread scaling mode"; + strings_["mining_idle_gpu_on_tooltip"] = "GPU-aware: ON\nGPU activity (video, games) prevents idle mining\nClick for unrestricted mode"; + strings_["mining_idle_gpu_off_tooltip"] = "Unrestricted: ON\nOnly keyboard/mouse input determines idle state\nClick to enable GPU-aware detection"; strings_["mining_idle_threads_active_tooltip"] = "Threads when user is active"; strings_["mining_idle_threads_idle_tooltip"] = "Threads when system is idle"; strings_["mining_local_hashrate"] = "Local Hashrate"; @@ -866,6 +868,11 @@ void I18n::loadBuiltinEnglish() strings_["mining_recent_payouts"] = "RECENT POOL PAYOUTS"; strings_["mining_remove"] = "Remove"; strings_["mining_reset_defaults"] = "Reset Defaults"; + strings_["mining_benchmark_tooltip"] = "Find optimal thread count for this CPU"; + strings_["mining_benchmark_testing"] = "Testing"; + strings_["mining_benchmark_cancel"] = "Cancel benchmark"; + strings_["mining_benchmark_result"] = "Optimal"; + strings_["mining_benchmark_dismiss"] = "Dismiss"; strings_["mining_save_payout_address"] = "Save payout address"; strings_["mining_save_pool_url"] = "Save pool URL"; strings_["mining_saved_addresses"] = "Saved Addresses:"; diff --git a/src/util/platform.cpp b/src/util/platform.cpp index 16c8e65..247c692 100644 --- a/src/util/platform.cpp +++ b/src/util/platform.cpp @@ -688,5 +688,106 @@ int Platform::getSystemIdleSeconds() #endif } +// ============================================================================ +// GPU utilization detection +// ============================================================================ + +int Platform::getGpuUtilization() +{ +#ifdef _WIN32 + // Windows: read GPU utilization via SetupAPI / D3DKMT + // Not all GPUs expose this; return -1 if unavailable. + // Use a popen fallback: nvidia-smi for NVIDIA, or return -1. + static bool s_tried_nvidia = false; + static bool s_has_nvidia = false; + if (!s_tried_nvidia) { + s_tried_nvidia = true; + FILE* f = _popen("where nvidia-smi 2>nul", "r"); + if (f) { + char buf[256]; + s_has_nvidia = (fgets(buf, sizeof(buf), f) != nullptr); + _pclose(f); + } + } + if (s_has_nvidia) { + FILE* f = _popen("nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits 2>nul", "r"); + if (f) { + char buf[64]; + int util = -1; + if (fgets(buf, sizeof(buf), f)) { + util = atoi(buf); + if (util < 0 || util > 100) util = -1; + } + _pclose(f); + return util; + } + } + return -1; +#elif defined(__APPLE__) + return -1; +#else + // Linux: try multiple GPU sysfs paths + + // AMD: /sys/class/drm/card*/device/gpu_busy_percent + { + // Try card0 through card3 + char path[128]; + for (int i = 0; i < 4; i++) { + snprintf(path, sizeof(path), "/sys/class/drm/card%d/device/gpu_busy_percent", i); + std::ifstream ifs(path); + if (ifs.is_open()) { + int val = -1; + ifs >> val; + if (val >= 0 && val <= 100) + return val; + } + } + } + + // NVIDIA: nvidia-smi (binary may exist even without sysfs) + { + static bool s_tried = false; + static bool s_has_nvidia_smi = false; + if (!s_tried) { + s_tried = true; + FILE* f = popen("which nvidia-smi 2>/dev/null", "r"); + if (f) { + char buf[256]; + s_has_nvidia_smi = (fgets(buf, sizeof(buf), f) != nullptr); + pclose(f); + } + } + if (s_has_nvidia_smi) { + FILE* f = popen("nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits 2>/dev/null", "r"); + if (f) { + char buf[64]; + int util = -1; + if (fgets(buf, sizeof(buf), f)) { + util = atoi(buf); + if (util < 0 || util > 100) util = -1; + } + pclose(f); + return util; + } + } + } + + // Intel: compare current vs max freq as a rough proxy + { + std::ifstream curF("/sys/class/drm/card0/gt_cur_freq_mhz"); + std::ifstream maxF("/sys/class/drm/card0/gt_max_freq_mhz"); + if (curF.is_open() && maxF.is_open()) { + int cur = 0, mx = 0; + curF >> cur; + maxF >> mx; + if (mx > 0) + return std::min(100, (cur * 100) / mx); + } + } + + return -1; +#endif +} + } // namespace util } // namespace dragonx diff --git a/src/util/platform.h b/src/util/platform.h index 24ca6d7..f39dc23 100644 --- a/src/util/platform.h +++ b/src/util/platform.h @@ -131,6 +131,14 @@ public: * @return Seconds since last user input, or 0 on failure */ static int getSystemIdleSeconds(); + + /** + * @brief Get GPU utilization percentage (0–100). + * Linux: reads sysfs for AMD, /proc for NVIDIA. + * Windows: queries PDH GPU engine counters. + * @return GPU busy percent, or -1 if unavailable. + */ + static int getGpuUtilization(); }; /**