iOS: Replace the private keyboard and vibration APIs with public ones

Sideloaded builds read hardware keyboard events from the private
UIEvent._gsEvent, at offsets hardcoded for iOS 7 and 9, through a
UIApplication subclass. The UIPress path the App Store build uses works
from iOS 13.4, so use it in all builds, and delete the hack along with
PPSSPPUIApplication and SmartKeyboardMap.

System_Vibrate asked the private UIDevice feedbackSupportLevel whether
to use the feedback generator, in App Store builds too, and sideloaded
builds fell back to the private AudioServicesPlaySystemSoundWithVibration.
The feedback generator just does nothing on devices without a Taptic
Engine, so always use it. That leaves the iPhone 6s and first SE
without virtual button vibration in sideloaded builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-10-10 16:28:27 +02:00
1 parent 6fe0f420b1
commit 3375fbdbbd
9 files changed
+7 -326

No files matched your search

-5
View File
@@ -955,10 +955,6 @@ elseif(IOS AND NOT LIBRETRO)
ios/CameraHelper.h
ios/LocationHelper.mm
ios/LocationHelper.h
ios/PPSSPPUIApplication.h
ios/PPSSPPUIApplication.mm
ios/SmartKeyboardMap.cpp
ios/SmartKeyboardMap.hpp
ios/iCade/iCadeReaderView.h
ios/iCade/iCadeReaderView.m
ios/iCade/iCadeState.h
@@ -989,7 +985,6 @@ elseif(IOS AND NOT LIBRETRO)
ios/ViewControllerMetal.mm
ios/iOSCoreAudio.mm
ios/IAPManager.mm
ios/PPSSPPUIApplication.mm
ios/iCade/iCadeReaderView.m
ios/main.mm
ios/CameraHelper.mm
-32
View File
@@ -473,14 +473,6 @@
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\ios\SmartKeyboardMap.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\libretro\libretro.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">true</ExcludedFromBuild>
@@ -770,22 +762,6 @@
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
</ClInclude>
<ClInclude Include="..\ios\PPSSPPUIApplication.h">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
</ClInclude>
<ClInclude Include="..\ios\SmartKeyboardMap.hpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
</ClInclude>
<ClInclude Include="..\ios\SubtleVolume.h">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">true</ExcludedFromBuild>
@@ -1207,14 +1183,6 @@
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
</None>
<None Include="..\ios\PPSSPPUIApplication.mm">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
</None>
<None Include="..\ios\README.md" />
<None Include="..\ios\SubtleVolume.mm">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</ExcludedFromBuild>
-12
View File
@@ -184,9 +184,6 @@
<ClCompile Include="..\android\jni\app-android.cpp">
<Filter>Other Platforms\Android</Filter>
</ClCompile>
<ClCompile Include="..\ios\SmartKeyboardMap.cpp">
<Filter>Other Platforms\iOS</Filter>
</ClCompile>
<ClCompile Include="..\libretro\LibretroGraphicsContext.cpp">
<Filter>Other Platforms\libretro</Filter>
</ClCompile>
@@ -388,9 +385,6 @@
<ClInclude Include="..\ios\ViewController.h">
<Filter>Other Platforms\iOS</Filter>
</ClInclude>
<ClInclude Include="..\ios\PPSSPPUIApplication.h">
<Filter>Other Platforms\iOS</Filter>
</ClInclude>
<ClInclude Include="..\ios\AppDelegate.h">
<Filter>Other Platforms\iOS</Filter>
</ClInclude>
@@ -406,9 +400,6 @@
<ClInclude Include="..\ios\iOSCoreAudio.h">
<Filter>Other Platforms\iOS</Filter>
</ClInclude>
<ClInclude Include="..\ios\SmartKeyboardMap.hpp">
<Filter>Other Platforms\iOS</Filter>
</ClInclude>
<ClInclude Include="..\ios\SubtleVolume.h">
<Filter>Other Platforms\iOS</Filter>
</ClInclude>
@@ -623,9 +614,6 @@
<None Include="..\android\git-version-gen.sh">
<Filter>Build</Filter>
</None>
<None Include="..\ios\PPSSPPUIApplication.mm">
<Filter>Other Platforms\iOS</Filter>
</None>
<None Include="..\ios\AppDelegate.mm">
<Filter>Other Platforms\iOS</Filter>
</None>
-23
View File
@@ -1,23 +0,0 @@
//
// PPSSPPUIApplication.h
// PPSSPP
//
// Created by xieyi on 2017/9/4.
//
//
#ifndef PPSSPPUIApplication_h
#define PPSSPPUIApplication_h
#import <UIKit/UIKit.h>
@interface PPSSPPUIApplication : UIApplication
{
}
@property (nonatomic, strong) UISelectionFeedbackGenerator *feedbackGenerator;
@end
#endif /* PPSSPPUIApplication_h */
-119
View File
@@ -1,119 +0,0 @@
//
// PPSSPPUIApplication.m
// PPSSPP
//
// Created by xieyi on 2017/9/4.
//
//
#import "PPSSPPUIApplication.h"
#import <Foundation/Foundation.h>
#import <Foundation/NSObjCRuntime.h>
#import <GLKit/GLKit.h>
#import "ios/IAPManager.h"
#include "Common/System/Display.h"
#include "Common/System/System.h"
#include "Common/System/NativeApp.h"
#include "Common/TimeUtil.h"
#include "Common/File/VFS/VFS.h"
#include "Common/File/VFS/DirectoryReader.h"
#include "Common/Input/InputState.h"
#include "Common/Net/Resolve.h"
#include "Common/UI/Screen.h"
#include "Common/GPU/thin3d.h"
#include "Common/Input/KeyCodes.h"
#include "Common/GPU/OpenGL/GLFeatures.h"
#import "ios/AppDelegate.h"
#include "ios/SmartKeyboardMap.hpp"
#include "Core/Config.h"
#include "Common/GPU/GraphicsContext.h"
#include <sys/types.h>
#include <sys/sysctl.h>
#include <mach/machine.h>
// This stuff reads directly from _gsEvent which is not allowed on the App Store.
#ifndef IS_IOS7
#define IS_IOS7 ([[UIDevice currentDevice].systemVersion floatValue]>=7.0)
#endif
#ifndef IS_IOS9
#define IS_IOS9 ([[UIDevice currentDevice].systemVersion floatValue]>=9.0)
#endif
#define IS_64BIT (sizeof(NSUInteger)==8)
#define GSEVENT_TYPE 2
#define GSEVENT_FLAGS (IS_64BIT?10:12)
#define GSEVENTKEY_KEYCODE (IS_64BIT?(IS_IOS9?13:19):(IS_IOS7?17:15))
#define GSEVENT_TYPE_KEYUP 11
#define GSEVENT_TYPE_KEYDOWN 10
#define GSEVENT_TYPE_MODIFIER 12
#define GSEVENT_FLAG_LCMD 65536 // 0x00010000
#define GSEVENT_FLAG_LSHIFT 131072 // 0x00020000
#define GSEVENT_FLAG_LCTRL 1048576 // 0x00100000
#define GSEVENT_FLAG_LALT 524288 // 0x00080000
#define GSEVENT_FLAG_RSHIFT 2097152 // 0x00200000 - not sent IOS9
#define GSEVENT_FLAG_RCTRL 8388608 // 0x00800000 - not sent IOS9
#define GSEVENT_FLAG_RALT 4194304 // 0x00400000 - not sent IOS9
@implementation PPSSPPUIApplication
#if !PPSSPP_PLATFORM(IOS_APP_STORE)
- (void)decodeKeyEvent:(NSInteger *)eventMem {
NSInteger eventType = eventMem[GSEVENT_TYPE];
NSInteger eventScanCode = eventMem[GSEVENTKEY_KEYCODE];
//NSLog(@"Got key: %d", (int)eventScanCode);
if (eventType == GSEVENT_TYPE_KEYUP) {
struct KeyInput key;
key.flags = KeyInputFlags::UP;
key.keyCode = getSmartKeyboardMap((int)eventScanCode);
key.deviceId = DEVICE_ID_KEYBOARD;
NativeKey(key);
} else if (eventType == GSEVENT_TYPE_KEYDOWN) {
struct KeyInput key;
key.flags = KeyInputFlags::DOWN;
key.keyCode = getSmartKeyboardMap((int)eventScanCode);
key.deviceId = DEVICE_ID_KEYBOARD;
NativeKey(key);
}
}
- (void)handleKeyUIEvent:(UIEvent *) event {
if ([event respondsToSelector:@selector(_gsEvent)]) {
NSInteger *eventMem;
eventMem = (NSInteger *) (__bridge void*)[event performSelector:@selector(_gsEvent)];
if (eventMem) {
[self decodeKeyEvent:eventMem];
}
}
}
- (void)sendEvent:(UIEvent *)event {
[super sendEvent:event];
if ([event respondsToSelector:@selector(_gsEvent)]) {
NSInteger *eventMem;
eventMem = (NSInteger *) (__bridge void*)[event performSelector:@selector(_gsEvent)];
if (eventMem) {
[self decodeKeyEvent:eventMem];
}
}
}
#endif // !IOS_APP_STORE
@end
-78
View File
@@ -1,78 +0,0 @@
//
// SmartKeyboardMap.cpp
// PPSSPP
//
// Created by xieyi on 2017/9/4.
//
//
#include "SmartKeyboardMap.hpp"
#include "Common/Input/KeyCodes.h"
InputKeyCode getSmartKeyboardMap(int keycode) {
switch(keycode) {
case 4: return NKCODE_A;
case 5: return NKCODE_B;
case 6: return NKCODE_C;
case 7: return NKCODE_D;
case 8: return NKCODE_E;
case 9: return NKCODE_F;
case 10: return NKCODE_G;
case 11: return NKCODE_H;
case 12: return NKCODE_I;
case 13: return NKCODE_J;
case 14: return NKCODE_K;
case 15: return NKCODE_L;
case 16: return NKCODE_M;
case 17: return NKCODE_N;
case 18: return NKCODE_O;
case 19: return NKCODE_P;
case 20: return NKCODE_Q;
case 21: return NKCODE_R;
case 22: return NKCODE_S;
case 23: return NKCODE_T;
case 24: return NKCODE_U;
case 25: return NKCODE_V;
case 26: return NKCODE_W;
case 27: return NKCODE_X;
case 28: return NKCODE_Y;
case 29: return NKCODE_Z;
case 30: return NKCODE_1;
case 31: return NKCODE_2;
case 32: return NKCODE_3;
case 33: return NKCODE_4;
case 34: return NKCODE_5;
case 35: return NKCODE_6;
case 36: return NKCODE_7;
case 37: return NKCODE_8;
case 38: return NKCODE_9;
case 39: return NKCODE_0;
case 40: return NKCODE_ENTER;
case 43: return NKCODE_TAB;
case 44: return NKCODE_SPACE;
case 45: return NKCODE_MINUS;
case 46: return NKCODE_EQUALS;
case 47: return NKCODE_LEFT_BRACKET;
case 48: return NKCODE_RIGHT_BRACKET;
case 49: return NKCODE_BACKSLASH;
case 51: return NKCODE_SEMICOLON;
case 52: return NKCODE_APOSTROPHE;
case 53: return NKCODE_BACK;//NKCODE_GRAVE;
case 54: return NKCODE_COMMA;
case 55: return NKCODE_PERIOD;
case 56: return NKCODE_SLASH;
case 57: return NKCODE_CAPS_LOCK;
case 79: return NKCODE_DPAD_RIGHT;
case 80: return NKCODE_DPAD_LEFT;
case 81: return NKCODE_DPAD_DOWN;
case 82: return NKCODE_DPAD_UP;
case 224: return NKCODE_CTRL_LEFT;
case 225: return NKCODE_SHIFT_LEFT;
case 226: return NKCODE_META_LEFT;
case 227: return NKCODE_ALT_LEFT;
case 229: return NKCODE_SHIFT_RIGHT;
case 230: return NKCODE_META_RIGHT;
case 231: return NKCODE_ALT_RIGHT;
default: return NKCODE_UNKNOWN;
}
}
-13
View File
@@ -1,13 +0,0 @@
//
// SmartKeyboardMap.hpp
// PPSSPP
//
// Created by xieyi on 2017/9/4.
//
//
#pragma once
#include "Common/Input/KeyCodes.h"
InputKeyCode getSmartKeyboardMap(int keycode);
-4
View File
@@ -469,9 +469,6 @@ extern float g_safeInsetBottom;
g_iCadeTracker.ButtonUp(button);
}
// See PPSSPPUIApplication.mm for the other method
#if PPSSPP_PLATFORM(IOS_APP_STORE)
- (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event {
KeyboardPressesBegan(presses, event);
}
@@ -484,7 +481,6 @@ extern float g_safeInsetBottom;
KeyboardPressesEnded(presses, event);
}
#endif
#pragma mark - Status Bar Control
// The immersive mode setting is per-orientation, so we need to know which way we're facing.
+7 -40
View File
@@ -19,10 +19,8 @@
#import <stdlib.h>
#import <sys/syscall.h>
#import <sys/utsname.h>
#import <AudioToolbox/AudioToolbox.h>
#import "AppDelegate.h"
#import "PPSSPPUIApplication.h"
#import "ViewController.h"
#import "iOSCoreAudio.h"
#import "IAPManager.h"
@@ -635,47 +633,16 @@ PermissionStatus System_GetPermissionStatus(SystemPermission permission) {
return PERMISSION_STATUS_GRANTED;
}
#if !PPSSPP_PLATFORM(IOS_APP_STORE)
FOUNDATION_EXTERN void AudioServicesPlaySystemSoundWithVibration(unsigned long, objc_object*, NSDictionary*);
#endif
BOOL SupportsTaptic() {
// we're on an iOS version that cannot instantiate UISelectionFeedbackGenerator, so no.
if(!NSClassFromString(@"UISelectionFeedbackGenerator")) {
return NO;
}
// http://www.mikitamanko.com/blog/2017/01/29/haptic-feedback-with-uifeedbackgenerator/
// use private API against UIDevice to determine the haptic stepping
// 2 - iPhone 7 or above, full taptic feedback
// 1 - iPhone 6S, limited taptic feedback
// 0 - iPhone 6 or below, no taptic feedback
NSNumber* val = (NSNumber*)[[UIDevice currentDevice] valueForKey:@"feedbackSupportLevel"];
return [val intValue] >= 2;
}
void System_Vibrate(int mode) {
// Comes from the UI code on the emu thread, and UIKit wants the main thread.
dispatch_async(dispatch_get_main_queue(), ^{
if (SupportsTaptic()) {
PPSSPPUIApplication* app = (PPSSPPUIApplication*)[UIApplication sharedApplication];
if(app.feedbackGenerator == nil)
{
app.feedbackGenerator = [[UISelectionFeedbackGenerator alloc] init];
[app.feedbackGenerator prepare];
}
[app.feedbackGenerator selectionChanged];
} else {
#if !PPSSPP_PLATFORM(IOS_APP_STORE)
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
NSArray *pattern = @[@YES, @30, @NO, @2];
dictionary[@"VibePattern"] = pattern;
dictionary[@"Intensity"] = @2;
AudioServicesPlaySystemSoundWithVibration(kSystemSoundID_Vibrate, nil, dictionary);
#endif
// Does nothing on devices without a Taptic Engine.
static UISelectionFeedbackGenerator *feedbackGenerator;
if (!feedbackGenerator) {
feedbackGenerator = [[UISelectionFeedbackGenerator alloc] init];
[feedbackGenerator prepare];
}
[feedbackGenerator selectionChanged];
});
}
@@ -742,7 +709,7 @@ int main(int argc, char *argv[]) {
#endif
@autoreleasepool {
return UIApplicationMain(argc, argv, NSStringFromClass([PPSSPPUIApplication class]), NSStringFromClass([AppDelegate class]));
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}