From 8d7ce57fa85d20e2bfa3187d649198c2192067fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sat, 17 Oct 2026 20:04:18 +0200 Subject: [PATCH] Metal: Hook up the backend on Mac, with the null GPU for now Selectable with --graphics=metal, the backend choice in settings and the Mac menu bar. SDL creates a Metal view for it, and headless renders offscreen. Games run on the null GPU until there's a GPU/Metal. Co-Authored-By: Claude Opus 5.5 (1M context) --- Common/GPU/Metal/MetalGraphicsContext.mm | 1 + Core/CmdLine.cpp | 3 +++ Core/CoreParameter.h | 1 + GPU/GPU.cpp | 3 +++ SDL/CocoaBarItems.mm | 2 +- SDL/SDLMain.cpp | 29 +++++++++++++++++++++++- UI/DevScreens.cpp | 4 +++- UI/EmuScreen.cpp | 3 +++ UI/GameSettingsScreen.cpp | 4 +++- headless/Headless.cpp | 14 +++++++++++- 10 files changed, 59 insertions(+), 5 deletions(-) diff --git a/Common/GPU/Metal/MetalGraphicsContext.mm b/Common/GPU/Metal/MetalGraphicsContext.mm index d30b8aeea9..75d27bc27b 100644 --- a/Common/GPU/Metal/MetalGraphicsContext.mm +++ b/Common/GPU/Metal/MetalGraphicsContext.mm @@ -57,6 +57,7 @@ bool MetalGraphicsContext::InitSurface(WindowSystem winsys, void *data1, void *d renderManager_ = new MetalRenderManager(device, layer, offscreenWidth_, offscreenHeight_); renderManager_->SetInflightFrames(g_Config.iInflightFrames); draw_ = Draw::T3DCreateMetalContext(renderManager_); + INFO_LOG(Log::G3D, "Metal: Using %s, backbuffer %dx%d%s", [[device name] UTF8String], renderManager_->BackbufferWidth(), renderManager_->BackbufferHeight(), layer ? "" : " (offscreen)"); SetGPUBackend(GPUBackend::METAL, [[device name] UTF8String]); if (!draw_->CreatePresets()) { *errorMessage = "Failed to compile preset shaders"; diff --git a/Core/CmdLine.cpp b/Core/CmdLine.cpp index 58e172fa0a..f7c17b7d00 100644 --- a/Core/CmdLine.cpp +++ b/Core/CmdLine.cpp @@ -468,6 +468,9 @@ CommandLineParseResult CommandLineOptions::Parse(int argc, const char *argv[], C } else if (restOfOption == "vulkan") { gpuBackend = GPUBackend::VULKAN; softwareRendering = false; + } else if (restOfOption == "metal") { + gpuBackend = GPUBackend::METAL; + softwareRendering = false; } else if (restOfOption == "software") { gpuBackend = GPUBackend::OPENGL; softwareRendering = true; diff --git a/Core/CoreParameter.h b/Core/CoreParameter.h index 195a49d25a..1fcd5be0cd 100644 --- a/Core/CoreParameter.h +++ b/Core/CoreParameter.h @@ -29,6 +29,7 @@ enum GPUCore : int { GPUCORE_DIRECTX11 = 3, GPUCORE_VULKAN = 4, GPUCORE_NULL = 5, // Draws nothing, see GPU_Null.h. + GPUCORE_METAL = 6, }; enum class FPSLimit { diff --git a/GPU/GPU.cpp b/GPU/GPU.cpp index b6161d6ab3..7b1d9a0284 100644 --- a/GPU/GPU.cpp +++ b/GPU/GPU.cpp @@ -70,6 +70,9 @@ static GPUCommon *CreateGPUCore(GPUCore gpuCore, GraphicsContext *ctx, Draw::Dra } return new GPU_Vulkan(ctx, draw); #endif + case GPUCORE_METAL: + // TODO: The Metal GPU backend. Until then, nothing is drawn. + return CreateNullGPU(ctx); case GPUCORE_NULL: return CreateNullGPU(ctx); default: diff --git a/SDL/CocoaBarItems.mm b/SDL/CocoaBarItems.mm index 35be14791a..f769122653 100644 --- a/SDL/CocoaBarItems.mm +++ b/SDL/CocoaBarItems.mm @@ -467,7 +467,7 @@ static bool CanUseSaveStates(bool isSaveAction) { [self addSeparatorTo:menu]; NSMenu *backends = [self addSubmenu:DesktopUI("Backend") to:menu]; - for (GPUBackend backend : { GPUBackend::OPENGL, GPUBackend::VULKAN }) { + for (GPUBackend backend : { GPUBackend::OPENGL, GPUBackend::VULKAN, GPUBackend::METAL }) { if (!g_Config.IsBackendEnabled(backend)) { continue; } diff --git a/SDL/SDLMain.cpp b/SDL/SDLMain.cpp index a8c14238ca..bb484cd52f 100644 --- a/SDL/SDLMain.cpp +++ b/SDL/SDLMain.cpp @@ -75,6 +75,8 @@ SDLJoystick *joystick = NULL; #if PPSSPP_PLATFORM(MAC) || PPSSPP_PLATFORM(IOS) #include "Core/Util/DarwinFileSystemServices.h" #include "SDL/SDLCocoaMetalLayer.h" +#include "Common/GPU/Metal/MetalGraphicsContext.h" +#include #endif #if PPSSPP_PLATFORM(MAC) @@ -2158,6 +2160,11 @@ int main(int argc, char *argv[]) { case GPUBackend::OPENGL: fallbackGPUBackend = (int)GPUBackend::VULKAN; break; +#if PPSSPP_PLATFORM(MAC) + case GPUBackend::METAL: + fallbackGPUBackend = vulkanMayBeAvailable ? (int)GPUBackend::VULKAN : (int)GPUBackend::OPENGL; + break; +#endif default: fprintf(stderr, "Unknown GPU backend %d, switching to Vulkan.\n", g_Config.iGPUBackend); g_Config.iGPUBackend = (int)GPUBackend::VULKAN; @@ -2178,6 +2185,26 @@ int main(int argc, char *argv[]) { windowDesc.data2 = (void *)glContext; ctx = new SDLGLGraphicsContext(); +#if PPSSPP_PLATFORM(MAC) + } else if (backend == GPUBackend::METAL) { + window = SDL_CreateWindow("Initializing graphics...", w, h, (SDL_WindowFlags)(mode | SDL_WINDOW_METAL | SDL_WINDOW_HIDDEN)); + if (!window) { + *errorMessage = StringFromFormat("Error creating SDL window: %s", SDL_GetError()); + return false; + } + if (x != SDL_WINDOWPOS_UNDEFINED && y != SDL_WINDOWPOS_UNDEFINED) { + SDL_SetWindowPosition(window, x, y); + } + SDL_MetalView metalView = SDL_Metal_CreateView(window); + if (!metalView) { + *errorMessage = StringFromFormat("Error creating Metal view: %s", SDL_GetError()); + return false; + } + windowDesc.winsys = WINDOWSYSTEM_METAL_EXT; + windowDesc.data1 = SDL_Metal_GetLayer(metalView); + windowDesc.data2 = (void *)window; + ctx = new MetalGraphicsContext(); +#endif } else { // Use a local copy of mode: this flag combination is Vulkan-specific, and if we fall back to // OpenGL below, we don't want SDL_WINDOW_VULKAN to stick around and get OR'd in there too. @@ -2231,7 +2258,7 @@ int main(int argc, char *argv[]) { if (!initializeBackend((GPUBackend)g_Config.iGPUBackend, &graphicsContext, &error_message)) { fprintf(stderr, "Failed to initialize graphics backend: %s\n", error_message.c_str()); if (fallbackGPUBackend != -1) { - fprintf(stderr, "Attempting to fall back to %s...\n", fallbackGPUBackend == (int)GPUBackend::OPENGL ? "OpenGL" : "Vulkan"); + fprintf(stderr, "Attempting to fall back to %s...\n", GPUBackendToString((GPUBackend)fallbackGPUBackend).c_str()); g_Config.iGPUBackend = fallbackGPUBackend; error_message.clear(); if (!initializeBackend((GPUBackend)g_Config.iGPUBackend, &graphicsContext, &error_message)) { diff --git a/UI/DevScreens.cpp b/UI/DevScreens.cpp index 8f1073904b..cdd9846c80 100644 --- a/UI/DevScreens.cpp +++ b/UI/DevScreens.cpp @@ -763,7 +763,7 @@ void TouchTestScreen::CreateViews() { root_->Add(theTwo); #if !PPSSPP_PLATFORM(UWP) - static const char *renderingBackend[] = { "OpenGL", "(n/a)", "Direct3D 11", "Vulkan" }; + static const char *renderingBackend[] = { "OpenGL", "(n/a)", "Direct3D 11", "Vulkan", "Metal" }; PopupMultiChoice *renderingBackendChoice = root_->Add(new PopupMultiChoice(&g_Config.iGPUBackend, gr->T("Backend"), renderingBackend, (int)GPUBackend::OPENGL, ARRAY_SIZE(renderingBackend), I18NCat::GRAPHICS, screenManager())); renderingBackendChoice->OnChoice.Handle(this, &TouchTestScreen::OnRenderingBackend); @@ -774,6 +774,8 @@ void TouchTestScreen::CreateViews() { renderingBackendChoice->HideChoice((int)GPUBackend::DIRECT3D11); if (!g_Config.IsBackendEnabled(GPUBackend::VULKAN)) renderingBackendChoice->HideChoice((int)GPUBackend::VULKAN); + if (!g_Config.IsBackendEnabled(GPUBackend::METAL)) + renderingBackendChoice->HideChoice((int)GPUBackend::METAL); #endif #if PPSSPP_PLATFORM(ANDROID) diff --git a/UI/EmuScreen.cpp b/UI/EmuScreen.cpp index 567a312375..2727e5e1af 100644 --- a/UI/EmuScreen.cpp +++ b/UI/EmuScreen.cpp @@ -312,6 +312,9 @@ void EmuScreen::ProcessGameBoot(const Path &filename) { coreParam.gpuCore = GPUCORE_VULKAN; break; #endif + case GPUBackend::METAL: + coreParam.gpuCore = GPUCORE_METAL; + break; } // Preserve the existing graphics context. diff --git a/UI/GameSettingsScreen.cpp b/UI/GameSettingsScreen.cpp index 4df70181c1..de3c6b1fa6 100644 --- a/UI/GameSettingsScreen.cpp +++ b/UI/GameSettingsScreen.cpp @@ -279,7 +279,7 @@ void GameSettingsScreen::CreateGraphicsSettings(UI::ViewGroup *graphicsSettings) Draw::DrawContext *draw = screenManager()->getDrawContext(); #if !PPSSPP_PLATFORM(UWP) - static const char *renderingBackend[] = { "OpenGL", "Direct3D 9", "Direct3D 11", "Vulkan" }; + static const char *renderingBackend[] = { "OpenGL", "Direct3D 9", "Direct3D 11", "Vulkan", "Metal" }; PopupMultiChoice *renderingBackendChoice = graphicsSettings->Add(new PopupMultiChoice(&g_Config.iGPUBackend, gr->T("Backend"), renderingBackend, (int)GPUBackend::OPENGL, ARRAY_SIZE(renderingBackend), I18NCat::GRAPHICS, screenManager())); renderingBackendChoice->SetPreOpenCallback([this](UI::PopupMultiChoice *choice) { // Don't filter until the last possible moment, since it involves trying to initialize Vulkan, if we were @@ -293,6 +293,8 @@ void GameSettingsScreen::CreateGraphicsSettings(UI::ViewGroup *graphicsSettings) choice->HideChoice((int)GPUBackend::DIRECT3D11); if (!g_Config.IsBackendEnabled(GPUBackend::VULKAN)) choice->HideChoice((int)GPUBackend::VULKAN); + if (!g_Config.IsBackendEnabled(GPUBackend::METAL)) + choice->HideChoice((int)GPUBackend::METAL); }); if (!IsFirstInstance()) { diff --git a/headless/Headless.cpp b/headless/Headless.cpp index 18b0455516..e3e515f4f3 100644 --- a/headless/Headless.cpp +++ b/headless/Headless.cpp @@ -49,6 +49,9 @@ #include "Common/StringUtils.h" #include "Common/Thread/ThreadManager.h" #include "Common/GPU/Vulkan/VulkanGraphicsContext.h" +#if PPSSPP_PLATFORM(MAC) || PPSSPP_PLATFORM(IOS) +#include "Common/GPU/Metal/MetalGraphicsContext.h" +#endif #include "Core/CmdLine.h" #include "Common/Net/HTTPRequest.h" #include "Core/Config.h" @@ -422,7 +425,7 @@ struct AutoTestOptions { // swaps with interval 0). D3D11 presents to a hidden window, which could. static void EndDrawFrame(Draw::DrawContext *draw) { draw->EndFrame(); - if (GetGPUBackend() == GPUBackend::VULKAN || GetGPUBackend() == GPUBackend::OPENGL) { + if (GetGPUBackend() == GPUBackend::VULKAN || GetGPUBackend() == GPUBackend::OPENGL || GetGPUBackend() == GPUBackend::METAL) { draw->Present(Draw::PresentMode::FIFO); } } @@ -1080,6 +1083,9 @@ int main(int argc, const char* argv[]) { case GPUBackend::VULKAN: gpuCore = GPUCORE_VULKAN; break; + case GPUBackend::METAL: + gpuCore = GPUCORE_METAL; + break; } } if (cmdLineOptions.nullGraphics.value_or(false)) { @@ -1106,6 +1112,12 @@ int main(int argc, const char* argv[]) { vulkanContext->SetOffscreen(480, 272); graphicsContext = vulkanContext; deviceSetting = &g_Config.sVulkanDevice; +#if PPSSPP_PLATFORM(MAC) || PPSSPP_PLATFORM(IOS) + } else if (gpuCore == GPUCORE_METAL) { + MetalGraphicsContext *metalContext = new MetalGraphicsContext(); + metalContext->SetOffscreen(480, 272); + graphicsContext = metalContext; +#endif #if PPSSPP_PLATFORM(MAC) && defined(SDL) } else if (gpuCore == GPUCORE_GLES) { // So does OpenGL, into a framebuffer object of its own.