Merge pull request #20885 from hrydgard/ios-scene-ui

iOS: Initial attempt at switching to SceneDelegate (UIScene)
This commit is contained in:
Henrik Rydgård authored and GitHub committed 2025-10-18 14:48:43 +02:00
commit 42be86552f
12 files changed
+236 -118

No files matched your search

+3 -1
View File
@@ -1340,6 +1340,8 @@ elseif(IOS AND NOT LIBRETRO)
ios/main.mm
ios/AppDelegate.mm
ios/AppDelegate.h
ios/SceneDelegate.mm
ios/SceneDelegate.h
ios/DisplayManager.h
ios/DisplayManager.mm
ios/Controls.h
@@ -2964,7 +2966,7 @@ endif()
# packaging and code signing
if(IOS AND NOT LIBRETRO)
if(IOS_APP_STORE)
set(DEPLOYMENT_TARGET 12.0)
set(DEPLOYMENT_TARGET 13.0)
else()
set(DEPLOYMENT_TARGET 11.0)
endif()
+10 -1
View File
@@ -202,6 +202,15 @@ static bool DefaultVSync() {
#endif
}
static int DefaultVulkanPresentMode() {
#if PPSSPP_PLATFORM(IOS)
// iOS only supports FIFO.
return (int)PresentMode::Fifo;
#else
return (int)PresentMode::Mailbox;
#endif
}
static bool DefaultEnableStateUndo() {
#if PPSSPP_PLATFORM(ANDROID) || PPSSPP_PLATFORM(IOS)
// Off on mobile to save disk space.
@@ -730,7 +739,7 @@ static const ConfigSetting graphicsSettings[] = {
ConfigSetting("TexDeposterize", &g_Config.bTexDeposterize, false, CfgFlag::PER_GAME | CfgFlag::REPORT),
ConfigSetting("TexHardwareScaling", &g_Config.bTexHardwareScaling, false, CfgFlag::PER_GAME | CfgFlag::REPORT),
ConfigSetting("VSync", &g_Config.bVSync, &DefaultVSync, CfgFlag::PER_GAME),
ConfigSetting("VulkanPresentMode", &g_Config.iVulkanPresentationMode, (int)PresentMode::Mailbox, CfgFlag::PER_GAME),
ConfigSetting("VulkanPresentMode", &g_Config.iVulkanPresentationMode, &DefaultVulkanPresentMode, CfgFlag::PER_GAME),
ConfigSetting("BloomHack", &g_Config.iBloomHack, 0, CfgFlag::PER_GAME | CfgFlag::REPORT),
// Not really a graphics setting...
+32 -28
View File
@@ -31,7 +31,7 @@
#include "Common/UI/Context.h"
#include "Common/Render/ManagedTexture.h"
#include "Common/VR/PPSSPPVR.h"
#include "Common/BitSet.h"
#include "Common/System/Display.h" // Only to check screen aspect ratio with pixel_yres/pixel_xres
#include "Common/System/Request.h"
#include "Common/System/OSD.h"
@@ -376,36 +376,40 @@ void GameSettingsScreen::CreateGraphicsSettings(UI::ViewGroup *graphicsSettings)
"FIFO: relaxed",
"FIFO (higher latency, framerate stability)",
};
UI::PopupMultiChoice *presentationMode = graphicsSettings->Add(new PopupMultiChoice(&g_Config.iVulkanPresentationMode, gr->T("Frame presentation mode"), presentationModes, 0, ARRAY_SIZE(presentationModes), I18NCat::GRAPHICS, screenManager()));
presentationMode->SetChoiceIcon(0, ImageID("I_WARNING"));
if (!(draw->GetDeviceCaps().presentModesSupported & Draw::PresentMode::IMMEDIATE)) {
presentationMode->HideChoice(0);
}
if (!(draw->GetDeviceCaps().presentModesSupported & Draw::PresentMode::MAILBOX)) {
presentationMode->HideChoice(1);
}
if (!(draw->GetDeviceCaps().presentModesSupported & Draw::PresentMode::FIFO_LATEST_READY)) {
presentationMode->HideChoice(2);
}
if (!(draw->GetDeviceCaps().presentModesSupported & Draw::PresentMode::FIFO_RELAXED)) {
presentationMode->HideChoice(3);
}
if (!(draw->GetDeviceCaps().presentModesSupported & Draw::PresentMode::FIFO)) {
presentationMode->HideChoice(4);
}
// Force the setting to a good supported mode if the current one is not supported, otherwise this will look weird.
if (presentationMode->IsChoiceHidden(g_Config.iVulkanPresentationMode)) {
if (draw->GetDeviceCaps().presentModesSupported & Draw::PresentMode::MAILBOX) {
g_Config.iVulkanPresentationMode = (int)Draw::PresentMode::MAILBOX;
} else if (draw->GetDeviceCaps().presentModesSupported & Draw::PresentMode::FIFO) {
g_Config.iVulkanPresentationMode = (int)Draw::PresentMode::FIFO;
// If only one mode is supported (like FIFO on iOS), no need to show the option.
if (CountSetBits((u32)draw->GetDeviceCaps().presentModesSupported) > 1) {
UI::PopupMultiChoice *presentationMode = graphicsSettings->Add(new PopupMultiChoice(&g_Config.iVulkanPresentationMode, gr->T("Frame presentation mode"), presentationModes, 0, ARRAY_SIZE(presentationModes), I18NCat::GRAPHICS, screenManager()));
presentationMode->SetChoiceIcon(0, ImageID("I_WARNING"));
if (!(draw->GetDeviceCaps().presentModesSupported & Draw::PresentMode::IMMEDIATE)) {
presentationMode->HideChoice(0);
}
if (!(draw->GetDeviceCaps().presentModesSupported & Draw::PresentMode::MAILBOX)) {
presentationMode->HideChoice(1);
}
if (!(draw->GetDeviceCaps().presentModesSupported & Draw::PresentMode::FIFO_LATEST_READY)) {
presentationMode->HideChoice(2);
}
if (!(draw->GetDeviceCaps().presentModesSupported & Draw::PresentMode::FIFO_RELAXED)) {
presentationMode->HideChoice(3);
}
if (!(draw->GetDeviceCaps().presentModesSupported & Draw::PresentMode::FIFO)) {
presentationMode->HideChoice(4);
}
}
presentationMode->OnChoice.Add([=](EventParams &e) {
NativeResized();
});
// Force the setting to a good supported mode if the current one is not supported, otherwise this will look weird.
if (presentationMode->IsChoiceHidden(g_Config.iVulkanPresentationMode)) {
if (draw->GetDeviceCaps().presentModesSupported & Draw::PresentMode::MAILBOX) {
g_Config.iVulkanPresentationMode = (int)Draw::PresentMode::MAILBOX;
} else if (draw->GetDeviceCaps().presentModesSupported & Draw::PresentMode::FIFO) {
g_Config.iVulkanPresentationMode = (int)Draw::PresentMode::FIFO;
}
}
presentationMode->OnChoice.Add([=](EventParams &e) {
NativeResized();
});
}
} else {
// All backends support FIFO. Check if any immediate modes are supported, if so we can allow the user to choose.
if (draw->GetDeviceCaps().presentModesSupported & (Draw::PresentMode::IMMEDIATE | Draw::PresentMode::MAILBOX)) {
+1 -1
View File
@@ -1531,7 +1531,7 @@ void LaunchBuyGold(ScreenManager *screenManager) {
screenManager->push(new IAPScreen());
} else {
#if PPSSPP_PLATFORM(IOS_APP_STORE)
System_LaunchUrl(LaunchUrlType::BROWSER_URL, "https://apps.apple.com/us/app/ppsspp-gold-psp-emulator/id6502287918");
System_LaunchUrl(LaunchUrlType::BROWSER_URL, "https://www.ppsspp.org/buygold_ios");
#elif PPSSPP_PLATFORM(ANDROID)
System_LaunchUrl(LaunchUrlType::BROWSER_URL, "market://details?id=org.ppsspp.ppssppgold");
#else
+1 -1
View File
@@ -12,7 +12,7 @@
# PPSSPP platform flags
set(MOBILE_DEVICE ON)
set(USING_GLES2 ON)
set(IPHONEOS_DEPLOYMENT_TARGET 12.0)
set(IPHONEOS_DEPLOYMENT_TARGET 13.0)
add_definitions(
-DGL_ETC1_RGB8_OES=0
-U__STRICT_ANSI__
+1 -3
View File
@@ -8,10 +8,8 @@
@property (strong, nonatomic) UIWindow *window;
@property (strong, nonatomic) UIScreen *screen;
@property (nonatomic, strong) NSDictionary *launchOptions;
@property (strong, nonatomic) id<PPSSPPViewController> viewController;
- (void)restart:(const char *)args;
- (BOOL)launchPPSSPP:(int)argc argv:(char**)argv;
@end
+16 -82
View File
@@ -1,7 +1,5 @@
#import "AppDelegate.h"
#import "ViewControllerCommon.h"
#import "ViewController.h"
#import "ViewControllerMetal.h"
#import "SceneDelegate.h"
#import "iOSCoreAudio.h"
#import "Common/System/System.h"
#import "Common/System/NativeApp.h"
@@ -11,6 +9,12 @@
#import "IAPManager.h"
#import <AVFoundation/AVFoundation.h>
#import <objc/runtime.h>
// TODO: Unfortunate hack to force link SceneDelegate class.
// This is necessary because otherwise classes from static libraries aren't loaded.
// We should move all the iOS code into the main binary directly to avoid this problem.
__attribute__((used)) static Class _forceLinkSceneDelegate = [SceneDelegate class];
@implementation AppDelegate
@@ -77,49 +81,21 @@
}
-(BOOL) application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
int argc = 1;
char *argv[5]{};
NSURL *nsUrl = [launchOptions objectForKey:UIApplicationLaunchOptionsURLKey];
if (nsUrl != nullptr && nsUrl.isFileURL) {
NSString *nsString = nsUrl.path;
const char *string = nsString.UTF8String;
argv[argc++] = (char*)string;
self.launchOptions = launchOptions;
// Make sure SceneDelegate class is loaded
Class cls = objc_getClass("SceneDelegate");
if (!cls) {
NSLog(@"⚠️ SceneDelegate not found via objc_getClass");
} else {
NSLog(@"✅ SceneDelegate loaded via objc_getClass");
}
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleAudioSessionInterruption:) name:AVAudioSessionInterruptionNotification object:[AVAudioSession sharedInstance]];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleMediaServicesWereReset:) name:AVAudioSessionMediaServicesWereResetNotification object:nil];
#if PPSSPP_PLATFORM(IOS_APP_STORE)
[IAPManager sharedIAPManager]; // Kick off the IAPManager early.
#endif // IOS_APP_STORE
return [self launchPPSSPP:argc argv:argv];
}
- (BOOL)launchPPSSPP:(int)argc argv:(char**)argv;
{
NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
NSString *bundlePath = [[[NSBundle mainBundle] resourcePath] stringByAppendingString:@"/assets/"];
NativeInit(argc, (const char**)argv, documentsPath.UTF8String, bundlePath.UTF8String, NULL);
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// Choose viewcontroller depending on backend.
if (g_Config.iGPUBackend == (int)GPUBackend::VULKAN) {
PPSSPPViewControllerMetal *vc = [[PPSSPPViewControllerMetal alloc] init];
self.viewController = vc;
self.window.rootViewController = vc;
} else {
PPSSPPViewControllerGL *vc = [[PPSSPPViewControllerGL alloc] init];
// Here we can switch viewcontroller depending on backend.
self.viewController = vc;
self.window.rootViewController = vc;
}
[self.window makeKeyAndVisible];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleAudioSessionInterruption:) name:AVAudioSessionInterruptionNotification object:[AVAudioSession sharedInstance]];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleMediaServicesWereReset:) name:AVAudioSessionMediaServicesWereResetNotification object:nil];
return YES;
}
@@ -128,49 +104,7 @@
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)restart:(const char*)restartArgs {
INFO_LOG(Log::G3D, "Restart requested: %s", restartArgs);
[self.viewController willResignActive];
[self.viewController shutdown];
self.window.rootViewController = nil;
self.viewController = nil;
// App was requested to restart, probably.
INFO_LOG(Log::G3D, "viewController nilled");
NativeShutdown();
// TODO: Ignoring the command line for now.
// Hoping that overwriting the viewController works as expected...
[self launchPPSSPP:0 argv:nullptr];
[self.viewController didBecomeActive];
}
- (void)applicationWillResignActive:(UIApplication *)application {
INFO_LOG(Log::G3D, "willResignActive");
[self.viewController willResignActive];
if (g_Config.bEnableSound) {
iOSCoreAudioShutdown();
}
System_PostUIMessage(UIMessage::LOST_FOCUS);
}
- (void)applicationDidBecomeActive:(UIApplication *)application {
INFO_LOG(Log::G3D, "didBecomeActive");
if (g_Config.bEnableSound) {
iOSCoreAudioInit();
}
System_PostUIMessage(UIMessage::GOT_FOCUS);
[self.viewController didBecomeActive];
}
- (void)applicationWillTerminate:(UIApplication *)application {
// Seems like a bad idea.
// exit(0);
}
@end
+8
View File
@@ -215,4 +215,12 @@ static bool SafeStringEqual(NSString *a, NSString *b) {
}
}
- (void)productsRequest:(nonnull SKProductsRequest *)request didReceiveResponse:(nonnull SKProductsResponse *)response {
NSLog(@"Ignoring [productsRequest]");
}
- (void)paymentQueue:(nonnull SKPaymentQueue *)queue updatedTransactions:(nonnull NSArray<SKPaymentTransaction *> *)transactions {
NSLog(@"Ignoring [paymentQueue]");
}
@end
+15
View File
@@ -206,5 +206,20 @@
</dict>
</dict>
</array>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>SceneDelegate</string>
</dict>
</array>
</dict>
</dict>
</dict>
</plist>
+12
View File
@@ -0,0 +1,12 @@
// SceneDelegate.h
#import <UIKit/UIKit.h>
@interface SceneDelegate : UIResponder <UIWindowSceneDelegate>
- (void)restart:(const char *)args;
@property (strong, nonatomic) UIWindow * window;
@property (strong, nonatomic) UIViewController *viewController;
@property (strong, nonatomic) UIWindowScene *windowScene;
@end
+124
View File
@@ -0,0 +1,124 @@
// SceneDelegate.mm
#import "SceneDelegate.h"
#import "AppDelegate.h"
#import "iOSCoreAudio.h"
#import "ViewControllerCommon.h"
#import "ViewController.h"
#import "ViewControllerMetal.h"
#import "Common/System/NativeApp.h"
#import "Common/System/System.h"
#import "Core/System.h"
#import "Core/Config.h"
#import "Common/Log.h"
#import "IAPManager.h"
#import <AVFoundation/AVFoundation.h>
@implementation SceneDelegate
+ (void)load {
NSLog(@"✅ SceneDelegate class was loaded!");
}
- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions {
if (![scene isKindOfClass:[UIWindowScene class]]) {
return;
}
UIWindowScene *windowScene = (UIWindowScene *)scene;
if (self.window) {
NSLog(@"✅ Window already exists, not creating again!");
return;
}
self.windowScene = windowScene;
[self launchPPSSPP];
}
-(void) launchPPSSPP {
INFO_LOG(Log::G3D, "SceneDelegate: Launching PPSSPP");
AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
NSDictionary *launchOptions = appDelegate.launchOptions;
int argc = 1;
char *argv[5]{};
NSURL *nsUrl = [launchOptions objectForKey:UIApplicationLaunchOptionsURLKey];
if (nsUrl != nullptr && nsUrl.isFileURL) {
NSString *nsString = nsUrl.path;
const char *string = nsString.UTF8String;
argv[argc++] = (char*)string;
}
NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
NSString *bundlePath = [[[NSBundle mainBundle] resourcePath] stringByAppendingString:@"/assets/"];
NativeInit(argc, (const char**)argv, documentsPath.UTF8String, bundlePath.UTF8String, NULL);
self.window = [[UIWindow alloc] initWithWindowScene:self.windowScene];
// self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// Choose viewcontroller depending on backend.
if (g_Config.iGPUBackend == (int)GPUBackend::VULKAN) {
PPSSPPViewControllerMetal *vc = [[PPSSPPViewControllerMetal alloc] init];
self.viewController = vc;
self.window.rootViewController = vc;
} else {
PPSSPPViewControllerGL *vc = [[PPSSPPViewControllerGL alloc] init];
// Here we can switch viewcontroller depending on backend.
self.viewController = vc;
self.window.rootViewController = vc;
}
[self.window makeKeyAndVisible];
}
- (void)restart:(const char*)restartArgs {
INFO_LOG(Log::G3D, "SceneDelegate: Restart requested: %s", restartArgs);
// Notify current view controller
[self.viewController willResignActive];
[self.viewController shutdown];
// Remove the current root view controller
self.window.rootViewController = nil;
self.viewController = nil;
INFO_LOG(Log::G3D, "SceneDelegate: viewController nilled");
// Shut down native systems
NativeShutdown();
// Launch a fresh instance
[self launchPPSSPP];
// Notify new view controller
[self.viewController didBecomeActive];
}
- (void)sceneWillResignActive:(UIScene *)scene {
INFO_LOG(Log::G3D, "sceneWillResignActive");
[self.viewController willResignActive];
if (g_Config.bEnableSound) {
iOSCoreAudioShutdown();
}
System_PostUIMessage(UIMessage::LOST_FOCUS);
}
- (void)sceneDidBecomeActive:(UIScene *)scene {
INFO_LOG(Log::G3D, "sceneDidBecomeActive");
if (g_Config.bEnableSound) {
iOSCoreAudioInit();
}
System_PostUIMessage(UIMessage::GOT_FOCUS);
[self.viewController didBecomeActive];
}
@end
+13 -1
View File
@@ -26,6 +26,7 @@
#import "ViewController.h"
#import "iOSCoreAudio.h"
#import "IAPManager.h"
#import "SceneDelegate.h"
#include "Common/MemoryUtil.h"
#include "Common/Audio/AudioBackend.h"
@@ -439,7 +440,18 @@ bool System_MakeRequest(SystemRequestType type, int requestId, const std::string
switch (type) {
case SystemRequestType::RESTART_APP:
dispatch_async(dispatch_get_main_queue(), ^{
[(AppDelegate *)[[UIApplication sharedApplication] delegate] restart:param1.c_str()];
// Get the connected scenes
NSSet<UIScene *> *scenes = [UIApplication sharedApplication].connectedScenes;
// Loop through scenes to find a UIWindowScene that is active
for (UIScene *scene in scenes) {
if ([scene isKindOfClass:[UIWindowScene class]]) {
UIWindowScene *windowScene = (UIWindowScene *)scene;
SceneDelegate *sceneDelegate = (SceneDelegate *)windowScene.delegate;
[sceneDelegate restart:param1.c_str()];
break; // call only on the first active scene
}
}
});
return true;