Headless: Make all GPU backends work correctly on Windows

This commit is contained in:
Henrik Rydgård committed 2026-08-05 00:15:56 +02:00
1 parent 25cf52176f
commit 551e4cd0ab
11 files changed
+85 -62

No files matched your search

+4 -1
View File
@@ -668,7 +668,10 @@ bool CreateDir(const Path &path, bool quiet) {
return false; return false;
} }
DEBUG_LOG(Log::IO, "CreateDir('%s')", path.c_str()); if (!quiet) {
DEBUG_LOG(Log::IO, "CreateDir('%s')", path.c_str());
}
#ifdef HAVE_LIBRETRO_VFS #ifdef HAVE_LIBRETRO_VFS
switch (LibretroMkdir(path.ToString().c_str())) { switch (LibretroMkdir(path.ToString().c_str())) {
case -2: case -2:
+10
View File
@@ -6,6 +6,16 @@
#include "Common/GPU/thin3d.h" #include "Common/GPU/thin3d.h"
#include "Common/TimeUtil.h" #include "Common/TimeUtil.h"
struct WindowDesc {
WindowSystem winsys{};
void *data1 = nullptr;
void *data2 = nullptr;
bool Valid() const {
return winsys != WindowSystem::WINDOWSYSTEM_UNINITIALIZED && data2; // Not all platforms use data1, so don't check it.
}
};
// Init is done differently on each platform, and done close to the creation, so it's // Init is done differently on each platform, and done close to the creation, so it's
// expected to be implemented by subclasses. // expected to be implemented by subclasses.
class GraphicsContext { class GraphicsContext {
-5
View File
@@ -6,11 +6,6 @@ class GraphicsContext;
class Application { class Application {
public: public:
WindowSystem windowSystem;
void *windowData1 = nullptr;
void *windowData2 = nullptr; // This should be the window, if there is one.
void *Window() const { return windowData2; }
virtual ~Application() = default; virtual ~Application() = default;
virtual bool InitGraphics(GraphicsContext *graphicsContext) = 0; virtual bool InitGraphics(GraphicsContext *graphicsContext) = 0;
virtual void ShutdownGraphics(GraphicsContext *graphicsContext) = 0; virtual void ShutdownGraphics(GraphicsContext *graphicsContext) = 0;
+3 -3
View File
@@ -396,10 +396,10 @@ CommandLineParseResult CommandLineOptions::Parse(int argc, const char *argv[], C
gpuBackend = GPUBackend::OPENGL; gpuBackend = GPUBackend::OPENGL;
softwareRendering = true; softwareRendering = true;
} else if (sscanf(restOfOption.c_str(), "gles%lg", &glVersionTemp) == 1 || sscanf(restOfOption.c_str(), "opengl%lg", &glVersionTemp) == 1) { } else if (sscanf(restOfOption.c_str(), "gles%lg", &glVersionTemp) == 1 || sscanf(restOfOption.c_str(), "opengl%lg", &glVersionTemp) == 1) {
g_Config.iGPUBackend = (int)GPUBackend::OPENGL; gpuBackend = GPUBackend::OPENGL;
g_Config.bSoftwareRendering = false; softwareRendering = false;
force_gl_version = int(10.0 * glVersionTemp + 0.5); force_gl_version = int(10.0 * glVersionTemp + 0.5);
} else if (restOfOption == "gles") { } else if (restOfOption == "gles" || restOfOption == "opengl") {
gpuBackend = GPUBackend::OPENGL; gpuBackend = GPUBackend::OPENGL;
softwareRendering = false; softwareRendering = false;
} else { } else {
+2 -8
View File
@@ -100,8 +100,6 @@ bool RunMainLoop(GraphicsContext *graphicsContext, Application *application, std
application->InitGraphics(graphicsContext); application->InitGraphics(graphicsContext);
DEBUG_LOG(Log::Boot, "Done.");
g_inLoop = true; g_inLoop = true;
while (frame(graphicsContext)) { while (frame(graphicsContext)) {
@@ -114,7 +112,6 @@ bool RunMainLoop(GraphicsContext *graphicsContext, Application *application, std
// Process the shutdown. Without this, non-GL delays 800ms on shutdown. TODO: is this still an issue? // Process the shutdown. Without this, non-GL delays 800ms on shutdown. TODO: is this still an issue?
Core_StateProcessed(); Core_StateProcessed();
frame(graphicsContext);
g_inLoop = false; g_inLoop = false;
@@ -124,20 +121,17 @@ bool RunMainLoop(GraphicsContext *graphicsContext, Application *application, std
} }
// Call InitAPI and ShutdownAPI outside this! // Call InitAPI and ShutdownAPI outside this!
bool MainThreadFunc(GraphicsContext *graphicsContext, Application *application, WindowSystem windowSystem, void *windowData1, void *windowData2, std::function<bool(GraphicsContext *)> frame) { bool MainThreadFunc(GraphicsContext *graphicsContext, Application *application, const WindowDesc &windowDesc, std::function<bool(GraphicsContext *)> frame) {
// This is now the render thread, and will spawn the emu thread below. // This is now the render thread, and will spawn the emu thread below.
std::string error_string; std::string error_string;
bool success = graphicsContext->InitSurface(windowSystem, windowData1, windowData2, &error_string); bool success = graphicsContext->InitSurface(windowDesc.winsys, windowDesc.data1, windowDesc.data2, &error_string);
if (!success) { if (!success) {
return false; return false;
} }
std::string errorMessage; std::string errorMessage;
if (graphicsContext->NeedsSeparateEmuThread()) { if (graphicsContext->NeedsSeparateEmuThread()) {
SetCurrentThreadName("RenderThread"); SetCurrentThreadName("RenderThread");
DEBUG_LOG(Log::Boot, "Done.");
g_inLoop = true; g_inLoop = true;
std::thread emuThread = EmuThread_Start(graphicsContext, application, frame); std::thread emuThread = EmuThread_Start(graphicsContext, application, frame);
graphicsContext->ThreadStart(); graphicsContext->ThreadStart();
+2 -1
View File
@@ -26,12 +26,13 @@
bool MainThread_Ready(); bool MainThread_Ready();
class GraphicsContext; class GraphicsContext;
struct WindowDesc;
// Doesn't take ownership of the graphicsContext, you have to delete it. // Doesn't take ownership of the graphicsContext, you have to delete it.
// This should be used by platforms that launch a separate thread and doesn't // This should be used by platforms that launch a separate thread and doesn't
// need to run a polling loop in it. // need to run a polling loop in it.
// NOTE: Does take ownership over Application (which is just a wrapper for NativeInitGraphics/NativeShutdownGraphics/NativeFrame). // NOTE: Does take ownership over Application (which is just a wrapper for NativeInitGraphics/NativeShutdownGraphics/NativeFrame).
bool MainThreadFunc(GraphicsContext * graphicsContext, Application *application, WindowSystem windowSystem, void *windowData1, void *windowData2, std::function<bool(GraphicsContext *)> frame); bool MainThreadFunc(GraphicsContext * graphicsContext, Application *application, const WindowDesc &windowDesc, std::function<bool(GraphicsContext *)> frame);
// If you're not using MainThreadFunc, you can at least use these to manage a spinning EmuThread (that calls NativeFrame), // If you're not using MainThreadFunc, you can at least use these to manage a spinning EmuThread (that calls NativeFrame),
// whether your graphics context requires multithreading or not. Then use RunMainLoop to implement your main loop for // whether your graphics context requires multithreading or not. Then use RunMainLoop to implement your main loop for
+7 -2
View File
@@ -1236,7 +1236,12 @@ int WINAPI WinMain(HINSTANCE _hInstance, HINSTANCE hPrevInstance, LPSTR szCmdLin
System_SetWindowTitle(""); System_SetWindowTitle("");
System_Notify(SystemNotification::UI); System_Notify(SystemNotification::UI);
std::thread mainThread = std::thread([]() { WindowDesc desc;
desc.winsys = WINDOWSYSTEM_WIN32;
desc.data1 = MainWindow::GetHInstance();
desc.data2 = MainWindow::GetHWND();
std::thread mainThread = std::thread([desc]() {
// TODO: We can really merge all of this into MainThreadFunc // TODO: We can really merge all of this into MainThreadFunc
std::string errorMessage; std::string errorMessage;
std::string *deviceNameSetting; std::string *deviceNameSetting;
@@ -1245,7 +1250,7 @@ int WINAPI WinMain(HINSTANCE _hInstance, HINSTANCE hPrevInstance, LPSTR szCmdLin
HandleGraphicsFailure(errorMessage); HandleGraphicsFailure(errorMessage);
return; return;
} }
if (!MainThreadFunc(graphicsContext.get(), new NativeApplication(), WINDOWSYSTEM_WIN32, MainWindow::GetHInstance(), MainWindow::GetHWND(), if (!MainThreadFunc(graphicsContext.get(), new NativeApplication(), desc,
[](GraphicsContext *graphicsContext) { [](GraphicsContext *graphicsContext) {
NativeFrame(graphicsContext); NativeFrame(graphicsContext);
return GetUIState() != UISTATE_EXIT; return GetUIState() != UISTATE_EXIT;
+35 -30
View File
@@ -233,14 +233,12 @@ void System_SendDebugScreenshot(const uint8_t *data, int width, int height) {
} }
} }
static GraphicsContext *CreateGraphicsContext(GPUCore gpuCore, std::string **deviceSetting, WindowSystem *winsys) { static GraphicsContext *CreateGraphicsContext(GPUCore gpuCore, std::string **deviceSetting) {
#ifdef SDL #ifdef SDL
*deviceSetting = nullptr; *deviceSetting = nullptr;
*winsys = WindowSystem::WINDOWSYSTEM_SDL;
return new SDLHeadlessGLGraphicsContext(); return new SDLHeadlessGLGraphicsContext();
#elif PPSSPP_PLATFORM(WINDOWS) && !PPSSPP_PLATFORM(UWP) #elif PPSSPP_PLATFORM(WINDOWS) && !PPSSPP_PLATFORM(UWP)
GraphicsContext *graphicsContext = nullptr; GraphicsContext *graphicsContext = nullptr;
*winsys = WINDOWSYSTEM_WIN32;
switch (gpuCore) { switch (gpuCore) {
case GPUCORE_GLES: case GPUCORE_GLES:
*deviceSetting = nullptr; *deviceSetting = nullptr;
@@ -353,12 +351,11 @@ static bool RunAutoTest(GraphicsContext *graphicsContext, CoreParameter &corePar
} }
if (draw) { if (draw) {
draw->BindFramebufferAsRenderTarget(nullptr, { Draw::RPAction::CLEAR, Draw::RPAction::DONT_CARE, Draw::RPAction::DONT_CARE }, "BackBuffer");
// Vulkan may get angry if we don't do a final present. // Vulkan may get angry if we don't do a final present.
if (gpu) { if (gpu) {
gpu->SetCurFramebufferDirty(true); gpu->SetCurFramebufferDirty(true);
gpu->PrepareCopyDisplayToOutput(g_Config.GetDisplayLayoutConfig(DeviceOrientation::Landscape)); gpu->PrepareCopyDisplayToOutput(g_Config.GetDisplayLayoutConfig(DeviceOrientation::Landscape));
draw->BindFramebufferAsRenderTarget(nullptr, {RPAction::CLEAR, RPAction::CLEAR, RPAction::CLEAR}, "BackBuffer"); draw->BindFramebufferAsRenderTarget(nullptr, {RPAction::CLEAR, RPAction::DONT_CARE, RPAction::DONT_CARE}, "BackBuffer");
gpu->CopyDisplayToOutput(g_Config.GetDisplayLayoutConfig(DeviceOrientation::Landscape)); gpu->CopyDisplayToOutput(g_Config.GetDisplayLayoutConfig(DeviceOrientation::Landscape));
} }
@@ -497,8 +494,6 @@ public:
void ShutdownGraphics(GraphicsContext *graphicsContext) override { void ShutdownGraphics(GraphicsContext *graphicsContext) override {
graphicsContext->NotifyEmuThreadExit(); graphicsContext->NotifyEmuThreadExit();
} }
private:
std::function<void()> frameCallback_;
}; };
int main(int argc, const char* argv[]) { int main(int argc, const char* argv[]) {
@@ -656,7 +651,7 @@ int main(int argc, const char* argv[]) {
g_Config.iInternalResolution = cmdLineOptions.resolutionScale.value_or(1); g_Config.iInternalResolution = cmdLineOptions.resolutionScale.value_or(1);
g_Config.bEnableLogging = (fullLog || outputDebugStringLog); g_Config.bEnableLogging = (fullLog || outputDebugStringLog);
g_Config.bVertexDecoderJit = true; g_Config.bVertexDecoderJit = true;
g_Config.bSoftwareRendering = gpuCore == GPUCORE_SOFTWARE; g_Config.bSoftwareRendering = cmdLineOptions.softwareRendering.value_or(false);
g_Config.bSoftwareRenderingJit = true; g_Config.bSoftwareRenderingJit = true;
g_Config.iSplineBezierQuality = 2; g_Config.iSplineBezierQuality = 2;
g_Config.bHighQualityDepth = true; g_Config.bHighQualityDepth = true;
@@ -676,19 +671,33 @@ int main(int argc, const char* argv[]) {
// overrides above, so a matching command line flag always wins. // overrides above, so a matching command line flag always wins.
cmdLineOptions.ApplyToConfig(); cmdLineOptions.ApplyToConfig();
// TODO: Will we need a larger window for higher resolutions? Well, not if we use buffered rendering. // Translate backend to core. We probably should consider merging these enums.
void *window = nullptr; if (!g_Config.bSoftwareRendering && cmdLineOptions.gpuBackend.has_value()) {
switch (cmdLineOptions.gpuBackend.value()) {
case GPUBackend::OPENGL:
gpuCore = GPUCORE_GLES;
break;
case GPUBackend::DIRECT3D11:
gpuCore = GPUCORE_DIRECTX11;
break;
case GPUBackend::VULKAN:
gpuCore = GPUCORE_VULKAN;
break;
}
}
// Time to set up graphics // Time to set up graphics
GraphicsContext *graphicsContext = nullptr; GraphicsContext *graphicsContext = nullptr;
std::string *deviceSetting = nullptr; std::string *deviceSetting = nullptr;
WindowSystem winsys = WindowSystem::WINDOWSYSTEM_NONE; WindowDesc windowDesc;
if (g_Config.bSoftwareRendering) { if (g_Config.bSoftwareRendering) {
// For software rendering, we just create a dummy graphics context (to share as much code as possible). // For software rendering, we just create a dummy graphics context (to share as much code as possible).
// We don't bother with a window.
graphicsContext = new NullGraphicsContext(); graphicsContext = new NullGraphicsContext();
} else { } else {
window = CreateHiddenWindow(480, 272); // TODO: Will we need a larger window for higher resolutions? Well, not if we use buffered rendering.
graphicsContext = CreateGraphicsContext(gpuCore, &deviceSetting, &winsys); windowDesc = CreateHiddenWindow(480, 272);
graphicsContext = CreateGraphicsContext(gpuCore, &deviceSetting);
if (!graphicsContext) { if (!graphicsContext) {
// If we don't get the desired context, we DO NOT fall back. // If we don't get the desired context, we DO NOT fall back.
fprintf(stderr, "Failed to create a graphics context for GPU core"); fprintf(stderr, "Failed to create a graphics context for GPU core");
@@ -785,31 +794,27 @@ int main(int argc, const char* argv[]) {
SaveState::Load(Path(stateToLoad), -1); SaveState::Load(Path(stateToLoad), -1);
} }
std::string errorMessage;
Application *app = new HeadlessApplication([&]() { if (!graphicsContext->InitAPI(windowDesc.data2, deviceSetting, &errorMessage)) {
// This is called from the main thread, so we can run the emulation here if we don't need a separate thread. // No fallbacks in headless - if we can't run it, we can't. Let's not get confusing.
if (!graphicsContext->NeedsSeparateEmuThread()) { fprintf(stderr, "Failed to initialize graphics API: %s\n", errorMessage.c_str());
RunTests(graphicsContext, coreParameter, testOptions, testFilenames); return 1;
}
});
if (graphicsContext->NeedsSeparateEmuThread()) {
std::thread emuThread = EmuThread_Start(graphicsContext, app, nullptr);
emuThread.join();
} }
int retval = 0;
MainThreadFunc(graphicsContext, new HeadlessApplication(), windowDesc, [&retval, &coreParameter, &testOptions, &testFilenames](GraphicsContext *graphicsContext) {
retval = RunTests(graphicsContext, coreParameter, testOptions, testFilenames);
return false;
});
// Run the tests (or frame dumps), one after another. graphicsContext->ShutdownAPI();
const int retval = RunTests(graphicsContext, coreParameter, testOptions, testFilenames);
if (cmdLineOptions.debuggerPort.has_value()) { if (cmdLineOptions.debuggerPort.has_value()) {
ShutdownWebServer(); ShutdownWebServer();
} }
if (window) { if (windowDesc.Valid()) {
DestroyHiddenWindow(window); DestroyHiddenWindow(windowDesc);
} }
g_VFS.Clear(); g_VFS.Clear();
+8 -5
View File
@@ -37,17 +37,20 @@
const bool WINDOW_VISIBLE = false; const bool WINDOW_VISIBLE = false;
void *CreateHiddenWindow(int w, int h) { WindowDesc CreateHiddenWindow(int w, int h) {
Uint32 flags = SDL_WINDOW_OPENGL | SDL_WINDOW_BORDERLESS; Uint32 flags = SDL_WINDOW_OPENGL | SDL_WINDOW_BORDERLESS;
if (!WINDOW_VISIBLE) { if (!WINDOW_VISIBLE) {
flags |= SDL_WINDOW_HIDDEN; flags |= SDL_WINDOW_HIDDEN;
} }
return SDL_CreateWindow("PPSSPPHeadless", w, h, flags); WindowDesc desc;
desc.data2 = SDL_CreateWindow("PPSSPPHeadless", w, h, flags);
desc.winsys = WindowSystem::WINDOWSYSTEM_SDL;
return desc;
} }
void DestroyHiddenWindow(void *window) { void DestroyHiddenWindow(WindowDesc window) {
if (window) { if (window.data2) {
SDL_DestroyWindow(static_cast<SDL_Window *>(window)); SDL_DestroyWindow(static_cast<SDL_Window *>(window.data2));
SDL_Quit(); SDL_Quit();
} }
} }
+10 -5
View File
@@ -19,10 +19,11 @@
#include "headless/WindowsHeadlessHost.h" #include "headless/WindowsHeadlessHost.h"
#include "Common/CommonWindows.h" #include "Common/CommonWindows.h"
#include "Common/GPU/GraphicsContext.h"
const bool WINDOW_VISIBLE = false; const bool WINDOW_VISIBLE = false;
void *CreateHiddenWindow(int w, int h) { WindowDesc CreateHiddenWindow(int w, int h) {
static WNDCLASSEX wndClass = { static WNDCLASSEX wndClass = {
sizeof(WNDCLASSEX), sizeof(WNDCLASSEX),
CS_HREDRAW | CS_VREDRAW | CS_OWNDC, CS_HREDRAW | CS_VREDRAW | CS_OWNDC,
@@ -46,11 +47,15 @@ void *CreateHiddenWindow(int w, int h) {
ShowWindow(wnd, TRUE); ShowWindow(wnd, TRUE);
SetFocus(wnd); SetFocus(wnd);
} }
return static_cast<void *>(wnd); WindowDesc desc;
desc.data1 = GetModuleHandle(NULL);
desc.data2 = wnd;
desc.winsys = WindowSystem::WINDOWSYSTEM_WIN32;
return desc;
} }
void DestroyHiddenWindow(void *window) { void DestroyHiddenWindow(WindowDesc window) {
if (window) { if (window.data2) {
DestroyWindow(static_cast<HWND>(window)); DestroyWindow(static_cast<HWND>(window.data2));
} }
} }
+4 -2
View File
@@ -17,6 +17,8 @@
#pragma once #pragma once
struct WindowDesc;
// Same API as for SDL // Same API as for SDL
void *CreateHiddenWindow(int w, int h); WindowDesc CreateHiddenWindow(int w, int h);
void DestroyHiddenWindow(void *window); void DestroyHiddenWindow(WindowDesc window);