diff --git a/.gitmodules b/.gitmodules index 17fee96935..f4dc9f382a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -59,3 +59,6 @@ [submodule "ext/OpenXR-SDK"] path = ext/OpenXR-SDK url = https://github.com/KhronosGroup/OpenXR-SDK.git +[submodule "ios/MoltenVK"] + path = ios/MoltenVK + url = git@github.com:hrydgard/ppsspp-moltenvk.git diff --git a/CMakeLists.txt b/CMakeLists.txt index b1e38b10a5..51697fed25 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -140,6 +140,8 @@ if(IOS_APP_STORE) # Set a global default to not generate schemes for each target. # Makes using XCode sligthly more sane. set(CMAKE_XCODE_GENERATE_SCHEME NO) + set(CMAKE_XCODE_SCHEME_ENABLE_GPU_API_VALIDATION FALSE) + set(CMAKE_XCODE_SCHEME_ENABLE_GPU_FRAME_CAPTURE_MODE DISABLED) message("iOS App Store build") else() message("iOS sideload build") @@ -1295,6 +1297,8 @@ elseif(IOS AND NOT LIBRETRO) ios/ViewControllerCommon.h ios/ViewController.mm ios/ViewController.h + ios/ViewControllerMetal.mm + ios/ViewControllerMetal.h ios/iOSCoreAudio.mm ios/iOSCoreAudio.h ios/CameraHelper.mm @@ -1313,7 +1317,7 @@ elseif(IOS AND NOT LIBRETRO) Common/Battery/AppleBatteryClient.m ) - set(nativeExtraLibs ${nativeExtraLibs} "-framework Foundation -framework MediaPlayer -framework AudioToolbox -framework CoreGraphics -framework QuartzCore -framework UIKit -framework GLKit -framework OpenAL -framework AVFoundation -framework CoreLocation -framework CoreVideo -framework CoreMedia -framework CoreServices" ) + set(nativeExtraLibs ${nativeExtraLibs} "-framework Foundation -framework MediaPlayer -framework AudioToolbox -framework CoreGraphics -framework QuartzCore -framework UIKit -framework GLKit -framework OpenAL -framework AVFoundation -framework CoreLocation -framework CoreVideo -framework CoreMedia -framework CoreServices -framework Metal -framework IOSurface" ) if(EXISTS "${CMAKE_IOS_SDK_ROOT}/System/Library/Frameworks/GameController.framework") set(nativeExtraLibs ${nativeExtraLibs} "-weak_framework GameController") endif() @@ -2879,13 +2883,18 @@ if(IOS AND NOT LIBRETRO) # This updates the version in the plist. COMMAND /bin/bash "${CMAKE_SOURCE_DIR}/ios/iosbundle.sh" \"${APP_DIR_NAME}\" "${CMAKE_CURRENT_BINARY_DIR}" ) + + # Can't figure out properly using .xcframework from CMake, so just linking directly to the .a file. + target_link_libraries(${TargetBin} + "${CMAKE_CURRENT_SOURCE_DIR}/ios/MoltenVK/MoltenVK.xcframework/ios-arm64/libMoltenVK.a" + ) + # https://stackoverflow.com/questions/40664125/cmake-and-code-signing-in-xcode-8-for-ios-projects set_target_properties(${TargetBin} PROPERTIES XCODE_GENERATE_SCHEME YES # Avoid the scheme bloat in XCode by only setting it to YES for this target. RESOURCE "ios/Launch Screen.storyboard" RESOURCE "ios/Settings.bundle" RESOURCE "ios/assets.xcassets" - RESOURCE "ext/vulkan/iOS/Frameworks" XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER ${BUNDLE_IDENTIFIER} XCODE_ATTRIBUTE_PRODUCT_NAME ${PRODUCT_NAME} XCODE_ATTRIBUTE_ASSETCATALOG_COMPILER_APPICON_NAME ${ICON_NAME} diff --git a/Common/GPU/OpenGL/GLRenderManager.cpp b/Common/GPU/OpenGL/GLRenderManager.cpp index 575289707b..b08c59a23a 100644 --- a/Common/GPU/OpenGL/GLRenderManager.cpp +++ b/Common/GPU/OpenGL/GLRenderManager.cpp @@ -348,6 +348,9 @@ void GLRenderManager::BeginFrame(bool enableProfiling) { curProgram_ = nullptr; #endif + // Shouldn't call BeginFrame unless we're in a run state. + _dbg_assert_(runCompileThread_); + int curFrame = GetCurFrame(); FrameTimeData &frameTimeData = frameTimeHistory_.Add(frameIdGen_); @@ -368,10 +371,6 @@ void GLRenderManager::BeginFrame(bool enableProfiling) { frameData.readyForFence = false; } - if (!runCompileThread_) { - WARN_LOG(G3D, "BeginFrame while !run_!"); - } - insideFrame_ = true; } diff --git a/Common/GPU/Vulkan/VulkanContext.cpp b/Common/GPU/Vulkan/VulkanContext.cpp index fed7938c77..a482d52b0f 100644 --- a/Common/GPU/Vulkan/VulkanContext.cpp +++ b/Common/GPU/Vulkan/VulkanContext.cpp @@ -183,6 +183,11 @@ VkResult VulkanContext::CreateInstance(const CreateInfo &info) { if (EnableInstanceExtension(VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME, 0)) { extensionsLookup_.EXT_swapchain_colorspace = true; } +#if PPSSPP_PLATFORM(IOS_APP_STORE) + if (EnableInstanceExtension(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME, 0)) { + + } +#endif // Validate that all the instance extensions we ask for are actually available. for (auto ext : instance_extensions_enabled_) { @@ -206,6 +211,10 @@ VkResult VulkanContext::CreateInstance(const CreateInfo &info) { inst_info.enabledExtensionCount = (uint32_t)instance_extensions_enabled_.size(); inst_info.ppEnabledExtensionNames = instance_extensions_enabled_.size() ? instance_extensions_enabled_.data() : nullptr; +#if PPSSPP_PLATFORM(IOS_APP_STORE) + inst_info.flags |= VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR; +#endif + #if SIMULATE_VULKAN_FAILURE == 2 VkResult res = VK_ERROR_INCOMPATIBLE_DRIVER; #else @@ -896,7 +905,7 @@ VkResult VulkanContext::ReinitSurface() { surface_ = VK_NULL_HANDLE; } - INFO_LOG(G3D, "Creating Vulkan surface for window (%p %p)", winsysData1_, winsysData2_); + INFO_LOG(G3D, "Creating Vulkan surface for window (data1=%p data2=%p)", winsysData1_, winsysData2_); VkResult retval = VK_SUCCESS; @@ -1311,7 +1320,11 @@ bool VulkanContext::InitSwapchain() { VkExtent2D currentExtent { surfCapabilities_.currentExtent }; // https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkSurfaceCapabilitiesKHR.html // currentExtent is the current width and height of the surface, or the special value (0xFFFFFFFF, 0xFFFFFFFF) indicating that the surface size will be determined by the extent of a swapchain targeting the surface. - if (currentExtent.width == 0xFFFFFFFFu || currentExtent.height == 0xFFFFFFFFu) { + if (currentExtent.width == 0xFFFFFFFFu || currentExtent.height == 0xFFFFFFFFu +#if PPSSPP_PLATFORM(IOS) + || currentExtent.width == 0 || currentExtent.height == 0 +#endif + ) { _dbg_assert_((bool)cbGetDrawSize_) if (cbGetDrawSize_) { currentExtent = cbGetDrawSize_(); diff --git a/Common/GPU/Vulkan/VulkanLoader.cpp b/Common/GPU/Vulkan/VulkanLoader.cpp index 17596b50a3..ab0f6633a9 100644 --- a/Common/GPU/Vulkan/VulkanLoader.cpp +++ b/Common/GPU/Vulkan/VulkanLoader.cpp @@ -39,6 +39,7 @@ #endif namespace PPSSPP_VK { +#if !PPSSPP_PLATFORM(IOS_APP_STORE) PFN_vkCreateInstance vkCreateInstance; PFN_vkDestroyInstance vkDestroyInstance; PFN_vkEnumeratePhysicalDevices vkEnumeratePhysicalDevices; @@ -240,12 +241,14 @@ PFN_vkCreateRenderPass2 vkCreateRenderPass2; PFN_vkWaitForPresentKHR vkWaitForPresentKHR; PFN_vkGetPastPresentationTimingGOOGLE vkGetPastPresentationTimingGOOGLE; PFN_vkGetRefreshCycleDurationGOOGLE vkGetRefreshCycleDurationGOOGLE; - +#endif } // namespace PPSSPP_VK using namespace PPSSPP_VK; -#if PPSSPP_PLATFORM(SWITCH) +#if PPSSPP_PLATFORM(IOS_APP_STORE) +// Statically linked MoltenVK +#elif PPSSPP_PLATFORM(SWITCH) typedef void *VulkanLibraryHandle; static VulkanLibraryHandle vulkanLibrary; #define dlsym(x, y) nullptr @@ -280,7 +283,8 @@ static const char * const device_name_blacklist[] = { #ifndef _WIN32 static const char * const so_names[] = { -#if PPSSPP_PLATFORM(IOS) +#if PPSSPP_PLATFORM(IOS_APP_STORE) +#elif PPSSPP_PLATFORM(IOS) "@executable_path/Frameworks/libMoltenVK.dylib", "MoltenVK", #elif PPSSPP_PLATFORM(MAC) @@ -295,6 +299,7 @@ static const char * const so_names[] = { }; #endif +#if !PPSSPP_PLATFORM(IOS_APP_STORE) static VulkanLibraryHandle VulkanLoadLibrary(std::string *errorString) { #if PPSSPP_PLATFORM(SWITCH) // Always unavailable, for now. @@ -346,11 +351,13 @@ static VulkanLibraryHandle VulkanLoadLibrary(std::string *errorString) { break; } } -} + } return lib; #endif } +#endif +#if !PPSSPP_PLATFORM(IOS_APP_STORE) static void VulkanFreeLibrary(VulkanLibraryHandle &h) { if (h) { #if PPSSPP_PLATFORM(SWITCH) @@ -363,6 +370,7 @@ static void VulkanFreeLibrary(VulkanLibraryHandle &h) { h = nullptr; } } +#endif void VulkanSetAvailable(bool available) { INFO_LOG(G3D, "Setting Vulkan availability to true"); @@ -371,6 +379,12 @@ void VulkanSetAvailable(bool available) { } bool VulkanMayBeAvailable() { +#if PPSSPP_PLATFORM(IOS_APP_STORE) + g_vulkanAvailabilityChecked = true; + g_vulkanMayBeAvailable = true; + return true; +#else + // Unsupported in VR at the moment if (IsVREnabled()) { return false; @@ -557,9 +571,15 @@ bail: WARN_LOG(G3D, "Vulkan with working device not detected."); } return g_vulkanMayBeAvailable; +#endif } bool VulkanLoad(std::string *errorStr) { +#if PPSSPP_PLATFORM(IOS_APP_STORE) + INFO_LOG(G3D, "iOS: Vulkan doesn't need loading"); + return true; +#else + if (!vulkanLibrary) { vulkanLibrary = VulkanLoadLibrary(errorStr); if (!vulkanLibrary) { @@ -585,9 +605,11 @@ bool VulkanLoad(std::string *errorStr) { VulkanFreeLibrary(vulkanLibrary); return false; } +#endif } void VulkanLoadInstanceFunctions(VkInstance instance, const VulkanExtensions &enabledExtensions, uint32_t vulkanApiVersion) { +#if !PPSSPP_PLATFORM(IOS_APP_STORE) // OK, let's use the above functions to get the rest. LOAD_INSTANCE_FUNC(instance, vkDestroyInstance); LOAD_INSTANCE_FUNC(instance, vkEnumeratePhysicalDevices); @@ -655,12 +677,14 @@ void VulkanLoadInstanceFunctions(VkInstance instance, const VulkanExtensions &en } INFO_LOG(G3D, "Vulkan instance functions loaded."); +#endif } // On some implementations, loading functions (that have Device as their first parameter) via vkGetDeviceProcAddr may // increase performance - but then these function pointers will only work on that specific device. Thus, this loader is not very // good for multi-device - not likely we'll ever try that anyway though. void VulkanLoadDeviceFunctions(VkDevice device, const VulkanExtensions &enabledExtensions, uint32_t vulkanApiVersion) { +#if !PPSSPP_PLATFORM(IOS_APP_STORE) INFO_LOG(G3D, "Vulkan device functions loaded."); LOAD_DEVICE_FUNC(device, vkQueueSubmit); @@ -799,10 +823,13 @@ void VulkanLoadDeviceFunctions(VkDevice device, const VulkanExtensions &enabledE if (enabledExtensions.KHR_create_renderpass2) { LOAD_DEVICE_FUNC_CORE(device, vkCreateRenderPass2, vkCreateRenderPass2KHR, VK_API_VERSION_1_2); } +#endif } void VulkanFree() { +#if !PPSSPP_PLATFORM(IOS_APP_STORE) VulkanFreeLibrary(vulkanLibrary); +#endif } const char *VulkanResultToString(VkResult res) { diff --git a/Common/GPU/Vulkan/VulkanLoader.h b/Common/GPU/Vulkan/VulkanLoader.h index 1d1c3d0a17..a7ca178c16 100644 --- a/Common/GPU/Vulkan/VulkanLoader.h +++ b/Common/GPU/Vulkan/VulkanLoader.h @@ -17,7 +17,9 @@ #pragma once -#ifdef __ANDROID__ +#include "ppsspp_config.h" + +#if PPSSPP_PLATFORM(ANDROID) #define VK_USE_PLATFORM_ANDROID_KHR #elif defined(_WIN32) #define VK_USE_PLATFORM_WIN32_KHR @@ -29,7 +31,10 @@ #define VK_USE_PLATFORM_METAL_EXT #endif +#if !PPSSPP_PLATFORM(IOS_APP_STORE) #define VK_NO_PROTOTYPES +#define VK_ENABLE_BETA_EXTENSIONS 1 // VK_KHR_portability_subset +#endif #include "ext/vulkan/vulkan.h" #include @@ -40,6 +45,7 @@ #endif namespace PPSSPP_VK { +#if !PPSSPP_PLATFORM(IOS_APP_STORE) // Putting our own Vulkan function pointers in a namespace ensures that ppsspp_libretro.so doesn't collide with libvulkan.so. extern PFN_vkCreateInstance vkCreateInstance; extern PFN_vkDestroyInstance vkDestroyInstance; @@ -240,6 +246,7 @@ extern PFN_vkGetMemoryHostPointerPropertiesEXT vkGetMemoryHostPointerPropertiesE extern PFN_vkWaitForPresentKHR vkWaitForPresentKHR; extern PFN_vkGetPastPresentationTimingGOOGLE vkGetPastPresentationTimingGOOGLE; extern PFN_vkGetRefreshCycleDurationGOOGLE vkGetRefreshCycleDurationGOOGLE; +#endif // !PPSSPP_PLATFORM(IOS_APP_STORE) } // namespace PPSSPP_VK // For fast extension-enabled checks. diff --git a/Common/GPU/Vulkan/VulkanRenderManager.cpp b/Common/GPU/Vulkan/VulkanRenderManager.cpp index d8a2bea729..472f2e240f 100644 --- a/Common/GPU/Vulkan/VulkanRenderManager.cpp +++ b/Common/GPU/Vulkan/VulkanRenderManager.cpp @@ -563,6 +563,7 @@ void VulkanRenderManager::RenderThreadFunc() { void VulkanRenderManager::PresentWaitThreadFunc() { SetCurrentThreadName("PresentWait"); +#if !PPSSPP_PLATFORM(IOS_APP_STORE) _dbg_assert_(vkWaitForPresentKHR != nullptr); uint64_t waitedId = frameIdGen_; @@ -579,6 +580,7 @@ void VulkanRenderManager::PresentWaitThreadFunc() { } _dbg_assert_(waitedId <= frameIdGen_); } +#endif INFO_LOG(G3D, "Leaving PresentWaitThreadFunc()"); } diff --git a/UI/NativeApp.cpp b/UI/NativeApp.cpp index b1cefec604..948a6d4a7b 100644 --- a/UI/NativeApp.cpp +++ b/UI/NativeApp.cpp @@ -1502,7 +1502,7 @@ void NativeShutdown() { ShutdownWebServer(); -#if PPSSPP_PLATFORM(ANDROID) || PPSSPP_PLATFORM(IOS) +#if PPSSPP_PLATFORM(ANDROID) System_ExitApp(); #endif diff --git a/android/jni/app-android.cpp b/android/jni/app-android.cpp index 76e0603c9f..6345d08985 100644 --- a/android/jni/app-android.cpp +++ b/android/jni/app-android.cpp @@ -112,6 +112,7 @@ enum class EmuThreadState { STOPPED, }; +// OpenGL emu thread static std::thread emuThread; static std::atomic emuThreadState((int)EmuThreadState::DISABLED); @@ -1549,7 +1550,7 @@ static void ProcessFrameCommands(JNIEnv *env) { } } -std::thread g_vulkanRenderLoopThread; +std::thread g_renderLoopThread; static void VulkanEmuThread(ANativeWindow *wnd); @@ -1563,7 +1564,7 @@ extern "C" bool JNICALL Java_org_ppsspp_ppsspp_NativeActivity_runVulkanRenderLoo return false; } - if (g_vulkanRenderLoopThread.joinable()) { + if (g_renderLoopThread.joinable()) { ERROR_LOG(G3D, "runVulkanRenderLoop: Already running"); return false; } @@ -1577,7 +1578,7 @@ extern "C" bool JNICALL Java_org_ppsspp_ppsspp_NativeActivity_runVulkanRenderLoo return false; } - g_vulkanRenderLoopThread = std::thread(VulkanEmuThread, wnd); + g_renderLoopThread = std::thread(VulkanEmuThread, wnd); return true; } @@ -1586,11 +1587,11 @@ extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeActivity_requestExitVulkanR ERROR_LOG(SYSTEM, "Render loop already exited"); return; } - _assert_(g_vulkanRenderLoopThread.joinable()); + _assert_(g_renderLoopThread.joinable()); exitRenderLoop = true; - g_vulkanRenderLoopThread.join(); - _assert_(!g_vulkanRenderLoopThread.joinable()); - g_vulkanRenderLoopThread = std::thread(); + g_renderLoopThread.join(); + _assert_(!g_renderLoopThread.joinable()); + g_renderLoopThread = std::thread(); } // TODO: Merge with the Win32 EmuThread and so on, and the Java EmuThread? diff --git a/ios/AppDelegate.h b/ios/AppDelegate.h index bd5bf00795..06ab74d32a 100644 --- a/ios/AppDelegate.h +++ b/ios/AppDelegate.h @@ -11,4 +11,7 @@ @property (strong, nonatomic) id viewController; +- (void)restart:(const char *)args; +- (BOOL)launchPPSSPP:(int)argc argv:(char**)argv; + @end diff --git a/ios/AppDelegate.mm b/ios/AppDelegate.mm index fbcb84be8d..b0c478166e 100644 --- a/ios/AppDelegate.mm +++ b/ios/AppDelegate.mm @@ -1,5 +1,7 @@ #import "AppDelegate.h" +#import "ViewControllerCommon.h" #import "ViewController.h" +#import "ViewControllerMetal.h" #import "iOSCoreAudio.h" #import "Common/System/System.h" #import "Common/System/NativeApp.h" @@ -74,10 +76,9 @@ } -(BOOL) application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - int argc = 1; - char *argv[3] = { 0 }; - NSURL* nsUrl = [launchOptions objectForKey:UIApplicationLaunchOptionsURLKey]; + char *argv[5]{}; + NSURL *nsUrl = [launchOptions objectForKey:UIApplicationLaunchOptionsURLKey]; if (nsUrl != nullptr && nsUrl.isFileURL) { NSString *nsString = nsUrl.path; @@ -85,31 +86,66 @@ argv[argc++] = (char*)string; } + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleAudioSessionInterruption:) name:AVAudioSessionInterruptionNotification object:[AVAudioSession sharedInstance]]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleMediaServicesWereReset:) name:AVAudioSessionMediaServicesWereResetNotification object:nil]; + + 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]]; - PPSSPPViewControllerGL *vc = [[PPSSPPViewControllerGL alloc] init]; - // Here we can switch viewcontroller depending on backend. - self.viewController = vc; + // Choose viewcontroller depending on backend. + if (g_Config.iGPUBackend == (int)GPUBackend::VULKAN) { + PPSSPPViewControllerMetal *vc = [[PPSSPPViewControllerMetal alloc] init]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleAudioSessionInterruption:) name:AVAudioSessionInterruptionNotification object:[AVAudioSession sharedInstance]]; + self.viewController = vc; + self.window.rootViewController = vc; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleMediaServicesWereReset:) name:AVAudioSessionMediaServicesWereResetNotification object:nil]; + } else { + PPSSPPViewControllerGL *vc = [[PPSSPPViewControllerGL alloc] init]; + // Here we can switch viewcontroller depending on backend. + self.viewController = vc; + self.window.rootViewController = vc; + } - self.window.rootViewController = vc; [self.window makeKeyAndVisible]; return YES; } --(void) dealloc { +- (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } --(void) applicationWillResignActive:(UIApplication *)application { +- (void)restart:(const char*)restartArgs { + INFO_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(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(G3D, "willResignActive"); + + [self.viewController willResignActive]; + if (g_Config.bEnableSound) { iOSCoreAudioShutdown(); } @@ -117,12 +153,14 @@ System_PostUIMessage(UIMessage::LOST_FOCUS); } --(void) applicationDidBecomeActive:(UIApplication *)application { +- (void)applicationDidBecomeActive:(UIApplication *)application { + INFO_LOG(G3D, "didBecomeActive"); if (g_Config.bEnableSound) { iOSCoreAudioInit(); } System_PostUIMessage(UIMessage::GOT_FOCUS); + [self.viewController didBecomeActive]; } - (void)applicationWillTerminate:(UIApplication *)application { diff --git a/ios/DisplayManager.mm b/ios/DisplayManager.mm index 02c795de9f..3e19194bef 100644 --- a/ios/DisplayManager.mm +++ b/ios/DisplayManager.mm @@ -133,18 +133,11 @@ } - (void)updateResolution:(UIScreen *)screen { - float scale = screen.scale; - - if ([screen respondsToSelector:@selector(nativeScale)]) { - scale = screen.nativeScale; - } - + float scale = screen.nativeScale; CGSize size = screen.bounds.size; if (size.height > size.width) { - float h = size.height; - size.height = size.width; - size.width = h; + std::swap(size.height, size.width); } if (screen == [UIScreen mainScreen]) { @@ -159,10 +152,10 @@ g_display.dpi_scale_real_y = g_display.dpi_scale_y; g_display.pixel_xres = size.width * scale; g_display.pixel_yres = size.height * scale; - + g_display.dp_xres = g_display.pixel_xres * g_display.dpi_scale_x; g_display.dp_yres = g_display.pixel_yres * g_display.dpi_scale_y; - + g_display.pixel_in_dps_x = (float)g_display.pixel_xres / (float)g_display.dp_xres; g_display.pixel_in_dps_y = (float)g_display.pixel_yres / (float)g_display.dp_yres; @@ -171,7 +164,7 @@ // PSP native resize PSP_CoreParameter().pixelWidth = g_display.pixel_xres; PSP_CoreParameter().pixelHeight = g_display.pixel_yres; - + NativeResized(); NSLog(@"Updated display resolution: (%d, %d) @%.1fx", g_display.pixel_xres, g_display.pixel_yres, scale); diff --git a/ios/Launch Screen.storyboard b/ios/Launch Screen.storyboard index 513011ac21..78a1ea3758 100644 --- a/ios/Launch Screen.storyboard +++ b/ios/Launch Screen.storyboard @@ -25,35 +25,6 @@ - - - - - - - - - - - diff --git a/ios/MoltenVK b/ios/MoltenVK new file mode 160000 index 0000000000..ad018e8664 --- /dev/null +++ b/ios/MoltenVK @@ -0,0 +1 @@ +Subproject commit ad018e8664d343dd1e6d7a6ce712c64a6f8fee2a diff --git a/ios/README.md b/ios/README.md index f451fb5c1c..1db38a22a6 100644 --- a/ios/README.md +++ b/ios/README.md @@ -1,5 +1,11 @@ -iOS Build Instructions -====================== +The Old iOS Build Instructions +============================== + +Important Notes +--------------- + +These instructions have not been updated for a long time and may be outdated. + Prerequisites: -------------- diff --git a/ios/ViewController.h b/ios/ViewController.h index fb4fe1c9dc..a6eaabeb70 100644 --- a/ios/ViewController.h +++ b/ios/ViewController.h @@ -3,6 +3,7 @@ #import #import #import + #import "iCade/iCadeReaderView.h" #import "CameraHelper.h" #import "LocationHelper.h" @@ -13,5 +14,3 @@ iCadeEventDelegate, LocationHandlerDelegate, CameraFrameDelegate, UIGestureRecognizerDelegate, UIKeyInput, PPSSPPViewController> @end - -extern id sharedViewController; diff --git a/ios/ViewController.mm b/ios/ViewController.mm index e97d67406f..803c19b5e2 100644 --- a/ios/ViewController.mm +++ b/ios/ViewController.mm @@ -26,6 +26,7 @@ #include "Common/System/OSD.h" #include "Common/System/NativeApp.h" #include "Common/File/VFS/VFS.h" +#include "Common/Thread/ThreadUtil.h" #include "Common/Log.h" #include "Common/TimeUtil.h" #include "Common/Input/InputState.h" @@ -39,13 +40,6 @@ #include "Core/HLE/sceUsbCam.h" #include "Core/HLE/sceUsbGps.h" -#include -#include -#include - -#define IS_IPAD() ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) -#define IS_IPHONE() ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone) - class IOSGLESContext : public GraphicsContext { public: IOSGLESContext() { @@ -88,20 +82,19 @@ private: GLRenderManager *renderManager_; }; -static bool threadEnabled = true; -static bool threadStopped = false; +static std::atomic exitRenderLoop; +static std::atomic renderLoopRunning; +static std::thread g_renderLoopThread; id sharedViewController; -// TODO: Reach these through sharedViewController -static CameraHelper *cameraHelper; - @interface PPSSPPViewControllerGL () { ICadeTracker g_iCadeTracker; TouchTracker g_touchTracker; - GraphicsContext *graphicsContext; + IOSGLESContext *graphicsContext; LocationHelper *locationHelper; + CameraHelper *cameraHelper; } @property (nonatomic, strong) EAGLContext* context; @@ -120,12 +113,8 @@ static CameraHelper *cameraHelper; g_iCadeTracker.InitKeyMap(); [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appWillTerminate:) name:UIApplicationWillTerminateNotification object:nil]; - - if ([GCController class]) // Checking the availability of a GameController framework - { - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(controllerDidConnect:) name:GCControllerDidConnectNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(controllerDidDisconnect:) name:GCControllerDidDisconnectNotification object:nil]; - } + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(controllerDidConnect:) name:GCControllerDidConnectNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(controllerDidDisconnect:) name:GCControllerDidDisconnectNotification object:nil]; } return self; } @@ -158,6 +147,59 @@ extern float g_safeInsetBottom; } } +// The actual rendering is NOT on this thread, this is the emu thread +// that runs game logic. +void GLRenderLoop(IOSGLESContext *graphicsContext) { + SetCurrentThreadName("EmuThreadGL"); + renderLoopRunning = true; + + // graphicsContext->StartThread(); + NativeInitGraphics(graphicsContext); + + INFO_LOG(SYSTEM, "Emulation thread starting\n"); + while (!exitRenderLoop) { + NativeFrame(graphicsContext); + } + + INFO_LOG(SYSTEM, "Emulation thread shutting down\n"); + NativeShutdownGraphics(); + + // Also ask the main thread to stop, so it doesn't hang waiting for a new frame. + INFO_LOG(SYSTEM, "Emulation thread stopping\n"); + + exitRenderLoop = false; + renderLoopRunning = false; +} + +- (bool)runGLRenderLoop { + if (!graphicsContext) { + ERROR_LOG(G3D, "runVulkanRenderLoop: Tried to enter without a created graphics context."); + return false; + } + + if (g_renderLoopThread.joinable()) { + ERROR_LOG(G3D, "runVulkanRenderLoop: Already running"); + return false; + } + + _dbg_assert_(!renderLoopRunning); + _dbg_assert_(!exitRenderLoop); + + g_renderLoopThread = std::thread(GLRenderLoop, graphicsContext); + return true; +} + +- (void)requestExitGLRenderLoop { + if (!renderLoopRunning) { + ERROR_LOG(SYSTEM, "Render loop already exited"); + return; + } + _assert_(g_renderLoopThread.joinable()); + exitRenderLoop = true; + g_renderLoopThread.join(); + _assert_(!g_renderLoopThread.joinable()); +} + - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; [self hideKeyboard]; @@ -176,10 +218,6 @@ extern float g_safeInsetBottom; self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]; } - UIScreenEdgePanGestureRecognizer *mBackGestureRecognizer = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(handleSwipeFrom:) ]; - [mBackGestureRecognizer setEdges:UIRectEdgeLeft]; - [[self view] addGestureRecognizer:mBackGestureRecognizer]; - GLKView* view = (GLKView *)self.view; view.context = self.context; view.drawableDepthFormat = GLKViewDrawableDepthFormat24; @@ -202,10 +240,8 @@ extern float g_safeInsetBottom; self.iCadeView.delegate = self; self.iCadeView.active = YES;*/ - if ([GCController class]) { - if ([[GCController controllers] count] > 0) { - [self setupController:[[GCController controllers] firstObject]]; - } + if ([[GCController controllers] count] > 0) { + [self setupController:[[GCController controllers] firstObject]]; } cameraHelper = [[CameraHelper alloc] init]; @@ -216,23 +252,11 @@ extern float g_safeInsetBottom; [self hideKeyboard]; - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ - NativeInitGraphics(graphicsContext); + UIScreenEdgePanGestureRecognizer *mBackGestureRecognizer = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(handleSwipeFrom:) ]; + [mBackGestureRecognizer setEdges:UIRectEdgeLeft]; + [[self view] addGestureRecognizer:mBackGestureRecognizer]; - INFO_LOG(SYSTEM, "Emulation thread starting\n"); - while (threadEnabled) { - NativeFrame(graphicsContext); - } - - INFO_LOG(SYSTEM, "Emulation thread shutting down\n"); - NativeShutdownGraphics(); - - // Also ask the main thread to stop, so it doesn't hang waiting for a new frame. - INFO_LOG(SYSTEM, "Emulation thread stopping\n"); - graphicsContext->StopThread(); - - threadStopped = true; - }); + INFO_LOG(G3D, "Done with viewDidLoad. Next up, OpenGL"); } - (void)handleSwipeFrom:(UIScreenEdgePanGestureRecognizer *)recognizer @@ -252,23 +276,24 @@ extern float g_safeInsetBottom; [self shutdown]; } +- (void)didBecomeActive { + INFO_LOG(SYSTEM, "didBecomeActive begin"); + [self runGLRenderLoop]; + INFO_LOG(SYSTEM, "didBecomeActive end"); +} + +- (void)willResignActive { + INFO_LOG(SYSTEM, "willResignActive begin"); + [self requestExitGLRenderLoop]; + INFO_LOG(SYSTEM, "willResignActive end"); +} + - (void)shutdown { - if (sharedViewController == nil) { - return; - } - - iOSCoreAudioShutdown(); - - if (threadEnabled) { - threadEnabled = false; - while (graphicsContext->ThreadFrame()) { - continue; - } - while (!threadStopped) {} - graphicsContext->ThreadEnd(); - } + INFO_LOG(SYSTEM, "shutdown GL"); + _dbg_assert_(graphicsContext); + _dbg_assert_(sharedViewController != nil); sharedViewController = nil; if (self.context) { @@ -280,22 +305,17 @@ extern float g_safeInsetBottom; [[NSNotificationCenter defaultCenter] removeObserver:self]; - if ([GCController class]) { - self.gameController = nil; - } + self.gameController = nil; - if (graphicsContext) { - graphicsContext->Shutdown(); - delete graphicsContext; - graphicsContext = NULL; - } - - NativeShutdown(); + graphicsContext->StopThread(); + graphicsContext->Shutdown(); + delete graphicsContext; + graphicsContext = nullptr; } - (void)dealloc { - [self shutdown]; + INFO_LOG(SYSTEM, "dealloc GL"); } - (NSUInteger)supportedInterfaceOrientations @@ -305,8 +325,14 @@ extern float g_safeInsetBottom; - (void)glkView:(GLKView *)view drawInRect:(CGRect)rect { + if (!renderLoopRunning) { + INFO_LOG(G3D, "Ignoring drawInRect"); + return; + } + INFO_LOG(G3D, "drawInRect start"); if (sharedViewController) graphicsContext->ThreadFrame(); + INFO_LOG(G3D, "drawInRect end"); } - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event diff --git a/ios/ViewControllerCommon.h b/ios/ViewControllerCommon.h index 52766bff4d..cfbe5e7daf 100644 --- a/ios/ViewControllerCommon.h +++ b/ios/ViewControllerCommon.h @@ -16,4 +16,13 @@ - (void)startVideo:(int)width height:(int)height; - (void)stopVideo; +// Forwarded from the AppDelegate +- (void)didBecomeActive; +- (void)willResignActive; + @end + +extern id sharedViewController; + +#define IS_IPAD() ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) +#define IS_IPHONE() ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone) diff --git a/ios/ViewControllerMetal.h b/ios/ViewControllerMetal.h new file mode 100644 index 0000000000..bb236e4195 --- /dev/null +++ b/ios/ViewControllerMetal.h @@ -0,0 +1,18 @@ +// ViewControllerMetal +// Used by both Vulkan/MoltenVK and the future Metal backend. + +#pragma once +#include "ViewControllerCommon.h" + +#import "iCade/iCadeReaderView.h" +#import "CameraHelper.h" +#import "LocationHelper.h" + +@interface PPSSPPViewControllerMetal : UIViewController< + iCadeEventDelegate, LocationHandlerDelegate, CameraFrameDelegate, + UIGestureRecognizerDelegate, UIKeyInput, PPSSPPViewController> +@end + +/** The Metal-compatibile view. */ +@interface PPSSPPMetalView : UIView +@end \ No newline at end of file diff --git a/ios/ViewControllerMetal.mm b/ios/ViewControllerMetal.mm new file mode 100644 index 0000000000..1fd177a8f3 --- /dev/null +++ b/ios/ViewControllerMetal.mm @@ -0,0 +1,647 @@ +#import "AppDelegate.h" +#import "ViewControllerMetal.h" +#import "DisplayManager.h" +#include "Controls.h" +#import "iOSCoreAudio.h" + +#include "Common/Log.h" + +#include "Common/GPU/Vulkan/VulkanLoader.h" +#include "Common/GPU/Vulkan/VulkanContext.h" +#include "Common/GPU/Vulkan/VulkanRenderManager.h" +#include "Common/GPU/thin3d.h" +#include "Common/GPU/thin3d_create.h" +#include "Common/Data/Text/Parsers.h" +#include "Common/Data/Encoding/Utf8.h" +#include "Common/System/Display.h" +#include "Common/System/System.h" +#include "Common/System/OSD.h" +#include "Common/System/NativeApp.h" +#include "Common/GraphicsContext.h" +#include "Common/Thread/ThreadUtil.h" + +#include "Core/Config.h" +#include "Core/ConfigValues.h" +#include "Core/System.h" +#include "Core/HLE/sceUsbCam.h" +#include "Core/HLE/sceUsbGps.h" + +// ViewController lifecycle: +// https://www.progressconcepts.com/blog/ios-appdelegate-viewcontroller-method-order/ + +// TODO: Share this between backends. +static uint32_t FlagsFromConfig() { + uint32_t flags; + if (g_Config.bVSync) { + flags = VULKAN_FLAG_PRESENT_FIFO; + } else { + flags = VULKAN_FLAG_PRESENT_MAILBOX | VULKAN_FLAG_PRESENT_IMMEDIATE; + } + return flags; +} + +enum class GraphicsContextState { + PENDING, + INITIALIZED, + FAILED_INIT, + SHUTDOWN, +}; + +class IOSVulkanContext : public GraphicsContext { +public: + IOSVulkanContext(); + ~IOSVulkanContext() { + delete g_Vulkan; + g_Vulkan = nullptr; + } + + bool InitAPI(); + + bool InitFromRenderThread(CAMetalLayer *layer, int desiredBackbufferSizeX, int desiredBackbufferSizeY); + void ShutdownFromRenderThread(); // Inverses InitFromRenderThread. + + void Shutdown(); + void Resize(); + + void *GetAPIContext() { return g_Vulkan; } + Draw::DrawContext *GetDrawContext() { return draw_; } + +private: + VulkanContext *g_Vulkan = nullptr; + Draw::DrawContext *draw_ = nullptr; + GraphicsContextState state_ = GraphicsContextState::PENDING; +}; + +IOSVulkanContext::IOSVulkanContext() {} + +bool IOSVulkanContext::InitFromRenderThread(CAMetalLayer *layer, int desiredBackbufferSizeX, int desiredBackbufferSizeY) { + INFO_LOG(G3D, "IOSVulkanContext::InitFromRenderThread: desiredwidth=%d desiredheight=%d", desiredBackbufferSizeX, desiredBackbufferSizeY); + if (!g_Vulkan) { + ERROR_LOG(G3D, "IOSVulkanContext::InitFromRenderThread: No Vulkan context"); + return false; + } + + VkResult res = g_Vulkan->InitSurface(WINDOWSYSTEM_METAL_EXT, (void *)layer, nullptr); + if (res != VK_SUCCESS) { + ERROR_LOG(G3D, "g_Vulkan->InitSurface failed: '%s'", VulkanResultToString(res)); + return false; + } + + bool success = true; + if (g_Vulkan->InitSwapchain()) { + bool useMultiThreading = g_Config.bRenderMultiThreading; + if (g_Config.iInflightFrames == 1) { + useMultiThreading = false; + } + draw_ = Draw::T3DCreateVulkanContext(g_Vulkan, useMultiThreading); + SetGPUBackend(GPUBackend::VULKAN); + success = draw_->CreatePresets(); // Doesn't fail, we ship the compiler. + _assert_msg_(success, "Failed to compile preset shaders"); + draw_->HandleEvent(Draw::Event::GOT_BACKBUFFER, g_Vulkan->GetBackbufferWidth(), g_Vulkan->GetBackbufferHeight()); + + VulkanRenderManager *renderManager = (VulkanRenderManager *)draw_->GetNativeObject(Draw::NativeObject::RENDER_MANAGER); + renderManager->SetInflightFrames(g_Config.iInflightFrames); + success = renderManager->HasBackbuffers(); + } else { + success = false; + } + + INFO_LOG(G3D, "IOSVulkanContext::Init completed, %s", success ? "successfully" : "but failed"); + if (!success) { + g_Vulkan->DestroySwapchain(); + g_Vulkan->DestroySurface(); + g_Vulkan->DestroyDevice(); + g_Vulkan->DestroyInstance(); + } + return success; +} + +void IOSVulkanContext::ShutdownFromRenderThread() { + INFO_LOG(G3D, "IOSVulkanContext::Shutdown"); + draw_->HandleEvent(Draw::Event::LOST_BACKBUFFER, g_Vulkan->GetBackbufferWidth(), g_Vulkan->GetBackbufferHeight()); + delete draw_; + draw_ = nullptr; + g_Vulkan->WaitUntilQueueIdle(); + g_Vulkan->PerformPendingDeletes(); + g_Vulkan->DestroySwapchain(); + g_Vulkan->DestroySurface(); + INFO_LOG(G3D, "Done with ShutdownFromRenderThread"); +} + +void IOSVulkanContext::Shutdown() { + INFO_LOG(G3D, "Calling NativeShutdownGraphics"); + g_Vulkan->DestroyDevice(); + g_Vulkan->DestroyInstance(); + // We keep the g_Vulkan context around to avoid invalidating a ton of pointers around the app. + finalize_glslang(); + INFO_LOG(G3D, "IOSVulkanContext::Shutdown completed"); +} + +void IOSVulkanContext::Resize() { + INFO_LOG(G3D, "IOSVulkanContext::Resize begin (oldsize: %dx%d)", g_Vulkan->GetBackbufferWidth(), g_Vulkan->GetBackbufferHeight()); + + draw_->HandleEvent(Draw::Event::LOST_BACKBUFFER, g_Vulkan->GetBackbufferWidth(), g_Vulkan->GetBackbufferHeight()); + g_Vulkan->DestroySwapchain(); + g_Vulkan->DestroySurface(); + + g_Vulkan->UpdateFlags(FlagsFromConfig()); + + g_Vulkan->ReinitSurface(); + g_Vulkan->InitSwapchain(); + draw_->HandleEvent(Draw::Event::GOT_BACKBUFFER, g_Vulkan->GetBackbufferWidth(), g_Vulkan->GetBackbufferHeight()); + INFO_LOG(G3D, "IOSVulkanContext::Resize end (final size: %dx%d)", g_Vulkan->GetBackbufferWidth(), g_Vulkan->GetBackbufferHeight()); +} + +bool IOSVulkanContext::InitAPI() { + INFO_LOG(G3D, "IOSVulkanContext::Init"); + init_glslang(); + + g_LogOptions.breakOnError = true; + g_LogOptions.breakOnWarning = true; + g_LogOptions.msgBoxOnError = false; + + INFO_LOG(G3D, "Creating Vulkan context"); + Version gitVer(PPSSPP_GIT_VERSION); + + std::string errorStr; + if (!VulkanLoad(&errorStr)) { + ERROR_LOG(G3D, "Failed to load Vulkan driver library: %s", errorStr.c_str()); + state_ = GraphicsContextState::FAILED_INIT; + return false; + } + + if (!g_Vulkan) { + // TODO: Assert if g_Vulkan already exists here? + g_Vulkan = new VulkanContext(); + } + + VulkanContext::CreateInfo info{}; + info.app_name = "PPSSPP"; + info.app_ver = gitVer.ToInteger(); + info.flags = FlagsFromConfig(); + if (!g_Vulkan->CreateInstanceAndDevice(info)) { + delete g_Vulkan; + g_Vulkan = nullptr; + state_ = GraphicsContextState::FAILED_INIT; + return false; + } + + g_Vulkan->SetCbGetDrawSize([]() { + return VkExtent2D {(uint32_t)g_display.pixel_xres, (uint32_t)g_display.pixel_yres}; + }); + + INFO_LOG(G3D, "Vulkan device created!"); + state_ = GraphicsContextState::INITIALIZED; + return true; +} + + +#pragma mark - +#pragma mark PPSSPPViewControllerMetal + +static std::atomic exitRenderLoop; +static std::atomic renderLoopRunning; +static std::thread g_renderLoopThread; + +@interface PPSSPPViewControllerMetal () { + ICadeTracker g_iCadeTracker; + TouchTracker g_touchTracker; + + IOSVulkanContext *graphicsContext; + LocationHelper *locationHelper; + CameraHelper *cameraHelper; +} + +@property (nonatomic) GCController *gameController __attribute__((weak_import)); + +@end // @interface + +@implementation PPSSPPViewControllerMetal + +- (id)init { + self = [super init]; + if (self) { + sharedViewController = self; + g_iCadeTracker.InitKeyMap(); + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appWillTerminate:) name:UIApplicationWillTerminateNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(controllerDidConnect:) name:GCControllerDidConnectNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(controllerDidDisconnect:) name:GCControllerDidDisconnectNotification object:nil]; + } + return self; +} + +// Should be very similar to the Android one, probably mergeable. +void VulkanRenderLoop(IOSVulkanContext *graphicsContext, CAMetalLayer *metalLayer) { + SetCurrentThreadName("EmuThreadVulkan"); + INFO_LOG(G3D, "Entering EmuThreadVulkan"); + + if (!graphicsContext) { + ERROR_LOG(G3D, "runVulkanRenderLoop: Tried to enter without a created graphics context."); + renderLoopRunning = false; + exitRenderLoop = false; + return; + } + + if (exitRenderLoop) { + WARN_LOG(G3D, "runVulkanRenderLoop: ExitRenderLoop requested at start, skipping the whole thing."); + renderLoopRunning = false; + exitRenderLoop = false; + return; + } + + // This is up here to prevent race conditions, in case we pause during init. + renderLoopRunning = true; + + int desiredBackbufferSizeX = g_display.pixel_xres; + int desiredBackbufferSizeY = g_display.pixel_yres; + + //WARN_LOG(G3D, "runVulkanRenderLoop. desiredBackbufferSizeX=%d desiredBackbufferSizeY=%d", + // desiredBackbufferSizeX, desiredBackbufferSizeY); + + if (!graphicsContext->InitFromRenderThread(metalLayer, desiredBackbufferSizeX, desiredBackbufferSizeY)) { + // On Android, if we get here, really no point in continuing. + // The UI is supposed to render on any device both on OpenGL and Vulkan. If either of those don't work + // on a device, we blacklist it. Hopefully we should have already failed in InitAPI anyway and reverted to GL back then. + ERROR_LOG(G3D, "Failed to initialize graphics context."); + System_Toast("Failed to initialize graphics context."); + + delete graphicsContext; + graphicsContext = nullptr; + renderLoopRunning = false; + return; + } + + if (!exitRenderLoop) { + if (!NativeInitGraphics(graphicsContext)) { + ERROR_LOG(G3D, "Failed to initialize graphics."); + // Gonna be in a weird state here.. + } + graphicsContext->ThreadStart(); + while (!exitRenderLoop) { + NativeFrame(graphicsContext); + } + INFO_LOG(G3D, "Leaving Vulkan main loop."); + } else { + INFO_LOG(G3D, "Not entering main loop."); + } + + NativeShutdownGraphics(); + + graphicsContext->ThreadEnd(); + + // Shut the graphics context down to the same state it was in when we entered the render thread. + INFO_LOG(G3D, "Shutting down graphics context..."); + graphicsContext->ShutdownFromRenderThread(); + renderLoopRunning = false; + exitRenderLoop = false; + + WARN_LOG(G3D, "Render loop function exited."); +} + +- (bool)runVulkanRenderLoop { + INFO_LOG(G3D, "runVulkanRenderLoop"); + + if (!graphicsContext) { + ERROR_LOG(G3D, "runVulkanRenderLoop: Tried to enter without a created graphics context."); + return false; + } + + if (g_renderLoopThread.joinable()) { + ERROR_LOG(G3D, "runVulkanRenderLoop: Already running"); + return false; + } + + CAMetalLayer *metalLayer = (CAMetalLayer *)self.view.layer; + g_renderLoopThread = std::thread(VulkanRenderLoop, graphicsContext, metalLayer); + return true; +} + +- (void)requestExitVulkanRenderLoop { + INFO_LOG(G3D, "requestExitVulkanRenderLoop"); + + if (!renderLoopRunning) { + ERROR_LOG(SYSTEM, "Render loop already exited"); + return; + } + _assert_(g_renderLoopThread.joinable()); + exitRenderLoop = true; + g_renderLoopThread.join(); + _assert_(!g_renderLoopThread.joinable()); +} + +// These two are forwarded from the appDelegate +- (void)didBecomeActive { + INFO_LOG(G3D, "didBecomeActive GL"); + + // Spin up the emu thread. It will in turn spin up the Vulkan render thread + // on its own. + [self runVulkanRenderLoop]; +} + +- (void)willResignActive { + INFO_LOG(G3D, "willResignActive GL"); + [self requestExitVulkanRenderLoop]; +} + +- (void)shutdown +{ + INFO_LOG(SYSTEM, "shutdown VK"); + + _dbg_assert_(sharedViewController != nil); + sharedViewController = nil; + + [[NSNotificationCenter defaultCenter] removeObserver:self]; + + self.gameController = nil; + + if (graphicsContext) { + graphicsContext->Shutdown(); + delete graphicsContext; + graphicsContext = NULL; + } +} + +- (void)dealloc +{ + INFO_LOG(SYSTEM, "dealloc VK"); +} + +- (void)loadView { + INFO_LOG(G3D, "Creating metal view"); + + CGRect screenRect = [[UIScreen mainScreen] bounds]; + CGFloat screenWidth = screenRect.size.width; + CGFloat screenHeight = screenRect.size.height; + + PPSSPPMetalView *metalView = [[PPSSPPMetalView alloc] initWithFrame:CGRectMake(0, 0, screenWidth,screenHeight)]; + self.view = metalView; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + [[DisplayManager shared] setupDisplayListener]; + + INFO_LOG(SYSTEM, "Metal viewDidLoad"); + + UIScreen* screen = [(AppDelegate*)[UIApplication sharedApplication].delegate screen]; + self.view.frame = [screen bounds]; + self.view.multipleTouchEnabled = YES; + graphicsContext = new IOSVulkanContext(); + + [[DisplayManager shared] updateResolution:[UIScreen mainScreen]]; + + if (!graphicsContext->InitAPI()) { + _assert_msg_(false, "Failed to init Vulkan"); + } + + if ([[GCController controllers] count] > 0) { + [self setupController:[[GCController controllers] firstObject]]; + } + + INFO_LOG(G3D, "Detected size: %dx%d", g_display.pixel_xres, g_display.pixel_yres); + + cameraHelper = [[CameraHelper alloc] init]; + [cameraHelper setDelegate:self]; + + locationHelper = [[LocationHelper alloc] init]; + [locationHelper setDelegate:self]; + + [self hideKeyboard]; + + UIScreenEdgePanGestureRecognizer *mBackGestureRecognizer = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(handleSwipeFrom:) ]; + [mBackGestureRecognizer setEdges:UIRectEdgeLeft]; + [[self view] addGestureRecognizer:mBackGestureRecognizer]; +} + +// Allow device rotation to resize the swapchain +-(void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator { + [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; + // TODO: Handle resizing properly. +} + +- (UIView *)getView { + return [self view]; +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + INFO_LOG(G3D, "viewWillAppear"); + self.view.contentScaleFactor = UIScreen.mainScreen.nativeScale; +} + +- (void)viewWillDisappear:(BOOL)animated { + [super viewWillDisappear:animated]; + INFO_LOG(G3D, "viewWillDisappear"); +} + +- (void)viewDidDisappear:(BOOL)animated { + [super viewDidDisappear: animated]; +} + +- (void)viewDidAppear:(BOOL)animated { + [super viewDidAppear:animated]; + INFO_LOG(G3D, "viewDidAppear"); + [self hideKeyboard]; + INFO_LOG(G3D, "viewDidAppearDone"); +} + +- (BOOL)prefersHomeIndicatorAutoHidden { + return YES; +} + +- (void)shareText:(NSString *)text { + NSArray *items = @[text]; + UIActivityViewController * viewController = [[UIActivityViewController alloc] initWithActivityItems:items applicationActivities:nil]; + dispatch_async(dispatch_get_main_queue(), ^{ + [self presentViewController:viewController animated:YES completion:nil]; + }); +} + +extern float g_safeInsetLeft; +extern float g_safeInsetRight; +extern float g_safeInsetTop; +extern float g_safeInsetBottom; + +- (void)viewSafeAreaInsetsDidChange { + if (@available(iOS 11.0, *)) { + [super viewSafeAreaInsetsDidChange]; + // we use 0.0f instead of safeAreaInsets.bottom because the bottom overlay isn't disturbing (for now) + g_safeInsetLeft = self.view.safeAreaInsets.left; + g_safeInsetRight = self.view.safeAreaInsets.right; + g_safeInsetTop = self.view.safeAreaInsets.top; + g_safeInsetBottom = 0.0f; + } +} + +// Enables tapping for edge area. +-(UIRectEdge)preferredScreenEdgesDeferringSystemGestures +{ + return UIRectEdgeAll; +} + +- (void)bindDefaultFBO +{ + // Do nothing +} + +- (NSUInteger)supportedInterfaceOrientations +{ + return UIInterfaceOrientationMaskLandscape; +} + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +{ + g_touchTracker.Began(touches, self.view); +} + +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event +{ + g_touchTracker.Moved(touches, self.view); +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event +{ + g_touchTracker.Ended(touches, self.view); +} + +- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event +{ + g_touchTracker.Cancelled(touches, self.view); +} + +- (void)buttonDown:(iCadeState)button +{ + g_iCadeTracker.ButtonDown(button); +} + +- (void)buttonUp:(iCadeState)button +{ + g_iCadeTracker.ButtonUp(button); +} + +- (void)controllerDidConnect:(NSNotification *)note +{ + if (![[GCController controllers] containsObject:self.gameController]) self.gameController = nil; + + if (self.gameController != nil) return; // already have a connected controller + + [self setupController:(GCController *)note.object]; +} + +- (void)controllerDidDisconnect:(NSNotification *)note +{ + if (self.gameController == note.object) { + self.gameController = nil; + + if ([[GCController controllers] count] > 0) { + [self setupController:[[GCController controllers] firstObject]]; + } + } +} + +- (void)setupController:(GCController *)controller +{ + self.gameController = controller; + if (!SetupController(controller)) { + self.gameController = nil; + } +} + +- (void)startVideo:(int)width height:(int)height { + [cameraHelper startVideo:width h:height]; +} + +- (void)stopVideo { + [cameraHelper stopVideo]; +} + +- (void)PushCameraImageIOS:(long long)len buffer:(unsigned char*)data { + Camera::pushCameraImage(len, data); +} + +- (void)startLocation { + [locationHelper startLocationUpdates]; +} + +- (void)stopLocation { + [locationHelper stopLocationUpdates]; +} + +- (void)SetGpsDataIOS:(CLLocation *)newLocation { + GPS::setGpsData((long long)newLocation.timestamp.timeIntervalSince1970, + newLocation.horizontalAccuracy/5.0, + newLocation.coordinate.latitude, newLocation.coordinate.longitude, + newLocation.altitude, + MAX(newLocation.speed * 3.6, 0.0), /* m/s to km/h */ + 0 /* bearing */); +} + +- (void)handleSwipeFrom:(UIScreenEdgePanGestureRecognizer *)recognizer +{ + if (recognizer.state == UIGestureRecognizerStateEnded) { + KeyInput key; + key.flags = KEY_DOWN | KEY_UP; + key.keyCode = NKCODE_BACK; + key.deviceId = DEVICE_ID_TOUCH; + NativeKey(key); + INFO_LOG(SYSTEM, "Detected back swipe"); + } +} +// The below is inspired by https://stackoverflow.com/questions/7253477/how-to-display-the-iphone-ipad-keyboard-over-a-full-screen-opengl-es-app +// It's a bit limited but good enough. + +-(void) deleteBackward { + KeyInput input{}; + input.deviceId = DEVICE_ID_KEYBOARD; + input.flags = KEY_DOWN | KEY_UP; + input.keyCode = NKCODE_DEL; + NativeKey(input); + INFO_LOG(SYSTEM, "Backspace"); +} + +-(BOOL) hasText +{ + return YES; +} + +-(void) insertText:(NSString *)text +{ + std::string str = std::string([text UTF8String]); + INFO_LOG(SYSTEM, "Chars: %s", str.c_str()); + UTF8 chars(str); + while (!chars.end()) { + uint32_t codePoint = chars.next(); + KeyInput input{}; + input.deviceId = DEVICE_ID_KEYBOARD; + input.flags = KEY_CHAR; + input.unicodeChar = codePoint; + NativeKey(input); + } +} + +-(BOOL) canBecomeFirstResponder +{ + return YES; +} + +-(void) showKeyboard { + dispatch_async(dispatch_get_main_queue(), ^{ + [self becomeFirstResponder]; + }); +} + +-(void) hideKeyboard { + dispatch_async(dispatch_get_main_queue(), ^{ + [self resignFirstResponder]; + }); +} + +@end + +@implementation PPSSPPMetalView + +/** Returns a Metal-compatible layer. */ ++(Class) layerClass { return [CAMetalLayer class]; } + +@end diff --git a/ios/main.mm b/ios/main.mm index bc0909e8f6..48690865b2 100644 --- a/ios/main.mm +++ b/ios/main.mm @@ -31,6 +31,7 @@ #include "Common/System/Request.h" #include "Common/StringUtils.h" #include "Common/Profiler/Profiler.h" +#include "Common/Thread/ThreadUtil.h" #include "Core/Config.h" #include "Common/Log.h" #include "UI/DarwinFileSystemServices.h" @@ -391,7 +392,14 @@ void System_Notify(SystemNotification notification) { bool System_MakeRequest(SystemRequestType type, int requestId, const std::string ¶m1, const std::string ¶m2, int64_t param3, int64_t param4) { switch (type) { + case SystemRequestType::RESTART_APP: + dispatch_async(dispatch_get_main_queue(), ^{ + [(AppDelegate *)[[UIApplication sharedApplication] delegate] restart:param1.c_str()]; + }); + break; + case SystemRequestType::EXIT_APP: + // NOTE: on iOS, this is considered a crash and not a valid way to exit. exit(0); // The below seems right, but causes hangs. See #12140. // dispatch_async(dispatch_get_main_queue(), ^{ @@ -543,6 +551,7 @@ void System_Vibrate(int mode) { int main(int argc, char *argv[]) { + // SetCurrentThreadName("MainThread"); version = [[[UIDevice currentDevice] systemVersion] UTF8String]; if (2 != sscanf(version.c_str(), "%d", &g_iosVersionMajor)) { // Just set it to 14.0 if the parsing fails for whatever reason.