Merge pull request #22033 from hrydgard/sdl-headless-vulkan

Headless SDL: Add support for Vulkan rendering
This commit is contained in:
Henrik Rydgård authored and GitHub committed 2026-08-06 13:28:33 +02:00
commit a8b88856f7
12 files changed
+197 -98

No files matched your search

+12 -2
View File
@@ -1049,6 +1049,7 @@ else()
SDL/SDLJoystick.h
SDL/SDLJoystick.cpp
SDL/SDLMain.cpp
SDL/SDLUtil.cpp
SDL/SDLGLGraphicsContext.cpp
)
if(SDL_TTF_LIB_TARGET)
@@ -1329,13 +1330,22 @@ if(HEADLESS)
headless/Headless.cpp
headless/Compare.cpp
headless/Compare.h
headless/SDLHeadlessGLGraphicsContext.cpp
headless/SDLHeadlessGLGraphicsContext.h
)
if(SDL_LIB_TARGET)
list(APPEND HeadlessSource
headless/SDLHeadlessGLGraphicsContext.cpp
headless/SDLHeadlessGLGraphicsContext.h
# TODO: Break these out into a library instead of cross linking like this.
SDL/SDLUtil.cpp
SDL/SDLUtil.h
)
endif()
if(APPLE)
list(APPEND HeadlessSource
Common/Render/Text/draw_text_cocoa.mm
Common/Render/Text/draw_text_cocoa.h
SDL/SDLCocoaMetalLayer.h
SDL/SDLCocoaMetalLayer.mm
)
set_source_files_properties(Common/Render/Text/draw_text_cocoa.mm
PROPERTIES COMPILE_FLAGS -fobjc-arc)
+20 -3
View File
@@ -91,6 +91,23 @@ const char *VulkanImageLayoutToString(VkImageLayout imageLayout) {
}
}
const char *WindowSystemToString(WindowSystem winsys) {
switch (winsys) {
case WINDOWSYSTEM_UNINITIALIZED: return "UNINITIALIZED";
case WINDOWSYSTEM_WIN32: return "WIN32";
case WINDOWSYSTEM_ANDROID: return "ANDROID";
case WINDOWSYSTEM_METAL_EXT: return "METAL_EXT";
case WINDOWSYSTEM_XLIB: return "XLIB";
case WINDOWSYSTEM_XCB: return "XCB";
case WINDOWSYSTEM_WAYLAND: return "WAYLAND";
case WINDOWSYSTEM_DISPLAY: return "DISPLAY";
case WINDOWSYSTEM_SDL: return "SDL";
case WINDOWSYSTEM_NONE: return "NONE";
default:
return "UNKNOWN";
}
}
VulkanContext::VulkanContext() {
// Do nothing here.
}
@@ -970,9 +987,9 @@ VkResult VulkanContext::ReinitSurface() {
surface_ = VK_NULL_HANDLE;
}
INFO_LOG(Log::G3D, "Creating Vulkan surface for window (data1=%p data2=%p)", winsysData1_, winsysData2_);
INFO_LOG(Log::G3D, "Creating Vulkan surface for window (winsys=%s data1=%p data2=%p)", WindowSystemToString(winsys_), winsysData1_, winsysData2_);
VkResult retval = VK_SUCCESS;
VkResult retval = VK_SUCCESS;
switch (winsys_) {
#ifdef _WIN32
@@ -1233,7 +1250,7 @@ VkResult VulkanContext::ReinitSurface() {
#endif
default:
_assert_msg_(false, "Vulkan support for chosen window system not implemented");
_assert_msg_(false, "Vulkan support for chosen window system (%s) not implemented", WindowSystemToString(winsys_));
return VK_ERROR_INITIALIZATION_FAILED;
}
+7 -61
View File
@@ -67,6 +67,7 @@ SDLJoystick *joystick = NULL;
#include "Core/Config.h"
#include "Core/ConfigValues.h"
#include "SDLGLGraphicsContext.h"
#include "SDLUtil.h"
#include <SDL3/SDL_vulkan.h>
@@ -1726,58 +1727,6 @@ void UpdateSDLCursor() {
#endif
}
bool DetermineVulkanWindowSystem(SDL_Window *window, WindowSystem *windowSystem, void **data1, void **data2, std::string *errorMessage) {
_dbg_assert_(window);
SDL_PropertiesID windowProps = SDL_GetWindowProperties(window);
void *x11Display = SDL_GetPointerProperty(windowProps, SDL_PROP_WINDOW_X11_DISPLAY_POINTER, nullptr);
if (x11Display != nullptr) {
intptr_t x11Window = (intptr_t)SDL_GetNumberProperty(windowProps, SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0);
#if defined(VK_USE_PLATFORM_XLIB_KHR)
*windowSystem = WINDOWSYSTEM_XLIB;
*data1 = x11Display;
*data2 = (void *)x11Window;
#elif defined(VK_USE_PLATFORM_XCB_KHR)
*windowSystem = WINDOWSYSTEM_XCB;
*data1 = (void *)XGetXCBConnection((Display *)x11Display);
*data2 = (void *)x11Window;
#endif
return true;
}
#if defined(VK_USE_PLATFORM_WAYLAND_KHR)
void *waylandDisplay = SDL_GetPointerProperty(windowProps, SDL_PROP_WINDOW_WAYLAND_DISPLAY_POINTER, nullptr);
void *waylandSurface = SDL_GetPointerProperty(windowProps, SDL_PROP_WINDOW_WAYLAND_SURFACE_POINTER, nullptr);
if (waylandDisplay != nullptr && waylandSurface != nullptr) {
*windowSystem = WINDOWSYSTEM_WAYLAND;
*data1 = waylandDisplay;
*data2 = waylandSurface;
return true;
}
#elif defined(VK_USE_PLATFORM_METAL_EXT)
#if PPSSPP_PLATFORM(MAC)
void *cocoaWindow = SDL_GetPointerProperty(windowProps, SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, nullptr);
if (cocoaWindow != nullptr) {
*windowSystem = WINDOWSYSTEM_METAL_EXT;
*data1 = makeWindowMetalCompatible(cocoaWindow);
*data2 = nullptr;
return true;
}
#else
// This path is currently not used as we do not use SDL on iOS, but it is here for completeness.
void *uikitWindow = SDL_GetPointerProperty(windowProps, SDL_PROP_WINDOW_UIKIT_WINDOW_POINTER, nullptr);
if (uikitWindow != nullptr) {
*windowSystem = WINDOWSYSTEM_METAL_EXT;
*data1 = makeWindowMetalCompatible(uikitWindow);
*data2 = nullptr;
return true;
}
#endif
#endif // VK_USE_PLATFORM_METAL_EXT
if (errorMessage) {
*errorMessage = "Unable to determine Vulkan window system from SDL3 window properties";
}
return false;
}
#ifdef _WIN32
#undef main
#endif
@@ -2007,19 +1956,16 @@ int main(int argc, char *argv[]) {
}
SDL_Window *window = nullptr;
WindowDesc windowDesc;
auto initializeBackend = [&](GPUBackend backend, GraphicsContext **graphicsContext, std::string *errorMessage) -> bool {
// Surface init params.
WindowSystem windowSystem = WINDOWSYSTEM_NONE;
void *data1 = nullptr;
void *data2 = nullptr;
GraphicsContext *ctx = nullptr;
if (backend == GPUBackend::OPENGL) {
SDL_GLContext glContext = nullptr;
window = CreateSDLGLWindowAndContext(x, y, w, h, mode, cmdLineOptions.force_gl_version, &glContext, errorMessage);
data1 = (void *)window;
data2 = (void *)glContext;
windowDesc.winsys = WINDOWSYSTEM_SDL;
windowDesc.data1 = (void *)window;
windowDesc.data2 = (void *)glContext;
ctx = new SDLGLGraphicsContext();
} else {
@@ -2038,7 +1984,7 @@ int main(int argc, char *argv[]) {
}
// Overwrite the surface init params with what we need for Vulkan..
if (!DetermineVulkanWindowSystem(window, &windowSystem, &data1, &data2, errorMessage)) {
if (!DetermineVulkanWindowSystem(window, &windowDesc, errorMessage)) {
return false;
}
// NOTE : This should match the lines below in the Vulkan case.
@@ -2061,7 +2007,7 @@ int main(int argc, char *argv[]) {
});
}
if (!ctx->InitSurface(windowSystem, data1, data2, errorMessage)) {
if (!ctx->InitSurface(windowDesc.winsys, windowDesc.data1, windowDesc.data2, errorMessage)) {
fprintf(stderr, "Surface creation failed: %s\n", errorMessage->c_str());
return false;
}
+66
View File
@@ -0,0 +1,66 @@
#include <SDL3/SDL.h>
#include "ppsspp_config.h"
#if PPSSPP_PLATFORM(MAC) || PPSSPP_PLATFORM(IOS)
#include "SDL/SDLCocoaMetalLayer.h"
#endif
#include "Common/GPU/MiscTypes.h"
#include "Common/GPU/Vulkan/VulkanLoader.h"
#include "Common/GPU/Vulkan/VulkanContext.h"
#include "Common/GPU/GraphicsContext.h"
#include "SDL/SDLUtil.h"
bool DetermineVulkanWindowSystem(SDL_Window *window, WindowDesc *desc, std::string *errorMessage) {
_dbg_assert_(window);
SDL_PropertiesID windowProps = SDL_GetWindowProperties(window);
void *x11Display = SDL_GetPointerProperty(windowProps, SDL_PROP_WINDOW_X11_DISPLAY_POINTER, nullptr);
if (x11Display != nullptr) {
intptr_t x11Window = (intptr_t)SDL_GetNumberProperty(windowProps, SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0);
#if defined(VK_USE_PLATFORM_XLIB_KHR)
desc->winsys = WINDOWSYSTEM_XLIB;
desc->data1 = x11Display;
desc->data2 = (void *)x11Window;
#elif defined(VK_USE_PLATFORM_XCB_KHR)
desc->winsys = WINDOWSYSTEM_XCB;
desc->data1 = (void *)XGetXCBConnection((Display *)x11Display);
desc->data2 = (void *)x11Window;
#endif
return true;
}
#if defined(VK_USE_PLATFORM_WAYLAND_KHR)
void *waylandDisplay = SDL_GetPointerProperty(windowProps, SDL_PROP_WINDOW_WAYLAND_DISPLAY_POINTER, nullptr);
void *waylandSurface = SDL_GetPointerProperty(windowProps, SDL_PROP_WINDOW_WAYLAND_SURFACE_POINTER, nullptr);
if (waylandDisplay != nullptr && waylandSurface != nullptr) {
desc->winsys = WINDOWSYSTEM_WAYLAND;
desc->data1 = waylandDisplay;
desc->data2 = waylandSurface;
return true;
}
#elif defined(VK_USE_PLATFORM_METAL_EXT)
#if PPSSPP_PLATFORM(MAC)
void *cocoaWindow = SDL_GetPointerProperty(windowProps, SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, nullptr);
if (cocoaWindow != nullptr) {
desc->winsys = WINDOWSYSTEM_METAL_EXT;
desc->data1 = makeWindowMetalCompatible(cocoaWindow);
desc->data2 = nullptr;
return true;
}
#else
// This path is currently not used as we do not use SDL on iOS, but it is here for completeness.
void *uikitWindow = SDL_GetPointerProperty(windowProps, SDL_PROP_WINDOW_UIKIT_WINDOW_POINTER, nullptr);
if (uikitWindow != nullptr) {
desc->winsys = WINDOWSYSTEM_METAL_EXT;
desc->data1 = makeWindowMetalCompatible(uikitWindow);
desc->data2 = nullptr;
return true;
}
#endif
#endif // VK_USE_PLATFORM_METAL_EXT
if (errorMessage) {
*errorMessage = "Unable to determine Vulkan window system from SDL3 window properties";
}
return false;
}
+9
View File
@@ -0,0 +1,9 @@
#pragma once
#include <SDL3/SDL.h>
#include "Common/GPU/MiscTypes.h"
#include <string>
struct WindowDesc;
bool DetermineVulkanWindowSystem(SDL_Window *window, WindowDesc *desc, std::string *errorMessage);
+15 -7
View File
@@ -228,7 +228,15 @@ void System_SendDebugScreenshot(const uint8_t *data, int width, int height) {
static GraphicsContext *CreateGraphicsContext(GPUCore gpuCore, std::string **deviceSetting) {
#ifdef SDL
*deviceSetting = nullptr;
return new SDLHeadlessGLGraphicsContext();
switch (gpuCore) {
case GPUCORE_GLES:
return new SDLHeadlessGLGraphicsContext();
case GPUCORE_VULKAN:
*deviceSetting = &g_Config.sVulkanDevice;
return new VulkanGraphicsContext();
default:
return nullptr;
}
#elif PPSSPP_PLATFORM(WINDOWS) && !PPSSPP_PLATFORM(UWP)
switch (gpuCore) {
#if PPSSPP_API(ANY_GL)
@@ -244,8 +252,7 @@ static GraphicsContext *CreateGraphicsContext(GPUCore gpuCore, std::string **dev
return new VulkanGraphicsContext();
case GPUCORE_SOFTWARE:
default:
_assert_(false);
break;
return nullptr;
}
#elif PPSSPP_ARCH(LOONGARCH64)
// The loongarch64 cross-compilation toolchain has no SDL3 packages available (see the
@@ -260,8 +267,8 @@ static GraphicsContext *CreateGraphicsContext(GPUCore gpuCore, std::string **dev
return nullptr;
#else
#error The Headless build is not supported on this platform. Please use SDL (Mac/Linux) or Windows (non-UWP).
#endif
return nullptr;
#endif
}
struct AutoTestOptions {
@@ -675,6 +682,7 @@ int main(int argc, const char* argv[]) {
// Time to set up graphics
GraphicsContext *graphicsContext = nullptr;
std::string *deviceSetting = nullptr;
void *window = nullptr;
WindowDesc windowDesc;
if (g_Config.bSoftwareRendering) {
// For software rendering, we just create a dummy graphics context (to share as much code as possible).
@@ -686,7 +694,7 @@ int main(int argc, const char* argv[]) {
return 1;
#else
// TODO: Will we need a larger window for higher resolutions? Well, not if we use buffered rendering.
windowDesc = CreateHiddenWindow(480, 272, cmdLineOptions.gpuBackend.value());
window = CreateHiddenWindow(480, 272, cmdLineOptions.gpuBackend.value(), &windowDesc);
if (!windowDesc.Valid()) {
fprintf(stderr, "Failed to create a window for graphics context");
return 1;
@@ -811,8 +819,8 @@ int main(int argc, const char* argv[]) {
#if PPSSPP_PLATFORM(ANDROID) || PPSSPP_ARCH(LOONGARCH64)
// ... see above
#else
if (windowDesc.Valid()) {
DestroyHiddenWindow(windowDesc);
if (window) {
DestroyHiddenWindow(window, windowDesc);
}
#endif
+24 -1
View File
@@ -338,6 +338,14 @@
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\SDL\SDLUtil.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\Windows\CaptureDevice.cpp" />
<ClCompile Include="..\Windows\GPU\D3D11Context.cpp" />
<ClCompile Include="..\Windows\GPU\WindowsGLContext.cpp">
@@ -399,8 +407,23 @@
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\Common\GPU\Vulkan\VulkanGraphicsContext.h" />
<ClInclude Include="..\SDL\SDLUtil.h">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
</ClInclude>
<ClInclude Include="Compare.h" />
<ClInclude Include="SDLHeadlessGLGraphicsContext.h" />
<ClInclude Include="SDLHeadlessGLGraphicsContext.h">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
</ClInclude>
<ClInclude Include="WindowsHeadlessHost.h" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
+6
View File
@@ -25,6 +25,9 @@
<ClCompile Include="..\Common\GPU\Vulkan\VulkanGraphicsContext.cpp">
<Filter>Windows</Filter>
</ClCompile>
<ClCompile Include="..\SDL\SDLUtil.cpp">
<Filter>Other Platforms</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<None Include="..\test.py" />
@@ -41,6 +44,9 @@
<ClInclude Include="..\Common\GPU\Vulkan\VulkanGraphicsContext.h">
<Filter>Windows</Filter>
</ClInclude>
<ClInclude Include="..\SDL\SDLUtil.h">
<Filter>Other Platforms</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<Filter Include="Windows">
+25 -11
View File
@@ -34,10 +34,11 @@
#include "Core/Config.h"
#include "Core/System.h"
#include "GPU/GPUState.h"
#include "SDL/SDLUtil.h"
const bool WINDOW_VISIBLE = false;
WindowDesc CreateHiddenWindow(int w, int h, GPUBackend backend) {
void *CreateHiddenWindow(int w, int h, GPUBackend backend, WindowDesc *desc) {
Uint32 flags = SDL_WINDOW_BORDERLESS;
if (backend == GPUBackend::OPENGL) {
flags |= SDL_WINDOW_OPENGL;
@@ -47,20 +48,33 @@ WindowDesc CreateHiddenWindow(int w, int h, GPUBackend backend) {
if (!WINDOW_VISIBLE) {
flags |= SDL_WINDOW_HIDDEN;
}
WindowDesc desc;
desc.data2 = SDL_CreateWindow("PPSSPPHeadless", w, h, flags);
desc.winsys = WindowSystem::WINDOWSYSTEM_SDL;
if (!desc.data2) {
SDL_Window *window = SDL_CreateWindow("PPSSPPHeadless", w, h, flags);
if (!window) {
const char *err = SDL_GetError();
printf("Failed to create offscreen window: %s\n", err ? err : "(unknown error)");
return {};
fprintf(stderr, "Failed to create offscreen window: %s\n", err ? err : "(unknown error)");
return nullptr;
}
return desc;
if (backend == GPUBackend::VULKAN) {
// Overwrite the surface init params with what we need for Vulkan..
std::string errorMessage;
if (!DetermineVulkanWindowSystem(window, desc, &errorMessage)) {
fprintf(stderr, "Failed to determine Vulkan window system: %s\n", errorMessage.c_str());
SDL_DestroyWindow(window);
return nullptr;
}
} else {
desc->winsys = WindowSystem::WINDOWSYSTEM_SDL;
// For OpenGL, we just need the SDL_Window pointer.
desc->data2 = window;
}
return window;
}
void DestroyHiddenWindow(WindowDesc window) {
if (window.data2) {
SDL_DestroyWindow(static_cast<SDL_Window *>(window.data2));
void DestroyHiddenWindow(void *window, WindowDesc desc) {
if (window) {
SDL_DestroyWindow(static_cast<SDL_Window *>(window));
SDL_Quit();
}
}
+2 -2
View File
@@ -67,7 +67,7 @@ private:
SDL_GLContext glContext_;
};
WindowDesc CreateHiddenWindow(int w, int h, GPUBackend backend);
void DestroyHiddenWindow(WindowDesc window);
void *CreateHiddenWindow(int w, int h, GPUBackend backend, WindowDesc *desc);
void DestroyHiddenWindow(void *window, WindowDesc desc);
#endif
+9 -9
View File
@@ -23,7 +23,7 @@
const bool WINDOW_VISIBLE = false;
WindowDesc CreateHiddenWindow(int w, int h, GPUBackend backend) {
void *CreateHiddenWindow(int w, int h, GPUBackend backend, WindowDesc *desc) {
static WNDCLASSEX wndClass = {
sizeof(WNDCLASSEX),
CS_HREDRAW | CS_VREDRAW | CS_OWNDC,
@@ -47,15 +47,15 @@ WindowDesc CreateHiddenWindow(int w, int h, GPUBackend backend) {
ShowWindow(wnd, TRUE);
SetFocus(wnd);
}
WindowDesc desc;
desc.data1 = GetModuleHandle(NULL);
desc.data2 = wnd;
desc.winsys = WindowSystem::WINDOWSYSTEM_WIN32;
return desc;
desc->data1 = GetModuleHandle(NULL); // easy way to get the HINSTANCE.
desc->data2 = wnd;
desc->winsys = WindowSystem::WINDOWSYSTEM_WIN32;
return static_cast<void *>(wnd);
}
void DestroyHiddenWindow(WindowDesc window) {
if (window.data2) {
DestroyWindow(static_cast<HWND>(window.data2));
void DestroyHiddenWindow(void *window, WindowDesc desc) {
if (window) {
DestroyWindow(static_cast<HWND>(window));
UnregisterClass(L"PPSSPPHeadless", static_cast<HINSTANCE>(desc.data1));
}
}
+2 -2
View File
@@ -22,5 +22,5 @@
struct WindowDesc;
// Same API as for SDL
WindowDesc CreateHiddenWindow(int w, int h, GPUBackend backend);
void DestroyHiddenWindow(WindowDesc window);
void *CreateHiddenWindow(int w, int h, GPUBackend backend, WindowDesc *desc);
void DestroyHiddenWindow(void *window, WindowDesc desc);