From a07495aa95fac97655cedb02c4c506d3d33654f2 Mon Sep 17 00:00:00 2001 From: RealAmethyst <69372246+RealAmethyst@users.noreply.github.com> Date: Wed, 3 Jun 2026 20:46:59 +0200 Subject: [PATCH] Service COM during frame throttle so screen readers stay responsive --- src/BizHawk.Client.EmuHawk/Throttle.cs | 35 +++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/src/BizHawk.Client.EmuHawk/Throttle.cs b/src/BizHawk.Client.EmuHawk/Throttle.cs index 45557b1ee0a..0ddc276500c 100644 --- a/src/BizHawk.Client.EmuHawk/Throttle.cs +++ b/src/BizHawk.Client.EmuHawk/Throttle.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Runtime.InteropServices; using System.Threading; using BizHawk.Client.Common; @@ -34,8 +35,8 @@ public void Step(Config config, Sound sound, bool allowSleep, int forceFrameSkip skipNextFrame = false; framesToSkip = 0; - //keep from burning CPU - Thread.Sleep(15); + //keep from burning CPU (but dispatch COM so screen-reader accessibility stays serviced) + ResponsiveSleep(15); return; } @@ -277,6 +278,34 @@ private int AutoFrameSkip_GetSkipAmount(int min, int max) return rv; } + [DllImport("ole32.dll")] + private static extern int CoWaitForMultipleHandles(uint dwFlags, uint dwTimeout, int cHandles, IntPtr[] pHandles, out uint lpdwindex); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern IntPtr CreateEventW(IntPtr lpEventAttributes, bool bManualReset, bool bInitialState, IntPtr lpName); + + private const uint COWAIT_DISPATCH_CALLS = 0x8; + private static readonly IntPtr[] _responsiveSleepHandles = { CreateEventW(IntPtr.Zero, true, false, IntPtr.Zero) }; + + /// + /// Sleeps ~ ms while still dispatching incoming COM calls. Screen readers' + /// UI Automation / MSAA queries are COM calls marshaled to this STA UI thread; a plain Thread.Sleep + /// does NOT service them, so during each frame's sleep the thread stops answering screen readers and + /// ALL of them (NVDA, Narrator, ...) go laggy while EmuHawk is foreground — even though the app stays + /// responsive to its own input. COWAIT_DISPATCH_CALLS dispatches only COM calls (not window + /// input/paint), keeping accessibility responsive without reentering the emulation loop. + /// + private static void ResponsiveSleep(int ms) + { + if (ms < 1) ms = 1; + if (OSTailoredCode.IsUnixHost) + { + Thread.Sleep(ms); // CoWaitForMultipleHandles is Windows-only + return; + } + _ = CoWaitForMultipleHandles(COWAIT_DISPATCH_CALLS, (uint)ms, 1, _responsiveSleepHandles, out _); + } + private void SpeedThrottle(Sound sound, bool paused) { AutoFrameSkip_BeforeThrottle(); @@ -326,7 +355,7 @@ private void SpeedThrottle(Sound sound, bool paused) break; } - Thread.Sleep(Math.Max(sleepTime, 1)); + ResponsiveSleep(Math.Max(sleepTime, 1)); } else if (sleepTime > 0) // spin for <1 millisecond waits {