From 1b31e9f6dce360fdfce005677c1380ea5fe15321 Mon Sep 17 00:00:00 2001 From: Sam Lantinga Date: Tue, 13 Oct 2020 21:08:13 -0700 Subject: [PATCH] Added support for game controller rumble on iOS 14 --- Xcode/SDL/SDL.xcodeproj/project.pbxproj | 8 ++ src/joystick/iphoneos/SDL_sysjoystick.m | 161 +++++++++++++++++++++- src/joystick/iphoneos/SDL_sysjoystick_c.h | 1 + 3 files changed, 169 insertions(+), 1 deletion(-) mode change 100644 => 100755 Xcode/SDL/SDL.xcodeproj/project.pbxproj diff --git a/Xcode/SDL/SDL.xcodeproj/project.pbxproj b/Xcode/SDL/SDL.xcodeproj/project.pbxproj old mode 100644 new mode 100755 index 4d390ed9d..172655383 --- a/Xcode/SDL/SDL.xcodeproj/project.pbxproj +++ b/Xcode/SDL/SDL.xcodeproj/project.pbxproj @@ -3972,6 +3972,8 @@ F37DC5C0252FDE620002E6F7 /* SDL_sysurl.c in Sources */ = {isa = PBXBuildFile; fileRef = F37DC5BC252FDE620002E6F7 /* SDL_sysurl.c */; }; F37DC5C1252FDE620002E6F7 /* SDL_sysurl.c in Sources */ = {isa = PBXBuildFile; fileRef = F37DC5BC252FDE620002E6F7 /* SDL_sysurl.c */; }; F37DC5C2252FDE620002E6F7 /* SDL_sysurl.c in Sources */ = {isa = PBXBuildFile; fileRef = F37DC5BC252FDE620002E6F7 /* SDL_sysurl.c */; }; + F37DC5F325350EBC0002E6F7 /* CoreHaptics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F37DC5F225350EBC0002E6F7 /* CoreHaptics.framework */; }; + F37DC5F525350ECC0002E6F7 /* CoreHaptics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F37DC5F425350ECC0002E6F7 /* CoreHaptics.framework */; }; F3950CD8212BC88D00F51292 /* SDL_sensor.h in Headers */ = {isa = PBXBuildFile; fileRef = F3950CD7212BC88D00F51292 /* SDL_sensor.h */; settings = {ATTRIBUTES = (Public, ); }; }; F3950CD9212BC88D00F51292 /* SDL_sensor.h in Headers */ = {isa = PBXBuildFile; fileRef = F3950CD7212BC88D00F51292 /* SDL_sensor.h */; settings = {ATTRIBUTES = (Public, ); }; }; F3950CDA212BC88D00F51292 /* SDL_sensor.h in Headers */ = {isa = PBXBuildFile; fileRef = F3950CD7212BC88D00F51292 /* SDL_sensor.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -4524,6 +4526,8 @@ BECDF6BE0761BA81005FE872 /* SDL2 */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = SDL2; sourceTree = BUILT_PRODUCTS_DIR; }; DB31407717554B71006C0E22 /* libSDL2.dylib */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.dylib"; includeInIndex = 0; path = libSDL2.dylib; sourceTree = BUILT_PRODUCTS_DIR; }; F37DC5BC252FDE620002E6F7 /* SDL_sysurl.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_sysurl.c; sourceTree = ""; }; + F37DC5F225350EBC0002E6F7 /* CoreHaptics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreHaptics.framework; path = System/Library/Frameworks/CoreHaptics.framework; sourceTree = SDKROOT; }; + F37DC5F425350ECC0002E6F7 /* CoreHaptics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreHaptics.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS14.0.sdk/System/Library/Frameworks/CoreHaptics.framework; sourceTree = DEVELOPER_DIR; }; F3950CD7212BC88D00F51292 /* SDL_sensor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDL_sensor.h; sourceTree = ""; }; F59C710300D5CB5801000001 /* ReadMe.txt */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = text; path = ReadMe.txt; sourceTree = ""; }; F59C710600D5CB5801000001 /* SDL.info */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = text; path = SDL.info; sourceTree = ""; }; @@ -4618,6 +4622,7 @@ A75FDB9D23E4CAFA00529352 /* hidapi.framework in Frameworks */, A7D88BC823E24B0300DCD162 /* CoreGraphics.framework in Frameworks */, A7D88BC223E24A8800DCD162 /* CoreMotion.framework in Frameworks */, + F37DC5F325350EBC0002E6F7 /* CoreHaptics.framework in Frameworks */, A7D88B4E23E2437C00DCD162 /* CoreVideo.framework in Frameworks */, A7D88BBE23E24A6000DCD162 /* GameController.framework in Frameworks */, A7D88B5023E2437C00DCD162 /* IOKit.framework in Frameworks */, @@ -4637,6 +4642,7 @@ A7D88D0723E24BED00DCD162 /* CoreAudio.framework in Frameworks */, A7D88D0823E24BED00DCD162 /* CoreFoundation.framework in Frameworks */, A7D88D0923E24BED00DCD162 /* CoreGraphics.framework in Frameworks */, + F37DC5F525350ECC0002E6F7 /* CoreHaptics.framework in Frameworks */, A7D88D0B23E24BED00DCD162 /* CoreVideo.framework in Frameworks */, A7D88D0C23E24BED00DCD162 /* GameController.framework in Frameworks */, A7D88D0E23E24BED00DCD162 /* Metal.framework in Frameworks */, @@ -4875,6 +4881,8 @@ 564624341FF821B70074AC87 /* Frameworks */ = { isa = PBXGroup; children = ( + F37DC5F225350EBC0002E6F7 /* CoreHaptics.framework */, + F37DC5F425350ECC0002E6F7 /* CoreHaptics.framework */, A75FDADE23E28D6600529352 /* AudioToolbox.framework */, A75FDADC23E28D5500529352 /* AVFoundation.framework */, A75FDADA23E28D4900529352 /* CoreAudio.framework */, diff --git a/src/joystick/iphoneos/SDL_sysjoystick.m b/src/joystick/iphoneos/SDL_sysjoystick.m index 115226351..55374f593 100644 --- a/src/joystick/iphoneos/SDL_sysjoystick.m +++ b/src/joystick/iphoneos/SDL_sysjoystick.m @@ -42,6 +42,7 @@ #if !TARGET_OS_TV #import #endif +#import #ifdef SDL_JOYSTICK_MFI #import @@ -771,10 +772,163 @@ IOS_MFIJoystickUpdate(SDL_Joystick * joystick) #endif /* SDL_JOYSTICK_MFI */ } +@interface SDL_RumbleMotor : NSObject +@end + +@implementation SDL_RumbleMotor { + CHHapticEngine *engine API_AVAILABLE(ios(13.0), tvos(14.0)); + id player API_AVAILABLE(ios(13.0), tvos(14.0)); +} + +-(void)cleanup +{ + if (self->player != nil) { + [self->player cancelAndReturnError:nil]; + self->player = nil; + } + if (self->engine != nil) { + [self->engine stopWithCompletionHandler:nil]; + self->engine = nil; + } +} + +-(int)setIntensity:(float)intensity +{ + if (@available(iOS 14.0, tvOS 14.0, *)) { + NSError *error; + + if (self->player != nil) { + [self->player stopAtTime:0 error:&error]; + self->player = nil; + } + + if (self->engine == nil) { + return SDL_SetError("Haptics engine not available"); + } + + if (intensity <= 0.01f) { + return 0; + } + + CHHapticEventParameter *param = [[CHHapticEventParameter alloc] initWithParameterID:CHHapticEventParameterIDHapticIntensity value:intensity]; + CHHapticEvent *event = [[CHHapticEvent alloc] initWithEventType:CHHapticEventTypeHapticContinuous parameters:[NSArray arrayWithObjects:param, nil] relativeTime:0 duration:GCHapticDurationInfinite]; + CHHapticPattern *pattern = [[CHHapticPattern alloc] initWithEvents:[NSArray arrayWithObject:event] parameters:[[NSArray alloc] init] error:&error]; + if (error != nil) { + return SDL_SetError("Couldn't create haptic pattern: %s", [error.localizedDescription UTF8String]); + } + + self->player = [self->engine createPlayerWithPattern:pattern error:&error]; + if (error != nil) { + return SDL_SetError("Couldn't create haptic player: %s", [error.localizedDescription UTF8String]); + } + + [self->player startAtTime:0 error:&error]; + if (error != nil) { + self->player = nil; + return SDL_SetError("Couldn't start playback: %s", [error.localizedDescription UTF8String]); + } + } + return 0; +} + +-(id) initWithController:(GCController*)controller locality:(GCHapticsLocality)locality API_AVAILABLE(ios(14.0), tvos(14.0)) +{ + NSError *error; + + self->engine = [controller.haptics createEngineWithLocality:locality]; + if (self->engine == nil) { + return nil; + } + + [self->engine startAndReturnError:&error]; + if (error != nil) { + return nil; + } + + __weak typeof(self) weakSelf = self; + self->engine.stoppedHandler = ^(CHHapticEngineStoppedReason stoppedReason) { + SDL_RumbleMotor *_this = weakSelf; + if (_this == nil) { + return; + } + + _this->player = nil; + _this->engine = nil; + }; + self->engine.resetHandler = ^{ + SDL_RumbleMotor *_this = weakSelf; + if (_this == nil) { + return; + } + + _this->player = nil; + [_this->engine startAndReturnError:nil]; + }; + + return self; +} + +@end + +@interface SDL_RumbleContext : NSObject +@end + +@implementation SDL_RumbleContext { + SDL_RumbleMotor *low_frequency_motor; + SDL_RumbleMotor *high_frequency_motor; +} + +-(id) initWithLowFrequencyMotor:(SDL_RumbleMotor*)low_frequency_motor andHighFrequencyMotor:(SDL_RumbleMotor*)high_frequency_motor +{ + self->low_frequency_motor = low_frequency_motor; + self->high_frequency_motor = high_frequency_motor; + return self; +} + +-(int) rumbleWithLowFrequency:(Uint16)low_frequency_rumble andHighFrequency:(Uint16)high_frequency_rumble +{ + int result = 0; + + result += [self->low_frequency_motor setIntensity:((float)low_frequency_rumble / 65535.0f)]; + result += [self->high_frequency_motor setIntensity:((float)high_frequency_rumble / 65535.0f)]; + return ((result < 0) ? -1 : 0); +} + +@end + +static SDL_RumbleContext *IOS_JoystickInitRumble(GCController *controller) +{ + @autoreleasepool { + if (@available(iOS 14.0, tvOS 14.0, *)) { + SDL_RumbleMotor *low_frequency_motor = [[SDL_RumbleMotor alloc] initWithController:controller locality:GCHapticsLocalityLeftHandle]; + SDL_RumbleMotor *high_frequency_motor = [[SDL_RumbleMotor alloc] initWithController:controller locality:GCHapticsLocalityRightHandle]; + if (low_frequency_motor && high_frequency_motor) { + return [[SDL_RumbleContext alloc] initWithLowFrequencyMotor:low_frequency_motor andHighFrequencyMotor:high_frequency_motor]; + } + } + } + return nil; +} + static int IOS_JoystickRumble(SDL_Joystick * joystick, Uint16 low_frequency_rumble, Uint16 high_frequency_rumble) { - return SDL_Unsupported(); + SDL_JoystickDeviceItem *device = joystick->hwdata; + if (@available(iOS 14.0, tvOS 14.0, *)) { + if (!device->rumble && device->controller && device->controller.haptics) { + SDL_RumbleContext *rumble = IOS_JoystickInitRumble(device->controller); + if (rumble) { + device->rumble = (void *)CFBridgingRetain(rumble); + } + } + } + + if (device->rumble) { + SDL_RumbleContext *rumble = (__bridge SDL_RumbleContext *)device->rumble; + return [rumble rumbleWithLowFrequency:low_frequency_rumble andHighFrequency:high_frequency_rumble]; + } else { + return SDL_Unsupported(); + } } static void @@ -805,6 +959,11 @@ IOS_JoystickClose(SDL_Joystick * joystick) device->joystick = NULL; @autoreleasepool { + if (device->rumble) { + CFRelease(device->rumble); + device->rumble = NULL; + } + if (device->accelerometer) { #if !TARGET_OS_TV [motionManager stopAccelerometerUpdates]; diff --git a/src/joystick/iphoneos/SDL_sysjoystick_c.h b/src/joystick/iphoneos/SDL_sysjoystick_c.h index 0ae00edbd..8dcdeccec 100644 --- a/src/joystick/iphoneos/SDL_sysjoystick_c.h +++ b/src/joystick/iphoneos/SDL_sysjoystick_c.h @@ -34,6 +34,7 @@ typedef struct joystick_hwdata SDL_bool remote; GCController __unsafe_unretained *controller; + void *rumble; SDL_bool uses_pause_handler; int num_pause_presses; Uint32 pause_button_down_time;