Base GCMouse raw input implementation
Some checks failed
Build (All) / Create test plan (push) Has been cancelled
Build (All) / level1 (push) Has been cancelled
Build (All) / level2 (push) Has been cancelled

Fix duplicate button/scroll events when GCMouse active

Fix duplicate events and add thread-safe atomic for GCMouse

Fix GCMouse relative mode sync when connected after mode enabled

Respect SDL_HINT_MOUSE_RELATIVE_SYSTEM_SCALE in GCMouse handler

Fix variable shadowing in GCMouse motion handler
This commit is contained in:
Edgar J San Martin
2025-12-30 17:15:47 -05:00
committed by Sam Lantinga
parent a48dee5ac1
commit ad91384704
5 changed files with 284 additions and 10 deletions

View File

@@ -246,6 +246,22 @@ You are free to modify your Cocoa app with generally no consequence
to SDL. You cannot, however, easily change the SDL window itself.
Functionality may be added in the future to help this.
## Raw Mouse Input
On macOS 11.0 (Big Sur) and later, SDL uses the Game Controller framework's
GCMouse API to provide raw, unaccelerated mouse input in relative mode. This
is ideal for games and applications requiring precise 1:1 mouse movement.
On older macOS versions, SDL falls back to NSEvent-based mouse input, which
includes system mouse acceleration.
To use accelerated (system-scaled) mouse movement on macOS 11.0+, set the hint:
```c
SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_SYSTEM_SCALE, "1");
```
# Bug reports
Bugs are tracked at [the GitHub issue tracker](https://github.com/libsdl-org/SDL/issues/).

View File

@@ -32,6 +32,11 @@ extern void Cocoa_HandleMouseWheel(SDL_Window *window, NSEvent *event);
extern void Cocoa_HandleMouseWarp(CGFloat x, CGFloat y);
extern void Cocoa_QuitMouse(SDL_VideoDevice *_this);
extern void Cocoa_InitGCMouse(void);
extern bool Cocoa_GCMouseRelativeMode(void);
extern bool Cocoa_HasGCMouse(void);
extern void Cocoa_QuitGCMouse(void);
struct SDL_CursorData
{
NSTimer *frameTimer;

View File

@@ -27,6 +27,8 @@
#include "../../events/SDL_mouse_c.h"
#import <GameController/GameController.h>
#if 0
#define DEBUG_COCOAMOUSE
#endif
@@ -254,6 +256,219 @@ static SDL_Cursor *Cocoa_CreateDefaultCursor(void)
return Cocoa_CreateSystemCursor(id);
}
// GCMouse support for raw (unaccelerated) mouse input on macOS 11.0+
static id cocoa_mouse_connect_observer = nil;
static id cocoa_mouse_disconnect_observer = nil;
// Atomic for thread-safe access during high-frequency mouse input
static SDL_AtomicInt cocoa_gcmouse_relative_mode;
static bool cocoa_has_gcmouse = false;
static SDL_MouseWheelDirection cocoa_mouse_scroll_direction = SDL_MOUSEWHEEL_NORMAL;
static void Cocoa_UpdateGCMouseScrollDirection(void)
{
Boolean keyExistsAndHasValidFormat = NO;
Boolean naturalScrollDirection = CFPreferencesGetAppBooleanValue(
CFSTR("com.apple.swipescrolldirection"),
kCFPreferencesAnyApplication,
&keyExistsAndHasValidFormat);
if (!keyExistsAndHasValidFormat) {
// Couldn't read the preference, assume natural scrolling direction
naturalScrollDirection = YES;
}
if (naturalScrollDirection) {
cocoa_mouse_scroll_direction = SDL_MOUSEWHEEL_FLIPPED;
} else {
cocoa_mouse_scroll_direction = SDL_MOUSEWHEEL_NORMAL;
}
}
static bool Cocoa_SetGCMouseRelativeMode(bool enabled)
{
SDL_SetAtomicInt(&cocoa_gcmouse_relative_mode, enabled ? 1 : 0);
return true;
}
static void Cocoa_OnGCMouseButtonChanged(SDL_MouseID mouseID, Uint8 button,
BOOL pressed)
{
Uint64 timestamp = SDL_GetTicksNS();
SDL_SendMouseButton(timestamp, SDL_GetMouseFocus(), mouseID, button,
pressed);
}
static void Cocoa_OnGCMouseConnected(GCMouse *mouse)
API_AVAILABLE(macos(11.0))
{
SDL_MouseID mouseID = (SDL_MouseID)(uintptr_t)mouse;
SDL_AddMouse(mouseID, NULL);
cocoa_has_gcmouse = true;
// Sync with SDL's current relative mode state (may have been set before
// GCMouse connected)
SDL_Mouse *sdl_mouse = SDL_GetMouse();
if (sdl_mouse && sdl_mouse->relative_mode) {
SDL_SetAtomicInt(&cocoa_gcmouse_relative_mode, 1);
}
mouse.mouseInput.leftButton.pressedChangedHandler =
^(GCControllerButtonInput *button, float value, BOOL pressed) {
Cocoa_OnGCMouseButtonChanged(mouseID, SDL_BUTTON_LEFT, pressed);
};
mouse.mouseInput.middleButton.pressedChangedHandler =
^(GCControllerButtonInput *button, float value, BOOL pressed) {
Cocoa_OnGCMouseButtonChanged(mouseID, SDL_BUTTON_MIDDLE, pressed);
};
mouse.mouseInput.rightButton.pressedChangedHandler =
^(GCControllerButtonInput *button, float value, BOOL pressed) {
Cocoa_OnGCMouseButtonChanged(mouseID, SDL_BUTTON_RIGHT, pressed);
};
int auxiliary_button = SDL_BUTTON_X1;
for (GCControllerButtonInput *btn in mouse.mouseInput.auxiliaryButtons) {
const int current_button = auxiliary_button;
btn.pressedChangedHandler =
^(GCControllerButtonInput *button, float value, BOOL pressed) {
Cocoa_OnGCMouseButtonChanged(mouseID, current_button, pressed);
};
++auxiliary_button;
}
mouse.mouseInput.mouseMovedHandler =
^(GCMouseInput *mouseInput, float deltaX, float deltaY) {
if (Cocoa_GCMouseRelativeMode()) {
// Skip raw input if user wants system-scaled (accelerated) deltas
SDL_Mouse *m = SDL_GetMouse();
if (m && m->enable_relative_system_scale) {
return;
}
Uint64 timestamp = SDL_GetTicksNS();
SDL_SendMouseMotion(timestamp, SDL_GetMouseFocus(), mouseID,
true, deltaX, -deltaY);
}
};
mouse.mouseInput.scroll.valueChangedHandler =
^(GCControllerDirectionPad *dpad, float xValue, float yValue) {
Uint64 timestamp = SDL_GetTicksNS();
// Raw scroll values: vertical in first axis, horizontal in second.
// Vertical values are inverted compared to SDL conventions.
float vertical = -xValue;
float horizontal = yValue;
if (cocoa_mouse_scroll_direction == SDL_MOUSEWHEEL_FLIPPED) {
vertical = -vertical;
horizontal = -horizontal;
}
SDL_SendMouseWheel(timestamp, SDL_GetMouseFocus(), mouseID,
horizontal, vertical,
cocoa_mouse_scroll_direction);
};
Cocoa_UpdateGCMouseScrollDirection();
// Use high-priority queue for low-latency input
dispatch_queue_t queue = dispatch_queue_create("org.libsdl.input.mouse",
DISPATCH_QUEUE_SERIAL);
dispatch_set_target_queue(queue,
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0));
mouse.handlerQueue = queue;
}
static void Cocoa_OnGCMouseDisconnected(GCMouse *mouse)
API_AVAILABLE(macos(11.0))
{
SDL_MouseID mouseID = (SDL_MouseID)(uintptr_t)mouse;
mouse.mouseInput.mouseMovedHandler = nil;
mouse.mouseInput.leftButton.pressedChangedHandler = nil;
mouse.mouseInput.middleButton.pressedChangedHandler = nil;
mouse.mouseInput.rightButton.pressedChangedHandler = nil;
mouse.mouseInput.scroll.valueChangedHandler = nil;
for (GCControllerButtonInput *button in mouse.mouseInput.auxiliaryButtons) {
button.pressedChangedHandler = nil;
}
SDL_RemoveMouse(mouseID);
// Check if any GCMouse devices remain
if (@available(macOS 11.0, *)) {
cocoa_has_gcmouse = ([GCMouse mice].count > 0);
}
}
void Cocoa_InitGCMouse(void)
{
@autoreleasepool {
if (@available(macOS 11.0, *)) {
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
cocoa_mouse_connect_observer = [center
addObserverForName:GCMouseDidConnectNotification
object:nil
queue:nil
usingBlock:^(NSNotification *note) {
GCMouse *mouse = note.object;
Cocoa_OnGCMouseConnected(mouse);
}];
cocoa_mouse_disconnect_observer = [center
addObserverForName:GCMouseDidDisconnectNotification
object:nil
queue:nil
usingBlock:^(NSNotification *note) {
GCMouse *mouse = note.object;
Cocoa_OnGCMouseDisconnected(mouse);
}];
// Enumerate already-connected mice
for (GCMouse *mouse in [GCMouse mice]) {
Cocoa_OnGCMouseConnected(mouse);
}
}
}
}
bool Cocoa_GCMouseRelativeMode(void)
{
return SDL_GetAtomicInt(&cocoa_gcmouse_relative_mode) != 0;
}
bool Cocoa_HasGCMouse(void)
{
return cocoa_has_gcmouse;
}
void Cocoa_QuitGCMouse(void)
{
@autoreleasepool {
if (@available(macOS 11.0, *)) {
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
if (cocoa_mouse_connect_observer) {
[center removeObserver:cocoa_mouse_connect_observer
name:GCMouseDidConnectNotification
object:nil];
cocoa_mouse_connect_observer = nil;
}
if (cocoa_mouse_disconnect_observer) {
[center removeObserver:cocoa_mouse_disconnect_observer
name:GCMouseDidDisconnectNotification
object:nil];
cocoa_mouse_disconnect_observer = nil;
}
for (GCMouse *mouse in [GCMouse mice]) {
Cocoa_OnGCMouseDisconnected(mouse);
}
cocoa_has_gcmouse = false;
SDL_SetAtomicInt(&cocoa_gcmouse_relative_mode, 0);
}
}
}
static void Cocoa_FreeCursor(SDL_Cursor *cursor)
{
@autoreleasepool {
@@ -360,19 +575,29 @@ static bool Cocoa_SetRelativeMouseMode(bool enabled)
{
CGError result;
// Update GCMouse relative mode state if available
if (Cocoa_HasGCMouse()) {
Cocoa_SetGCMouseRelativeMode(enabled);
}
if (enabled) {
SDL_Window *window = SDL_GetKeyboardFocus();
if (window) {
/* We will re-apply the relative mode when the window finishes being moved,
* if it is being moved right now.
/* We will re-apply the relative mode when the window finishes
* being moved, if it is being moved right now.
*/
SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal;
SDL_CocoaWindowData *data =
(__bridge SDL_CocoaWindowData *)window->internal;
if ([data.listener isMovingOrFocusClickPending]) {
return true;
}
// make sure the mouse isn't at the corner of the window, as this can confuse things if macOS thinks a window resize is happening on the first click.
const CGPoint point = CGPointMake((float)(window->x + (window->w / 2)), (float)(window->y + (window->h / 2)));
// Make sure the mouse isn't at the corner of the window, as this
// can confuse things if macOS thinks a window resize is happening
// on the first click.
const CGPoint point = CGPointMake(
(float)(window->x + (window->w / 2)),
(float)(window->y + (window->h / 2)));
Cocoa_HandleMouseWarp(point.x, point.y);
CGWarpMouseCursorPosition(point);
}
@@ -590,6 +815,17 @@ void Cocoa_HandleMouseEvent(SDL_VideoDevice *_this, NSEvent *event)
return;
}
// When GCMouse is active in relative mode, it handles motion events
// directly with raw (unaccelerated) deltas. Skip NSEvent-based motion
// unless the user wants system-scaled (accelerated) input.
if (Cocoa_HasGCMouse() && Cocoa_GCMouseRelativeMode()) {
if (!mouse->enable_relative_system_scale) {
// GCMouse is providing raw input, skip NSEvent deltas
return;
}
// SYSTEM_SCALE is enabled: use NSEvent accelerated deltas instead
}
// Ignore events that aren't inside the client area (i.e. title bar.)
if ([event window]) {
NSRect windowRect = [[[event window] contentView] frame];
@@ -606,14 +842,21 @@ void Cocoa_HandleMouseEvent(SDL_VideoDevice *_this, NSEvent *event)
deltaX += (lastMoveX - data->lastWarpX);
deltaY += ((videodata.mainDisplayHeight - lastMoveY) - data->lastWarpY);
DLog("Motion was (%g, %g), offset to (%g, %g)", [event deltaX], [event deltaY], deltaX, deltaY);
DLog("Motion was (%g, %g), offset to (%g, %g)", [event deltaX],
[event deltaY], deltaX, deltaY);
}
SDL_SendMouseMotion(Cocoa_GetEventTimestamp([event timestamp]), mouse->focus, mouseID, true, deltaX, deltaY);
SDL_SendMouseMotion(Cocoa_GetEventTimestamp([event timestamp]),
mouse->focus, mouseID, true, deltaX, deltaY);
}
void Cocoa_HandleMouseWheel(SDL_Window *window, NSEvent *event)
{
// GCMouse handles scroll events directly, skip NSEvent path to avoid duplicates
if (Cocoa_HasGCMouse()) {
return;
}
SDL_MouseID mouseID = SDL_DEFAULT_MOUSE_ID;
SDL_MouseWheelDirection direction;
CGFloat x, y;

View File

@@ -209,10 +209,14 @@ static bool Cocoa_VideoInit(SDL_VideoDevice *_this)
return false;
}
// Assume we have a mouse and keyboard
// We could use GCMouse and GCKeyboard if we needed to, as is done in SDL_uikitevents.m
// Initialize GCMouse for raw input on macOS 11.0+
Cocoa_InitGCMouse();
// Add default keyboard and mouse if GCMouse didn't provide any
SDL_AddKeyboard(SDL_DEFAULT_KEYBOARD_ID, NULL);
SDL_AddMouse(SDL_DEFAULT_MOUSE_ID, NULL);
if (!Cocoa_HasGCMouse()) {
SDL_AddMouse(SDL_DEFAULT_MOUSE_ID, NULL);
}
data.allow_spaces = SDL_GetHintBoolean(SDL_HINT_VIDEO_MAC_FULLSCREEN_SPACES, true);
data.trackpad_is_touch_only = SDL_GetHintBoolean(SDL_HINT_TRACKPAD_IS_TOUCH_ONLY, false);
@@ -233,6 +237,7 @@ void Cocoa_VideoQuit(SDL_VideoDevice *_this)
SDL_CocoaVideoData *data = (__bridge SDL_CocoaVideoData *)_this->internal;
Cocoa_QuitModes(_this);
Cocoa_QuitKeyboard(_this);
Cocoa_QuitGCMouse();
Cocoa_QuitMouse(_this);
Cocoa_QuitPen(_this);
SDL_DestroyMutex(data.swaplock);

View File

@@ -1717,6 +1717,11 @@ static NSCursor *Cocoa_GetDesiredCursor(void)
static void Cocoa_SendMouseButtonClicks(SDL_Mouse *mouse, NSEvent *theEvent, SDL_Window *window, Uint8 button, bool down)
{
// GCMouse handles button events directly, skip NSEvent path to avoid duplicates
if (Cocoa_HasGCMouse()) {
return;
}
SDL_MouseID mouseID = SDL_DEFAULT_MOUSE_ID;
//const int clicks = (int)[theEvent clickCount];
SDL_Window *focus = SDL_GetKeyboardFocus();