From 8eeda02d509409c537bb05fa99ca7119643cafd2 Mon Sep 17 00:00:00 2001 From: Lior Halphon Date: Sat, 30 Dec 2017 16:23:17 +0200 Subject: [PATCH] Added Joypad support, closes #9 --- Cocoa/GBJoystickListener.h | 8 + Cocoa/GBPreferencesWindow.h | 5 +- Cocoa/GBPreferencesWindow.m | 114 ++++++ Cocoa/GBView.h | 3 +- Cocoa/GBView.m | 47 +++ Cocoa/Preferences.xib | 50 ++- Cocoa/joypad.m | 752 ++++++++++++++++++++++++++++++++++++ Makefile | 2 +- 8 files changed, 965 insertions(+), 16 deletions(-) create mode 100644 Cocoa/GBJoystickListener.h create mode 100755 Cocoa/joypad.m diff --git a/Cocoa/GBJoystickListener.h b/Cocoa/GBJoystickListener.h new file mode 100644 index 0000000..690fde9 --- /dev/null +++ b/Cocoa/GBJoystickListener.h @@ -0,0 +1,8 @@ +#import + +@protocol GBJoystickListener + +- (void) joystick:(NSString *)joystick_name button: (unsigned)button changedState: (bool) state; +- (void) joystick:(NSString *)joystick_name axis: (unsigned)axis movedTo: (signed) value; + +@end diff --git a/Cocoa/GBPreferencesWindow.h b/Cocoa/GBPreferencesWindow.h index 52d26e0..88cd926 100644 --- a/Cocoa/GBPreferencesWindow.h +++ b/Cocoa/GBPreferencesWindow.h @@ -1,9 +1,12 @@ #import +#import "GBJoystickListener.h" -@interface GBPreferencesWindow : NSWindow +@interface GBPreferencesWindow : NSWindow @property IBOutlet NSTableView *controlsTableView; @property IBOutlet NSPopUpButton *graphicsFilterPopupButton; @property (strong) IBOutlet NSButton *aspectRatioCheckbox; @property (strong) IBOutlet NSPopUpButton *highpassFilterPopupButton; @property (strong) IBOutlet NSPopUpButton *colorCorrectionPopupButton; +@property (strong) IBOutlet NSButton *configureJoypadButton; +@property (strong) IBOutlet NSButton *skipButton; @end diff --git a/Cocoa/GBPreferencesWindow.m b/Cocoa/GBPreferencesWindow.m index af88ab3..e85d2e1 100644 --- a/Cocoa/GBPreferencesWindow.m +++ b/Cocoa/GBPreferencesWindow.m @@ -7,6 +7,9 @@ { bool is_button_being_modified; NSInteger button_being_modified; + signed joystick_configuration_state; + NSString *joystick_being_configured; + signed last_axis; NSPopUpButton *_graphicsFilterPopupButton; NSPopUpButton *_highpassFilterPopupButton; @@ -36,6 +39,15 @@ return filters; } +- (void)close +{ + joystick_configuration_state = -1; + [self.configureJoypadButton setEnabled:YES]; + [self.skipButton setEnabled:NO]; + [self.configureJoypadButton setTitle:@"Configure Joypad"]; + [super close]; +} + - (NSPopUpButton *)graphicsFilterPopupButton { return _graphicsFilterPopupButton; @@ -149,6 +161,108 @@ } +- (IBAction) configureJoypad:(id)sender +{ + [self.configureJoypadButton setEnabled:NO]; + [self.skipButton setEnabled:YES]; + joystick_being_configured = nil; + [self advanceConfigurationStateMachine]; + last_axis = -1; +} + +- (IBAction) skipButton:(id)sender +{ + [self advanceConfigurationStateMachine]; +} + +- (void) advanceConfigurationStateMachine +{ + joystick_configuration_state++; + if (joystick_configuration_state < GBButtonCount) { + [self.configureJoypadButton setTitle:[NSString stringWithFormat:@"Press Button for %@", GBButtonNames[joystick_configuration_state]]]; + } + else if (joystick_configuration_state == GBButtonCount) { + [self.configureJoypadButton setTitle:@"Move the Analog Stick"]; + } + else { + joystick_configuration_state = -1; + [self.configureJoypadButton setEnabled:YES]; + [self.skipButton setEnabled:NO]; + [self.configureJoypadButton setTitle:@"Configure Joypad"]; + } +} + +- (void) joystick:(NSString *)joystick_name button: (unsigned)button changedState: (bool) state +{ + if (!state) return; + if (joystick_configuration_state == -1) return; + if (joystick_configuration_state == GBButtonCount) return; + if (!joystick_being_configured) { + joystick_being_configured = joystick_name; + } + else if (![joystick_being_configured isEqualToString:joystick_name]) { + return; + } + + NSMutableDictionary *all_mappings = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBJoypadMappings"] mutableCopy]; + + if (!all_mappings) { + all_mappings = [[NSMutableDictionary alloc] init]; + } + + NSMutableDictionary *mapping = [[all_mappings objectForKey:joystick_name] mutableCopy]; + + if (!mapping) { + mapping = [[NSMutableDictionary alloc] init]; + } + + mapping[GBButtonNames[joystick_configuration_state]] = @(button); + + all_mappings[joystick_name] = mapping; + [[NSUserDefaults standardUserDefaults] setObject:all_mappings forKey:@"GBJoypadMappings"]; + [self advanceConfigurationStateMachine]; +} + +- (void) joystick:(NSString *)joystick_name axis: (unsigned)axis movedTo: (signed) value +{ + if (abs(value) < 0x4000) return; + if (joystick_configuration_state != GBButtonCount) return; + if (!joystick_being_configured) { + joystick_being_configured = joystick_name; + } + else if (![joystick_being_configured isEqualToString:joystick_name]) { + return; + } + + if (last_axis == -1) { + last_axis = axis; + return; + } + + if (axis == last_axis) { + return; + } + + NSMutableDictionary *all_mappings = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBJoypadMappings"] mutableCopy]; + + if (!all_mappings) { + all_mappings = [[NSMutableDictionary alloc] init]; + } + + NSMutableDictionary *mapping = [[all_mappings objectForKey:joystick_name] mutableCopy]; + + if (!mapping) { + mapping = [[NSMutableDictionary alloc] init]; + } + + mapping[@"XAxis"] = @(MIN(axis, last_axis)); + mapping[@"YAxis"] = @(MAX(axis, last_axis)); + + all_mappings[joystick_name] = mapping; + [[NSUserDefaults standardUserDefaults] setObject:all_mappings forKey:@"GBJoypadMappings"]; + [self advanceConfigurationStateMachine]; +} + - (NSButton *)aspectRatioCheckbox { return _aspectRatioCheckbox; diff --git a/Cocoa/GBView.h b/Cocoa/GBView.h index a1d4f55..813f8c0 100644 --- a/Cocoa/GBView.h +++ b/Cocoa/GBView.h @@ -1,8 +1,9 @@ #import #include +#import "GBJoystickListener.h" #import "GBShader.h" -@interface GBView : NSOpenGLView +@interface GBView : NSOpenGLView - (void) flip; - (uint32_t *) pixels; @property GB_gameboy_t *gb; diff --git a/Cocoa/GBView.m b/Cocoa/GBView.m index 2e907ed..dfcc2f2 100644 --- a/Cocoa/GBView.m +++ b/Cocoa/GBView.m @@ -11,6 +11,7 @@ BOOL mouse_hidden; NSTrackingArea *tracking_area; BOOL _mouseHidingEnabled; + bool enableAnalog; } - (void) awakeFromNib @@ -213,6 +214,51 @@ } } +- (void) joystick:(NSString *)joystick_name button: (unsigned)button changedState: (bool) state +{ + NSDictionary *mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBJoypadMappings"][joystick_name]; + + for (GBButton i = 0; i < GBButtonCount; i++) { + NSNumber *mapped_button = [mapping objectForKey:GBButtonNames[i]]; + if (mapped_button && [mapped_button integerValue] == button) { + switch (i) { + case GBTurbo: + GB_set_turbo_mode(_gb, state, false); + break; + + default: + if (i < GB_KEY_A) { + enableAnalog = false; + } + GB_set_key_state(_gb, (GB_key_t)i, state); + break; + } + } + } +} + +- (void) joystick:(NSString *)joystick_name axis: (unsigned)axis movedTo: (signed) value +{ + NSDictionary *mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBJoypadMappings"][joystick_name]; + NSNumber *x_axis = [mapping objectForKey:@"XAxis"]; + NSNumber *y_axis = [mapping objectForKey:@"YAxis"]; + + if (value > 0x4000 || value < -0x4000) { + enableAnalog = true; + } + if (!enableAnalog) return; + + if (x_axis && [x_axis integerValue] == axis) { + GB_set_key_state(_gb, GB_KEY_LEFT, value < -0x4000); + GB_set_key_state(_gb, GB_KEY_RIGHT, value > 0x4000); + } + else if (y_axis && [y_axis integerValue] == axis) { + GB_set_key_state(_gb, GB_KEY_UP, value < -0x4000); + GB_set_key_state(_gb, GB_KEY_DOWN, value > 0x4000); + } +} + + - (BOOL)acceptsFirstResponder { return YES; @@ -259,4 +305,5 @@ { return _mouseHidingEnabled; } + @end diff --git a/Cocoa/Preferences.xib b/Cocoa/Preferences.xib index 78020b6..57c42db 100644 --- a/Cocoa/Preferences.xib +++ b/Cocoa/Preferences.xib @@ -1,8 +1,8 @@ - + - + @@ -17,14 +17,14 @@ - + - + - + @@ -33,7 +33,7 @@ - + @@ -67,7 +67,7 @@ - + @@ -76,7 +76,7 @@ - + @@ -97,7 +97,7 @@ - + @@ -117,7 +117,7 @@ - + @@ -137,7 +137,7 @@ - + @@ -146,7 +146,7 @@ - + @@ -203,15 +203,39 @@ + + + + diff --git a/Cocoa/joypad.m b/Cocoa/joypad.m new file mode 100755 index 0000000..74f6d8d --- /dev/null +++ b/Cocoa/joypad.m @@ -0,0 +1,752 @@ +/* + Joypad support is based on a stripped-down version of SDL's Darwin implementation + of the Joystick API, under the following license: +*/ + +/* + Simple DirectMedia Layer + Copyright (C) 1997-2017 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#include +#include +#include +#include +#include "GBJoystickListener.h" + +typedef signed SDL_JoystickID; +typedef struct _SDL_Joystick SDL_Joystick; + +typedef struct _SDL_JoystickAxisInfo +{ + int16_t initial_value; /* Initial axis state */ + int16_t value; /* Current axis state */ + int16_t zero; /* Zero point on the axis (-32768 for triggers) */ + bool has_initial_value; /* Whether we've seen a value on the axis yet */ + bool sent_initial_value; /* Whether we've sent the initial axis value */ +} SDL_JoystickAxisInfo; + +struct _SDL_Joystick +{ + SDL_JoystickID instance_id; /* Device instance, monotonically increasing from 0 */ + char *name; /* Joystick name - system dependent */ + + int naxes; /* Number of axis controls on the joystick */ + SDL_JoystickAxisInfo *axes; + + int nbuttons; /* Number of buttons on the joystick */ + uint8_t *buttons; /* Current button states */ + + struct joystick_hwdata *hwdata; /* Driver dependent information */ + + int ref_count; /* Reference count for multiple opens */ + + bool is_game_controller; + bool force_recentering; /* SDL_TRUE if this device needs to have its state reset to 0 */ + struct _SDL_Joystick *next; /* pointer to next joystick we have allocated */ +}; + +typedef struct { + uint8_t data[16]; +} SDL_JoystickGUID; + +struct recElement +{ + IOHIDElementRef elementRef; + IOHIDElementCookie cookie; + uint32_t usagePage, usage; /* HID usage */ + SInt32 min; /* reported min value possible */ + SInt32 max; /* reported max value possible */ + + /* runtime variables used for auto-calibration */ + SInt32 minReport; /* min returned value */ + SInt32 maxReport; /* max returned value */ + + struct recElement *pNext; /* next element in list */ +}; +typedef struct recElement recElement; + +struct joystick_hwdata +{ + IOHIDDeviceRef deviceRef; /* HIDManager device handle */ + io_service_t ffservice; /* Interface for force feedback, 0 = no ff */ + + char product[256]; /* name of product */ + uint32_t usage; /* usage page from IOUSBHID Parser.h which defines general usage */ + uint32_t usagePage; /* usage within above page from IOUSBHID Parser.h which defines specific usage */ + + int axes; /* number of axis (calculated, not reported by device) */ + int buttons; /* number of buttons (calculated, not reported by device) */ + int elements; /* number of total elements (should be total of above) (calculated, not reported by device) */ + + recElement *firstAxis; + recElement *firstButton; + + bool removed; + + int instance_id; + SDL_JoystickGUID guid; + + SDL_Joystick joystick; + + struct joystick_hwdata *pNext; /* next device */ +}; +typedef struct joystick_hwdata recDevice; + +#define SDL_JOYSTICK_RUNLOOP_MODE CFSTR("SDLJoystick") + +/* The base object of the HID Manager API */ +static IOHIDManagerRef hidman = NULL; + +/* Linked list of all available devices */ +static recDevice *gpDeviceList = NULL; + +/* static incrementing counter for new joystick devices seen on the system. Devices should start with index 0 */ +static int s_joystick_instance_id = -1; + +#define SDL_JOYSTICK_AXIS_MAX 32767 + +void SDL_PrivateJoystickAxis(SDL_Joystick * joystick, uint8_t axis, int16_t value) +{ + /* Make sure we're not getting garbage or duplicate events */ + if (axis >= joystick->naxes) { + return; + } + if (!joystick->axes[axis].has_initial_value) { + joystick->axes[axis].initial_value = value; + joystick->axes[axis].value = value; + joystick->axes[axis].zero = value; + joystick->axes[axis].has_initial_value = true; + } + if (value == joystick->axes[axis].value) { + return; + } + if (!joystick->axes[axis].sent_initial_value) { + /* Make sure we don't send motion until there's real activity on this axis */ + const int MAX_ALLOWED_JITTER = SDL_JOYSTICK_AXIS_MAX / 80; /* ShanWan PS3 controller needed 96 */ + if (abs(value - joystick->axes[axis].value) <= MAX_ALLOWED_JITTER) { + return; + } + joystick->axes[axis].sent_initial_value = true; + joystick->axes[axis].value = value; /* Just so we pass the check above */ + SDL_PrivateJoystickAxis(joystick, axis, joystick->axes[axis].initial_value); + } + + /* Update internal joystick state */ + joystick->axes[axis].value = value; + + NSResponder *responder = (typeof(responder)) [[NSApp keyWindow] firstResponder]; + while (responder) { + if ([responder respondsToSelector:@selector(joystick:axis:movedTo:)]) { + [responder joystick:@(joystick->name) axis:axis movedTo:value]; + break; + } + responder = (typeof(responder)) [responder nextResponder]; + } +} + +void SDL_PrivateJoystickButton(SDL_Joystick *joystick, uint8_t button, uint8_t state) +{ + + /* Make sure we're not getting garbage or duplicate events */ + if (button >= joystick->nbuttons) { + return; + } + if (state == joystick->buttons[button]) { + return; + } + + /* Update internal joystick state */ + joystick->buttons[button] = state; + + NSResponder *responder = (typeof(responder)) [[NSApp keyWindow] firstResponder]; + while (responder) { + if ([responder respondsToSelector:@selector(joystick:button:changedState:)]) { + [responder joystick:@(joystick->name) button:button changedState:state]; + break; + } + responder = (typeof(responder)) [responder nextResponder]; + } +} + +static void +FreeElementList(recElement *pElement) +{ + while (pElement) { + recElement *pElementNext = pElement->pNext; + free(pElement); + pElement = pElementNext; + } +} + + +static recDevice * +FreeDevice(recDevice *removeDevice) +{ + recDevice *pDeviceNext = NULL; + if (removeDevice) { + if (removeDevice->deviceRef) { + IOHIDDeviceUnscheduleFromRunLoop(removeDevice->deviceRef, CFRunLoopGetCurrent(), SDL_JOYSTICK_RUNLOOP_MODE); + removeDevice->deviceRef = NULL; + } + + /* save next device prior to disposing of this device */ + pDeviceNext = removeDevice->pNext; + + if ( gpDeviceList == removeDevice ) { + gpDeviceList = pDeviceNext; + } else { + recDevice *device = gpDeviceList; + while (device->pNext != removeDevice) { + device = device->pNext; + } + device->pNext = pDeviceNext; + } + removeDevice->pNext = NULL; + + /* free element lists */ + FreeElementList(removeDevice->firstAxis); + FreeElementList(removeDevice->firstButton); + + free(removeDevice); + } + return pDeviceNext; +} + +static SInt32 +GetHIDElementState(recDevice *pDevice, recElement *pElement) +{ + SInt32 value = 0; + + if (pDevice && pElement) { + IOHIDValueRef valueRef; + if (IOHIDDeviceGetValue(pDevice->deviceRef, pElement->elementRef, &valueRef) == kIOReturnSuccess) { + value = (SInt32) IOHIDValueGetIntegerValue(valueRef); + + /* record min and max for auto calibration */ + if (value < pElement->minReport) { + pElement->minReport = value; + } + if (value > pElement->maxReport) { + pElement->maxReport = value; + } + } + } + + return value; +} + +static SInt32 +GetHIDScaledCalibratedState(recDevice * pDevice, recElement * pElement, SInt32 min, SInt32 max) +{ + const float deviceScale = max - min; + const float readScale = pElement->maxReport - pElement->minReport; + const SInt32 value = GetHIDElementState(pDevice, pElement); + if (readScale == 0) { + return value; /* no scaling at all */ + } + return ((value - pElement->minReport) * deviceScale / readScale) + min; +} + + +static void +JoystickDeviceWasRemovedCallback(void *ctx, IOReturn result, void *sender) +{ + recDevice *device = (recDevice *) ctx; + device->removed = true; + device->deviceRef = NULL; // deviceRef was invalidated due to the remove +} + + +static void AddHIDElement(const void *value, void *parameter); + +/* Call AddHIDElement() on all elements in an array of IOHIDElementRefs */ +static void +AddHIDElements(CFArrayRef array, recDevice *pDevice) +{ + const CFRange range = { 0, CFArrayGetCount(array) }; + CFArrayApplyFunction(array, range, AddHIDElement, pDevice); +} + +static bool +ElementAlreadyAdded(const IOHIDElementCookie cookie, const recElement *listitem) { + while (listitem) { + if (listitem->cookie == cookie) { + return true; + } + listitem = listitem->pNext; + } + return false; +} + +/* See if we care about this HID element, and if so, note it in our recDevice. */ +static void +AddHIDElement(const void *value, void *parameter) +{ + recDevice *pDevice = (recDevice *) parameter; + IOHIDElementRef refElement = (IOHIDElementRef) value; + const CFTypeID elementTypeID = refElement ? CFGetTypeID(refElement) : 0; + + if (refElement && (elementTypeID == IOHIDElementGetTypeID())) { + const IOHIDElementCookie cookie = IOHIDElementGetCookie(refElement); + const uint32_t usagePage = IOHIDElementGetUsagePage(refElement); + const uint32_t usage = IOHIDElementGetUsage(refElement); + recElement *element = NULL; + recElement **headElement = NULL; + + /* look at types of interest */ + switch (IOHIDElementGetType(refElement)) { + case kIOHIDElementTypeInput_Misc: + case kIOHIDElementTypeInput_Button: + case kIOHIDElementTypeInput_Axis: { + switch (usagePage) { /* only interested in kHIDPage_GenericDesktop and kHIDPage_Button */ + case kHIDPage_GenericDesktop: + switch (usage) { + case kHIDUsage_GD_X: + case kHIDUsage_GD_Y: + case kHIDUsage_GD_Z: + case kHIDUsage_GD_Rx: + case kHIDUsage_GD_Ry: + case kHIDUsage_GD_Rz: + case kHIDUsage_GD_Slider: + case kHIDUsage_GD_Dial: + case kHIDUsage_GD_Wheel: + if (!ElementAlreadyAdded(cookie, pDevice->firstAxis)) { + element = (recElement *) calloc(1, sizeof (recElement)); + if (element) { + pDevice->axes++; + headElement = &(pDevice->firstAxis); + } + } + break; + + case kHIDUsage_GD_DPadUp: + case kHIDUsage_GD_DPadDown: + case kHIDUsage_GD_DPadRight: + case kHIDUsage_GD_DPadLeft: + case kHIDUsage_GD_Start: + case kHIDUsage_GD_Select: + case kHIDUsage_GD_SystemMainMenu: + if (!ElementAlreadyAdded(cookie, pDevice->firstButton)) { + element = (recElement *) calloc(1, sizeof (recElement)); + if (element) { + pDevice->buttons++; + headElement = &(pDevice->firstButton); + } + } + break; + } + break; + + case kHIDPage_Simulation: + switch (usage) { + case kHIDUsage_Sim_Rudder: + case kHIDUsage_Sim_Throttle: + case kHIDUsage_Sim_Accelerator: + case kHIDUsage_Sim_Brake: + if (!ElementAlreadyAdded(cookie, pDevice->firstAxis)) { + element = (recElement *) calloc(1, sizeof (recElement)); + if (element) { + pDevice->axes++; + headElement = &(pDevice->firstAxis); + } + } + break; + + default: + break; + } + break; + + case kHIDPage_Button: + case kHIDPage_Consumer: /* e.g. 'pause' button on Steelseries MFi gamepads. */ + if (!ElementAlreadyAdded(cookie, pDevice->firstButton)) { + element = (recElement *) calloc(1, sizeof (recElement)); + if (element) { + pDevice->buttons++; + headElement = &(pDevice->firstButton); + } + } + break; + + default: + break; + } + } + break; + + case kIOHIDElementTypeCollection: { + CFArrayRef array = IOHIDElementGetChildren(refElement); + if (array) { + AddHIDElements(array, pDevice); + } + } + break; + + default: + break; + } + + if (element && headElement) { /* add to list */ + recElement *elementPrevious = NULL; + recElement *elementCurrent = *headElement; + while (elementCurrent && usage >= elementCurrent->usage) { + elementPrevious = elementCurrent; + elementCurrent = elementCurrent->pNext; + } + if (elementPrevious) { + elementPrevious->pNext = element; + } else { + *headElement = element; + } + + element->elementRef = refElement; + element->usagePage = usagePage; + element->usage = usage; + element->pNext = elementCurrent; + + element->minReport = element->min = (SInt32) IOHIDElementGetLogicalMin(refElement); + element->maxReport = element->max = (SInt32) IOHIDElementGetLogicalMax(refElement); + element->cookie = IOHIDElementGetCookie(refElement); + + pDevice->elements++; + } + } +} + +static bool +GetDeviceInfo(IOHIDDeviceRef hidDevice, recDevice *pDevice) +{ + const uint16_t BUS_USB = 0x03; + const uint16_t BUS_BLUETOOTH = 0x05; + int32_t vendor = 0; + int32_t product = 0; + int32_t version = 0; + CFTypeRef refCF = NULL; + CFArrayRef array = NULL; + uint16_t *guid16 = (uint16_t *)pDevice->guid.data; + + /* get usage page and usage */ + refCF = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDPrimaryUsagePageKey)); + if (refCF) { + CFNumberGetValue(refCF, kCFNumberSInt32Type, &pDevice->usagePage); + } + if (pDevice->usagePage != kHIDPage_GenericDesktop) { + return false; /* Filter device list to non-keyboard/mouse stuff */ + } + + refCF = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDPrimaryUsageKey)); + if (refCF) { + CFNumberGetValue(refCF, kCFNumberSInt32Type, &pDevice->usage); + } + + if ((pDevice->usage != kHIDUsage_GD_Joystick && + pDevice->usage != kHIDUsage_GD_GamePad && + pDevice->usage != kHIDUsage_GD_MultiAxisController)) { + return false; /* Filter device list to non-keyboard/mouse stuff */ + } + + pDevice->deviceRef = hidDevice; + + /* get device name */ + refCF = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDProductKey)); + if (!refCF) { + /* Maybe we can't get "AwesomeJoystick2000", but we can get "Logitech"? */ + refCF = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDManufacturerKey)); + } + if ((!refCF) || (!CFStringGetCString(refCF, pDevice->product, sizeof (pDevice->product), kCFStringEncodingUTF8))) { + strlcpy(pDevice->product, "Unidentified joystick", sizeof (pDevice->product)); + } + + refCF = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDVendorIDKey)); + if (refCF) { + CFNumberGetValue(refCF, kCFNumberSInt32Type, &vendor); + } + + refCF = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDProductIDKey)); + if (refCF) { + CFNumberGetValue(refCF, kCFNumberSInt32Type, &product); + } + + refCF = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDVersionNumberKey)); + if (refCF) { + CFNumberGetValue(refCF, kCFNumberSInt32Type, &version); + } + + memset(pDevice->guid.data, 0, sizeof(pDevice->guid.data)); + + if (vendor && product) { + *guid16++ = BUS_USB; + *guid16++ = 0; + *guid16++ = vendor; + *guid16++ = 0; + *guid16++ = product; + *guid16++ = 0; + *guid16++ = version; + *guid16++ = 0; + } else { + *guid16++ = BUS_BLUETOOTH; + *guid16++ = 0; + strlcpy((char*)guid16, pDevice->product, sizeof(pDevice->guid.data) - 4); + } + + array = IOHIDDeviceCopyMatchingElements(hidDevice, NULL, kIOHIDOptionsTypeNone); + if (array) { + AddHIDElements(array, pDevice); + CFRelease(array); + } + + return true; +} + +static bool +JoystickAlreadyKnown(IOHIDDeviceRef ioHIDDeviceObject) +{ + recDevice *i; + for (i = gpDeviceList; i != NULL; i = i->pNext) { + if (i->deviceRef == ioHIDDeviceObject) { + return true; + } + } + return false; +} + +void +SDL_SYS_JoystickUpdate(SDL_Joystick * joystick) +{ + recDevice *device = joystick->hwdata; + recElement *element; + SInt32 value; + int i; + + if (!device) { + return; + } + + if (device->removed) { /* device was unplugged; ignore it. */ + if (joystick->hwdata) { + joystick->force_recentering = true; + joystick->hwdata = NULL; + } + return; + } + + element = device->firstAxis; + i = 0; + while (element) { + value = GetHIDScaledCalibratedState(device, element, -32768, 32767); + SDL_PrivateJoystickAxis(joystick, i, value); + element = element->pNext; + ++i; + } + + element = device->firstButton; + i = 0; + while (element) { + value = GetHIDElementState(device, element); + if (value > 1) { /* handle pressure-sensitive buttons */ + value = 1; + } + SDL_PrivateJoystickButton(joystick, i, value); + element = element->pNext; + ++i; + } +} + +static void +JoystickDeviceWasAddedCallback(void *ctx, IOReturn res, void *sender, IOHIDDeviceRef ioHIDDeviceObject) +{ + recDevice *device; + int device_index = 0; + io_service_t ioservice; + + if (res != kIOReturnSuccess) { + return; + } + + if (JoystickAlreadyKnown(ioHIDDeviceObject)) { + return; /* IOKit sent us a duplicate. */ + } + + device = (recDevice *) calloc(1, sizeof(recDevice)); + + if (!device) { + abort(); + return; + } + + if (!GetDeviceInfo(ioHIDDeviceObject, device)) { + free(device); + return; /* not a device we care about, probably. */ + } + + SDL_Joystick *joystick = &device->joystick; + + joystick->instance_id = device->instance_id; + joystick->hwdata = device; + joystick->name = device->product; + + joystick->naxes = device->axes; + joystick->nbuttons = device->buttons; + + if (joystick->naxes > 0) { + joystick->axes = (SDL_JoystickAxisInfo *) calloc(joystick->naxes, sizeof(SDL_JoystickAxisInfo)); + } + if (joystick->nbuttons > 0) { + joystick->buttons = (uint8_t *) calloc(joystick->nbuttons, 1); + } + + /* Get notified when this device is disconnected. */ + IOHIDDeviceRegisterRemovalCallback(ioHIDDeviceObject, JoystickDeviceWasRemovedCallback, device); + IOHIDDeviceScheduleWithRunLoop(ioHIDDeviceObject, CFRunLoopGetCurrent(), SDL_JOYSTICK_RUNLOOP_MODE); + + /* Allocate an instance ID for this device */ + device->instance_id = ++s_joystick_instance_id; + + /* We have to do some storage of the io_service_t for SDL_HapticOpenFromJoystick */ + ioservice = IOHIDDeviceGetService(ioHIDDeviceObject); + + /* Add device to the end of the list */ + if ( !gpDeviceList ) { + gpDeviceList = device; + } else { + recDevice *curdevice; + + curdevice = gpDeviceList; + while ( curdevice->pNext ) { + ++device_index; + curdevice = curdevice->pNext; + } + curdevice->pNext = device; + ++device_index; /* bump by one since we counted by pNext. */ + } +} + +static bool +ConfigHIDManager(CFArrayRef matchingArray) +{ + CFRunLoopRef runloop = CFRunLoopGetCurrent(); + + if (IOHIDManagerOpen(hidman, kIOHIDOptionsTypeNone) != kIOReturnSuccess) { + return false; + } + + IOHIDManagerSetDeviceMatchingMultiple(hidman, matchingArray); + IOHIDManagerRegisterDeviceMatchingCallback(hidman, JoystickDeviceWasAddedCallback, NULL); + IOHIDManagerScheduleWithRunLoop(hidman, runloop, SDL_JOYSTICK_RUNLOOP_MODE); + + while (CFRunLoopRunInMode(SDL_JOYSTICK_RUNLOOP_MODE,0,TRUE) == kCFRunLoopRunHandledSource) { + /* no-op. Callback fires once per existing device. */ + } + + /* future hotplug events will come through SDL_JOYSTICK_RUNLOOP_MODE now. */ + + return true; /* good to go. */ +} + + +static CFDictionaryRef +CreateHIDDeviceMatchDictionary(const UInt32 page, const UInt32 usage, int *okay) +{ + 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); + } + + if (!retval) { + *okay = 0; + } + + return retval; +} + +static bool +CreateHIDManager(void) +{ + bool retval = false; + int okay = 1; + const void *vals[] = { + (void *) CreateHIDDeviceMatchDictionary(kHIDPage_GenericDesktop, kHIDUsage_GD_Joystick, &okay), + (void *) CreateHIDDeviceMatchDictionary(kHIDPage_GenericDesktop, kHIDUsage_GD_GamePad, &okay), + (void *) CreateHIDDeviceMatchDictionary(kHIDPage_GenericDesktop, kHIDUsage_GD_MultiAxisController, &okay), + }; + const size_t numElements = sizeof(vals) / sizeof(vals[0]); + CFArrayRef array = okay ? CFArrayCreate(kCFAllocatorDefault, vals, numElements, &kCFTypeArrayCallBacks) : NULL; + size_t i; + + for (i = 0; i < numElements; i++) { + if (vals[i]) { + CFRelease((CFTypeRef) vals[i]); + } + } + + if (array) { + hidman = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone); + if (hidman != NULL) { + retval = ConfigHIDManager(array); + } + CFRelease(array); + } + + return retval; +} + +void SDL_JoystickRun(void) +{ + recDevice *device = gpDeviceList; + while (device) { + if (device->removed) { + device = FreeDevice(device); + } else { + SDL_SYS_JoystickUpdate(&device->joystick); + device = device->pNext; + } + } + + /* run this after the checks above so we don't set device->removed and delete the device before + SDL_SYS_JoystickUpdate can run to clean up the SDL_Joystick object that owns this device */ + while (CFRunLoopRunInMode(SDL_JOYSTICK_RUNLOOP_MODE,0,TRUE) == kCFRunLoopRunHandledSource) { + /* no-op. Pending callbacks will fire in CFRunLoopRunInMode(). */ + } +} + +void __attribute__((constructor)) SDL_SYS_JoystickInit(void) +{ + if (!CreateHIDManager()) { + fprintf(stderr, "Joystick: Couldn't initialize HID Manager"); + } + else { + [[NSRunLoop mainRunLoop] addTimer: + [NSTimer timerWithTimeInterval:1/120.0 repeats:YES block:^(NSTimer * _Nonnull timer) { + SDL_JoystickRun(); + }] forMode:NSDefaultRunLoopMode]; + } +} diff --git a/Makefile b/Makefile index 23338dd..1a2c2e7 100755 --- a/Makefile +++ b/Makefile @@ -175,7 +175,7 @@ $(BIN)/SameBoy.app: $(BIN)/SameBoy.app/Contents/MacOS/SameBoy \ $(BIN)/SameBoy.app/Contents/MacOS/SameBoy: $(CORE_OBJECTS) $(COCOA_OBJECTS) -@$(MKDIR) -p $(dir $@) - $(CC) $^ -o $@ $(LDFLAGS) -framework OpenGL -framework AudioUnit -framework AVFoundation -framework CoreVideo -framework CoreMedia + $(CC) $^ -o $@ $(LDFLAGS) -framework OpenGL -framework AudioUnit -framework AVFoundation -framework CoreVideo -framework CoreMedia -framework IOKit ifeq ($(CONF), release) strip $@ endif