[Scummvm-git-logs] scummvm master -> 6d61014e37a487e31ed72bf94e6161230084b723

sev- noreply at scummvm.org
Tue Jul 14 22:42:39 UTC 2026


This automated email contains information about 2 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .

Summary:
cbc81b9707 SDL: Work around stale mouse positions on macOS 26
6d61014e37 TESTBED: Add stale mouse position check


Commit: cbc81b97074f2cde21241194b09ecd94b3b8f175
    https://github.com/scummvm/scummvm/commit/cbc81b97074f2cde21241194b09ecd94b3b8f175
Author: Ryan Landay (rlanday at gmail.com)
Date: 2026-07-15T00:42:36+02:00

Commit Message:
SDL: Work around stale mouse positions on macOS 26

WORKAROUND for an Apple bug, reported to SDL as
https://github.com/libsdl-org/SDL/issues/15967

macOS 26 sometimes delivers a mouse motion event carrying a stale
position, typically replaying an old cursor position when the cursor
crosses the window edge shortly after a focus change. SDL trusts the
event, synthesizing spurious mouse enter/motion pairs from it, and the
cursor visibly teleports to the stale position for a frame.

SDL 3.4 already works around another form of the same Apple bug
(incorrect motion positions in fullscreen Spaces, SDL commit
c39d772a07) by noting that the global mouse position query remains
correct even when event positions lie. Event capture confirms that
truthful motion events match SDL_GetGlobalMouseState() minus the
window position exactly, while the stale replays deviate by hundreds
of pixels, and that no plausibility heuristic on event positions alone
can work (real coalesced flicks jump farther than some stale replays).

So validate each motion event against the global mouse position and,
when they disagree significantly, substitute the global-derived
position rather than dropping the event: stale positions get corrected
to where the cursor physically is, and a false trigger merely places
the cursor slightly ahead along its real path.

The workaround can be removed once SDL or macOS fixes the underlying
bug. To allow checking for that periodically, it is switchable at
runtime via the new OSystem::kFeatureStaleMousePositionWorkaround; a
follow-up commit adds a testbed check built on it.

Co-Authored-By: Claude Fable 5 <noreply at anthropic.com>

Changed paths:
    backends/events/sdl/sdl-common-events.cpp
    backends/events/sdl/sdl-events.h
    backends/platform/sdl/sdl.cpp
    common/system.h


diff --git a/backends/events/sdl/sdl-common-events.cpp b/backends/events/sdl/sdl-common-events.cpp
index 2821bf1ec8d..3aca0b1047b 100644
--- a/backends/events/sdl/sdl-common-events.cpp
+++ b/backends/events/sdl/sdl-common-events.cpp
@@ -27,6 +27,7 @@
 #include "backends/platform/sdl/sdl.h"
 #include "backends/graphics/graphics.h"
 #include "common/config-manager.h"
+#include "common/debug.h"
 #include "common/textconsole.h"
 #include "common/fs.h"
 #include "engines/engine.h"
