Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ca199ef195 | |||
| 97bd2f8168 | |||
|
|
09f287fbc5 | ||
|
|
b3d43ba0ad | ||
| 430290f97a | |||
|
|
30fc5da520 | ||
| f02c965929 |
@@ -3,8 +3,19 @@
|
|||||||
# Released under the GPLv3
|
# Released under the GPLv3
|
||||||
|
|
||||||
cmake_minimum_required(VERSION 3.20)
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
|
||||||
|
# macOS: set deployment target and universal architectures BEFORE project()
|
||||||
|
# so they propagate to all targets, including FetchContent dependencies (SDL3, etc.)
|
||||||
|
if(APPLE)
|
||||||
|
set(CMAKE_OSX_DEPLOYMENT_TARGET "11.0" CACHE STRING "Minimum macOS version" FORCE)
|
||||||
|
# Build universal binary (Apple Silicon + Intel) unless the user explicitly set architectures
|
||||||
|
if(NOT DEFINED CMAKE_OSX_ARCHITECTURES OR CMAKE_OSX_ARCHITECTURES STREQUAL "")
|
||||||
|
set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64" CACHE STRING "macOS architectures" FORCE)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
project(ObsidianDragon
|
project(ObsidianDragon
|
||||||
VERSION 1.1.0
|
VERSION 1.1.1
|
||||||
LANGUAGES C CXX
|
LANGUAGES C CXX
|
||||||
DESCRIPTION "DragonX Cryptocurrency Wallet"
|
DESCRIPTION "DragonX Cryptocurrency Wallet"
|
||||||
)
|
)
|
||||||
|
|||||||
64
build.sh
64
build.sh
@@ -20,7 +20,7 @@
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
VERSION="1.0.0"
|
VERSION="1.1.1"
|
||||||
|
|
||||||
# ── Colours ──────────────────────────────────────────────────────────────────
|
# ── Colours ──────────────────────────────────────────────────────────────────
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
@@ -197,7 +197,14 @@ bundle_linux_daemon() {
|
|||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
build_dev() {
|
build_dev() {
|
||||||
header "Dev Build ($(uname -s) / $BUILD_TYPE)"
|
header "Dev Build ($(uname -s) / $BUILD_TYPE)"
|
||||||
local bd="$SCRIPT_DIR/build/linux"
|
|
||||||
|
# Use platform-appropriate build directory
|
||||||
|
if [[ "$(uname -s)" == "Darwin" ]]; then
|
||||||
|
local bd="$SCRIPT_DIR/build/mac"
|
||||||
|
export MACOSX_DEPLOYMENT_TARGET="11.0"
|
||||||
|
else
|
||||||
|
local bd="$SCRIPT_DIR/build/linux"
|
||||||
|
fi
|
||||||
|
|
||||||
if $CLEAN; then
|
if $CLEAN; then
|
||||||
info "Cleaning $bd ..."; rm -rf "$bd"
|
info "Cleaning $bd ..."; rm -rf "$bd"
|
||||||
@@ -261,7 +268,7 @@ build_release_linux() {
|
|||||||
rm -rf "$out"
|
rm -rf "$out"
|
||||||
mkdir -p "$out"
|
mkdir -p "$out"
|
||||||
|
|
||||||
local DIST="ObsidianDragon-Linux-x64"
|
local DIST="ObsidianDragon-${VERSION}-Linux-x64"
|
||||||
local dist_dir="$out/$DIST"
|
local dist_dir="$out/$DIST"
|
||||||
mkdir -p "$dist_dir"
|
mkdir -p "$dist_dir"
|
||||||
|
|
||||||
@@ -379,9 +386,9 @@ APPRUN
|
|||||||
local ARCH
|
local ARCH
|
||||||
ARCH=$(uname -m)
|
ARCH=$(uname -m)
|
||||||
cd "$bd"
|
cd "$bd"
|
||||||
ARCH="$ARCH" "$APPIMAGETOOL" "$APPDIR" "ObsidianDragon-${ARCH}.AppImage" 2>/dev/null && {
|
ARCH="$ARCH" "$APPIMAGETOOL" "$APPDIR" "ObsidianDragon-${VERSION}-${ARCH}.AppImage" 2>/dev/null && {
|
||||||
cp "ObsidianDragon-${ARCH}.AppImage" "$out/ObsidianDragon.AppImage"
|
cp "ObsidianDragon-${VERSION}-${ARCH}.AppImage" "$out/ObsidianDragon-${VERSION}.AppImage"
|
||||||
info "AppImage: $out/ObsidianDragon.AppImage ($(du -h "$out/ObsidianDragon.AppImage" | cut -f1))"
|
info "AppImage: $out/ObsidianDragon-${VERSION}.AppImage ($(du -h "$out/ObsidianDragon-${VERSION}.AppImage" | cut -f1))"
|
||||||
} || warn "AppImage creation failed — binaries zip still in release/linux/"
|
} || warn "AppImage creation failed — binaries zip still in release/linux/"
|
||||||
|
|
||||||
info "Linux release artifacts: $out/"
|
info "Linux release artifacts: $out/"
|
||||||
@@ -604,7 +611,7 @@ HDR
|
|||||||
rm -rf "$out"
|
rm -rf "$out"
|
||||||
mkdir -p "$out"
|
mkdir -p "$out"
|
||||||
|
|
||||||
local DIST="ObsidianDragon-Windows-x64"
|
local DIST="ObsidianDragon-${VERSION}-Windows-x64"
|
||||||
local dist_dir="$out/$DIST"
|
local dist_dir="$out/$DIST"
|
||||||
mkdir -p "$dist_dir"
|
mkdir -p "$dist_dir"
|
||||||
cp bin/ObsidianDragon.exe "$dist_dir/"
|
cp bin/ObsidianDragon.exe "$dist_dir/"
|
||||||
@@ -628,8 +635,8 @@ HDR
|
|||||||
cp -r bin/res "$dist_dir/" 2>/dev/null || true
|
cp -r bin/res "$dist_dir/" 2>/dev/null || true
|
||||||
|
|
||||||
# ── Single-file exe (all resources embedded) ────────────────────────────
|
# ── Single-file exe (all resources embedded) ────────────────────────────
|
||||||
cp bin/ObsidianDragon.exe "$out/"
|
cp bin/ObsidianDragon.exe "$out/ObsidianDragon-${VERSION}.exe"
|
||||||
info "Single-file exe: $out/ObsidianDragon.exe ($(du -h "$out/ObsidianDragon.exe" | cut -f1))"
|
info "Single-file exe: $out/ObsidianDragon-${VERSION}.exe ($(du -h "$out/ObsidianDragon-${VERSION}.exe" | cut -f1))"
|
||||||
|
|
||||||
# ── Zip ──────────────────────────────────────────────────────────────────
|
# ── Zip ──────────────────────────────────────────────────────────────────
|
||||||
if command -v zip &>/dev/null; then
|
if command -v zip &>/dev/null; then
|
||||||
@@ -732,7 +739,9 @@ build_release_mac() {
|
|||||||
fi
|
fi
|
||||||
info "macOS cross-compiler: $OSXCROSS_CXX (arch: $MAC_ARCH)"
|
info "macOS cross-compiler: $OSXCROSS_CXX (arch: $MAC_ARCH)"
|
||||||
else
|
else
|
||||||
MAC_ARCH=$(uname -m)
|
# Native macOS: build universal binary (arm64 + x86_64)
|
||||||
|
MAC_ARCH="universal"
|
||||||
|
export MACOSX_DEPLOYMENT_TARGET="11.0"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
header "Release: macOS ($MAC_ARCH$(${IS_CROSS} && echo ' — cross-compile'))"
|
header "Release: macOS ($MAC_ARCH$(${IS_CROSS} && echo ' — cross-compile'))"
|
||||||
@@ -811,12 +820,31 @@ TOOLCHAIN
|
|||||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
|
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
|
||||||
${COMPILER_RT:+-DOSXCROSS_COMPILER_RT="$COMPILER_RT"}
|
${COMPILER_RT:+-DOSXCROSS_COMPILER_RT="$COMPILER_RT"}
|
||||||
else
|
else
|
||||||
info "Configuring (native) ..."
|
# Build libsodium as universal if needed
|
||||||
|
local need_sodium=false
|
||||||
|
if [[ ! -f "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" ]] && \
|
||||||
|
[[ ! -f "$SCRIPT_DIR/libs/libsodium-mac/lib/libsodium.a" ]]; then
|
||||||
|
need_sodium=true
|
||||||
|
elif [[ -f "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" ]]; then
|
||||||
|
# Rebuild if existing lib is not universal (single-arch won't link)
|
||||||
|
if ! lipo -info "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" 2>/dev/null | grep -q "arm64.*x86_64\|x86_64.*arm64"; then
|
||||||
|
info "Existing libsodium is not universal — rebuilding ..."
|
||||||
|
rm -rf "$SCRIPT_DIR/libs/libsodium"
|
||||||
|
need_sodium=true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if $need_sodium; then
|
||||||
|
info "Building libsodium (universal) ..."
|
||||||
|
"$SCRIPT_DIR/scripts/fetch-libsodium.sh"
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Configuring (native universal arm64+x86_64) ..."
|
||||||
cmake "$SCRIPT_DIR" \
|
cmake "$SCRIPT_DIR" \
|
||||||
-DCMAKE_BUILD_TYPE=Release \
|
-DCMAKE_BUILD_TYPE=Release \
|
||||||
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
|
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
|
||||||
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
|
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
|
||||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0
|
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
|
||||||
|
-DCMAKE_OSX_ARCHITECTURES="arm64;x86_64"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
info "Building with $JOBS jobs ..."
|
info "Building with $JOBS jobs ..."
|
||||||
@@ -836,6 +864,11 @@ TOOLCHAIN
|
|||||||
else
|
else
|
||||||
info "Stripping ..."
|
info "Stripping ..."
|
||||||
strip bin/ObsidianDragon
|
strip bin/ObsidianDragon
|
||||||
|
# Verify universal binary
|
||||||
|
if command -v lipo &>/dev/null; then
|
||||||
|
info "Architecture info:"
|
||||||
|
lipo -info bin/ObsidianDragon
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
info "Binary: $(du -h bin/ObsidianDragon | cut -f1)"
|
info "Binary: $(du -h bin/ObsidianDragon | cut -f1)"
|
||||||
|
|
||||||
@@ -1019,6 +1052,13 @@ PLIST
|
|||||||
|
|
||||||
info ".app bundle created: $APP"
|
info ".app bundle created: $APP"
|
||||||
|
|
||||||
|
# ── Zip the .app bundle ──────────────────────────────────────────────────
|
||||||
|
local APP_ZIP="ObsidianDragon-${VERSION}-macOS-${MAC_ARCH}.app.zip"
|
||||||
|
if command -v zip &>/dev/null; then
|
||||||
|
(cd "$out" && zip -r "$APP_ZIP" "ObsidianDragon.app")
|
||||||
|
info "App zip: $out/$APP_ZIP ($(du -h "$out/$APP_ZIP" | cut -f1))"
|
||||||
|
fi
|
||||||
|
|
||||||
# ── Create DMG ───────────────────────────────────────────────────────────
|
# ── Create DMG ───────────────────────────────────────────────────────────
|
||||||
local DMG_NAME="DragonX_Wallet-${VERSION}-macOS-${MAC_ARCH}.dmg"
|
local DMG_NAME="DragonX_Wallet-${VERSION}-macOS-${MAC_ARCH}.dmg"
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<assemblyIdentity
|
<assemblyIdentity
|
||||||
type="win32"
|
type="win32"
|
||||||
name="DragonX.ObsidianDragon.Wallet"
|
name="DragonX.ObsidianDragon.Wallet"
|
||||||
version="1.1.0.0"
|
version="1.1.1.0"
|
||||||
processorArchitecture="amd64"
|
processorArchitecture="amd64"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ set -e
|
|||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
BUILD_DIR="${SCRIPT_DIR}/build/linux"
|
BUILD_DIR="${SCRIPT_DIR}/build/linux"
|
||||||
APPDIR="${BUILD_DIR}/AppDir"
|
APPDIR="${BUILD_DIR}/AppDir"
|
||||||
VERSION="1.0.0"
|
VERSION="1.1.1"
|
||||||
|
|
||||||
# Colors
|
# Colors
|
||||||
GREEN='\033[0;32m'
|
GREEN='\033[0;32m'
|
||||||
|
|||||||
@@ -50,12 +50,20 @@ if [[ ! -f "$TARBALL" ]]; then
|
|||||||
curl -fSL -o "$TARBALL" "$SODIUM_URL"
|
curl -fSL -o "$TARBALL" "$SODIUM_URL"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Verify checksum
|
# Verify checksum (sha256sum on Linux, shasum on macOS)
|
||||||
echo "$SODIUM_SHA256 $TARBALL" | sha256sum -c - || {
|
if command -v sha256sum &>/dev/null; then
|
||||||
echo "[fetch-libsodium] ERROR: SHA256 mismatch! Removing corrupted download."
|
echo "$SODIUM_SHA256 $TARBALL" | sha256sum -c - || {
|
||||||
rm -f "$TARBALL"
|
echo "[fetch-libsodium] ERROR: SHA256 mismatch! Removing corrupted download."
|
||||||
exit 1
|
rm -f "$TARBALL"
|
||||||
}
|
exit 1
|
||||||
|
}
|
||||||
|
elif command -v shasum &>/dev/null; then
|
||||||
|
echo "$SODIUM_SHA256 $TARBALL" | shasum -a 256 -c - || {
|
||||||
|
echo "[fetch-libsodium] ERROR: SHA256 mismatch! Removing corrupted download."
|
||||||
|
rm -f "$TARBALL"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
# ── Extract ─────────────────────────────────────────────────────────────────
|
# ── Extract ─────────────────────────────────────────────────────────────────
|
||||||
if [[ ! -d "$SRC_DIR" ]]; then
|
if [[ ! -d "$SRC_DIR" ]]; then
|
||||||
@@ -115,6 +123,69 @@ case "$TARGET" in
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
# ── Native macOS: build universal binary (arm64 + x86_64) ───────────────────
|
||||||
|
IS_MACOS_NATIVE=false
|
||||||
|
if [[ "$TARGET" == "native" && "$(uname -s)" == "Darwin" ]]; then
|
||||||
|
IS_MACOS_NATIVE=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if $IS_MACOS_NATIVE; then
|
||||||
|
echo "[fetch-libsodium] Building universal (arm64 + x86_64) for macOS..."
|
||||||
|
export MACOSX_DEPLOYMENT_TARGET="11.0"
|
||||||
|
|
||||||
|
INSTALL_ARM64="$PROJECT_DIR/libs/libsodium-arm64"
|
||||||
|
INSTALL_X86_64="$PROJECT_DIR/libs/libsodium-x86_64"
|
||||||
|
|
||||||
|
for ARCH in arm64 x86_64; do
|
||||||
|
echo "[fetch-libsodium] Building for $ARCH..."
|
||||||
|
cd "$SRC_DIR"
|
||||||
|
make clean 2>/dev/null || true
|
||||||
|
make distclean 2>/dev/null || true
|
||||||
|
|
||||||
|
if [[ "$ARCH" == "arm64" ]]; then
|
||||||
|
ARCH_INSTALL="$INSTALL_ARM64"
|
||||||
|
HOST_TRIPLE="aarch64-apple-darwin"
|
||||||
|
else
|
||||||
|
ARCH_INSTALL="$INSTALL_X86_64"
|
||||||
|
HOST_TRIPLE="x86_64-apple-darwin"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ARCH_CFLAGS="-arch $ARCH -mmacosx-version-min=11.0"
|
||||||
|
|
||||||
|
./configure \
|
||||||
|
--prefix="$ARCH_INSTALL" \
|
||||||
|
--disable-shared \
|
||||||
|
--enable-static \
|
||||||
|
--with-pic \
|
||||||
|
--host="$HOST_TRIPLE" \
|
||||||
|
CFLAGS="$ARCH_CFLAGS" \
|
||||||
|
LDFLAGS="-arch $ARCH" \
|
||||||
|
> /dev/null
|
||||||
|
|
||||||
|
make -j"$(sysctl -n hw.ncpu 2>/dev/null || echo 4)" > /dev/null 2>&1
|
||||||
|
make install > /dev/null
|
||||||
|
done
|
||||||
|
|
||||||
|
# Merge with lipo
|
||||||
|
echo "[fetch-libsodium] Creating universal binary with lipo..."
|
||||||
|
mkdir -p "$INSTALL_DIR/lib" "$INSTALL_DIR/include"
|
||||||
|
lipo -create \
|
||||||
|
"$INSTALL_ARM64/lib/libsodium.a" \
|
||||||
|
"$INSTALL_X86_64/lib/libsodium.a" \
|
||||||
|
-output "$INSTALL_DIR/lib/libsodium.a"
|
||||||
|
cp -R "$INSTALL_ARM64/include/"* "$INSTALL_DIR/include/"
|
||||||
|
|
||||||
|
# Clean up per-arch builds
|
||||||
|
rm -rf "$INSTALL_ARM64" "$INSTALL_X86_64"
|
||||||
|
cd "$PROJECT_DIR"
|
||||||
|
rm -rf "$SRC_DIR"
|
||||||
|
rm -f "$TARBALL"
|
||||||
|
|
||||||
|
echo "[fetch-libsodium] Done (universal): $INSTALL_DIR/lib/libsodium.a"
|
||||||
|
lipo -info "$INSTALL_DIR/lib/libsodium.a"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
echo "[fetch-libsodium] Configuring for target: $TARGET ..."
|
echo "[fetch-libsodium] Configuring for target: $TARGET ..."
|
||||||
./configure "${CONFIGURE_ARGS[@]}" > /dev/null
|
./configure "${CONFIGURE_ARGS[@]}" > /dev/null
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ set -e
|
|||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
BUILD_DIR="${SCRIPT_DIR}/build/linux"
|
BUILD_DIR="${SCRIPT_DIR}/build/linux"
|
||||||
VERSION="1.0.0"
|
VERSION="1.1.1"
|
||||||
|
|
||||||
# Colors for output
|
# Colors for output
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
|
|||||||
36
src/app.cpp
36
src/app.cpp
@@ -564,13 +564,26 @@ void App::update()
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return [this, done, anySuccess]() {
|
// Extract txids from successful operations so shielded
|
||||||
|
// sends are discoverable by z_viewtransaction.
|
||||||
|
std::vector<std::string> successTxids;
|
||||||
|
for (const auto& op : result) {
|
||||||
|
if (op.value("status", "") == "success"
|
||||||
|
&& op.contains("result") && op["result"].contains("txid")) {
|
||||||
|
successTxids.push_back(op["result"]["txid"].get<std::string>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [this, done, anySuccess,
|
||||||
|
successTxids = std::move(successTxids)]() {
|
||||||
for (const auto& id : done) {
|
for (const auto& id : done) {
|
||||||
pending_opids_.erase(
|
pending_opids_.erase(
|
||||||
std::remove(pending_opids_.begin(), pending_opids_.end(), id),
|
std::remove(pending_opids_.begin(), pending_opids_.end(), id),
|
||||||
pending_opids_.end());
|
pending_opids_.end());
|
||||||
}
|
}
|
||||||
if (anySuccess) {
|
if (anySuccess) {
|
||||||
|
for (const auto& txid : successTxids) {
|
||||||
|
send_txids_.insert(txid);
|
||||||
|
}
|
||||||
// Transaction confirmed by daemon — force immediate data refresh
|
// Transaction confirmed by daemon — force immediate data refresh
|
||||||
transactions_dirty_ = true;
|
transactions_dirty_ = true;
|
||||||
addresses_dirty_ = true;
|
addresses_dirty_ = true;
|
||||||
@@ -1095,9 +1108,10 @@ void App::render()
|
|||||||
case ui::NavPage::Console:
|
case ui::NavPage::Console:
|
||||||
// Use fast-lane worker for console commands to avoid head-of-line
|
// Use fast-lane worker for console commands to avoid head-of-line
|
||||||
// blocking behind the consolidated refreshData() batch.
|
// blocking behind the consolidated refreshData() batch.
|
||||||
|
// Fall back to main rpc/worker if fast-lane hasn't connected yet.
|
||||||
console_tab_.render(embedded_daemon_.get(),
|
console_tab_.render(embedded_daemon_.get(),
|
||||||
fast_rpc_ ? fast_rpc_.get() : rpc_.get(),
|
(fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get(),
|
||||||
fast_worker_ ? fast_worker_.get() : worker_.get(),
|
(fast_rpc_ && fast_rpc_->isConnected() && fast_worker_) ? fast_worker_.get() : worker_.get(),
|
||||||
xmrig_manager_.get());
|
xmrig_manager_.get());
|
||||||
break;
|
break;
|
||||||
case ui::NavPage::Settings:
|
case ui::NavPage::Settings:
|
||||||
@@ -2124,15 +2138,19 @@ void App::stopEmbeddedDaemon()
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (stop_sent) {
|
if (stop_sent) {
|
||||||
DEBUG_LOGF("Waiting for daemon to begin shutdown...\n");
|
DEBUG_LOGF("Waiting for daemon to flush block index and shut down...\n");
|
||||||
shutdown_status_ = "Waiting for daemon to begin shutdown...";
|
shutdown_status_ = "Waiting for daemon to flush block index...";
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
// Give the daemon time to flush LevelDB to disk before we
|
||||||
|
// escalate to SIGTERM. On macOS/APFS, LevelDB compaction +
|
||||||
|
// fsync can take 15-20s on a large chain. The stop() method
|
||||||
|
// will wait this long for a *natural* exit (via the RPC stop
|
||||||
|
// we already sent) before falling back to SIGTERM.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for process to exit; SIGTERM/TerminateProcess as last resort.
|
|
||||||
// 10 seconds is generous — if the daemon hasn't exited by then it's stuck.
|
|
||||||
shutdown_status_ = "Waiting for dragonxd process to exit...";
|
shutdown_status_ = "Waiting for dragonxd process to exit...";
|
||||||
embedded_daemon_->stop(10000);
|
// 20s grace period for the RPC "stop" to complete (LevelDB flush).
|
||||||
|
// Only after that does stop() escalate to SIGTERM, then SIGKILL.
|
||||||
|
embedded_daemon_->stop(20000);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool App::isEmbeddedDaemonRunning() const
|
bool App::isEmbeddedDaemonRunning() const
|
||||||
|
|||||||
@@ -173,6 +173,9 @@ public:
|
|||||||
// Pool mining (xmrig)
|
// Pool mining (xmrig)
|
||||||
void startPoolMining(int threads);
|
void startPoolMining(int threads);
|
||||||
void stopPoolMining();
|
void stopPoolMining();
|
||||||
|
int getXmrigRequestedThreads() const {
|
||||||
|
return xmrig_manager_ ? xmrig_manager_->getRequestedThreads() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Mine-when-idle state query
|
// Mine-when-idle state query
|
||||||
bool isIdleMiningActive() const { return idle_mining_active_; }
|
bool isIdleMiningActive() const { return idle_mining_active_; }
|
||||||
@@ -494,6 +497,12 @@ private:
|
|||||||
float opid_poll_timer_ = 0.0f;
|
float opid_poll_timer_ = 0.0f;
|
||||||
static constexpr float OPID_POLL_INTERVAL = 2.0f;
|
static constexpr float OPID_POLL_INTERVAL = 2.0f;
|
||||||
|
|
||||||
|
// Txids from completed z_sendmany operations.
|
||||||
|
// Ensures shielded sends are discoverable by z_viewtransaction
|
||||||
|
// even when they don't appear in listtransactions or
|
||||||
|
// z_listreceivedbyaddress.
|
||||||
|
std::unordered_set<std::string> send_txids_;
|
||||||
|
|
||||||
// First-run wizard state
|
// First-run wizard state
|
||||||
WizardPhase wizard_phase_ = WizardPhase::None;
|
WizardPhase wizard_phase_ = WizardPhase::None;
|
||||||
std::unique_ptr<util::Bootstrap> bootstrap_;
|
std::unique_ptr<util::Bootstrap> bootstrap_;
|
||||||
|
|||||||
@@ -437,6 +437,9 @@ void App::refreshData()
|
|||||||
// Snapshot viewtx cache for the worker thread
|
// Snapshot viewtx cache for the worker thread
|
||||||
auto viewtxCacheSnap = viewtx_cache_;
|
auto viewtxCacheSnap = viewtx_cache_;
|
||||||
|
|
||||||
|
// Snapshot send txids so the worker can include them in enrichment
|
||||||
|
auto sendTxidsSnap = send_txids_;
|
||||||
|
|
||||||
// Single consolidated worker task — all RPC calls happen back-to-back
|
// Single consolidated worker task — all RPC calls happen back-to-back
|
||||||
// on a single thread with no inter-task queue overhead.
|
// on a single thread with no inter-task queue overhead.
|
||||||
worker_->post([this, doAddresses, doPeers, doEncrypt, doTransactions,
|
worker_->post([this, doAddresses, doPeers, doEncrypt, doTransactions,
|
||||||
@@ -444,7 +447,8 @@ void App::refreshData()
|
|||||||
fullyEnriched = std::move(fullyEnriched),
|
fullyEnriched = std::move(fullyEnriched),
|
||||||
cachedConfirmedTxns = std::move(cachedConfirmedTxns),
|
cachedConfirmedTxns = std::move(cachedConfirmedTxns),
|
||||||
cachedConfirmedIds = std::move(cachedConfirmedIds),
|
cachedConfirmedIds = std::move(cachedConfirmedIds),
|
||||||
viewtxCacheSnap = std::move(viewtxCacheSnap)]() -> rpc::RPCWorker::MainCb {
|
viewtxCacheSnap = std::move(viewtxCacheSnap),
|
||||||
|
sendTxidsSnap = std::move(sendTxidsSnap)]() -> rpc::RPCWorker::MainCb {
|
||||||
// ================================================================
|
// ================================================================
|
||||||
// Phase 1: Balance + blockchain info
|
// Phase 1: Balance + blockchain info
|
||||||
// ================================================================
|
// ================================================================
|
||||||
@@ -596,6 +600,13 @@ void App::refreshData()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Include txids from completed z_sendmany operations so that
|
||||||
|
// pure shielded sends (which don't appear in listtransactions
|
||||||
|
// or z_listreceivedbyaddress) are discoverable.
|
||||||
|
for (const auto& txid : sendTxidsSnap) {
|
||||||
|
knownTxids.insert(txid);
|
||||||
|
}
|
||||||
|
|
||||||
// Phase 3c: detect shielded sends via z_viewtransaction
|
// Phase 3c: detect shielded sends via z_viewtransaction
|
||||||
// Check the in-memory viewtx cache first; only make RPC calls
|
// Check the in-memory viewtx cache first; only make RPC calls
|
||||||
// for txids we haven't seen before.
|
// for txids we haven't seen before.
|
||||||
@@ -858,6 +869,8 @@ void App::refreshData()
|
|||||||
// Merge new z_viewtransaction results into the persistent cache
|
// Merge new z_viewtransaction results into the persistent cache
|
||||||
for (auto& [txid, entry] : newViewTxEntries) {
|
for (auto& [txid, entry] : newViewTxEntries) {
|
||||||
viewtx_cache_[txid] = std::move(entry);
|
viewtx_cache_[txid] = std::move(entry);
|
||||||
|
// Once cached, no need to keep in send_txids_
|
||||||
|
send_txids_.erase(txid);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rebuild confirmed transaction cache: txns with >= 10
|
// Rebuild confirmed transaction cache: txns with >= 10
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
#include "ui/schema/ui_schema.h"
|
#include "ui/schema/ui_schema.h"
|
||||||
#include "ui/theme.h"
|
#include "ui/theme.h"
|
||||||
#include "ui/effects/imgui_acrylic.h"
|
#include "ui/effects/imgui_acrylic.h"
|
||||||
|
#include "ui/windows/mining_tab.h"
|
||||||
#include "util/platform.h"
|
#include "util/platform.h"
|
||||||
#include "util/secure_vault.h"
|
#include "util/secure_vault.h"
|
||||||
#include "util/perf_log.h"
|
#include "util/perf_log.h"
|
||||||
@@ -439,12 +440,25 @@ void App::checkIdleMining() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip idle mining adjustments while thread benchmark is running
|
||||||
|
if (ui::IsMiningBenchmarkActive()) return;
|
||||||
|
|
||||||
int idleSec = util::Platform::getSystemIdleSeconds();
|
int idleSec = util::Platform::getSystemIdleSeconds();
|
||||||
int delay = settings_->getMineIdleDelay();
|
int delay = settings_->getMineIdleDelay();
|
||||||
bool isPool = settings_->getPoolMode();
|
bool isPool = settings_->getPoolMode();
|
||||||
bool threadScaling = settings_->getIdleThreadScaling();
|
bool threadScaling = settings_->getIdleThreadScaling();
|
||||||
int maxThreads = std::max(1, (int)std::thread::hardware_concurrency());
|
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)
|
// Check if mining is already running (manually started by user)
|
||||||
bool miningActive = isPool
|
bool miningActive = isPool
|
||||||
? (xmrig_manager_ && xmrig_manager_->isRunning())
|
? (xmrig_manager_ && xmrig_manager_->isRunning())
|
||||||
@@ -461,7 +475,7 @@ void App::checkIdleMining() {
|
|||||||
if (activeThreads <= 0) activeThreads = std::max(1, maxThreads / 2);
|
if (activeThreads <= 0) activeThreads = std::max(1, maxThreads / 2);
|
||||||
if (idleThreads <= 0) idleThreads = maxThreads;
|
if (idleThreads <= 0) idleThreads = maxThreads;
|
||||||
|
|
||||||
if (idleSec >= delay) {
|
if (systemIdle) {
|
||||||
// System is idle — scale up to idle thread count
|
// System is idle — scale up to idle thread count
|
||||||
if (!idle_scaled_to_idle_) {
|
if (!idle_scaled_to_idle_) {
|
||||||
idle_scaled_to_idle_ = true;
|
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);
|
DEBUG_LOGF("[App] Idle thread scaling: %d -> %d threads (idle)\n", activeThreads, idleThreads);
|
||||||
}
|
}
|
||||||
} else {
|
} 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_) {
|
if (idle_scaled_to_idle_) {
|
||||||
idle_scaled_to_idle_ = false;
|
idle_scaled_to_idle_ = false;
|
||||||
if (isPool) {
|
if (isPool) {
|
||||||
@@ -484,11 +498,26 @@ void App::checkIdleMining() {
|
|||||||
startMining(activeThreads);
|
startMining(activeThreads);
|
||||||
}
|
}
|
||||||
DEBUG_LOGF("[App] Idle thread scaling: %d -> %d threads (active)\n", idleThreads, 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 {
|
} else {
|
||||||
// --- Start/Stop mode (original behavior) ---
|
// --- Start/Stop mode (original behavior) ---
|
||||||
if (idleSec >= delay) {
|
if (systemIdle) {
|
||||||
// System is idle — start mining if not already running
|
// System is idle — start mining if not already running
|
||||||
if (!miningActive && !idle_mining_active_ && !mining_toggle_in_progress_.load()) {
|
if (!miningActive && !idle_mining_active_ && !mining_toggle_in_progress_.load()) {
|
||||||
// For solo mining, need daemon connected and synced
|
// 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_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_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_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()) {
|
if (j.contains("saved_pool_urls") && j["saved_pool_urls"].is_array()) {
|
||||||
saved_pool_urls_.clear();
|
saved_pool_urls_.clear();
|
||||||
for (const auto& u : j["saved_pool_urls"])
|
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_thread_scaling"] = idle_thread_scaling_;
|
||||||
j["idle_threads_active"] = idle_threads_active_;
|
j["idle_threads_active"] = idle_threads_active_;
|
||||||
j["idle_threads_idle"] = idle_threads_idle_;
|
j["idle_threads_idle"] = idle_threads_idle_;
|
||||||
|
j["idle_gpu_aware"] = idle_gpu_aware_;
|
||||||
j["saved_pool_urls"] = json::array();
|
j["saved_pool_urls"] = json::array();
|
||||||
for (const auto& u : saved_pool_urls_)
|
for (const auto& u : saved_pool_urls_)
|
||||||
j["saved_pool_urls"].push_back(u);
|
j["saved_pool_urls"].push_back(u);
|
||||||
|
|||||||
@@ -221,6 +221,8 @@ public:
|
|||||||
void setIdleThreadsActive(int v) { idle_threads_active_ = std::max(0, v); }
|
void setIdleThreadsActive(int v) { idle_threads_active_ = std::max(0, v); }
|
||||||
int getIdleThreadsIdle() const { return idle_threads_idle_; }
|
int getIdleThreadsIdle() const { return idle_threads_idle_; }
|
||||||
void setIdleThreadsIdle(int v) { idle_threads_idle_ = std::max(0, v); }
|
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)
|
// Saved pool URLs (user-managed favorites dropdown)
|
||||||
const std::vector<std::string>& getSavedPoolUrls() const { return saved_pool_urls_; }
|
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
|
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_active_ = 0; // threads when user active (0 = auto)
|
||||||
int idle_threads_idle_ = 0; // threads when idle (0 = auto = all)
|
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_urls_; // user-saved pool URL favorites
|
||||||
std::vector<std::string> saved_pool_workers_; // user-saved worker address favorites
|
std::vector<std::string> saved_pool_workers_; // user-saved worker address favorites
|
||||||
|
|
||||||
|
|||||||
@@ -7,10 +7,10 @@
|
|||||||
// !! DO NOT EDIT version.h — it is generated from version.h.in by CMake.
|
// !! DO NOT EDIT version.h — it is generated from version.h.in by CMake.
|
||||||
// !! Change the version in CMakeLists.txt: project(... VERSION x.y.z ...)
|
// !! Change the version in CMakeLists.txt: project(... VERSION x.y.z ...)
|
||||||
|
|
||||||
#define DRAGONX_VERSION "1.1.0"
|
#define DRAGONX_VERSION "1.1.1"
|
||||||
#define DRAGONX_VERSION_MAJOR 1
|
#define DRAGONX_VERSION_MAJOR 1
|
||||||
#define DRAGONX_VERSION_MINOR 1
|
#define DRAGONX_VERSION_MINOR 1
|
||||||
#define DRAGONX_VERSION_PATCH 0
|
#define DRAGONX_VERSION_PATCH 1
|
||||||
|
|
||||||
#define DRAGONX_APP_NAME "ObsidianDragon"
|
#define DRAGONX_APP_NAME "ObsidianDragon"
|
||||||
#define DRAGONX_ORG_NAME "Hush"
|
#define DRAGONX_ORG_NAME "Hush"
|
||||||
|
|||||||
@@ -34,6 +34,9 @@
|
|||||||
#include <sys/socket.h>
|
#include <sys/socket.h>
|
||||||
#include <netinet/in.h>
|
#include <netinet/in.h>
|
||||||
#include <arpa/inet.h>
|
#include <arpa/inet.h>
|
||||||
|
#ifdef __APPLE__
|
||||||
|
#include <sys/sysctl.h>
|
||||||
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
@@ -152,6 +155,41 @@ std::vector<std::string> EmbeddedDaemon::getChainParams()
|
|||||||
// DragonX chain parameters.
|
// DragonX chain parameters.
|
||||||
// On Windows, omit -printtoconsole: we tail debug.log instead of piping stdout.
|
// On Windows, omit -printtoconsole: we tail debug.log instead of piping stdout.
|
||||||
// On Linux, -printtoconsole is used for pipe-based output capture.
|
// On Linux, -printtoconsole is used for pipe-based output capture.
|
||||||
|
// Auto-detect a reasonable -dbcache based on available physical RAM.
|
||||||
|
// Default LevelDB cache is small (~450MB); larger caches improve sync
|
||||||
|
// performance and reduce disk I/O — especially on macOS with APFS.
|
||||||
|
std::string dbcache_arg = "-dbcache=450";
|
||||||
|
{
|
||||||
|
#ifdef __APPLE__
|
||||||
|
// sysctl hw.memsize returns total physical RAM in bytes
|
||||||
|
int64_t memsize = 0;
|
||||||
|
size_t len = sizeof(memsize);
|
||||||
|
if (sysctlbyname("hw.memsize", &memsize, &len, nullptr, 0) == 0 && memsize > 0) {
|
||||||
|
int totalMB = static_cast<int>(memsize / (1024 * 1024));
|
||||||
|
// Use ~12.5% of RAM for dbcache, clamped to [450, 4096]
|
||||||
|
int cache = std::max(450, std::min(4096, totalMB / 8));
|
||||||
|
dbcache_arg = "-dbcache=" + std::to_string(cache);
|
||||||
|
}
|
||||||
|
#elif defined(__linux__)
|
||||||
|
long pages = sysconf(_SC_PHYS_PAGES);
|
||||||
|
long page_size = sysconf(_SC_PAGE_SIZE);
|
||||||
|
if (pages > 0 && page_size > 0) {
|
||||||
|
int totalMB = static_cast<int>((static_cast<int64_t>(pages) * page_size) / (1024 * 1024));
|
||||||
|
int cache = std::max(450, std::min(4096, totalMB / 8));
|
||||||
|
dbcache_arg = "-dbcache=" + std::to_string(cache);
|
||||||
|
}
|
||||||
|
#elif defined(_WIN32)
|
||||||
|
MEMORYSTATUSEX memInfo;
|
||||||
|
memInfo.dwLength = sizeof(memInfo);
|
||||||
|
if (GlobalMemoryStatusEx(&memInfo)) {
|
||||||
|
int totalMB = static_cast<int>(memInfo.ullTotalPhys / (1024 * 1024));
|
||||||
|
int cache = std::max(450, std::min(4096, totalMB / 8));
|
||||||
|
dbcache_arg = "-dbcache=" + std::to_string(cache);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
DEBUG_LOGF("[INFO] Using %s\n", dbcache_arg.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"-tls=only",
|
"-tls=only",
|
||||||
#ifndef _WIN32
|
#ifndef _WIN32
|
||||||
@@ -166,7 +204,8 @@ std::vector<std::string> EmbeddedDaemon::getChainParams()
|
|||||||
"-ac_private=1",
|
"-ac_private=1",
|
||||||
"-addnode=176.126.87.241",
|
"-addnode=176.126.87.241",
|
||||||
"-experimentalfeatures",
|
"-experimentalfeatures",
|
||||||
"-developerencryptwallet"
|
"-developerencryptwallet",
|
||||||
|
dbcache_arg
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1045,14 +1084,12 @@ void EmbeddedDaemon::stop(int wait_ms)
|
|||||||
if (process_pid_ > 0) {
|
if (process_pid_ > 0) {
|
||||||
setState(State::Stopping, "Stopping dragonxd...");
|
setState(State::Stopping, "Stopping dragonxd...");
|
||||||
|
|
||||||
// Send SIGTERM to the entire process group (negative PID).
|
// Phase 1: Wait for the daemon to exit naturally.
|
||||||
// This ensures that if dragonxd is a shell script wrapper,
|
// The caller (stopEmbeddedDaemon) already sent an RPC "stop" which
|
||||||
// both bash AND the actual dragonxd child receive the signal.
|
// tells the daemon to flush LevelDB, close sockets, and exit cleanly.
|
||||||
// Without this, only bash is killed and dragonxd is orphaned.
|
// On macOS/APFS the LevelDB flush can take several seconds — we must
|
||||||
DEBUG_LOGF("Sending SIGTERM to process group -%d\n", process_pid_);
|
// NOT send SIGTERM until the daemon has had enough time to finish.
|
||||||
kill(-process_pid_, SIGTERM);
|
DEBUG_LOGF("Waiting up to %d ms for daemon to exit after RPC stop...\n", wait_ms);
|
||||||
|
|
||||||
// Wait for process to exit, draining stdout each iteration
|
|
||||||
auto start = std::chrono::steady_clock::now();
|
auto start = std::chrono::steady_clock::now();
|
||||||
while (isRunning()) {
|
while (isRunning()) {
|
||||||
drainOutput();
|
drainOutput();
|
||||||
@@ -1061,15 +1098,34 @@ void EmbeddedDaemon::stop(int wait_ms)
|
|||||||
std::chrono::steady_clock::now() - start).count();
|
std::chrono::steady_clock::now() - start).count();
|
||||||
|
|
||||||
if (elapsed >= wait_ms) {
|
if (elapsed >= wait_ms) {
|
||||||
// Force kill the entire process group
|
|
||||||
DEBUG_LOGF("Forcing dragonxd termination with SIGKILL (group -%d)...\n", process_pid_);
|
|
||||||
kill(-process_pid_, SIGKILL);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 2: If still running, send SIGTERM and wait a further 10s.
|
||||||
|
if (isRunning()) {
|
||||||
|
DEBUG_LOGF("Daemon did not exit gracefully — sending SIGTERM to process group -%d\n", process_pid_);
|
||||||
|
kill(-process_pid_, SIGTERM);
|
||||||
|
|
||||||
|
auto sigterm_start = std::chrono::steady_clock::now();
|
||||||
|
while (isRunning()) {
|
||||||
|
drainOutput();
|
||||||
|
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
|
std::chrono::steady_clock::now() - sigterm_start).count();
|
||||||
|
if (elapsed >= 10000) {
|
||||||
|
// Phase 3: Force kill
|
||||||
|
DEBUG_LOGF("Forcing dragonxd termination with SIGKILL (group -%d)...\n", process_pid_);
|
||||||
|
kill(-process_pid_, SIGKILL);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DEBUG_LOGF("Daemon exited cleanly after RPC stop\n");
|
||||||
|
}
|
||||||
|
|
||||||
drainOutput(); // read any remaining output
|
drainOutput(); // read any remaining output
|
||||||
|
|
||||||
// Reap the child process
|
// Reap the child process
|
||||||
|
|||||||
@@ -88,6 +88,10 @@ public:
|
|||||||
const PoolStats& getStats() const { return stats_; }
|
const PoolStats& getStats() const { return stats_; }
|
||||||
const std::string& getLastError() const { return last_error_; }
|
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).
|
* @brief Get last N lines of xmrig stdout (thread-safe snapshot).
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -307,8 +307,9 @@ void ImportKeyDialog::render(App* app)
|
|||||||
|
|
||||||
// Import keys on worker thread to avoid freezing UI
|
// Import keys on worker thread to avoid freezing UI
|
||||||
bool rescan = s_rescan;
|
bool rescan = s_rescan;
|
||||||
|
int rescanHeight = s_rescan_height;
|
||||||
if (app->worker()) {
|
if (app->worker()) {
|
||||||
app->worker()->post([rpc = app->rpc(), keys, rescan]() -> rpc::RPCWorker::MainCb {
|
app->worker()->post([rpc = app->rpc(), keys, rescan, rescanHeight]() -> rpc::RPCWorker::MainCb {
|
||||||
int imported = 0;
|
int imported = 0;
|
||||||
int failed = 0;
|
int failed = 0;
|
||||||
|
|
||||||
@@ -317,10 +318,22 @@ void ImportKeyDialog::render(App* app)
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (keyType == "z-spending") {
|
if (keyType == "z-spending") {
|
||||||
rpc->call("z_importkey", {key, rescan ? "yes" : "no"});
|
// z_importkey "key" "yes"|"no" startheight
|
||||||
|
if (rescan && rescanHeight > 0) {
|
||||||
|
rpc->call("z_importkey", {key, "yes", rescanHeight});
|
||||||
|
} else {
|
||||||
|
rpc->call("z_importkey", {key, rescan ? "yes" : "no"});
|
||||||
|
}
|
||||||
imported++;
|
imported++;
|
||||||
} else if (keyType == "t-privkey") {
|
} else if (keyType == "t-privkey") {
|
||||||
rpc->call("importprivkey", {key, "", rescan});
|
// importprivkey does not accept a startheight;
|
||||||
|
// import without rescan, then rescanblockchain
|
||||||
|
// from the requested height afterward.
|
||||||
|
if (rescan && rescanHeight > 0) {
|
||||||
|
rpc->call("importprivkey", {key, "", false});
|
||||||
|
} else {
|
||||||
|
rpc->call("importprivkey", {key, "", rescan});
|
||||||
|
}
|
||||||
imported++;
|
imported++;
|
||||||
} else {
|
} else {
|
||||||
failed++;
|
failed++;
|
||||||
@@ -329,6 +342,17 @@ void ImportKeyDialog::render(App* app)
|
|||||||
failed++;
|
failed++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If t-keys were imported without rescan (because a
|
||||||
|
// specific start height was requested), trigger a
|
||||||
|
// single rescanblockchain from that height now.
|
||||||
|
if (rescan && rescanHeight > 0 && imported > 0) {
|
||||||
|
try {
|
||||||
|
rpc->call("rescanblockchain", {rescanHeight});
|
||||||
|
} catch (...) {
|
||||||
|
// rescan failure is non-fatal; user can retry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return [imported, failed]() {
|
return [imported, failed]() {
|
||||||
s_imported_keys = imported;
|
s_imported_keys = imported;
|
||||||
|
|||||||
@@ -43,6 +43,81 @@ static int s_drag_anchor_thread = 0; // thread# where drag started
|
|||||||
// Earnings filter: 0 = All, 1 = Solo, 2 = Pool
|
// Earnings filter: 0 = All, 1 = Solo, 2 = Pool
|
||||||
static int s_earnings_filter = 0;
|
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
|
// Pool mode state
|
||||||
static bool s_pool_mode = false;
|
static bool s_pool_mode = false;
|
||||||
static char s_pool_url[256] = "pool.dragonx.is:3433";
|
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
|
// Sync thread grid with actual count when idle thread scaling adjusts threads
|
||||||
if (app->settings()->getMineWhenIdle() && app->settings()->getIdleThreadScaling() && !s_drag_active) {
|
// Skip during benchmark — the benchmark controls thread counts directly
|
||||||
if (s_pool_mode && state.pool_mining.xmrig_running && state.pool_mining.threads_active > 0) {
|
if (app->settings()->getMineWhenIdle() && app->settings()->getIdleThreadScaling() && !s_drag_active && !IsMiningBenchmarkActive()) {
|
||||||
s_selected_threads = std::min(state.pool_mining.threads_active, max_threads);
|
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) {
|
} else if (mining.generate && mining.genproclimit > 0) {
|
||||||
s_selected_threads = std::min(mining.genproclimit, max_threads);
|
s_selected_threads = std::min(mining.genproclimit, max_threads);
|
||||||
}
|
}
|
||||||
@@ -239,6 +321,84 @@ static void RenderMiningTabContent(App* app)
|
|||||||
? state.pool_mining.xmrig_running
|
? state.pool_mining.xmrig_running
|
||||||
: (mining.generate || 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
|
// Proportional section budget — ensures all content fits without
|
||||||
// scrolling at the minimum window size (1024×775).
|
// scrolling at the minimum window size (1024×775).
|
||||||
@@ -936,6 +1096,41 @@ static void RenderMiningTabContent(App* app)
|
|||||||
idleRightEdge = sBtnX - 4.0f * dp;
|
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)
|
// Idle delay combo (to the left, when idle is enabled and NOT in thread scaling mode)
|
||||||
if (idleOn && !threadScaling) {
|
if (idleOn && !threadScaling) {
|
||||||
struct DelayOption { int seconds; const char* label; };
|
struct DelayOption { int seconds; const char* label; };
|
||||||
@@ -1076,6 +1271,145 @@ static void RenderMiningTabContent(App* app)
|
|||||||
ImGui::SetCursorScreenPos(savedCur);
|
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)
|
// Active mining indicator (left of idle toggle)
|
||||||
if (mining.generate) {
|
if (mining.generate) {
|
||||||
float pulse = effects::isLowSpecMode()
|
float pulse = effects::isLowSpecMode()
|
||||||
@@ -1115,11 +1449,13 @@ static void RenderMiningTabContent(App* app)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Show pointer cursor when hovering the thread grid
|
// 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);
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
|
||||||
// Drag-to-select logic
|
// Drag-to-select logic (disabled during benchmark)
|
||||||
if (ImGui::IsMouseClicked(0) && hovered_thread > 0) {
|
if (!benchActive && ImGui::IsMouseClicked(0) && hovered_thread > 0) {
|
||||||
// Begin drag
|
// Begin drag
|
||||||
s_drag_active = true;
|
s_drag_active = true;
|
||||||
s_drag_anchor_thread = hovered_thread;
|
s_drag_anchor_thread = hovered_thread;
|
||||||
|
|||||||
@@ -15,5 +15,11 @@ namespace ui {
|
|||||||
*/
|
*/
|
||||||
void RenderMiningTab(App* app);
|
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 ui
|
||||||
} // namespace dragonx
|
} // namespace dragonx
|
||||||
|
|||||||
@@ -845,6 +845,8 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["mining_idle_on_tooltip"] = "Disable idle mining";
|
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_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_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_active_tooltip"] = "Threads when user is active";
|
||||||
strings_["mining_idle_threads_idle_tooltip"] = "Threads when system is idle";
|
strings_["mining_idle_threads_idle_tooltip"] = "Threads when system is idle";
|
||||||
strings_["mining_local_hashrate"] = "Local Hashrate";
|
strings_["mining_local_hashrate"] = "Local Hashrate";
|
||||||
@@ -866,6 +868,11 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["mining_recent_payouts"] = "RECENT POOL PAYOUTS";
|
strings_["mining_recent_payouts"] = "RECENT POOL PAYOUTS";
|
||||||
strings_["mining_remove"] = "Remove";
|
strings_["mining_remove"] = "Remove";
|
||||||
strings_["mining_reset_defaults"] = "Reset Defaults";
|
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_payout_address"] = "Save payout address";
|
||||||
strings_["mining_save_pool_url"] = "Save pool URL";
|
strings_["mining_save_pool_url"] = "Save pool URL";
|
||||||
strings_["mining_saved_addresses"] = "Saved Addresses:";
|
strings_["mining_saved_addresses"] = "Saved Addresses:";
|
||||||
|
|||||||
@@ -688,5 +688,106 @@ int Platform::getSystemIdleSeconds()
|
|||||||
#endif
|
#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 util
|
||||||
} // namespace dragonx
|
} // namespace dragonx
|
||||||
|
|||||||
@@ -131,6 +131,14 @@ public:
|
|||||||
* @return Seconds since last user input, or 0 on failure
|
* @return Seconds since last user input, or 0 on failure
|
||||||
*/
|
*/
|
||||||
static int getSystemIdleSeconds();
|
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