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) <noreply@anthropic.com>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-10-19 12:02:22 +02:00
1 parent 16ed2ad55a
commit 8d7ce57fa8
10 files changed
+59 -5

No files matched your search

+1
View File
@@ -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";
+3
View File
@@ -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;
+1
View File
@@ -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 {
+3
View File
@@ -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:
+1 -1
View File
@@ -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;
}
+28 -1
View File
@@ -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 <SDL3/SDL_metal.h>
#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)) {
+3 -1
View File
@@ -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)
+3
View File
@@ -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.
+3 -1
View File
@@ -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()) {
+13 -1
View File
@@ -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.