From f7c2c55e8337cc4a30b2e02bc5c5d98a31222851 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sun, 30 Aug 2026 01:18:37 +0200 Subject: [PATCH] Vulkan: Report surface init failures instead of asserting later VulkanGraphicsContext::InitSurface() threw away VulkanContext::InitSurface()'s VkResult and carried on, so a failed surface init surfaced as _dbg_assert_(GetAvailablePresentModes().size() > 0) in the VKContext constructor rather than as a graphics error with the usual backend fallback. The vkCreate*SurfaceKHR failure path in ReinitSurface() didn't log anything either, so the assert was the only trace of it. Now ReinitSurface() logs and sets init_error_ for all three ways it can bail (surface creation, ChooseQueue, present mode enumeration), InitSurface() checks the result, and MainThreadFunc() passes the message back out instead of writing it to a local it then drops - Windows/main.cpp was reporting "Failed to initialize main thread function." to the user. Also deletes the Application on that failure path, which was leaked. --- Common/GPU/Vulkan/VulkanContext.cpp | 11 +++++++++++ Common/GPU/Vulkan/VulkanGraphicsContext.cpp | 13 ++++++++++++- Core/EmuThread.cpp | 9 ++++----- Core/EmuThread.h | 5 ++++- Windows/main.cpp | 4 ++-- headless/Headless.cpp | 8 ++++++-- 6 files changed, 39 insertions(+), 11 deletions(-) diff --git a/Common/GPU/Vulkan/VulkanContext.cpp b/Common/GPU/Vulkan/VulkanContext.cpp index 4b6604a2ef..2b1e2a6a27 100644 --- a/Common/GPU/Vulkan/VulkanContext.cpp +++ b/Common/GPU/Vulkan/VulkanContext.cpp @@ -1316,10 +1316,14 @@ VkResult VulkanContext::ReinitSurface() { } if (retval != VK_SUCCESS) { + init_error_ = StringFromFormat("Failed to create a Vulkan surface for window system %s: %s", WindowSystemToString(winsys_), VulkanResultToString(retval)); + ERROR_LOG(Log::G3D, "%s", init_error_.c_str()); return retval; } if (!ChooseQueue()) { + init_error_ = "Failed to find a Vulkan queue and surface format that can present to the window"; + ERROR_LOG(Log::G3D, "%s", init_error_.c_str()); return VK_ERROR_INITIALIZATION_FAILED; } @@ -1337,6 +1341,13 @@ VkResult VulkanContext::ReinitSurface() { res = vkGetPhysicalDeviceSurfacePresentModesKHR(physical_devices_[physical_device_], surface_, &presentModeCount, availablePresentModes_.data()); _dbg_assert_(res == VK_SUCCESS); } + if (res != VK_SUCCESS || availablePresentModes_.empty()) { + // Should be impossible - the spec guarantees at least FIFO for any surface we could create. + availablePresentModes_.clear(); + init_error_ = StringFromFormat("Failed to enumerate Vulkan present modes: %s (count %d)", VulkanResultToString(res), (int)presentModeCount); + ERROR_LOG(Log::G3D, "%s", init_error_.c_str()); + return res != VK_SUCCESS ? res : VK_ERROR_INITIALIZATION_FAILED; + } return VK_SUCCESS; } diff --git a/Common/GPU/Vulkan/VulkanGraphicsContext.cpp b/Common/GPU/Vulkan/VulkanGraphicsContext.cpp index b1ace7a65f..91242dccdb 100644 --- a/Common/GPU/Vulkan/VulkanGraphicsContext.cpp +++ b/Common/GPU/Vulkan/VulkanGraphicsContext.cpp @@ -61,6 +61,7 @@ #include "Common/GPU/Vulkan/VulkanRenderManager.h" #include "Common/GPU/Vulkan/VulkanGraphicsContext.h" #include "Common/Data/Text/Parsers.h" +#include "Common/StringUtils.h" #include "GPU/Vulkan/VulkanUtil.h" #ifdef _DEBUG @@ -119,7 +120,17 @@ bool VulkanGraphicsContext::InitAPI(void *wnd, std::string *deviceName, std::str } bool VulkanGraphicsContext::InitSurface(WindowSystem winsys, void *data1, void *data2, std::string *errorMessage) { - vulkan_->InitSurface(winsys, data1, data2); + // Don't proceed on failure - without a surface there's no present mode, no swapchain and no queue, + // so everything below would just fail in more confusing ways further down (it used to assert deep + // inside the thin3d context constructor). Let the caller fall back to another backend instead. + VkResult res = vulkan_->InitSurface(winsys, data1, data2); + if (res != VK_SUCCESS) { + *errorMessage = vulkan_->InitError(); + if (errorMessage->empty()) { + *errorMessage = StringFromFormat("Failed to initialize Vulkan surface: %s", VulkanResultToString(res)); + } + return false; + } bool useMultiThreading = g_Config.bRenderMultiThreading; if (g_Config.iInflightFrames == 1) { diff --git a/Core/EmuThread.cpp b/Core/EmuThread.cpp index 01c82f80ca..aed82b24f4 100644 --- a/Core/EmuThread.cpp +++ b/Core/EmuThread.cpp @@ -126,14 +126,13 @@ bool RunMainLoop(GraphicsContext *graphicsContext, Application *application, std } // Call InitAPI and ShutdownAPI outside this! -bool MainThreadFunc(GraphicsContext *graphicsContext, Application *application, const WindowDesc &windowDesc, std::function frame) { +bool MainThreadFunc(GraphicsContext *graphicsContext, Application *application, const WindowDesc &windowDesc, std::function frame, std::string *errorMessage) { // This is now the render thread, and will spawn the emu thread below. - std::string error_string; - bool success = graphicsContext->InitSurface(windowDesc.winsys, windowDesc.data1, windowDesc.data2, &error_string); - if (!success) { + if (!graphicsContext->InitSurface(windowDesc.winsys, windowDesc.data1, windowDesc.data2, errorMessage)) { + ERROR_LOG(Log::G3D, "MainThreadFunc: InitSurface failed: %s", errorMessage->c_str()); + delete application; return false; } - std::string errorMessage; if (graphicsContext->NeedsSeparateEmuThread()) { SetCurrentThreadName("RenderThread"); diff --git a/Core/EmuThread.h b/Core/EmuThread.h index 9da6977169..b220b0c5fe 100644 --- a/Core/EmuThread.h +++ b/Core/EmuThread.h @@ -18,6 +18,7 @@ #pragma once #include +#include #include #include "Common/System/Application.h" @@ -34,7 +35,9 @@ struct WindowDesc; // This should be used by platforms that launch a separate thread and doesn't // need to run a polling loop in it. // NOTE: Does take ownership over Application (which is just a wrapper for NativeInitGraphics/NativeShutdownGraphics/NativeFrame). -bool MainThreadFunc(GraphicsContext * graphicsContext, Application *application, const WindowDesc &windowDesc, std::function frame); +// On failure (which currently only means the graphics surface couldn't be initialized), errorMessage +// gets the reason - pass it on to the user, it's the only place it's available. +bool MainThreadFunc(GraphicsContext * graphicsContext, Application *application, const WindowDesc &windowDesc, std::function frame, std::string *errorMessage); // 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 diff --git a/Windows/main.cpp b/Windows/main.cpp index 4828013d6d..97cc7a7ba8 100644 --- a/Windows/main.cpp +++ b/Windows/main.cpp @@ -1251,8 +1251,8 @@ int WINAPI WinMain(HINSTANCE _hInstance, HINSTANCE hPrevInstance, LPSTR szCmdLin [](GraphicsContext *graphicsContext) { NativeFrame(graphicsContext); return GetUIState() != UISTATE_EXIT; - })) { - HandleGraphicsFailure("Failed to initialize main thread function."); + }, &errorMessage)) { + HandleGraphicsFailure(errorMessage); return; } graphicsContext->ShutdownAPI(); diff --git a/headless/Headless.cpp b/headless/Headless.cpp index 79e9ea0f92..647f09670c 100644 --- a/headless/Headless.cpp +++ b/headless/Headless.cpp @@ -884,10 +884,14 @@ int main(int argc, const char* argv[]) { } int retval = 0; - MainThreadFunc(graphicsContext, new HeadlessApplication(), windowDesc, [&retval, &coreParameter, &testOptions, &testFilenames](GraphicsContext *graphicsContext) { + if (!MainThreadFunc(graphicsContext, new HeadlessApplication(), windowDesc, [&retval, &coreParameter, &testOptions, &testFilenames](GraphicsContext *graphicsContext) { retval = RunTests(graphicsContext, coreParameter, testOptions, testFilenames); return false; - }); + }, &errorMessage)) { + // No fallbacks in headless - if we can't run it, we can't. Let's not get confusing. + fprintf(stderr, "Failed to initialize graphics surface: %s\n", errorMessage.c_str()); + retval = 1; + } graphicsContext->ShutdownAPI();