Jelajahi Sumber

Base GCMouse raw input implementation

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
Edgar J San Martin 2 bulan lalu
induk
melakukan
ad91384704

+ 16 - 0
docs/README-macos.md

@@ -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/).

+ 5 - 0
src/video/cocoa/SDL_cocoamouse.h

@@ -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;

+ 250 - 7
src/video/cocoa/SDL_cocoamouse.m

@@ -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;

+ 8 - 3
src/video/cocoa/SDL_cocoavideo.m

@@ -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);

+ 5 - 0
src/video/cocoa/SDL_cocoawindow.m

@@ -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();