diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs index b7a4684e..5c522cb9 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs @@ -512,6 +512,10 @@ internal static unsafe class VulkanVideoPresenter private static uint _windowHeight; private static bool _closed; private static bool _presenterCloseRequested; + // Set once the render thread finishes Vulkan init and begins consuming the + // guest-work queue. Guest GPU work enqueued before this point would sit + // unexecuted behind CreateSwapchain, so ordered submissions wait on it. + private static bool _presenterReady; private const string DebugUtilsExtensionName = "VK_EXT_debug_utils"; private const uint NvidiaVendorId = 0x10DE; private const uint AmdVendorId = 0x1002; @@ -737,6 +741,7 @@ internal static unsafe class VulkanVideoPresenter _hostSurface = surface; _closed = false; _presenterCloseRequested = false; + _presenterReady = false; return true; } } @@ -1428,12 +1433,77 @@ internal static unsafe class VulkanVideoPresenter ArgumentNullException.ThrowIfNull(action); lock (_gate) { - return _closed || _thread is null + return _closed || _thread is null || !WaitForPresenterReadyLocked() ? 0 : EnqueueGuestWorkLocked(new VulkanOrderedGuestAction(action, debugName)); } } + /// + /// Marks the guest-work consumer as live. Called by the render thread once + /// Vulkan init completes and before it starts draining the queue. + /// + private static void SignalPresenterReady() + { + lock (_gate) + { + _presenterReady = true; + System.Threading.Monitor.PulseAll(_gate); + } + } + + // How long a guest submission may wait for the presenter to come up before + // giving up and letting the caller run its side effect inline. Vulkan init + // (device + swapchain against an embedded host window) can take seconds on + // a cold start; a guest that submits GPU work in that window would + // otherwise queue label writes nobody executes, and titles that poll those + // labels declare a GPU hang and abort. Waiting is correct: real hardware + // cannot accept submissions before the GPU exists either. + private static readonly int _presenterReadyTimeoutMs = + int.TryParse( + Environment.GetEnvironmentVariable("SHARPEMU_PRESENTER_READY_TIMEOUT_MS"), + out var configuredTimeout) && configuredTimeout >= 0 + ? configuredTimeout + : 10_000; + + private static bool _presenterReadyTimeoutLogged; + + /// + /// Blocks the calling guest thread until the presenter is consuming guest + /// work. Returns false if the presenter closed or failed to come up in + /// time, in which case callers fall back to running the action inline. + /// + private static bool WaitForPresenterReadyLocked() + { + if (_presenterReady) + { + return true; + } + + var deadline = Environment.TickCount64 + _presenterReadyTimeoutMs; + while (!_presenterReady && !_closed && _thread is not null) + { + var remaining = deadline - Environment.TickCount64; + if (remaining <= 0) + { + if (!_presenterReadyTimeoutLogged) + { + _presenterReadyTimeoutLogged = true; + Console.Error.WriteLine( + "[LOADER][WARN] Vulkan VideoOut presenter not ready after " + + $"{_presenterReadyTimeoutMs} ms; running guest GPU side effects inline. " + + "Cross-queue ordering is not guaranteed for these submissions."); + } + + return false; + } + + System.Threading.Monitor.Wait(_gate, (int)Math.Min(remaining, 100)); + } + + return _presenterReady && !_closed; + } + /// /// Sequence currently being executed by the single guest-work consumer. /// Intended only for address-filtered lifetime diagnostics emitted from a @@ -1621,7 +1691,7 @@ internal static unsafe class VulkanVideoPresenter out var lastVersion) ? lastVersion : 0; - return _closed || _thread is null + return _closed || _thread is null || !WaitForPresenterReadyLocked() ? 0 : EnqueueGuestWorkLocked( new VulkanOrderedGuestFlipWait( @@ -2157,6 +2227,7 @@ internal static unsafe class VulkanVideoPresenter { _closed = true; _thread = null; + _presenterReady = false; if (_hostSurfacePendingDetach is not null && ReferenceEquals(_hostSurface, _hostSurfacePendingDetach)) { @@ -3209,6 +3280,7 @@ internal static unsafe class VulkanVideoPresenter CreateCommandResources(); CreateGuestDrawResources(); _vulkanReady = true; + SignalPresenterReady(); Console.Error.WriteLine( $"[LOADER][INFO] Vulkan VideoOut ready: {_extent.Width}x{_extent.Height}, format={_swapchainFormat}"); }