UI: LoadGuestApplication asynchronous cancellation

Fixed LoadGuestApplication hanging when cancelled.
Since startup procedure has technically changed, we should consider testing this with a variety of game formats to ensure regressions do not occur.
This commit is contained in:
Max
2026-05-04 01:48:31 -05:00
committed by KeatonTheBot
parent 7f99e32a0a
commit 4b4fd560ed
6 changed files with 112 additions and 44 deletions
+63 -32
View File
@@ -4,6 +4,8 @@ using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Input;
using Avalonia.Threading;
using Gommon;
using LibHac.Common;
using LibHac.Ns;
using LibHac.Tools.FsSystem;
using Ryujinx.Audio.Backends.Apple;
using Ryujinx.Audio.Backends.Dummy;
@@ -64,7 +66,7 @@ using VSyncMode = Ryujinx.Common.Configuration.VSyncMode;
namespace Ryujinx.Ava
{
internal class AppHost
internal class AppHost : IDisposable
{
private const int CursorHideIdleTime = 5; // Hide Cursor seconds.
private const float MaxResolutionScale = 4.0f; // Max resolution hotkeys can scale to before wrapping.
@@ -453,7 +455,7 @@ namespace Ryujinx.Ava
SaveBitmapAsPng(bitmapToSave, path);
Logger.Notice.Print(LogClass.Application, $"Screenshot saved to {path}", "Screenshot");
Logger.Notice.Print(LogClass.Application, $"Screenshot saved to '{path}'.", "Screenshot");
}
});
}
@@ -592,27 +594,37 @@ namespace Ryujinx.Ava
_isActive = false;
// NOTE: The render loop is allowed to stay alive until the renderer itself is disposed, as it may handle resource dispose.
// We only need to wait for all commands submitted during the main gpu loop to be processed.
_gpuDoneEvent.WaitOne();
_gpuDoneEvent.Dispose();
DisplaySleep.Restore();
NpadManager.Dispose();
TouchScreenManager.Dispose();
Device.Dispose();
DisposeGpu();
// NOTE: The render loop is allowed to stay alive until the renderer itself is disposed, as it may handle resource dispose.
// We only need to wait for all commands submitted during the main gpu loop to be processed.
// If the GPU has no work and is cancelled, we need to handle that as well.
WaitHandle.WaitAny(new[] { _gpuDoneEvent, _gpuCancellationTokenSource.Token.WaitHandle });
_gpuCancellationTokenSource.Dispose();
// Waiting for work to be finished before we dispose.
if (_renderingStarted)
{
Device.Gpu.WaitUntilGpuReady();
}
_gpuDoneEvent.Dispose();
DisposeGpu();
AppExit?.Invoke(this, EventArgs.Empty);
}
private void Dispose()
// MUST be public to inherit from IDisposable
public void Dispose()
{
if (Device.Processes != null)
{
MainWindowViewModel.UpdateGameMetadata(Device.Processes.ActiveApplication.ProgramIdText);
MainWindowViewModel.UpdateGameMetadata(Device.Processes.ActiveApplication?.ProgramIdText);
}
ConfigurationState.Instance.System.IgnoreMissingServices.Event -= UpdateIgnoreMissingServicesState;
@@ -629,7 +641,6 @@ namespace Ryujinx.Ava
_topLevel.PointerExited -= TopLevel_PointerExited;
_gpuCancellationTokenSource.Cancel();
_gpuCancellationTokenSource.Dispose();
_chrono.Stop();
}
@@ -654,6 +665,12 @@ namespace Ryujinx.Ava
}
else
{
// No use waiting on something that never started work
if (_renderingStarted)
{
Device.Gpu.WaitUntilGpuReady();
}
Device.DisposeGpu();
}
}
@@ -668,7 +685,7 @@ namespace Ryujinx.Ava
_cursorState = CursorStates.ForceChangeCursor;
}
public async Task<bool> LoadGuestApplication()
public async Task LoadGuestApplication(CancellationTokenSource cts, BlitStruct<ApplicationControlProperty>? customNacpData = null)
{
InitializeSwitchInstance();
MainWindow.UpdateGraphicsConfig();
@@ -696,7 +713,8 @@ namespace Ryujinx.Ava
await UserErrorDialog.ShowUserErrorDialog(userError);
Device.Dispose();
return false;
cts.Cancel();
throw new OperationCanceledException(cts.Token);
}
}
@@ -705,10 +723,11 @@ namespace Ryujinx.Ava
await UserErrorDialog.ShowUserErrorDialog(userError);
Device.Dispose();
return false;
cts.Cancel();
throw new OperationCanceledException(cts.Token);
}
// Tell the user that we installed a firmware for them.
// Tell the user that we installed firmware for them.
if (userError == UserError.NoFirmware)
{
firmwareVersion = ContentManager.GetCurrentFirmwareVersion();
@@ -728,7 +747,8 @@ namespace Ryujinx.Ava
await UserErrorDialog.ShowUserErrorDialog(userError);
Device.Dispose();
return false;
cts.Cancel();
throw new OperationCanceledException(cts.Token);
}
}
}
@@ -744,7 +764,8 @@ namespace Ryujinx.Ava
{
Device.Dispose();
return false;
cts.Cancel();
throw new OperationCanceledException(cts.Token);
}
}
else if (Directory.Exists(ApplicationPath))
@@ -762,20 +783,24 @@ namespace Ryujinx.Ava
if (!Device.LoadCart(ApplicationPath, romFsFiles[0]))
{
await ContentDialogHelper.CreateErrorDialog(
"Please specify an unpacked game directory with a valid exefs or NSO/NRO.");
Device.Dispose();
return false;
cts.Cancel();
throw new OperationCanceledException(cts.Token);
}
}
else
{
Logger.Info?.Print(LogClass.Application, "Loading as cart WITHOUT RomFS.");
if (!Device.LoadCart(ApplicationPath))
{
await ContentDialogHelper.CreateErrorDialog(
"Please specify an unpacked game directory with a valid exefs or NSO/NRO.");
Device.Dispose();
return false;
cts.Cancel();
throw new OperationCanceledException(cts.Token);
}
}
}
@@ -791,7 +816,8 @@ namespace Ryujinx.Ava
{
Device.Dispose();
return false;
cts.Cancel();
throw new OperationCanceledException(cts.Token);
}
break;
@@ -804,7 +830,8 @@ namespace Ryujinx.Ava
{
Device.Dispose();
return false;
cts.Cancel();
throw new OperationCanceledException(cts.Token);
}
break;
@@ -818,7 +845,8 @@ namespace Ryujinx.Ava
{
Device.Dispose();
return false;
cts.Cancel();
throw new OperationCanceledException(cts.Token);
}
break;
@@ -833,7 +861,8 @@ namespace Ryujinx.Ava
{
Device.Dispose();
return false;
cts.Cancel();
throw new OperationCanceledException(cts.Token);
}
}
catch (ArgumentOutOfRangeException)
@@ -842,7 +871,8 @@ namespace Ryujinx.Ava
Device.Dispose();
return false;
cts.Cancel();
throw new OperationCanceledException(cts.Token);
}
break;
@@ -851,11 +881,12 @@ namespace Ryujinx.Ava
}
else
{
Logger.Warning?.Print(LogClass.Application, "Please specify a valid XCI/NCA/NSP/PFS0/NRO file.");
Logger.Warning?.Print(LogClass.Application, "Please specify a valid XCI/NCA/NSP/PFS0/NSO/NRO file.");
Device.Dispose();
return false;
cts.Cancel();
throw new OperationCanceledException(cts.Token);
}
ApplicationMetadata appMeta = ApplicationLibrary.LoadAndSaveMetaData(Device.Processes.ActiveApplication.ProgramIdText,
@@ -863,8 +894,6 @@ namespace Ryujinx.Ava
);
DiscordIntegrationModule.SwitchToPlayingState(appMeta, Device.Processes.ActiveApplication);
return true;
}
internal void Resume()
@@ -873,7 +902,7 @@ namespace Ryujinx.Ava
_viewModel.IsPaused = false;
_viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version);
Logger.Info?.Print(LogClass.Emulation, "Emulation was resumed");
Logger.Info?.Print(LogClass.Emulation, "Emulation was resumed.");
}
internal void Pause()
@@ -882,7 +911,7 @@ namespace Ryujinx.Ava
_viewModel.IsPaused = true;
_viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, LocaleManager.Instance[LocaleKeys.Paused]);
Logger.Info?.Print(LogClass.Emulation, "Emulation was paused");
Logger.Info?.Print(LogClass.Emulation, "Emulation was paused.");
}
private void InitializeSwitchInstance()
@@ -1119,7 +1148,9 @@ namespace Ryujinx.Ava
// Make sure all commands in the run loop are fully executed before leaving the loop.
if (Device.Gpu.Renderer is ThreadedRenderer threaded)
{
Logger.Info?.PrintMsg(LogClass.Gpu, "Flushing threaded commands...");
threaded.FlushThreadedCommands();
Logger.Info?.PrintMsg(LogClass.Gpu, "Flushed!");
}
_gpuDoneEvent.Set();