@@ -79,6 +80,60 @@ void SdlEventSource::convertTouchXYToGameXY(float touchX, float touchY, int *gam
 bool SdlEventSource::handleMouseMotion(SDL_Event &ev, Common::Event &event) {
 	event.type = Common::EVENT_MOUSEMOVE;
 
+#if defined(MACOSX) && SDL_VERSION_ATLEAST(2, 0, 4)
+	// WORKAROUND: macOS 26 sometimes delivers a mouse motion event carrying
+	// a stale position, typically replaying an old cursor position when the
+	// cursor crosses the window edge shortly after a focus change, which
+	// makes the cursor visibly teleport for a frame. This is an Apple bug,
+	// reported to SDL as https://github.com/libsdl-org/SDL/issues/15967;
+	// SDL works around another form of it (in fullscreen Spaces, SDL commit
+	// c39d772a07) by relying on the global mouse position query, which
+	// remains correct even when event positions lie. A truthful motion
+	// event matches the global mouse position minus the window position (up
+	// to a small pipeline lag), so when the two disagree significantly,
+	// trust the global position instead of the event.
+	//
+	// The workaround can be disabled at runtime through
+	// OSystem::kFeatureStaleMousePositionWorkaround; the testbed engine
+	// does this in EventTests::staleMousePosition() so it can be checked
+	// periodically whether the underlying bug still occurs. Once SDL or
+	// macOS fixes it, the workaround can be removed.
+	//
+	// SDL 2.0.4 is required because that is when SDL_GetGlobalMouseState()
+	// was introduced. With older SDL (e.g. an SDL 1.2 build via
+	// sdl12-compat) the workaround is unavailable and the bug remains.
+	if (_staleMousePositionWorkaround) {
+		SdlGraphicsManager *gm = dynamic_cast<SdlGraphicsManager *>(_graphicsManager);
+		SDL_Window *win = gm ? gm->getWindow()->getSDLWindow() : nullptr;
+		bool relativeMode;
+		int gx, gy;
+#if SDL_VERSION_ATLEAST(3, 0, 0)
+		relativeMode = win && SDL_GetWindowRelativeMouseMode(win);
+		float fgx, fgy;
+		SDL_GetGlobalMouseState(&fgx, &fgy);
+		gx = (int)fgx;
+		gy = (int)fgy;
+#else
+		relativeMode = SDL_GetRelativeMouseMode() == SDL_TRUE;
+		SDL_GetGlobalMouseState(&gx, &gy);
+#endif
+		if (win && !relativeMode) {
+			int wx, wy, ww, wh;
+			SDL_GetWindowPosition(win, &wx, &wy);
+			SDL_GetWindowSize(win, &ww, &wh);
+			// Clamp to the window like SDL does for motion events at the edges
+			const int tx = CLIP<int>(gx - wx, 0, ww - 1);
+			const int ty = CLIP<int>(gy - wy, 0, wh - 1);
+			if (ABS(tx - (int)ev.motion.x) > 16 || ABS(ty - (int)ev.motion.y) > 16) {
+				debug(2, "SdlEventSource: corrected stale mouse position (%d,%d) to (%d,%d)",
+						(int)ev.motion.x, (int)ev.motion.y, tx, ty);
+				ev.motion.x = tx;
+				ev.motion.y = ty;
+			}
+		}
+	}
+#endif
+
 	return processMouseEvent(event, ev.motion.x, ev.motion.y, ev.motion.xrel, ev.motion.yrel);
 }
 
diff --git a/backends/events/sdl/sdl-events.h b/backends/events/sdl/sdl-events.h
index 375ae78263b..7e96e472f03 100644
--- a/backends/events/sdl/sdl-events.h
+++ b/backends/events/sdl/sdl-events.h
@@ -213,6 +213,20 @@ protected:
 	 */
 	Common::Event _fakeMouseMove;
 
+	/**
+	 * WORKAROUND: Whether stale mouse positions on macOS 26 are corrected in
+	 * handleMouseMotion(). Enabled by default; the testbed engine disables it
+	 * at runtime, through OSystem::kFeatureStaleMousePositionWorkaround, to
+	 * check whether the underlying bug still occurs.
+	 */
+	bool _staleMousePositionWorkaround = true;
+
+public:
+	void setStaleMousePositionWorkaround(bool enable) { _staleMousePositionWorkaround = enable; }
+	bool getStaleMousePositionWorkaround() const { return _staleMousePositionWorkaround; }
+
+protected:
+
 	uint8 _lastHatPosition;
 
 #if SDL_VERSION_ATLEAST(2, 0, 0)
diff --git a/backends/platform/sdl/sdl.cpp b/backends/platform/sdl/sdl.cpp
index f42179cc843..f2c4e5149d1 100644
--- a/backends/platform/sdl/sdl.cpp
+++ b/backends/platform/sdl/sdl.cpp
@@ -219,6 +219,10 @@ bool OSystem_SDL::hasFeature(Feature f) {
 	if (f == kFeatureJoystickDeadzone || f == kFeatureKbdMouseSpeed) {
 		return _eventSource->isJoystickConnected();
 	}
+#if defined(MACOSX) && SDL_VERSION_ATLEAST(2, 0, 4)
+	// WORKAROUND: see SdlEventSource::handleMouseMotion()
+	if (f == kFeatureStaleMousePositionWorkaround) return true;
+#endif
 #if defined(USE_OPENGL_GAME) || defined(USE_OPENGL_SHADERS)
 	/* Even if we are using the SDL graphics manager,
 	 * we are at one initGraphics3d call of supporting OpenGL */
@@ -248,6 +252,9 @@ void OSystem_SDL::setFeatureState(Feature f, bool enable) {
 	case kFeatureTouchpadMode:
 		ConfMan.setBool("touchpad_mouse_mode", enable);
 		break;
+	case kFeatureStaleMousePositionWorkaround:
+		_eventSource->setStaleMousePositionWorkaround(enable);
+		break;
 	default:
 		ModularGraphicsBackend::setFeatureState(f, enable);
 		break;
@@ -259,6 +266,9 @@ bool OSystem_SDL::getFeatureState(Feature f) {
 	case kFeatureTouchpadMode:
 		return ConfMan.getBool("touchpad_mouse_mode");
 		break;
+	case kFeatureStaleMousePositionWorkaround:
+		return _eventSource->getStaleMousePositionWorkaround();
+		break;
 	default:
 		return ModularGraphicsBackend::getFeatureState(f);
 		break;
diff --git a/common/system.h b/common/system.h
index 3fe96306bfd..3aa1449ccc6 100644
--- a/common/system.h
+++ b/common/system.h
@@ -634,6 +634,16 @@ public:
 		* Graphics code is able to rotate the screen
 		*/
 		kFeatureRotationMode,
+
+		/**
+		* WORKAROUND: The backend corrects mouse motion events that carry a
+		* stale position, an Apple bug on macOS 26
+		* (https://github.com/libsdl-org/SDL/issues/15967). The workaround is
+		* enabled by default where available; the testbed engine disables it
+		* at runtime to check whether the underlying bug still occurs (see
+		* EventTests::staleMousePosition()).
+		*/
+		kFeatureStaleMousePositionWorkaround,
 	};
 
 	/**


Commit: 6d61014e37a487e31ed72bf94e6161230084b723
    https://github.com/scummvm/scummvm/commit/6d61014e37a487e31ed72bf94e6161230084b723
Author: Ryan Landay (rlanday at gmail.com)
Date: 2026-07-15T00:42:36+02:00

Commit Message:
TESTBED: Add stale mouse position check

Add an interactive Events test that disables the macOS 26 stale mouse
position workaround (see the previous commit and
https://github.com/libsdl-org/SDL/issues/15967) through
OSystem::kFeatureStaleMousePositionWorkaround, guides the user through
reproducing the underlying OS bug, and counts/logs each stale position
observed in the raw events. A stale position replay is recognized by
its signature: the position jumps far away for a single event and
immediately snaps back near where it came from.

This allows checking periodically whether the bug still occurs and the
workaround is still needed, without recompiling.

Co-Authored-By: Claude Fable 5 <noreply at anthropic.com>

Changed paths:
    engines/testbed/events.cpp
    engines/testbed/events.h


diff --git a/engines/testbed/events.cpp b/engines/testbed/events.cpp
index 57093a6e04b..17780fb8823 100644
--- a/engines/testbed/events.cpp
+++ b/engines/testbed/events.cpp
@@ -264,6 +264,129 @@ TestExitStatus EventTests::mouseEvents() {
 	return passed;
 }
 
+/**
+ * Interactive check for the WORKAROUND for stale mouse positions on macOS 26
+ * (https://github.com/libsdl-org/SDL/issues/15967), worth running a couple of
+ * times a year or after SDL/macOS updates. The backend normally corrects
+ * mouse motion events that carry a stale (old) position; this test disables
+ * the correction through kFeatureStaleMousePositionWorkaround, guides the
+ * user through reproducing the underlying OS bug, and reports each raw stale
+ * position it observes. If the bug stops being reproducible (and the SDL
+ * issue above is resolved), the workaround can be removed.
+ *
+ * A stale position replay is recognized by its signature: the reported
+ * position jumps far away for a single event and immediately snaps back near
+ * where it came from. Genuine movement does not do this: a fast flick
+ * continues from the new position, and leaving/re-entering the window at a
+ * different edge does not snap back.
+ */
+TestExitStatus EventTests::staleMousePosition() {
+	Testsuite::clearScreen();
+
+	if (!g_system->hasFeature(OSystem::kFeatureStaleMousePositionWorkaround)) {
+		Testsuite::logPrintf("Info! Skipping test : Stale mouse position "
+				"(backend has no such workaround)\n");
+		return kTestSkipped;
+	}
+
+	Common::String info = "Stale mouse position check.\n"
+		"Some systems (macOS 26) sometimes report an old, stale mouse position. The backend "
+		"normally corrects this; this test disables the correction and watches the raw events "
+		"to check whether the underlying OS bug still occurs.\n"
+		"1. Run this test in windowed mode.\n"
+		"2. Click another application to unfocus this window, then click this window again.\n"
+		"3. Move the mouse smoothly off a window edge and back in. Repeat with all edges.\n"
+		"A counter increases each time a stale position is detected. Press X to finish.";
+
+	if (Testsuite::handleInteractiveInput(info, "OK", "Skip", kOptionRight)) {
+		Testsuite::logPrintf("Info! Skipping test : Stale mouse position\n");
+		return kTestSkipped;
+	}
+
+	Common::EventManager *eventMan = g_system->getEventManager();
+
+	Common::Point pt(0, 40);
+	Testsuite::writeOnScreen("Unfocus and refocus the window, then move the mouse", pt);
+	pt.y += 15;
+	Testsuite::writeOnScreen("off a window edge and back in. Repeat with all edges.", pt);
+	pt.y += 15;
+	Testsuite::writeOnScreen("Press X to finish", pt);
+	pt.y += 30;
+	Common::Rect rectCount = Testsuite::writeOnScreen("Stale mouse positions detected: 0", pt);
+
+	// Watch the raw, uncorrected events while this test runs
+	g_system->setFeatureState(OSystem::kFeatureStaleMousePositionWorkaround, false);
+
+	const int16 jumpX = g_system->getWidth() / 8, jumpY = g_system->getHeight() / 8;
+	const int16 nearX = g_system->getWidth() / 16, nearY = g_system->getHeight() / 16;
+
+	Common::Point prev(-1, -1), anomalyOrigin, anomalyPos;
+	int anomalyCountdown = 0;
+	int detected = 0;
+
+	bool quitLoop = false;
+	Common::Event event;
+	while (!quitLoop) {
+		// Show mouse
+		CursorMan.showMouse(true);
+		g_system->updateScreen();
+
+		while (eventMan->pollEvent(event)) {
+			if (Engine::shouldQuit()) {
+				quitLoop = true;
+				break;
+			}
+			switch (event.type) {
+			case Common::EVENT_MOUSEMOVE:
+				if (anomalyCountdown > 0 &&
+						ABS(event.mouse.x - anomalyOrigin.x) <= nearX &&
+						ABS(event.mouse.y - anomalyOrigin.y) <= nearY) {
+					// The position jumped far away and immediately snapped
+					// back: the jump was a stale position, not real movement
+					++detected;
+					Testsuite::logDetailedPrintf("Stale mouse position: (%d,%d) -> (%d,%d) -> (%d,%d)\n",
+							anomalyOrigin.x, anomalyOrigin.y, anomalyPos.x, anomalyPos.y,
+							event.mouse.x, event.mouse.y);
+					Testsuite::clearScreen(rectCount);
+					rectCount = Testsuite::writeOnScreen(
+							Common::String::format("Stale mouse positions detected: %d", detected),
+							Common::Point(rectCount.left, rectCount.top));
+					anomalyCountdown = 0;
+				} else if (anomalyCountdown > 0) {
+					// Movement continuing away from the origin: presumably a
+					// genuine fast flick or a window re-entry elsewhere
+					--anomalyCountdown;
+				} else if (prev.x >= 0 &&
+						(ABS(event.mouse.x - prev.x) > jumpX || ABS(event.mouse.y - prev.y) > jumpY)) {
+					anomalyOrigin = prev;
+					anomalyPos = event.mouse;
+					anomalyCountdown = 4;
+				}
+				prev = event.mouse;
+				break;
+			case Common::EVENT_KEYDOWN:
+				if (event.kbd.keycode == Common::KEYCODE_x)
+					quitLoop = true;
+				break;
+			default:
+				break;
+			}
+		}
+	}
+
+	CursorMan.showMouse(false);
+	g_system->setFeatureState(OSystem::kFeatureStaleMousePositionWorkaround, true);
+
+	if (detected) {
+		Testsuite::logPrintf("Info! Stale mouse position : reproduced %d time(s), "
+				"the backend workaround is still needed\n", detected);
+	} else {
+		Testsuite::logPrintf("Info! Stale mouse position : not reproduced; "
+				"if this persists, the backend workaround may no longer be needed\n");
+	}
+	return kTestPassed;
+}
+
 TestExitStatus EventTests::doubleClickTime() {
 	Testsuite::clearScreen();
 
@@ -372,6 +495,7 @@ TestExitStatus EventTests::showMainMenu() {
 
 EventTestSuite::EventTestSuite() {
 	addTest("MouseEvents", &EventTests::mouseEvents);
+	addTest("StaleMousePosition", &EventTests::staleMousePosition);
 	addTest("DoubleClickTime", &EventTests::doubleClickTime);
 	addTest("KeyboardEvents", &EventTests::kbdEvents);
 	addTest("MainmenuEvent", &EventTests::showMainMenu);
diff --git a/engines/testbed/events.h b/engines/testbed/events.h
index 50fc0698b2c..4e29aa1c02b 100644
--- a/engines/testbed/events.h
+++ b/engines/testbed/events.h
@@ -33,6 +33,7 @@ char keystrokeToChar();
 Common::Rect drawFinishZone();
 // will contain function declarations for Event tests
 TestExitStatus mouseEvents();
+TestExitStatus staleMousePosition();
 TestExitStatus doubleClickTime();
 TestExitStatus kbdEvents();
 TestExitStatus showMainMenu();




More information about the Scummvm-git-logs mailing list