diff --git a/src/SharpEmu.CLI/Program.cs b/src/SharpEmu.CLI/Program.cs index 39b32212..d47ca78b 100644 --- a/src/SharpEmu.CLI/Program.cs +++ b/src/SharpEmu.CLI/Program.cs @@ -5,6 +5,7 @@ using SharpEmu.Core.Runtime; using SharpEmu.Core.Cpu; using SharpEmu.GUI; using SharpEmu.HLE; +using SharpEmu.Libs.VideoOut; using SharpEmu.Logging; using System.Runtime.InteropServices; using System.Text; @@ -111,8 +112,16 @@ internal static partial class Program using var runtime = SharpEmuRuntime.CreateDefault(runtimeOptions); OrbisGen2Result result; + ConsoleCancelEventHandler? cancelHandler = null; try { + cancelHandler = (_, eventArgs) => + { + eventArgs.Cancel = true; + VideoOutExports.NotifyHostInterrupt(); + }; + Console.CancelKeyPress += cancelHandler; + Console.Error.WriteLine($"[DEBUG] Running: {ebootPath}"); result = runtime.Run(ebootPath); Console.Error.WriteLine($"[DEBUG] Result: {result}"); @@ -123,6 +132,13 @@ internal static partial class Program Log.Error("SharpEmu failed to run.", ex); return 3; } + finally + { + if (cancelHandler is not null) + { + Console.CancelKeyPress -= cancelHandler; + } + } Log.Info($"SharpEmu execution completed. Result={result} (0x{(int)result:X8})"); if (!string.IsNullOrWhiteSpace(runtime.LastSessionSummary)) diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs index fcf1c436..5a26d07f 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs @@ -238,6 +238,22 @@ public sealed partial class DirectExecutionBackend cpuContext[CpuRegister.Rax] = 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 flag = num7 >= 2156221920u && num7 <= 2156225024u; bool flag2 = num7 >= 2156351360u && num7 <= 2156352080u; @@ -975,6 +991,31 @@ public sealed partial class DirectExecutionBackend 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) { ulong hostExit = ActiveEntryReturnSentinelRip; diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs index 65f997c8..05f0c10b 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs @@ -576,6 +576,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I // thread entering a freed stub). private volatile bool _guestTeardownRequested; + private volatile bool _hostShutdownRequested; + + private static volatile DirectExecutionBackend? _activeSessionBackend; + private int _readyGuestThreadCount; private readonly Dictionary _guestThreads = new Dictionary(); @@ -812,7 +816,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I private bool ActiveForcedGuestExit { - get => HasActiveExecutionThread ? _activeForcedGuestExit : _forcedGuestExit; + get => _hostShutdownRequested || + (HasActiveExecutionThread ? _activeForcedGuestExit : _forcedGuestExit); set { if (HasActiveExecutionThread) @@ -974,7 +979,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I _importLoopGuardSeconds = GetImportLoopGuardSeconds(); _entryReturnSentinelRip = 0uL; _forcedGuestExit = false; + _hostShutdownRequested = false; _guestTeardownRequested = false; + _activeSessionBackend = this; + HostSessionControl.SetShutdownHandler(RequestHostShutdown); _importLoopSignatureCount = 0; _importLoopSignatureWriteIndex = 0; _importLoopPatternHits = 0; @@ -1022,12 +1030,28 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I } finally { + HostSessionControl.SetShutdownHandler(null); + if (ReferenceEquals(_activeSessionBackend, this)) + { + _activeSessionBackend = null; + } DrainDeferredBootstrapTraces(); GuestThreadExecution.Scheduler = previousGuestThreadScheduler; 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 importStubs) { 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; 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); RequestGuestThreadTeardown(3000); diff --git a/src/SharpEmu.HLE/HostSessionControl.cs b/src/SharpEmu.HLE/HostSessionControl.cs new file mode 100644 index 00000000..133a6f19 --- /dev/null +++ b/src/SharpEmu.HLE/HostSessionControl.cs @@ -0,0 +1,31 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +namespace SharpEmu.HLE; + +/// +/// Lets host-facing libraries (VideoOut, AudioOut) request cooperative guest +/// shutdown without taking a dependency on SharpEmu.Core. +/// +public static class HostSessionControl +{ + private static Action? _shutdownHandler; + + public static void SetShutdownHandler(Action? 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}"); + } + } +} diff --git a/src/SharpEmu.Libs/Audio/AudioOutExports.cs b/src/SharpEmu.Libs/Audio/AudioOutExports.cs index 98da34ac..b2b3f307 100644 --- a/src/SharpEmu.Libs/Audio/AudioOutExports.cs +++ b/src/SharpEmu.Libs/Audio/AudioOutExports.cs @@ -213,6 +213,17 @@ public static class AudioOutExports : (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( int rawFormat, out int channels, diff --git a/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs b/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs index 3775dda3..05dffd18 100644 --- a/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs +++ b/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later using SharpEmu.HLE; +using SharpEmu.Libs.Audio; using SharpEmu.Libs.Kernel; using SharpEmu.Logging; using System.Buffers.Binary; @@ -52,6 +53,8 @@ public static class VideoOutExports private const int VblankWaitTimeoutMilliseconds = 100; private static Thread? _vblankPumpThread; private static int _vblankPumpStarted; + private static volatile int _vblankPumpStopRequested; + private static volatile int _presentationWindowCloseNotified; private static readonly object _vblankEdgeGate = new(); private static ulong _vblankEdgeSequence; @@ -80,7 +83,7 @@ public static class VideoOutExports var intervalTicks = Math.Max(1L, (long)(Stopwatch.Frequency / VblankHz)); var nextEdge = Stopwatch.GetTimestamp() + intervalTicks; - while (true) + while (_vblankPumpStopRequested == 0) { WaitUntilTimestamp(nextEdge); 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) { var spinThresholdTicks = Stopwatch.Frequency * 2L / 1000L; diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs index 54126750..ac3d89b5 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs @@ -1154,7 +1154,13 @@ internal static unsafe class VulkanVideoPresenter _window = Window.Create(options); _window.Load += Initialize; _window.Render += Render; - _window.Closing += DisposeVulkan; + _window.Closing += OnWindowClosing; + } + + private void OnWindowClosing() + { + VideoOutExports.NotifyPresentationWindowClosed(); + DisposeVulkan(); } public void Run() => _window.Run();