diff --git a/src/video/cocoa/SDL_cocoaevents.m b/src/video/cocoa/SDL_cocoaevents.m --- a/src/video/cocoa/SDL_cocoaevents.m +++ b/src/video/cocoa/SDL_cocoaevents.m @@ -334,6 +334,10 @@ /* Pass through to NSApp to make sure everything stays in sync */ [NSApp sendEvent:event]; } + + /* Relative mouse events are handled separately because they're potentially + high frequency (1000Hz), and they're collected in a background thread. */ + Cocoa_PumpRawMouseMotion(); }} #endif /* SDL_VIDEO_DRIVER_COCOA */ diff --git a/src/video/cocoa/SDL_cocoamouse.h b/src/video/cocoa/SDL_cocoamouse.h --- a/src/video/cocoa/SDL_cocoamouse.h +++ b/src/video/cocoa/SDL_cocoamouse.h @@ -31,6 +31,8 @@ extern void Cocoa_HandleMouseWarp(CGFloat x, CGFloat y); extern void Cocoa_QuitMouse(_THIS); +extern void Cocoa_PumpRawMouseMotion(void); + typedef struct { /* Wether we've seen a cursor warp since the last move event. */ SDL_bool seenWarp; @@ -41,6 +43,8 @@ CGFloat lastMoveX; CGFloat lastMoveY; void *tapdata; + /* For raw motion */ + void *rawMouseListener; } SDL_MouseData; @interface NSCursor (InvisibleCursor) diff --git a/src/video/cocoa/SDL_cocoamouse.m b/src/video/cocoa/SDL_cocoamouse.m --- a/src/video/cocoa/SDL_cocoamouse.m +++ b/src/video/cocoa/SDL_cocoamouse.m @@ -22,6 +22,8 @@ #if SDL_VIDEO_DRIVER_COCOA +#include + #include "SDL_assert.h" #include "SDL_events.h" #include "SDL_cocoamouse.h" @@ -37,6 +39,166 @@ #define DLog(...) do { } while (0) #endif +/** + * Helper object that spawns a background thread which uses an IOHIDManager + * to listen for raw mouse motion. + * + * The reason for scheduling the IOHIDManager value change notifications + * in a background thread is, if you do it on the main thread, you can lose + * events if you don't run the runloop often enough - this would lead to a + * bad situation where the frequency that an application calls SDL_PumpEvents + * at could affect the mouse sensitivity! + * + * The -getAndClearMovementX:Y: method returns the amount of motion accumulated + * in the X and Y axes since the last call to that method (or the listener was + * created). + * + * Call -stop when you are done using the object, which will shut down the + * background thread, before releasing it. + */ +@interface Cocoa_RawMouseListener : NSObject +{ +@private + BOOL shouldExit; + int accumulatedRelX; + int accumulatedRelY; +} + +- (void) stop; +- (void) getAndClearMovementX: (int *)Xout Y: (int *)Yout; + +@end + +@implementation Cocoa_RawMouseListener + +- (id) init +{ + self = [super init]; + if (!self) return nil; + + [NSThread detachNewThreadSelector: @selector(rawMouseListenerThreadMain:) toTarget:self withObject: nil]; + + return self; +} + +- (void) stop +{ + shouldExit = YES; +} + +- (void) getAndClearMovementX: (int *)Xout Y: (int *)Yout +{ + /* i.e., atomically copy the value of accumulatedRelX into Xout and then zero accumulatedRelX */ + *Xout = OSAtomicAnd32Orig(0, (uint32_t*)&accumulatedRelX); + *Yout = OSAtomicAnd32Orig(0, (uint32_t*)&accumulatedRelY); +} + +static void +Cocoa_RawValueCallback(void *context, IOReturn res, void *sender, IOHIDValueRef val) +{ + if (res != kIOReturnSuccess) + return; + + Cocoa_RawMouseListener *listener = (Cocoa_RawMouseListener *)context; + + IOHIDElementRef elem = IOHIDValueGetElement(val); + const CFIndex value = IOHIDValueGetIntegerValue(val); + const uint32_t page = IOHIDElementGetUsagePage(elem); + const uint32_t usage = IOHIDElementGetUsage(elem); + + if (page == kHIDPage_GenericDesktop) + { + switch (usage) + { + case kHIDUsage_GD_X: + OSAtomicAdd32(value, &listener->accumulatedRelX); + break; + case kHIDUsage_GD_Y: + OSAtomicAdd32(value, &listener->accumulatedRelY); + break; + } + } +} + +static SDL_bool +ConfigHIDManager(IOHIDManagerRef hidman, CFDictionaryRef matchingDict, Cocoa_RawMouseListener *listener) +{ + CFRunLoopRef runloop = CFRunLoopGetCurrent(); + + if (IOHIDManagerOpen(hidman, kIOHIDOptionsTypeNone) != kIOReturnSuccess) { + return SDL_FALSE; + } + + IOHIDManagerRegisterInputValueCallback(hidman, Cocoa_RawValueCallback, listener); + IOHIDManagerScheduleWithRunLoop(hidman, runloop, kCFRunLoopDefaultMode); + IOHIDManagerSetDeviceMatching(hidman, matchingDict); + + return SDL_TRUE; +} + +static CFDictionaryRef +CreateHIDDeviceMatchDictionary(const UInt32 page, const UInt32 usage) +{ + CFDictionaryRef retval = NULL; + CFNumberRef pageNumRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &page); + CFNumberRef usageNumRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &usage); + const void *keys[2] = { (void *) CFSTR(kIOHIDDeviceUsagePageKey), (void *) CFSTR(kIOHIDDeviceUsageKey) }; + const void *vals[2] = { (void *) pageNumRef, (void *) usageNumRef }; + + if (pageNumRef && usageNumRef) { + retval = CFDictionaryCreate(kCFAllocatorDefault, keys, vals, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + } + + if (pageNumRef) { + CFRelease(pageNumRef); + } + if (usageNumRef) { + CFRelease(usageNumRef); + } + return retval; +} + +static IOHIDManagerRef +CreateHIDManager(Cocoa_RawMouseListener *listener) +{ + CFDictionaryRef dict = CreateHIDDeviceMatchDictionary(kHIDPage_GenericDesktop, kHIDUsage_GD_Mouse); + IOHIDManagerRef hidman = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone); + + if (hidman && dict) { + ConfigHIDManager(hidman, dict, listener); + } + + if (dict) { + CFRelease(dict); + } + + return hidman; +} + +- (void) rawMouseListenerThreadMain: (id)param +{ @autoreleasepool +{ + /* N.B.: NSThread retains self (the Cocoa_RawMouseListener instance) + * until this method returns. When this method returns, the thread exits. + */ + + IOHIDManagerRef hidman = CreateHIDManager(self); + if (hidman) + { + while (!shouldExit) + { + /* Check shouldExit flag every second */ + CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1, NO); + } + /* Shut down */ + IOHIDManagerUnscheduleFromRunLoop(hidman, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); + IOHIDManagerClose(hidman, kIOHIDOptionsTypeNone); + CFRelease(hidman); + } +}} + +@end + @implementation NSCursor (InvisibleCursor) + (NSCursor *)invisibleCursor { @@ -233,6 +395,58 @@ Cocoa_WarpMouseGlobal(x + window->x, y + window->y); } +void Cocoa_PumpRawMouseMotion(void) +{ + SDL_Mouse *mouse = SDL_GetMouse(); + if (!mouse) { + return; + } + + SDL_MouseData *driverdata = (SDL_MouseData *)mouse->driverdata; + if (!mouse->driverdata) { + return; + } + + /* Read how much movement has happened since the last call to Cocoa_PumpRawMouseMotion */ + + int x = 0, y = 0; + Cocoa_RawMouseListener *listener = (Cocoa_RawMouseListener *)driverdata->rawMouseListener; + if (!listener) { + return; + } + + [listener getAndClearMovementX: &x Y: &y]; + + /* Only post a motion event if we're in relative mode and the app is in the foreground */ + + if (mouse->focus == NULL) { + return; + } + if (!(mouse->focus->flags & SDL_WINDOW_MOUSE_FOCUS)) { + return; + } + if (!mouse->relative_mode) { + return; + } + + SDL_SendMouseMotion(mouse->focus, mouse->mouseID, 1, x, y); +} + +static void +Cocoa_InitRawMouseCallback(SDL_MouseData *driverdata) +{ + driverdata->rawMouseListener = [[Cocoa_RawMouseListener alloc] init]; +} + +static void +Cocoa_QuitRawMouseCallback(SDL_MouseData *driverdata) +{ + Cocoa_RawMouseListener *listener = (Cocoa_RawMouseListener *)driverdata->rawMouseListener; + [listener stop]; + [listener release]; + driverdata->rawMouseListener = nil; +} + static int Cocoa_SetRelativeMouseMode(SDL_bool enabled) { @@ -272,6 +486,20 @@ } else { [NSCursor unhide]; } + + SDL_Mouse *mouse = SDL_GetMouse(); + if (!mouse) { + return SDL_FALSE; + } + + SDL_MouseData *driverdata = (SDL_MouseData *)mouse->driverdata; + if (!mouse->driverdata) { + return SDL_FALSE; + } + + if (enabled && driverdata->rawMouseListener == NULL) { + Cocoa_InitRawMouseCallback(driverdata); + } return 0; } @@ -366,11 +594,13 @@ driverdata->lastMoveY = location.y; DLog("Last seen mouse: (%g, %g)", location.x, location.y); + /* The following is now handled by Cocoa_PumpRawMouseMotion */ +#if 0 /* Non-relative movement is handled in -[Cocoa_WindowListener mouseMoved:] */ if (!mouse->relative_mode) { return; } - + /* Ignore events that aren't inside the client area (i.e. title bar.) */ if ([event window]) { NSRect windowRect = [[[event window] contentView] frame]; @@ -390,6 +620,7 @@ } SDL_SendMouseMotion(mouse->focus, mouse->mouseID, 1, (int)deltaX, (int)deltaY); +#endif } void @@ -432,8 +663,10 @@ { SDL_Mouse *mouse = SDL_GetMouse(); if (mouse) { - if (mouse->driverdata) { - Cocoa_QuitMouseEventTap(((SDL_MouseData*)mouse->driverdata)); + SDL_MouseData *driverdata = (SDL_MouseData*)mouse->driverdata; + if (driverdata) { + Cocoa_QuitRawMouseCallback(driverdata); + Cocoa_QuitMouseEventTap(driverdata); } SDL_free(mouse->driverdata); diff --git a/test/testrelative.c b/test/testrelative.c --- a/test/testrelative.c +++ b/test/testrelative.c @@ -76,6 +76,11 @@ switch(event.type) { case SDL_MOUSEMOTION: { + static int last; + const int now = SDL_GetTicks(); + printf("SDL_MOUSEMOTION (%d %d) (%d ms since last event)\n", event.motion.xrel, event.motion.yrel, now - last); + last = now; + rect.x += event.motion.xrel; rect.y += event.motion.yrel; }