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.
This commit is contained in:
Shyanne
2025-12-27 22:34:48 -05:00
parent 39f55b2af3
commit 8bd290cc57
5 changed files with 89 additions and 46 deletions

View File

@@ -14,6 +14,7 @@ using Ryujinx.HLE.Loaders.Executables;
using Ryujinx.HLE.Loaders.Processes.Extensions; using Ryujinx.HLE.Loaders.Processes.Extensions;
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO; using System.IO;
using Path = System.IO.Path; using Path = System.IO.Path;
@@ -27,10 +28,15 @@ namespace Ryujinx.HLE.Loaders.Processes
private ulong _latestPid; private ulong _latestPid;
public ProcessResult ActiveApplication public ProcessResult? ActiveApplication
{ {
get 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)) if (!_processesByPid.TryGetValue(_latestPid, out ProcessResult value))
throw new RyujinxException( throw new RyujinxException(
$"The HLE Process map did not have a process with ID {_latestPid}. Are you missing firmware?"); $"The HLE Process map did not have a process with ID {_latestPid}. Are you missing firmware?");

View File

@@ -4,7 +4,6 @@ using LibHac.Ns;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Cpu; using Ryujinx.Cpu;
using Ryujinx.HLE.HOS.SystemState; using Ryujinx.HLE.HOS.SystemState;
using Ryujinx.HLE.Loaders.Processes.Extensions;
using Ryujinx.Horizon.Common; using Ryujinx.Horizon.Common;
namespace Ryujinx.HLE.Loaders.Processes namespace Ryujinx.HLE.Loaders.Processes
@@ -52,6 +51,7 @@ namespace Ryujinx.HLE.Loaders.Processes
if (metaLoader is not null) if (metaLoader is not null)
{ {
Logger.Info?.Print(LogClass.Application,$"metaLoader: {metaLoader}");
ulong programId = metaLoader.ProgramId; ulong programId = metaLoader.ProgramId;
Name = ApplicationControlProperties.Title[(int)titleLanguage].NameString.ToString(); Name = ApplicationControlProperties.Title[(int)titleLanguage].NameString.ToString();
@@ -73,6 +73,13 @@ namespace Ryujinx.HLE.Loaders.Processes
Is64Bit = metaLoader.IsProgram64Bit; Is64Bit = metaLoader.IsProgram64Bit;
} }
else
{
Logger.Error?.Print(LogClass.Application,$"metaLoader is null !!!");
ProcessId = 0;
return;
}
DiskCacheEnabled = diskCacheEnabled; DiskCacheEnabled = diskCacheEnabled;
AllowCodeMemoryForJit = allowCodeMemoryForJit; AllowCodeMemoryForJit = allowCodeMemoryForJit;
} }

View File

