From 41f8670d2a80f0456435f461b49354e1294cd3d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 20 May 2024 23:13:58 +0200 Subject: [PATCH 01/10] Add MoltenVK.xcframework as a submodule --- .gitmodules | 3 +++ ios/MoltenVK | 1 + 2 files changed, 4 insertions(+) create mode 160000 ios/MoltenVK 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/ios/MoltenVK b/ios/MoltenVK new file mode 160000 index 0000000000..ad018e8664 --- /dev/null +++ b/ios/MoltenVK @@ -0,0 +1 @@ +Subproject commit ad018e8664d343dd1e6d7a6ce712c64a6f8fee2a From 47f12dfeecc63f42862d741a2d0920e73ab72754 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 21 May 2024 10:41:04 +0200 Subject: [PATCH 02/10] Make linking to MoltenVK work --- CMakeLists.txt | 9 ++++-- Common/GPU/Vulkan/VulkanLoader.cpp | 36 ++++++++++++++++++++--- Common/GPU/Vulkan/VulkanLoader.h | 8 ++++- Common/GPU/Vulkan/VulkanRenderManager.cpp | 2 ++ UI/MainScreen.cpp | 2 +- ios/ViewController.mm | 3 +- 6 files changed, 51 insertions(+), 9 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b1e38b10a5..cdd17ec7ec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1313,7 +1313,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 +2879,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/Vulkan/VulkanLoader.cpp b/Common/GPU/Vulkan/VulkanLoader.cpp index 17596b50a3..aba900b43e 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,13 @@ void VulkanSetAvailable(bool available) { } bool VulkanMayBeAvailable() { +#if PPSSPP_PLATFORM(IOS_APP_STORE) + g_vulkanAvailabilityChecked = true; + g_vulkanMayBeAvailable = true; + INFO_LOG(G3D, "iOS: VulkanMayBeAvailable(): Reporting Vulkan as available"); + return true; +#else + // Unsupported in VR at the moment if (IsVREnabled()) { return false; @@ -557,9 +572,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 +606,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 +678,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 +824,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..15fd7d5358 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,9 @@ #define VK_USE_PLATFORM_METAL_EXT #endif +#if !PPSSPP_PLATFORM(IOS_APP_STORE) #define VK_NO_PROTOTYPES +#endif #include "ext/vulkan/vulkan.h" #include @@ -40,6 +44,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 +245,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/MainScreen.cpp b/UI/MainScreen.cpp index 5ae17fc84b..b423d5832a 100644 --- a/UI/MainScreen.cpp +++ b/UI/MainScreen.cpp @@ -1289,7 +1289,7 @@ void MainScreen::CreateViews() { } rightColumnChoices->Add(new Spacer(25.0)); -#if !PPSSPP_PLATFORM(IOS_APP_STORE) +#if !PPSSPP_PLATFORM(IOS_APP_STORE) || defined(_DEBUG) // Officially, iOS apps should not have exit buttons. Remove it to maximize app store review chances. rightColumnChoices->Add(new Choice(mm->T("Exit")))->OnClick.Handle(this, &MainScreen::OnExit); #endif diff --git a/ios/ViewController.mm b/ios/ViewController.mm index e97d67406f..4ec2bf6e9b 100644 --- a/ios/ViewController.mm +++ b/ios/ViewController.mm @@ -216,7 +216,8 @@ extern float g_safeInsetBottom; [self hideKeyboard]; - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ + // Was previously DISPATCH_QUEUE_PRIORITY_HIGH. + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NativeInitGraphics(graphicsContext); INFO_LOG(SYSTEM, "Emulation thread starting\n"); From 946881dcf138b45d65c30bfc290a80b973df7217 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 21 May 2024 13:11:09 +0200 Subject: [PATCH 03/10] Minor cleanup (remove redundant stuff from the storyboard) --- ios/Launch Screen.storyboard | 29 ----------------------------- ios/README.md | 10 ++++++++-- 2 files changed, 8 insertions(+), 31 deletions(-) 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/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: -------------- From 09e2c78a332b5e0f5a8f2b257078353166e462fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Wed, 22 May 2024 13:56:53 +0200 Subject: [PATCH 04/10] Add ViewControllerMetal placeholders --- CMakeLists.txt | 2 ++ ios/ViewControllerMetal.h | 3 +++ ios/ViewControllerMetal.mm | 1 + 3 files changed, 6 insertions(+) create mode 100644 ios/ViewControllerMetal.h create mode 100644 ios/ViewControllerMetal.mm diff --git a/CMakeLists.txt b/CMakeLists.txt index cdd17ec7ec..a967799992 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1295,6 +1295,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 diff --git a/ios/ViewControllerMetal.h b/ios/ViewControllerMetal.h new file mode 100644 index 0000000000..58587b4175 --- /dev/null +++ b/ios/ViewControllerMetal.h @@ -0,0 +1,3 @@ +// ViewControllerMetal +// Used by both Vulkan/MoltenVK and the future Metal backend. + diff --git a/ios/ViewControllerMetal.mm b/ios/ViewControllerMetal.mm new file mode 100644 index 0000000000..d9958a4676 --- /dev/null +++ b/ios/ViewControllerMetal.mm @@ -0,0 +1 @@ +#include "ViewControllerMetal.h" \ No newline at end of file From 5f79046e01bb84e03c03eabf4c5d9be03016172a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Wed, 22 May 2024 15:47:04 +0200 Subject: [PATCH 05/10] Get Vulkan rendering on iOS (still, need to hook up input etc) --- CMakeLists.txt | 2 + Common/GPU/Vulkan/VulkanContext.cpp | 17 +- Common/GPU/Vulkan/VulkanLoader.h | 1 + ios/AppDelegate.mm | 23 +- ios/DisplayManager.mm | 17 +- ios/ViewController.h | 3 +- ios/ViewController.mm | 4 +- ios/ViewControllerCommon.h | 2 + ios/ViewControllerMetal.h | 15 + ios/ViewControllerMetal.mm | 515 +++++++++++++++++++++++++++- 10 files changed, 572 insertions(+), 27 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a967799992..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") 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.h b/Common/GPU/Vulkan/VulkanLoader.h index 15fd7d5358..a7ca178c16 100644 --- a/Common/GPU/Vulkan/VulkanLoader.h +++ b/Common/GPU/Vulkan/VulkanLoader.h @@ -33,6 +33,7 @@ #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" diff --git a/ios/AppDelegate.mm b/ios/AppDelegate.mm index fbcb84be8d..5ee599bb7b 100644 --- a/ios/AppDelegate.mm +++ b/ios/AppDelegate.mm @@ -1,5 +1,6 @@ #import "AppDelegate.h" #import "ViewController.h" +#import "ViewControllerMetal.h" #import "iOSCoreAudio.h" #import "Common/System/System.h" #import "Common/System/NativeApp.h" @@ -91,17 +92,25 @@ 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]; + + 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]; - self.window.rootViewController = vc; - [self.window makeKeyAndVisible]; - return YES; } 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/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 4ec2bf6e9b..8758ed92b7 100644 --- a/ios/ViewController.mm +++ b/ios/ViewController.mm @@ -93,15 +93,13 @@ static bool threadStopped = false; id sharedViewController; -// TODO: Reach these through sharedViewController -static CameraHelper *cameraHelper; - @interface PPSSPPViewControllerGL () { ICadeTracker g_iCadeTracker; TouchTracker g_touchTracker; GraphicsContext *graphicsContext; LocationHelper *locationHelper; + CameraHelper *cameraHelper; } @property (nonatomic, strong) EAGLContext* context; diff --git a/ios/ViewControllerCommon.h b/ios/ViewControllerCommon.h index 52766bff4d..2758cb10cb 100644 --- a/ios/ViewControllerCommon.h +++ b/ios/ViewControllerCommon.h @@ -17,3 +17,5 @@ - (void)stopVideo; @end + +extern id sharedViewController; diff --git a/ios/ViewControllerMetal.h b/ios/ViewControllerMetal.h index 58587b4175..bb236e4195 100644 --- a/ios/ViewControllerMetal.h +++ b/ios/ViewControllerMetal.h @@ -1,3 +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 index d9958a4676..bc7113ae78 100644 --- a/ios/ViewControllerMetal.mm +++ b/ios/ViewControllerMetal.mm @@ -1 +1,514 @@ -#include "ViewControllerMetal.h" \ No newline at end of file +#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" + +// 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()); +} + +extern const char *PPSSPP_GIT_VERSION; + +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(); + VkResult res = g_Vulkan->CreateInstance(info); + if (res != VK_SUCCESS) { + ERROR_LOG(G3D, "Failed to create vulkan context: %s", g_Vulkan->InitError().c_str()); + VulkanSetAvailable(false); + delete g_Vulkan; + g_Vulkan = nullptr; + state_ = GraphicsContextState::FAILED_INIT; + return false; + } + + int physicalDevice = g_Vulkan->GetBestPhysicalDevice(); + if (physicalDevice < 0) { + ERROR_LOG(G3D, "No usable Vulkan device found."); + g_Vulkan->DestroyInstance(); + delete g_Vulkan; + g_Vulkan = nullptr; + state_ = GraphicsContextState::FAILED_INIT; + return false; + } + + g_Vulkan->ChooseDevice(physicalDevice); + + INFO_LOG(G3D, "Creating Vulkan device (flags: %08x)", info.flags); + if (g_Vulkan->CreateDevice() != VK_SUCCESS) { + INFO_LOG(G3D, "Failed to create vulkan device: %s", g_Vulkan->InitError().c_str()); + System_Toast("No Vulkan driver found. Using OpenGL instead."); + g_Vulkan->DestroyInstance(); + 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 bool renderer_inited = false; +static std::mutex renderLock; + +@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]; + + 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]; + } + } + return self; +} + +// Should be very similar to the Android one, probably mergeable. +void VulkanRenderLoop(IOSVulkanContext *graphicsContext, CAMetalLayer *metalLayer, int desiredBackbufferSizeX, int desiredBackbufferSizeY) { + SetCurrentThreadName("EmuThread"); + + 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; + + //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(); + renderer_inited = true; + + while (!exitRenderLoop) { + { + std::lock_guard renderGuard(renderLock); + NativeFrame(graphicsContext); + } + // Here Android processes frame commands. + } + INFO_LOG(G3D, "Leaving Vulkan main loop."); + } else { + INFO_LOG(G3D, "Not entering main loop."); + } + + NativeShutdownGraphics(); + + renderer_inited = false; + 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."); +} + +- (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"); + } + + int desiredBackbufferSizeX = g_display.pixel_xres; + int desiredBackbufferSizeY = g_display.pixel_yres; + + INFO_LOG(G3D, "Detected size: %dx%d", desiredBackbufferSizeX, desiredBackbufferSizeY); + CAMetalLayer *layer = (CAMetalLayer *)self.view.layer; + + [self hideKeyboard]; + + // Spin up the emu thread. It will in turn spin up the Vulkan render thread + // on its own. + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + VulkanRenderLoop(graphicsContext, layer, desiredBackbufferSizeX, desiredBackbufferSizeY); + }); +} + +// Allow device rotation to resize the swapchain +-(void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator { + [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; + // TODO: Handle resizing properly. + // demo_resize(&demo); +} + +- (UIView *)getView { + return [self view]; +} + +/** Since this is a single-view app, initialize Vulkan as view is appearing. */ +- (void)viewWillAppear:(BOOL) animated { + [super viewWillAppear: animated]; + + self.view.contentScaleFactor = UIScreen.mainScreen.nativeScale; + + uint32_t fps = 60; + /* + _displayLink = [CADisplayLink displayLinkWithTarget: self selector: @selector(renderLoop)]; + [_displayLink setFrameInterval: 60 / fps]; + [_displayLink addToRunLoop: NSRunLoop.currentRunLoop forMode: NSDefaultRunLoopMode]; + */ +} + +/* +-(void) renderLoop { + demo_draw(&demo); +} +*/ + +- (void)viewDidDisappear: (BOOL) animated { + // [_displayLink invalidate]; + // [_displayLink release]; + // demo_cleanup(&demo); + [super viewDidDisappear: animated]; +} + + +- (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; + } +} + +- (void)bindDefaultFBO +{ + // Do nothing +} + +- (void)buttonDown:(iCadeState)button +{ + g_iCadeTracker.ButtonDown(button); +} + +- (void)buttonUp:(iCadeState)button +{ + g_iCadeTracker.ButtonUp(button); +} + +// 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 From b66f7ea9090bb19a683c2a989c503f1e665e2ff0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Wed, 22 May 2024 21:36:55 +0200 Subject: [PATCH 06/10] Vulkan on iOS: Hook up remaining functionality --- ios/ViewController.mm | 34 +++------ ios/ViewControllerCommon.h | 3 + ios/ViewControllerMetal.mm | 149 ++++++++++++++++++++++++++++++------- 3 files changed, 136 insertions(+), 50 deletions(-) diff --git a/ios/ViewController.mm b/ios/ViewController.mm index 8758ed92b7..cec6e3d434 100644 --- a/ios/ViewController.mm +++ b/ios/ViewController.mm @@ -39,13 +39,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() { @@ -118,12 +111,8 @@ id sharedViewController; 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; } @@ -174,10 +163,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; @@ -200,10 +185,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]; @@ -214,6 +197,10 @@ extern float g_safeInsetBottom; [self hideKeyboard]; + UIScreenEdgePanGestureRecognizer *mBackGestureRecognizer = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(handleSwipeFrom:) ]; + [mBackGestureRecognizer setEdges:UIRectEdgeLeft]; + [[self view] addGestureRecognizer:mBackGestureRecognizer]; + // Was previously DISPATCH_QUEUE_PRIORITY_HIGH. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NativeInitGraphics(graphicsContext); @@ -279,9 +266,7 @@ extern float g_safeInsetBottom; [[NSNotificationCenter defaultCenter] removeObserver:self]; - if ([GCController class]) { - self.gameController = nil; - } + self.gameController = nil; if (graphicsContext) { graphicsContext->Shutdown(); @@ -294,6 +279,7 @@ extern float g_safeInsetBottom; - (void)dealloc { + INFO_LOG(SYSTEM, "dealloc"); [self shutdown]; } diff --git a/ios/ViewControllerCommon.h b/ios/ViewControllerCommon.h index 2758cb10cb..f4e9159e22 100644 --- a/ios/ViewControllerCommon.h +++ b/ios/ViewControllerCommon.h @@ -19,3 +19,6 @@ @end extern id sharedViewController; + +#define IS_IPAD() ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) +#define IS_IPHONE() ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone) diff --git a/ios/ViewControllerMetal.mm b/ios/ViewControllerMetal.mm index bc7113ae78..32061b89eb 100644 --- a/ios/ViewControllerMetal.mm +++ b/ios/ViewControllerMetal.mm @@ -23,6 +23,8 @@ #include "Core/Config.h" #include "Core/ConfigValues.h" #include "Core/System.h" +#include "Core/HLE/sceUsbCam.h" +#include "Core/HLE/sceUsbGps.h" // TODO: Share this between backends. static uint32_t FlagsFromConfig() { @@ -249,12 +251,8 @@ static std::mutex renderLock; 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; } @@ -361,11 +359,25 @@ void VulkanRenderLoop(IOSVulkanContext *graphicsContext, CAMetalLayer *metalLaye int desiredBackbufferSizeX = g_display.pixel_xres; int desiredBackbufferSizeY = g_display.pixel_yres; + if ([[GCController controllers] count] > 0) { + [self setupController:[[GCController controllers] firstObject]]; + } + INFO_LOG(G3D, "Detected size: %dx%d", desiredBackbufferSizeX, desiredBackbufferSizeY); CAMetalLayer *layer = (CAMetalLayer *)self.view.layer; + 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]; + // Spin up the emu thread. It will in turn spin up the Vulkan render thread // on its own. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ @@ -384,33 +396,19 @@ void VulkanRenderLoop(IOSVulkanContext *graphicsContext, CAMetalLayer *metalLaye return [self view]; } -/** Since this is a single-view app, initialize Vulkan as view is appearing. */ -- (void)viewWillAppear:(BOOL) animated { +- (void)viewWillAppear:(BOOL)animated { [super viewWillAppear: animated]; - self.view.contentScaleFactor = UIScreen.mainScreen.nativeScale; - - uint32_t fps = 60; - /* - _displayLink = [CADisplayLink displayLinkWithTarget: self selector: @selector(renderLoop)]; - [_displayLink setFrameInterval: 60 / fps]; - [_displayLink addToRunLoop: NSRunLoop.currentRunLoop forMode: NSDefaultRunLoopMode]; - */ } -/* --(void) renderLoop { - demo_draw(&demo); -} -*/ - -- (void)viewDidDisappear: (BOOL) animated { - // [_displayLink invalidate]; - // [_displayLink release]; - // demo_cleanup(&demo); +- (void)viewDidDisappear:(BOOL)animated { [super viewDidDisappear: animated]; } +- (void)viewDidAppear:(BOOL)animated { + [super viewDidAppear:animated]; + [self hideKeyboard]; +} - (BOOL)prefersHomeIndicatorAutoHidden { return YES; @@ -440,11 +438,42 @@ extern float g_safeInsetBottom; } } +// 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); @@ -455,6 +484,74 @@ extern float g_safeInsetBottom; 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. From aa3256f4e8528a0c52125ccb0988bab0de8422a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 23 May 2024 12:52:16 +0200 Subject: [PATCH 07/10] Turn off the Vulkan thread when switching away from the app. --- ios/AppDelegate.mm | 6 +++ ios/ViewController.mm | 8 ++++ ios/ViewControllerCommon.h | 4 ++ ios/ViewControllerMetal.mm | 95 ++++++++++++++++++++++---------------- ios/main.mm | 1 + 5 files changed, 75 insertions(+), 39 deletions(-) diff --git a/ios/AppDelegate.mm b/ios/AppDelegate.mm index 5ee599bb7b..f98f87865d 100644 --- a/ios/AppDelegate.mm +++ b/ios/AppDelegate.mm @@ -1,4 +1,5 @@ #import "AppDelegate.h" +#import "ViewControllerCommon.h" #import "ViewController.h" #import "ViewControllerMetal.h" #import "iOSCoreAudio.h" @@ -119,6 +120,9 @@ } -(void) applicationWillResignActive:(UIApplication *)application { + INFO_LOG(G3D, "willResignActive"); + + [self.viewController willResignActive]; if (g_Config.bEnableSound) { iOSCoreAudioShutdown(); } @@ -127,11 +131,13 @@ } -(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/ViewController.mm b/ios/ViewController.mm index cec6e3d434..8ccecd1bff 100644 --- a/ios/ViewController.mm +++ b/ios/ViewController.mm @@ -238,6 +238,14 @@ extern float g_safeInsetBottom; [self shutdown]; } +- (void)willResignActive { + +} + +- (void)didBecomeActive { + +} + - (void)shutdown { if (sharedViewController == nil) { diff --git a/ios/ViewControllerCommon.h b/ios/ViewControllerCommon.h index f4e9159e22..cfbe5e7daf 100644 --- a/ios/ViewControllerCommon.h +++ b/ios/ViewControllerCommon.h @@ -16,6 +16,10 @@ - (void)startVideo:(int)width height:(int)height; - (void)stopVideo; +// Forwarded from the AppDelegate +- (void)didBecomeActive; +- (void)willResignActive; + @end extern id sharedViewController; diff --git a/ios/ViewControllerMetal.mm b/ios/ViewControllerMetal.mm index 32061b89eb..082487e573 100644 --- a/ios/ViewControllerMetal.mm +++ b/ios/ViewControllerMetal.mm @@ -26,6 +26,9 @@ #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; @@ -178,33 +181,7 @@ bool IOSVulkanContext::InitAPI() { info.app_name = "PPSSPP"; info.app_ver = gitVer.ToInteger(); info.flags = FlagsFromConfig(); - VkResult res = g_Vulkan->CreateInstance(info); - if (res != VK_SUCCESS) { - ERROR_LOG(G3D, "Failed to create vulkan context: %s", g_Vulkan->InitError().c_str()); - VulkanSetAvailable(false); - delete g_Vulkan; - g_Vulkan = nullptr; - state_ = GraphicsContextState::FAILED_INIT; - return false; - } - - int physicalDevice = g_Vulkan->GetBestPhysicalDevice(); - if (physicalDevice < 0) { - ERROR_LOG(G3D, "No usable Vulkan device found."); - g_Vulkan->DestroyInstance(); - delete g_Vulkan; - g_Vulkan = nullptr; - state_ = GraphicsContextState::FAILED_INIT; - return false; - } - - g_Vulkan->ChooseDevice(physicalDevice); - - INFO_LOG(G3D, "Creating Vulkan device (flags: %08x)", info.flags); - if (g_Vulkan->CreateDevice() != VK_SUCCESS) { - INFO_LOG(G3D, "Failed to create vulkan device: %s", g_Vulkan->InitError().c_str()); - System_Toast("No Vulkan driver found. Using OpenGL instead."); - g_Vulkan->DestroyInstance(); + if (!g_Vulkan->CreateInstanceAndDevice(info)) { delete g_Vulkan; g_Vulkan = nullptr; state_ = GraphicsContextState::FAILED_INIT; @@ -228,6 +205,7 @@ static std::atomic exitRenderLoop; static std::atomic renderLoopRunning; static bool renderer_inited = false; static std::mutex renderLock; +static std::thread g_vulkanRenderLoopThread; @interface PPSSPPViewControllerMetal () { ICadeTracker g_iCadeTracker; @@ -258,7 +236,7 @@ static std::mutex renderLock; } // Should be very similar to the Android one, probably mergeable. -void VulkanRenderLoop(IOSVulkanContext *graphicsContext, CAMetalLayer *metalLayer, int desiredBackbufferSizeX, int desiredBackbufferSizeY) { +void VulkanRenderLoop(IOSVulkanContext *graphicsContext, CAMetalLayer *metalLayer) { SetCurrentThreadName("EmuThread"); if (!graphicsContext) { @@ -278,6 +256,9 @@ void VulkanRenderLoop(IOSVulkanContext *graphicsContext, CAMetalLayer *metalLaye // 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); @@ -328,6 +309,45 @@ void VulkanRenderLoop(IOSVulkanContext *graphicsContext, CAMetalLayer *metalLaye WARN_LOG(G3D, "Render loop function exited."); } +- (bool)runVulkanRenderLoop { + if (!graphicsContext) { + ERROR_LOG(G3D, "runVulkanRenderLoop: Tried to enter without a created graphics context."); + return false; + } + + if (g_vulkanRenderLoopThread.joinable()) { + ERROR_LOG(G3D, "runVulkanRenderLoop: Already running"); + return false; + } + + CAMetalLayer *metalLayer = (CAMetalLayer *)self.view.layer; + g_vulkanRenderLoopThread = std::thread(VulkanRenderLoop, graphicsContext, metalLayer); + return true; +} + +- (void)requestExitVulkanRenderLoop { + if (!renderLoopRunning) { + ERROR_LOG(SYSTEM, "Render loop already exited"); + return; + } + _assert_(g_vulkanRenderLoopThread.joinable()); + exitRenderLoop = true; + g_vulkanRenderLoopThread.join(); + _assert_(!g_vulkanRenderLoopThread.joinable()); + g_vulkanRenderLoopThread = std::thread(); +} + +// These two are forwarded from the appDelegate +- (void)didBecomeActive { + // Spin up the emu thread. It will in turn spin up the Vulkan render thread + // on its own. + [self runVulkanRenderLoop]; +} + +- (void)willResignActive { + [self requestExitVulkanRenderLoop]; +} + - (void)loadView { INFO_LOG(G3D, "Creating metal view"); @@ -356,15 +376,11 @@ void VulkanRenderLoop(IOSVulkanContext *graphicsContext, CAMetalLayer *metalLaye _assert_msg_(false, "Failed to init Vulkan"); } - int desiredBackbufferSizeX = g_display.pixel_xres; - int desiredBackbufferSizeY = g_display.pixel_yres; - if ([[GCController controllers] count] > 0) { [self setupController:[[GCController controllers] firstObject]]; } - INFO_LOG(G3D, "Detected size: %dx%d", desiredBackbufferSizeX, desiredBackbufferSizeY); - CAMetalLayer *layer = (CAMetalLayer *)self.view.layer; + INFO_LOG(G3D, "Detected size: %dx%d", g_display.pixel_xres, g_display.pixel_yres); cameraHelper = [[CameraHelper alloc] init]; [cameraHelper setDelegate:self]; @@ -378,11 +394,6 @@ void VulkanRenderLoop(IOSVulkanContext *graphicsContext, CAMetalLayer *metalLaye [mBackGestureRecognizer setEdges:UIRectEdgeLeft]; [[self view] addGestureRecognizer:mBackGestureRecognizer]; - // Spin up the emu thread. It will in turn spin up the Vulkan render thread - // on its own. - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - VulkanRenderLoop(graphicsContext, layer, desiredBackbufferSizeX, desiredBackbufferSizeY); - }); } // Allow device rotation to resize the swapchain @@ -397,10 +408,16 @@ void VulkanRenderLoop(IOSVulkanContext *graphicsContext, CAMetalLayer *metalLaye } - (void)viewWillAppear:(BOOL)animated { - [super viewWillAppear: 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]; } diff --git a/ios/main.mm b/ios/main.mm index bc0909e8f6..7e8be2cdaa 100644 --- a/ios/main.mm +++ b/ios/main.mm @@ -392,6 +392,7 @@ 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::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(), ^{ From 740219a65be07d40c07f569d5f03c7b97582a218 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 23 May 2024 14:16:07 +0200 Subject: [PATCH 08/10] Plumb the restart event into AppDelegate --- Common/GPU/Vulkan/VulkanLoader.cpp | 1 - ios/AppDelegate.h | 3 +++ ios/AppDelegate.mm | 17 +++++++++++++---- ios/main.mm | 6 ++++++ 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/Common/GPU/Vulkan/VulkanLoader.cpp b/Common/GPU/Vulkan/VulkanLoader.cpp index aba900b43e..ab0f6633a9 100644 --- a/Common/GPU/Vulkan/VulkanLoader.cpp +++ b/Common/GPU/Vulkan/VulkanLoader.cpp @@ -382,7 +382,6 @@ bool VulkanMayBeAvailable() { #if PPSSPP_PLATFORM(IOS_APP_STORE) g_vulkanAvailabilityChecked = true; g_vulkanMayBeAvailable = true; - INFO_LOG(G3D, "iOS: VulkanMayBeAvailable(): Reporting Vulkan as available"); return true; #else diff --git a/ios/AppDelegate.h b/ios/AppDelegate.h index bd5bf00795..31cca2a660 100644 --- a/ios/AppDelegate.h +++ b/ios/AppDelegate.h @@ -11,4 +11,7 @@ @property (strong, nonatomic) id viewController; +- (void)restart; +- (BOOL)launchPPSSPP:(int)argc argv:(char**)argv; + @end diff --git a/ios/AppDelegate.mm b/ios/AppDelegate.mm index f98f87865d..89d8a6754c 100644 --- a/ios/AppDelegate.mm +++ b/ios/AppDelegate.mm @@ -76,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; @@ -87,6 +86,11 @@ argv[argc++] = (char*)string; } + 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); @@ -115,10 +119,15 @@ return YES; } --(void) dealloc { +- (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } +- (void)restart { + // App was requested to restart, probably. + INFO_LOG(G3D, "Restart requested"); +} + -(void) applicationWillResignActive:(UIApplication *)application { INFO_LOG(G3D, "willResignActive"); diff --git a/ios/main.mm b/ios/main.mm index 7e8be2cdaa..ff55fed448 100644 --- a/ios/main.mm +++ b/ios/main.mm @@ -391,6 +391,12 @@ 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]; + }); + break; + case SystemRequestType::EXIT_APP: // NOTE: on iOS, this is considered a crash and not a valid way to exit. exit(0); From 9a93a478d1c2405f48926af8a3486d915a2768f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 23 May 2024 14:37:12 +0200 Subject: [PATCH 09/10] Fix smooth switching from Vulkan to GL --- UI/NativeApp.cpp | 2 +- ios/AppDelegate.h | 2 +- ios/AppDelegate.mm | 20 +++++++++++++++----- ios/main.mm | 2 +- 4 files changed, 18 insertions(+), 8 deletions(-) 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/ios/AppDelegate.h b/ios/AppDelegate.h index 31cca2a660..06ab74d32a 100644 --- a/ios/AppDelegate.h +++ b/ios/AppDelegate.h @@ -11,7 +11,7 @@ @property (strong, nonatomic) id viewController; -- (void)restart; +- (void)restart:(const char *)args; - (BOOL)launchPPSSPP:(int)argc argv:(char**)argv; @end diff --git a/ios/AppDelegate.mm b/ios/AppDelegate.mm index 89d8a6754c..d1b78c7344 100644 --- a/ios/AppDelegate.mm +++ b/ios/AppDelegate.mm @@ -86,6 +86,9 @@ 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]; } @@ -113,9 +116,6 @@ [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; } @@ -123,9 +123,19 @@ [[NSNotificationCenter defaultCenter] removeObserver:self]; } -- (void)restart { +- (void)restart:(const char*)restartArgs { + INFO_LOG(G3D, "Restart requested: %s", restartArgs); + [self.viewController willResignActive]; + self.viewController = nil; + // App was requested to restart, probably. - INFO_LOG(G3D, "Restart requested"); + 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]; } -(void) applicationWillResignActive:(UIApplication *)application { diff --git a/ios/main.mm b/ios/main.mm index ff55fed448..8af1a372b0 100644 --- a/ios/main.mm +++ b/ios/main.mm @@ -393,7 +393,7 @@ 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]; + [(AppDelegate *)[[UIApplication sharedApplication] delegate] restart:param1.c_str()]; }); break; From f864f52b641bef9a11e3d7be4b4f42b26dcc7727 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Fri, 24 May 2024 11:36:20 +0200 Subject: [PATCH 10/10] More app lifecycle work. Switching between Vulkan and OpenGL now works. --- Common/GPU/OpenGL/GLRenderManager.cpp | 7 +- UI/MainScreen.cpp | 2 +- android/jni/app-android.cpp | 15 +-- ios/AppDelegate.mm | 8 +- ios/ViewController.mm | 129 ++++++++++++++++---------- ios/ViewControllerMetal.mm | 63 ++++++++----- ios/main.mm | 2 + 7 files changed, 142 insertions(+), 84 deletions(-) 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/UI/MainScreen.cpp b/UI/MainScreen.cpp index b423d5832a..5ae17fc84b 100644 --- a/UI/MainScreen.cpp +++ b/UI/MainScreen.cpp @@ -1289,7 +1289,7 @@ void MainScreen::CreateViews() { } rightColumnChoices->Add(new Spacer(25.0)); -#if !PPSSPP_PLATFORM(IOS_APP_STORE) || defined(_DEBUG) +#if !PPSSPP_PLATFORM(IOS_APP_STORE) // Officially, iOS apps should not have exit buttons. Remove it to maximize app store review chances. rightColumnChoices->Add(new Choice(mm->T("Exit")))->OnClick.Handle(this, &MainScreen::OnExit); #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.mm b/ios/AppDelegate.mm index d1b78c7344..b0c478166e 100644 --- a/ios/AppDelegate.mm +++ b/ios/AppDelegate.mm @@ -126,6 +126,8 @@ - (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. @@ -136,12 +138,14 @@ // 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 { +- (void)applicationWillResignActive:(UIApplication *)application { INFO_LOG(G3D, "willResignActive"); [self.viewController willResignActive]; + if (g_Config.bEnableSound) { iOSCoreAudioShutdown(); } @@ -149,7 +153,7 @@ System_PostUIMessage(UIMessage::LOST_FOCUS); } --(void) applicationDidBecomeActive:(UIApplication *)application { +- (void)applicationDidBecomeActive:(UIApplication *)application { INFO_LOG(G3D, "didBecomeActive"); if (g_Config.bEnableSound) { iOSCoreAudioInit(); diff --git a/ios/ViewController.mm b/ios/ViewController.mm index 8ccecd1bff..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" @@ -81,8 +82,9 @@ 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; @@ -90,7 +92,7 @@ id sharedViewController; ICadeTracker g_iCadeTracker; TouchTracker g_touchTracker; - GraphicsContext *graphicsContext; + IOSGLESContext *graphicsContext; LocationHelper *locationHelper; CameraHelper *cameraHelper; } @@ -145,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]; @@ -201,24 +256,7 @@ extern float g_safeInsetBottom; [mBackGestureRecognizer setEdges:UIRectEdgeLeft]; [[self view] addGestureRecognizer:mBackGestureRecognizer]; - // Was previously DISPATCH_QUEUE_PRIORITY_HIGH. - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - NativeInitGraphics(graphicsContext); - - 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 @@ -238,31 +276,24 @@ extern float g_safeInsetBottom; [self shutdown]; } -- (void)willResignActive { - +- (void)didBecomeActive { + INFO_LOG(SYSTEM, "didBecomeActive begin"); + [self runGLRenderLoop]; + INFO_LOG(SYSTEM, "didBecomeActive end"); } -- (void)didBecomeActive { - +- (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) { @@ -276,19 +307,15 @@ extern float g_safeInsetBottom; self.gameController = nil; - if (graphicsContext) { - graphicsContext->Shutdown(); - delete graphicsContext; - graphicsContext = NULL; - } - - NativeShutdown(); + graphicsContext->StopThread(); + graphicsContext->Shutdown(); + delete graphicsContext; + graphicsContext = nullptr; } - (void)dealloc { - INFO_LOG(SYSTEM, "dealloc"); - [self shutdown]; + INFO_LOG(SYSTEM, "dealloc GL"); } - (NSUInteger)supportedInterfaceOrientations @@ -298,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/ViewControllerMetal.mm b/ios/ViewControllerMetal.mm index 082487e573..1fd177a8f3 100644 --- a/ios/ViewControllerMetal.mm +++ b/ios/ViewControllerMetal.mm @@ -152,8 +152,6 @@ void IOSVulkanContext::Resize() { INFO_LOG(G3D, "IOSVulkanContext::Resize end (final size: %dx%d)", g_Vulkan->GetBackbufferWidth(), g_Vulkan->GetBackbufferHeight()); } -extern const char *PPSSPP_GIT_VERSION; - bool IOSVulkanContext::InitAPI() { INFO_LOG(G3D, "IOSVulkanContext::Init"); init_glslang(); @@ -203,9 +201,7 @@ bool IOSVulkanContext::InitAPI() { static std::atomic exitRenderLoop; static std::atomic renderLoopRunning; -static bool renderer_inited = false; -static std::mutex renderLock; -static std::thread g_vulkanRenderLoopThread; +static std::thread g_renderLoopThread; @interface PPSSPPViewControllerMetal () { ICadeTracker g_iCadeTracker; @@ -237,7 +233,8 @@ static std::thread g_vulkanRenderLoopThread; // Should be very similar to the Android one, probably mergeable. void VulkanRenderLoop(IOSVulkanContext *graphicsContext, CAMetalLayer *metalLayer) { - SetCurrentThreadName("EmuThread"); + SetCurrentThreadName("EmuThreadVulkan"); + INFO_LOG(G3D, "Entering EmuThreadVulkan"); if (!graphicsContext) { ERROR_LOG(G3D, "runVulkanRenderLoop: Tried to enter without a created graphics context."); @@ -281,14 +278,8 @@ void VulkanRenderLoop(IOSVulkanContext *graphicsContext, CAMetalLayer *metalLaye // Gonna be in a weird state here.. } graphicsContext->ThreadStart(); - renderer_inited = true; - while (!exitRenderLoop) { - { - std::lock_guard renderGuard(renderLock); - NativeFrame(graphicsContext); - } - // Here Android processes frame commands. + NativeFrame(graphicsContext); } INFO_LOG(G3D, "Leaving Vulkan main loop."); } else { @@ -297,7 +288,6 @@ void VulkanRenderLoop(IOSVulkanContext *graphicsContext, CAMetalLayer *metalLaye NativeShutdownGraphics(); - renderer_inited = false; graphicsContext->ThreadEnd(); // Shut the graphics context down to the same state it was in when we entered the render thread. @@ -310,44 +300,73 @@ void VulkanRenderLoop(IOSVulkanContext *graphicsContext, CAMetalLayer *metalLaye } - (bool)runVulkanRenderLoop { + INFO_LOG(G3D, "runVulkanRenderLoop"); + if (!graphicsContext) { ERROR_LOG(G3D, "runVulkanRenderLoop: Tried to enter without a created graphics context."); return false; } - if (g_vulkanRenderLoopThread.joinable()) { + if (g_renderLoopThread.joinable()) { ERROR_LOG(G3D, "runVulkanRenderLoop: Already running"); return false; } CAMetalLayer *metalLayer = (CAMetalLayer *)self.view.layer; - g_vulkanRenderLoopThread = std::thread(VulkanRenderLoop, graphicsContext, metalLayer); + 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_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()); } // 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"); @@ -393,14 +412,12 @@ void VulkanRenderLoop(IOSVulkanContext *graphicsContext, CAMetalLayer *metalLaye 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. - // demo_resize(&demo); } - (UIView *)getView { @@ -424,7 +441,9 @@ void VulkanRenderLoop(IOSVulkanContext *graphicsContext, CAMetalLayer *metalLaye - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; + INFO_LOG(G3D, "viewDidAppear"); [self hideKeyboard]; + INFO_LOG(G3D, "viewDidAppearDone"); } - (BOOL)prefersHomeIndicatorAutoHidden { diff --git a/ios/main.mm b/ios/main.mm index 8af1a372b0..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" @@ -550,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.