From 8bd290cc57776b7a666013d5cdc499272a648d71 Mon Sep 17 00:00:00 2001 From: Shyanne Date: Sat, 27 Dec 2025 22:34:48 -0500 Subject: [PATCH 1/8] Addressed empty NCA lockup - Updated LoadGuestApplication to use a CancellationTokenSource so we can properly asynchronously cancel and not hang (other things could listen to this too, or cancel it). - Moved PrepareLoadScreen() to later in the pipeline (because cancelling LoadGuestApplication causes issues). - Added Metadata read/write logging. - Made AppHost inherit Disposable interface so that the garbage collector kicks in (side effect: made private Dispose() public) - Added a WaitHandle to wait on either gpuDoneEvent or gpuCTS.Cancel event (LoadGuestApplication cancellation). - Set invalid title ID for metadata. --- .../Loaders/Processes/ProcessLoader.cs | 10 ++- .../Loaders/Processes/ProcessResult.cs | 11 ++- src/Ryujinx/Systems/AppHost.cs | 75 +++++++++++-------- .../Systems/AppLibrary/ApplicationLibrary.cs | 5 +- .../UI/ViewModels/MainWindowViewModel.cs | 34 ++++++--- 5 files changed, 89 insertions(+), 46 deletions(-) diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs index 48b5b724c..e0edd2df5 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs @@ -14,6 +14,7 @@ using Ryujinx.HLE.Loaders.Executables; using Ryujinx.HLE.Loaders.Processes.Extensions; using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.IO; using Path = System.IO.Path; @@ -27,10 +28,15 @@ namespace Ryujinx.HLE.Loaders.Processes private ulong _latestPid; - public ProcessResult ActiveApplication + public ProcessResult? ActiveApplication { get { + return _processesByPid.GetValueOrDefault(_latestPid); + + // Using this if statement locks up the UI and prevents a new game from loading. + // Haven't quite deduced why yet. + if (!_processesByPid.TryGetValue(_latestPid, out ProcessResult value)) throw new RyujinxException( $"The HLE Process map did not have a process with ID {_latestPid}. Are you missing firmware?"); @@ -144,7 +150,7 @@ namespace Ryujinx.HLE.Loaders.Processes public bool LoadUnpackedNca(string exeFsDirPath, string romFsPath = null) { ProcessResult processResult = new LocalFileSystem(exeFsDirPath).Load(_device, romFsPath); - + if (processResult.ProcessId != 0 && _processesByPid.TryAdd(processResult.ProcessId, processResult)) { if (processResult.Start(_device)) diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs index d6e492317..66bdd57ef 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs @@ -4,7 +4,6 @@ using LibHac.Ns; using Ryujinx.Common.Logging; using Ryujinx.Cpu; using Ryujinx.HLE.HOS.SystemState; -using Ryujinx.HLE.Loaders.Processes.Extensions; using Ryujinx.Horizon.Common; namespace Ryujinx.HLE.Loaders.Processes @@ -52,6 +51,7 @@ namespace Ryujinx.HLE.Loaders.Processes if (metaLoader is not null) { + Logger.Info?.Print(LogClass.Application,$"metaLoader: {metaLoader}"); ulong programId = metaLoader.ProgramId; Name = ApplicationControlProperties.Title[(int)titleLanguage].NameString.ToString(); @@ -71,8 +71,15 @@ namespace Ryujinx.HLE.Loaders.Processes ProgramId = programId; ProgramIdText = $"{programId:x16}"; Is64Bit = metaLoader.IsProgram64Bit; + } + + else + { + Logger.Error?.Print(LogClass.Application,$"metaLoader is null !!!"); + ProcessId = 0; + return; } - + DiskCacheEnabled = diskCacheEnabled; AllowCodeMemoryForJit = allowCodeMemoryForJit; } diff --git a/src/Ryujinx/Systems/AppHost.cs b/src/Ryujinx/Systems/AppHost.cs index 2eba0d26b..06a60dc82 100644 --- a/src/Ryujinx/Systems/AppHost.cs +++ b/src/Ryujinx/Systems/AppHost.cs @@ -61,7 +61,7 @@ using VSyncMode = Ryujinx.Common.Configuration.VSyncMode; namespace Ryujinx.Ava.Systems { - internal class AppHost + internal class AppHost : IDisposable // notate this { private const int CursorHideIdleTime = 5; // Hide Cursor seconds. private const float MaxResolutionScale = 4.0f; // Max resolution hotkeys can scale to before wrapping. @@ -437,7 +437,7 @@ namespace Ryujinx.Ava.Systems SaveBitmapAsPng(bitmapToSave, path); - Logger.Notice.Print(LogClass.Application, $"Screenshot saved to {path}", "Screenshot"); + Logger.Notice.Print(LogClass.Application, $"Screenshot saved to {path}.", "Screenshot"); } }); } @@ -612,7 +612,9 @@ namespace Ryujinx.Ava.Systems // 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(); + + WaitHandle.WaitAny(new []{_gpuDoneEvent, _gpuCancellationTokenSource.Token.WaitHandle}); // notate this + _gpuCancellationTokenSource.Dispose(); _gpuDoneEvent.Dispose(); DisplaySleep.Restore(); @@ -620,17 +622,19 @@ namespace Ryujinx.Ava.Systems NpadManager.Dispose(); TouchScreenManager.Dispose(); Device.Dispose(); - + DisposeGpu(); - AppExit?.Invoke(this, EventArgs.Empty); } - private void Dispose() + public void Dispose() // notate this { if (Device.Processes != null) - MainWindowViewModel.UpdateGameMetadata(Device.Processes.ActiveApplication.ProgramIdText, _playTimer.Elapsed); - + { + MainWindowViewModel.UpdateGameMetadata(Device.Processes.ActiveApplication?.ProgramIdText ?? "", // notate this + _playTimer.Elapsed); + } + ConfigurationState.Instance.System.IgnoreMissingServices.Event -= UpdateIgnoreMissingServicesState; ConfigurationState.Instance.Graphics.AspectRatio.Event -= UpdateAspectRatioState; ConfigurationState.Instance.System.EnableDockedMode.Event -= UpdateDockedModeState; @@ -645,7 +649,6 @@ namespace Ryujinx.Ava.Systems _topLevel.PointerExited -= TopLevel_PointerExited; _gpuCancellationTokenSource.Cancel(); - _gpuCancellationTokenSource.Dispose(); _chrono.Stop(); _playTimer.Stop(); @@ -685,7 +688,7 @@ namespace Ryujinx.Ava.Systems _cursorState = CursorStates.ForceChangeCursor; } - public async Task LoadGuestApplication(BlitStruct? customNacpData = null) + public async Task LoadGuestApplication(CancellationTokenSource cts, BlitStruct? customNacpData = null) { DiscordIntegrationModule.GuestAppStartedAt = Timestamps.Now; @@ -714,7 +717,8 @@ namespace Ryujinx.Ava.Systems await UserErrorDialog.ShowUserErrorDialog(userError); Device.Dispose(); - return false; + cts.Cancel(); + throw new OperationCanceledException(cts.Token); } } @@ -723,10 +727,11 @@ namespace Ryujinx.Ava.Systems 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 is UserError.NoFirmware) { firmwareVersion = ContentManager.GetCurrentFirmwareVersion(); @@ -746,7 +751,8 @@ namespace Ryujinx.Ava.Systems await UserErrorDialog.ShowUserErrorDialog(userError); Device.Dispose(); - return false; + cts.Cancel(); + throw new OperationCanceledException(cts.Token); } } } @@ -761,7 +767,8 @@ namespace Ryujinx.Ava.Systems { Device.Dispose(); - return false; + cts.Cancel(); + throw new OperationCanceledException(cts.Token); } } else if (Directory.Exists(ApplicationPath)) @@ -781,20 +788,24 @@ namespace Ryujinx.Ava.Systems if (!Device.LoadCart(ApplicationPath, romFsFiles[0])) { + 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)) { + 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); } } } @@ -812,7 +823,8 @@ namespace Ryujinx.Ava.Systems { Device.Dispose(); - return false; + cts.Cancel(); + throw new OperationCanceledException(cts.Token); } break; @@ -825,7 +837,8 @@ namespace Ryujinx.Ava.Systems { Device.Dispose(); - return false; + cts.Cancel(); + throw new OperationCanceledException(cts.Token); } break; @@ -839,7 +852,8 @@ namespace Ryujinx.Ava.Systems { Device.Dispose(); - return false; + cts.Cancel(); + throw new OperationCanceledException(cts.Token); } break; @@ -854,7 +868,8 @@ namespace Ryujinx.Ava.Systems { Device.Dispose(); - return false; + cts.Cancel(); + throw new OperationCanceledException(cts.Token); } } catch (ArgumentOutOfRangeException) @@ -863,7 +878,8 @@ namespace Ryujinx.Ava.Systems Device.Dispose(); - return false; + cts.Cancel(); + throw new OperationCanceledException(cts.Token); } break; @@ -872,19 +888,18 @@ namespace Ryujinx.Ava.Systems } 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); } ApplicationLibrary.LoadAndSaveMetaData(Device.Processes.ActiveApplication.ProgramIdText, appMetadata => appMetadata.UpdatePreGame() ); _playTimer.Start(); - - return true; } internal void Resume() @@ -894,7 +909,7 @@ namespace Ryujinx.Ava.Systems _viewModel.IsPaused = false; _playTimer.Start(); _viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowOldUI); - Logger.Info?.Print(LogClass.Emulation, "Emulation was resumed"); + Logger.Info?.Print(LogClass.Emulation, "Emulation was resumed."); } internal void Pause() @@ -904,7 +919,7 @@ namespace Ryujinx.Ava.Systems _viewModel.IsPaused = true; _playTimer.Stop(); _viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowOldUI, LocaleManager.Instance[LocaleKeys.Paused]); - Logger.Info?.Print(LogClass.Emulation, "Emulation was paused"); + Logger.Info?.Print(LogClass.Emulation, "Emulation was paused."); } private void InitEmulatedSwitch() diff --git a/src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs b/src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs index 2831802fe..8a9a5eb12 100644 --- a/src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs +++ b/src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs @@ -1165,9 +1165,10 @@ namespace Ryujinx.Ava.Systems.AppLibrary string metadataFile = Path.Combine(metadataFolder, "metadata.json"); ApplicationMetadata appMetadata; - + if (!File.Exists(metadataFile)) { + Logger.Info?.Print(LogClass.Application, $"Metadata file does not exist. Creating metadata for {titleId}..."); Directory.CreateDirectory(metadataFolder); appMetadata = new ApplicationMetadata(); @@ -1177,12 +1178,12 @@ namespace Ryujinx.Ava.Systems.AppLibrary try { + Logger.Debug?.Print(LogClass.Application, $"Deserializing metadata for {titleId}..."); appMetadata = JsonHelper.DeserializeFromFile(metadataFile, _serializerContext.ApplicationMetadata); } catch (JsonException) { Logger.Warning?.Print(LogClass.Application, $"Failed to parse metadata json for {titleId}. Loading defaults."); - appMetadata = new ApplicationMetadata(); } diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index 2236b27f6..9e6375d55 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -1700,11 +1700,6 @@ namespace Ryujinx.Ava.UI.ViewModels Logger.RestartTime(); - SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(application.Path, - ConfigurationState.Instance.System.Language, application.Id); - - PrepareLoadScreen(); - RendererHostControl = new RendererHost(); AppHost = new AppHost( @@ -1718,18 +1713,36 @@ namespace Ryujinx.Ava.UI.ViewModels UserChannelPersistence, this, TopLevel); + + // Needs a new name to better fit code styling + CancellationTokenSource cts = new CancellationTokenSource(); - if (!await AppHost.LoadGuestApplication(customNacpData)) + try { + await AppHost.LoadGuestApplication(cts, customNacpData); + } + catch (OperationCanceledException exception) + { + Logger.Info?.Print(LogClass.Application, + "LoadGuestApplication was interrupted !!! " + exception.Message); AppHost.DisposeContext(); AppHost = null; - return; } - + finally + { + cts.Dispose(); + } + CanUpdate = false; application.Name ??= AppHost.Device.Processes.ActiveApplication.Name; + + // notate this + SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(application.Path, + ConfigurationState.Instance.System.Language, application.Id); + + PrepareLoadScreen(); LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, application.Name); @@ -1751,8 +1764,9 @@ namespace Ryujinx.Ava.UI.ViewModels RendererHostControl.Focus(); }); - public static void UpdateGameMetadata(string titleId, TimeSpan playTime) - => ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => appMetadata.UpdatePostGame(playTime)); + public static void UpdateGameMetadata(string titleId, TimeSpan playTime) + =>ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => appMetadata.UpdatePostGame(playTime)); + public void RefreshFirmwareStatus() { From cf72e189b752ca4a9c28aa7a5f68ded2a75cbc9d Mon Sep 17 00:00:00 2001 From: Shyanne Date: Sat, 27 Dec 2025 23:04:20 -0500 Subject: [PATCH 2/8] Amended typos and updated commentary --- src/Ryujinx/Systems/AppHost.cs | 15 +++++++++------ src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs | 6 ++---- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/Ryujinx/Systems/AppHost.cs b/src/Ryujinx/Systems/AppHost.cs index 06a60dc82..4b7ed71ec 100644 --- a/src/Ryujinx/Systems/AppHost.cs +++ b/src/Ryujinx/Systems/AppHost.cs @@ -61,7 +61,7 @@ using VSyncMode = Ryujinx.Common.Configuration.VSyncMode; namespace Ryujinx.Ava.Systems { - internal class AppHost : IDisposable // notate this + 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. @@ -437,7 +437,7 @@ namespace Ryujinx.Ava.Systems SaveBitmapAsPng(bitmapToSave, path); - Logger.Notice.Print(LogClass.Application, $"Screenshot saved to {path}.", "Screenshot"); + Logger.Notice.Print(LogClass.Application, $"Screenshot saved to '{path}'.", "Screenshot"); } }); } @@ -611,9 +611,9 @@ namespace Ryujinx.Ava.Systems _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. + // We only need to wait for all commands submitted during the main gpu loop to be processed, unless the GPU event is cancelled. - WaitHandle.WaitAny(new []{_gpuDoneEvent, _gpuCancellationTokenSource.Token.WaitHandle}); // notate this + WaitHandle.WaitAny(new []{_gpuDoneEvent, _gpuCancellationTokenSource.Token.WaitHandle}); _gpuCancellationTokenSource.Dispose(); _gpuDoneEvent.Dispose(); @@ -627,11 +627,14 @@ namespace Ryujinx.Ava.Systems AppExit?.Invoke(this, EventArgs.Empty); } - public void Dispose() // notate this + // MUST be public to inherit from IDisposable + public void Dispose() { if (Device.Processes != null) { - MainWindowViewModel.UpdateGameMetadata(Device.Processes.ActiveApplication?.ProgramIdText ?? "", // notate this + // If the ActiveApplication is null, then the ProgramIdText should be + // so that we aren't arbitrarily applying metadata to something that doesn't exist. + MainWindowViewModel.UpdateGameMetadata(Device.Processes.ActiveApplication?.ProgramIdText ?? "", _playTimer.Elapsed); } diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index 9e6375d55..70f4952e4 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -1738,7 +1738,6 @@ namespace Ryujinx.Ava.UI.ViewModels application.Name ??= AppHost.Device.Processes.ActiveApplication.Name; - // notate this SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(application.Path, ConfigurationState.Instance.System.Language, application.Id); @@ -1765,9 +1764,8 @@ namespace Ryujinx.Ava.UI.ViewModels }); public static void UpdateGameMetadata(string titleId, TimeSpan playTime) - =>ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => appMetadata.UpdatePostGame(playTime)); - - + => ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => appMetadata.UpdatePostGame(playTime)); + public void RefreshFirmwareStatus() { SystemVersion version = null; From 445924102e46b3a14934d3b3966fa89db4a4ac10 Mon Sep 17 00:00:00 2001 From: Shyanne Date: Sat, 27 Dec 2025 22:34:48 -0500 Subject: [PATCH 3/8] Addressed empty NCA lockup - Updated LoadGuestApplication to use a CancellationTokenSource so we can properly asynchronously cancel and not hang (other things could listen to this too, or cancel it). - Moved PrepareLoadScreen() to later in the pipeline (because cancelling LoadGuestApplication causes issues). - Added Metadata read/write logging. - Made AppHost inherit Disposable interface so that the garbage collector kicks in (side effect: made private Dispose() public) - Added a WaitHandle to wait on either gpuDoneEvent or gpuCTS.Cancel event (LoadGuestApplication cancellation). - Set invalid title ID for metadata. --- .../Loaders/Processes/ProcessLoader.cs | 10 ++- .../Loaders/Processes/ProcessResult.cs | 11 ++- src/Ryujinx/Systems/AppHost.cs | 75 +++++++++++-------- .../Systems/AppLibrary/ApplicationLibrary.cs | 5 +- .../UI/ViewModels/MainWindowViewModel.cs | 34 ++++++--- 5 files changed, 89 insertions(+), 46 deletions(-) diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs index 48b5b724c..e0edd2df5 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs @@ -14,6 +14,7 @@ using Ryujinx.HLE.Loaders.Executables; using Ryujinx.HLE.Loaders.Processes.Extensions; using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.IO; using Path = System.IO.Path; @@ -27,10 +28,15 @@ namespace Ryujinx.HLE.Loaders.Processes private ulong _latestPid; - public ProcessResult ActiveApplication + public ProcessResult? ActiveApplication { get { + return _processesByPid.GetValueOrDefault(_latestPid); + + // Using this if statement locks up the UI and prevents a new game from loading. + // Haven't quite deduced why yet. + if (!_processesByPid.TryGetValue(_latestPid, out ProcessResult value)) throw new RyujinxException( $"The HLE Process map did not have a process with ID {_latestPid}. Are you missing firmware?"); @@ -144,7 +150,7 @@ namespace Ryujinx.HLE.Loaders.Processes public bool LoadUnpackedNca(string exeFsDirPath, string romFsPath = null) { ProcessResult processResult = new LocalFileSystem(exeFsDirPath).Load(_device, romFsPath); - + if (processResult.ProcessId != 0 && _processesByPid.TryAdd(processResult.ProcessId, processResult)) { if (processResult.Start(_device)) diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs index d6e492317..66bdd57ef 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs @@ -4,7 +4,6 @@ using LibHac.Ns; using Ryujinx.Common.Logging; using Ryujinx.Cpu; using Ryujinx.HLE.HOS.SystemState; -using Ryujinx.HLE.Loaders.Processes.Extensions; using Ryujinx.Horizon.Common; namespace Ryujinx.HLE.Loaders.Processes @@ -52,6 +51,7 @@ namespace Ryujinx.HLE.Loaders.Processes if (metaLoader is not null) { + Logger.Info?.Print(LogClass.Application,$"metaLoader: {metaLoader}"); ulong programId = metaLoader.ProgramId; Name = ApplicationControlProperties.Title[(int)titleLanguage].NameString.ToString(); @@ -71,8 +71,15 @@ namespace Ryujinx.HLE.Loaders.Processes ProgramId = programId; ProgramIdText = $"{programId:x16}"; Is64Bit = metaLoader.IsProgram64Bit; + } + + else + { + Logger.Error?.Print(LogClass.Application,$"metaLoader is null !!!"); + ProcessId = 0; + return; } - + DiskCacheEnabled = diskCacheEnabled; AllowCodeMemoryForJit = allowCodeMemoryForJit; } diff --git a/src/Ryujinx/Systems/AppHost.cs b/src/Ryujinx/Systems/AppHost.cs index 2eba0d26b..06a60dc82 100644 --- a/src/Ryujinx/Systems/AppHost.cs +++ b/src/Ryujinx/Systems/AppHost.cs @@ -61,7 +61,7 @@ using VSyncMode = Ryujinx.Common.Configuration.VSyncMode; namespace Ryujinx.Ava.Systems { - internal class AppHost + internal class AppHost : IDisposable // notate this { private const int CursorHideIdleTime = 5; // Hide Cursor seconds. private const float MaxResolutionScale = 4.0f; // Max resolution hotkeys can scale to before wrapping. @@ -437,7 +437,7 @@ namespace Ryujinx.Ava.Systems SaveBitmapAsPng(bitmapToSave, path); - Logger.Notice.Print(LogClass.Application, $"Screenshot saved to {path}", "Screenshot"); + Logger.Notice.Print(LogClass.Application, $"Screenshot saved to {path}.", "Screenshot"); } }); } @@ -612,7 +612,9 @@ namespace Ryujinx.Ava.Systems // 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(); + + WaitHandle.WaitAny(new []{_gpuDoneEvent, _gpuCancellationTokenSource.Token.WaitHandle}); // notate this + _gpuCancellationTokenSource.Dispose(); _gpuDoneEvent.Dispose(); DisplaySleep.Restore(); @@ -620,17 +622,19 @@ namespace Ryujinx.Ava.Systems NpadManager.Dispose(); TouchScreenManager.Dispose(); Device.Dispose(); - + DisposeGpu(); - AppExit?.Invoke(this, EventArgs.Empty); } - private void Dispose() + public void Dispose() // notate this { if (Device.Processes != null) - MainWindowViewModel.UpdateGameMetadata(Device.Processes.ActiveApplication.ProgramIdText, _playTimer.Elapsed); - + { + MainWindowViewModel.UpdateGameMetadata(Device.Processes.ActiveApplication?.ProgramIdText ?? "", // notate this + _playTimer.Elapsed); + } + ConfigurationState.Instance.System.IgnoreMissingServices.Event -= UpdateIgnoreMissingServicesState; ConfigurationState.Instance.Graphics.AspectRatio.Event -= UpdateAspectRatioState; ConfigurationState.Instance.System.EnableDockedMode.Event -= UpdateDockedModeState; @@ -645,7 +649,6 @@ namespace Ryujinx.Ava.Systems _topLevel.PointerExited -= TopLevel_PointerExited; _gpuCancellationTokenSource.Cancel(); - _gpuCancellationTokenSource.Dispose(); _chrono.Stop(); _playTimer.Stop(); @@ -685,7 +688,7 @@ namespace Ryujinx.Ava.Systems _cursorState = CursorStates.ForceChangeCursor; } - public async Task LoadGuestApplication(BlitStruct? customNacpData = null) + public async Task LoadGuestApplication(CancellationTokenSource cts, BlitStruct? customNacpData = null) { DiscordIntegrationModule.GuestAppStartedAt = Timestamps.Now; @@ -714,7 +717,8 @@ namespace Ryujinx.Ava.Systems await UserErrorDialog.ShowUserErrorDialog(userError); Device.Dispose(); - return false; + cts.Cancel(); + throw new OperationCanceledException(cts.Token); } } @@ -723,10 +727,11 @@ namespace Ryujinx.Ava.Systems 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 is UserError.NoFirmware) { firmwareVersion = ContentManager.GetCurrentFirmwareVersion(); @@ -746,7 +751,8 @@ namespace Ryujinx.Ava.Systems await UserErrorDialog.ShowUserErrorDialog(userError); Device.Dispose(); - return false; + cts.Cancel(); + throw new OperationCanceledException(cts.Token); } } } @@ -761,7 +767,8 @@ namespace Ryujinx.Ava.Systems { Device.Dispose(); - return false; + cts.Cancel(); + throw new OperationCanceledException(cts.Token); } } else if (Directory.Exists(ApplicationPath)) @@ -781,20 +788,24 @@ namespace Ryujinx.Ava.Systems if (!Device.LoadCart(ApplicationPath, romFsFiles[0])) { + 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)) { + 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); } } } @@ -812,7 +823,8 @@ namespace Ryujinx.Ava.Systems { Device.Dispose(); - return false; + cts.Cancel(); + throw new OperationCanceledException(cts.Token); } break; @@ -825,7 +837,8 @@ namespace Ryujinx.Ava.Systems { Device.Dispose(); - return false; + cts.Cancel(); + throw new OperationCanceledException(cts.Token); } break; @@ -839,7 +852,8 @@ namespace Ryujinx.Ava.Systems { Device.Dispose(); - return false; + cts.Cancel(); + throw new OperationCanceledException(cts.Token); } break; @@ -854,7 +868,8 @@ namespace Ryujinx.Ava.Systems { Device.Dispose(); - return false; + cts.Cancel(); + throw new OperationCanceledException(cts.Token); } } catch (ArgumentOutOfRangeException) @@ -863,7 +878,8 @@ namespace Ryujinx.Ava.Systems Device.Dispose(); - return false; + cts.Cancel(); + throw new OperationCanceledException(cts.Token); } break; @@ -872,19 +888,18 @@ namespace Ryujinx.Ava.Systems } 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); } ApplicationLibrary.LoadAndSaveMetaData(Device.Processes.ActiveApplication.ProgramIdText, appMetadata => appMetadata.UpdatePreGame() ); _playTimer.Start(); - - return true; } internal void Resume() @@ -894,7 +909,7 @@ namespace Ryujinx.Ava.Systems _viewModel.IsPaused = false; _playTimer.Start(); _viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowOldUI); - Logger.Info?.Print(LogClass.Emulation, "Emulation was resumed"); + Logger.Info?.Print(LogClass.Emulation, "Emulation was resumed."); } internal void Pause() @@ -904,7 +919,7 @@ namespace Ryujinx.Ava.Systems _viewModel.IsPaused = true; _playTimer.Stop(); _viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowOldUI, LocaleManager.Instance[LocaleKeys.Paused]); - Logger.Info?.Print(LogClass.Emulation, "Emulation was paused"); + Logger.Info?.Print(LogClass.Emulation, "Emulation was paused."); } private void InitEmulatedSwitch() diff --git a/src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs b/src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs index 2831802fe..8a9a5eb12 100644 --- a/src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs +++ b/src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs @@ -1165,9 +1165,10 @@ namespace Ryujinx.Ava.Systems.AppLibrary string metadataFile = Path.Combine(metadataFolder, "metadata.json"); ApplicationMetadata appMetadata; - + if (!File.Exists(metadataFile)) { + Logger.Info?.Print(LogClass.Application, $"Metadata file does not exist. Creating metadata for {titleId}..."); Directory.CreateDirectory(metadataFolder); appMetadata = new ApplicationMetadata(); @@ -1177,12 +1178,12 @@ namespace Ryujinx.Ava.Systems.AppLibrary try { + Logger.Debug?.Print(LogClass.Application, $"Deserializing metadata for {titleId}..."); appMetadata = JsonHelper.DeserializeFromFile(metadataFile, _serializerContext.ApplicationMetadata); } catch (JsonException) { Logger.Warning?.Print(LogClass.Application, $"Failed to parse metadata json for {titleId}. Loading defaults."); - appMetadata = new ApplicationMetadata(); } diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index 651dc901c..46c641fac 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -1703,11 +1703,6 @@ namespace Ryujinx.Ava.UI.ViewModels Logger.RestartTime(); - SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(application.Path, - ConfigurationState.Instance.System.Language, application.Id); - - PrepareLoadScreen(); - RendererHostControl = new RendererHost(); AppHost = new AppHost( @@ -1721,18 +1716,36 @@ namespace Ryujinx.Ava.UI.ViewModels UserChannelPersistence, this, TopLevel); + + // Needs a new name to better fit code styling + CancellationTokenSource cts = new CancellationTokenSource(); - if (!await AppHost.LoadGuestApplication(customNacpData)) + try { + await AppHost.LoadGuestApplication(cts, customNacpData); + } + catch (OperationCanceledException exception) + { + Logger.Info?.Print(LogClass.Application, + "LoadGuestApplication was interrupted !!! " + exception.Message); AppHost.DisposeContext(); AppHost = null; - return; } - + finally + { + cts.Dispose(); + } + CanUpdate = false; application.Name ??= AppHost.Device.Processes.ActiveApplication.Name; + + // notate this + SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(application.Path, + ConfigurationState.Instance.System.Language, application.Id); + + PrepareLoadScreen(); LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, application.Name); @@ -1754,8 +1767,9 @@ namespace Ryujinx.Ava.UI.ViewModels RendererHostControl.Focus(); }); - public static void UpdateGameMetadata(string titleId, TimeSpan playTime) - => ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => appMetadata.UpdatePostGame(playTime)); + public static void UpdateGameMetadata(string titleId, TimeSpan playTime) + =>ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => appMetadata.UpdatePostGame(playTime)); + public void RefreshFirmwareStatus() { From f6328ebb69c66725358895c655e9ebaa1a672498 Mon Sep 17 00:00:00 2001 From: Shyanne Date: Sat, 27 Dec 2025 23:04:20 -0500 Subject: [PATCH 4/8] Amended typos and updated commentary --- src/Ryujinx/Systems/AppHost.cs | 15 +++++++++------ src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs | 6 ++---- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/Ryujinx/Systems/AppHost.cs b/src/Ryujinx/Systems/AppHost.cs index 06a60dc82..4b7ed71ec 100644 --- a/src/Ryujinx/Systems/AppHost.cs +++ b/src/Ryujinx/Systems/AppHost.cs @@ -61,7 +61,7 @@ using VSyncMode = Ryujinx.Common.Configuration.VSyncMode; namespace Ryujinx.Ava.Systems { - internal class AppHost : IDisposable // notate this + 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. @@ -437,7 +437,7 @@ namespace Ryujinx.Ava.Systems SaveBitmapAsPng(bitmapToSave, path); - Logger.Notice.Print(LogClass.Application, $"Screenshot saved to {path}.", "Screenshot"); + Logger.Notice.Print(LogClass.Application, $"Screenshot saved to '{path}'.", "Screenshot"); } }); } @@ -611,9 +611,9 @@ namespace Ryujinx.Ava.Systems _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. + // We only need to wait for all commands submitted during the main gpu loop to be processed, unless the GPU event is cancelled. - WaitHandle.WaitAny(new []{_gpuDoneEvent, _gpuCancellationTokenSource.Token.WaitHandle}); // notate this + WaitHandle.WaitAny(new []{_gpuDoneEvent, _gpuCancellationTokenSource.Token.WaitHandle}); _gpuCancellationTokenSource.Dispose(); _gpuDoneEvent.Dispose(); @@ -627,11 +627,14 @@ namespace Ryujinx.Ava.Systems AppExit?.Invoke(this, EventArgs.Empty); } - public void Dispose() // notate this + // MUST be public to inherit from IDisposable + public void Dispose() { if (Device.Processes != null) { - MainWindowViewModel.UpdateGameMetadata(Device.Processes.ActiveApplication?.ProgramIdText ?? "", // notate this + // If the ActiveApplication is null, then the ProgramIdText should be + // so that we aren't arbitrarily applying metadata to something that doesn't exist. + MainWindowViewModel.UpdateGameMetadata(Device.Processes.ActiveApplication?.ProgramIdText ?? "", _playTimer.Elapsed); } diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index 46c641fac..c537b7ad8 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -1741,7 +1741,6 @@ namespace Ryujinx.Ava.UI.ViewModels application.Name ??= AppHost.Device.Processes.ActiveApplication.Name; - // notate this SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(application.Path, ConfigurationState.Instance.System.Language, application.Id); @@ -1768,9 +1767,8 @@ namespace Ryujinx.Ava.UI.ViewModels }); public static void UpdateGameMetadata(string titleId, TimeSpan playTime) - =>ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => appMetadata.UpdatePostGame(playTime)); - - + => ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => appMetadata.UpdatePostGame(playTime)); + public void RefreshFirmwareStatus() { SystemVersion version = null; From 3cc02ebaefb24ed3f7c36846df421dd7581a1bac Mon Sep 17 00:00:00 2001 From: Shyanne Date: Sun, 28 Dec 2025 16:54:21 -0500 Subject: [PATCH 5/8] maybe don't dispose of the gpu when it doesnt exist --- src/Ryujinx/Systems/AppHost.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Ryujinx/Systems/AppHost.cs b/src/Ryujinx/Systems/AppHost.cs index 4b7ed71ec..b5f40a4dc 100644 --- a/src/Ryujinx/Systems/AppHost.cs +++ b/src/Ryujinx/Systems/AppHost.cs @@ -610,18 +610,18 @@ namespace Ryujinx.Ava.Systems _isActive = false; + DisplaySleep.Restore(); + + NpadManager.Dispose(); + TouchScreenManager.Dispose(); + Device.Dispose(); + // 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, unless the GPU event is cancelled. WaitHandle.WaitAny(new []{_gpuDoneEvent, _gpuCancellationTokenSource.Token.WaitHandle}); _gpuCancellationTokenSource.Dispose(); _gpuDoneEvent.Dispose(); - - DisplaySleep.Restore(); - - NpadManager.Dispose(); - TouchScreenManager.Dispose(); - Device.Dispose(); DisposeGpu(); AppExit?.Invoke(this, EventArgs.Empty); From 6780ff0d8aceaf14bd70fa2147b84f4efc309f0b Mon Sep 17 00:00:00 2001 From: Shyanne Date: Mon, 29 Dec 2025 00:28:50 -0500 Subject: [PATCH 6/8] =?UTF-8?q?=F0=9F=91=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Ryujinx.Graphics.Vulkan/Auto.cs | 5 ++- src/Ryujinx/Systems/AppHost.cs | 34 +++++++++++++++---- .../UI/ViewModels/MainWindowViewModel.cs | 1 - 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/Ryujinx.Graphics.Vulkan/Auto.cs b/src/Ryujinx.Graphics.Vulkan/Auto.cs index 7ce309a5d..9c8c7ff4b 100644 --- a/src/Ryujinx.Graphics.Vulkan/Auto.cs +++ b/src/Ryujinx.Graphics.Vulkan/Auto.cs @@ -1,3 +1,4 @@ +using Ryujinx.Common.Logging; using System; using System.Diagnostics; using System.Threading; @@ -114,7 +115,7 @@ namespace Ryujinx.Graphics.Vulkan cbs.AddDependant(this); // We need to add a dependency on the command buffer to all objects this object - // references aswell. + // references as well. if (_referencedObjs != null) { for (int i = 0; i < _referencedObjs.Length; i++) @@ -176,6 +177,8 @@ namespace Ryujinx.Graphics.Vulkan } } + // This can somehow become -1. + // Logger.Info?.PrintMsg(LogClass.Gpu, $"_referenceCount: {_referenceCount}"); Debug.Assert(_referenceCount >= 0); } diff --git a/src/Ryujinx/Systems/AppHost.cs b/src/Ryujinx/Systems/AppHost.cs index b5f40a4dc..4000f370a 100644 --- a/src/Ryujinx/Systems/AppHost.cs +++ b/src/Ryujinx/Systems/AppHost.cs @@ -617,10 +617,18 @@ namespace Ryujinx.Ava.Systems Device.Dispose(); // 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, unless the GPU event is cancelled. + // 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}); + 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(); @@ -632,10 +640,16 @@ namespace Ryujinx.Ava.Systems { if (Device.Processes != null) { - // If the ActiveApplication is null, then the ProgramIdText should be - // so that we aren't arbitrarily applying metadata to something that doesn't exist. - MainWindowViewModel.UpdateGameMetadata(Device.Processes.ActiveApplication?.ProgramIdText ?? "", - _playTimer.Elapsed); + // If the ActiveApplication is null, then the ProgramIdText is invalid. + if (Device.Processes.ActiveApplication is not null) + { + MainWindowViewModel.UpdateGameMetadata(Device.Processes.ActiveApplication.ProgramIdText, + _playTimer.Elapsed); + } + else + { + Logger.Error?.PrintMsg(LogClass.Application, "Cannot save metadata because title ID is invalid."); + } } ConfigurationState.Instance.System.IgnoreMissingServices.Event -= UpdateIgnoreMissingServicesState; @@ -677,6 +691,12 @@ namespace Ryujinx.Ava.Systems } else { + // No use waiting on something that never started work + if (_renderingStarted) + { + Device.Gpu.WaitUntilGpuReady(); + } + Device.DisposeGpu(); } } @@ -1115,7 +1135,9 @@ namespace Ryujinx.Ava.Systems // 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(); diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index c537b7ad8..d720568b7 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -1717,7 +1717,6 @@ namespace Ryujinx.Ava.UI.ViewModels this, TopLevel); - // Needs a new name to better fit code styling CancellationTokenSource cts = new CancellationTokenSource(); try From 34190c918477cd229b4cc189e4da14bb1ffea0bd Mon Sep 17 00:00:00 2001 From: Shyanne Date: Mon, 29 Dec 2025 12:09:44 -0500 Subject: [PATCH 7/8] Moved null title id check to LoadAndSaveMetadata --- src/Ryujinx/Systems/AppHost.cs | 12 ++---------- src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs | 9 ++++++++- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/Ryujinx/Systems/AppHost.cs b/src/Ryujinx/Systems/AppHost.cs index 4000f370a..693e40a0b 100644 --- a/src/Ryujinx/Systems/AppHost.cs +++ b/src/Ryujinx/Systems/AppHost.cs @@ -640,16 +640,8 @@ namespace Ryujinx.Ava.Systems { if (Device.Processes != null) { - // If the ActiveApplication is null, then the ProgramIdText is invalid. - if (Device.Processes.ActiveApplication is not null) - { - MainWindowViewModel.UpdateGameMetadata(Device.Processes.ActiveApplication.ProgramIdText, - _playTimer.Elapsed); - } - else - { - Logger.Error?.PrintMsg(LogClass.Application, "Cannot save metadata because title ID is invalid."); - } + MainWindowViewModel.UpdateGameMetadata(Device.Processes.ActiveApplication?.ProgramIdText, + _playTimer.Elapsed); } ConfigurationState.Instance.System.IgnoreMissingServices.Event -= UpdateIgnoreMissingServicesState; diff --git a/src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs b/src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs index 8a9a5eb12..a39a8b5cc 100644 --- a/src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs +++ b/src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs @@ -1159,8 +1159,15 @@ namespace Ryujinx.Ava.Systems.AppLibrary ApplicationCountUpdated?.Invoke(null, e); } - public static ApplicationMetadata LoadAndSaveMetaData(string titleId, Action modifyFunction = null) + public static ApplicationMetadata? LoadAndSaveMetaData(string titleId, Action modifyFunction = null) { + + if (titleId is null) + { + Logger.Warning?.PrintMsg(LogClass.Application, "Cannot save metadata because title ID is invalid."); + return null; + } + string metadataFolder = Path.Combine(AppDataManager.GamesDirPath, titleId, "gui"); string metadataFile = Path.Combine(metadataFolder, "metadata.json"); From 2274a32813d71c119c148e3947ed54f9959bfba1 Mon Sep 17 00:00:00 2001 From: Shyanne Date: Tue, 30 Dec 2025 17:06:21 -0500 Subject: [PATCH 8/8] Updated LoadAndSaveMetadata to return Optional type (but for real this time) - fixed a typo i found - awaiting dialog on invalid unpacked error - --- src/Ryujinx.HLE/FileSystem/ContentManager.cs | 2 +- src/Ryujinx/Systems/AppHost.cs | 4 ++-- src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs | 6 +++--- src/Ryujinx/Systems/DiscordIntegrationModule.cs | 2 +- src/Ryujinx/UI/Models/SaveModel.cs | 11 +++++++++-- src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs | 2 +- 6 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/Ryujinx.HLE/FileSystem/ContentManager.cs b/src/Ryujinx.HLE/FileSystem/ContentManager.cs index d0fe0f1a7..6df359c56 100644 --- a/src/Ryujinx.HLE/FileSystem/ContentManager.cs +++ b/src/Ryujinx.HLE/FileSystem/ContentManager.cs @@ -1059,7 +1059,7 @@ namespace Ryujinx.HLE.FileSystem } } - public static bool AreKeysAlredyPresent(string pathToCheck) + public static bool AreKeysAlreadyPresent(string pathToCheck) { string[] fileNames = ["prod.keys", "title.keys", "console.keys", "dev.keys"]; foreach (string file in fileNames) diff --git a/src/Ryujinx/Systems/AppHost.cs b/src/Ryujinx/Systems/AppHost.cs index 693e40a0b..5779d261a 100644 --- a/src/Ryujinx/Systems/AppHost.cs +++ b/src/Ryujinx/Systems/AppHost.cs @@ -803,7 +803,7 @@ namespace Ryujinx.Ava.Systems if (!Device.LoadCart(ApplicationPath, romFsFiles[0])) { - ContentDialogHelper.CreateErrorDialog( + await ContentDialogHelper.CreateErrorDialog( "Please specify an unpacked game directory with a valid exefs or NSO/NRO."); Device.Dispose(); @@ -816,7 +816,7 @@ namespace Ryujinx.Ava.Systems Logger.Info?.Print(LogClass.Application, "Loading as cart WITHOUT RomFS."); if (!Device.LoadCart(ApplicationPath)) { - ContentDialogHelper.CreateErrorDialog( + await ContentDialogHelper.CreateErrorDialog( "Please specify an unpacked game directory with a valid exefs or NSO/NRO."); Device.Dispose(); cts.Cancel(); diff --git a/src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs b/src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs index a39a8b5cc..6513738cb 100644 --- a/src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs +++ b/src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs @@ -849,7 +849,8 @@ namespace Ryujinx.Ava.Systems.AppLibrary foreach (ApplicationData installedApplication in Applications.Items) { - temporary += LoadAndSaveMetaData(installedApplication.IdString).TimePlayed; + // this should always exist... should... + temporary += LoadAndSaveMetaData(installedApplication.IdString).Value.TimePlayed; } TotalTimePlayed = temporary; @@ -1159,9 +1160,8 @@ namespace Ryujinx.Ava.Systems.AppLibrary ApplicationCountUpdated?.Invoke(null, e); } - public static ApplicationMetadata? LoadAndSaveMetaData(string titleId, Action modifyFunction = null) + public static Gommon.Optional LoadAndSaveMetaData(string titleId, Action modifyFunction = null) { - if (titleId is null) { Logger.Warning?.PrintMsg(LogClass.Application, "Cannot save metadata because title ID is invalid."); diff --git a/src/Ryujinx/Systems/DiscordIntegrationModule.cs b/src/Ryujinx/Systems/DiscordIntegrationModule.cs index 5b61340b6..da6371682 100644 --- a/src/Ryujinx/Systems/DiscordIntegrationModule.cs +++ b/src/Ryujinx/Systems/DiscordIntegrationModule.cs @@ -82,7 +82,7 @@ namespace Ryujinx.Ava.Systems public static void Use(Optional titleId) { - if (titleId.TryGet(out string tid)) + if (titleId.TryGet(out string tid) && Switch.Shared.Processes.ActiveApplication is not null) SwitchToPlayingState( ApplicationLibrary.LoadAndSaveMetaData(tid), Switch.Shared.Processes.ActiveApplication diff --git a/src/Ryujinx/UI/Models/SaveModel.cs b/src/Ryujinx/UI/Models/SaveModel.cs index d245ed4d9..bd9c93f5d 100644 --- a/src/Ryujinx/UI/Models/SaveModel.cs +++ b/src/Ryujinx/UI/Models/SaveModel.cs @@ -57,8 +57,15 @@ namespace Ryujinx.Ava.UI.Models } else { - ApplicationMetadata appMetadata = ApplicationLibrary.LoadAndSaveMetaData(TitleIdString); - Title = appMetadata.Title ?? TitleIdString; + Gommon.Optional appMetadata = ApplicationLibrary.LoadAndSaveMetaData(TitleIdString); + if (appMetadata != null) + { + Title = appMetadata.Value.Title ?? TitleIdString; + } + else + { + Title = ""; + } } Task.Run(() => diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index d720568b7..568688bc4 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -1029,7 +1029,7 @@ namespace Ryujinx.Ava.UI.ViewModels string dialogMessage = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogKeysInstallerKeysInstallMessage); - if (ContentManager.AreKeysAlredyPresent(systemDirectory)) + if (ContentManager.AreKeysAlreadyPresent(systemDirectory)) { dialogMessage += LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys