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:
dan_s
2026-04-01 17:06:05 -05:00
parent b3d43ba0ad
commit 09f287fbc5
10 changed files with 508 additions and 9 deletions

View File

@@ -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:";

View File

@@ -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

View File

@@ -131,6 +131,14 @@ public:
* @return Seconds since last user input, or 0 on failure
*/
static int getSystemIdleSeconds();
/**
* @brief Get GPU utilization percentage (0100).
* 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();
};
/**