From fbafd3f429a882291d0f20137cbd6edf0347c4d3 Mon Sep 17 00:00:00 2001 From: Berk Date: Fri, 17 Jul 2026 18:57:10 +0300 Subject: [PATCH] [GUI] Add native Vulkan host surface support and more (#337) * [GUI] Add native Vulkan host surface support and more * reuse * [GUI] set width session menu --- REUSE.toml | 1 + src/SharpEmu.CLI/Program.cs | 175 +++-- src/SharpEmu.CLI/SharpEmu.CLI.csproj | 1 + src/SharpEmu.CLI/app.manifest | 16 + src/SharpEmu.CLI/packages.lock.json | 588 ++++++++++++++++ src/SharpEmu.Core/Cpu/CpuDispatcher.cs | 8 + .../Cpu/Native/DirectExecutionBackend.cs | 101 ++- src/SharpEmu.Core/Runtime/SharpEmuRuntime.cs | 20 + src/SharpEmu.GUI/App.axaml | 17 + src/SharpEmu.GUI/EmulatorProcess.cs | 647 +++++++++--------- src/SharpEmu.GUI/GameSurfaceHost.cs | 613 +++++++++++++++++ src/SharpEmu.GUI/GuiConsoleMirror.cs | 116 ++++ src/SharpEmu.GUI/MainWindow.axaml | 76 +- src/SharpEmu.GUI/MainWindow.axaml.cs | 625 ++++++++++++++--- src/SharpEmu.GUI/SharpEmu.GUI.csproj | 18 +- src/SharpEmu.HLE/Host/Posix/PosixHostInput.cs | 111 ++- .../Host/Windows/WindowsDualSenseReader.cs | 6 +- .../Host/Windows/WindowsHostInput.cs | 19 +- .../Host/Windows/WindowsXInputReader.cs | 6 +- src/SharpEmu.HLE/HostSessionControl.cs | 73 +- .../Kernel/KernelSemaphoreCompatExports.cs | 76 +- src/SharpEmu.Libs/VideoOut/PerfOverlay.cs | 5 +- src/SharpEmu.Libs/VideoOut/VideoOutExports.cs | 24 +- .../VideoOut/VulkanHostSurface.cs | 270 ++++++++ .../VideoOut/VulkanVideoPresenter.cs | 634 +++++++++++++++-- 25 files changed, 3661 insertions(+), 585 deletions(-) create mode 100644 src/SharpEmu.CLI/app.manifest create mode 100644 src/SharpEmu.CLI/packages.lock.json create mode 100644 src/SharpEmu.GUI/GameSurfaceHost.cs create mode 100644 src/SharpEmu.GUI/GuiConsoleMirror.cs create mode 100644 src/SharpEmu.Libs/VideoOut/VulkanHostSurface.cs diff --git a/REUSE.toml b/REUSE.toml index 528d3346..df760ef8 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -5,6 +5,7 @@ path = [ "REUSE.toml", "nuget.config", "global.json", + "**/packages.lock.json", "scripts/ps5_names.txt", "src/SharpEmu.GUI/Languages/**", "_logs/**", diff --git a/src/SharpEmu.CLI/Program.cs b/src/SharpEmu.CLI/Program.cs index 8343f619..478ff37b 100644 --- a/src/SharpEmu.CLI/Program.cs +++ b/src/SharpEmu.CLI/Program.cs @@ -69,10 +69,9 @@ internal static partial class Program } args = NormalizeInternalArguments(args, out var isMitigatedChild); - if (args.Length == 0 && !isMitigatedChild) + + if (args.Length == 0) { - // No arguments: open the desktop frontend. Any argument selects - // the classic CLI behavior below. return GuiLauncher.Run(); } @@ -228,7 +227,17 @@ internal static partial class Program return childExitCode; } - if (!TryParseArguments(args, out var ebootPath, out var runtimeOptions, out var logLevel, out var logFilePath)) + if (!TryExtractHostSurfaceArgument(args, out var emulatorArgs, out var hostSurface, out var hostSurfaceError)) + { + Console.Error.WriteLine($"[LOADER][ERROR] {hostSurfaceError}"); + return 1; + } + + HostSessionControl.SetEmbeddedHostSurface( + hostSurface?.WindowHandle ?? 0, + hostSurface?.DisplayHandle ?? 0); + + if (!TryParseArguments(emulatorArgs, out var ebootPath, out var runtimeOptions, out var logLevel, out var logFilePath)) { PrintUsage(); return 1; @@ -255,66 +264,122 @@ internal static partial class Program Console.Error.WriteLine("[DEBUG] Creating runtime..."); - using var runtime = SharpEmuRuntime.CreateDefault(runtimeOptions); - - OrbisGen2Result result; - ConsoleCancelEventHandler? cancelHandler = null; try { - cancelHandler = (_, eventArgs) => + if (hostSurface is not null && !VulkanVideoHost.TryAttachSurface(hostSurface)) { - eventArgs.Cancel = true; - VideoOutExports.NotifyHostInterrupt(); - }; - Console.CancelKeyPress += cancelHandler; + Console.Error.WriteLine("[LOADER][ERROR] The requested GUI host surface is already active."); + return 3; + } - Console.Error.WriteLine($"[DEBUG] Running: {ebootPath}"); - result = runtime.Run(ebootPath); - Console.Error.WriteLine($"[DEBUG] Result: {result}"); - } - catch (Exception ex) - { - Console.Error.WriteLine($"[DEBUG] Exception: {ex}"); - Log.Error("SharpEmu failed to run.", ex); - return 3; + 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}"); + } + catch (Exception ex) + { + Console.Error.WriteLine($"[DEBUG] Exception: {ex}"); + 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)) + { + Log.Info(runtime.LastSessionSummary); + } + + if (!string.IsNullOrWhiteSpace(runtime.LastBasicBlockTrace)) + { + Log.Info("BB trace:"); + Log.Info(runtime.LastBasicBlockTrace); + } + + if (!string.IsNullOrWhiteSpace(runtime.LastMilestoneLog)) + { + Log.Info(runtime.LastMilestoneLog); + } + + if (result != OrbisGen2Result.ORBIS_GEN2_OK && !string.IsNullOrWhiteSpace(runtime.LastExecutionDiagnostics)) + { + Log.Warn(runtime.LastExecutionDiagnostics); + } + + if (runtimeOptions.ImportTraceLimit > 0 && !string.IsNullOrWhiteSpace(runtime.LastExecutionTrace)) + { + Log.Info("Import trace:"); + Log.Info(runtime.LastExecutionTrace); + } + + return result == OrbisGen2Result.ORBIS_GEN2_OK ? 0 : 4; } finally { - if (cancelHandler is not null) + HostSessionControl.SetEmbeddedHostSurface(0); + if (hostSurface is not null) { - Console.CancelKeyPress -= cancelHandler; + VulkanVideoHost.RequestClose(); + VulkanVideoHost.DetachSurface(hostSurface); + hostSurface.Dispose(); + } + } + } + + private static bool TryExtractHostSurfaceArgument( + IReadOnlyList args, + out string[] emulatorArgs, + out VulkanHostSurface? hostSurface, + out string? error) + { + const string hostSurfacePrefix = "--host-surface="; + var remaining = new List(args.Count); + hostSurface = null; + error = null; + foreach (var argument in args) + { + if (!argument.StartsWith(hostSurfacePrefix, StringComparison.OrdinalIgnoreCase)) + { + remaining.Add(argument); + continue; + } + + if (hostSurface is not null) + { + emulatorArgs = []; + error = "more than one GUI host surface was specified"; + return false; + } + + var descriptor = argument[hostSurfacePrefix.Length..]; + if (!VulkanHostSurface.TryCreateChildProcessSurface(descriptor, out hostSurface, out error)) + { + emulatorArgs = []; + return false; } } - Log.Info($"SharpEmu execution completed. Result={result} (0x{(int)result:X8})"); - if (!string.IsNullOrWhiteSpace(runtime.LastSessionSummary)) - { - Log.Info(runtime.LastSessionSummary); - } - - if (!string.IsNullOrWhiteSpace(runtime.LastBasicBlockTrace)) - { - Log.Info("BB trace:"); - Log.Info(runtime.LastBasicBlockTrace); - } - - if (!string.IsNullOrWhiteSpace(runtime.LastMilestoneLog)) - { - Log.Info(runtime.LastMilestoneLog); - } - - if (result != OrbisGen2Result.ORBIS_GEN2_OK && !string.IsNullOrWhiteSpace(runtime.LastExecutionDiagnostics)) - { - Log.Warn(runtime.LastExecutionDiagnostics); - } - - if (runtimeOptions.ImportTraceLimit > 0 && !string.IsNullOrWhiteSpace(runtime.LastExecutionTrace)) - { - Log.Info("Import trace:"); - Log.Info(runtime.LastExecutionTrace); - } - - return result == OrbisGen2Result.ORBIS_GEN2_OK ? 0 : 4; + emulatorArgs = remaining.ToArray(); + return true; } private static void EnsureCliConsole() @@ -411,7 +476,9 @@ internal static partial class Program return handle != 0 && handle != -1; } - private static string[] NormalizeInternalArguments(string[] args, out bool isMitigatedChild) + private static string[] NormalizeInternalArguments( + string[] args, + out bool isMitigatedChild) { isMitigatedChild = false; var trustedMitigatedChild = string.Equals( diff --git a/src/SharpEmu.CLI/SharpEmu.CLI.csproj b/src/SharpEmu.CLI/SharpEmu.CLI.csproj index 5b9ddc84..09cce948 100644 --- a/src/SharpEmu.CLI/SharpEmu.CLI.csproj +++ b/src/SharpEmu.CLI/SharpEmu.CLI.csproj @@ -54,6 +54,7 @@ SPDX-License-Identifier: GPL-2.0-or-later ..\..\assets\images\SharpEmu.ico ..\..\assets\images\SharpEmu.ico + app.manifest diff --git a/src/SharpEmu.CLI/app.manifest b/src/SharpEmu.CLI/app.manifest new file mode 100644 index 00000000..61dfc52d --- /dev/null +++ b/src/SharpEmu.CLI/app.manifest @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/src/SharpEmu.CLI/packages.lock.json b/src/SharpEmu.CLI/packages.lock.json new file mode 100644 index 00000000..503035f7 --- /dev/null +++ b/src/SharpEmu.CLI/packages.lock.json @@ -0,0 +1,588 @@ +{ + "version": 2, + "dependencies": { + "net10.0": { + "Microsoft.NET.ILLink.Tasks": { + "type": "Direct", + "requested": "[10.0.3, )", + "resolved": "10.0.3", + "contentHash": "0B6nZyCHWXnvmlB559oduOspVdNOnpNXPjhpWVMovLPAsDVG7A4jJR9rzECf67JUzxP8/ee/wA8clwIzJcWNFA==" + }, + "Avalonia.Angle.Windows.Natives": { + "type": "Transitive", + "resolved": "2.1.25547.20250602", + "contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A==" + }, + "Avalonia.BuildServices": { + "type": "Transitive", + "resolved": "11.3.2", + "contentHash": "qHDToxto1e3hci5YqbG9n0Ty8mlp3zBUN5wT66wKqaDVzXyQ0do3EnRILd4Ke9jpvsktaPpgE0YjEk7hornryQ==" + }, + "Avalonia.FreeDesktop": { + "type": "Transitive", + "resolved": "11.3.18", + "contentHash": "aUwv8BNruRUOaUfMu4U3uibIUS60/rSHgGOhd8zBkLkpxY3JFJvgRbeq5ZzHIyKXCuKi18PO00YHAgCarp3wdw==", + "dependencies": { + "Avalonia": "11.3.18", + "Tmds.DBus.Protocol": "0.21.3" + } + }, + "Avalonia.Native": { + "type": "Transitive", + "resolved": "11.3.18", + "contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==", + "dependencies": { + "Avalonia": "11.3.18" + } + }, + "Avalonia.Remote.Protocol": { + "type": "Transitive", + "resolved": "11.3.18", + "contentHash": "vw+6ZfgTuu72dA9aVWn6u56t2nrBd5MoMU0wo/qI9XJAl/c0oYYphIvwLvJP1JorubQY4UE3d0ac8ULBhrGBiA==" + }, + "Avalonia.Skia": { + "type": "Transitive", + "resolved": "11.3.18", + "contentHash": "/B4aXmNRNjG8I5U/a1xJI+bIi0XO6DDzS3mBrIKlVnJRY2CyZiUeESRQXLnIU77Z9TvqkUROs+D47s085YjFtA==", + "dependencies": { + "Avalonia": "11.3.18", + "HarfBuzzSharp": "8.3.1.1", + "HarfBuzzSharp.NativeAssets.Linux": "8.3.1.1", + "HarfBuzzSharp.NativeAssets.WebAssembly": "8.3.1.1", + "SkiaSharp": "2.88.9", + "SkiaSharp.NativeAssets.Linux": "2.88.9", + "SkiaSharp.NativeAssets.WebAssembly": "2.88.9" + } + }, + "Avalonia.Win32": { + "type": "Transitive", + "resolved": "11.3.18", + "contentHash": "eioUHkM2PeLPETd1aEks3rvb9plbba6buIrNdrqCpwE/qgHKUjvRNBd5mUQfAbGgTLiAes524gB8uUMDhrsJVQ==", + "dependencies": { + "Avalonia": "11.3.18", + "Avalonia.Angle.Windows.Natives": "2.1.25547.20250602" + } + }, + "Avalonia.X11": { + "type": "Transitive", + "resolved": "11.3.18", + "contentHash": "m4Ki/G5Dovnq+6QzfS0iGbK8V77Q6oTjToMLOB0CxPCCrl3Oxywh6kIjuGJDPaN6kopMmjxlNShyQf+vPYL+JA==", + "dependencies": { + "Avalonia": "11.3.18", + "Avalonia.FreeDesktop": "11.3.18", + "Avalonia.Skia": "11.3.18" + } + }, + "HarfBuzzSharp": { + "type": "Transitive", + "resolved": "8.3.1.1", + "contentHash": "tLZN66oe/uiRPTZfrCU4i8ScVGwqHNh5MHrXj0yVf4l7Mz0FhTGnQ71RGySROTmdognAs0JtluHkL41pIabWuQ==", + "dependencies": { + "HarfBuzzSharp.NativeAssets.Win32": "8.3.1.1", + "HarfBuzzSharp.NativeAssets.macOS": "8.3.1.1" + } + }, + "HarfBuzzSharp.NativeAssets.Linux": { + "type": "Transitive", + "resolved": "8.3.1.1", + "contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg==" + }, + "HarfBuzzSharp.NativeAssets.macOS": { + "type": "Transitive", + "resolved": "8.3.1.1", + "contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ==" + }, + "HarfBuzzSharp.NativeAssets.WebAssembly": { + "type": "Transitive", + "resolved": "8.3.1.1", + "contentHash": "loJweK2u/mH/3C2zBa0ggJlITIszOkK64HLAZB7FUT670dTg965whLFYHDQo69NmC4+d9UN0icLC9VHidXaVCA==" + }, + "HarfBuzzSharp.NativeAssets.Win32": { + "type": "Transitive", + "resolved": "8.3.1.1", + "contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA==" + }, + "MicroCom.Runtime": { + "type": "Transitive", + "resolved": "0.11.0", + "contentHash": "MEnrZ3UIiH40hjzMDsxrTyi8dtqB5ziv3iBeeU4bXsL/7NLSal9F1lZKpK+tfBRnUoDSdtcW3KufE4yhATOMCA==" + }, + "Microsoft.DotNet.PlatformAbstractions": { + "type": "Transitive", + "resolved": "3.1.6", + "contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "9.0.9", + "contentHash": "fNGvKct2De8ghm0Bpfq0iWthtzIWabgOTi+gJhNOPhNJIowXNEUE2eZNW/zNCzrHVA3PXg2yZ+3cWZndC2IqYA==" + }, + "Silk.NET.Core": { + "type": "Transitive", + "resolved": "2.23.0", + "contentHash": "D7AT/nnwlB+4RZ84XY8QNGBZMJI5z9l4CSSETIJ1wCfRJzRt/341y3MRZ4HbnFz4r/IGaWOEZr86iE+0/65yyQ==", + "dependencies": { + "Microsoft.DotNet.PlatformAbstractions": "3.1.6", + "Microsoft.Extensions.DependencyModel": "9.0.9" + } + }, + "Silk.NET.GLFW": { + "type": "Transitive", + "resolved": "2.23.0", + "contentHash": "UIs4sH57xlPUNHQ/1bt9rymPWlGy8IMDCNv86h0iM4TOA1CkIx0XM/n/tA4AReh1zQkNrvkxPEdZ3Blvy1dyXg==", + "dependencies": { + "Silk.NET.Core": "2.23.0", + "Ultz.Native.GLFW": "3.4.0" + } + }, + "Silk.NET.Input.Common": { + "type": "Transitive", + "resolved": "2.23.0", + "contentHash": "QbJVV7kFBHEByayXCYdJtXXI9Sp4a+QAf0IdGV6uCWkFYcEmqBYW3aaNGvFOdSwTBDbHL5T/OtOCrGh4qYhk7A==", + "dependencies": { + "Silk.NET.Windowing.Common": "2.23.0" + } + }, + "Silk.NET.Input.Glfw": { + "type": "Transitive", + "resolved": "2.23.0", + "contentHash": "KGHYqsv/IQRJtD6dloYh2tN4CkaM40vxM2kj0cGKBoCQiBDYHHhJiyDTyMPx0W7Fz5IgnhnG42ELmIAa0DH69A==", + "dependencies": { + "Silk.NET.Input.Common": "2.23.0", + "Silk.NET.Windowing.Glfw": "2.23.0" + } + }, + "Silk.NET.Maths": { + "type": "Transitive", + "resolved": "2.23.0", + "contentHash": "r8PdIVzME8EH0qAgbmRPO87I4GfgR2j8TofT7EMuRJDf1QluoQwnVypDoFJjQ2ZBSRsGYk5unYxxogI05Ogsmw==" + }, + "Silk.NET.Windowing.Common": { + "type": "Transitive", + "resolved": "2.23.0", + "contentHash": "ThStSinmY9KQI8DGiF5XEhkLJVnBcgRTBTzL9ijg1wMZAYuckz7ykrNw04fjRm2Gryh6tCNGbvz2XaY0efeFzg==", + "dependencies": { + "Silk.NET.Core": "2.23.0", + "Silk.NET.Maths": "2.23.0" + } + }, + "Silk.NET.Windowing.Glfw": { + "type": "Transitive", + "resolved": "2.23.0", + "contentHash": "aYBudKmENmvLRn9p15HbdvlQTnnXskcDfTfbYwSb/4fr263rGLwYuDw/txUEc2jihHJiWCp5+75Y7z5wTJWl7g==", + "dependencies": { + "Silk.NET.GLFW": "2.23.0", + "Silk.NET.Windowing.Common": "2.23.0" + } + }, + "SkiaSharp": { + "type": "Transitive", + "resolved": "2.88.9", + "contentHash": "3MD5VHjXXieSHCleRLuaTXmL2pD0mB7CcOB1x2kA1I4bhptf4e3R27iM93264ZYuAq6mkUyX5XbcxnZvMJYc1Q==", + "dependencies": { + "SkiaSharp.NativeAssets.Win32": "2.88.9", + "SkiaSharp.NativeAssets.macOS": "2.88.9" + } + }, + "SkiaSharp.NativeAssets.Linux": { + "type": "Transitive", + "resolved": "2.88.9", + "contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==", + "dependencies": { + "SkiaSharp": "2.88.9" + } + }, + "SkiaSharp.NativeAssets.macOS": { + "type": "Transitive", + "resolved": "2.88.9", + "contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA==" + }, + "SkiaSharp.NativeAssets.WebAssembly": { + "type": "Transitive", + "resolved": "2.88.9", + "contentHash": "kt06RccBHSnAs2wDYdBSfsjIDbY3EpsOVqnlDgKdgvyuRA8ZFDaHRdWNx1VHjGgYzmnFCGiTJBnXFl5BqGwGnA==" + }, + "SkiaSharp.NativeAssets.Win32": { + "type": "Transitive", + "resolved": "2.88.9", + "contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w==" + }, + "Ultz.Native.GLFW": { + "type": "Transitive", + "resolved": "3.4.0", + "contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA==" + }, + "sharpemu.core": { + "type": "Project", + "dependencies": { + "Iced": "[1.21.0, )", + "SharpEmu.HLE": "[0.0.2-beta.2, )", + "SharpEmu.Libs": "[0.0.2-beta.2, )", + "SharpEmu.Logging": "[0.0.2-beta.2, )" + } + }, + "sharpemu.gui": { + "type": "Project", + "dependencies": { + "Avalonia": "[11.3.18, )", + "Avalonia.Desktop": "[11.3.18, )", + "Avalonia.Fonts.Inter": "[11.3.18, )", + "Avalonia.Themes.Fluent": "[11.3.18, )", + "SharpEmu.Core": "[0.0.2-beta.2, )", + "SharpEmu.Libs": "[0.0.2-beta.2, )", + "SharpEmu.Logging": "[0.0.2-beta.2, )", + "Tmds.DBus.Protocol": "[0.21.3, )" + } + }, + "sharpemu.hle": { + "type": "Project", + "dependencies": { + "SharpEmu.Logging": "[0.0.2-beta.2, )" + } + }, + "sharpemu.libs": { + "type": "Project", + "dependencies": { + "SharpEmu.HLE": "[0.0.2-beta.2, )", + "SharpEmu.ShaderCompiler": "[0.0.2-beta.2, )", + "SharpEmu.ShaderCompiler.Vulkan": "[0.0.2-beta.2, )", + "Silk.NET.Input": "[2.23.0, )", + "Silk.NET.Vulkan": "[2.23.0, )", + "Silk.NET.Vulkan.Extensions.EXT": "[2.23.0, )", + "Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )", + "Silk.NET.Windowing": "[2.23.0, )" + } + }, + "sharpemu.logging": { + "type": "Project" + }, + "sharpemu.shadercompiler": { + "type": "Project", + "dependencies": { + "SharpEmu.HLE": "[0.0.2-beta.2, )" + } + }, + "sharpemu.shadercompiler.vulkan": { + "type": "Project", + "dependencies": { + "SharpEmu.ShaderCompiler": "[0.0.2-beta.2, )" + } + }, + "Avalonia": { + "type": "CentralTransitive", + "requested": "[11.3.18, )", + "resolved": "11.3.18", + "contentHash": "2C4UxhWUObWGgYKWic1x5BMMWGJP6SElb91WeOxs+X/iR26rtkqpxFFwwo50FXS9AyYnHfk8QKXDEfe7oT/kZA==", + "dependencies": { + "Avalonia.BuildServices": "11.3.2", + "Avalonia.Remote.Protocol": "11.3.18", + "MicroCom.Runtime": "0.11.0" + } + }, + "Avalonia.Desktop": { + "type": "CentralTransitive", + "requested": "[11.3.18, )", + "resolved": "11.3.18", + "contentHash": "bilMPa5vYiis6fbNovb6esKytBnOCEGojBa1XFegLCRHCP6g6PvZwS0XF/YOAGkENRlHG8dI7lohOpQ9bIkq1g==", + "dependencies": { + "Avalonia": "11.3.18", + "Avalonia.Native": "11.3.18", + "Avalonia.Skia": "11.3.18", + "Avalonia.Win32": "11.3.18", + "Avalonia.X11": "11.3.18" + } + }, + "Avalonia.Fonts.Inter": { + "type": "CentralTransitive", + "requested": "[11.3.18, )", + "resolved": "11.3.18", + "contentHash": "27u6hB3Y2Ue586yjfeVakberY73VNQXtuKwe/P927XG1QPlhsfmOyifLHDDpSHG85Zl1x/Xv9IZ3+tk9FnjcZQ==", + "dependencies": { + "Avalonia": "11.3.18" + } + }, + "Avalonia.Themes.Fluent": { + "type": "CentralTransitive", + "requested": "[11.3.18, )", + "resolved": "11.3.18", + "contentHash": "+Q/TJoynD0zNuu5w2gD+xcTl7GNKJFxlPYAndRLs/mTDrNbbsvv/271WyIysbMPsXSjCyBDp7RCZzQkpD6x5Bg==", + "dependencies": { + "Avalonia": "11.3.18" + } + }, + "Iced": { + "type": "CentralTransitive", + "requested": "[1.21.0, )", + "resolved": "1.21.0", + "contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg==" + }, + "Silk.NET.Input": { + "type": "CentralTransitive", + "requested": "[2.23.0, )", + "resolved": "2.23.0", + "contentHash": "Xzl+tVwAp2eEd8blmGQjmJrsZPnp3PWG0KJjiAQHaY2Zr/ELVWeAROKXmZdCAvexzmte2JVGEy/dxnMycbxlpg==", + "dependencies": { + "Silk.NET.Input.Common": "2.23.0", + "Silk.NET.Input.Glfw": "2.23.0" + } + }, + "Silk.NET.Vulkan": { + "type": "CentralTransitive", + "requested": "[2.23.0, )", + "resolved": "2.23.0", + "contentHash": "3/irtlSWXZ3eTi8N6nelI6L34NTB8ZJHpqVMNzZx2aX7Ek9YEQ34NoQW8/Tljrtmkg8KRhHW8hKTEzZaKV8PgA==", + "dependencies": { + "Silk.NET.Core": "2.23.0" + } + }, + "Silk.NET.Vulkan.Extensions.EXT": { + "type": "CentralTransitive", + "requested": "[2.23.0, )", + "resolved": "2.23.0", + "contentHash": "+Oth189ksRiL6HvGCwIdnsYHawqrbO8y49u1H61z3wsfcHhQZeVDYe/wF5LD7fk3NcdgDvwFD3mLm1QWhdZySw==", + "dependencies": { + "Silk.NET.Core": "2.23.0", + "Silk.NET.Vulkan": "2.23.0" + } + }, + "Silk.NET.Vulkan.Extensions.KHR": { + "type": "CentralTransitive", + "requested": "[2.23.0, )", + "resolved": "2.23.0", + "contentHash": "uRaf4j+SmH3DumjSSSUbFg33BnsGZUyXGj93O9NgGKZSJN3OTmNmQDxRew+/KiVLcgH6qzbto8aNGZ++j9GFWg==", + "dependencies": { + "Silk.NET.Core": "2.23.0", + "Silk.NET.Vulkan": "2.23.0" + } + }, + "Silk.NET.Windowing": { + "type": "CentralTransitive", + "requested": "[2.23.0, )", + "resolved": "2.23.0", + "contentHash": "OPNPmt/lRyUKVYrFLQXVxyATqD3MKLc1iY1oKx1/2GppgmZxVZPwN12tekrQ4C7408kgB1L5JD1Wnirqqeb2kg==", + "dependencies": { + "Silk.NET.Windowing.Common": "2.23.0", + "Silk.NET.Windowing.Glfw": "2.23.0" + } + }, + "Tmds.DBus.Protocol": { + "type": "CentralTransitive", + "requested": "[0.21.3, )", + "resolved": "0.21.3", + "contentHash": "hDwB8WsQoyALQKqIbwzS68UKdlnafDm4T/DkO/JrA/YIneP/rKv96SxYPVXeh3FP4i/SXfShrYftKLtciJAIlw==" + } + }, + "net10.0/linux-x64": { + "Avalonia.Angle.Windows.Natives": { + "type": "Transitive", + "resolved": "2.1.25547.20250602", + "contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A==" + }, + "Avalonia.Native": { + "type": "Transitive", + "resolved": "11.3.18", + "contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==", + "dependencies": { + "Avalonia": "11.3.18" + } + }, + "HarfBuzzSharp.NativeAssets.Linux": { + "type": "Transitive", + "resolved": "8.3.1.1", + "contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg==" + }, + "HarfBuzzSharp.NativeAssets.macOS": { + "type": "Transitive", + "resolved": "8.3.1.1", + "contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ==" + }, + "HarfBuzzSharp.NativeAssets.Win32": { + "type": "Transitive", + "resolved": "8.3.1.1", + "contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA==" + }, + "SkiaSharp.NativeAssets.Linux": { + "type": "Transitive", + "resolved": "2.88.9", + "contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==", + "dependencies": { + "SkiaSharp": "2.88.9" + } + }, + "SkiaSharp.NativeAssets.macOS": { + "type": "Transitive", + "resolved": "2.88.9", + "contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA==" + }, + "SkiaSharp.NativeAssets.Win32": { + "type": "Transitive", + "resolved": "2.88.9", + "contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w==" + }, + "Ultz.Native.GLFW": { + "type": "Transitive", + "resolved": "3.4.0", + "contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA==" + } + }, + "net10.0/osx-arm64": { + "Avalonia.Angle.Windows.Natives": { + "type": "Transitive", + "resolved": "2.1.25547.20250602", + "contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A==" + }, + "Avalonia.Native": { + "type": "Transitive", + "resolved": "11.3.18", + "contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==", + "dependencies": { + "Avalonia": "11.3.18" + } + }, + "HarfBuzzSharp.NativeAssets.Linux": { + "type": "Transitive", + "resolved": "8.3.1.1", + "contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg==" + }, + "HarfBuzzSharp.NativeAssets.macOS": { + "type": "Transitive", + "resolved": "8.3.1.1", + "contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ==" + }, + "HarfBuzzSharp.NativeAssets.Win32": { + "type": "Transitive", + "resolved": "8.3.1.1", + "contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA==" + }, + "SkiaSharp.NativeAssets.Linux": { + "type": "Transitive", + "resolved": "2.88.9", + "contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==", + "dependencies": { + "SkiaSharp": "2.88.9" + } + }, + "SkiaSharp.NativeAssets.macOS": { + "type": "Transitive", + "resolved": "2.88.9", + "contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA==" + }, + "SkiaSharp.NativeAssets.Win32": { + "type": "Transitive", + "resolved": "2.88.9", + "contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w==" + }, + "Ultz.Native.GLFW": { + "type": "Transitive", + "resolved": "3.4.0", + "contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA==" + } + }, + "net10.0/osx-x64": { + "Avalonia.Angle.Windows.Natives": { + "type": "Transitive", + "resolved": "2.1.25547.20250602", + "contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A==" + }, + "Avalonia.Native": { + "type": "Transitive", + "resolved": "11.3.18", + "contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==", + "dependencies": { + "Avalonia": "11.3.18" + } + }, + "HarfBuzzSharp.NativeAssets.Linux": { + "type": "Transitive", + "resolved": "8.3.1.1", + "contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg==" + }, + "HarfBuzzSharp.NativeAssets.macOS": { + "type": "Transitive", + "resolved": "8.3.1.1", + "contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ==" + }, + "HarfBuzzSharp.NativeAssets.Win32": { + "type": "Transitive", + "resolved": "8.3.1.1", + "contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA==" + }, + "SkiaSharp.NativeAssets.Linux": { + "type": "Transitive", + "resolved": "2.88.9", + "contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==", + "dependencies": { + "SkiaSharp": "2.88.9" + } + }, + "SkiaSharp.NativeAssets.macOS": { + "type": "Transitive", + "resolved": "2.88.9", + "contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA==" + }, + "SkiaSharp.NativeAssets.Win32": { + "type": "Transitive", + "resolved": "2.88.9", + "contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w==" + }, + "Ultz.Native.GLFW": { + "type": "Transitive", + "resolved": "3.4.0", + "contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA==" + } + }, + "net10.0/win-x64": { + "Avalonia.Angle.Windows.Natives": { + "type": "Transitive", + "resolved": "2.1.25547.20250602", + "contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A==" + }, + "Avalonia.Native": { + "type": "Transitive", + "resolved": "11.3.18", + "contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==", + "dependencies": { + "Avalonia": "11.3.18" + } + }, + "HarfBuzzSharp.NativeAssets.Linux": { + "type": "Transitive", + "resolved": "8.3.1.1", + "contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg==" + }, + "HarfBuzzSharp.NativeAssets.macOS": { + "type": "Transitive", + "resolved": "8.3.1.1", + "contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ==" + }, + "HarfBuzzSharp.NativeAssets.Win32": { + "type": "Transitive", + "resolved": "8.3.1.1", + "contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA==" + }, + "SkiaSharp.NativeAssets.Linux": { + "type": "Transitive", + "resolved": "2.88.9", + "contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==", + "dependencies": { + "SkiaSharp": "2.88.9" + } + }, + "SkiaSharp.NativeAssets.macOS": { + "type": "Transitive", + "resolved": "2.88.9", + "contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA==" + }, + "SkiaSharp.NativeAssets.Win32": { + "type": "Transitive", + "resolved": "2.88.9", + "contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w==" + }, + "Ultz.Native.GLFW": { + "type": "Transitive", + "resolved": "3.4.0", + "contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA==" + } + } + } +} \ No newline at end of file diff --git a/src/SharpEmu.Core/Cpu/CpuDispatcher.cs b/src/SharpEmu.Core/Cpu/CpuDispatcher.cs index 02fad8cd..c7635998 100644 --- a/src/SharpEmu.Core/Cpu/CpuDispatcher.cs +++ b/src/SharpEmu.Core/Cpu/CpuDispatcher.cs @@ -693,6 +693,13 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable return _virtualMemory.TryWrite(address, buffer); } + /// + /// True when the disposed native backend left its session state alive + /// because guest workers were still executing guest code. The guest + /// address space must then stay mapped as well. + /// + internal bool NativeSessionLeaked { get; private set; } + public void Dispose() { if (_nativeCpuBackend is IDisposable disposableBackend) @@ -700,6 +707,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable disposableBackend.Dispose(); } + NativeSessionLeaked = _nativeCpuBackend is DirectExecutionBackend { GuestSessionLeaked: true }; _nativeCpuBackend = null; } } diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs index 6ebbb9c9..3ceceb1c 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs @@ -718,7 +718,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I private string? _guestThreadYieldReason; - private bool _forcedGuestExit; + private volatile bool _forcedGuestExit; private ulong _lastAvTraceRip; @@ -917,7 +917,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I private bool ActiveForcedGuestExit { - get => HasActiveExecutionThread ? _activeForcedGuestExit : _forcedGuestExit; + // Host shutdown is requested from a UI or VideoOut thread. Native guest + // workers have their own thread-local execution state, so they must also + // observe the backend-wide shutdown flag. + get => _forcedGuestExit || (HasActiveExecutionThread && _activeForcedGuestExit); set { if (HasActiveExecutionThread) @@ -3409,6 +3412,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I { returnValue = 0; error = null; + if (_forcedGuestExit) + { + error = "guest execution is shutting down"; + return false; + } if (entryPoint < 65536) { error = $"invalid guest callback entry=0x{entryPoint:X16}"; @@ -3655,6 +3663,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I out string? error) { error = null; + if (_forcedGuestExit) + { + error = "guest execution is shutting down"; + return false; + } if (continuation.Rip < 65536 || continuation.Rsp == 0) { error = $"invalid guest continuation rip=0x{continuation.Rip:X16} rsp=0x{continuation.Rsp:X16}"; @@ -4685,6 +4698,21 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I private void RunGuestThread(GuestThreadState thread, string reason) { + if (_forcedGuestExit) + { + // Host shutdown: never enter guest code again. Teardown is about to + // free trampolines and the guest address space, and it only waits + // for executors that are already inside a slice. + lock (_guestThreadGate) + { + thread.State = GuestThreadRunState.Faulted; + thread.BlockReason = "host shutdown"; + thread.HostThread = null; + thread.ExecutorActive = false; + } + return; + } + lock (_guestThreadGate) { if (!thread.ExecutorActive) @@ -5526,7 +5554,14 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I { num6 = CallNativeEntry(ptr); Console.Error.WriteLine($"[LOADER][INFO] Guest returned: {num6}"); - PumpUntilGuestThreadsIdle(context, "entry_return"); + // A host stop has already invalidated the session. Draining guest + // continuations here can re-enter a blocked HLE call after its owner + // has exited, preventing the embedded GUI from receiving its exit + // callback. + if (!ActiveForcedGuestExit) + { + PumpUntilGuestThreadsIdle(context, "entry_return"); + } } catch (AccessViolationException ex) { @@ -6179,8 +6214,68 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I [return: MarshalAs(UnmanagedType.Bool)] private static extern bool Win32CloseHandle(nint hObject); + /// + /// Set when intentionally left the native session + /// state (import trampolines, TLS, exception handlers) alive because guest + /// worker threads were still executing guest code. The owning runtime must + /// then keep the guest address space mapped as well; freeing either under a + /// running worker faults the whole process, which hosts the GUI launcher. + /// + internal bool GuestSessionLeaked { get; private set; } + + private bool WaitForGuestThreadQuiescence(TimeSpan timeout) + { + var deadline = Stopwatch.GetTimestamp() + (long)(timeout.TotalSeconds * Stopwatch.Frequency); + while (true) + { + var busyCount = 0; + string? busyName = null; + var now = Stopwatch.GetTimestamp(); + using (LockGate("WaitForGuestThreadQuiescence")) + { + foreach (var thread in _guestThreads.Values) + { + if (thread.ExecutorActive) + { + busyCount++; + busyName ??= thread.Name; + } + } + } + + if (busyCount == 0) + { + return true; + } + + if (now >= deadline) + { + Console.Error.WriteLine( + $"[LOADER][WARN] {busyCount} guest worker(s) (first: '{busyName}') did not leave guest code " + + "during shutdown; the native session state stays alive to avoid faulting them."); + return false; + } + + Thread.Sleep(5); + } + } + public unsafe void Dispose() { + // Guest workers unwind cooperatively at their next import or block + // boundary once the forced-exit flag is set. Everything freed below is + // still reachable from a worker inside guest code, so drain the + // scheduler first and leak the session rather than fault a straggler. + _forcedGuestExit = true; + StopReadyThreadDispatcher(); + StopStallWatchdog(); + if (!WaitForGuestThreadQuiescence(TimeSpan.FromSeconds(5))) + { + GuestSessionLeaked = true; + return; + } + + ClearGuestThreads(); if (ReferenceEquals(_posixSignalBackend, this)) { // The signal handlers stay installed (they chain to the previous diff --git a/src/SharpEmu.Core/Runtime/SharpEmuRuntime.cs b/src/SharpEmu.Core/Runtime/SharpEmuRuntime.cs index d773bdf0..6af1f74c 100644 --- a/src/SharpEmu.Core/Runtime/SharpEmuRuntime.cs +++ b/src/SharpEmu.Core/Runtime/SharpEmuRuntime.cs @@ -189,6 +189,16 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime Console.Error.WriteLine($"[RUNTIME] DispatchEntry returned: {result}"); Console.Error.WriteLine($"[RUNTIME] Dispatch result: {result}"); + + // Stop is a host operation, not an emulation failure. The detailed + // trace and session-summary builders can traverse a partially torn + // down native backend, delaying the GUI exit callback indefinitely. + if (HostSessionControl.IsShutdownRequested) + { + Console.Error.WriteLine("[RUNTIME] Skipping post-exit diagnostics for host shutdown."); + return result; + } + LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace; LastMilestoneLog = _cpuDispatcher.LastMilestoneLog; LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary); @@ -1152,6 +1162,16 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime disposableDispatcher.Dispose(); } + if (_cpuDispatcher is CpuDispatcher { NativeSessionLeaked: true }) + { + // A guest worker is still inside guest code; unmapping the guest + // address space under it would fault the whole process, which + // hosts the GUI launcher in embedded sessions. + Console.Error.WriteLine( + "[RUNTIME] Guest workers were still active at teardown; keeping the guest address space mapped."); + return; + } + if (_virtualMemory is IDisposable disposableMemory) { disposableMemory.Dispose(); diff --git a/src/SharpEmu.GUI/App.axaml b/src/SharpEmu.GUI/App.axaml index 78c924dd..e1e3554d 100644 --- a/src/SharpEmu.GUI/App.axaml +++ b/src/SharpEmu.GUI/App.axaml @@ -29,6 +29,7 @@ SPDX-License-Identifier: GPL-2.0-or-later + @@ -55,6 +56,22 @@ SPDX-License-Identifier: GPL-2.0-or-later + + + + +