From cc83084c94ec32a8cd9900acfa65d11d2dff5673 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Fri, 25 Sep 2026 09:06:56 -0600 Subject: [PATCH] Headless: Run Vulkan offscreen, without a window Vulkan no longer needs a hidden window (which on macOS could never work, as the Metal window description has no data2). It now uses the offscreen mode, and presents each frame like the app does, as an unpresented frame would wait forever for its next image. MoltenVK's console logging is limited to errors, as it mixed into the test output. Co-Authored-By: Claude Opus 5.5 (1M context) --- headless/Headless.cpp | 55 ++++++++++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/headless/Headless.cpp b/headless/Headless.cpp index e687ec7971..2b96974088 100644 --- a/headless/Headless.cpp +++ b/headless/Headless.cpp @@ -271,9 +271,6 @@ static GraphicsContext *CreateGraphicsContext(GPUCore gpuCore, std::string **dev switch (gpuCore) { case GPUCORE_GLES: return new SDLHeadlessGLGraphicsContext(); - case GPUCORE_VULKAN: - *deviceSetting = &g_Config.sVulkanDevice; - return new VulkanGraphicsContext(); default: return nullptr; } @@ -287,9 +284,6 @@ static GraphicsContext *CreateGraphicsContext(GPUCore gpuCore, std::string **dev case GPUCORE_DIRECTX11: *deviceSetting = &g_Config.sD3D11Device; return new D3D11Context(); - case GPUCORE_VULKAN: - *deviceSetting = &g_Config.sVulkanDevice; - return new VulkanGraphicsContext(); case GPUCORE_SOFTWARE: default: return nullptr; @@ -336,6 +330,16 @@ struct AutoTestOptions { int requiredDisableHLE; }; +// Ends a frame of the draw context the way the app does, presenting it. With Vulkan, presenting is +// also what returns a frame's image, and a frame that isn't presented would wait forever for its +// next one. The other backends present to a hidden window, which could wait for vsync. +static void EndDrawFrame(Draw::DrawContext *draw) { + draw->EndFrame(); + if (GetGPUBackend() == GPUBackend::VULKAN) { + draw->Present(Draw::PresentMode::FIFO); + } +} + static bool RunAutoTest(GraphicsContext *graphicsContext, CoreParameter &coreParameter, const AutoTestOptions &opt) { using namespace Draw; @@ -469,7 +473,7 @@ static bool RunAutoTest(GraphicsContext *graphicsContext, CoreParameter &corePar gpu->EndHostFrame(); } if (draw) { - draw->EndFrame(); + EndDrawFrame(draw); draw->BeginFrame(Draw::DebugFlags::NONE); } if (gpu) { @@ -515,7 +519,7 @@ static bool RunAutoTest(GraphicsContext *graphicsContext, CoreParameter &corePar gpu->CopyDisplayToOutput(g_Config.GetDisplayLayoutConfig(DeviceOrientation::Landscape)); } - draw->EndFrame(); + EndDrawFrame(draw); } if (!g_screenshotSavePath.empty() && !g_screenshotSaved) { @@ -682,6 +686,11 @@ int main(int argc, const char* argv[]) { perror("Unable to ignore SIGPIPE"); } #endif +#if PPSSPP_PLATFORM(MAC) + // MoltenVK logs its setup and every unsupported feature to the console, which mixes into the + // test output. Only its errors, unless asked for more. + setenv("MVK_CONFIG_LOG_LEVEL", "1", 0); +#endif SetupCRT(true); @@ -949,17 +958,25 @@ int main(int argc, const char* argv[]) { fprintf(stderr, "Headless graphics context creation is not supported on this platform.\n"); return 1; #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); - if (!windowDesc.Valid()) { - fprintf(stderr, "Failed to create a window for graphics context"); - return 1; - } - graphicsContext = CreateGraphicsContext(gpuCore, &deviceSetting); - if (!graphicsContext) { - // If we don't get the desired context, we DO NOT fall back. - fprintf(stderr, "Failed to create a graphics context for GPU core"); - return 1; + if (gpuCore == GPUCORE_VULKAN) { + // Vulkan renders into images of its own, with no window or swapchain. + VulkanGraphicsContext *vulkanContext = new VulkanGraphicsContext(); + vulkanContext->SetOffscreen(480, 272); + graphicsContext = vulkanContext; + deviceSetting = &g_Config.sVulkanDevice; + } 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); + if (!windowDesc.Valid()) { + fprintf(stderr, "Failed to create a window for graphics context\n"); + return 1; + } + graphicsContext = CreateGraphicsContext(gpuCore, &deviceSetting); + if (!graphicsContext) { + // If we don't get the desired context, we DO NOT fall back. + fprintf(stderr, "Failed to create a graphics context for GPU core\n"); + return 1; + } } #endif }