fix(app): advance logic timers by wall-clock time
Logic timers advanced by io.DeltaTime, which update() clamps to 0.25 s so animations don't jump after a pause. That matched real time only while the main loop rendered continuously: with frames seconds apart, a timer fed the clamped delta runs up to 8x slow. Affected: the refresh scheduler, the seed-migration poll, the chat fast scan, the encryption check, the loading and shutdown timers, the lock-screen countdowns, the Settings debug gate and the mining benchmark. - App::logicDeltaTime(): steady-clock seconds since the previous update(), capped at 6 s so a sleep/resume fires each timer once instead of replaying the gap. Visual interpolation keeps the clamped io.DeltaTime. - The refresh timers tick before the RPC results are drained, so a hold that a callback arms (the connect-retry backoff) starts fresh instead of being charged the gap before it existed. - The shutdown timer is measured from shutdown_start_time_: beginShutdown runs from SDL event handling, so an accumulated delta would count the idle gap before the close request. - The lock-screen error and lockout countdowns subtract only the time since they were armed: a failed unlock arms them from an RPC result, which can land on the frame that ends an idle gap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+24
-9
@@ -746,10 +746,19 @@ void App::update()
|
|||||||
{
|
{
|
||||||
PERF_SCOPE("Update.Total");
|
PERF_SCOPE("Update.Total");
|
||||||
ImGuiIO& io = ImGui::GetIO();
|
ImGuiIO& io = ImGui::GetIO();
|
||||||
// Clamp the frame delta: after a long pause (window minimized, or the machine slept) NewFrame reports
|
// Clamp the frame delta for visual interpolation: after a long pause (window minimized, an idle
|
||||||
// a huge DeltaTime that would fire every refresh/animation timer at once. Every timer reads
|
// stretch, or the machine slept) NewFrame reports a large DeltaTime that would make animations jump.
|
||||||
// io.DeltaTime, so one clamp here bounds them all (also caps the real-clock delta fed while minimized).
|
|
||||||
if (io.DeltaTime > 0.25f) io.DeltaTime = 0.25f;
|
if (io.DeltaTime > 0.25f) io.DeltaTime = 0.25f;
|
||||||
|
// Logic timers use real elapsed time instead (see logicDeltaTime()), capped so a sleep/resume fires
|
||||||
|
// each timer at most once rather than replaying the whole gap.
|
||||||
|
{
|
||||||
|
const auto now = std::chrono::steady_clock::now();
|
||||||
|
logic_dt_ = (last_update_time_ == std::chrono::steady_clock::time_point{})
|
||||||
|
? 0.0f
|
||||||
|
: std::min(std::chrono::duration<float>(now - last_update_time_).count(), kMaxLogicDeltaSeconds);
|
||||||
|
if (logic_dt_ < 0.0f) logic_dt_ = 0.0f;
|
||||||
|
last_update_time_ = now;
|
||||||
|
}
|
||||||
|
|
||||||
// Full UI screenshot sweep: demo state is injected once and must stay frozen. Skip every live
|
// Full UI screenshot sweep: demo state is injected once and must stay frozen. Skip every live
|
||||||
// op (refresh/connect/pumps) so a real daemon can't clobber it — on Windows a running node's
|
// op (refresh/connect/pumps) so a real daemon can't clobber it — on Windows a running node's
|
||||||
@@ -768,6 +777,11 @@ void App::update()
|
|||||||
last_interaction_ = std::chrono::steady_clock::now();
|
last_interaction_ = std::chrono::steady_clock::now();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Advance the refresh timers by the real time since the last update BEFORE the RPC results are
|
||||||
|
// applied, so a hold one of those callbacks arms now (e.g. the connect-retry backoff) starts fresh
|
||||||
|
// instead of being charged the idle gap that elapsed before it existed.
|
||||||
|
network_refresh_.tick(logic_dt_);
|
||||||
|
|
||||||
// Drain completed RPC results back onto the main thread
|
// Drain completed RPC results back onto the main thread
|
||||||
if (worker_) {
|
if (worker_) {
|
||||||
worker_->drainResults();
|
worker_->drainResults();
|
||||||
@@ -901,7 +915,7 @@ void App::update()
|
|||||||
pumpWalletRestore();
|
pumpWalletRestore();
|
||||||
// While confirming the sweep, poll the tx confirmations + legacy balance every ~5s.
|
// While confirming the sweep, poll the tx confirmations + legacy balance every ~5s.
|
||||||
if (show_seed_migration_ && seed_migration_step_ == SeedMigrationStep::Confirming) {
|
if (show_seed_migration_ && seed_migration_step_ == SeedMigrationStep::Confirming) {
|
||||||
seed_migration_poll_timer_ -= ImGui::GetIO().DeltaTime;
|
seed_migration_poll_timer_ -= logic_dt_;
|
||||||
if (seed_migration_poll_timer_ <= 0.0f) {
|
if (seed_migration_poll_timer_ <= 0.0f) {
|
||||||
seed_migration_poll_timer_ = 5.0f;
|
seed_migration_poll_timer_ = 5.0f;
|
||||||
pollSweepStatus();
|
pollSweepStatus();
|
||||||
@@ -923,7 +937,6 @@ void App::update()
|
|||||||
}
|
}
|
||||||
|
|
||||||
using RefreshTimer = services::NetworkRefreshService::Timer;
|
using RefreshTimer = services::NetworkRefreshService::Timer;
|
||||||
network_refresh_.tick(io.DeltaTime);
|
|
||||||
|
|
||||||
// Wipe a secret (seed/private key) from the clipboard once its auto-clear delay elapses.
|
// Wipe a secret (seed/private key) from the clipboard once its auto-clear delay elapses.
|
||||||
pumpSecretClipboardClear();
|
pumpSecretClipboardClear();
|
||||||
@@ -1368,7 +1381,7 @@ void App::update()
|
|||||||
// (slower, 10–15s) Transactions cadence — so incoming messages surface in a few seconds. The
|
// (slower, 10–15s) Transactions cadence — so incoming messages surface in a few seconds. The
|
||||||
// normal harvest only re-scans on a new block. Self-gated (no-op without a chat identity /
|
// normal harvest only re-scans on a new block. Self-gated (no-op without a chat identity /
|
||||||
// connection / on lite); the in-flight guard keeps overlapping RPCs from stacking.
|
// connection / on lite); the in-flight guard keeps overlapping RPCs from stacking.
|
||||||
chat_fast_scan_accum_ += ImGui::GetIO().DeltaTime;
|
chat_fast_scan_accum_ += logic_dt_;
|
||||||
const float chatPoll = settings_ ? settings_->getChatPollRateSec() : 2.5f; // user-configurable
|
const float chatPoll = settings_ ? settings_->getChatPollRateSec() : 2.5f; // user-configurable
|
||||||
if (chat_fast_scan_accum_ >= chatPoll) {
|
if (chat_fast_scan_accum_ >= chatPoll) {
|
||||||
chat_fast_scan_accum_ = 0.0f;
|
chat_fast_scan_accum_ = 0.0f;
|
||||||
@@ -1993,7 +2006,7 @@ void App::render()
|
|||||||
} else if (pageNeedsWalletData && (!daemonReady || (state_.connected && !state_.encryption_state_known))) {
|
} else if (pageNeedsWalletData && (!daemonReady || (state_.connected && !state_.encryption_state_known))) {
|
||||||
// Track how long we've been waiting for encryption state
|
// Track how long we've been waiting for encryption state
|
||||||
if (state_.connected && !state_.encryption_state_known) {
|
if (state_.connected && !state_.encryption_state_known) {
|
||||||
encryption_check_timer_ += ImGui::GetIO().DeltaTime;
|
encryption_check_timer_ += logic_dt_;
|
||||||
if (encryption_check_timer_ >= 15.0f) {
|
if (encryption_check_timer_ >= 15.0f) {
|
||||||
DEBUG_LOGF("[App] Encryption state check timed out after 15s — assuming unencrypted\n");
|
DEBUG_LOGF("[App] Encryption state check timed out after 15s — assuming unencrypted\n");
|
||||||
state_.encryption_state_known = true;
|
state_.encryption_state_known = true;
|
||||||
@@ -6424,7 +6437,9 @@ void App::renderShutdownScreen()
|
|||||||
float v = ui::schema::UI().drawElement("components.shutdown", key).size;
|
float v = ui::schema::UI().drawElement("components.shutdown", key).size;
|
||||||
return v >= 0 ? v : fb;
|
return v >= 0 ? v : fb;
|
||||||
};
|
};
|
||||||
shutdown_timer_ += ImGui::GetIO().DeltaTime;
|
// Elapsed since beginShutdown, from the wall clock: beginShutdown runs from SDL event handling, so an
|
||||||
|
// accumulated delta would count the idle gap before the close request as shutdown time.
|
||||||
|
shutdown_timer_ = std::chrono::duration<float>(std::chrono::steady_clock::now() - shutdown_start_time_).count();
|
||||||
|
|
||||||
const std::string curShut = shutdown_status_.get(); // one consistent snapshot per frame (M-05)
|
const std::string curShut = shutdown_status_.get(); // one consistent snapshot per frame (M-05)
|
||||||
// After a few seconds, offer to close the wallet and leave the node running: stopping can take as long
|
// After a few seconds, offer to close the wallet and leave the node running: stopping can take as long
|
||||||
@@ -6746,7 +6761,7 @@ void App::renderLoadingOverlay(float contentH)
|
|||||||
return v >= 0 ? v : fb;
|
return v >= 0 ? v : fb;
|
||||||
};
|
};
|
||||||
|
|
||||||
loading_timer_ += ImGui::GetIO().DeltaTime;
|
loading_timer_ += logic_dt_;
|
||||||
|
|
||||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||||
ImVec2 wp = ImGui::GetWindowPos();
|
ImVec2 wp = ImGui::GetWindowPos();
|
||||||
|
|||||||
@@ -114,6 +114,17 @@ public:
|
|||||||
*/
|
*/
|
||||||
void update();
|
void update();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Wall-clock seconds since the previous update(), capped at kMaxLogicDeltaSeconds.
|
||||||
|
*
|
||||||
|
* Logic timers (refresh scheduler, poll intervals, countdowns) advance by this. Visual
|
||||||
|
* interpolation keeps io.DeltaTime, which update() clamps to 0.25 s so animations don't jump. The
|
||||||
|
* main loop idles between frames (one frame per 2 s wake when nothing is happening), so a timer
|
||||||
|
* fed the clamped delta would run up to 8x slow.
|
||||||
|
*/
|
||||||
|
float logicDeltaTime() const { return logic_dt_; }
|
||||||
|
static constexpr float kMaxLogicDeltaSeconds = 6.0f;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Pre-frame tasks that must run BEFORE ImGui::NewFrame().
|
* @brief Pre-frame tasks that must run BEFORE ImGui::NewFrame().
|
||||||
*
|
*
|
||||||
@@ -1521,6 +1532,7 @@ private:
|
|||||||
float lock_error_timer_ = 0.0f;
|
float lock_error_timer_ = 0.0f;
|
||||||
int lock_attempts_ = 0;
|
int lock_attempts_ = 0;
|
||||||
float lock_lockout_timer_ = 0.0f;
|
float lock_lockout_timer_ = 0.0f;
|
||||||
|
std::chrono::steady_clock::time_point lock_timer_armed_at_{}; // when lock_error/lockout timers were last armed
|
||||||
bool lock_unlock_in_progress_ = false;
|
bool lock_unlock_in_progress_ = false;
|
||||||
|
|
||||||
// Encrypt wallet dialog state
|
// Encrypt wallet dialog state
|
||||||
@@ -1562,6 +1574,10 @@ private:
|
|||||||
// Auto-lock on idle
|
// Auto-lock on idle
|
||||||
std::chrono::steady_clock::time_point last_interaction_ = std::chrono::steady_clock::now();
|
std::chrono::steady_clock::time_point last_interaction_ = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
|
// Wall-clock delta for logic timers (see logicDeltaTime()).
|
||||||
|
std::chrono::steady_clock::time_point last_update_time_{};
|
||||||
|
float logic_dt_ = 0.0f;
|
||||||
|
|
||||||
// Mine-when-idle: auto-start/stop mining based on system idle state
|
// Mine-when-idle: auto-start/stop mining based on system idle state
|
||||||
bool idle_mining_active_ = false; // true when mining was auto-started by idle detection
|
bool idle_mining_active_ = false; // true when mining was auto-started by idle detection
|
||||||
bool idle_scaled_to_idle_ = false; // true when threads have been scaled up for idle
|
bool idle_scaled_to_idle_ = false; // true when threads have been scaled up for idle
|
||||||
|
|||||||
@@ -472,6 +472,7 @@ void App::applyUnlockFailure(const std::string& errorMessage) {
|
|||||||
lock_attempts_++;
|
lock_attempts_++;
|
||||||
lock_error_msg_ = errorMessage;
|
lock_error_msg_ = errorMessage;
|
||||||
lock_error_timer_ = 3.0f;
|
lock_error_timer_ = 3.0f;
|
||||||
|
lock_timer_armed_at_ = std::chrono::steady_clock::now();
|
||||||
memset(lock_passphrase_buf_, 0, sizeof(lock_passphrase_buf_));
|
memset(lock_passphrase_buf_, 0, sizeof(lock_passphrase_buf_));
|
||||||
const float baseDelay = ui::schema::UI().drawElement("security", "lockout-base-delay").sizeOr(2.0f);
|
const float baseDelay = ui::schema::UI().drawElement("security", "lockout-base-delay").sizeOr(2.0f);
|
||||||
const int maxAttempts = (int)ui::schema::UI().drawElement("security", "max-attempts-before-lockout").sizeOr(5.0f);
|
const int maxAttempts = (int)ui::schema::UI().drawElement("security", "max-attempts-before-lockout").sizeOr(5.0f);
|
||||||
@@ -963,9 +964,14 @@ void App::renderLockScreen() {
|
|||||||
cy += ts.y + 20.0f * dp;
|
cy += ts.y + 20.0f * dp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Count down only the time since the timers were armed: a failed unlock arms them from an RPC result,
|
||||||
|
// which can land on a frame that ends an idle gap the countdown would otherwise lose at once.
|
||||||
|
const float lockDt = std::min(logic_dt_, std::chrono::duration<float>(
|
||||||
|
std::chrono::steady_clock::now() - lock_timer_armed_at_).count());
|
||||||
|
|
||||||
// Lockout timer
|
// Lockout timer
|
||||||
if (lock_lockout_timer_ > 0.0f) {
|
if (lock_lockout_timer_ > 0.0f) {
|
||||||
lock_lockout_timer_ -= ImGui::GetIO().DeltaTime;
|
lock_lockout_timer_ -= lockDt;
|
||||||
if (lock_lockout_timer_ < 0) lock_lockout_timer_ = 0;
|
if (lock_lockout_timer_ < 0) lock_lockout_timer_ = 0;
|
||||||
|
|
||||||
char msg[128];
|
char msg[128];
|
||||||
@@ -1072,7 +1078,7 @@ void App::renderLockScreen() {
|
|||||||
|
|
||||||
// Error message
|
// Error message
|
||||||
if (!lock_error_msg_.empty() && lock_error_timer_ > 0) {
|
if (!lock_error_msg_.empty() && lock_error_timer_ > 0) {
|
||||||
lock_error_timer_ -= ImGui::GetIO().DeltaTime;
|
lock_error_timer_ -= lockDt;
|
||||||
ImVec2 es = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, lock_error_msg_.c_str());
|
ImVec2 es = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, lock_error_msg_.c_str());
|
||||||
dl->AddText(captionFont, captionFont->LegacySize,
|
dl->AddText(captionFont, captionFont->LegacySize,
|
||||||
ImVec2(cardX + (cardW - es.x) * 0.5f, cy), ui::material::Error(),
|
ImVec2(cardX + (cardW - es.x) * 0.5f, cy), ui::material::Error(),
|
||||||
@@ -1151,6 +1157,7 @@ void App::renderLockScreen() {
|
|||||||
lock_error_msg_ = TR("pin_not_set");
|
lock_error_msg_ = TR("pin_not_set");
|
||||||
lock_use_pin_ = false;
|
lock_use_pin_ = false;
|
||||||
lock_error_timer_ = 3.0f;
|
lock_error_timer_ = 3.0f;
|
||||||
|
lock_timer_armed_at_ = std::chrono::steady_clock::now();
|
||||||
} else {
|
} else {
|
||||||
applyUnlockFailure(TR("incorrect_pin"));
|
applyUnlockFailure(TR("incorrect_pin"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2865,7 +2865,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
if (s_settingsState.debug_gate_open) {
|
if (s_settingsState.debug_gate_open) {
|
||||||
auto& st = s_settingsState;
|
auto& st = s_settingsState;
|
||||||
if (st.debug_gate_err_timer > 0.0f) {
|
if (st.debug_gate_err_timer > 0.0f) {
|
||||||
st.debug_gate_err_timer -= ImGui::GetIO().DeltaTime;
|
st.debug_gate_err_timer -= app->logicDeltaTime();
|
||||||
if (st.debug_gate_err_timer <= 0.0f) st.debug_gate_err.clear();
|
if (st.debug_gate_err_timer <= 0.0f) st.debug_gate_err.clear();
|
||||||
}
|
}
|
||||||
const bool needAuth = app->debugGateRequiresAuth();
|
const bool needAuth = app->debugGateRequiresAuth();
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ static void RenderMiningTabContent(App* app)
|
|||||||
// ================================================================
|
// ================================================================
|
||||||
if (s_benchmark.active()) {
|
if (s_benchmark.active()) {
|
||||||
auto benchmarkUpdate = AdvanceThreadBenchmark(
|
auto benchmarkUpdate = AdvanceThreadBenchmark(
|
||||||
s_benchmark, ImGui::GetIO().DeltaTime, state.pool_mining.hashrate_10s);
|
s_benchmark, app->logicDeltaTime(), state.pool_mining.hashrate_10s);
|
||||||
if (benchmarkUpdate.stopPoolMining) {
|
if (benchmarkUpdate.stopPoolMining) {
|
||||||
app->stopPoolMining();
|
app->stopPoolMining();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user