Keep mouse-hook process out of EcoQoS so clicks aren't missed in power-saving mode
Template tests / tests (push) Successful in 1m41s

The real cause of "only the first couple of clicks were captured while on
power saving mode": Windows Power Throttling (EcoQoS) CPU-starves background
processes under a power-saving plan. The low-level WH_MOUSE_LL hook lives in
the spawned PowerShell watcher; when its callback is starved past the system
LowLevelHooksTimeout, Windows silently stops delivering mouse events to it —
the process keeps running (so no exit/crash, no fallback to interval capture
fires), it just misses clicks, and those clicks never become steps.

The previous powerSaveBlocker('prevent-app-suspension') only maps to
SetThreadExecutionState, which blocks system sleep but does NOT opt a process
out of EcoQoS, so it couldn't fix this.

Have the watcher process opt itself out of execution-speed throttling via
SetProcessInformation(ProcessPowerThrottling, ...) and raise itself to
HIGH_PRIORITY_CLASS at startup, so the hook callback always runs at full
speed and every click is delivered regardless of the laptop's power mode.
This commit is contained in:
2026-06-15 18:29:12 -05:00
parent 5edf740fda
commit 94f14851fa
+43
View File
@@ -798,6 +798,16 @@ public static class SFMouseHook {
private const int WM_MBUTTONDOWN = 0x0207;
private const int WM_XBUTTONDOWN = 0x020B;
private const long UnixEpochMilliseconds = 62135596800000L;
// Opting this process out of Windows Power Throttling (EcoQoS). In a
// power-saving plan the OS CPU-starves background processes; a starved
// low-level mouse hook whose callback exceeds LowLevelHooksTimeout is
// silently skipped by Windows (events stop arriving) while the process
// stays alive, so clicks are missed with no error — the "only the first
// couple of clicks were captured" symptom.
private const int ProcessPowerThrottling = 4;
private const uint PROCESS_POWER_THROTTLING_CURRENT_VERSION = 1;
private const uint PROCESS_POWER_THROTTLING_EXECUTION_SPEED = 0x1;
private const uint HIGH_PRIORITY_CLASS = 0x00000080;
private static IntPtr hook = IntPtr.Zero;
private static LowLevelMouseProc proc = HookCallback;
@@ -829,6 +839,13 @@ public static class SFMouseHook {
public POINT pt;
}
[StructLayout(LayoutKind.Sequential)]
private struct PROCESS_POWER_THROTTLING_STATE {
public uint Version;
public uint ControlMask;
public uint StateMask;
}
private delegate IntPtr LowLevelMouseProc(int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll", SetLastError = true)]
@@ -855,7 +872,33 @@ public static class SFMouseHook {
[DllImport("user32.dll")]
private static extern bool SetProcessDpiAwarenessContext(IntPtr value);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool SetProcessInformation(IntPtr hProcess, int ProcessInformationClass, ref PROCESS_POWER_THROTTLING_STATE ProcessInformation, uint ProcessInformationSize);
[DllImport("kernel32.dll")]
private static extern IntPtr GetCurrentProcess();
[DllImport("kernel32.dll")]
private static extern bool SetPriorityClass(IntPtr hProcess, uint dwPriorityClass);
// Force this process to run at full CPU speed regardless of the power plan,
// so the mouse-hook callback never trips LowLevelHooksTimeout and clicks
// keep being delivered while the laptop is in eco / power-saving mode.
private static void KeepProcessResponsive() {
try {
PROCESS_POWER_THROTTLING_STATE state = new PROCESS_POWER_THROTTLING_STATE();
state.Version = PROCESS_POWER_THROTTLING_CURRENT_VERSION;
// Control EXECUTION_SPEED and set its state bit to 0 => throttling off
// (the documented way to opt a process out of EcoQoS).
state.ControlMask = PROCESS_POWER_THROTTLING_EXECUTION_SPEED;
state.StateMask = 0;
SetProcessInformation(GetCurrentProcess(), ProcessPowerThrottling, ref state, (uint)Marshal.SizeOf(state));
} catch { }
try { SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS); } catch { }
}
public static void Run() {
KeepProcessResponsive();
try { SetProcessDpiAwarenessContext(new IntPtr(-4)); } catch { }
Thread writer = new Thread(WriterLoop);