From 96fe01df2b27b6f5eb393161b1874dba46bcafd5 Mon Sep 17 00:00:00 2001 From: Max <112043822+Maxaubert@users.noreply.github.com> Date: Sat, 3 Oct 2026 04:12:42 +0200 Subject: [PATCH] perf(tick): raise the tick thread priority and opt out of power throttling (#334) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KPUNAWcwghXdHCApcKKjSG --- docs/architecture/02-tick-loop.md | 9 +++++++++ src/main.cpp | 5 +++++ src/sched_priority.h | 30 ++++++++++++++++++++++++++++++ src/version.h | 4 ++-- ui/src/controls/Preview.svelte | 2 +- 5 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 src/sched_priority.h diff --git a/docs/architecture/02-tick-loop.md b/docs/architecture/02-tick-loop.md index 0489bbd..8e4e8d5 100644 --- a/docs/architecture/02-tick-loop.md +++ b/docs/architecture/02-tick-loop.md @@ -322,6 +322,15 @@ not see it - a silently evicted hook). Measured: Wind CPU at 1x 37.5 -> 3.1 ms p starts 3-6 ms after a wheel notch that wakes it. The focus tracker installs its LOCATIONCHANGE hook and 16 ms caret poll only while active, and is switched off at every zoom-out. +## Priority under background load (issue #334) + +The tick thread runs at `THREAD_PRIORITY_HIGHEST` and the process opts out of power throttling +(`src/sched_priority.h`, both set once before the loop). At normal priority a saturated PC (renders, +builds) queued the tick behind every other normal thread: traces showed 139-737 ms stalls mid-zoom, +only while the machine was busy. One step above normal is enough to win against background work and +stays below DWM, so the tick never delays composition; the loop sleeps or timer-waits, so it costs +nothing at rest. It does not help when the GPU is the bottleneck (DWM does that work). + ## Threads: hooks, the focus tracker, and the Magnification runtime Wind has three threads that matter beyond the tick thread itself, each split off for its own diff --git a/src/main.cpp b/src/main.cpp index fc60d04..fdeb0cf 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -71,6 +71,7 @@ static void EnsureCompositePulse() { #include "cursor_lock.h" #include "inspect_focus.h" #include "launch_quiesce.h" +#include "sched_priority.h" // tick thread priority + no power throttling (#334) #include "resource.h" using namespace wind; @@ -3179,6 +3180,10 @@ int WINAPI wWinMain(HINSTANCE hInst, HINSTANCE, PWSTR, int) { int pacedHz = ts.hz; // hz the timer interval below is computed for LARGE_INTEGER due; due.QuadPart = -(10000000LL / pacedHz); + // Background CPU load must not stall a zoom (#334): this thread runs the tick loop. + wind::RaiseTickThreadPriority(); + wind::OptOutOfPowerThrottling(); + bool running = true; unsigned long long nextRecoverMs = 0; // device-lost recovery backoff gate (GetTickCount64) // The transform model does no blocking present, so it can never self-pace via Present(1,0) or diff --git a/src/sched_priority.h b/src/sched_priority.h new file mode 100644 index 0000000..4c17561 --- /dev/null +++ b/src/sched_priority.h @@ -0,0 +1,30 @@ +// src/sched_priority.h - keep the tick loop on time when the rest of the PC is busy (issue #334). +// +// The tick thread ran at normal priority, so under heavy background CPU load (renders, builds) it +// queued behind every other normal thread and a zoom in/out froze for a few frames (traces: 139-737 ms +// stalls, only while the machine was saturated). Wind also never owns a foreground window, so Windows +// may power-throttle it as a background process. +// +// THREAD_PRIORITY_HIGHEST, not MMCSS or the realtime band: one step above normal work is enough to win +// against background load, and it stays below DWM, so the tick can never delay composition. The loop +// sleeps at 1x and waits on a timer when zoomed, so the raised priority costs nothing while idle. +#pragma once +#include + +namespace wind { + +// Call once on the tick thread, before the loop. +inline void RaiseTickThreadPriority() { + SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST); +} + +// Never run Wind in EcoQoS / efficiency mode, and honour its timer resolution even with no visible window. +inline void OptOutOfPowerThrottling() { + PROCESS_POWER_THROTTLING_STATE s{}; + s.Version = PROCESS_POWER_THROTTLING_CURRENT_VERSION; + s.ControlMask = PROCESS_POWER_THROTTLING_EXECUTION_SPEED | PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION; + s.StateMask = 0; // controlled bits off = never throttled + SetProcessInformation(GetCurrentProcess(), ProcessPowerThrottling, &s, sizeof(s)); +} + +} // namespace wind diff --git a/src/version.h b/src/version.h index 619d0de..c79c886 100644 --- a/src/version.h +++ b/src/version.h @@ -4,7 +4,7 @@ #define WIND_VER_MAJOR 0 #define WIND_VER_MINOR 21 -#define WIND_VER_PATCH 1 +#define WIND_VER_PATCH 2 // String form for logs/snapshot/UI. Keep in sync with the numeric parts above. -#define WIND_VERSION_STR "0.21.1" +#define WIND_VERSION_STR "0.21.2" diff --git a/ui/src/controls/Preview.svelte b/ui/src/controls/Preview.svelte index 6a28872..4e7b824 100644 --- a/ui/src/controls/Preview.svelte +++ b/ui/src/controls/Preview.svelte @@ -23,7 +23,7 @@ const visible = (r) => !r.showIf || String(values[r.showIf.key]) === String(r.showIf.eq); const set = (key, val) => { values[key] = val; calls.push([key, val]); }; const extra = { - runningModel: 'hybrid', mpoNeedsRestart: false, version: '0.21.1', + runningModel: 'hybrid', mpoNeedsRestart: false, version: '0.21.2', onRestart: () => calls.push('restart'), onAction: (a) => calls.push(['action', a]), onRepo: () => calls.push('repo'), pick: async () => 'RDR2.exe',