From ea101b714fd950080df38eb233979b3364c2941e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Fri, 25 Sep 2026 09:28:20 -0600 Subject: [PATCH] Headless: Run OpenGL offscreen on macOS, without a window A CGL context with no drawable, rendering into a framebuffer object of its own that stands in for the backbuffer through g_defaultFBO. Core profile, as the SDL app uses on macOS. Co-Authored-By: Claude Opus 5.5 (1M context) --- headless/Headless.cpp | 5 ++ headless/SDLHeadlessGLGraphicsContext.cpp | 91 +++++++++++++++++++++++ headless/SDLHeadlessGLGraphicsContext.h | 55 ++++++++++++++ 3 files changed, 151 insertions(+) diff --git a/headless/Headless.cpp b/headless/Headless.cpp index e33f85ea2f..9e6c057c0b 100644 --- a/headless/Headless.cpp +++ b/headless/Headless.cpp @@ -966,6 +966,11 @@ int main(int argc, const char* argv[]) { vulkanContext->SetOffscreen(480, 272); graphicsContext = vulkanContext; deviceSetting = &g_Config.sVulkanDevice; +#if PPSSPP_PLATFORM(MAC) && defined(SDL) + } else if (gpuCore == GPUCORE_GLES) { + // So does OpenGL, into a framebuffer object of its own. + graphicsContext = new CGLHeadlessGraphicsContext(480, 272); +#endif } else { // TODO: Will we need a larger window for higher resolutions? Well, not if we use buffered rendering. window = CreateHiddenWindow(480, 272, cmdLineOptions.gpuBackend.value_or(GPUBackend::OPENGL), &windowDesc); diff --git a/headless/SDLHeadlessGLGraphicsContext.cpp b/headless/SDLHeadlessGLGraphicsContext.cpp index 3e81455d0c..a3bb7417d2 100644 --- a/headless/SDLHeadlessGLGraphicsContext.cpp +++ b/headless/SDLHeadlessGLGraphicsContext.cpp @@ -25,7 +25,12 @@ #include "headless/SDLHeadlessGLGraphicsContext.h" #include "Common/GPU/OpenGL/GLCommon.h" #include "Common/GPU/OpenGL/GLFeatures.h" +#if PPSSPP_PLATFORM(MAC) +// After glew, which has its own definitions of what this would include. +#include +#endif #include "Common/GPU/thin3d_create.h" +#include "Common/StringUtils.h" #include "Common/File/VFS/VFS.h" #include "Common/File/VFS/DirectoryReader.h" #include "Common/GPU/GraphicsContext.h" @@ -92,6 +97,92 @@ bool SDLHeadlessGLGraphicsContext::InitSurface(WindowSystem winsys, void *data1, return true; } +#if PPSSPP_PLATFORM(MAC) + +// Bound by the GL backend wherever it would bind the default framebuffer. +extern GLuint g_defaultFBO; + +bool CGLHeadlessGraphicsContext::InitAPI(void *wnd, std::string *deviceName, std::string *errorMessage) { + const CGLPixelFormatAttribute attributes[] = { + kCGLPFAAccelerated, + kCGLPFAOpenGLProfile, (CGLPixelFormatAttribute)kCGLOGLPVersion_GL4_Core, + kCGLPFAColorSize, (CGLPixelFormatAttribute)24, + kCGLPFAAlphaSize, (CGLPixelFormatAttribute)8, + (CGLPixelFormatAttribute)0, + }; + CGLPixelFormatObj pixelFormat = nullptr; + GLint formatCount = 0; + if (CGLChoosePixelFormat(attributes, &pixelFormat, &formatCount) != kCGLNoError || !pixelFormat) { + *errorMessage = "CGLChoosePixelFormat failed"; + return false; + } + CGLContextObj context = nullptr; + CGLError err = CGLCreateContext(pixelFormat, nullptr, &context); + CGLDestroyPixelFormat(pixelFormat); + if (err != kCGLNoError) { + *errorMessage = StringFromFormat("CGLCreateContext failed: %s", CGLErrorString(err)); + return false; + } + context_ = context; + CGLSetCurrentContext(context); + + // Core profile drivers leave some extensions out of the list, so glew has to look for them anyway. + SetGLCoreContext(true); + glewExperimental = true; + if (glewInit() != GLEW_OK) { + *errorMessage = "Failed to initialize glew"; + return false; + } + // glew causes an invalid enum error with core profiles, ignore it. + glGetError(); + + // There's no drawable, so the backbuffer is a framebuffer object of our own. + glGenRenderbuffers(1, &colorBuffer_); + glBindRenderbuffer(GL_RENDERBUFFER, colorBuffer_); + glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, width_, height_); + glGenRenderbuffers(1, &depthStencilBuffer_); + glBindRenderbuffer(GL_RENDERBUFFER, depthStencilBuffer_); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, width_, height_); + glGenFramebuffers(1, &fbo_); + glBindFramebuffer(GL_FRAMEBUFFER, fbo_); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, colorBuffer_); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, depthStencilBuffer_); + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + *errorMessage = "The offscreen framebuffer is incomplete"; + return false; + } + g_defaultFBO = fbo_; + + CheckGLExtensions(); + SetGPUBackend(GPUBackend::OPENGL); + draw_ = Draw::T3DCreateGLContext(false); + renderManager_ = (GLRenderManager *)draw_->GetNativeObject(Draw::NativeObject::RENDER_MANAGER); + renderManager_->SetInflightFrames(g_Config.iInflightFrames); + bool success = draw_->CreatePresets(); + _assert_(success); + // Nothing to swap. Flushing keeps the frames moving like a swap would. + renderManager_->SetSwapFunction([]() { + glFlush(); + }); + return success; +} + +void CGLHeadlessGraphicsContext::ShutdownSurface() { + delete draw_; + draw_ = nullptr; + + g_defaultFBO = 0; + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glDeleteFramebuffers(1, &fbo_); + glDeleteRenderbuffers(1, &colorBuffer_); + glDeleteRenderbuffers(1, &depthStencilBuffer_); + CGLSetCurrentContext(nullptr); + CGLDestroyContext((CGLContextObj)context_); + context_ = nullptr; +} + +#endif + bool SDLHeadlessGLGraphicsContext::InitAPI(void *wnd, std::string *deviceName, std::string *errorMessage) { SDL_Init(SDL_INIT_VIDEO); diff --git a/headless/SDLHeadlessGLGraphicsContext.h b/headless/SDLHeadlessGLGraphicsContext.h index e8fa3764af..52a57c103c 100644 --- a/headless/SDLHeadlessGLGraphicsContext.h +++ b/headless/SDLHeadlessGLGraphicsContext.h @@ -19,6 +19,7 @@ #ifdef SDL +#include "ppsspp_config.h" #include #include "Common/GPU/GraphicsContext.h" @@ -70,4 +71,58 @@ private: void *CreateHiddenWindow(int w, int h, GPUBackend backend, WindowDesc *desc); void DestroyHiddenWindow(void *window, WindowDesc desc); +#if PPSSPP_PLATFORM(MAC) + +// An OpenGL context with no window: a CGL context without a drawable, rendering into a framebuffer +// object of its own that stands in for the backbuffer (through g_defaultFBO). Core profile, like +// the SDL app uses on macOS. +class CGLHeadlessGraphicsContext : public GraphicsContext { +public: + CGLHeadlessGraphicsContext(int width, int height) : width_(width), height_(height) {} + ~CGLHeadlessGraphicsContext() { delete draw_; } + + bool InitAPI(void *wnd, std::string *deviceNameSetting, std::string *errorMessage) override; + bool InitSurface(WindowSystem winsys, void *data1, void *data2, std::string *errorMessage) override { + return true; + } + void ShutdownSurface() override; + + bool NeedsSeparateEmuThread() const override { return true; } + + Draw::DrawContext *GetDrawContext() override { + return draw_; + } + + void ThreadStart() override { + renderManager_->ThreadStart(draw_); + } + + bool ThreadFrame() override { + return renderManager_->ThreadFrame(); + } + + void ThreadEnd() override { + renderManager_->ThreadEnd(); + } + + void Resize() override {} + + // Call from emu thread + void NotifyEmuThreadExit() override { + renderManager_->NotifyEmuThreadExit(); + } + +private: + Draw::DrawContext *draw_ = nullptr; + GLRenderManager *renderManager_ = nullptr; + void *context_ = nullptr; // CGLContextObj + unsigned int fbo_ = 0; + unsigned int colorBuffer_ = 0; + unsigned int depthStencilBuffer_ = 0; + int width_; + int height_; +}; + +#endif + #endif