@@ -61,7 +61,7 @@ using VSyncMode = Ryujinx.Common.Configuration.VSyncMode;
namespace Ryujinx.Ava.Systems namespace Ryujinx.Ava.Systems
{ {
internal class AppHost internal class AppHost : IDisposable // notate this
{ {
private const int CursorHideIdleTime = 5; // Hide Cursor seconds. private const int CursorHideIdleTime = 5; // Hide Cursor seconds.
private const float MaxResolutionScale = 4.0f; // Max resolution hotkeys can scale to before wrapping. 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); 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. // 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.
_gpuDoneEvent.WaitOne();
WaitHandle.WaitAny(new []{_gpuDoneEvent, _gpuCancellationTokenSource.Token.WaitHandle}); // notate this
_gpuCancellationTokenSource.Dispose();
_gpuDoneEvent.Dispose(); _gpuDoneEvent.Dispose();
DisplaySleep.Restore(); DisplaySleep.Restore();
@@ -622,14 +624,16 @@ namespace Ryujinx.Ava.Systems
Device.Dispose(); Device.Dispose();
DisposeGpu(); DisposeGpu();
AppExit?.Invoke(this, EventArgs.Empty); AppExit?.Invoke(this, EventArgs.Empty);
} }
private void Dispose() public void Dispose() // notate this
{ {
if (Device.Processes != null) if (Device.Processes != null)
MainWindowViewModel.UpdateGameMetadata(Device.Processes.ActiveApplication.ProgramIdText, _playTimer.Elapsed); {
MainWindowViewModel.UpdateGameMetadata(Device.Processes.ActiveApplication?.ProgramIdText ?? "<INVALID>", // notate this
_playTimer.Elapsed);
}
ConfigurationState.Instance.System.IgnoreMissingServices.Event -= UpdateIgnoreMissingServicesState; ConfigurationState.Instance.System.IgnoreMissingServices.Event -= UpdateIgnoreMissingServicesState;
ConfigurationState.Instance.Graphics.AspectRatio.Event -= UpdateAspectRatioState; ConfigurationState.Instance.Graphics.AspectRatio.Event -= UpdateAspectRatioState;
@@ -645,7 +649,6 @@ namespace Ryujinx.Ava.Systems
_topLevel.PointerExited -= TopLevel_PointerExited; _topLevel.PointerExited -= TopLevel_PointerExited;
_gpuCancellationTokenSource.Cancel(); _gpuCancellationTokenSource.Cancel();
_gpuCancellationTokenSource.Dispose();
_chrono.Stop(); _chrono.Stop();
_playTimer.Stop(); _playTimer.Stop();
@@ -685,7 +688,7 @@ namespace Ryujinx.Ava.Systems
_cursorState = CursorStates.ForceChangeCursor; _cursorState = CursorStates.ForceChangeCursor;
} }
public async Task<bool> LoadGuestApplication(BlitStruct<ApplicationControlProperty>? customNacpData = null) public async Task LoadGuestApplication(CancellationTokenSource cts, BlitStruct<ApplicationControlProperty>? customNacpData = null)
{ {
DiscordIntegrationModule.GuestAppStartedAt = Timestamps.Now; DiscordIntegrationModule.GuestAppStartedAt = Timestamps.Now;
@@ -714,7 +717,8 @@ namespace Ryujinx.Ava.Systems
await UserErrorDialog.ShowUserErrorDialog(userError); await UserErrorDialog.ShowUserErrorDialog(userError);
Device.Dispose(); Device.Dispose();
return false; cts.Cancel();
throw new OperationCanceledException(cts.Token);
} }
} }
@@ -723,10 +727,11 @@ namespace Ryujinx.Ava.Systems
await UserErrorDialog.ShowUserErrorDialog(userError); await UserErrorDialog.ShowUserErrorDialog(userError);
Device.Dispose(); 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) if (userError is UserError.NoFirmware)
{ {
firmwareVersion = ContentManager.GetCurrentFirmwareVersion(); firmwareVersion = ContentManager.GetCurrentFirmwareVersion();
@@ -746,7 +751,8 @@ namespace Ryujinx.Ava.Systems
await UserErrorDialog.ShowUserErrorDialog(userError); await UserErrorDialog.ShowUserErrorDialog(userError);
Device.Dispose(); Device.Dispose();
return false; cts.Cancel();
throw new OperationCanceledException(cts.Token);
} }
} }
} }
@@ -761,7 +767,8 @@ namespace Ryujinx.Ava.Systems
{ {
Device.Dispose(); Device.Dispose();
return false; cts.Cancel();
throw new OperationCanceledException(cts.Token);
} }
} }
else if (Directory.Exists(ApplicationPath)) else if (Directory.Exists(ApplicationPath))
@@ -781,20 +788,24 @@ namespace Ryujinx.Ava.Systems
if (!Device.LoadCart(ApplicationPath, romFsFiles[0])) if (!Device.LoadCart(ApplicationPath, romFsFiles[0]))
{ {
ContentDialogHelper.CreateErrorDialog(
"Please specify an unpacked game directory with a valid exefs or NSO/NRO.");
Device.Dispose(); Device.Dispose();
return false; cts.Cancel();
throw new OperationCanceledException(cts.Token);
} }
} }
else else
{ {
Logger.Info?.Print(LogClass.Application, "Loading as cart WITHOUT RomFS."); Logger.Info?.Print(LogClass.Application, "Loading as cart WITHOUT RomFS.");
if (!Device.LoadCart(ApplicationPath)) if (!Device.LoadCart(ApplicationPath))
{ {
ContentDialogHelper.CreateErrorDialog(
"Please specify an unpacked game directory with a valid exefs or NSO/NRO.");
Device.Dispose(); Device.Dispose();
cts.Cancel();
return false; throw new OperationCanceledException(cts.Token);
} }
} }
} }
@@ -812,7 +823,8 @@ namespace Ryujinx.Ava.Systems
{ {
Device.Dispose(); Device.Dispose();
return false; cts.Cancel();
throw new OperationCanceledException(cts.Token);
} }
break; break;
@@ -825,7 +837,8 @@ namespace Ryujinx.Ava.Systems
{ {
Device.Dispose(); Device.Dispose();
return false; cts.Cancel();
throw new OperationCanceledException(cts.Token);
} }
break; break;
@@ -839,7 +852,8 @@ namespace Ryujinx.Ava.Systems
{ {
Device.Dispose(); Device.Dispose();
return false; cts.Cancel();
throw new OperationCanceledException(cts.Token);
} }
break; break;
@@ -854,7 +868,8 @@ namespace Ryujinx.Ava.Systems
{ {
Device.Dispose(); Device.Dispose();
return false; cts.Cancel();
throw new OperationCanceledException(cts.Token);
} }
} }
catch (ArgumentOutOfRangeException) catch (ArgumentOutOfRangeException)
@@ -863,7 +878,8 @@ namespace Ryujinx.Ava.Systems
Device.Dispose(); Device.Dispose();
return false; cts.Cancel();
throw new OperationCanceledException(cts.Token);
} }
break; break;
@@ -872,19 +888,18 @@ namespace Ryujinx.Ava.Systems
} }
else 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(); Device.Dispose();
return false; cts.Cancel();
throw new OperationCanceledException(cts.Token);
} }
ApplicationLibrary.LoadAndSaveMetaData(Device.Processes.ActiveApplication.ProgramIdText, ApplicationLibrary.LoadAndSaveMetaData(Device.Processes.ActiveApplication.ProgramIdText,
appMetadata => appMetadata.UpdatePreGame() appMetadata => appMetadata.UpdatePreGame()
); );
_playTimer.Start(); _playTimer.Start();
return true;
} }
internal void Resume() internal void Resume()
@@ -894,7 +909,7 @@ namespace Ryujinx.Ava.Systems
_viewModel.IsPaused = false; _viewModel.IsPaused = false;
_playTimer.Start(); _playTimer.Start();
_viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowOldUI); _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() internal void Pause()
@@ -904,7 +919,7 @@ namespace Ryujinx.Ava.Systems
_viewModel.IsPaused = true; _viewModel.IsPaused = true;
_playTimer.Stop(); _playTimer.Stop();
_viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowOldUI, LocaleManager.Instance[LocaleKeys.Paused]); _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() private void InitEmulatedSwitch()

