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
This commit is contained in:
@@ -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_; }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<bool>();
|
||||
if (j.contains("idle_threads_active")) idle_threads_active_ = j["idle_threads_active"].get<int>();
|
||||
if (j.contains("idle_threads_idle")) idle_threads_idle_ = j["idle_threads_idle"].get<int>();
|
||||
if (j.contains("idle_gpu_aware")) idle_gpu_aware_ = j["idle_gpu_aware"].get<bool>();
|
||||
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);
|
||||
|
||||
@@ -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<std::string>& 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<std::string> saved_pool_urls_; // user-saved pool URL favorites
|
||||
std::vector<std::string> saved_pool_workers_; // user-saved worker address favorites
|
||||
|
||||
|
||||
@@ -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).
|
||||
*/
|
||||
|
||||
@@ -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<int> candidates;
|
||||
int current_index = 0;
|
||||
|
||||
struct Result {
|
||||
int threads;
|
||||
double hashrate;
|
||||
};
|
||||
std::vector<Result> 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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user