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] 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