mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-01 14:58:14 +00:00
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) <[email protected]>
This commit is contained in:
1 parent
c9cddb99e9
commit
ea101b714f
3 files changed
+151
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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 <OpenGL/OpenGL.h>
|
||||
#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);
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#ifdef SDL
|
||||
|
||||
#include "ppsspp_config.h"
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#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
|
||||
Reference in new issue
Block a user