Files
ppsspp/ios/main.mm
T
Henrik RydgårdandClaude Opus 5.5 f364f297f1 Touch controls: Use the full width when the side insets are only for a top corner
On the iPhone Duo's cover screen, the camera sits in a box in the top right
corner, which gives a side inset down the whole right edge in portrait. The
controls are far below it, so they lose width for nothing. iOS 27.1 reports
the box as an occlusion region, and from that we report how far down the side
insets are needed (SYSPROP_DISPLAY_SIDE_INSETS_BOTTOM). When that's within the
top third, the game screen and the touch layout editor skip the side insets.
The rest of the UI, including the FPS counter, still keeps them.

The regions can show up after the size and insets have settled, so the insets
are now recomputed on every layout, and a screen recreates its views when its
layout mode changes, since the touch controls are placed for the bounds they
were created in.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-26 09:39:49 +01:00

803 lines
24 KiB
Plaintext

// main.mm boilerplate
//
// Overview
//
// main.mm: JIT enablement, the System_ functions, starting the next step
// AppDelegate.mm: Audio session interruptions, deep link and library export helpers
// SceneDelegate.mm: Runs NativeInit, creates the window and the view controller
// ViewController.mm: The view controller. Rendering (Vulkan to a CAMetalLayer), input, keyboard, pickers and sizing
#import <UIKit/UIKit.h>
#import <dlfcn.h>
#import <mach/mach.h>
#import <mach-o/loader.h>
#import <mach-o/getsect.h>
#import <pthread.h>
#import <spawn.h>
#import <signal.h>
#import <string>
#import <stdio.h>
#import <stdlib.h>
#import <sys/syscall.h>
#import "AppDelegate.h"
#import "ViewController.h"
#import "ExternalDisplaySceneDelegate.h"
#import "iOSCoreAudio.h"
#import "IAPManager.h"
#import "SceneDelegate.h"
#include "Common/MemoryUtil.h"
#include "Common/Audio/AudioBackend.h"
#include "Common/System/NativeApp.h"
#include "Common/System/System.h"
#include "Common/System/Request.h"
#include "Common/StringUtils.h"
#include "Common/Profiler/Profiler.h"
#include "Common/Thread/ThreadUtil.h"
#include "Common/TimeUtil.h"
#include "Common/System/Display.h"
#include "Core/Config.h"
#include "Common/Log.h"
#include "Common/Log/LogManager.h"
#include "Core/Util/DarwinFileSystemServices.h"
#if !PPSSPP_PLATFORM(TVOS)
#import <CoreMotion/CoreMotion.h>
#endif
// Compile out all the hackery in app store builds. tvOS doesn't have posix_spawn, and there's no
// jailbreak scene to speak of.
#if !PPSSPP_PLATFORM(IOS_APP_STORE) && !PPSSPP_PLATFORM(TVOS)
struct cs_blob_index {
uint32_t type;
uint32_t offset;
};
struct cs_superblob {
uint32_t magic;
uint32_t length;
uint32_t count;
struct cs_blob_index index[];
};
struct cs_entitlements {
uint32_t magic;
uint32_t length;
char entitlements[];
};
static int (*csops)(pid_t pid, unsigned int ops, void * useraddr, size_t usersize);
static boolean_t (*exc_server)(mach_msg_header_t *, mach_msg_header_t *);
static int (*ptrace)(int request, pid_t pid, caddr_t addr, int data);
#define CS_OPS_STATUS 0 /* return status */
#define CS_KILL 0x00000200 /* kill process if it becomes invalid */
#define CS_DEBUGGED 0x10000000 /* process is currently or has previously been debugged and allowed to run with invalid pages */
#define PT_TRACE_ME 0 /* child declares it's being traced */
#define PT_SIGEXC 12 /* signals as exceptions for current_proc */
#define ptrace(a, b, c, d) syscall(SYS_ptrace, a, b, c, d)
static void *exception_handler(void *argument) {
mach_port_t port = *(mach_port_t *)argument;
mach_msg_server(exc_server, 2048, port, 0);
return NULL;
}
static NSDictionary *parse_entitlements(const void *entitlements, size_t length) {
char *copy = (char *)malloc(length);
memcpy(copy, entitlements, length);
NSData *data = [NSData dataWithBytes:copy length:length];
free(copy);
return [NSPropertyListSerialization propertyListWithData:data
options:NSPropertyListImmutable
format:nil
error:nil];
}
static NSDictionary *app_entitlements(void) {
// Inspired by codesign.c in Darwin sources for Security.framework
// Find our mach-o header
Dl_info dl_info;
if (dladdr((const void *)app_entitlements, &dl_info) == 0)
return nil;
if (dl_info.dli_fbase == NULL)
return nil;
char *base = (char *)dl_info.dli_fbase;
struct mach_header_64 *header = (struct mach_header_64 *)dl_info.dli_fbase;
if (header->magic != MH_MAGIC_64)
return nil;
// Simulator executables have fake entitlements in the code signature. The real entitlements can be found in an __entitlements section.
size_t entitlements_size;
uint8_t *entitlements_data = getsectiondata(header, "__TEXT", "__entitlements", &entitlements_size);
if (entitlements_data != NULL) {
NSData *data = [NSData dataWithBytesNoCopy:entitlements_data
length:entitlements_size
freeWhenDone:NO];
return [NSPropertyListSerialization propertyListWithData:data
options:NSPropertyListImmutable
format:nil
error:nil];
}
// Find the LC_CODE_SIGNATURE
struct load_command *lc = (struct load_command *) (base + sizeof(*header));
struct linkedit_data_command *cs_lc = NULL;
for (uint32_t i = 0; i < header->ncmds; i++) {
if (lc->cmd == LC_CODE_SIGNATURE) {
cs_lc = (struct linkedit_data_command *) lc;
break;
}
lc = (struct load_command *) ((char *) lc + lc->cmdsize);
}
if (cs_lc == NULL)
return nil;
// Read the code signature off disk, as it's apparently not loaded into memory
NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingFromURL:NSBundle.mainBundle.executableURL error:nil];
if (fileHandle == nil)
return nil;
[fileHandle seekToFileOffset:cs_lc->dataoff];
NSData *csData = [fileHandle readDataOfLength:cs_lc->datasize];
[fileHandle closeFile];
const struct cs_superblob *cs = (const struct cs_superblob *)csData.bytes;
if (ntohl(cs->magic) != 0xfade0cc0)
return nil;
// Find the entitlements in the code signature
for (uint32_t i = 0; i < ntohl(cs->count); i++) {
struct cs_entitlements *ents = (struct cs_entitlements *) ((char *) cs + ntohl(cs->index[i].offset));
if (ntohl(ents->magic) == 0xfade7171) {
return parse_entitlements(ents->entitlements, ntohl(ents->length) - offsetof(struct cs_entitlements, entitlements));
}
}
return nil;
}
static NSDictionary *cached_app_entitlements(void) {
static NSDictionary *entitlements = nil;
if (!entitlements) {
entitlements = app_entitlements();
}
return entitlements;
}
bool jb_has_container(void) {
NSDictionary *entitlements = cached_app_entitlements();
return ![entitlements[@"com.apple.private.security.no-sandbox"] boolValue];
}
static char *childArgv[] = {NULL, "debugme", NULL};
bool jb_spawn_ptrace_child(int argc, char **argv) {
int ret; pid_t pid;
if (argc > 1 && strcmp(argv[1], childArgv[1]) == 0) {
ret = ptrace(PT_TRACE_ME, 0, NULL, 0);
NSLog(@"child: ptrace(PT_TRACE_ME) %d", ret);
exit(ret);
}
if (jb_has_container()) {
return false;
}
childArgv[0] = argv[0];
if ((ret = posix_spawnp(&pid, argv[0], NULL, NULL, (char *const *)childArgv, NULL)) != 0) {
return false;
}
return true;
}
bool jb_has_jit_entitlement(void) {
NSDictionary *entitlements = cached_app_entitlements();
return [entitlements[@"dynamic-codesigning"] boolValue];
}
bool jb_has_cs_disabled(void) {
int flags;
return !csops(getpid(), CS_OPS_STATUS, &flags, sizeof(flags)) && (flags & ~CS_KILL) == flags;
}
static bool jb_has_debugger_attached(void) {
int flags;
return !csops(getpid(), CS_OPS_STATUS, &flags, sizeof(flags)) && flags & CS_DEBUGGED;
}
bool jb_enable_ptrace_hack(void) {
bool debugged = jb_has_debugger_attached();
// Thanks to this comment: https://news.ycombinator.com/item?id=18431524
// We use this hack to allow mmap with PROT_EXEC (which usually requires the
// dynamic-codesigning entitlement) by tricking the process into thinking
// that Xcode is debugging it. We abuse the fact that JIT is needed to
// debug the process.
if (ptrace(PT_TRACE_ME, 0, NULL, 0) < 0) {
return false;
}
// ptracing ourselves confuses the kernel and will cause bad things to
// happen to the system (hangs…) if an exception or signal occurs. Setup
// some "safety nets" so we can cause the process to exit in a somewhat sane
// state. We only need to do this if the debugger isn't attached. (It'll do
// this itself, and if we do it we'll interfere with its normal operation
// anyways.)
if (!debugged) {
// First, ensure that signals are delivered as Mach software exceptions…
ptrace(PT_SIGEXC, 0, NULL, 0);
// …then ensure that this exception goes through our exception handler.
// I think it's OK to just watch for EXC_SOFTWARE because the other
// exceptions (e.g. EXC_BAD_ACCESS, EXC_BAD_INSTRUCTION, and friends)
// will end up being delivered as signals anyways, and we can get them
// once they're resent as a software exception.
mach_port_t port = MACH_PORT_NULL;
mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &port);
mach_port_insert_right(mach_task_self(), port, port, MACH_MSG_TYPE_MAKE_SEND);
task_set_exception_ports(mach_task_self(), EXC_MASK_SOFTWARE, port, EXCEPTION_DEFAULT, THREAD_STATE_NONE);
pthread_t thread;
pthread_create(&thread, NULL, exception_handler, (void *)&port);
}
return true;
}
#endif
float g_safeInsetLeft = 0.0;
float g_safeInsetRight = 0.0;
float g_safeInsetTop = 0.0;
float g_safeInsetBottom = 0.0;
float g_sideInsetsBottom = -1.0;
static bool g_jitAvailable = true;
static int g_iosVersionMajor;
static std::string version;
std::string System_GetProperty(SystemProperty prop) {
switch (prop) {
case SYSPROP_NAME:
#if PPSSPP_PLATFORM(TVOS)
return StringFromFormat("tvOS %s", version.c_str());
#else
return StringFromFormat("iOS %s", version.c_str());
#endif
case SYSPROP_LANGREGION:
return [[[NSLocale currentLocale] objectForKey:NSLocaleIdentifier] UTF8String];
case SYSPROP_BUILD_VERSION:
return PPSSPP_GIT_VERSION;
case SYSPROP_DEVICE_NAME: {
// Since iOS 16, without the user-assigned-device-name entitlement this is just the model.
UIDevice *device = [UIDevice currentDevice];
return [device.name isEqualToString:device.model] ? "" : device.name.UTF8String;
}
case SYSPROP_DEVICE_MODEL:
return [UIDevice currentDevice].model.UTF8String;
default:
return "";
}
}
std::vector<std::string> System_GetPropertyStringVec(SystemProperty prop) {
switch (prop) {
case SYSPROP_TEMP_DIRS:
default:
return std::vector<std::string>();
}
}
#if !PPSSPP_PLATFORM(TVOS)
extern "C" {
int Apple_GetCurrentBatteryCapacity();
}
#endif
int64_t System_GetPropertyInt(SystemProperty prop) {
switch (prop) {
case SYSPROP_AUDIO_SAMPLE_RATE:
return 44100;
case SYSPROP_DEVICE_TYPE:
return IS_TV() ? DEVICE_TYPE_TV : DEVICE_TYPE_MOBILE;
case SYSPROP_SYSTEMVERSION:
return g_iosVersionMajor;
#if !PPSSPP_PLATFORM(TVOS)
case SYSPROP_BATTERY_PERCENTAGE:
return Apple_GetCurrentBatteryCapacity();
#endif
default:
return -1;
}
}
float System_GetPropertyFloat(SystemProperty prop) {
switch (prop) {
case SYSPROP_DISPLAY_REFRESH_RATE:
return 60.f;
case SYSPROP_DISPLAY_SAFE_INSET_LEFT:
return g_safeInsetLeft * g_display.dpi_scale_x;
case SYSPROP_DISPLAY_SAFE_INSET_RIGHT:
return g_safeInsetRight * g_display.dpi_scale_x;
case SYSPROP_DISPLAY_SAFE_INSET_TOP:
return g_safeInsetTop * g_display.dpi_scale_y;
case SYSPROP_DISPLAY_SAFE_INSET_BOTTOM:
return g_safeInsetBottom * g_display.dpi_scale_y;
case SYSPROP_DISPLAY_SIDE_INSETS_BOTTOM:
return g_sideInsetsBottom < 0.0f ? -1.0f : g_sideInsetsBottom * g_display.dpi_scale_y;
default:
return -1;
}
}
bool System_GetPropertyBool(SystemProperty prop) {
#if PPSSPP_PLATFORM(TVOS)
// No document picker, share sheet, clipboard, camera, location, motion, microphone, battery,
// rotation or web browser. And no files we didn't put there ourselves, so no file browsing either.
switch (prop) {
case SYSPROP_HAS_FILE_BROWSER:
case SYSPROP_HAS_FOLDER_BROWSER:
case SYSPROP_HAS_IMAGE_BROWSER:
case SYSPROP_HAS_ACCELEROMETER:
case SYSPROP_HAS_GYRO:
case SYSPROP_HAS_KEYBOARD:
case SYSPROP_KEYBOARD_IS_SOFT:
case SYSPROP_SUPPORTS_SHARE_TEXT:
case SYSPROP_SUPPORTS_SHARE_FILE:
case SYSPROP_CAN_LAUNCH_URL:
case SYSPROP_DISPLAY_HAS_CAMERA_CUTOUT:
case SYSPROP_CAN_READ_BATTERY_PERCENTAGE:
case SYSPROP_CAN_RESTRICT_ORIENTATION:
case SYSPROP_MICROPHONE_NEEDS_POLLING:
return false;
case SYSPROP_HAS_BACK_BUTTON:
// The remote's Menu/Back button.
return true;
case SYSPROP_LIMITED_FILE_BROWSING:
// Nothing outside the app's container lists, so there'd be no way back.
return true;
default:
break;
}
#endif
switch (prop) {
case SYSPROP_HAS_DEEP_LINKS:
return true;
case SYSPROP_CAN_LAUNCH_URL:
return true;
case SYSPROP_DISPLAY_HAS_CAMERA_CUTOUT:
return true;
case SYSPROP_HAS_FILE_BROWSER:
return true;
case SYSPROP_HAS_FOLDER_BROWSER:
return true;
case SYSPROP_HAS_IMAGE_BROWSER:
return true;
case SYSPROP_HAS_OPEN_DIRECTORY:
return false;
case SYSPROP_HAS_BACK_BUTTON:
return false;
case SYSPROP_HAS_ACCELEROMETER:
return true;
#if !PPSSPP_PLATFORM(TVOS)
case SYSPROP_HAS_GYRO:
{
static const bool hasGyro = [[CMMotionManager alloc] init].gyroAvailable;
return hasGyro;
}
#endif
case SYSPROP_HAS_KEYBOARD:
return true;
case SYSPROP_SUPPORTS_SHARE_TEXT:
case SYSPROP_SUPPORTS_SHARE_FILE:
return true;
case SYSPROP_KEYBOARD_IS_SOFT:
// If a hardware keyboard is connected, and we add support, we could return false here.
return true;
case SYSPROP_APP_GOLD:
#ifdef GOLD
// This is deprecated.
return true;
#elif PPSSPP_PLATFORM(IOS_APP_STORE)
// Check the IAP status.
return [[IAPManager sharedIAPManager] isGoldUnlocked];
#else
return false;
#endif
case SYSPROP_USE_IAP:
#if PPSSPP_PLATFORM(IOS_APP_STORE) && defined(USE_IAP)
return true;
#else
return false;
#endif
case SYSPROP_CAN_JIT:
return g_jitAvailable;
case SYSPROP_LIMITED_FILE_BROWSING:
#if PPSSPP_PLATFORM(IOS_APP_STORE)
return true;
#else
return false; // But will return true in app store builds.
#endif
#ifndef HTTPS_NOT_AVAILABLE
case SYSPROP_SUPPORTS_HTTPS:
return true;
#endif
case SYSPROP_CAN_READ_BATTERY_PERCENTAGE:
return true;
case SYSPROP_CAN_RESTRICT_ORIENTATION:
return true;
case SYSPROP_CAN_GET_FREE_SPACE_FAST:
return false;
case SYSPROP_MICROPHONE_NEEDS_POLLING:
return true;
default:
return false;
}
}
void System_Notify(SystemNotification notification) {
switch (notification) {
case SystemNotification::APP_SWITCH_MODE_CHANGED:
dispatch_async(dispatch_get_main_queue(), ^{
if (sharedViewController) {
[sharedViewController appSwitchModeChanged];
}
});
break;
case SystemNotification::EXTERNAL_DISPLAY_CHANGED:
dispatch_async(dispatch_get_main_queue(), ^{
ExternalDisplaySettingChanged();
});
break;
case SystemNotification::IMMERSIVE_MODE_CHANGE:
dispatch_async(dispatch_get_main_queue(), ^{
if (sharedViewController) {
[sharedViewController immersiveModeChanged];
}
});
break;
case SystemNotification::UI_STATE_CHANGED:
dispatch_async(dispatch_get_main_queue(), ^{
if (sharedViewController) {
[sharedViewController uiStateChanged];
}
});
break;
case SystemNotification::TOP_LEVEL_CHANGED:
dispatch_async(dispatch_get_main_queue(), ^{
if (sharedViewController) {
[sharedViewController topLevelChanged];
}
});
break;
case SystemNotification::AUDIO_MODE_CHANGED:
dispatch_async(dispatch_get_main_queue(), ^{
iOSCoreAudioUpdateSession();
});
break;
#if !PPSSPP_PLATFORM(TVOS)
case SystemNotification::ROTATE_UPDATED:
dispatch_async(dispatch_get_main_queue(), ^{
if (sharedViewController) {
INFO_LOG(Log::System, "Requesting device orientation update");
if (@available(iOS 16.0, *)) {
[sharedViewController setNeedsUpdateOfSupportedInterfaceOrientations];
} else {
[UIViewController attemptRotationToDeviceOrientation];
}
}
});
break;
#endif
default:
break;
}
}
bool System_MakeRequest(SystemRequestType type, int requestId, const std::string &param1, const std::string &param2, int64_t param3, int64_t param4) {
switch (type) {
case SystemRequestType::RESTART_APP:
{
// A copy, since the block outlives param1.
std::string args = param1;
dispatch_async(dispatch_get_main_queue(), ^{
// Get the connected scenes
NSSet<UIScene *> *scenes = [UIApplication sharedApplication].connectedScenes;
// Find the app's scene. An external display has a scene of its own.
for (UIScene *scene in scenes) {
if ([scene.delegate isKindOfClass:[SceneDelegate class]]) {
[(SceneDelegate *)scene.delegate restart:args.c_str()];
break;
}
}
});
return true;
}
case SystemRequestType::EXIT_APP:
// NOTE: on iOS, this is considered a crash and not a valid way to exit.
// We're on the render thread. Calling exit() here would destroy its still joinable std::thread,
// which aborts, so let the main thread join it first.
dispatch_async(dispatch_get_main_queue(), ^{
[sharedViewController shutdown];
exit(0);
});
break;
case SystemRequestType::BROWSE_FOR_FILE:
{
if (!System_GetPropertyBool(SYSPROP_HAS_FILE_BROWSER)) {
return false;
}
DarwinDirectoryPanelCallback callback = [requestId] (bool success, Path path) {
if (success) {
g_requestManager.PostSystemSuccess(requestId, path.c_str());
} else {
g_requestManager.PostSystemFailure(requestId);
}
};
DarwinFileSystemServices::presentDirectoryPanel(callback, /* allowFiles = */ true, /* allowDirectories = */ false);
return true;
}
case SystemRequestType::BROWSE_FOR_FOLDER:
{
if (!System_GetPropertyBool(SYSPROP_HAS_FOLDER_BROWSER)) {
return false;
}
DarwinDirectoryPanelCallback callback = [requestId] (bool success, Path path) {
if (success) {
g_requestManager.PostSystemSuccess(requestId, path.c_str());
} else {
g_requestManager.PostSystemFailure(requestId);
}
};
DarwinFileSystemServices::presentDirectoryPanel(callback, /* allowFiles = */ false, /* allowDirectories = */ true);
return true;
}
case SystemRequestType::BROWSE_FOR_IMAGE:
{
if (!System_GetPropertyBool(SYSPROP_HAS_IMAGE_BROWSER)) {
return false;
}
NSString *filename = [NSString stringWithUTF8String:param2.c_str()];
dispatch_async(dispatch_get_main_queue(), ^{
[sharedViewController pickPhoto:filename requestId:requestId];
});
return true;
}
case SystemRequestType::CAMERA_COMMAND:
{
// These come from the emu thread. UIKit and Core Location want the main thread, and that's
// also the only thread that changes sharedViewController.
std::string command = param1;
dispatch_async(dispatch_get_main_queue(), ^{
if (!strncmp(command.c_str(), "startVideo", 10)) {
int width = 0, height = 0;
sscanf(command.c_str(), "startVideo_%dx%d", &width, &height);
[sharedViewController startVideo:width height:height];
} else if (command == "stopVideo") {
[sharedViewController stopVideo];
}
});
return true;
}
case SystemRequestType::GPS_COMMAND:
{
std::string command = param1;
dispatch_async(dispatch_get_main_queue(), ^{
if (command == "open") {
[sharedViewController startLocation];
} else if (command == "close") {
[sharedViewController stopLocation];
}
});
return true;
}
case SystemRequestType::MICROPHONE_COMMAND:
// From the CPU thread. Polling has to happen right here, the rest moves to the main thread inside.
if (startsWith(param1, "startRecording:")) {
int sampleRate = 0;
if (sscanf(param1.c_str(), "startRecording:%d", &sampleRate) != 1 || sampleRate <= 0) {
return false;
}
iOSCoreAudioStartRecording(sampleRate);
} else if (param1 == "stopRecording") {
iOSCoreAudioStopRecording();
} else if (param1 == "pollRecording") {
iOSCoreAudioPollRecording();
} else {
return false;
}
return true;
case SystemRequestType::SHARE_TEXT:
{
if (!System_GetPropertyBool(SYSPROP_SUPPORTS_SHARE_TEXT)) {
return false;
}
NSString *text = [NSString stringWithUTF8String:param1.c_str()];
dispatch_async(dispatch_get_main_queue(), ^{
[sharedViewController shareText:text];
});
return true;
}
case SystemRequestType::SHARE_FILE:
{
if (!System_GetPropertyBool(SYSPROP_SUPPORTS_SHARE_FILE)) {
return false;
}
// param2, the MIME type, isn't needed. The share sheet goes by the extension.
NSString *path = [NSString stringWithUTF8String:param1.c_str()];
if (![[NSFileManager defaultManager] fileExistsAtPath:path]) {
ERROR_LOG(Log::System, "SHARE_FILE: '%s' doesn't exist", param1.c_str());
return false;
}
NSURL *url = [NSURL fileURLWithPath:path];
dispatch_async(dispatch_get_main_queue(), ^{
[sharedViewController shareFile:url];
});
return true;
}
case SystemRequestType::NOTIFY_UI_EVENT:
{
const UIEventNotification event = (UIEventNotification)param3;
dispatch_async(dispatch_get_main_queue(), ^{
switch (event) {
case UIEventNotification::POPUP_CLOSED:
case UIEventNotification::DIALOG_CLOSED:
case UIEventNotification::TEXT_LOSTFOCUS:
[sharedViewController hideKeyboard];
break;
case UIEventNotification::TEXT_GOTFOCUS:
[sharedViewController showKeyboard];
break;
default:
break;
}
});
return true;
}
#if PPSSPP_PLATFORM(IOS_APP_STORE)
case SystemRequestType::IAP_RESTORE_PURCHASES:
{
[[IAPManager sharedIAPManager] restorePurchasesWithRequestID:requestId];
return true;
}
case SystemRequestType::IAP_MAKE_PURCHASE:
{
[[IAPManager sharedIAPManager] buyGoldWithRequestID:requestId];
return true;
}
#endif
#if !PPSSPP_PLATFORM(TVOS)
case SystemRequestType::COPY_TO_CLIPBOARD:
{
NSString *text = [NSString stringWithUTF8String:param1.c_str()];
dispatch_async(dispatch_get_main_queue(), ^{
[UIPasteboard generalPasteboard].string = text;
});
return true;
}
#endif
case SystemRequestType::SET_KEEP_SCREEN_BRIGHT:
dispatch_async(dispatch_get_main_queue(), ^{
INFO_LOG(Log::System, "SET_KEEP_SCREEN_BRIGHT: %d", (int)param3);
[[UIApplication sharedApplication] setIdleTimerDisabled: (param3 ? YES : NO)];
});
return true;
default:
break;
}
return false;
}
void System_Toast(std::string_view text) {}
void System_AskForPermission(SystemPermission permission) {}
void System_LaunchUrl(LaunchUrlType urlType, std::string_view url) {
std::string strUrl(url);
NSURL *nsUrl = [NSURL URLWithString:[NSString stringWithUTF8String:strUrl.c_str()]];
if (!nsUrl) {
ERROR_LOG(Log::System, "Failed to parse URL: %s", strUrl.c_str());
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
[[UIApplication sharedApplication] openURL:nsUrl options:@{} completionHandler:nil];
});
}
bool System_AudioRecordingIsAvailable() {
#if PPSSPP_PLATFORM(TVOS)
return false;
#else
// Every iPhone and iPad has a microphone. Permission is asked for when recording starts.
return true;
#endif
}
PermissionStatus System_GetPermissionStatus(SystemPermission permission) {
return PERMISSION_STATUS_GRANTED;
}
void System_Vibrate(int mode) {
#if !PPSSPP_PLATFORM(TVOS)
// Comes from the UI code on the emu thread, and UIKit wants the main thread.
dispatch_async(dispatch_get_main_queue(), ^{
// Does nothing on devices without a Taptic Engine.
static UISelectionFeedbackGenerator *feedbackGenerator;
if (!feedbackGenerator) {
feedbackGenerator = [[UISelectionFeedbackGenerator alloc] init];
[feedbackGenerator prepare];
}
[feedbackGenerator selectionChanged];
});
#endif
}
#if PPSSPP_PLATFORM(TVOS)
// The iOS one is in CameraHelper.mm.
std::vector<std::string> System_GetCameraDeviceList() {
return std::vector<std::string>();
}
#endif
AudioBackend *System_CreateAudioBackend() {
// Use legacy mechanisms.
return nullptr;
}
int main(int argc, char *argv[]) {
TimeInit();
version = [[[UIDevice currentDevice] systemVersion] UTF8String];
if (1 != sscanf(version.c_str(), "%d", &g_iosVersionMajor)) {
// Just set it to 15 (the minimum) if the parsing fails for whatever reason.
g_iosVersionMajor = 15;
}
g_logManager.EnableOutput(LogOutput::Stdio);
#if PPSSPP_PLATFORM(IOS_APP_STORE) || PPSSPP_PLATFORM(TVOS)
g_jitAvailable = false;
#else
// Hacky hacks to try to enable JIT by pretending to be a debugger.
csops = reinterpret_cast<decltype(csops)>(dlsym(dlopen(nullptr, RTLD_LAZY), "csops"));
exc_server = reinterpret_cast<decltype(exc_server)>(dlsym(dlopen(NULL, RTLD_LAZY), "exc_server"));
ptrace = reinterpret_cast<decltype(ptrace)>(dlsym(dlopen(NULL, RTLD_LAZY), "ptrace"));
// see https://github.com/hrydgard/ppsspp/issues/11905
if (jb_spawn_ptrace_child(argc, argv)) {
INFO_LOG(Log::System, "JIT: ptrace() child spawn trick\n");
} else if (jb_has_jit_entitlement()) {
INFO_LOG(Log::System, "JIT: found entitlement\n");
} else if (jb_has_cs_disabled()) {
INFO_LOG(Log::System, "JIT: CS_KILL disabled\n");
} else if (jb_enable_ptrace_hack()) {
INFO_LOG(Log::System, "JIT: ptrace() hack supported\n");
} else {
INFO_LOG(Log::System, "JIT: ptrace() hack failed\n");
g_jitAvailable = false;
}
// Tried checking for JIT support here with AllocateExecutableMemory and ProtectMemoryPages,
// but it just succeeds, and then fails when you try to execute from it.
#endif
// Ignore sigpipe.
if (signal(SIGPIPE, SIG_IGN) == SIG_ERR) {
perror("Unable to ignore SIGPIPE");
}
PROFILE_INIT();
#if PPSSPP_PLATFORM(IOS_APP_STORE) && defined(_DEBUG)
g_logManager.SetOutputsEnabled(LogOutput::Stdio);
#endif
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}