From 263eb97f79f02db12c2499ceec7e957b91bc008b Mon Sep 17 00:00:00 2001 From: TSRBerry <20988865+TSRBerry@users.noreply.github.com> Date: Sat, 3 Aug 2024 23:31:34 +0200 Subject: [PATCH] Avoid race conditions while launching games directly from the command line (#7116) * optimization: Load application metadata only for applications with IDs * Load applications when necessary This prevents loading applications when launching an application directly from the command line (or a shortcut). Instead, applications will be loaded after the emulation was stopped by the user. * Show the title in the configured language when launching an application * Rename DesiredTitleLanguage to DesiredLanguage --- src/Ryujinx.Gtk3/Program.cs | 6 ++ src/Ryujinx.Gtk3/UI/MainWindow.cs | 9 ++- .../App/ApplicationLibrary.cs | 59 ++++++++++--------- src/Ryujinx/UI/Windows/MainWindow.axaml.cs | 22 ++++++- 4 files changed, 62 insertions(+), 34 deletions(-) diff --git a/src/Ryujinx.Gtk3/Program.cs b/src/Ryujinx.Gtk3/Program.cs index 0fb712885b..8bb6516409 100644 --- a/src/Ryujinx.Gtk3/Program.cs +++ b/src/Ryujinx.Gtk3/Program.cs @@ -256,6 +256,12 @@ namespace Ryujinx MainWindow mainWindow = new(); mainWindow.Show(); + // Load the game table if no application was requested by the command line + if (CommandLineState.LaunchPathArg == null) + { + mainWindow.UpdateGameTable(); + } + if (OperatingSystem.IsLinux()) { int currentVmMaxMapCount = LinuxHelper.VmMaxMapCount; diff --git a/src/Ryujinx.Gtk3/UI/MainWindow.cs b/src/Ryujinx.Gtk3/UI/MainWindow.cs index 7f9eceb38a..66c0afae03 100644 --- a/src/Ryujinx.Gtk3/UI/MainWindow.cs +++ b/src/Ryujinx.Gtk3/UI/MainWindow.cs @@ -187,7 +187,10 @@ namespace Ryujinx.UI : IntegrityCheckLevel.None; // Instantiate GUI objects. - ApplicationLibrary = new ApplicationLibrary(_virtualFileSystem, checkLevel); + ApplicationLibrary = new ApplicationLibrary(_virtualFileSystem, checkLevel) + { + DesiredLanguage = ConfigurationState.Instance.System.Language, + }; _uiHandler = new GtkHostUIHandler(this); _deviceExitStatus = new AutoResetEvent(false); @@ -325,7 +328,6 @@ namespace Ryujinx.UI _hideUI.Label = _hideUI.Label.Replace("SHOWUIKEY", ConfigurationState.Instance.Hid.Hotkeys.Value.ShowUI.ToString()); UpdateColumns(); - UpdateGameTable(); ConfigurationState.Instance.UI.GameDirs.Event += (sender, args) => { @@ -738,7 +740,8 @@ namespace Ryujinx.UI Thread applicationLibraryThread = new(() => { - ApplicationLibrary.LoadApplications(ConfigurationState.Instance.UI.GameDirs, ConfigurationState.Instance.System.Language); + ApplicationLibrary.DesiredLanguage = ConfigurationState.Instance.System.Language; + ApplicationLibrary.LoadApplications(ConfigurationState.Instance.UI.GameDirs); _updatingGameTable = false; }) diff --git a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs index cd95c4d870..2defc1f6c8 100644 --- a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs +++ b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs @@ -34,6 +34,7 @@ namespace Ryujinx.UI.App.Common { public class ApplicationLibrary { + public Language DesiredLanguage { get; set; } public event EventHandler ApplicationAdded; public event EventHandler ApplicationCountUpdated; @@ -45,7 +46,6 @@ namespace Ryujinx.UI.App.Common private readonly VirtualFileSystem _virtualFileSystem; private readonly IntegrityCheckLevel _checkLevel; - private Language _desiredTitleLanguage; private CancellationTokenSource _cancellationToken; private static readonly ApplicationJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); @@ -221,7 +221,7 @@ namespace Ryujinx.UI.App.Common { using UniqueRef icon = new(); - controlFs.OpenFile(ref icon.Ref, $"/icon_{_desiredTitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure(); + controlFs.OpenFile(ref icon.Ref, $"/icon_{DesiredLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure(); using MemoryStream stream = new(); @@ -432,35 +432,40 @@ namespace Ryujinx.UI.App.Common foreach (var data in applications) { - ApplicationMetadata appMetadata = LoadAndSaveMetaData(data.IdString, appMetadata => + // Only load metadata for applications with an ID + if (data.Id != 0) { - appMetadata.Title = data.Name; - - // Only do the migration if time_played has a value and timespan_played hasn't been updated yet. - if (appMetadata.TimePlayedOld != default && appMetadata.TimePlayed == TimeSpan.Zero) + ApplicationMetadata appMetadata = LoadAndSaveMetaData(data.IdString, appMetadata => { - appMetadata.TimePlayed = TimeSpan.FromSeconds(appMetadata.TimePlayedOld); - appMetadata.TimePlayedOld = default; - } + appMetadata.Title = data.Name; - // Only do the migration if last_played has a value and last_played_utc doesn't exist yet. - if (appMetadata.LastPlayedOld != default && !appMetadata.LastPlayed.HasValue) - { - // Migrate from string-based last_played to DateTime-based last_played_utc. - if (DateTime.TryParse(appMetadata.LastPlayedOld, out DateTime lastPlayedOldParsed)) + // Only do the migration if time_played has a value and timespan_played hasn't been updated yet. + if (appMetadata.TimePlayedOld != default && appMetadata.TimePlayed == TimeSpan.Zero) { - appMetadata.LastPlayed = lastPlayedOldParsed; - - // Migration successful: deleting last_played from the metadata file. - appMetadata.LastPlayedOld = default; + appMetadata.TimePlayed = TimeSpan.FromSeconds(appMetadata.TimePlayedOld); + appMetadata.TimePlayedOld = default; } - } - }); + // Only do the migration if last_played has a value and last_played_utc doesn't exist yet. + if (appMetadata.LastPlayedOld != default && !appMetadata.LastPlayed.HasValue) + { + // Migrate from string-based last_played to DateTime-based last_played_utc. + if (DateTime.TryParse(appMetadata.LastPlayedOld, out DateTime lastPlayedOldParsed)) + { + appMetadata.LastPlayed = lastPlayedOldParsed; + + // Migration successful: deleting last_played from the metadata file. + appMetadata.LastPlayedOld = default; + } + + } + }); + + data.Favorite = appMetadata.Favorite; + data.TimePlayed = appMetadata.TimePlayed; + data.LastPlayed = appMetadata.LastPlayed; + } - data.Favorite = appMetadata.Favorite; - data.TimePlayed = appMetadata.TimePlayed; - data.LastPlayed = appMetadata.LastPlayed; data.FileExtension = Path.GetExtension(applicationPath).TrimStart('.').ToUpper(); data.FileSize = fileSize; data.Path = applicationPath; @@ -482,13 +487,11 @@ namespace Ryujinx.UI.App.Common controlFile.Get.Read(out _, 0, outProperty, ReadOption.None).ThrowIfFailure(); } - public void LoadApplications(List appDirs, Language desiredTitleLanguage) + public void LoadApplications(List appDirs) { int numApplicationsFound = 0; int numApplicationsLoaded = 0; - _desiredTitleLanguage = desiredTitleLanguage; - _cancellationToken = new CancellationTokenSource(); // Builds the applications list with paths to found applications @@ -847,7 +850,7 @@ namespace Ryujinx.UI.App.Common private void GetApplicationInformation(ref ApplicationControlProperty controlData, ref ApplicationData data) { - _ = Enum.TryParse(_desiredTitleLanguage.ToString(), out TitleLanguage desiredTitleLanguage); + _ = Enum.TryParse(DesiredLanguage.ToString(), out TitleLanguage desiredTitleLanguage); if (controlData.Title.ItemsRo.Length > (int)desiredTitleLanguage) { diff --git a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs index dc5336ab3f..348412e78c 100644 --- a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs @@ -37,6 +37,7 @@ namespace Ryujinx.Ava.UI.Windows internal static MainWindowViewModel MainWindowViewModel { get; private set; } private bool _isLoading; + private bool _applicationsLoadedOnce; private UserChannelPersistence _userChannelPersistence; private static bool _deferLoad; @@ -224,7 +225,10 @@ namespace Ryujinx.Ava.UI.Windows ? IntegrityCheckLevel.ErrorOnInvalid : IntegrityCheckLevel.None; - ApplicationLibrary = new ApplicationLibrary(VirtualFileSystem, checkLevel); + ApplicationLibrary = new ApplicationLibrary(VirtualFileSystem, checkLevel) + { + DesiredLanguage = ConfigurationState.Instance.System.Language, + }; // Save data created before we supported extra data in directory save data will not work properly if // given empty extra data. Luckily some of that extra data can be created using the data from the @@ -472,7 +476,11 @@ namespace Ryujinx.Ava.UI.Windows ViewModel.RefreshFirmwareStatus(); - LoadApplications(); + // Load applications if no application was requested by the command line + if (!_deferLoad) + { + LoadApplications(); + } #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed CheckLaunchState(); @@ -485,6 +493,12 @@ namespace Ryujinx.Ava.UI.Windows if (MainContent.Content != content) { + // Load applications while switching to the GameLibrary if we haven't done that yet + if (!_applicationsLoadedOnce && content == GameLibrary) + { + LoadApplications(); + } + MainContent.Content = content; } } @@ -581,6 +595,7 @@ namespace Ryujinx.Ava.UI.Windows public void LoadApplications() { + _applicationsLoadedOnce = true; ViewModel.Applications.Clear(); StatusBarView.LoadProgressBar.IsVisible = true; @@ -622,7 +637,8 @@ namespace Ryujinx.Ava.UI.Windows Thread applicationLibraryThread = new(() => { - ApplicationLibrary.LoadApplications(ConfigurationState.Instance.UI.GameDirs, ConfigurationState.Instance.System.Language); + ApplicationLibrary.DesiredLanguage = ConfigurationState.Instance.System.Language; + ApplicationLibrary.LoadApplications(ConfigurationState.Instance.UI.GameDirs); _isLoading = false; })