Files
ppsspp/ios/ViewControllerMetal.mm
T

419 lines
12 KiB
Plaintext

#import "AppDelegate.h"
#import "ViewControllerMetal.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/System/Display.h"
#include "Common/System/System.h"
#include "Common/System/OSD.h"
#include "Common/System/NativeApp.h"
#include "Common/System/Request.h"
#include "Common/GraphicsContext.h"
#include "Common/Thread/ThreadUtil.h"
#include "Common/StringUtils.h"
#include "Core/Config.h"
#include "Core/ConfigValues.h"
#include "Core/System.h"
#include "GPU/Vulkan/VulkanUtil.h"
// ViewController lifecycle:
// https://www.progressconcepts.com/blog/ios-appdelegate-viewcontroller-method-order/
enum class GraphicsContextState {
PENDING,
INITIALIZED,
FAILED_INIT,
SHUTDOWN,
};
class IOSVulkanContext : public GraphicsContext {
public:
IOSVulkanContext() {}
~IOSVulkanContext() {
delete vulkan_;
vulkan_ = nullptr;
}
bool InitAPI(void *wnd, std::string *deviceName, std::string *errorMessage) override;
void ShutdownAPI() override;
bool InitSurface(WindowSystem winsys, void *data1, void *data2, std::string *error_message) override;
void ShutdownSurface() override;
void Resize() override {}
void *GetAPIContext() override { return vulkan_; }
Draw::DrawContext *GetDrawContext() override { return draw_; }
private:
VulkanContext *vulkan_ = nullptr;
Draw::DrawContext *draw_ = nullptr;
GraphicsContextState state_ = GraphicsContextState::PENDING;
};
bool IOSVulkanContext::InitAPI(void *wnd, std::string *deviceName, std::string *errorMessage) {
INFO_LOG(Log::G3D, "IOSVulkanContext::Init");
init_glslang();
g_LogOptions.breakOnError = true;
g_LogOptions.breakOnWarning = true;
g_LogOptions.msgBoxOnError = false;
INFO_LOG(Log::G3D, "Creating Vulkan context");
Version gitVer(PPSSPP_GIT_VERSION);
if (!VulkanLoad(errorMessage)) {
ERROR_LOG(Log::G3D, "Failed to load Vulkan driver library: %s", errorMessage->c_str());
state_ = GraphicsContextState::FAILED_INIT;
return false;
}
if (!vulkan_) {
// TODO: Assert if g_Vulkan already exists here?
vulkan_ = new VulkanContext();
}
VulkanContext::CreateInfo info{};
InitVulkanCreateInfoFromConfig(&info);
if (!vulkan_->CreateInstanceAndDevice(info, &g_Config.sVulkanDevice)) {
delete vulkan_;
vulkan_ = nullptr;
state_ = GraphicsContextState::FAILED_INIT;
return false;
}
INFO_LOG(Log::G3D, "Vulkan device created!");
return true;
}
bool IOSVulkanContext::InitSurface(WindowSystem winsys, void *data1, void *data2, std::string *errorMessage) {
_dbg_assert_(winsys == WINDOWSYSTEM_METAL_EXT);
CAMetalLayer *layer = (__bridge CAMetalLayer *)data1;
_assert_(vulkan_);
VkResult res = vulkan_->InitSurface(winsys, data1, nullptr);
if (res != VK_SUCCESS) {
ERROR_LOG(Log::G3D, "g_Vulkan->InitSurface failed: '%s'", VulkanResultToString(res));
*errorMessage = StringFromFormat("InitSurface failed: %s", VulkanResultToString(res));
return false;
}
bool useMultiThreading = g_Config.bRenderMultiThreading;
if (g_Config.iInflightFrames == 1) {
useMultiThreading = false;
}
draw_ = Draw::T3DCreateVulkanContext(vulkan_, useMultiThreading);
VkPresentModeKHR presentMode = ConfigPresentModeToVulkan(draw_);
// This MUST run on the main thread. We're taking our chances with a dispatch_sync here.
vulkan_->InitSwapchain(presentMode);
SetGPUBackend(GPUBackend::VULKAN);
bool shaderSuccess = draw_->CreatePresets(); // Doesn't fail, we ship the compiler.
_assert_msg_(shaderSuccess, "Failed to compile preset shaders");
draw_->HandleEvent(Draw::Event::GOT_BACKBUFFER, vulkan_->GetBackbufferWidth(), vulkan_->GetBackbufferHeight());
VulkanRenderManager *renderManager = (VulkanRenderManager *)draw_->GetNativeObject(Draw::NativeObject::RENDER_MANAGER);
renderManager->SetInflightFrames(g_Config.iInflightFrames);
vulkan_->SetCbGetDrawSize([]() {
return VkExtent2D {(uint32_t)g_display.pixel_xres, (uint32_t)g_display.pixel_yres};
});
state_ = GraphicsContextState::INITIALIZED;
return true;
}
void IOSVulkanContext::ShutdownSurface() {
INFO_LOG(Log::G3D, "IOSVulkanContext::Shutdown");
draw_->HandleEvent(Draw::Event::LOST_BACKBUFFER, vulkan_->GetBackbufferWidth(), vulkan_->GetBackbufferHeight());
delete draw_;
draw_ = nullptr;
vulkan_->WaitUntilQueueIdle();
vulkan_->PerformPendingDeletes();
vulkan_->DestroySwapchain();
vulkan_->DestroySurface();
INFO_LOG(Log::G3D, "Done with ShutdownFromRenderThread");
}
void IOSVulkanContext::ShutdownAPI() {
INFO_LOG(Log::G3D, "IOSVulkanContext::Shutdown");
vulkan_->DestroyDevice();
vulkan_->DestroyInstance();
// We keep the g_Vulkan context around to avoid invalidating a ton of pointers around the app.
finalize_glslang();
INFO_LOG(Log::G3D, "IOSVulkanContext::Shutdown completed");
}
#pragma mark -
#pragma mark PPSSPPViewControllerMetal
static std::atomic<bool> exitRenderLoop;
static std::atomic<bool> renderLoopRunning;
static std::thread g_renderLoopThread;
@interface PPSSPPViewControllerMetal () {
IOSVulkanContext *graphicsContext;
}
@end // @interface
@implementation PPSSPPViewControllerMetal {}
- (id)init {
self = [super init];
return self;
}
// Should be very similar to the Android one, probably mergeable.
// This is the EmuThread for iOS.
static void VulkanRenderLoop(GraphicsContext *graphicsContext, CAMetalLayer *metalLayer) {
SetCurrentThreadName("EmuThreadVulkan");
INFO_LOG(Log::G3D, "Entering EmuThreadVulkan");
_assert_(graphicsContext);
if (exitRenderLoop) {
WARN_LOG(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);
std::string errorMessage;
if (!graphicsContext->InitSurface(WINDOWSYSTEM_METAL_EXT, (__bridge void *)metalLayer, nullptr, &errorMessage)) {
// 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(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(Log::G3D, "Failed to initialize graphics.");
// Gonna be in a weird state here..
}
graphicsContext->ThreadStart();
while (!exitRenderLoop) {
NativeFrame(graphicsContext);
}
INFO_LOG(Log::G3D, "Leaving Vulkan main loop.");
} else {
INFO_LOG(Log::G3D, "Not entering main loop.");
}
NativeShutdownGraphics(graphicsContext);
graphicsContext->ThreadEnd();
// Shut the graphics context down to the same state it was in when we entered the render thread.
INFO_LOG(Log::G3D, "Shutting down graphics context...");
graphicsContext->ShutdownSurface();
renderLoopRunning = false;
exitRenderLoop = false;
WARN_LOG(Log::G3D, "Render loop function exited.");
}
- (bool)runVulkanRenderLoop {
INFO_LOG(Log::G3D, "runVulkanRenderLoop");
if (!graphicsContext) {
ERROR_LOG(Log::G3D, "runVulkanRenderLoop: Tried to enter without a created graphics context.");
return false;
}
if (g_renderLoopThread.joinable()) {
ERROR_LOG(Log::G3D, "runVulkanRenderLoop: Already running");
return false;
}
CAMetalLayer *metalLayer = (CAMetalLayer *)self.view.layer;
g_renderLoopThread = std::thread(VulkanRenderLoop, graphicsContext, metalLayer);
return true;
}
- (void)requestExitVulkanRenderLoop {
INFO_LOG(Log::G3D, "requestExitVulkanRenderLoop");
if (!renderLoopRunning) {
ERROR_LOG(Log::System, "Render loop already exited");
return;
}
_assert_(g_renderLoopThread.joinable());
exitRenderLoop = true;
g_renderLoopThread.join();
_assert_(!g_renderLoopThread.joinable());
}
// These two are forwarded from the appDelegate
- (void)didBecomeActive {
[super didBecomeActive];
INFO_LOG(Log::G3D, "didBecomeActive Metal");
// Spin up the emu thread. It will in turn spin up the Vulkan render thread
// on its own.
[self updateResolutionWithView:self.view];
[self runVulkanRenderLoop];
}
- (void)willResignActive {
INFO_LOG(Log::G3D, "willResignActive Metal");
[self requestExitVulkanRenderLoop];
[super willResignActive];
}
- (void)shutdown {
[super shutdown];
INFO_LOG(Log::System, "shutdown");
g_Config.Save("shutdown vk");
// Hopefully requestExitVulkanRenderLoop has been called from willResignActive here...
if (graphicsContext) {
graphicsContext->ShutdownAPI();
delete graphicsContext;
graphicsContext = NULL;
}
}
- (void)dealloc
{
INFO_LOG(Log::System, "dealloc VK");
}
- (void)loadView {
INFO_LOG(Log::G3D, "Creating metal view");
// The view gets auto-resized later.
PPSSPPMetalView *metalView = [[PPSSPPMetalView alloc] initWithFrame:CGRectMake(0, 0, 0, 0)];
self.view = metalView;
}
- (void)viewDidLoad {
[super viewDidLoad];
[self hideKeyboard];
INFO_LOG(Log::System, "Metal viewDidLoad");
UIScreen* screen = [(AppDelegate*)[UIApplication sharedApplication].delegate screen];
self.view.frame = [screen bounds];
self.view.multipleTouchEnabled = YES;
// self.view.insetsLayoutMarginsFromSafeArea = NO;
// self.view.clipsToBounds = YES;
graphicsContext = new IOSVulkanContext();
std::string errorMessage;
if (!graphicsContext->InitAPI(nullptr, &g_Config.sVulkanDevice, &errorMessage)) {
ERROR_LOG(Log::System, "Failed to initialize Vulkan, switching to OpenGL: %s", errorMessage.c_str());
g_Config.iGPUBackend = (int)GPUBackend::OPENGL;
SetGPUBackend(GPUBackend::OPENGL);
delete graphicsContext;
// TODO: What to do here?
}
[self updateResolutionWithView:self.view];
if ([[GCController controllers] count] > 0) {
[self setupController:[[GCController controllers] firstObject]];
}
INFO_LOG(Log::G3D, "Detected size: %dx%d", g_display.pixel_xres, g_display.pixel_yres);
}
- (UIView *)getView {
return [self view];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
INFO_LOG(Log::G3D, "viewWillAppear");
// This is to make sure we get 1:1 pixels.
UIWindowScene *scene = self.view.window.windowScene;
if (scene) {
self.view.contentScaleFactor = scene.screen.nativeScale;
}
if (@available(iOS 16.0, *)) {
[self setNeedsUpdateOfSupportedInterfaceOrientations];
}
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
INFO_LOG(Log::G3D, "viewWillDisappear");
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear: animated];
INFO_LOG(Log::G3D, "viewDidDisappear");
}
- (void)bindDefaultFBO {
// Do nothing
}
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
// This is the first reliable place where self.view.bounds
// matches the forced orientation.
CGRect bounds = self.view.bounds;
INFO_LOG(Log::G3D, "Correcting metal view layout: %dx%d",
(int)bounds.size.width, (int)bounds.size.height);
// Update your Metal layer/viewport here if necessary
}
- (void)viewWillTransitionToSize:(CGSize)size
withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
[self.view endEditing:YES]; // clears any input focus
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
NSLog(@"Rotating to size: %@", NSStringFromCGSize(size));
} completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
NSLog(@"Rotation finished");
// Reinitialize graphics context to match new size
[self requestExitVulkanRenderLoop];
[self updateResolutionWithView:self.view];
[self runVulkanRenderLoop];
}];
}
@end
@implementation PPSSPPMetalView
/** Returns a Metal-compatible layer. */
+(Class) layerClass { return [CAMetalLayer class]; }
@end