View File

@@ -1168,6 +1168,7 @@ namespace Ryujinx.Ava.Systems.AppLibrary
if (!File.Exists(metadataFile)) if (!File.Exists(metadataFile))
{ {
Logger.Info?.Print(LogClass.Application, $"Metadata file does not exist. Creating metadata for {titleId}...");
Directory.CreateDirectory(metadataFolder); Directory.CreateDirectory(metadataFolder);
appMetadata = new ApplicationMetadata(); appMetadata = new ApplicationMetadata();
@@ -1177,12 +1178,12 @@ namespace Ryujinx.Ava.Systems.AppLibrary
try try
{ {
Logger.Debug?.Print(LogClass.Application, $"Deserializing metadata for {titleId}...");
appMetadata = JsonHelper.DeserializeFromFile(metadataFile, _serializerContext.ApplicationMetadata); appMetadata = JsonHelper.DeserializeFromFile(metadataFile, _serializerContext.ApplicationMetadata);
} }
catch (JsonException) catch (JsonException)
{ {
Logger.Warning?.Print(LogClass.Application, $"Failed to parse metadata json for {titleId}. Loading defaults."); Logger.Warning?.Print(LogClass.Application, $"Failed to parse metadata json for {titleId}. Loading defaults.");
appMetadata = new ApplicationMetadata(); appMetadata = new ApplicationMetadata();
} }

View File

@@ -1700,11 +1700,6 @@ namespace Ryujinx.Ava.UI.ViewModels
Logger.RestartTime(); Logger.RestartTime();
SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(application.Path,
ConfigurationState.Instance.System.Language, application.Id);
PrepareLoadScreen();
RendererHostControl = new RendererHost(); RendererHostControl = new RendererHost();
AppHost = new AppHost( AppHost = new AppHost(
@@ -1719,18 +1714,36 @@ namespace Ryujinx.Ava.UI.ViewModels
this, this,
TopLevel); TopLevel);
if (!await AppHost.LoadGuestApplication(customNacpData)) // Needs a new name to better fit code styling
CancellationTokenSource cts = new CancellationTokenSource();
try
{ {
await AppHost.LoadGuestApplication(cts, customNacpData);
}
catch (OperationCanceledException exception)
{
Logger.Info?.Print(LogClass.Application,
"LoadGuestApplication was interrupted !!! " + exception.Message);
AppHost.DisposeContext(); AppHost.DisposeContext();
AppHost = null; AppHost = null;
return; return;
} }
finally
{
cts.Dispose();
}
CanUpdate = false; CanUpdate = false;
application.Name ??= AppHost.Device.Processes.ActiveApplication.Name; 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); LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, application.Name);
SwitchToRenderer(startFullscreen); SwitchToRenderer(startFullscreen);
@@ -1752,7 +1765,8 @@ namespace Ryujinx.Ava.UI.ViewModels
}); });
public static void UpdateGameMetadata(string titleId, TimeSpan playTime) 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() public void RefreshFirmwareStatus()
{ {