Fix guest shutdown when VideoOut window is closed (#184)

Propagate Silk window close to runtime teardown so audio and CPU workers stop instead of continuing after the presentation window is dismissed.
This commit is contained in:
Mike Saito
2026-07-15 00:52:01 +03:00
committed by GitHub
parent d2f3511002
commit caf859cc52
7 changed files with 191 additions and 4 deletions
+16
View File
@@ -5,6 +5,7 @@ using SharpEmu.Core.Runtime;
using SharpEmu.Core.Cpu; using SharpEmu.Core.Cpu;
using SharpEmu.GUI; using SharpEmu.GUI;
using SharpEmu.HLE; using SharpEmu.HLE;
using SharpEmu.Libs.VideoOut;
using SharpEmu.Logging; using SharpEmu.Logging;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
@@ -111,8 +112,16 @@ internal static partial class Program
using var runtime = SharpEmuRuntime.CreateDefault(runtimeOptions); using var runtime = SharpEmuRuntime.CreateDefault(runtimeOptions);
OrbisGen2Result result; OrbisGen2Result result;
ConsoleCancelEventHandler? cancelHandler = null;
try try
{ {
cancelHandler = (_, eventArgs) =>
{
eventArgs.Cancel = true;
VideoOutExports.NotifyHostInterrupt();
};
Console.CancelKeyPress += cancelHandler;
Console.Error.WriteLine($"[DEBUG] Running: {ebootPath}"); Console.Error.WriteLine($"[DEBUG] Running: {ebootPath}");
result = runtime.Run(ebootPath); result = runtime.Run(ebootPath);
Console.Error.WriteLine($"[DEBUG] Result: {result}"); Console.Error.WriteLine($"[DEBUG] Result: {result}");
@@ -123,6 +132,13 @@ internal static partial class Program
Log.Error("SharpEmu failed to run.", ex); Log.Error("SharpEmu failed to run.", ex);
return 3; return 3;
} }
finally
{
if (cancelHandler is not null)
{
Console.CancelKeyPress -= cancelHandler;
}
}
Log.Info($"SharpEmu execution completed. Result={result} (0x{(int)result:X8})"); Log.Info($"SharpEmu execution completed. Result={result} (0x{(int)result:X8})");
if (!string.IsNullOrWhiteSpace(runtime.LastSessionSummary)) if (!string.IsNullOrWhiteSpace(runtime.LastSessionSummary))
@@ -238,6 +238,22 @@ public sealed partial class DirectExecutionBackend
cpuContext[CpuRegister.Rax] = 0uL; cpuContext[CpuRegister.Rax] = 0uL;
return 0uL; return 0uL;
} }
if (_hostShutdownRequested)
{
if (isGuestWorker &&
TryYieldGuestThreadToHostStub(argPackPtr, num, num7, importStubEntry.Nid, "host shutdown"))
{
cpuContext[CpuRegister.Rax] = 0uL;
return 0uL;
}
if (!isGuestWorker &&
TryAbortGuestForHostShutdown(argPackPtr, num, num7))
{
cpuContext[CpuRegister.Rax] = 1uL;
return 1uL;
}
}
bool flag0 = ShouldSuppressStrlenTrace(importStubEntry.Nid); bool flag0 = ShouldSuppressStrlenTrace(importStubEntry.Nid);
bool flag = num7 >= 2156221920u && num7 <= 2156225024u; bool flag = num7 >= 2156221920u && num7 <= 2156225024u;
bool flag2 = num7 >= 2156351360u && num7 <= 2156352080u; bool flag2 = num7 >= 2156351360u && num7 <= 2156352080u;
@@ -975,6 +991,31 @@ public sealed partial class DirectExecutionBackend
return true; return true;
} }
private unsafe bool TryAbortGuestForHostShutdown(nint argPackPtr, long dispatchIndex, ulong returnRip)
{
ulong hostExit = ActiveEntryReturnSentinelRip;
if (hostExit < 65536 || !TryPatchActiveGuestReturnSlot(hostExit))
{
return false;
}
try
{
*(ulong*)(argPackPtr + 96) = hostExit;
}
catch
{
return false;
}
ActiveForcedGuestExit = true;
if (string.IsNullOrWhiteSpace(LastError))
{
LastError = "Host shutdown requested.";
}
Console.Error.WriteLine(
$"[LOADER][INFO] Guest unwind for host shutdown at import#{dispatchIndex} ret=0x{returnRip:X16} -> host_exit=0x{hostExit:X16}");
return true;
}
private unsafe bool TryCompleteGuestEntryToHostStub(nint argPackPtr, long dispatchIndex, ulong returnRip, string nid, string reason, ulong value) private unsafe bool TryCompleteGuestEntryToHostStub(nint argPackPtr, long dispatchIndex, ulong returnRip, string nid, string reason, ulong value)
{ {
ulong hostExit = ActiveEntryReturnSentinelRip; ulong hostExit = ActiveEntryReturnSentinelRip;
@@ -576,6 +576,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
// thread entering a freed stub). // thread entering a freed stub).
private volatile bool _guestTeardownRequested; private volatile bool _guestTeardownRequested;
private volatile bool _hostShutdownRequested;
private static volatile DirectExecutionBackend? _activeSessionBackend;
private int _readyGuestThreadCount; private int _readyGuestThreadCount;
private readonly Dictionary<ulong, GuestThreadState> _guestThreads = new Dictionary<ulong, GuestThreadState>(); private readonly Dictionary<ulong, GuestThreadState> _guestThreads = new Dictionary<ulong, GuestThreadState>();
@@ -812,7 +816,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
private bool ActiveForcedGuestExit private bool ActiveForcedGuestExit
{ {
get => HasActiveExecutionThread ? _activeForcedGuestExit : _forcedGuestExit; get => _hostShutdownRequested ||
(HasActiveExecutionThread ? _activeForcedGuestExit : _forcedGuestExit);
set set
{ {
if (HasActiveExecutionThread) if (HasActiveExecutionThread)
@@ -974,7 +979,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
_importLoopGuardSeconds = GetImportLoopGuardSeconds(); _importLoopGuardSeconds = GetImportLoopGuardSeconds();
_entryReturnSentinelRip = 0uL; _entryReturnSentinelRip = 0uL;
_forcedGuestExit = false; _forcedGuestExit = false;
_hostShutdownRequested = false;
_guestTeardownRequested = false; _guestTeardownRequested = false;
_activeSessionBackend = this;
HostSessionControl.SetShutdownHandler(RequestHostShutdown);
_importLoopSignatureCount = 0; _importLoopSignatureCount = 0;
_importLoopSignatureWriteIndex = 0; _importLoopSignatureWriteIndex = 0;
_importLoopPatternHits = 0; _importLoopPatternHits = 0;
@@ -1022,12 +1030,28 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
} }
finally finally
{ {
HostSessionControl.SetShutdownHandler(null);
if (ReferenceEquals(_activeSessionBackend, this))
{
_activeSessionBackend = null;
}
DrainDeferredBootstrapTraces(); DrainDeferredBootstrapTraces();
GuestThreadExecution.Scheduler = previousGuestThreadScheduler; GuestThreadExecution.Scheduler = previousGuestThreadScheduler;
Console.Error.WriteLine("[LOADER][INFO] === Execute END (LastError: " + (LastError ?? "null") + ") ==="); Console.Error.WriteLine("[LOADER][INFO] === Execute END (LastError: " + (LastError ?? "null") + ") ===");
} }
} }
internal void RequestHostShutdown(string reason)
{
_hostShutdownRequested = true;
_forcedGuestExit = true;
_guestTeardownRequested = true;
LastError = string.IsNullOrWhiteSpace(reason)
? "Host shutdown requested."
: $"Host shutdown requested: {reason}";
Console.Error.WriteLine($"[LOADER][INFO] {LastError}");
}
private bool SetupImportStubs(IReadOnlyDictionary<ulong, string> importStubs) private bool SetupImportStubs(IReadOnlyDictionary<ulong, string> importStubs)
{ {
Console.Error.WriteLine($"[LOADER][INFO] Setting up {importStubs.Count} import stubs..."); Console.Error.WriteLine($"[LOADER][INFO] Setting up {importStubs.Count} import stubs...");
@@ -4425,7 +4449,9 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
result = OrbisGen2Result.ORBIS_GEN2_ERROR_CPU_TRAP; result = OrbisGen2Result.ORBIS_GEN2_ERROR_CPU_TRAP;
if (string.IsNullOrEmpty(LastError)) if (string.IsNullOrEmpty(LastError))
{ {
LastError = "Detected repeating import loop and forced guest unwind to host."; LastError = _hostShutdownRequested
? "Host shutdown requested."
: "Detected repeating import loop and forced guest unwind to host.";
} }
Console.Error.WriteLine("[LOADER][ERROR] " + LastError); Console.Error.WriteLine("[LOADER][ERROR] " + LastError);
RequestGuestThreadTeardown(3000); RequestGuestThreadTeardown(3000);
+31
View File
@@ -0,0 +1,31 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE;
/// <summary>
/// Lets host-facing libraries (VideoOut, AudioOut) request cooperative guest
/// shutdown without taking a dependency on SharpEmu.Core.
/// </summary>
public static class HostSessionControl
{
private static Action<string>? _shutdownHandler;
public static void SetShutdownHandler(Action<string>? handler)
{
Volatile.Write(ref _shutdownHandler, handler);
}
public static void RequestShutdown(string reason)
{
try
{
Volatile.Read(ref _shutdownHandler)?.Invoke(reason);
}
catch (Exception exception)
{
Console.Error.WriteLine(
$"[LOADER][WARN] Host shutdown handler failed: {exception.Message}");
}
}
}
@@ -213,6 +213,17 @@ public static class AudioOutExports
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
public static void ShutdownAllPorts()
{
foreach (var handle in Ports.Keys)
{
if (Ports.TryRemove(handle, out var port))
{
port.Dispose();
}
}
}
private static bool TryGetFormat( private static bool TryGetFormat(
int rawFormat, int rawFormat,
out int channels, out int channels,
+57 -1
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE; using SharpEmu.HLE;
using SharpEmu.Libs.Audio;
using SharpEmu.Libs.Kernel; using SharpEmu.Libs.Kernel;
using SharpEmu.Logging; using SharpEmu.Logging;
using System.Buffers.Binary; using System.Buffers.Binary;
@@ -52,6 +53,8 @@ public static class VideoOutExports
private const int VblankWaitTimeoutMilliseconds = 100; private const int VblankWaitTimeoutMilliseconds = 100;
private static Thread? _vblankPumpThread; private static Thread? _vblankPumpThread;
private static int _vblankPumpStarted; private static int _vblankPumpStarted;
private static volatile int _vblankPumpStopRequested;
private static volatile int _presentationWindowCloseNotified;
private static readonly object _vblankEdgeGate = new(); private static readonly object _vblankEdgeGate = new();
private static ulong _vblankEdgeSequence; private static ulong _vblankEdgeSequence;
@@ -80,7 +83,7 @@ public static class VideoOutExports
var intervalTicks = Math.Max(1L, (long)(Stopwatch.Frequency / VblankHz)); var intervalTicks = Math.Max(1L, (long)(Stopwatch.Frequency / VblankHz));
var nextEdge = Stopwatch.GetTimestamp() + intervalTicks; var nextEdge = Stopwatch.GetTimestamp() + intervalTicks;
while (true) while (_vblankPumpStopRequested == 0)
{ {
WaitUntilTimestamp(nextEdge); WaitUntilTimestamp(nextEdge);
PumpVblanks(); PumpVblanks();
@@ -97,6 +100,59 @@ public static class VideoOutExports
} }
} }
public static void NotifyPresentationWindowClosed()
{
if (Interlocked.Exchange(ref _presentationWindowCloseNotified, 1) != 0)
{
return;
}
Console.Error.WriteLine("[LOADER][INFO] VideoOut presentation window closed");
RequestHostShutdown("videoout-window-closed");
}
public static void NotifyHostInterrupt()
{
if (Interlocked.Exchange(ref _presentationWindowCloseNotified, 1) != 0)
{
return;
}
Console.Error.WriteLine("[LOADER][INFO] Host interrupt requested");
RequestHostShutdown("host-interrupt");
}
private static void RequestHostShutdown(string reason)
{
AudioOutExports.ShutdownAllPorts();
StopVblankPump();
HostSessionControl.RequestShutdown(reason);
ScheduleProcessExitIfGuestDoesNotStop();
}
private static void ScheduleProcessExitIfGuestDoesNotStop()
{
ThreadPool.QueueUserWorkItem(static _ =>
{
Thread.Sleep(2000);
Environment.Exit(0);
});
}
public static void StopVblankPump()
{
if (Interlocked.Exchange(ref _vblankPumpStopRequested, 1) != 0)
{
return;
}
var thread = _vblankPumpThread;
if (thread is { IsAlive: true })
{
thread.Join(TimeSpan.FromSeconds(2));
}
}
private static void WaitUntilTimestamp(long deadlineTicks) private static void WaitUntilTimestamp(long deadlineTicks)
{ {
var spinThresholdTicks = Stopwatch.Frequency * 2L / 1000L; var spinThresholdTicks = Stopwatch.Frequency * 2L / 1000L;
@@ -1154,7 +1154,13 @@ internal static unsafe class VulkanVideoPresenter
_window = Window.Create(options); _window = Window.Create(options);
_window.Load += Initialize; _window.Load += Initialize;
_window.Render += Render; _window.Render += Render;
_window.Closing += DisposeVulkan; _window.Closing += OnWindowClosing;
}
private void OnWindowClosing()
{
VideoOutExports.NotifyPresentationWindowClosed();
DisposeVulkan();
} }
public void Run() => _window.Run(); public void Run() => _window.Run();