From 617c5700ca520d80fd25fc0fc9f2389394a46150 Mon Sep 17 00:00:00 2001 From: Somebody Whoisbored <13044396+shadowninja108@users.noreply.github.com> Date: Sun, 5 Nov 2023 04:32:17 -0700 Subject: [PATCH 01/23] Better handle instruction aborts when hitting unmapped memory (#5869) * Adjust ARMeilleure to better handle instruction aborts when hitting unmapped memory * Update src/ARMeilleure/Decoders/Decoder.cs Co-authored-by: gdkchan --------- Co-authored-by: gdkchan --- src/ARMeilleure/Decoders/Decoder.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ARMeilleure/Decoders/Decoder.cs b/src/ARMeilleure/Decoders/Decoder.cs index 6d07827a2..66d286928 100644 --- a/src/ARMeilleure/Decoders/Decoder.cs +++ b/src/ARMeilleure/Decoders/Decoder.cs @@ -38,7 +38,9 @@ namespace ARMeilleure.Decoders { block = new Block(blkAddress); - if ((dMode != DecoderMode.MultipleBlocks && visited.Count >= 1) || opsCount > instructionLimit || !memory.IsMapped(blkAddress)) + if ((dMode != DecoderMode.MultipleBlocks && visited.Count >= 1) || + opsCount > instructionLimit || + (visited.Count > 0 && !memory.IsMapped(blkAddress))) { block.Exit = true; block.EndAddress = blkAddress; From 623604c39186901fd64c8e04e9aa959d5c825529 Mon Sep 17 00:00:00 2001 From: SamusAranX Date: Mon, 6 Nov 2023 22:47:44 +0100 Subject: [PATCH 02/23] Overhaul of string formatting/parsing/sorting logic for TimeSpans, DateTimes, and file sizes (#4956) * Fixed formatting/parsing issues with ApplicationData properties TimePlayed, LastPlayed, and FileSize Replaced double-based TimePlayed property with TimeSpan?-based one in ApplicationData and ApplicationMetadata Added a migration for TimePlayed, just like in #4861 Consolidated ApplicationData's FileSize* properties into one FileSize property Added a formatting/parsing helper class ValueFormatUtils for TimeSpans, DateTimes, and file sizes Added new value converters for TimeSpans and file sizes for the Avalonia UI Added TimePlayedSortComparer Fixed sort order in LastPlayedSortComparer Fixed sort order for ApplicationData fields TimePlayed, LastPlayed, and FileSize Fixed crashes caused by SortHelper Replaced SystemInfo.ToMiBString with ToGiBString backed by ValueFormatUtils Replaced SaveModel.GetSizeString() with ValueFormatUtils * Additional ApplicationLibrary changes that got lost in the last commit * Removed unneeded usings * Removed converters as they are no longer needed * Updated comment on FormatDateTime * Removed base10 parameter from ValueFormatUtils FormatFileSize now always returns base 2 values with base 10 units Made ParseFileSize capable of parsing both base 2 and base 10 units * Removed nullable attribute from TimePlayed property Centralized TimePlayed update code into ApplicationMetadata * Changed UpdateTimePlayed() to use TimeSpan logic * Removed JsonIgnore attributes from ApplicationData * Implemented requested format changes * Fixed mistakes in method documentation comments * Made it so the Last Played value "Never" is localized in the Avalonia UI * Implemented suggestions * Remove unused import * Did a comment refinement pass in ValueFormatUtils.cs * Reordered ValueFormatUtils methods and sorted them into #regions * Integrated functionality from #5056 Also removed Logger print from last_played migration code * Implemented suggestions * Moved ValueFormatUtils and SystemInfo to namespace Ryujinx.Ui.Common * common: Respect proper value format convention and use base10 by default This could be discuss again in another issue/PR, for now revert to the previous behavior. Signed-off-by: Mary Guillemard --------- Signed-off-by: Mary Guillemard Co-authored-by: TSR Berry <20988865+TSRBerry@users.noreply.github.com> Co-authored-by: Mary Guillemard --- src/Ryujinx.Ava/AppHost.cs | 2 +- src/Ryujinx.Ava/Program.cs | 2 +- .../UI/Controls/ApplicationListView.axaml | 6 +- .../UI/Helpers/LocalizedNeverConverter.cs | 43 ++++ .../UI/Helpers/NullableDateTimeConverter.cs | 38 --- .../Models/Generic/LastPlayedSortComparer.cs | 13 +- .../Models/Generic/TimePlayedSortComparer.cs | 31 +++ src/Ryujinx.Ava/UI/Models/SaveModel.cs | 23 +- .../UI/ViewModels/MainWindowViewModel.cs | 31 +-- src/Ryujinx.Ui.Common/App/ApplicationData.cs | 26 +-- .../App/ApplicationLibrary.cs | 63 ++--- .../App/ApplicationMetadata.cs | 34 ++- .../Helper/ValueFormatUtils.cs | 219 ++++++++++++++++++ .../SystemInfo/LinuxSystemInfo.cs | 2 +- .../SystemInfo/MacOSSystemInfo.cs | 2 +- .../SystemInfo/SystemInfo.cs | 7 +- .../SystemInfo/WindowsSystemInfo.cs | 2 +- src/Ryujinx/Program.cs | 2 +- src/Ryujinx/Ui/Helper/SortHelper.cs | 81 +------ src/Ryujinx/Ui/MainWindow.cs | 14 +- 20 files changed, 398 insertions(+), 243 deletions(-) create mode 100644 src/Ryujinx.Ava/UI/Helpers/LocalizedNeverConverter.cs delete mode 100644 src/Ryujinx.Ava/UI/Helpers/NullableDateTimeConverter.cs create mode 100644 src/Ryujinx.Ava/UI/Models/Generic/TimePlayedSortComparer.cs create mode 100644 src/Ryujinx.Ui.Common/Helper/ValueFormatUtils.cs rename src/{Ryujinx.Common => Ryujinx.Ui.Common}/SystemInfo/LinuxSystemInfo.cs (98%) rename src/{Ryujinx.Common => Ryujinx.Ui.Common}/SystemInfo/MacOSSystemInfo.cs (99%) rename src/{Ryujinx.Common => Ryujinx.Ui.Common}/SystemInfo/SystemInfo.cs (87%) rename src/{Ryujinx.Common => Ryujinx.Ui.Common}/SystemInfo/WindowsSystemInfo.cs (98%) diff --git a/src/Ryujinx.Ava/AppHost.cs b/src/Ryujinx.Ava/AppHost.cs index cd066efba..4d751e2a9 100644 --- a/src/Ryujinx.Ava/AppHost.cs +++ b/src/Ryujinx.Ava/AppHost.cs @@ -716,7 +716,7 @@ namespace Ryujinx.Ava ApplicationLibrary.LoadAndSaveMetaData(Device.Processes.ActiveApplication.ProgramIdText, appMetadata => { - appMetadata.LastPlayed = DateTime.UtcNow; + appMetadata.UpdatePreGame(); }); return true; diff --git a/src/Ryujinx.Ava/Program.cs b/src/Ryujinx.Ava/Program.cs index 168e9216d..cc062a256 100644 --- a/src/Ryujinx.Ava/Program.cs +++ b/src/Ryujinx.Ava/Program.cs @@ -6,13 +6,13 @@ using Ryujinx.Common; using Ryujinx.Common.Configuration; using Ryujinx.Common.GraphicsDriver; using Ryujinx.Common.Logging; -using Ryujinx.Common.SystemInfo; using Ryujinx.Common.SystemInterop; using Ryujinx.Modules; using Ryujinx.SDL2.Common; using Ryujinx.Ui.Common; using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Common.Helper; +using Ryujinx.Ui.Common.SystemInfo; using System; using System.IO; using System.Runtime.InteropServices; diff --git a/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml b/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml index 09011005b..9004f7518 100644 --- a/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml +++ b/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml @@ -126,17 +126,17 @@ Spacing="5"> diff --git a/src/Ryujinx.Ava/UI/Helpers/LocalizedNeverConverter.cs b/src/Ryujinx.Ava/UI/Helpers/LocalizedNeverConverter.cs new file mode 100644 index 000000000..737896986 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Helpers/LocalizedNeverConverter.cs @@ -0,0 +1,43 @@ +using Avalonia.Data.Converters; +using Avalonia.Markup.Xaml; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ui.Common.Helper; +using System; +using System.Globalization; + +namespace Ryujinx.Ava.UI.Helpers +{ + /// + /// This makes sure that the string "Never" that's returned by is properly localized in the Avalonia UI. + /// After the Avalonia UI has been made the default and the GTK UI is removed, should be updated to directly return a localized string. + /// + internal class LocalizedNeverConverter : MarkupExtension, IValueConverter + { + private static readonly LocalizedNeverConverter _instance = new(); + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is not string valStr) + { + return ""; + } + + if (valStr == "Never") + { + return LocaleManager.Instance[LocaleKeys.Never]; + } + + return valStr; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } + + public override object ProvideValue(IServiceProvider serviceProvider) + { + return _instance; + } + } +} diff --git a/src/Ryujinx.Ava/UI/Helpers/NullableDateTimeConverter.cs b/src/Ryujinx.Ava/UI/Helpers/NullableDateTimeConverter.cs deleted file mode 100644 index e91937612..000000000 --- a/src/Ryujinx.Ava/UI/Helpers/NullableDateTimeConverter.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Avalonia.Data.Converters; -using Avalonia.Markup.Xaml; -using Ryujinx.Ava.Common.Locale; -using System; -using System.Globalization; - -namespace Ryujinx.Ava.UI.Helpers -{ - internal class NullableDateTimeConverter : MarkupExtension, IValueConverter - { - private static readonly NullableDateTimeConverter _instance = new(); - - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) - { - if (value == null) - { - return LocaleManager.Instance[LocaleKeys.Never]; - } - - if (value is DateTime dateTime) - { - return dateTime.ToLocalTime().ToString(culture); - } - - throw new NotSupportedException(); - } - - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) - { - throw new NotSupportedException(); - } - - public override object ProvideValue(IServiceProvider serviceProvider) - { - return _instance; - } - } -} diff --git a/src/Ryujinx.Ava/UI/Models/Generic/LastPlayedSortComparer.cs b/src/Ryujinx.Ava/UI/Models/Generic/LastPlayedSortComparer.cs index 8a4346556..8340d39df 100644 --- a/src/Ryujinx.Ava/UI/Models/Generic/LastPlayedSortComparer.cs +++ b/src/Ryujinx.Ava/UI/Models/Generic/LastPlayedSortComparer.cs @@ -13,20 +13,19 @@ namespace Ryujinx.Ava.UI.Models.Generic public int Compare(ApplicationData x, ApplicationData y) { - var aValue = x.LastPlayed; - var bValue = y.LastPlayed; + DateTime aValue = DateTime.UnixEpoch, bValue = DateTime.UnixEpoch; - if (!aValue.HasValue) + if (x?.LastPlayed != null) { - aValue = DateTime.UnixEpoch; + aValue = x.LastPlayed.Value; } - if (!bValue.HasValue) + if (y?.LastPlayed != null) { - bValue = DateTime.UnixEpoch; + bValue = y.LastPlayed.Value; } - return (IsAscending ? 1 : -1) * DateTime.Compare(bValue.Value, aValue.Value); + return (IsAscending ? 1 : -1) * DateTime.Compare(aValue, bValue); } } } diff --git a/src/Ryujinx.Ava/UI/Models/Generic/TimePlayedSortComparer.cs b/src/Ryujinx.Ava/UI/Models/Generic/TimePlayedSortComparer.cs new file mode 100644 index 000000000..d53ff566f --- /dev/null +++ b/src/Ryujinx.Ava/UI/Models/Generic/TimePlayedSortComparer.cs @@ -0,0 +1,31 @@ +using Ryujinx.Ui.App.Common; +using System; +using System.Collections.Generic; + +namespace Ryujinx.Ava.UI.Models.Generic +{ + internal class TimePlayedSortComparer : IComparer + { + public TimePlayedSortComparer() { } + public TimePlayedSortComparer(bool isAscending) { IsAscending = isAscending; } + + public bool IsAscending { get; } + + public int Compare(ApplicationData x, ApplicationData y) + { + TimeSpan aValue = TimeSpan.Zero, bValue = TimeSpan.Zero; + + if (x?.TimePlayed != null) + { + aValue = x.TimePlayed; + } + + if (y?.TimePlayed != null) + { + bValue = y.TimePlayed; + } + + return (IsAscending ? 1 : -1) * TimeSpan.Compare(aValue, bValue); + } + } +} diff --git a/src/Ryujinx.Ava/UI/Models/SaveModel.cs b/src/Ryujinx.Ava/UI/Models/SaveModel.cs index f15befbb3..7b476932b 100644 --- a/src/Ryujinx.Ava/UI/Models/SaveModel.cs +++ b/src/Ryujinx.Ava/UI/Models/SaveModel.cs @@ -4,7 +4,7 @@ using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Ava.UI.Windows; using Ryujinx.HLE.FileSystem; using Ryujinx.Ui.App.Common; -using System; +using Ryujinx.Ui.Common.Helper; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -38,26 +38,7 @@ namespace Ryujinx.Ava.UI.Models public bool SizeAvailable { get; set; } - public string SizeString => GetSizeString(); - - private string GetSizeString() - { - const int Scale = 1024; - string[] orders = { "GiB", "MiB", "KiB" }; - long max = (long)Math.Pow(Scale, orders.Length); - - foreach (string order in orders) - { - if (Size > max) - { - return $"{decimal.Divide(Size, max):##.##} {order}"; - } - - max /= Scale; - } - - return "0 KiB"; - } + public string SizeString => ValueFormatUtils.FormatFileSize(Size); public SaveModel(SaveDataInfo info) { diff --git a/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs index b14905204..80df5d398 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs @@ -930,21 +930,20 @@ namespace Ryujinx.Ava.UI.ViewModels return SortMode switch { #pragma warning disable IDE0055 // Disable formatting + ApplicationSort.Title => IsAscending ? SortExpressionComparer.Ascending(app => app.TitleName) + : SortExpressionComparer.Descending(app => app.TitleName), + ApplicationSort.Developer => IsAscending ? SortExpressionComparer.Ascending(app => app.Developer) + : SortExpressionComparer.Descending(app => app.Developer), ApplicationSort.LastPlayed => new LastPlayedSortComparer(IsAscending), - ApplicationSort.FileSize => IsAscending ? SortExpressionComparer.Ascending(app => app.FileSizeBytes) - : SortExpressionComparer.Descending(app => app.FileSizeBytes), - ApplicationSort.TotalTimePlayed => IsAscending ? SortExpressionComparer.Ascending(app => app.TimePlayedNum) - : SortExpressionComparer.Descending(app => app.TimePlayedNum), - ApplicationSort.Title => IsAscending ? SortExpressionComparer.Ascending(app => app.TitleName) - : SortExpressionComparer.Descending(app => app.TitleName), + ApplicationSort.TotalTimePlayed => new TimePlayedSortComparer(IsAscending), + ApplicationSort.FileType => IsAscending ? SortExpressionComparer.Ascending(app => app.FileExtension) + : SortExpressionComparer.Descending(app => app.FileExtension), + ApplicationSort.FileSize => IsAscending ? SortExpressionComparer.Ascending(app => app.FileSize) + : SortExpressionComparer.Descending(app => app.FileSize), + ApplicationSort.Path => IsAscending ? SortExpressionComparer.Ascending(app => app.Path) + : SortExpressionComparer.Descending(app => app.Path), ApplicationSort.Favorite => !IsAscending ? SortExpressionComparer.Ascending(app => app.Favorite) : SortExpressionComparer.Descending(app => app.Favorite), - ApplicationSort.Developer => IsAscending ? SortExpressionComparer.Ascending(app => app.Developer) - : SortExpressionComparer.Descending(app => app.Developer), - ApplicationSort.FileType => IsAscending ? SortExpressionComparer.Ascending(app => app.FileExtension) - : SortExpressionComparer.Descending(app => app.FileExtension), - ApplicationSort.Path => IsAscending ? SortExpressionComparer.Ascending(app => app.Path) - : SortExpressionComparer.Descending(app => app.Path), _ => null, #pragma warning restore IDE0055 }; @@ -1549,13 +1548,7 @@ namespace Ryujinx.Ava.UI.ViewModels { ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => { - if (appMetadata.LastPlayed.HasValue) - { - double sessionTimePlayed = DateTime.UtcNow.Subtract(appMetadata.LastPlayed.Value).TotalSeconds; - appMetadata.TimePlayed += Math.Round(sessionTimePlayed, MidpointRounding.AwayFromZero); - } - - appMetadata.LastPlayed = DateTime.UtcNow; + appMetadata.UpdatePostGame(); }); } diff --git a/src/Ryujinx.Ui.Common/App/ApplicationData.cs b/src/Ryujinx.Ui.Common/App/ApplicationData.cs index 1be883ee1..65ab01eeb 100644 --- a/src/Ryujinx.Ui.Common/App/ApplicationData.cs +++ b/src/Ryujinx.Ui.Common/App/ApplicationData.cs @@ -9,10 +9,9 @@ using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.NcaUtils; using Ryujinx.Common.Logging; using Ryujinx.HLE.FileSystem; +using Ryujinx.Ui.Common.Helper; using System; -using System.Globalization; using System.IO; -using System.Text.Json.Serialization; namespace Ryujinx.Ui.App.Common { @@ -24,29 +23,18 @@ namespace Ryujinx.Ui.App.Common public string TitleId { get; set; } public string Developer { get; set; } public string Version { get; set; } - public string TimePlayed { get; set; } - public double TimePlayedNum { get; set; } + public TimeSpan TimePlayed { get; set; } public DateTime? LastPlayed { get; set; } public string FileExtension { get; set; } - public string FileSize { get; set; } - public double FileSizeBytes { get; set; } + public long FileSize { get; set; } public string Path { get; set; } public BlitStruct ControlHolder { get; set; } - [JsonIgnore] - public string LastPlayedString - { - get - { - if (!LastPlayed.HasValue) - { - // TODO: maybe put localized string here instead of just "Never" - return "Never"; - } + public string TimePlayedString => ValueFormatUtils.FormatTimeSpan(TimePlayed); - return LastPlayed.Value.ToLocalTime().ToString(CultureInfo.CurrentCulture); - } - } + public string LastPlayedString => ValueFormatUtils.FormatDateTime(LastPlayed); + + public string FileSizeString => ValueFormatUtils.FormatFileSize(FileSize); public static string GetApplicationBuildId(VirtualFileSystem virtualFileSystem, string titleFilePath) { diff --git a/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs b/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs index 2f688126a..46f29851c 100644 --- a/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs +++ b/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs @@ -155,7 +155,7 @@ namespace Ryujinx.Ui.App.Common return; } - double fileSize = new FileInfo(applicationPath).Length * 0.000000000931; + long fileSize = new FileInfo(applicationPath).Length; string titleName = "Unknown"; string titleId = "0000000000000000"; string developer = "Unknown"; @@ -425,25 +425,25 @@ namespace Ryujinx.Ui.App.Common { appMetadata.Title = titleName; - if (appMetadata.LastPlayedOld == default || appMetadata.LastPlayed.HasValue) + // 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) { - // Don't do the migration if last_played doesn't exist or last_played_utc already has a value. - return; + appMetadata.TimePlayed = TimeSpan.FromSeconds(appMetadata.TimePlayedOld); + appMetadata.TimePlayedOld = default; } - // 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 last_played has a value and last_played_utc doesn't exist yet. + if (appMetadata.LastPlayedOld != default && !appMetadata.LastPlayed.HasValue) { - Logger.Info?.Print(LogClass.Application, $"last_played found: \"{appMetadata.LastPlayedOld}\", migrating to last_played_utc"); - appMetadata.LastPlayed = lastPlayedOldParsed; + // 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; + } - // Migration successful: deleting last_played from the metadata file. - appMetadata.LastPlayedOld = default; - } - else - { - // Migration failed: emitting warning but leaving the unparsable value in the metadata file so the user can fix it. - Logger.Warning?.Print(LogClass.Application, $"Last played string \"{appMetadata.LastPlayedOld}\" is invalid for current system culture, skipping (did current culture change?)"); } }); @@ -455,12 +455,10 @@ namespace Ryujinx.Ui.App.Common TitleId = titleId, Developer = developer, Version = version, - TimePlayed = ConvertSecondsToFormattedString(appMetadata.TimePlayed), - TimePlayedNum = appMetadata.TimePlayed, + TimePlayed = appMetadata.TimePlayed, LastPlayed = appMetadata.LastPlayed, - FileExtension = Path.GetExtension(applicationPath).ToUpper().Remove(0, 1), - FileSize = (fileSize < 1) ? (fileSize * 1024).ToString("0.##") + " MiB" : fileSize.ToString("0.##") + " GiB", - FileSizeBytes = fileSize, + FileExtension = Path.GetExtension(applicationPath).TrimStart('.').ToUpper(), + FileSize = fileSize, Path = applicationPath, ControlHolder = controlHolder, }; @@ -716,31 +714,6 @@ namespace Ryujinx.Ui.App.Common return applicationIcon ?? _ncaIcon; } - private static string ConvertSecondsToFormattedString(double seconds) - { - TimeSpan time = TimeSpan.FromSeconds(seconds); - - string timeString; - if (time.Days != 0) - { - timeString = $"{time.Days}d {time.Hours:D2}h {time.Minutes:D2}m"; - } - else if (time.Hours != 0) - { - timeString = $"{time.Hours:D2}h {time.Minutes:D2}m"; - } - else if (time.Minutes != 0) - { - timeString = $"{time.Minutes:D2}m"; - } - else - { - timeString = "Never"; - } - - return timeString; - } - private void GetGameInformation(ref ApplicationControlProperty controlData, out string titleName, out string titleId, out string publisher, out string version) { _ = Enum.TryParse(_desiredTitleLanguage.ToString(), out TitleLanguage desiredTitleLanguage); diff --git a/src/Ryujinx.Ui.Common/App/ApplicationMetadata.cs b/src/Ryujinx.Ui.Common/App/ApplicationMetadata.cs index 01b857a62..9e2ca6870 100644 --- a/src/Ryujinx.Ui.Common/App/ApplicationMetadata.cs +++ b/src/Ryujinx.Ui.Common/App/ApplicationMetadata.cs @@ -7,13 +7,45 @@ namespace Ryujinx.Ui.App.Common { public string Title { get; set; } public bool Favorite { get; set; } - public double TimePlayed { get; set; } + + [JsonPropertyName("timespan_played")] + public TimeSpan TimePlayed { get; set; } = TimeSpan.Zero; [JsonPropertyName("last_played_utc")] public DateTime? LastPlayed { get; set; } = null; + [JsonPropertyName("time_played")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public double TimePlayedOld { get; set; } + [JsonPropertyName("last_played")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public string LastPlayedOld { get; set; } + + /// + /// Updates . Call this before launching a game. + /// + public void UpdatePreGame() + { + LastPlayed = DateTime.UtcNow; + } + + /// + /// Updates and . Call this after a game ends. + /// + public void UpdatePostGame() + { + DateTime? prevLastPlayed = LastPlayed; + UpdatePreGame(); + + if (!prevLastPlayed.HasValue) + { + return; + } + + TimeSpan diff = DateTime.UtcNow - prevLastPlayed.Value; + double newTotalSeconds = TimePlayed.Add(diff).TotalSeconds; + TimePlayed = TimeSpan.FromSeconds(Math.Round(newTotalSeconds, MidpointRounding.AwayFromZero)); + } } } diff --git a/src/Ryujinx.Ui.Common/Helper/ValueFormatUtils.cs b/src/Ryujinx.Ui.Common/Helper/ValueFormatUtils.cs new file mode 100644 index 000000000..951cd089e --- /dev/null +++ b/src/Ryujinx.Ui.Common/Helper/ValueFormatUtils.cs @@ -0,0 +1,219 @@ +using System; +using System.Globalization; +using System.Linq; + +namespace Ryujinx.Ui.Common.Helper +{ + public static class ValueFormatUtils + { + private static readonly string[] _fileSizeUnitStrings = + { + "B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", // Base 10 units, used for formatting and parsing + "KB", "MB", "GB", "TB", "PB", "EB", // Base 2 units, used for parsing legacy values + }; + + /// + /// Used by . + /// + public enum FileSizeUnits + { + Auto = -1, + Bytes = 0, + Kibibytes = 1, + Mebibytes = 2, + Gibibytes = 3, + Tebibytes = 4, + Pebibytes = 5, + Exbibytes = 6, + Kilobytes = 7, + Megabytes = 8, + Gigabytes = 9, + Terabytes = 10, + Petabytes = 11, + Exabytes = 12, + } + + private const double SizeBase10 = 1000; + private const double SizeBase2 = 1024; + private const int UnitEBIndex = 6; + + #region Value formatters + + /// + /// Creates a human-readable string from a . + /// + /// The to be formatted. + /// A formatted string that can be displayed in the UI. + public static string FormatTimeSpan(TimeSpan? timeSpan) + { + if (!timeSpan.HasValue || timeSpan.Value.TotalSeconds < 1) + { + // Game was never played + return TimeSpan.Zero.ToString("c", CultureInfo.InvariantCulture); + } + + if (timeSpan.Value.TotalDays < 1) + { + // Game was played for less than a day + return timeSpan.Value.ToString("c", CultureInfo.InvariantCulture); + } + + // Game was played for more than a day + TimeSpan onlyTime = timeSpan.Value.Subtract(TimeSpan.FromDays(timeSpan.Value.Days)); + string onlyTimeString = onlyTime.ToString("c", CultureInfo.InvariantCulture); + + return $"{timeSpan.Value.Days}d, {onlyTimeString}"; + } + + /// + /// Creates a human-readable string from a . + /// + /// The to be formatted. This is expected to be UTC-based. + /// The that's used in formatting. Defaults to . + /// A formatted string that can be displayed in the UI. + public static string FormatDateTime(DateTime? utcDateTime, CultureInfo culture = null) + { + culture ??= CultureInfo.CurrentCulture; + + if (!utcDateTime.HasValue) + { + // In the Avalonia UI, this is turned into a localized version of "Never" by LocalizedNeverConverter. + return "Never"; + } + + return utcDateTime.Value.ToLocalTime().ToString(culture); + } + + /// + /// Creates a human-readable file size string. + /// + /// The file size in bytes. + /// Formats the passed size value as this unit, bypassing the automatic unit choice. + /// A human-readable file size string. + public static string FormatFileSize(long size, FileSizeUnits forceUnit = FileSizeUnits.Auto) + { + if (size <= 0) + { + return $"0 {_fileSizeUnitStrings[0]}"; + } + + int unitIndex = (int)forceUnit; + if (forceUnit == FileSizeUnits.Auto) + { + unitIndex = Convert.ToInt32(Math.Floor(Math.Log(size, SizeBase10))); + + // Apply an upper bound so that exabytes are the biggest unit used when formatting. + if (unitIndex > UnitEBIndex) + { + unitIndex = UnitEBIndex; + } + } + + double sizeRounded; + + if (unitIndex > UnitEBIndex) + { + sizeRounded = Math.Round(size / Math.Pow(SizeBase10, unitIndex - UnitEBIndex), 1); + } + else + { + sizeRounded = Math.Round(size / Math.Pow(SizeBase2, unitIndex), 1); + } + + string sizeFormatted = sizeRounded.ToString(CultureInfo.InvariantCulture); + + return $"{sizeFormatted} {_fileSizeUnitStrings[unitIndex]}"; + } + + #endregion + + #region Value parsers + + /// + /// Parses a string generated by and returns the original . + /// + /// A string representing a . + /// A object. If the input string couldn't been parsed, is returned. + public static TimeSpan ParseTimeSpan(string timeSpanString) + { + TimeSpan returnTimeSpan = TimeSpan.Zero; + + // An input string can either look like "01:23:45" or "1d, 01:23:45" if the timespan represents a duration of more than a day. + // Here, we split the input string to check if it's the former or the latter. + var valueSplit = timeSpanString.Split(", "); + if (valueSplit.Length > 1) + { + var dayPart = valueSplit[0].Split("d")[0]; + if (int.TryParse(dayPart, out int days)) + { + returnTimeSpan = returnTimeSpan.Add(TimeSpan.FromDays(days)); + } + } + + if (TimeSpan.TryParse(valueSplit.Last(), out TimeSpan parsedTimeSpan)) + { + returnTimeSpan = returnTimeSpan.Add(parsedTimeSpan); + } + + return returnTimeSpan; + } + + /// + /// Parses a string generated by and returns the original . + /// + /// The string representing a . + /// A object. If the input string couldn't be parsed, is returned. + public static DateTime ParseDateTime(string dateTimeString) + { + if (!DateTime.TryParse(dateTimeString, CultureInfo.CurrentCulture, out DateTime parsedDateTime)) + { + // Games that were never played are supposed to appear before the oldest played games in the list, + // so returning DateTime.UnixEpoch here makes sense. + return DateTime.UnixEpoch; + } + + return parsedDateTime; + } + + /// + /// Parses a string generated by and returns a representing a number of bytes. + /// + /// A string representing a file size formatted with . + /// A representing a number of bytes. + public static long ParseFileSize(string sizeString) + { + // Enumerating over the units backwards because otherwise, sizeString.EndsWith("B") would exit the loop in the first iteration. + for (int i = _fileSizeUnitStrings.Length - 1; i >= 0; i--) + { + string unit = _fileSizeUnitStrings[i]; + if (!sizeString.EndsWith(unit)) + { + continue; + } + + string numberString = sizeString.Split(" ")[0]; + if (!double.TryParse(numberString, CultureInfo.InvariantCulture, out double number)) + { + break; + } + + double sizeBase = SizeBase2; + + // If the unit index is one that points to a base 10 unit in the FileSizeUnitStrings array, subtract 6 to arrive at a usable power value. + if (i > UnitEBIndex) + { + i -= UnitEBIndex; + sizeBase = SizeBase10; + } + + number *= Math.Pow(sizeBase, i); + + return Convert.ToInt64(number); + } + + return 0; + } + + #endregion + } +} diff --git a/src/Ryujinx.Common/SystemInfo/LinuxSystemInfo.cs b/src/Ryujinx.Ui.Common/SystemInfo/LinuxSystemInfo.cs similarity index 98% rename from src/Ryujinx.Common/SystemInfo/LinuxSystemInfo.cs rename to src/Ryujinx.Ui.Common/SystemInfo/LinuxSystemInfo.cs index 08aa452eb..5f1ab5416 100644 --- a/src/Ryujinx.Common/SystemInfo/LinuxSystemInfo.cs +++ b/src/Ryujinx.Ui.Common/SystemInfo/LinuxSystemInfo.cs @@ -5,7 +5,7 @@ using System.Globalization; using System.IO; using System.Runtime.Versioning; -namespace Ryujinx.Common.SystemInfo +namespace Ryujinx.Ui.Common.SystemInfo { [SupportedOSPlatform("linux")] class LinuxSystemInfo : SystemInfo diff --git a/src/Ryujinx.Common/SystemInfo/MacOSSystemInfo.cs b/src/Ryujinx.Ui.Common/SystemInfo/MacOSSystemInfo.cs similarity index 99% rename from src/Ryujinx.Common/SystemInfo/MacOSSystemInfo.cs rename to src/Ryujinx.Ui.Common/SystemInfo/MacOSSystemInfo.cs index a968ad17b..3508ae3a4 100644 --- a/src/Ryujinx.Common/SystemInfo/MacOSSystemInfo.cs +++ b/src/Ryujinx.Ui.Common/SystemInfo/MacOSSystemInfo.cs @@ -5,7 +5,7 @@ using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Text; -namespace Ryujinx.Common.SystemInfo +namespace Ryujinx.Ui.Common.SystemInfo { [SupportedOSPlatform("macos")] partial class MacOSSystemInfo : SystemInfo diff --git a/src/Ryujinx.Common/SystemInfo/SystemInfo.cs b/src/Ryujinx.Ui.Common/SystemInfo/SystemInfo.cs similarity index 87% rename from src/Ryujinx.Common/SystemInfo/SystemInfo.cs rename to src/Ryujinx.Ui.Common/SystemInfo/SystemInfo.cs index 55ec0127c..6a4fe6803 100644 --- a/src/Ryujinx.Common/SystemInfo/SystemInfo.cs +++ b/src/Ryujinx.Ui.Common/SystemInfo/SystemInfo.cs @@ -1,10 +1,11 @@ using Ryujinx.Common.Logging; +using Ryujinx.Ui.Common.Helper; using System; using System.Runtime.InteropServices; using System.Runtime.Intrinsics.X86; using System.Text; -namespace Ryujinx.Common.SystemInfo +namespace Ryujinx.Ui.Common.SystemInfo { public class SystemInfo { @@ -20,13 +21,13 @@ namespace Ryujinx.Common.SystemInfo CpuName = "Unknown"; } - private static string ToMiBString(ulong bytesValue) => (bytesValue == 0) ? "Unknown" : $"{bytesValue / 1024 / 1024} MiB"; + private static string ToGBString(ulong bytesValue) => (bytesValue == 0) ? "Unknown" : ValueFormatUtils.FormatFileSize((long)bytesValue, ValueFormatUtils.FileSizeUnits.Gibibytes); public void Print() { Logger.Notice.Print(LogClass.Application, $"Operating System: {OsDescription}"); Logger.Notice.Print(LogClass.Application, $"CPU: {CpuName}"); - Logger.Notice.Print(LogClass.Application, $"RAM: Total {ToMiBString(RamTotal)} ; Available {ToMiBString(RamAvailable)}"); + Logger.Notice.Print(LogClass.Application, $"RAM: Total {ToGBString(RamTotal)} ; Available {ToGBString(RamAvailable)}"); } public static SystemInfo Gather() diff --git a/src/Ryujinx.Common/SystemInfo/WindowsSystemInfo.cs b/src/Ryujinx.Ui.Common/SystemInfo/WindowsSystemInfo.cs similarity index 98% rename from src/Ryujinx.Common/SystemInfo/WindowsSystemInfo.cs rename to src/Ryujinx.Ui.Common/SystemInfo/WindowsSystemInfo.cs index 3b36d6e2e..9bb0fbf74 100644 --- a/src/Ryujinx.Common/SystemInfo/WindowsSystemInfo.cs +++ b/src/Ryujinx.Ui.Common/SystemInfo/WindowsSystemInfo.cs @@ -4,7 +4,7 @@ using System.Management; using System.Runtime.InteropServices; using System.Runtime.Versioning; -namespace Ryujinx.Common.SystemInfo +namespace Ryujinx.Ui.Common.SystemInfo { [SupportedOSPlatform("windows")] partial class WindowsSystemInfo : SystemInfo diff --git a/src/Ryujinx/Program.cs b/src/Ryujinx/Program.cs index 50151d733..afb6a9925 100644 --- a/src/Ryujinx/Program.cs +++ b/src/Ryujinx/Program.cs @@ -3,7 +3,6 @@ using Ryujinx.Common; using Ryujinx.Common.Configuration; using Ryujinx.Common.GraphicsDriver; using Ryujinx.Common.Logging; -using Ryujinx.Common.SystemInfo; using Ryujinx.Common.SystemInterop; using Ryujinx.Modules; using Ryujinx.SDL2.Common; @@ -11,6 +10,7 @@ using Ryujinx.Ui; using Ryujinx.Ui.Common; using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Common.Helper; +using Ryujinx.Ui.Common.SystemInfo; using Ryujinx.Ui.Widgets; using SixLabors.ImageSharp.Formats.Jpeg; using System; diff --git a/src/Ryujinx/Ui/Helper/SortHelper.cs b/src/Ryujinx/Ui/Helper/SortHelper.cs index 0c0eefd2c..c7a72ab9b 100644 --- a/src/Ryujinx/Ui/Helper/SortHelper.cs +++ b/src/Ryujinx/Ui/Helper/SortHelper.cs @@ -1,4 +1,5 @@ using Gtk; +using Ryujinx.Ui.Common.Helper; using System; namespace Ryujinx.Ui.Helper @@ -7,88 +8,26 @@ namespace Ryujinx.Ui.Helper { public static int TimePlayedSort(ITreeModel model, TreeIter a, TreeIter b) { - static string ReverseFormat(string time) - { - if (time == "Never") - { - return "00"; - } + TimeSpan aTimeSpan = ValueFormatUtils.ParseTimeSpan(model.GetValue(a, 5).ToString()); + TimeSpan bTimeSpan = ValueFormatUtils.ParseTimeSpan(model.GetValue(b, 5).ToString()); - var numbers = time.Split(new char[] { 'd', 'h', 'm' }); - - time = time.Replace(" ", "").Replace("d", ".").Replace("h", ":").Replace("m", ""); - - if (numbers.Length == 2) - { - return $"00.00:{time}"; - } - else if (numbers.Length == 3) - { - return $"00.{time}"; - } - - return time; - } - - string aValue = ReverseFormat(model.GetValue(a, 5).ToString()); - string bValue = ReverseFormat(model.GetValue(b, 5).ToString()); - - return TimeSpan.Compare(TimeSpan.Parse(aValue), TimeSpan.Parse(bValue)); + return TimeSpan.Compare(aTimeSpan, bTimeSpan); } public static int LastPlayedSort(ITreeModel model, TreeIter a, TreeIter b) { - string aValue = model.GetValue(a, 6).ToString(); - string bValue = model.GetValue(b, 6).ToString(); + DateTime aDateTime = ValueFormatUtils.ParseDateTime(model.GetValue(a, 6).ToString()); + DateTime bDateTime = ValueFormatUtils.ParseDateTime(model.GetValue(b, 6).ToString()); - if (aValue == "Never") - { - aValue = DateTime.UnixEpoch.ToString(); - } - - if (bValue == "Never") - { - bValue = DateTime.UnixEpoch.ToString(); - } - - return DateTime.Compare(DateTime.Parse(bValue), DateTime.Parse(aValue)); + return DateTime.Compare(aDateTime, bDateTime); } public static int FileSizeSort(ITreeModel model, TreeIter a, TreeIter b) { - string aValue = model.GetValue(a, 8).ToString(); - string bValue = model.GetValue(b, 8).ToString(); + long aSize = ValueFormatUtils.ParseFileSize(model.GetValue(a, 8).ToString()); + long bSize = ValueFormatUtils.ParseFileSize(model.GetValue(b, 8).ToString()); - if (aValue[^3..] == "GiB") - { - aValue = (float.Parse(aValue[0..^3]) * 1024).ToString(); - } - else - { - aValue = aValue[0..^3]; - } - - if (bValue[^3..] == "GiB") - { - bValue = (float.Parse(bValue[0..^3]) * 1024).ToString(); - } - else - { - bValue = bValue[0..^3]; - } - - if (float.Parse(aValue) > float.Parse(bValue)) - { - return -1; - } - else if (float.Parse(bValue) > float.Parse(aValue)) - { - return 1; - } - else - { - return 0; - } + return aSize.CompareTo(bSize); } } } diff --git a/src/Ryujinx/Ui/MainWindow.cs b/src/Ryujinx/Ui/MainWindow.cs index a9d4be109..8b0b35e6c 100644 --- a/src/Ryujinx/Ui/MainWindow.cs +++ b/src/Ryujinx/Ui/MainWindow.cs @@ -954,7 +954,7 @@ namespace Ryujinx.Ui ApplicationLibrary.LoadAndSaveMetaData(_emulationContext.Processes.ActiveApplication.ProgramIdText, appMetadata => { - appMetadata.LastPlayed = DateTime.UtcNow; + appMetadata.UpdatePreGame(); }); } } @@ -1097,13 +1097,7 @@ namespace Ryujinx.Ui { ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => { - if (appMetadata.LastPlayed.HasValue) - { - double sessionTimePlayed = DateTime.UtcNow.Subtract(appMetadata.LastPlayed.Value).TotalSeconds; - appMetadata.TimePlayed += Math.Round(sessionTimePlayed, MidpointRounding.AwayFromZero); - } - - appMetadata.LastPlayed = DateTime.UtcNow; + appMetadata.UpdatePostGame(); }); } } @@ -1177,10 +1171,10 @@ namespace Ryujinx.Ui $"{args.AppData.TitleName}\n{args.AppData.TitleId.ToUpper()}", args.AppData.Developer, args.AppData.Version, - args.AppData.TimePlayed, + args.AppData.TimePlayedString, args.AppData.LastPlayedString, args.AppData.FileExtension, - args.AppData.FileSize, + args.AppData.FileSizeString, args.AppData.Path, args.AppData.ControlHolder); }); From 815819767c5794624e3e7bc2bebcabe8ea4de0f6 Mon Sep 17 00:00:00 2001 From: gdkchan Date: Tue, 7 Nov 2023 13:24:10 -0300 Subject: [PATCH 03/23] Force all exclusive memory accesses to be ordered on AppleHv (#5898) * Implement reprotect method on virtual memory manager (currently stubbed) * Force all exclusive memory accesses to be ordered on AppleHv * Format whitespace * Fix test build * Fix comment copy/paste * Fix wrong bit for isLoad * Update src/Ryujinx.Cpu/AppleHv/HvMemoryManager.cs Co-authored-by: riperiperi --------- Co-authored-by: riperiperi --- src/Ryujinx.Cpu/AppleHv/HvCodePatcher.cs | 62 +++++++++++++++++++ src/Ryujinx.Cpu/AppleHv/HvMemoryManager.cs | 33 +++++----- src/Ryujinx.Cpu/Jit/MemoryManager.cs | 23 ++----- .../Jit/MemoryManagerHostMapped.cs | 6 ++ .../HOS/Kernel/Memory/KMemoryPermission.cs | 46 ++++++++++++++ .../HOS/Kernel/Memory/KPageTable.cs | 10 +-- .../HOS/Kernel/Memory/KPageTableBase.cs | 6 +- .../HOS/Kernel/Memory/MemoryPermission.cs | 20 ------ src/Ryujinx.Memory/AddressSpaceManager.cs | 5 ++ src/Ryujinx.Memory/IVirtualMemoryManager.cs | 14 +++++ .../MockVirtualMemoryManager.cs | 5 ++ 11 files changed, 171 insertions(+), 59 deletions(-) create mode 100644 src/Ryujinx.Cpu/AppleHv/HvCodePatcher.cs create mode 100644 src/Ryujinx.HLE/HOS/Kernel/Memory/KMemoryPermission.cs delete mode 100644 src/Ryujinx.HLE/HOS/Kernel/Memory/MemoryPermission.cs diff --git a/src/Ryujinx.Cpu/AppleHv/HvCodePatcher.cs b/src/Ryujinx.Cpu/AppleHv/HvCodePatcher.cs new file mode 100644 index 000000000..876597b78 --- /dev/null +++ b/src/Ryujinx.Cpu/AppleHv/HvCodePatcher.cs @@ -0,0 +1,62 @@ +using System; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; + +namespace Ryujinx.Cpu.AppleHv +{ + static class HvCodePatcher + { + private const uint XMask = 0x3f808000u; + private const uint XValue = 0x8000000u; + + private const uint ZrIndex = 31u; + + public static void RewriteUnorderedExclusiveInstructions(Span code) + { + Span codeUint = MemoryMarshal.Cast(code); + Span> codeVector = MemoryMarshal.Cast>(code); + + Vector128 mask = Vector128.Create(XMask); + Vector128 value = Vector128.Create(XValue); + + for (int index = 0; index < codeVector.Length; index++) + { + Vector128 v = codeVector[index]; + + if (Vector128.EqualsAny(Vector128.BitwiseAnd(v, mask), value)) + { + int baseIndex = index * 4; + + for (int instIndex = baseIndex; instIndex < baseIndex + 4; instIndex++) + { + ref uint inst = ref codeUint[instIndex]; + + if ((inst & XMask) != XValue) + { + continue; + } + + bool isPair = (inst & (1u << 21)) != 0; + bool isLoad = (inst & (1u << 22)) != 0; + + uint rt2 = (inst >> 10) & 0x1fu; + uint rs = (inst >> 16) & 0x1fu; + + if (isLoad && rs != ZrIndex) + { + continue; + } + + if (!isPair && rt2 != ZrIndex) + { + continue; + } + + // Set the ordered flag. + inst |= 1u << 15; + } + } + } + } + } +} diff --git a/src/Ryujinx.Cpu/AppleHv/HvMemoryManager.cs b/src/Ryujinx.Cpu/AppleHv/HvMemoryManager.cs index d5ce817a4..947c37100 100644 --- a/src/Ryujinx.Cpu/AppleHv/HvMemoryManager.cs +++ b/src/Ryujinx.Cpu/AppleHv/HvMemoryManager.cs @@ -128,21 +128,6 @@ namespace Ryujinx.Cpu.AppleHv } } -#pragma warning disable IDE0051 // Remove unused private member - /// - /// Ensures the combination of virtual address and size is part of the addressable space and fully mapped. - /// - /// Virtual address of the range - /// Size of the range in bytes - private void AssertMapped(ulong va, ulong size) - { - if (!ValidateAddressAndSize(va, size) || !IsRangeMappedImpl(va, size)) - { - throw new InvalidMemoryRegionException($"Not mapped: va=0x{va:X16}, size=0x{size:X16}"); - } - } -#pragma warning restore IDE0051 - /// public void Map(ulong va, ulong pa, ulong size, MemoryMapFlags flags) { @@ -736,6 +721,24 @@ namespace Ryujinx.Cpu.AppleHv return (int)(vaSpan / PageSize); } + /// + public void Reprotect(ulong va, ulong size, MemoryPermission protection) + { + if (protection.HasFlag(MemoryPermission.Execute)) + { + // Some applications use unordered exclusive memory access instructions + // where it is not valid to do so, leading to memory re-ordering that + // makes the code behave incorrectly on some CPUs. + // To work around this, we force all such accesses to be ordered. + + using WritableRegion writableRegion = GetWritableRegion(va, (int)size); + + HvCodePatcher.RewriteUnorderedExclusiveInstructions(writableRegion.Memory.Span); + } + + // TODO + } + /// public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection) { diff --git a/src/Ryujinx.Cpu/Jit/MemoryManager.cs b/src/Ryujinx.Cpu/Jit/MemoryManager.cs index 1c27e97fa..912e3f7e3 100644 --- a/src/Ryujinx.Cpu/Jit/MemoryManager.cs +++ b/src/Ryujinx.Cpu/Jit/MemoryManager.cs @@ -575,24 +575,17 @@ namespace Ryujinx.Cpu.Jit } } -#pragma warning disable IDE0051 // Remove unused private member - private ulong GetPhysicalAddress(ulong va) - { - // We return -1L if the virtual address is invalid or unmapped. - if (!ValidateAddress(va) || !IsMapped(va)) - { - return ulong.MaxValue; - } - - return GetPhysicalAddressInternal(va); - } -#pragma warning restore IDE0051 - private ulong GetPhysicalAddressInternal(ulong va) { return PteToPa(_pageTable.Read((va / PageSize) * PteSize) & ~(0xffffUL << 48)) + (va & PageMask); } + /// + public void Reprotect(ulong va, ulong size, MemoryPermission protection) + { + // TODO + } + /// public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection) { @@ -698,9 +691,5 @@ namespace Ryujinx.Cpu.Jit /// Disposes of resources used by the memory manager. /// protected override void Destroy() => _pageTable.Dispose(); - -#pragma warning disable IDE0051 // Remove unused private member - private static void ThrowInvalidMemoryRegionException(string message) => throw new InvalidMemoryRegionException(message); -#pragma warning restore IDE0051 } } diff --git a/src/Ryujinx.Cpu/Jit/MemoryManagerHostMapped.cs b/src/Ryujinx.Cpu/Jit/MemoryManagerHostMapped.cs index 010a0bc25..6d32787ac 100644 --- a/src/Ryujinx.Cpu/Jit/MemoryManagerHostMapped.cs +++ b/src/Ryujinx.Cpu/Jit/MemoryManagerHostMapped.cs @@ -615,6 +615,12 @@ namespace Ryujinx.Cpu.Jit return (int)(vaSpan / PageSize); } + /// + public void Reprotect(ulong va, ulong size, MemoryPermission protection) + { + // TODO + } + /// public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection) { diff --git a/src/Ryujinx.HLE/HOS/Kernel/Memory/KMemoryPermission.cs b/src/Ryujinx.HLE/HOS/Kernel/Memory/KMemoryPermission.cs new file mode 100644 index 000000000..32734574e --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Kernel/Memory/KMemoryPermission.cs @@ -0,0 +1,46 @@ +using Ryujinx.Memory; +using System; + +namespace Ryujinx.HLE.HOS.Kernel.Memory +{ + [Flags] + enum KMemoryPermission : uint + { + None = 0, + UserMask = Read | Write | Execute, + Mask = uint.MaxValue, + + Read = 1 << 0, + Write = 1 << 1, + Execute = 1 << 2, + DontCare = 1 << 28, + + ReadAndWrite = Read | Write, + ReadAndExecute = Read | Execute, + } + + static class KMemoryPermissionExtensions + { + public static MemoryPermission Convert(this KMemoryPermission permission) + { + MemoryPermission output = MemoryPermission.None; + + if (permission.HasFlag(KMemoryPermission.Read)) + { + output = MemoryPermission.Read; + } + + if (permission.HasFlag(KMemoryPermission.Write)) + { + output |= MemoryPermission.Write; + } + + if (permission.HasFlag(KMemoryPermission.Execute)) + { + output |= MemoryPermission.Execute; + } + + return output; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTable.cs b/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTable.cs index dcfc8f4ff..4cd3e6fdd 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTable.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTable.cs @@ -203,15 +203,17 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory /// protected override Result Reprotect(ulong address, ulong pagesCount, KMemoryPermission permission) { - // TODO. + _cpuMemory.Reprotect(address, pagesCount * PageSize, permission.Convert()); + return Result.Success; } /// - protected override Result ReprotectWithAttributes(ulong address, ulong pagesCount, KMemoryPermission permission) + protected override Result ReprotectAndFlush(ulong address, ulong pagesCount, KMemoryPermission permission) { - // TODO. - return Result.Success; + // TODO: Flush JIT cache. + + return Reprotect(address, pagesCount, permission); } /// diff --git a/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTableBase.cs b/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTableBase.cs index 2b00f802a..2b6d4e4e9 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTableBase.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTableBase.cs @@ -1255,7 +1255,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory if ((oldPermission & KMemoryPermission.Execute) != 0) { - result = ReprotectWithAttributes(address, pagesCount, permission); + result = ReprotectAndFlush(address, pagesCount, permission); } else { @@ -3036,13 +3036,13 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory protected abstract Result Reprotect(ulong address, ulong pagesCount, KMemoryPermission permission); /// - /// Changes the permissions of a given virtual memory region. + /// Changes the permissions of a given virtual memory region, while also flushing the cache. /// /// Virtual address of the region to have the permission changes /// Number of pages to have their permissions changed /// New permission /// Result of the permission change operation - protected abstract Result ReprotectWithAttributes(ulong address, ulong pagesCount, KMemoryPermission permission); + protected abstract Result ReprotectAndFlush(ulong address, ulong pagesCount, KMemoryPermission permission); /// /// Alerts the memory tracking that a given region has been read from or written to. diff --git a/src/Ryujinx.HLE/HOS/Kernel/Memory/MemoryPermission.cs b/src/Ryujinx.HLE/HOS/Kernel/Memory/MemoryPermission.cs deleted file mode 100644 index 068cdbb88..000000000 --- a/src/Ryujinx.HLE/HOS/Kernel/Memory/MemoryPermission.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; - -namespace Ryujinx.HLE.HOS.Kernel.Memory -{ - [Flags] - enum KMemoryPermission : uint - { - None = 0, - UserMask = Read | Write | Execute, - Mask = uint.MaxValue, - - Read = 1 << 0, - Write = 1 << 1, - Execute = 1 << 2, - DontCare = 1 << 28, - - ReadAndWrite = Read | Write, - ReadAndExecute = Read | Execute, - } -} diff --git a/src/Ryujinx.Memory/AddressSpaceManager.cs b/src/Ryujinx.Memory/AddressSpaceManager.cs index 65b4d48f2..b8d48365c 100644 --- a/src/Ryujinx.Memory/AddressSpaceManager.cs +++ b/src/Ryujinx.Memory/AddressSpaceManager.cs @@ -455,6 +455,11 @@ namespace Ryujinx.Memory return _pageTable.Read(va) + (nuint)(va & PageMask); } + /// + public void Reprotect(ulong va, ulong size, MemoryPermission protection) + { + } + /// public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection) { diff --git a/src/Ryujinx.Memory/IVirtualMemoryManager.cs b/src/Ryujinx.Memory/IVirtualMemoryManager.cs index edbfc8855..8c9ca1684 100644 --- a/src/Ryujinx.Memory/IVirtualMemoryManager.cs +++ b/src/Ryujinx.Memory/IVirtualMemoryManager.cs @@ -104,6 +104,12 @@ namespace Ryujinx.Memory /// True if the data was changed, false otherwise bool WriteWithRedundancyCheck(ulong va, ReadOnlySpan data); + /// + /// Fills the specified memory region with the value specified in . + /// + /// Virtual address to fill the value into + /// Size of the memory region to fill + /// Value to fill with void Fill(ulong va, ulong size, byte value) { const int MaxChunkSize = 1 << 24; @@ -194,6 +200,14 @@ namespace Ryujinx.Memory /// Optional ID of the handles that should not be signalled void SignalMemoryTracking(ulong va, ulong size, bool write, bool precise = false, int? exemptId = null); + /// + /// Reprotect a region of virtual memory for guest access. + /// + /// Virtual address base + /// Size of the region to protect + /// Memory protection to set + void Reprotect(ulong va, ulong size, MemoryPermission protection); + /// /// Reprotect a region of virtual memory for tracking. /// diff --git a/src/Ryujinx.Tests.Memory/MockVirtualMemoryManager.cs b/src/Ryujinx.Tests.Memory/MockVirtualMemoryManager.cs index 59dc1a525..435bb35ae 100644 --- a/src/Ryujinx.Tests.Memory/MockVirtualMemoryManager.cs +++ b/src/Ryujinx.Tests.Memory/MockVirtualMemoryManager.cs @@ -102,6 +102,11 @@ namespace Ryujinx.Tests.Memory throw new NotImplementedException(); } + public void Reprotect(ulong va, ulong size, MemoryPermission protection) + { + throw new NotImplementedException(); + } + public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection) { OnProtect?.Invoke(va, size, protection); From c3555cb5d6c088bdb2e84ef16889a9f8c76d4319 Mon Sep 17 00:00:00 2001 From: jcm Date: Sat, 11 Nov 2023 08:27:53 -0600 Subject: [PATCH 04/23] UI: Change default hide cursor mode to OnIdle (#5906) Co-authored-by: jcm --- src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs b/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs index 9ed8fd8cc..c79fa56c6 100644 --- a/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs +++ b/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs @@ -784,7 +784,7 @@ namespace Ryujinx.Ui.Common.Configuration EnableDiscordIntegration.Value = true; CheckUpdatesOnStart.Value = true; ShowConfirmExit.Value = true; - HideCursor.Value = HideCursorMode.Never; + HideCursor.Value = HideCursorMode.OnIdle; Graphics.EnableVsync.Value = true; Graphics.EnableShaderCache.Value = true; Graphics.EnableTextureRecompression.Value = false; From 7e6342e44ddb004b0600f6bb4e6169aba6e4bf7c Mon Sep 17 00:00:00 2001 From: Isaac Marovitz <42140194+IsaacMarovitz@users.noreply.github.com> Date: Sat, 11 Nov 2023 09:57:15 -0500 Subject: [PATCH 05/23] Add accelerator keys for Options and Help (#5884) --- src/Ryujinx.Ava/Assets/Locales/en_US.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Ryujinx.Ava/Assets/Locales/en_US.json b/src/Ryujinx.Ava/Assets/Locales/en_US.json index 62aac1227..bc2bbfe82 100644 --- a/src/Ryujinx.Ava/Assets/Locales/en_US.json +++ b/src/Ryujinx.Ava/Assets/Locales/en_US.json @@ -14,7 +14,7 @@ "MenuBarFileOpenEmuFolder": "Open Ryujinx Folder", "MenuBarFileOpenLogsFolder": "Open Logs Folder", "MenuBarFileExit": "_Exit", - "MenuBarOptions": "Options", + "MenuBarOptions": "_Options", "MenuBarOptionsToggleFullscreen": "Toggle Fullscreen", "MenuBarOptionsStartGamesInFullscreen": "Start Games in Fullscreen Mode", "MenuBarOptionsStopEmulation": "Stop Emulation", @@ -30,7 +30,7 @@ "MenuBarToolsManageFileTypes": "Manage file types", "MenuBarToolsInstallFileTypes": "Install file types", "MenuBarToolsUninstallFileTypes": "Uninstall file types", - "MenuBarHelp": "Help", + "MenuBarHelp": "_Help", "MenuBarHelpCheckForUpdates": "Check for Updates", "MenuBarHelpAbout": "About", "MenuSearch": "Search...", From 55557525b16f8256d91f769e026874b5c70c3b2d Mon Sep 17 00:00:00 2001 From: NitroTears <73270647+NitroTears@users.noreply.github.com> Date: Sun, 12 Nov 2023 01:08:42 +1000 Subject: [PATCH 06/23] Create Desktop Shortcut fixes (#5852) * remove duplicate basePath arg, add --fullscreen arg * Changing FriendlyName to set "Ryujinx" text * Fix GetArgsString using the base path * Change desktop path to the Applications folder when creating shortcut on Mac Co-authored-by: Nicko Anastassiu <134955950+nickoanastassiu@users.noreply.github.com> * Move Create Shortcut button to top of context menu --------- Co-authored-by: TSR Berry <20988865+TSRBerry@users.noreply.github.com> Co-authored-by: Nicko Anastassiu <134955950+nickoanastassiu@users.noreply.github.com> --- .../UI/Controls/ApplicationContextMenu.axaml | 10 +++++----- src/Ryujinx.Ui.Common/Helper/ShortcutHelper.cs | 18 +++++++----------- .../Widgets/GameTableContextMenu.Designer.cs | 3 ++- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml index d81050f83..b8fe7e76f 100644 --- a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml +++ b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml @@ -12,6 +12,11 @@ Click="ToggleFavorite_Click" Header="{locale:Locale GameListContextMenuToggleFavorite}" ToolTip.Tip="{locale:Locale GameListContextMenuToggleFavoriteToolTip}" /> + - diff --git a/src/Ryujinx.Ui.Common/Helper/ShortcutHelper.cs b/src/Ryujinx.Ui.Common/Helper/ShortcutHelper.cs index dab473fa3..103b78c24 100644 --- a/src/Ryujinx.Ui.Common/Helper/ShortcutHelper.cs +++ b/src/Ryujinx.Ui.Common/Helper/ShortcutHelper.cs @@ -30,7 +30,7 @@ namespace Ryujinx.Ui.Common.Helper graphic.DrawImage(image, 0, 0, 128, 128); SaveBitmapAsIcon(bitmap, iconPath); - var shortcut = Shortcut.CreateShortcut(basePath, GetArgsString(basePath, applicationFilePath), iconPath, 0); + var shortcut = Shortcut.CreateShortcut(basePath, GetArgsString(applicationFilePath), iconPath, 0); shortcut.StringData.NameString = cleanedAppName; shortcut.WriteToFile(Path.Combine(desktopPath, cleanedAppName + ".lnk")); } @@ -46,16 +46,16 @@ namespace Ryujinx.Ui.Common.Helper image.SaveAsPng(iconPath); using StreamWriter outputFile = new(Path.Combine(desktopPath, cleanedAppName + ".desktop")); - outputFile.Write(desktopFile, cleanedAppName, iconPath, GetArgsString(basePath, applicationFilePath)); + outputFile.Write(desktopFile, cleanedAppName, iconPath, $"{basePath} {GetArgsString(applicationFilePath)}"); } [SupportedOSPlatform("macos")] private static void CreateShortcutMacos(string appFilePath, byte[] iconData, string desktopPath, string cleanedAppName) { - string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, AppDomain.CurrentDomain.FriendlyName); + string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Ryujinx"); var plistFile = EmbeddedResources.ReadAllText("Ryujinx.Ui.Common/shortcut-template.plist"); // Macos .App folder - string contentFolderPath = Path.Combine(desktopPath, cleanedAppName + ".app", "Contents"); + string contentFolderPath = Path.Combine("/Applications", cleanedAppName + ".app", "Contents"); string scriptFolderPath = Path.Combine(contentFolderPath, "MacOS"); if (!Directory.Exists(scriptFolderPath)) @@ -69,7 +69,7 @@ namespace Ryujinx.Ui.Common.Helper using StreamWriter scriptFile = new(scriptPath); scriptFile.WriteLine("#!/bin/sh"); - scriptFile.WriteLine(GetArgsString(basePath, appFilePath)); + scriptFile.WriteLine($"{basePath} {GetArgsString(appFilePath)}"); // Set execute permission FileInfo fileInfo = new(scriptPath); @@ -125,13 +125,10 @@ namespace Ryujinx.Ui.Common.Helper throw new NotImplementedException("Shortcut support has not been implemented yet for this OS."); } - private static string GetArgsString(string basePath, string appFilePath) + private static string GetArgsString(string appFilePath) { // args are first defined as a list, for easier adjustments in the future - var argsList = new List - { - basePath, - }; + var argsList = new List(); if (!string.IsNullOrEmpty(CommandLineState.BaseDirPathArg)) { @@ -141,7 +138,6 @@ namespace Ryujinx.Ui.Common.Helper argsList.Add($"\"{appFilePath}\""); - return String.Join(" ", argsList); } diff --git a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs index 75b166136..734437eea 100644 --- a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs +++ b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs @@ -211,6 +211,8 @@ namespace Ryujinx.Ui.Widgets _manageSubMenu.Append(_openPtcDirMenuItem); _manageSubMenu.Append(_openShaderCacheDirMenuItem); + Add(_createShortcutMenuItem); + Add(new SeparatorMenuItem()); Add(_openSaveUserDirMenuItem); Add(_openSaveDeviceDirMenuItem); Add(_openSaveBcatDirMenuItem); @@ -223,7 +225,6 @@ namespace Ryujinx.Ui.Widgets Add(new SeparatorMenuItem()); Add(_manageCacheMenuItem); Add(_extractMenuItem); - Add(_createShortcutMenuItem); ShowAll(); } From 5c3cfb84c09b0566da677425915afa0b2d76da55 Mon Sep 17 00:00:00 2001 From: TSRBerry <20988865+TSRBerry@users.noreply.github.com> Date: Sat, 11 Nov 2023 21:56:57 +0100 Subject: [PATCH 07/23] Add support for multi game XCIs (#5638) * Add default values to ApplicationData directly * Refactor application loading It should now be possible to load multi game XCIs. Included updates won't be detected for now. Opening a game from the command line currently only opens the first one. * Only include program NCAs where at least one tuple item is not null * Get application data by title id and add programIndex check back * Refactor application loading again and remove duplicate code * Actually use patch ncas for updates * Fix number of applications found with multi game xcis * Don't load bundled updates from multi game xcis * Change ApplicationData.TitleId type to ulong & Add TitleIdString property * Use cnmt files and ContentCollection to load programs * Ava: Add updates and DLCs from gamecarts * Get the cnmt file from its NCA * Ava: Identify bundled updates in updater window * Fix the (hopefully) last few bugs * Add idOffset parameter to GetNcaByType * Handle missing file for dlc.json * Ava: Shorten error message for invalid files * Gtk: Add additional string for bundled updates in TitleUpdateWindow * Hopefully fix DLC issues * Apply formatting * Finally fix DLC issues * Adjust property names and fileSize field * Read the correct update file * Fix wrong casing for application id strings * Rename TitleId to ApplicationId * Address review comments * Fix formatting issues * Apply suggestions from code review Co-authored-by: gdkchan * Gracefully fail when loading pfs for update and dlc window * Fix applications with multiple programs * Fix DLCWindow crash on GTK * Fix some GUI issues * Remove IsXci again --------- Co-authored-by: gdkchan --- src/Ryujinx.Ava/AppHost.cs | 9 +- src/Ryujinx.Ava/Assets/Locales/en_US.json | 2 + src/Ryujinx.Ava/Common/ApplicationHelper.cs | 9 +- .../Controls/ApplicationContextMenu.axaml.cs | 54 +- .../UI/Controls/ApplicationGridView.axaml | 2 +- .../UI/Controls/ApplicationListView.axaml | 4 +- .../UI/Models/DownloadableContentModel.cs | 6 +- src/Ryujinx.Ava/UI/Models/SaveModel.cs | 4 +- src/Ryujinx.Ava/UI/Models/TitleUpdateModel.cs | 5 +- .../DownloadableContentManagerViewModel.cs | 60 +- .../UI/ViewModels/MainWindowViewModel.cs | 47 +- .../UI/ViewModels/TitleUpdateViewModel.cs | 56 +- .../UI/Views/Main/MainMenuBarView.axaml.cs | 10 +- .../UI/Views/Main/MainViewControls.axaml | 2 +- .../UI/Windows/CheatWindow.axaml.cs | 7 +- .../DownloadableContentManagerWindow.axaml | 2 +- .../DownloadableContentManagerWindow.axaml.cs | 12 +- .../UI/Windows/MainWindow.axaml.cs | 19 +- .../UI/Windows/TitleUpdateWindow.axaml.cs | 14 +- .../FileSystem/ContentCollection.cs | 61 ++ .../Processes/Extensions/NcaExtensions.cs | 100 +- .../PartitionFileSystemExtensions.cs | 153 +-- .../Loaders/Processes/ProcessLoader.cs | 8 +- .../Loaders/Processes/ProcessLoaderHelper.cs | 9 +- src/Ryujinx.HLE/Switch.cs | 8 +- src/Ryujinx.Ui.Common/App/ApplicationData.cs | 18 +- .../App/ApplicationLibrary.cs | 916 +++++++++--------- src/Ryujinx/Program.cs | 8 +- src/Ryujinx/Ui/MainWindow.cs | 95 +- .../Ui/Widgets/GameTableContextMenu.cs | 89 +- src/Ryujinx/Ui/Windows/CheatWindow.cs | 9 +- src/Ryujinx/Ui/Windows/DlcWindow.cs | 123 +-- src/Ryujinx/Ui/Windows/TitleUpdateWindow.cs | 63 +- 33 files changed, 1168 insertions(+), 816 deletions(-) create mode 100644 src/Ryujinx.HLE/FileSystem/ContentCollection.cs diff --git a/src/Ryujinx.Ava/AppHost.cs b/src/Ryujinx.Ava/AppHost.cs index 4d751e2a9..053d5b521 100644 --- a/src/Ryujinx.Ava/AppHost.cs +++ b/src/Ryujinx.Ava/AppHost.cs @@ -54,8 +54,6 @@ using System.Threading.Tasks; using static Ryujinx.Ava.UI.Helpers.Win32NativeInterop; using AntiAliasing = Ryujinx.Common.Configuration.AntiAliasing; using Image = SixLabors.ImageSharp.Image; -using InputManager = Ryujinx.Input.HLE.InputManager; -using IRenderer = Ryujinx.Graphics.GAL.IRenderer; using Key = Ryujinx.Input.Key; using MouseButton = Ryujinx.Input.MouseButton; using ScalingFilter = Ryujinx.Common.Configuration.ScalingFilter; @@ -123,12 +121,14 @@ namespace Ryujinx.Ava public int Width { get; private set; } public int Height { get; private set; } public string ApplicationPath { get; private set; } + public ulong ApplicationId { get; private set; } public bool ScreenshotRequested { get; set; } public AppHost( RendererHost renderer, InputManager inputManager, string applicationPath, + ulong applicationId, VirtualFileSystem virtualFileSystem, ContentManager contentManager, AccountManager accountManager, @@ -152,6 +152,7 @@ namespace Ryujinx.Ava NpadManager = _inputManager.CreateNpadManager(); TouchScreenManager = _inputManager.CreateTouchScreenManager(); ApplicationPath = applicationPath; + ApplicationId = applicationId; VirtualFileSystem = virtualFileSystem; ContentManager = contentManager; @@ -641,7 +642,7 @@ namespace Ryujinx.Ava { Logger.Info?.Print(LogClass.Application, "Loading as XCI."); - if (!Device.LoadXci(ApplicationPath)) + if (!Device.LoadXci(ApplicationPath, ApplicationId)) { Device.Dispose(); @@ -668,7 +669,7 @@ namespace Ryujinx.Ava { Logger.Info?.Print(LogClass.Application, "Loading as NSP."); - if (!Device.LoadNsp(ApplicationPath)) + if (!Device.LoadNsp(ApplicationPath, ApplicationId)) { Device.Dispose(); diff --git a/src/Ryujinx.Ava/Assets/Locales/en_US.json b/src/Ryujinx.Ava/Assets/Locales/en_US.json index bc2bbfe82..be3e35a9c 100644 --- a/src/Ryujinx.Ava/Assets/Locales/en_US.json +++ b/src/Ryujinx.Ava/Assets/Locales/en_US.json @@ -539,6 +539,8 @@ "OpenSetupGuideMessage": "Open the Setup Guide", "NoUpdate": "No Update", "TitleUpdateVersionLabel": "Version {0}", + "TitleBundledUpdateVersionLabel": "Bundled: Version {0}", + "TitleBundledDlcLabel": "Bundled:", "RyujinxInfo": "Ryujinx - Info", "RyujinxConfirm": "Ryujinx - Confirmation", "FileDialogAllTypes": "All types", diff --git a/src/Ryujinx.Ava/Common/ApplicationHelper.cs b/src/Ryujinx.Ava/Common/ApplicationHelper.cs index 91ca8f4d5..dd4643297 100644 --- a/src/Ryujinx.Ava/Common/ApplicationHelper.cs +++ b/src/Ryujinx.Ava/Common/ApplicationHelper.cs @@ -18,7 +18,8 @@ using Ryujinx.Ava.UI.Helpers; using Ryujinx.Common.Logging; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS.Services.Account.Acc; -using Ryujinx.Ui.App.Common; +using Ryujinx.HLE.Loaders.Processes.Extensions; +using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Common.Helper; using System; using System.Buffers; @@ -226,7 +227,11 @@ namespace Ryujinx.Ava.Common return; } - (Nca updatePatchNca, _) = ApplicationLibrary.GetGameUpdateData(_virtualFileSystem, mainNca.Header.TitleId.ToString("x16"), programIndex, out _); + IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks + ? IntegrityCheckLevel.ErrorOnInvalid + : IntegrityCheckLevel.None; + + (Nca updatePatchNca, _) = mainNca.GetUpdateData(_virtualFileSystem, checkLevel, programIndex, out _); if (updatePatchNca != null) { patchNca = updatePatchNca; diff --git a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs index 0f0071065..69465c7c7 100644 --- a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs +++ b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs @@ -1,7 +1,6 @@ using Avalonia.Controls; using Avalonia.Interactivity; using Avalonia.Markup.Xaml; -using Avalonia.Threading; using LibHac.Fs; using LibHac.Tools.FsSystem.NcaUtils; using Ryujinx.Ava.Common; @@ -15,7 +14,6 @@ using Ryujinx.Ui.App.Common; using Ryujinx.Ui.Common.Helper; using System; using System.Collections.Generic; -using System.Globalization; using System.IO; using Path = System.IO.Path; @@ -41,7 +39,7 @@ namespace Ryujinx.Ava.UI.Controls { viewModel.SelectedApplication.Favorite = !viewModel.SelectedApplication.Favorite; - ApplicationLibrary.LoadAndSaveMetaData(viewModel.SelectedApplication.TitleId, appMetadata => + ApplicationLibrary.LoadAndSaveMetaData(viewModel.SelectedApplication.IdString, appMetadata => { appMetadata.Favorite = viewModel.SelectedApplication.Favorite; }); @@ -76,19 +74,9 @@ namespace Ryujinx.Ava.UI.Controls { if (viewModel?.SelectedApplication != null) { - if (!ulong.TryParse(viewModel.SelectedApplication.TitleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdNumber)) - { - Dispatcher.UIThread.InvokeAsync(async () => - { - await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogRyujinxErrorMessage], LocaleManager.Instance[LocaleKeys.DialogInvalidTitleIdErrorMessage]); - }); + var saveDataFilter = SaveDataFilter.Make(viewModel.SelectedApplication.Id, saveDataType, userId, saveDataId: default, index: default); - return; - } - - var saveDataFilter = SaveDataFilter.Make(titleIdNumber, saveDataType, userId, saveDataId: default, index: default); - - ApplicationHelper.OpenSaveDir(in saveDataFilter, titleIdNumber, viewModel.SelectedApplication.ControlHolder, viewModel.SelectedApplication.TitleName); + ApplicationHelper.OpenSaveDir(in saveDataFilter, viewModel.SelectedApplication.Id, viewModel.SelectedApplication.ControlHolder, viewModel.SelectedApplication.Name); } } @@ -98,7 +86,7 @@ namespace Ryujinx.Ava.UI.Controls if (viewModel?.SelectedApplication != null) { - await TitleUpdateWindow.Show(viewModel.VirtualFileSystem, ulong.Parse(viewModel.SelectedApplication.TitleId, NumberStyles.HexNumber), viewModel.SelectedApplication.TitleName); + await TitleUpdateWindow.Show(viewModel.VirtualFileSystem, viewModel.SelectedApplication); } } @@ -108,7 +96,7 @@ namespace Ryujinx.Ava.UI.Controls if (viewModel?.SelectedApplication != null) { - await DownloadableContentManagerWindow.Show(viewModel.VirtualFileSystem, ulong.Parse(viewModel.SelectedApplication.TitleId, NumberStyles.HexNumber), viewModel.SelectedApplication.TitleName); + await DownloadableContentManagerWindow.Show(viewModel.VirtualFileSystem, viewModel.SelectedApplication); } } @@ -120,8 +108,8 @@ namespace Ryujinx.Ava.UI.Controls { await new CheatWindow( viewModel.VirtualFileSystem, - viewModel.SelectedApplication.TitleId, - viewModel.SelectedApplication.TitleName, + viewModel.SelectedApplication.IdString, + viewModel.SelectedApplication.Name, viewModel.SelectedApplication.Path).ShowDialog(viewModel.TopLevel as Window); } } @@ -133,7 +121,7 @@ namespace Ryujinx.Ava.UI.Controls if (viewModel?.SelectedApplication != null) { string modsBasePath = ModLoader.GetModsBasePath(); - string titleModsPath = ModLoader.GetTitleDir(modsBasePath, viewModel.SelectedApplication.TitleId); + string titleModsPath = ModLoader.GetTitleDir(modsBasePath, viewModel.SelectedApplication.IdString); OpenHelper.OpenFolder(titleModsPath); } @@ -146,7 +134,7 @@ namespace Ryujinx.Ava.UI.Controls if (viewModel?.SelectedApplication != null) { string sdModsBasePath = ModLoader.GetSdModsBasePath(); - string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, viewModel.SelectedApplication.TitleId); + string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, viewModel.SelectedApplication.IdString); OpenHelper.OpenFolder(titleModsPath); } @@ -160,15 +148,15 @@ namespace Ryujinx.Ava.UI.Controls { UserResult result = await ContentDialogHelper.CreateConfirmationDialog( LocaleManager.Instance[LocaleKeys.DialogWarning], - LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionMessage, viewModel.SelectedApplication.TitleName), + LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionMessage, viewModel.SelectedApplication.Name), LocaleManager.Instance[LocaleKeys.InputDialogYes], LocaleManager.Instance[LocaleKeys.InputDialogNo], LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); if (result == UserResult.Yes) { - DirectoryInfo mainDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "cpu", "0")); - DirectoryInfo backupDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "cpu", "1")); + DirectoryInfo mainDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu", "0")); + DirectoryInfo backupDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu", "1")); List cacheFiles = new(); @@ -208,14 +196,14 @@ namespace Ryujinx.Ava.UI.Controls { UserResult result = await ContentDialogHelper.CreateConfirmationDialog( LocaleManager.Instance[LocaleKeys.DialogWarning], - LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogShaderDeletionMessage, viewModel.SelectedApplication.TitleName), + LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogShaderDeletionMessage, viewModel.SelectedApplication.Name), LocaleManager.Instance[LocaleKeys.InputDialogYes], LocaleManager.Instance[LocaleKeys.InputDialogNo], LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); if (result == UserResult.Yes) { - DirectoryInfo shaderCacheDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "shader")); + DirectoryInfo shaderCacheDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "shader")); List oldCacheDirectories = new(); List newCacheFiles = new(); @@ -263,7 +251,7 @@ namespace Ryujinx.Ava.UI.Controls if (viewModel?.SelectedApplication != null) { - string ptcDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "cpu"); + string ptcDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu"); string mainDir = Path.Combine(ptcDir, "0"); string backupDir = Path.Combine(ptcDir, "1"); @@ -284,7 +272,7 @@ namespace Ryujinx.Ava.UI.Controls if (viewModel?.SelectedApplication != null) { - string shaderCacheDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "shader"); + string shaderCacheDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "shader"); if (!Directory.Exists(shaderCacheDir)) { @@ -305,7 +293,7 @@ namespace Ryujinx.Ava.UI.Controls viewModel.StorageProvider, NcaSectionType.Code, viewModel.SelectedApplication.Path, - viewModel.SelectedApplication.TitleName); + viewModel.SelectedApplication.Name); } } @@ -319,7 +307,7 @@ namespace Ryujinx.Ava.UI.Controls viewModel.StorageProvider, NcaSectionType.Data, viewModel.SelectedApplication.Path, - viewModel.SelectedApplication.TitleName); + viewModel.SelectedApplication.Name); } } @@ -333,7 +321,7 @@ namespace Ryujinx.Ava.UI.Controls viewModel.StorageProvider, NcaSectionType.Logo, viewModel.SelectedApplication.Path, - viewModel.SelectedApplication.TitleName); + viewModel.SelectedApplication.Name); } } @@ -344,7 +332,7 @@ namespace Ryujinx.Ava.UI.Controls if (viewModel?.SelectedApplication != null) { ApplicationData selectedApplication = viewModel.SelectedApplication; - ShortcutHelper.CreateAppShortcut(selectedApplication.Path, selectedApplication.TitleName, selectedApplication.TitleId, selectedApplication.Icon); + ShortcutHelper.CreateAppShortcut(selectedApplication.Path, selectedApplication.Name, selectedApplication.IdString, selectedApplication.Icon); } } @@ -354,7 +342,7 @@ namespace Ryujinx.Ava.UI.Controls if (viewModel?.SelectedApplication != null) { - await viewModel.LoadApplication(viewModel.SelectedApplication.Path); + await viewModel.LoadApplication(viewModel.SelectedApplication); } } } diff --git a/src/Ryujinx.Ava/UI/Controls/ApplicationGridView.axaml b/src/Ryujinx.Ava/UI/Controls/ApplicationGridView.axaml index bbdb4c4a7..5919652e2 100644 --- a/src/Ryujinx.Ava/UI/Controls/ApplicationGridView.axaml +++ b/src/Ryujinx.Ava/UI/Controls/ApplicationGridView.axaml @@ -82,7 +82,7 @@ diff --git a/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml b/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml index 9004f7518..24ec2b357 100644 --- a/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml +++ b/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml @@ -85,7 +85,7 @@ Path.GetFileName(ContainerPath); + public string Label => + Path.GetExtension(FileName)?.ToLower() == ".xci" ? $"{LocaleManager.Instance[LocaleKeys.TitleBundledDlcLabel]} {FileName}" : FileName; + public DownloadableContentModel(string titleId, string containerPath, string fullPath, bool enabled) { TitleId = titleId; diff --git a/src/Ryujinx.Ava/UI/Models/SaveModel.cs b/src/Ryujinx.Ava/UI/Models/SaveModel.cs index 7b476932b..2e3ed3bae 100644 --- a/src/Ryujinx.Ava/UI/Models/SaveModel.cs +++ b/src/Ryujinx.Ava/UI/Models/SaveModel.cs @@ -46,14 +46,14 @@ namespace Ryujinx.Ava.UI.Models TitleId = info.ProgramId; UserId = info.UserId; - var appData = MainWindow.MainWindowViewModel.Applications.FirstOrDefault(x => x.TitleId.ToUpper() == TitleIdString); + var appData = MainWindow.MainWindowViewModel.Applications.FirstOrDefault(x => x.IdString.ToUpper() == TitleIdString); InGameList = appData != null; if (InGameList) { Icon = appData.Icon; - Title = appData.TitleName; + Title = appData.Name; } else { diff --git a/src/Ryujinx.Ava/UI/Models/TitleUpdateModel.cs b/src/Ryujinx.Ava/UI/Models/TitleUpdateModel.cs index 3b44e8ee6..fae2a08d0 100644 --- a/src/Ryujinx.Ava/UI/Models/TitleUpdateModel.cs +++ b/src/Ryujinx.Ava/UI/Models/TitleUpdateModel.cs @@ -8,7 +8,10 @@ namespace Ryujinx.Ava.UI.Models public ApplicationControlProperty Control { get; } public string Path { get; } - public string Label => LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.TitleUpdateVersionLabel, Control.DisplayVersionString.ToString()); + public string Label => LocaleManager.Instance.UpdateAndGetDynamicValue( + System.IO.Path.GetExtension(Path)?.ToLower() == ".xci" ? LocaleKeys.TitleBundledUpdateVersionLabel : LocaleKeys.TitleUpdateVersionLabel, + Control.DisplayVersionString.ToString() + ); public TitleUpdateModel(ApplicationControlProperty control, string path) { diff --git a/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs index cdecae77d..9f3a0045d 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs @@ -17,11 +17,12 @@ using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using Ryujinx.Common.Utilities; using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.Loaders.Processes.Extensions; +using Ryujinx.Ui.App.Common; using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Threading.Tasks; using Application = Avalonia.Application; using Path = System.IO.Path; @@ -38,7 +39,7 @@ namespace Ryujinx.Ava.UI.ViewModels private AvaloniaList _selectedDownloadableContents = new(); private string _search; - private readonly ulong _titleId; + private readonly ApplicationData _applicationData; private static readonly DownloadableContentJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); @@ -92,18 +93,25 @@ namespace Ryujinx.Ava.UI.ViewModels public IStorageProvider StorageProvider; - public DownloadableContentManagerViewModel(VirtualFileSystem virtualFileSystem, ulong titleId) + public DownloadableContentManagerViewModel(VirtualFileSystem virtualFileSystem, ApplicationData applicationData) { _virtualFileSystem = virtualFileSystem; - _titleId = titleId; + _applicationData = applicationData; if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { StorageProvider = desktop.MainWindow.StorageProvider; } - _downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "dlc.json"); + _downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, applicationData.IdString, "dlc.json"); + + if (!File.Exists(_downloadableContentJsonPath)) + { + _downloadableContentContainerList = new List(); + + Save(); + } try { @@ -120,6 +128,9 @@ namespace Ryujinx.Ava.UI.ViewModels private void LoadDownloadableContents() { + // NOTE: Try to load downloadable contents from PFS first. + AddDownloadableContent(_applicationData.Path); + foreach (DownloadableContentContainer downloadableContentContainer in _downloadableContentContainerList) { if (File.Exists(downloadableContentContainer.ContainerPath)) @@ -127,7 +138,11 @@ namespace Ryujinx.Ava.UI.ViewModels using FileStream containerFile = File.OpenRead(downloadableContentContainer.ContainerPath); PartitionFileSystem partitionFileSystem = new(); - partitionFileSystem.Initialize(containerFile.AsStorage()).ThrowIfFailure(); + + if (partitionFileSystem.Initialize(containerFile.AsStorage()).IsFailure()) + { + continue; + } _virtualFileSystem.ImportTickets(partitionFileSystem); @@ -220,22 +235,34 @@ namespace Ryujinx.Ava.UI.ViewModels foreach (var file in result) { - await AddDownloadableContent(file.Path.LocalPath); + if (!AddDownloadableContent(file.Path.LocalPath)) + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogDlcNoDlcErrorMessage]); + } } } - private async Task AddDownloadableContent(string path) + private bool AddDownloadableContent(string path) { if (!File.Exists(path) || DownloadableContents.FirstOrDefault(x => x.ContainerPath == path) != null) { - return; + return true; } using FileStream containerFile = File.OpenRead(path); - PartitionFileSystem partitionFileSystem = new(); - partitionFileSystem.Initialize(containerFile.AsStorage()).ThrowIfFailure(); - bool containsDownloadableContent = false; + IFileSystem partitionFileSystem; + + if (Path.GetExtension(path).ToLower() == ".xci") + { + partitionFileSystem = new Xci(_virtualFileSystem.KeySet, containerFile.AsStorage()).OpenPartition(XciPartitionType.Secure); + } + else + { + var pfsTemp = new PartitionFileSystem(); + pfsTemp.Initialize(containerFile.AsStorage()).ThrowIfFailure(); + partitionFileSystem = pfsTemp; + } _virtualFileSystem.ImportTickets(partitionFileSystem); @@ -253,7 +280,7 @@ namespace Ryujinx.Ava.UI.ViewModels if (nca.Header.ContentType == NcaContentType.PublicData) { - if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000) != _titleId) + if (nca.GetProgramIdBase() != _applicationData.IdBase) { break; } @@ -265,14 +292,11 @@ namespace Ryujinx.Ava.UI.ViewModels OnPropertyChanged(nameof(UpdateCount)); Sort(); - containsDownloadableContent = true; + return true; } } - if (!containsDownloadableContent) - { - await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogDlcNoDlcErrorMessage]); - } + return false; } public void Remove(DownloadableContentModel model) diff --git a/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs index 80df5d398..692df483d 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs @@ -95,7 +95,7 @@ namespace Ryujinx.Ava.UI.ViewModels private bool _canUpdate = true; private Cursor _cursor; private string _title; - private string _currentEmulatedGamePath; + private ApplicationData _currentApplicationData; private readonly AutoResetEvent _rendererWaitEvent; private WindowState _windowState; private double _windowWidth; @@ -106,7 +106,6 @@ namespace Ryujinx.Ava.UI.ViewModels public ApplicationData ListSelectedApplication; public ApplicationData GridSelectedApplication; - private string TitleName { get; set; } internal AppHost AppHost { get; set; } public MainWindowViewModel() @@ -930,8 +929,8 @@ namespace Ryujinx.Ava.UI.ViewModels return SortMode switch { #pragma warning disable IDE0055 // Disable formatting - ApplicationSort.Title => IsAscending ? SortExpressionComparer.Ascending(app => app.TitleName) - : SortExpressionComparer.Descending(app => app.TitleName), + ApplicationSort.Title => IsAscending ? SortExpressionComparer.Ascending(app => app.Name) + : SortExpressionComparer.Descending(app => app.Name), ApplicationSort.Developer => IsAscending ? SortExpressionComparer.Ascending(app => app.Developer) : SortExpressionComparer.Descending(app => app.Developer), ApplicationSort.LastPlayed => new LastPlayedSortComparer(IsAscending), @@ -968,7 +967,7 @@ namespace Ryujinx.Ava.UI.ViewModels { if (arg is ApplicationData app) { - return string.IsNullOrWhiteSpace(_searchText) || app.TitleName.ToLower().Contains(_searchText.ToLower()); + return string.IsNullOrWhiteSpace(_searchText) || app.Name.ToLower().Contains(_searchText.ToLower()); } return false; @@ -1097,7 +1096,7 @@ namespace Ryujinx.Ava.UI.ViewModels IsLoadingIndeterminate = false; break; case LoadState.Loaded: - LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, TitleName); + LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, _currentApplicationData.Name); IsLoadingIndeterminate = true; CacheLoadStatus = ""; break; @@ -1117,7 +1116,7 @@ namespace Ryujinx.Ava.UI.ViewModels IsLoadingIndeterminate = false; break; case ShaderCacheLoadingState.Loaded: - LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, TitleName); + LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, _currentApplicationData.Name); IsLoadingIndeterminate = true; CacheLoadStatus = ""; break; @@ -1168,13 +1167,13 @@ namespace Ryujinx.Ava.UI.ViewModels { UserChannelPersistence.ShouldRestart = false; - await LoadApplication(_currentEmulatedGamePath); + await LoadApplication(_currentApplicationData); } else { // Otherwise, clear state. UserChannelPersistence = new UserChannelPersistence(); - _currentEmulatedGamePath = null; + _currentApplicationData = null; } } @@ -1451,7 +1450,12 @@ namespace Ryujinx.Ava.UI.ViewModels if (result.Count > 0) { - await LoadApplication(result[0].Path.LocalPath); + ApplicationData applicationData = new() + { + Path = result[0].Path.LocalPath, + }; + + await LoadApplication(applicationData); } } @@ -1465,11 +1469,17 @@ namespace Ryujinx.Ava.UI.ViewModels if (result.Count > 0) { - await LoadApplication(result[0].Path.LocalPath); + ApplicationData applicationData = new() + { + Name = Path.GetFileNameWithoutExtension(result[0].Path.LocalPath), + Path = result[0].Path.LocalPath, + }; + + await LoadApplication(applicationData); } } - public async Task LoadApplication(string path, bool startFullscreen = false, string titleName = "") + public async Task LoadApplication(ApplicationData application, bool startFullscreen = false) { if (AppHost != null) { @@ -1489,7 +1499,7 @@ namespace Ryujinx.Ava.UI.ViewModels Logger.RestartTime(); - SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(path, ConfigurationState.Instance.System.Language); + SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(application.Path, ConfigurationState.Instance.System.Language, application.Id); PrepareLoadScreen(); @@ -1498,7 +1508,8 @@ namespace Ryujinx.Ava.UI.ViewModels AppHost = new AppHost( RendererHostControl, InputManager, - path, + application.Path, + application.Id, VirtualFileSystem, ContentManager, AccountManager, @@ -1516,17 +1527,17 @@ namespace Ryujinx.Ava.UI.ViewModels CanUpdate = false; - LoadHeading = TitleName = titleName; + LoadHeading = application.Name; - if (string.IsNullOrWhiteSpace(titleName)) + if (string.IsNullOrWhiteSpace(application.Name)) { LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, AppHost.Device.Processes.ActiveApplication.Name); - TitleName = AppHost.Device.Processes.ActiveApplication.Name; + application.Name = AppHost.Device.Processes.ActiveApplication.Name; } SwitchToRenderer(startFullscreen); - _currentEmulatedGamePath = path; + _currentApplicationData = application; Thread gameThread = new(InitializeGame) { Name = "GUI.WindowThread" }; gameThread.Start(); diff --git a/src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs index 5090a8c70..7bb96131d 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs @@ -1,4 +1,3 @@ -using Avalonia; using Avalonia.Collections; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Platform.Storage; @@ -8,6 +7,7 @@ using LibHac.Fs; using LibHac.Fs.Fsa; using LibHac.FsSystem; using LibHac.Ns; +using LibHac.Tools.Fs; using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.NcaUtils; using Ryujinx.Ava.Common.Locale; @@ -17,12 +17,16 @@ using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using Ryujinx.Common.Utilities; using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.Loaders.Processes.Extensions; using Ryujinx.Ui.App.Common; +using Ryujinx.Ui.Common.Configuration; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; +using Application = Avalonia.Application; +using ContentType = LibHac.Ncm.ContentType; using Path = System.IO.Path; using SpanHelpers = LibHac.Common.SpanHelpers; @@ -33,7 +37,7 @@ namespace Ryujinx.Ava.UI.ViewModels public TitleUpdateMetadata TitleUpdateWindowData; public readonly string TitleUpdateJsonPath; private VirtualFileSystem VirtualFileSystem { get; } - private ulong TitleId { get; } + private ApplicationData ApplicationData { get; } private AvaloniaList _titleUpdates = new(); private AvaloniaList _views = new(); @@ -73,18 +77,18 @@ namespace Ryujinx.Ava.UI.ViewModels public IStorageProvider StorageProvider; - public TitleUpdateViewModel(VirtualFileSystem virtualFileSystem, ulong titleId) + public TitleUpdateViewModel(VirtualFileSystem virtualFileSystem, ApplicationData applicationData) { VirtualFileSystem = virtualFileSystem; - TitleId = titleId; + ApplicationData = applicationData; if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { StorageProvider = desktop.MainWindow.StorageProvider; } - TitleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "updates.json"); + TitleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, ApplicationData.IdString, "updates.json"); try { @@ -92,7 +96,7 @@ namespace Ryujinx.Ava.UI.ViewModels } catch { - Logger.Warning?.Print(LogClass.Application, $"Failed to deserialize title update data for {TitleId} at {TitleUpdateJsonPath}"); + Logger.Warning?.Print(LogClass.Application, $"Failed to deserialize title update data for {ApplicationData.IdString} at {TitleUpdateJsonPath}"); TitleUpdateWindowData = new TitleUpdateMetadata { @@ -108,6 +112,9 @@ namespace Ryujinx.Ava.UI.ViewModels private void LoadUpdates() { + // Try to load updates from PFS first + AddUpdate(ApplicationData.Path, true); + foreach (string path in TitleUpdateWindowData.Paths) { AddUpdate(path); @@ -162,17 +169,41 @@ namespace Ryujinx.Ava.UI.ViewModels } } - private void AddUpdate(string path) + private void AddUpdate(string path, bool ignoreNotFound = false) { if (File.Exists(path) && TitleUpdates.All(x => x.Path != path)) { + IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks + ? IntegrityCheckLevel.ErrorOnInvalid + : IntegrityCheckLevel.None; + using FileStream file = new(path, FileMode.Open, FileAccess.Read); + IFileSystem pfs; + try { - var pfs = new PartitionFileSystem(); - pfs.Initialize(file.AsStorage()).ThrowIfFailure(); - (Nca patchNca, Nca controlNca) = ApplicationLibrary.GetGameUpdateDataFromPartition(VirtualFileSystem, pfs, TitleId.ToString("x16"), 0); + if (Path.GetExtension(path).ToLower() == ".xci") + { + pfs = new Xci(VirtualFileSystem.KeySet, file.AsStorage()).OpenPartition(XciPartitionType.Secure); + } + else + { + var pfsTemp = new PartitionFileSystem(); + pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure(); + pfs = pfsTemp; + } + + Dictionary updates = pfs.GetUpdateData(VirtualFileSystem, checkLevel); + + Nca patchNca = null; + Nca controlNca = null; + + if (updates.TryGetValue(ApplicationData.Id, out ContentCollection content)) + { + patchNca = content.GetNcaByType(VirtualFileSystem.KeySet, ContentType.Program); + controlNca = content.GetNcaByType(VirtualFileSystem.KeySet, ContentType.Control); + } if (controlNca != null && patchNca != null) { @@ -187,7 +218,10 @@ namespace Ryujinx.Ava.UI.ViewModels } else { - Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage])); + if (!ignoreNotFound) + { + Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage])); + } } } catch (Exception ex) diff --git a/src/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs b/src/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs index 4f2d262da..af3c5deb6 100644 --- a/src/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs +++ b/src/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs @@ -10,6 +10,7 @@ using Ryujinx.Ava.UI.Windows; using Ryujinx.Common; using Ryujinx.Common.Utilities; using Ryujinx.Modules; +using Ryujinx.Ui.App.Common; using Ryujinx.Ui.Common; using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Common.Helper; @@ -131,7 +132,14 @@ namespace Ryujinx.Ava.UI.Views.Main if (!string.IsNullOrEmpty(contentPath)) { - await ViewModel.LoadApplication(contentPath, false, "Mii Applet"); + ApplicationData applicationData = new() + { + Name = "miiEdit", + Id = 0x0100000000001009, + Path = contentPath, + }; + + await ViewModel.LoadApplication(applicationData, ViewModel.IsFullScreen || ViewModel.StartGamesInFullscreen); } } diff --git a/src/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml b/src/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml index cc21b5c60..7a716fb2a 100644 --- a/src/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml +++ b/src/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml @@ -104,7 +104,7 @@ Content="{locale:Locale GameListHeaderApplication}" GroupName="Sort" IsChecked="{Binding IsSortedByTitle, Mode=OneTime}" - Tag="Title" /> + Tag="Application" /> (); + IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks + ? IntegrityCheckLevel.ErrorOnInvalid + : IntegrityCheckLevel.None; Heading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.CheatWindowHeading, titleName, titleId.ToUpper()); - BuildId = ApplicationData.GetApplicationBuildId(virtualFileSystem, titlePath); + BuildId = ApplicationData.GetBuildId(virtualFileSystem, checkLevel, titlePath); InitializeComponent(); diff --git a/src/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml b/src/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml index 99cf28e77..98aac09ce 100644 --- a/src/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml +++ b/src/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml @@ -97,7 +97,7 @@ MaxLines="2" TextWrapping="Wrap" TextTrimming="CharacterEllipsis" - Text="{Binding FileName}" /> + Text="{Binding Label}" /> x.OfType().Name("DialogSpace").Child().OfType()); diff --git a/src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs index c78f4160d..352ac4e54 100644 --- a/src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs +++ b/src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs @@ -4,6 +4,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Interactivity; using Avalonia.Threading; using FluentAvalonia.UI.Controls; +using LibHac.Tools.FsSystem; using Ryujinx.Ava.Common; using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Input; @@ -23,7 +24,6 @@ using Ryujinx.Ui.Common; using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Common.Helper; using System; -using System.IO; using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; @@ -139,9 +139,7 @@ namespace Ryujinx.Ava.UI.Windows { ViewModel.SelectedIcon = args.Application.Icon; - string path = new FileInfo(args.Application.Path).FullName; - - ViewModel.LoadApplication(path).Wait(); + ViewModel.LoadApplication(args.Application).Wait(); } args.Handled = true; @@ -190,7 +188,11 @@ namespace Ryujinx.Ava.UI.Windows LibHacHorizonManager.InitializeBcatServer(); LibHacHorizonManager.InitializeSystemClients(); - ApplicationLibrary = new ApplicationLibrary(VirtualFileSystem); + IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks + ? IntegrityCheckLevel.ErrorOnInvalid + : IntegrityCheckLevel.None; + + ApplicationLibrary = new ApplicationLibrary(VirtualFileSystem, checkLevel); // 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 @@ -297,7 +299,12 @@ namespace Ryujinx.Ava.UI.Windows { _deferLoad = false; - ViewModel.LoadApplication(_launchPath, _startFullscreen).Wait(); + ApplicationData applicationData = new() + { + Path = _launchPath, + }; + + ViewModel.LoadApplication(applicationData, _startFullscreen).Wait(); } } else diff --git a/src/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml.cs index 7ece63355..8ecf165ce 100644 --- a/src/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml.cs +++ b/src/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml.cs @@ -7,15 +7,15 @@ using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Models; using Ryujinx.Ava.UI.ViewModels; using Ryujinx.HLE.FileSystem; +using Ryujinx.Ui.App.Common; using Ryujinx.Ui.Common.Helper; using System.Threading.Tasks; -using Button = Avalonia.Controls.Button; namespace Ryujinx.Ava.UI.Windows { public partial class TitleUpdateWindow : UserControl { - public TitleUpdateViewModel ViewModel; + public readonly TitleUpdateViewModel ViewModel; public TitleUpdateWindow() { @@ -24,22 +24,22 @@ namespace Ryujinx.Ava.UI.Windows InitializeComponent(); } - public TitleUpdateWindow(VirtualFileSystem virtualFileSystem, ulong titleId) + public TitleUpdateWindow(VirtualFileSystem virtualFileSystem, ApplicationData applicationData) { - DataContext = ViewModel = new TitleUpdateViewModel(virtualFileSystem, titleId); + DataContext = ViewModel = new TitleUpdateViewModel(virtualFileSystem, applicationData); InitializeComponent(); } - public static async Task Show(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName) + public static async Task Show(VirtualFileSystem virtualFileSystem, ApplicationData applicationData) { ContentDialog contentDialog = new() { PrimaryButtonText = "", SecondaryButtonText = "", CloseButtonText = "", - Content = new TitleUpdateWindow(virtualFileSystem, titleId), - Title = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.GameUpdateWindowHeading, titleName, titleId.ToString("X16")), + Content = new TitleUpdateWindow(virtualFileSystem, applicationData), + Title = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.GameUpdateWindowHeading, applicationData.Name, applicationData.IdString), }; Style bottomBorder = new(x => x.OfType().Name("DialogSpace").Child().OfType()); diff --git a/src/Ryujinx.HLE/FileSystem/ContentCollection.cs b/src/Ryujinx.HLE/FileSystem/ContentCollection.cs new file mode 100644 index 000000000..1c19887be --- /dev/null +++ b/src/Ryujinx.HLE/FileSystem/ContentCollection.cs @@ -0,0 +1,61 @@ +using LibHac.Common.Keys; +using LibHac.Fs.Fsa; +using LibHac.Ncm; +using LibHac.Tools.FsSystem.NcaUtils; +using LibHac.Tools.Ncm; +using Ryujinx.HLE.Loaders.Processes.Extensions; +using System; + +namespace Ryujinx.HLE.FileSystem +{ + /// + /// Thin wrapper around + /// + public class ContentCollection + { + private readonly IFileSystem _pfs; + private readonly Cnmt _cnmt; + + public ulong Id => _cnmt.TitleId; + public TitleVersion Version => _cnmt.TitleVersion; + public ContentMetaType Type => _cnmt.Type; + public ulong ApplicationId => _cnmt.ApplicationTitleId; + public ulong PatchId => _cnmt.PatchTitleId; + public TitleVersion RequiredSystemVersion => _cnmt.MinimumSystemVersion; + public TitleVersion RequiredApplicationVersion => _cnmt.MinimumApplicationVersion; + public byte[] Digest => _cnmt.Hash; + + public ulong ProgramBaseId => Id & ~0x1FFFUL; + public bool IsSystemTitle => _cnmt.Type < ContentMetaType.Application; + + public ContentCollection(IFileSystem pfs, Cnmt cnmt) + { + _pfs = pfs; + _cnmt = cnmt; + } + + public Nca GetNcaByType(KeySet keySet, ContentType type, int programIndex = 0) + { + // TODO: Replace this with a check for IdOffset as soon as LibHac supports it: + // && entry.IdOffset == programIndex + + foreach (var entry in _cnmt.ContentEntries) + { + if (entry.Type != type) + { + continue; + } + + string ncaId = BitConverter.ToString(entry.NcaId).Replace("-", null).ToLower(); + Nca nca = _pfs.GetNca(keySet, $"/{ncaId}.nca"); + + if (nca.GetProgramIndex() == programIndex) + { + return nca; + } + } + + return null; + } + } +} diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs index 4568b44da..6863d1a7c 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs @@ -2,21 +2,31 @@ using LibHac.Common; using LibHac.Fs; using LibHac.Fs.Fsa; +using LibHac.FsSystem; using LibHac.Loader; using LibHac.Ncm; using LibHac.Ns; +using LibHac.Tools.Fs; using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.NcaUtils; +using LibHac.Tools.Ncm; +using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; +using Ryujinx.Common.Utilities; +using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS; using System.IO; using System.Linq; using ApplicationId = LibHac.Ncm.ApplicationId; +using ContentType = LibHac.Ncm.ContentType; +using Path = System.IO.Path; namespace Ryujinx.HLE.Loaders.Processes.Extensions { - static class NcaExtensions + public static class NcaExtensions { + private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + public static ProcessResult Load(this Nca nca, Switch device, Nca patchNca, Nca controlNca) { // Extract RomFs and ExeFs from NCA. @@ -47,7 +57,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions nacpData = controlNca.GetNacp(device); } - /* TODO: Rework this since it's wrong and doesn't work as it takes the DisplayVersion from a "potential" inexistant update. + /* TODO: Rework this since it's wrong and doesn't work as it takes the DisplayVersion from a "potential" non-existent update. // Load program 0 control NCA as we are going to need it for display version. (_, Nca updateProgram0ControlNca) = GetGameUpdateData(_device.Configuration.VirtualFileSystem, mainNca.Header.TitleId.ToString("x16"), 0, out _); @@ -86,6 +96,11 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions return processResult; } + public static ulong GetProgramIdBase(this Nca nca) + { + return nca.Header.TitleId & ~0x1FFFUL; + } + public static int GetProgramIndex(this Nca nca) { return (int)(nca.Header.TitleId & 0xF); @@ -96,6 +111,11 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions return nca.Header.ContentType == NcaContentType.Program; } + public static bool IsMain(this Nca nca) + { + return nca.IsProgram() && !nca.IsPatch(); + } + public static bool IsPatch(this Nca nca) { int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); @@ -108,6 +128,56 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions return nca.Header.ContentType == NcaContentType.Control; } + public static (Nca, Nca) GetUpdateData(this Nca mainNca, VirtualFileSystem fileSystem, IntegrityCheckLevel checkLevel, int programIndex, out string updatePath) + { + updatePath = "(unknown)"; + + // Load Update NCAs. + Nca updatePatchNca = null; + Nca updateControlNca = null; + + // Clear the program index part. + ulong titleIdBase = mainNca.GetProgramIdBase(); + + // Load update information if exists. + string titleUpdateMetadataPath = Path.Combine(AppDataManager.GamesDirPath, mainNca.Header.TitleId.ToString("x16"), "updates.json"); + if (File.Exists(titleUpdateMetadataPath)) + { + updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected; + if (File.Exists(updatePath)) + { + var updateFile = new FileStream(updatePath, FileMode.Open, FileAccess.Read); + + IFileSystem updatePartitionFileSystem; + + if (Path.GetExtension(updatePath).ToLower() == ".xci") + { + updatePartitionFileSystem = new Xci(fileSystem.KeySet, updateFile.AsStorage()).OpenPartition(XciPartitionType.Secure); + } + else + { + PartitionFileSystem pfsTemp = new(); + pfsTemp.Initialize(updateFile.AsStorage()).ThrowIfFailure(); + updatePartitionFileSystem = pfsTemp; + } + + foreach ((ulong updateTitleId, ContentCollection content) in updatePartitionFileSystem.GetUpdateData(fileSystem, checkLevel)) + { + if ((updateTitleId & ~0x1FFFUL) != titleIdBase) + { + continue; + } + + updatePatchNca = content.GetNcaByType(fileSystem.KeySet, ContentType.Program, programIndex); + updateControlNca = content.GetNcaByType(fileSystem.KeySet, ContentType.Control, programIndex); + break; + } + } + } + + return (updatePatchNca, updateControlNca); + } + public static IFileSystem GetExeFs(this Nca nca, Switch device, Nca patchNca = null) { IFileSystem exeFs = null; @@ -172,5 +242,31 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions return nacpData; } + + public static Cnmt GetCnmt(this Nca cnmtNca, IntegrityCheckLevel checkLevel, ContentMetaType metaType) + { + string path = $"/{metaType}_{cnmtNca.Header.TitleId:x16}.cnmt"; + using var cnmtFile = new UniqueRef(); + + try + { + Result result = cnmtNca.OpenFileSystem(0, checkLevel) + .OpenFile(ref cnmtFile.Ref, path.ToU8Span(), OpenMode.Read); + + if (result.IsSuccess()) + { + return new Cnmt(cnmtFile.Release().AsStream()); + } + } + catch (HorizonResultException ex) + { + if (!ResultFs.PathNotFound.Includes(ex.ResultValue)) + { + Logger.Warning?.Print(LogClass.Application, $"Failed get cnmt for '{cnmtNca.Header.TitleId:x16}' from nca: {ex.Message}"); + } + } + + return null; + } } } diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs index 50f7d5853..5f45cd459 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs @@ -1,26 +1,87 @@ using LibHac.Common; +using LibHac.Common.Keys; using LibHac.Fs; using LibHac.Fs.Fsa; using LibHac.FsSystem; +using LibHac.Ncm; using LibHac.Tools.Fs; using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.NcaUtils; +using LibHac.Tools.Ncm; using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using Ryujinx.Common.Utilities; +using Ryujinx.HLE.FileSystem; using System; using System.Collections.Generic; -using System.Globalization; using System.IO; +using ContentType = LibHac.Ncm.ContentType; namespace Ryujinx.HLE.Loaders.Processes.Extensions { public static class PartitionFileSystemExtensions { private static readonly DownloadableContentJsonSerializerContext _contentSerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); - private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); - internal static (bool, ProcessResult) TryLoad(this PartitionFileSystemCore partitionFileSystem, Switch device, string path, out string errorMessage) + public static Dictionary GetApplicationData(this IFileSystem partitionFileSystem, + VirtualFileSystem fileSystem, IntegrityCheckLevel checkLevel) + { + fileSystem.ImportTickets(partitionFileSystem); + + var programs = new Dictionary(); + + foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.cnmt.nca")) + { + Cnmt cnmt = partitionFileSystem.GetNca(fileSystem.KeySet, fileEntry.FullPath).GetCnmt(checkLevel, ContentMetaType.Application); + + if (cnmt == null) + { + continue; + } + + ContentCollection content = new(partitionFileSystem, cnmt); + + if (content.Type != ContentMetaType.Application) + { + continue; + } + + programs.TryAdd(content.ApplicationId, content); + } + + return programs; + } + + public static Dictionary GetUpdateData(this IFileSystem partitionFileSystem, + VirtualFileSystem fileSystem, IntegrityCheckLevel checkLevel) + { + fileSystem.ImportTickets(partitionFileSystem); + + var programs = new Dictionary(); + + foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.cnmt.nca")) + { + Cnmt cnmt = partitionFileSystem.GetNca(fileSystem.KeySet, fileEntry.FullPath).GetCnmt(checkLevel, ContentMetaType.Patch); + + if (cnmt == null) + { + continue; + } + + ContentCollection content = new(partitionFileSystem, cnmt); + + if (content.Type != ContentMetaType.Patch) + { + continue; + } + + programs.TryAdd(content.ApplicationId, content); + } + + return programs; + } + + internal static (bool, ProcessResult) TryLoad(this PartitionFileSystemCore partitionFileSystem, Switch device, string path, ulong titleId, out string errorMessage) where TMetaData : PartitionFileSystemMetaCore, new() where TFormat : IPartitionFileSystemFormat where THeader : unmanaged, IPartitionFileSystemHeader @@ -35,31 +96,22 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions try { - device.Configuration.VirtualFileSystem.ImportTickets(partitionFileSystem); + Dictionary applications = partitionFileSystem.GetApplicationData(device.FileSystem, device.System.FsIntegrityCheckLevel); - // TODO: To support multi-games container, this should use CNMT NCA instead. - foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca")) + if (titleId == 0) { - Nca nca = partitionFileSystem.GetNca(device, fileEntry.FullPath); - - if (nca.GetProgramIndex() != device.Configuration.UserChannelPersistence.Index) + foreach ((ulong _, ContentCollection content) in applications) { - continue; - } - - if (nca.IsPatch()) - { - patchNca = nca; - } - else if (nca.IsProgram()) - { - mainNca = nca; - } - else if (nca.IsControl()) - { - controlNca = nca; + mainNca = content.GetNcaByType(device.FileSystem.KeySet, ContentType.Program, device.Configuration.UserChannelPersistence.Index); + controlNca = content.GetNcaByType(device.FileSystem.KeySet, ContentType.Control, device.Configuration.UserChannelPersistence.Index); + break; } } + else if (applications.TryGetValue(titleId, out ContentCollection content)) + { + mainNca = content.GetNcaByType(device.FileSystem.KeySet, ContentType.Program, device.Configuration.UserChannelPersistence.Index); + controlNca = content.GetNcaByType(device.FileSystem.KeySet, ContentType.Control, device.Configuration.UserChannelPersistence.Index); + } ProcessLoaderHelper.RegisterProgramMapInfo(device, partitionFileSystem).ThrowIfFailure(); } @@ -79,54 +131,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions return (false, ProcessResult.Failed); } - // Load Update NCAs. - Nca updatePatchNca = null; - Nca updateControlNca = null; - - if (ulong.TryParse(mainNca.Header.TitleId.ToString("x16"), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdBase)) - { - // Clear the program index part. - titleIdBase &= ~0xFUL; - - // Load update information if exists. - string titleUpdateMetadataPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, titleIdBase.ToString("x16"), "updates.json"); - if (File.Exists(titleUpdateMetadataPath)) - { - string updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected; - if (File.Exists(updatePath)) - { - PartitionFileSystem updatePartitionFileSystem = new(); - updatePartitionFileSystem.Initialize(new FileStream(updatePath, FileMode.Open, FileAccess.Read).AsStorage()).ThrowIfFailure(); - - device.Configuration.VirtualFileSystem.ImportTickets(updatePartitionFileSystem); - - // TODO: This should use CNMT NCA instead. - foreach (DirectoryEntryEx fileEntry in updatePartitionFileSystem.EnumerateEntries("/", "*.nca")) - { - Nca nca = updatePartitionFileSystem.GetNca(device, fileEntry.FullPath); - - if (nca.GetProgramIndex() != device.Configuration.UserChannelPersistence.Index) - { - continue; - } - - if ($"{nca.Header.TitleId.ToString("x16")[..^3]}000" != titleIdBase.ToString("x16")) - { - break; - } - - if (nca.IsProgram()) - { - updatePatchNca = nca; - } - else if (nca.IsControl()) - { - updateControlNca = nca; - } - } - } - } - } + (Nca updatePatchNca, Nca updateControlNca) = mainNca.GetUpdateData(device.FileSystem, device.System.FsIntegrityCheckLevel, device.Configuration.UserChannelPersistence.Index, out string _); if (updatePatchNca != null) { @@ -168,18 +173,18 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions return (true, mainNca.Load(device, patchNca, controlNca)); } - errorMessage = "Unable to load: Could not find Main NCA"; + errorMessage = $"Unable to load: Could not find Main NCA for title \"{titleId:X16}\""; return (false, ProcessResult.Failed); } - public static Nca GetNca(this IFileSystem fileSystem, Switch device, string path) + public static Nca GetNca(this IFileSystem fileSystem, KeySet keySet, string path) { using var ncaFile = new UniqueRef(); fileSystem.OpenFile(ref ncaFile.Ref, path.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - return new Nca(device.Configuration.VirtualFileSystem.KeySet, ncaFile.Release().AsStorage()); + return new Nca(keySet, ncaFile.Release().AsStorage()); } } } diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs index 220b868db..6b4a64be8 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs @@ -32,7 +32,7 @@ namespace Ryujinx.HLE.Loaders.Processes _processesByPid = new ConcurrentDictionary(); } - public bool LoadXci(string path) + public bool LoadXci(string path, ulong titleId) { FileStream stream = new(path, FileMode.Open, FileAccess.Read); Xci xci = new(_device.Configuration.VirtualFileSystem.KeySet, stream.AsStorage()); @@ -44,7 +44,7 @@ namespace Ryujinx.HLE.Loaders.Processes return false; } - (bool success, ProcessResult processResult) = xci.OpenPartition(XciPartitionType.Secure).TryLoad(_device, path, out string errorMessage); + (bool success, ProcessResult processResult) = xci.OpenPartition(XciPartitionType.Secure).TryLoad(_device, path, titleId, out string errorMessage); if (!success) { @@ -66,13 +66,13 @@ namespace Ryujinx.HLE.Loaders.Processes return false; } - public bool LoadNsp(string path) + public bool LoadNsp(string path, ulong titleId) { FileStream file = new(path, FileMode.Open, FileAccess.Read); PartitionFileSystem partitionFileSystem = new(); partitionFileSystem.Initialize(file.AsStorage()).ThrowIfFailure(); - (bool success, ProcessResult processResult) = partitionFileSystem.TryLoad(_device, path, out string errorMessage); + (bool success, ProcessResult processResult) = partitionFileSystem.TryLoad(_device, path, titleId, out string errorMessage); if (processResult.ProcessId == 0) { diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs index c229b1742..110bb0928 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs @@ -42,15 +42,14 @@ namespace Ryujinx.HLE.Loaders.Processes foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca")) { - Nca nca = partitionFileSystem.GetNca(device, fileEntry.FullPath); + Nca nca = partitionFileSystem.GetNca(device.FileSystem.KeySet, fileEntry.FullPath); - if (!nca.IsProgram() && nca.IsPatch()) + if (!nca.IsProgram()) { continue; } - ulong currentProgramId = nca.Header.TitleId; - ulong currentMainProgramId = currentProgramId & ~0xFFFul; + ulong currentMainProgramId = nca.GetProgramIdBase(); if (applicationId == 0 && currentMainProgramId != 0) { @@ -67,7 +66,7 @@ namespace Ryujinx.HLE.Loaders.Processes break; } - hasIndex[(int)(currentProgramId & 0xF)] = true; + hasIndex[nca.GetProgramIndex()] = true; } if (programCount == 0) diff --git a/src/Ryujinx.HLE/Switch.cs b/src/Ryujinx.HLE/Switch.cs index ae063a47d..3516049c9 100644 --- a/src/Ryujinx.HLE/Switch.cs +++ b/src/Ryujinx.HLE/Switch.cs @@ -72,9 +72,9 @@ namespace Ryujinx.HLE return Processes.LoadUnpackedNca(exeFsDir, romFsFile); } - public bool LoadXci(string xciFile) + public bool LoadXci(string xciFile, ulong titleId = 0) { - return Processes.LoadXci(xciFile); + return Processes.LoadXci(xciFile, titleId); } public bool LoadNca(string ncaFile) @@ -82,9 +82,9 @@ namespace Ryujinx.HLE return Processes.LoadNca(ncaFile); } - public bool LoadNsp(string nspFile) + public bool LoadNsp(string nspFile, ulong titleId = 0) { - return Processes.LoadNsp(nspFile); + return Processes.LoadNsp(nspFile, titleId); } public bool LoadProgram(string fileName) diff --git a/src/Ryujinx.Ui.Common/App/ApplicationData.cs b/src/Ryujinx.Ui.Common/App/ApplicationData.cs index 65ab01eeb..7495ccb56 100644 --- a/src/Ryujinx.Ui.Common/App/ApplicationData.cs +++ b/src/Ryujinx.Ui.Common/App/ApplicationData.cs @@ -9,9 +9,11 @@ using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.NcaUtils; using Ryujinx.Common.Logging; using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.Loaders.Processes.Extensions; using Ryujinx.Ui.Common.Helper; using System; using System.IO; +using System.Text.Json.Serialization; namespace Ryujinx.Ui.App.Common { @@ -19,10 +21,10 @@ namespace Ryujinx.Ui.App.Common { public bool Favorite { get; set; } public byte[] Icon { get; set; } - public string TitleName { get; set; } - public string TitleId { get; set; } - public string Developer { get; set; } - public string Version { get; set; } + public string Name { get; set; } = "Unknown"; + public ulong Id { get; set; } + public string Developer { get; set; } = "Unknown"; + public string Version { get; set; } = "0"; public TimeSpan TimePlayed { get; set; } public DateTime? LastPlayed { get; set; } public string FileExtension { get; set; } @@ -36,7 +38,11 @@ namespace Ryujinx.Ui.App.Common public string FileSizeString => ValueFormatUtils.FormatFileSize(FileSize); - public static string GetApplicationBuildId(VirtualFileSystem virtualFileSystem, string titleFilePath) + [JsonIgnore] public string IdString => Id.ToString("x16"); + + [JsonIgnore] public ulong IdBase => Id & ~0x1FFFUL; + + public static string GetBuildId(VirtualFileSystem virtualFileSystem, IntegrityCheckLevel checkLevel, string titleFilePath) { using FileStream file = new(titleFilePath, FileMode.Open, FileAccess.Read); @@ -105,7 +111,7 @@ namespace Ryujinx.Ui.App.Common return string.Empty; } - (Nca updatePatchNca, _) = ApplicationLibrary.GetGameUpdateData(virtualFileSystem, mainNca.Header.TitleId.ToString("x16"), 0, out _); + (Nca updatePatchNca, _) = mainNca.GetUpdateData(virtualFileSystem, checkLevel, 0, out string _); if (updatePatchNca != null) { diff --git a/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs b/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs index 46f29851c..976129717 100644 --- a/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs +++ b/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs @@ -14,17 +14,18 @@ using Ryujinx.Common.Utilities; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS.SystemState; using Ryujinx.HLE.Loaders.Npdm; +using Ryujinx.HLE.Loaders.Processes.Extensions; using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Common.Configuration.System; using System; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Text.Json; using System.Threading; +using ContentType = LibHac.Ncm.ContentType; using Path = System.IO.Path; using TimeSpan = System.TimeSpan; @@ -42,15 +43,16 @@ namespace Ryujinx.Ui.App.Common private readonly byte[] _nsoIcon; private readonly VirtualFileSystem _virtualFileSystem; + private readonly IntegrityCheckLevel _checkLevel; private Language _desiredTitleLanguage; private CancellationTokenSource _cancellationToken; private static readonly ApplicationJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); - private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); - public ApplicationLibrary(VirtualFileSystem virtualFileSystem) + public ApplicationLibrary(VirtualFileSystem virtualFileSystem, IntegrityCheckLevel checkLevel) { _virtualFileSystem = virtualFileSystem; + _checkLevel = checkLevel; _nspIcon = GetResourceBytes("Ryujinx.Ui.Common.Resources.Icon_NSP.png"); _xciIcon = GetResourceBytes("Ryujinx.Ui.Common.Resources.Icon_XCI.png"); @@ -69,6 +71,390 @@ namespace Ryujinx.Ui.App.Common return resourceByteArray; } + private ApplicationData GetApplicationFromExeFs(PartitionFileSystem pfs, string filePath) + { + ApplicationData data = new() + { + Icon = _nspIcon, + }; + + using UniqueRef npdmFile = new(); + + try + { + Result result = pfs.OpenFile(ref npdmFile.Ref, "/main.npdm".ToU8Span(), OpenMode.Read); + + if (ResultFs.PathNotFound.Includes(result)) + { + Npdm npdm = new(npdmFile.Get.AsStream()); + + data.Name = npdm.TitleName; + data.Id = npdm.Aci0.TitleId; + } + + return data; + } + catch (Exception exception) + { + Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. File: '{filePath}' Error: {exception.Message}"); + + return null; + } + } + + private ApplicationData GetApplicationFromNsp(PartitionFileSystem pfs, string filePath) + { + bool isExeFs = false; + + // If the NSP doesn't have a main NCA, decrement the number of applications found and then continue to the next application. + bool hasMainNca = false; + + foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*")) + { + if (Path.GetExtension(fileEntry.FullPath)?.ToLower() == ".nca") + { + using UniqueRef ncaFile = new(); + + pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + Nca nca = new(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage()); + int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); + + // Some main NCAs don't have a data partition, so check if the partition exists before opening it + if (nca.Header.ContentType == NcaContentType.Program && + !(nca.SectionExists(NcaSectionType.Data) && + nca.Header.GetFsHeader(dataIndex).IsPatchSection())) + { + hasMainNca = true; + + break; + } + } + else if (Path.GetFileNameWithoutExtension(fileEntry.FullPath) == "main") + { + isExeFs = true; + } + } + + if (hasMainNca) + { + List applications = GetApplicationsFromPfs(pfs, filePath); + + switch (applications.Count) + { + case 1: + return applications[0]; + case >= 1: + Logger.Warning?.Print(LogClass.Application, $"File '{filePath}' contains more applications than expected: {applications.Count}"); + return applications[0]; + default: + return null; + } + } + + if (isExeFs) + { + return GetApplicationFromExeFs(pfs, filePath); + } + + return null; + } + + private List GetApplicationsFromPfs(IFileSystem pfs, string filePath) + { + var applications = new List(); + string extension = Path.GetExtension(filePath).ToLower(); + + foreach ((ulong titleId, ContentCollection content) in pfs.GetApplicationData(_virtualFileSystem, _checkLevel)) + { + ApplicationData applicationData = new() + { + Id = titleId, + }; + + try + { + Nca mainNca = content.GetNcaByType(_virtualFileSystem.KeySet, ContentType.Program); + Nca controlNca = content.GetNcaByType(_virtualFileSystem.KeySet, ContentType.Control); + + BlitStruct controlHolder = new(1); + + IFileSystem controlFs = controlNca?.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None); + + // Check if there is an update available. + if (IsUpdateApplied(mainNca, out IFileSystem updatedControlFs)) + { + // Replace the original ControlFs by the updated one. + controlFs = updatedControlFs; + } + + ReadControlData(controlFs, controlHolder.ByteSpan); + + GetApplicationInformation(ref controlHolder.Value, ref applicationData); + + // Read the icon from the ControlFS and store it as a byte array + try + { + using UniqueRef icon = new(); + + controlFs.OpenFile(ref icon.Ref, $"/icon_{_desiredTitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + using MemoryStream stream = new(); + + icon.Get.AsStream().CopyTo(stream); + applicationData.Icon = stream.ToArray(); + } + catch (HorizonResultException) + { + foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*")) + { + if (entry.Name == "control.nacp") + { + continue; + } + + using var icon = new UniqueRef(); + + controlFs.OpenFile(ref icon.Ref, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + using MemoryStream stream = new(); + + icon.Get.AsStream().CopyTo(stream); + applicationData.Icon = stream.ToArray(); + + if (applicationData.Icon != null) + { + break; + } + } + + applicationData.Icon ??= extension == ".xci" ? _xciIcon : _nspIcon; + } + + applicationData.ControlHolder = controlHolder; + + applications.Add(applicationData); + } + catch (MissingKeyException exception) + { + applicationData.Icon = extension == ".xci" ? _xciIcon : _nspIcon; + + Logger.Warning?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}"); + } + catch (InvalidDataException) + { + applicationData.Icon = extension == ".xci" ? _xciIcon : _nspIcon; + + Logger.Warning?.Print(LogClass.Application, $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {filePath}"); + } + catch (Exception exception) + { + Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. File: '{filePath}' Error: {exception}"); + } + } + + return applications; + } + + private bool TryGetApplicationsFromFile(string applicationPath, out List applications) + { + applications = new List(); + + long fileSizeBytes = new FileInfo(applicationPath).Length; + + double fileSize = fileSizeBytes * 0.000000000931; + + BlitStruct controlHolder = new(1); + + try + { + string extension = Path.GetExtension(applicationPath).ToLower(); + + using FileStream file = new(applicationPath, FileMode.Open, FileAccess.Read); + + switch (extension) + { + case ".xci": + { + Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage()); + + applications = GetApplicationsFromPfs(xci.OpenPartition(XciPartitionType.Secure), applicationPath); + + if (applications.Count == 0) + { + return false; + } + + break; + } + case ".nsp": + case ".pfs0": + var pfs = new PartitionFileSystem(); + pfs.Initialize(file.AsStorage()).ThrowIfFailure(); + + ApplicationData result = GetApplicationFromNsp(pfs, applicationPath); + + if (result == null) + { + return false; + } + + applications.Add(result); + + break; + case ".nro": + { + BinaryReader reader = new(file); + ApplicationData application = new(); + + byte[] Read(long position, int size) + { + file.Seek(position, SeekOrigin.Begin); + + return reader.ReadBytes(size); + } + + try + { + file.Seek(24, SeekOrigin.Begin); + + int assetOffset = reader.ReadInt32(); + + if (Encoding.ASCII.GetString(Read(assetOffset, 4)) == "ASET") + { + byte[] iconSectionInfo = Read(assetOffset + 8, 0x10); + + long iconOffset = BitConverter.ToInt64(iconSectionInfo, 0); + long iconSize = BitConverter.ToInt64(iconSectionInfo, 8); + + ulong nacpOffset = reader.ReadUInt64(); + ulong nacpSize = reader.ReadUInt64(); + + // Reads and stores game icon as byte array + if (iconSize > 0) + { + application.Icon = Read(assetOffset + iconOffset, (int)iconSize); + } + else + { + application.Icon = _nroIcon; + } + + // Read the NACP data + Read(assetOffset + (int)nacpOffset, (int)nacpSize).AsSpan().CopyTo(controlHolder.ByteSpan); + + GetApplicationInformation(ref controlHolder.Value, ref application); + } + else + { + application.Icon = _nroIcon; + application.Name = Path.GetFileNameWithoutExtension(applicationPath); + } + + application.ControlHolder = controlHolder; + applications.Add(application); + } + catch + { + Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}"); + + return false; + } + + break; + } + case ".nca": + { + try + { + ApplicationData application = new(); + + Nca nca = new(_virtualFileSystem.KeySet, new FileStream(applicationPath, FileMode.Open, FileAccess.Read).AsStorage()); + + if (!nca.IsProgram() || nca.IsPatch()) + { + return false; + } + + application.Icon = _ncaIcon; + application.Name = Path.GetFileNameWithoutExtension(applicationPath); + application.ControlHolder = controlHolder; + + applications.Add(application); + } + catch (InvalidDataException) + { + Logger.Warning?.Print(LogClass.Application, $"The NCA header content type check has failed. This is usually because the header key is incorrect or missing. Errored File: {applicationPath}"); + } + catch + { + Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}"); + + return false; + } + + break; + } + // If its an NSO we just set defaults + case ".nso": + { + ApplicationData application = new() + { + Icon = _nsoIcon, + Name = Path.GetFileNameWithoutExtension(applicationPath), + }; + + applications.Add(application); + break; + } + } + } + catch (IOException exception) + { + Logger.Warning?.Print(LogClass.Application, exception.Message); + + return false; + } + + foreach (var data in applications) + { + ApplicationMetadata appMetadata = LoadAndSaveMetaData(data.IdString, appMetadata => + { + 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) + { + 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.FileExtension = Path.GetExtension(applicationPath).TrimStart('.').ToUpper(); + data.FileSize = new FileInfo(applicationPath).Length; + data.Path = applicationPath; + } + + return true; + } + public void CancelLoading() { _cancellationToken?.Cancel(); @@ -92,7 +478,7 @@ namespace Ryujinx.Ui.App.Common _cancellationToken = new CancellationTokenSource(); // Builds the applications list with paths to found applications - List applications = new(); + List applicationPaths = new(); try { @@ -136,7 +522,7 @@ namespace Ryujinx.Ui.App.Common if (!fileInfo.Attributes.HasFlag(FileAttributes.Hidden) && extension is ".nsp" or ".pfs0" or ".xci" or ".nca" or ".nro" or ".nso") { var fullPath = fileInfo.ResolveLinkTarget(true)?.FullName ?? fileInfo.FullName; - applications.Add(fullPath); + applicationPaths.Add(fullPath); numApplicationsFound++; } } @@ -148,328 +534,35 @@ namespace Ryujinx.Ui.App.Common } // Loops through applications list, creating a struct and then firing an event containing the struct for each application - foreach (string applicationPath in applications) + foreach (string applicationPath in applicationPaths) { if (_cancellationToken.Token.IsCancellationRequested) { return; } - long fileSize = new FileInfo(applicationPath).Length; - string titleName = "Unknown"; - string titleId = "0000000000000000"; - string developer = "Unknown"; - string version = "0"; - byte[] applicationIcon = null; - - BlitStruct controlHolder = new(1); - - try + if (TryGetApplicationsFromFile(applicationPath, out List applications)) { - string extension = Path.GetExtension(applicationPath).ToLower(); - - using FileStream file = new(applicationPath, FileMode.Open, FileAccess.Read); - - if (extension == ".nsp" || extension == ".pfs0" || extension == ".xci") + foreach (var application in applications) { - try + OnApplicationAdded(new ApplicationAddedEventArgs { - IFileSystem pfs; - - bool isExeFs = false; - - if (extension == ".xci") - { - Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage()); - - pfs = xci.OpenPartition(XciPartitionType.Secure); - } - else - { - var pfsTemp = new PartitionFileSystem(); - pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure(); - pfs = pfsTemp; - - // If the NSP doesn't have a main NCA, decrement the number of applications found and then continue to the next application. - bool hasMainNca = false; - - foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*")) - { - if (Path.GetExtension(fileEntry.FullPath).ToLower() == ".nca") - { - using UniqueRef ncaFile = new(); - - pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - - Nca nca = new(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage()); - int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); - - // Some main NCAs don't have a data partition, so check if the partition exists before opening it - if (nca.Header.ContentType == NcaContentType.Program && !(nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection())) - { - hasMainNca = true; - - break; - } - } - else if (Path.GetFileNameWithoutExtension(fileEntry.FullPath) == "main") - { - isExeFs = true; - } - } - - if (!hasMainNca && !isExeFs) - { - numApplicationsFound--; - - continue; - } - } - - if (isExeFs) - { - applicationIcon = _nspIcon; - - using UniqueRef npdmFile = new(); - - Result result = pfs.OpenFile(ref npdmFile.Ref, "/main.npdm".ToU8Span(), OpenMode.Read); - - if (ResultFs.PathNotFound.Includes(result)) - { - Npdm npdm = new(npdmFile.Get.AsStream()); - - titleName = npdm.TitleName; - titleId = npdm.Aci0.TitleId.ToString("x16"); - } - } - else - { - GetControlFsAndTitleId(pfs, out IFileSystem controlFs, out titleId); - - // Check if there is an update available. - if (IsUpdateApplied(titleId, out IFileSystem updatedControlFs)) - { - // Replace the original ControlFs by the updated one. - controlFs = updatedControlFs; - } - - ReadControlData(controlFs, controlHolder.ByteSpan); - - GetGameInformation(ref controlHolder.Value, out titleName, out _, out developer, out version); - - // Read the icon from the ControlFS and store it as a byte array - try - { - using UniqueRef icon = new(); - - controlFs.OpenFile(ref icon.Ref, $"/icon_{_desiredTitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure(); - - using MemoryStream stream = new(); - - icon.Get.AsStream().CopyTo(stream); - applicationIcon = stream.ToArray(); - } - catch (HorizonResultException) - { - foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*")) - { - if (entry.Name == "control.nacp") - { - continue; - } - - using var icon = new UniqueRef(); - - controlFs.OpenFile(ref icon.Ref, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - - using MemoryStream stream = new(); - - icon.Get.AsStream().CopyTo(stream); - applicationIcon = stream.ToArray(); - - if (applicationIcon != null) - { - break; - } - } - - applicationIcon ??= extension == ".xci" ? _xciIcon : _nspIcon; - } - } - } - catch (MissingKeyException exception) - { - applicationIcon = extension == ".xci" ? _xciIcon : _nspIcon; - - Logger.Warning?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}"); - } - catch (InvalidDataException) - { - applicationIcon = extension == ".xci" ? _xciIcon : _nspIcon; - - Logger.Warning?.Print(LogClass.Application, $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {applicationPath}"); - } - catch (Exception exception) - { - Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. File: '{applicationPath}' Error: {exception}"); - - numApplicationsFound--; - - continue; - } + AppData = application, + }); } - else if (extension == ".nro") + + if (applications.Count > 1) { - BinaryReader reader = new(file); - - byte[] Read(long position, int size) - { - file.Seek(position, SeekOrigin.Begin); - - return reader.ReadBytes(size); - } - - try - { - file.Seek(24, SeekOrigin.Begin); - - int assetOffset = reader.ReadInt32(); - - if (Encoding.ASCII.GetString(Read(assetOffset, 4)) == "ASET") - { - byte[] iconSectionInfo = Read(assetOffset + 8, 0x10); - - long iconOffset = BitConverter.ToInt64(iconSectionInfo, 0); - long iconSize = BitConverter.ToInt64(iconSectionInfo, 8); - - ulong nacpOffset = reader.ReadUInt64(); - ulong nacpSize = reader.ReadUInt64(); - - // Reads and stores game icon as byte array - if (iconSize > 0) - { - applicationIcon = Read(assetOffset + iconOffset, (int)iconSize); - } - else - { - applicationIcon = _nroIcon; - } - - // Read the NACP data - Read(assetOffset + (int)nacpOffset, (int)nacpSize).AsSpan().CopyTo(controlHolder.ByteSpan); - - GetGameInformation(ref controlHolder.Value, out titleName, out titleId, out developer, out version); - } - else - { - applicationIcon = _nroIcon; - titleName = Path.GetFileNameWithoutExtension(applicationPath); - } - } - catch - { - Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}"); - - numApplicationsFound--; - - continue; - } + numApplicationsFound += applications.Count - 1; } - else if (extension == ".nca") - { - try - { - Nca nca = new(_virtualFileSystem.KeySet, new FileStream(applicationPath, FileMode.Open, FileAccess.Read).AsStorage()); - int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); - if (nca.Header.ContentType != NcaContentType.Program || (nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection())) - { - numApplicationsFound--; - - continue; - } - } - catch (InvalidDataException) - { - Logger.Warning?.Print(LogClass.Application, $"The NCA header content type check has failed. This is usually because the header key is incorrect or missing. Errored File: {applicationPath}"); - } - catch - { - Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}"); - - numApplicationsFound--; - - continue; - } - - applicationIcon = _ncaIcon; - titleName = Path.GetFileNameWithoutExtension(applicationPath); - } - // If its an NSO we just set defaults - else if (extension == ".nso") - { - applicationIcon = _nsoIcon; - titleName = Path.GetFileNameWithoutExtension(applicationPath); - } + numApplicationsLoaded += applications.Count; } - catch (IOException exception) + else { - Logger.Warning?.Print(LogClass.Application, exception.Message); - numApplicationsFound--; - - continue; } - ApplicationMetadata appMetadata = LoadAndSaveMetaData(titleId, appMetadata => - { - appMetadata.Title = titleName; - - // 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.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; - } - - } - }); - - ApplicationData data = new() - { - Favorite = appMetadata.Favorite, - Icon = applicationIcon, - TitleName = titleName, - TitleId = titleId, - Developer = developer, - Version = version, - TimePlayed = appMetadata.TimePlayed, - LastPlayed = appMetadata.LastPlayed, - FileExtension = Path.GetExtension(applicationPath).TrimStart('.').ToUpper(), - FileSize = fileSize, - Path = applicationPath, - ControlHolder = controlHolder, - }; - - numApplicationsLoaded++; - - OnApplicationAdded(new ApplicationAddedEventArgs - { - AppData = data, - }); - OnApplicationCountUpdated(new ApplicationCountUpdatedEventArgs { NumAppsFound = numApplicationsFound, @@ -500,15 +593,6 @@ namespace Ryujinx.Ui.App.Common ApplicationCountUpdated?.Invoke(null, e); } - private void GetControlFsAndTitleId(IFileSystem pfs, out IFileSystem controlFs, out string titleId) - { - (_, _, Nca controlNca) = GetGameData(_virtualFileSystem, pfs, 0); - - // Return the ControlFS - controlFs = controlNca?.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None); - titleId = controlNca?.Header.TitleId.ToString("x16"); - } - public static ApplicationMetadata LoadAndSaveMetaData(string titleId, Action modifyFunction = null) { string metadataFolder = Path.Combine(AppDataManager.GamesDirPath, titleId, "gui"); @@ -546,10 +630,29 @@ namespace Ryujinx.Ui.App.Common return appMetadata; } - public byte[] GetApplicationIcon(string applicationPath, Language desiredTitleLanguage) + public byte[] GetApplicationIcon(string applicationPath, Language desiredTitleLanguage, ulong titleId) { byte[] applicationIcon = null; + if (titleId == 0) + { + if (Directory.Exists(applicationPath)) + { + return _ncaIcon; + } + + return Path.GetExtension(applicationPath).ToLower() switch + { + ".nsp" => _nspIcon, + ".pfs0" => _nspIcon, + ".xci" => _xciIcon, + ".nso" => _nsoIcon, + ".nro" => _nroIcon, + ".nca" => _ncaIcon, + _ => _ncaIcon, + }; + } + try { // Look for icon only if applicationPath is not a directory @@ -595,7 +698,16 @@ namespace Ryujinx.Ui.App.Common else { // Store the ControlFS in variable called controlFs - GetControlFsAndTitleId(pfs, out IFileSystem controlFs, out _); + Dictionary programs = pfs.GetApplicationData(_virtualFileSystem, _checkLevel); + IFileSystem controlFs = null; + + if (programs.ContainsKey(titleId)) + { + if (programs[titleId].GetNcaByType(_virtualFileSystem.KeySet, ContentType.Control) is { } controlNca) + { + controlFs = controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None); + } + } // Read the icon from the ControlFS and store it as a byte array try @@ -622,16 +734,11 @@ namespace Ryujinx.Ui.App.Common controlFs.OpenFile(ref icon.Ref, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - using (MemoryStream stream = new()) - { - icon.Get.AsStream().CopyTo(stream); - applicationIcon = stream.ToArray(); - } + using MemoryStream stream = new(); + icon.Get.AsStream().CopyTo(stream); + applicationIcon = stream.ToArray(); - if (applicationIcon != null) - { - break; - } + break; } applicationIcon ??= extension == ".xci" ? _xciIcon : _nspIcon; @@ -714,41 +821,41 @@ namespace Ryujinx.Ui.App.Common return applicationIcon ?? _ncaIcon; } - private void GetGameInformation(ref ApplicationControlProperty controlData, out string titleName, out string titleId, out string publisher, out string version) + private void GetApplicationInformation(ref ApplicationControlProperty controlData, ref ApplicationData data) { _ = Enum.TryParse(_desiredTitleLanguage.ToString(), out TitleLanguage desiredTitleLanguage); if (controlData.Title.ItemsRo.Length > (int)desiredTitleLanguage) { - titleName = controlData.Title[(int)desiredTitleLanguage].NameString.ToString(); - publisher = controlData.Title[(int)desiredTitleLanguage].PublisherString.ToString(); + data.Name = controlData.Title[(int)desiredTitleLanguage].NameString.ToString(); + data.Developer = controlData.Title[(int)desiredTitleLanguage].PublisherString.ToString(); } else { - titleName = null; - publisher = null; + data.Name = null; + data.Developer = null; } - if (string.IsNullOrWhiteSpace(titleName)) + if (string.IsNullOrWhiteSpace(data.Name)) { foreach (ref readonly var controlTitle in controlData.Title.ItemsRo) { if (!controlTitle.NameString.IsEmpty()) { - titleName = controlTitle.NameString.ToString(); + data.Name = controlTitle.NameString.ToString(); break; } } } - if (string.IsNullOrWhiteSpace(publisher)) + if (string.IsNullOrWhiteSpace(data.Developer)) { foreach (ref readonly var controlTitle in controlData.Title.ItemsRo) { if (!controlTitle.PublisherString.IsEmpty()) { - publisher = controlTitle.PublisherString.ToString(); + data.Developer = controlTitle.PublisherString.ToString(); break; } @@ -757,25 +864,21 @@ namespace Ryujinx.Ui.App.Common if (controlData.PresenceGroupId != 0) { - titleId = controlData.PresenceGroupId.ToString("x16"); + data.Id = controlData.PresenceGroupId; } else if (controlData.SaveDataOwnerId != 0) { - titleId = controlData.SaveDataOwnerId.ToString(); + data.Id = controlData.SaveDataOwnerId; } else if (controlData.AddOnContentBaseId != 0) { - titleId = (controlData.AddOnContentBaseId - 0x1000).ToString("x16"); - } - else - { - titleId = "0000000000000000"; + data.Id = (controlData.AddOnContentBaseId - 0x1000); } - version = controlData.DisplayVersionString.ToString(); + data.Version = controlData.DisplayVersionString.ToString(); } - private bool IsUpdateApplied(string titleId, out IFileSystem updatedControlFs) + private bool IsUpdateApplied(Nca mainNca, out IFileSystem updatedControlFs) { updatedControlFs = null; @@ -783,11 +886,11 @@ namespace Ryujinx.Ui.App.Common try { - (Nca patchNca, Nca controlNca) = GetGameUpdateData(_virtualFileSystem, titleId, 0, out updatePath); + (Nca patchNca, Nca controlNca) = mainNca.GetUpdateData(_virtualFileSystem, _checkLevel, 0, out updatePath); if (patchNca != null && controlNca != null) { - updatedControlFs = controlNca?.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None); + updatedControlFs = controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None); return true; } @@ -803,120 +906,5 @@ namespace Ryujinx.Ui.App.Common return false; } - - public static (Nca main, Nca patch, Nca control) GetGameData(VirtualFileSystem fileSystem, IFileSystem pfs, int programIndex) - { - Nca mainNca = null; - Nca patchNca = null; - Nca controlNca = null; - - fileSystem.ImportTickets(pfs); - - foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) - { - using var ncaFile = new UniqueRef(); - - pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - - Nca nca = new(fileSystem.KeySet, ncaFile.Release().AsStorage()); - - int ncaProgramIndex = (int)(nca.Header.TitleId & 0xF); - - if (ncaProgramIndex != programIndex) - { - continue; - } - - if (nca.Header.ContentType == NcaContentType.Program) - { - int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); - - if (nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection()) - { - patchNca = nca; - } - else - { - mainNca = nca; - } - } - else if (nca.Header.ContentType == NcaContentType.Control) - { - controlNca = nca; - } - } - - return (mainNca, patchNca, controlNca); - } - - public static (Nca patch, Nca control) GetGameUpdateDataFromPartition(VirtualFileSystem fileSystem, PartitionFileSystem pfs, string titleId, int programIndex) - { - Nca patchNca = null; - Nca controlNca = null; - - fileSystem.ImportTickets(pfs); - - foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) - { - using var ncaFile = new UniqueRef(); - - pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - - Nca nca = new(fileSystem.KeySet, ncaFile.Release().AsStorage()); - - int ncaProgramIndex = (int)(nca.Header.TitleId & 0xF); - - if (ncaProgramIndex != programIndex) - { - continue; - } - - if ($"{nca.Header.TitleId.ToString("x16")[..^3]}000" != titleId) - { - break; - } - - if (nca.Header.ContentType == NcaContentType.Program) - { - patchNca = nca; - } - else if (nca.Header.ContentType == NcaContentType.Control) - { - controlNca = nca; - } - } - - return (patchNca, controlNca); - } - - public static (Nca patch, Nca control) GetGameUpdateData(VirtualFileSystem fileSystem, string titleId, int programIndex, out string updatePath) - { - updatePath = null; - - if (ulong.TryParse(titleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdBase)) - { - // Clear the program index part. - titleIdBase &= ~0xFUL; - - // Load update information if exists. - string titleUpdateMetadataPath = Path.Combine(AppDataManager.GamesDirPath, titleIdBase.ToString("x16"), "updates.json"); - - if (File.Exists(titleUpdateMetadataPath)) - { - updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected; - - if (File.Exists(updatePath)) - { - FileStream file = new(updatePath, FileMode.Open, FileAccess.Read); - PartitionFileSystem nsp = new(); - nsp.Initialize(file.AsStorage()).ThrowIfFailure(); - - return GetGameUpdateDataFromPartition(fileSystem, nsp, titleIdBase.ToString("x16"), programIndex); - } - } - } - - return (null, null); - } } } diff --git a/src/Ryujinx/Program.cs b/src/Ryujinx/Program.cs index afb6a9925..14062481a 100644 --- a/src/Ryujinx/Program.cs +++ b/src/Ryujinx/Program.cs @@ -7,6 +7,7 @@ using Ryujinx.Common.SystemInterop; using Ryujinx.Modules; using Ryujinx.SDL2.Common; using Ryujinx.Ui; +using Ryujinx.Ui.App.Common; using Ryujinx.Ui.Common; using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Common.Helper; @@ -332,7 +333,12 @@ namespace Ryujinx if (CommandLineState.LaunchPathArg != null) { - mainWindow.RunApplication(CommandLineState.LaunchPathArg, CommandLineState.StartFullscreenArg); + ApplicationData applicationData = new() + { + Path = CommandLineState.LaunchPathArg, + }; + + mainWindow.RunApplication(applicationData, CommandLineState.StartFullscreenArg); } if (ConfigurationState.Instance.CheckUpdatesOnStart.Value && Updater.CanUpdate(false)) diff --git a/src/Ryujinx/Ui/MainWindow.cs b/src/Ryujinx/Ui/MainWindow.cs index 8b0b35e6c..884f6687e 100644 --- a/src/Ryujinx/Ui/MainWindow.cs +++ b/src/Ryujinx/Ui/MainWindow.cs @@ -39,6 +39,7 @@ using Silk.NET.Vulkan; using SPB.Graphics.Vulkan; using System; using System.Diagnostics; +using System.Globalization; using System.IO; using System.Reflection; using System.Threading; @@ -70,7 +71,7 @@ namespace Ryujinx.Ui private bool _gameLoaded; private bool _ending; - private string _currentEmulatedGamePath = null; + private ApplicationData _currentApplicationData = null; private string _lastScannedAmiiboId = ""; private bool _lastScannedAmiiboShowAll = false; @@ -181,8 +182,12 @@ namespace Ryujinx.Ui _accountManager = new AccountManager(_libHacHorizonManager.RyujinxClient, CommandLineState.Profile); _userChannelPersistence = new UserChannelPersistence(); + IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks + ? IntegrityCheckLevel.ErrorOnInvalid + : IntegrityCheckLevel.None; + // Instantiate GUI objects. - _applicationLibrary = new ApplicationLibrary(_virtualFileSystem); + _applicationLibrary = new ApplicationLibrary(_virtualFileSystem, checkLevel); _uiHandler = new GtkHostUiHandler(this); _deviceExitStatus = new AutoResetEvent(false); @@ -784,7 +789,7 @@ namespace Ryujinx.Ui } } - private bool LoadApplication(string path, bool isFirmwareTitle) + private bool LoadApplication(string path, ulong titleId, bool isFirmwareTitle) { SystemVersion firmwareVersion = _contentManager.GetCurrentFirmwareVersion(); @@ -858,7 +863,7 @@ namespace Ryujinx.Ui case ".xci": Logger.Info?.Print(LogClass.Application, "Loading as XCI."); - return _emulationContext.LoadXci(path); + return _emulationContext.LoadXci(path, titleId); case ".nca": Logger.Info?.Print(LogClass.Application, "Loading as NCA."); @@ -867,7 +872,7 @@ namespace Ryujinx.Ui case ".pfs0": Logger.Info?.Print(LogClass.Application, "Loading as NSP."); - return _emulationContext.LoadNsp(path); + return _emulationContext.LoadNsp(path, titleId); default: Logger.Info?.Print(LogClass.Application, "Loading as Homebrew."); try @@ -888,7 +893,7 @@ namespace Ryujinx.Ui return false; } - public void RunApplication(string path, bool startFullscreen = false) + public void RunApplication(ApplicationData application, bool startFullscreen = false) { if (_gameLoaded) { @@ -910,14 +915,14 @@ namespace Ryujinx.Ui bool isFirmwareTitle = false; - if (path.StartsWith("@SystemContent")) + if (application.Path.StartsWith("@SystemContent")) { - path = VirtualFileSystem.SwitchPathToSystemPath(path); + application.Path = VirtualFileSystem.SwitchPathToSystemPath(application.Path); isFirmwareTitle = true; } - if (!LoadApplication(path, isFirmwareTitle)) + if (!LoadApplication(application.Path, application.Id, isFirmwareTitle)) { _emulationContext.Dispose(); SwitchToGameTable(); @@ -927,7 +932,7 @@ namespace Ryujinx.Ui SetupProgressUiHandlers(); - _currentEmulatedGamePath = path; + _currentApplicationData = application; _deviceExitStatus.Reset(); @@ -1168,7 +1173,7 @@ namespace Ryujinx.Ui _tableStore.AppendValues( args.AppData.Favorite, new Gdk.Pixbuf(args.AppData.Icon, 75, 75), - $"{args.AppData.TitleName}\n{args.AppData.TitleId.ToUpper()}", + $"{args.AppData.Name}\n{args.AppData.IdString.ToUpper()}", args.AppData.Developer, args.AppData.Version, args.AppData.TimePlayedString, @@ -1256,9 +1261,22 @@ namespace Ryujinx.Ui { _gameTableSelection.GetSelected(out TreeIter treeIter); - string path = (string)_tableStore.GetValue(treeIter, 9); + ApplicationData application = new() + { + Favorite = (bool)_tableStore.GetValue(treeIter, 0), + Name = ((string)_tableStore.GetValue(treeIter, 2)).Split('\n')[0], + Id = ulong.Parse(((string)_tableStore.GetValue(treeIter, 2)).Split('\n')[1], NumberStyles.HexNumber), + Developer = (string)_tableStore.GetValue(treeIter, 3), + Version = (string)_tableStore.GetValue(treeIter, 4), + TimePlayed = ValueFormatUtils.ParseTimeSpan((string)_tableStore.GetValue(treeIter, 5)), + LastPlayed = ValueFormatUtils.ParseDateTime((string)_tableStore.GetValue(treeIter, 6)), + FileExtension = (string)_tableStore.GetValue(treeIter, 7), + FileSize = ValueFormatUtils.ParseFileSize((string)_tableStore.GetValue(treeIter, 8)), + Path = (string)_tableStore.GetValue(treeIter, 9), + ControlHolder = (BlitStruct)_tableStore.GetValue(treeIter, 10), + }; - RunApplication(path); + RunApplication(application); } private void VSyncStatus_Clicked(object sender, ButtonReleaseEventArgs args) @@ -1316,13 +1334,22 @@ namespace Ryujinx.Ui return; } - string titleFilePath = _tableStore.GetValue(treeIter, 9).ToString(); - string titleName = _tableStore.GetValue(treeIter, 2).ToString().Split("\n")[0]; - string titleId = _tableStore.GetValue(treeIter, 2).ToString().Split("\n")[1].ToLower(); + ApplicationData application = new() + { + Favorite = (bool)_tableStore.GetValue(treeIter, 0), + Name = ((string)_tableStore.GetValue(treeIter, 2)).Split('\n')[0], + Id = ulong.Parse(((string)_tableStore.GetValue(treeIter, 2)).Split('\n')[1], NumberStyles.HexNumber), + Developer = (string)_tableStore.GetValue(treeIter, 3), + Version = (string)_tableStore.GetValue(treeIter, 4), + TimePlayed = ValueFormatUtils.ParseTimeSpan((string)_tableStore.GetValue(treeIter, 5)), + LastPlayed = ValueFormatUtils.ParseDateTime((string)_tableStore.GetValue(treeIter, 6)), + FileExtension = (string)_tableStore.GetValue(treeIter, 7), + FileSize = ValueFormatUtils.ParseFileSize((string)_tableStore.GetValue(treeIter, 8)), + Path = (string)_tableStore.GetValue(treeIter, 9), + ControlHolder = (BlitStruct)_tableStore.GetValue(treeIter, 10), + }; - BlitStruct controlData = (BlitStruct)_tableStore.GetValue(treeIter, 10); - - _ = new GameTableContextMenu(this, _virtualFileSystem, _accountManager, _libHacHorizonManager.RyujinxClient, titleFilePath, titleName, titleId, controlData); + _ = new GameTableContextMenu(this, _virtualFileSystem, _accountManager, _libHacHorizonManager.RyujinxClient, application); } private void Load_Application_File(object sender, EventArgs args) @@ -1344,7 +1371,12 @@ namespace Ryujinx.Ui if (fileChooser.Run() == (int)ResponseType.Accept) { - RunApplication(fileChooser.Filename); + ApplicationData applicationData = new() + { + Path = fileChooser.Filename, + }; + + RunApplication(applicationData); } } @@ -1354,7 +1386,13 @@ namespace Ryujinx.Ui if (fileChooser.Run() == (int)ResponseType.Accept) { - RunApplication(fileChooser.Filename); + ApplicationData applicationData = new() + { + Name = System.IO.Path.GetFileNameWithoutExtension(fileChooser.Filename), + Path = fileChooser.Filename, + }; + + RunApplication(applicationData); } } @@ -1369,7 +1407,14 @@ namespace Ryujinx.Ui { string contentPath = _contentManager.GetInstalledContentPath(0x0100000000001009, StorageId.BuiltInSystem, NcaContentType.Program); - RunApplication(contentPath); + ApplicationData applicationData = new() + { + Name = "miiEdit", + Id = 0x0100000000001009ul, + Path = contentPath, + }; + + RunApplication(applicationData); } private void Open_Ryu_Folder(object sender, EventArgs args) @@ -1645,13 +1690,13 @@ namespace Ryujinx.Ui { _userChannelPersistence.ShouldRestart = false; - RunApplication(_currentEmulatedGamePath); + RunApplication(_currentApplicationData); } else { // otherwise, clear state. _userChannelPersistence = new UserChannelPersistence(); - _currentEmulatedGamePath = null; + _currentApplicationData = null; _actionMenu.Sensitive = false; _firmwareInstallFile.Sensitive = true; _firmwareInstallDirectory.Sensitive = true; @@ -1713,7 +1758,7 @@ namespace Ryujinx.Ui _emulationContext.Processes.ActiveApplication.ProgramId, _emulationContext.Processes.ActiveApplication.ApplicationControlProperties .Title[(int)_emulationContext.System.State.DesiredTitleLanguage].NameString.ToString(), - _currentEmulatedGamePath); + _currentApplicationData.Path); window.Destroyed += CheatWindow_Destroyed; window.Show(); diff --git a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs index 5af181b08..6903c9419 100644 --- a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs +++ b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs @@ -16,6 +16,7 @@ using Ryujinx.Common.Logging; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS; using Ryujinx.HLE.HOS.Services.Account.Acc; +using Ryujinx.HLE.Loaders.Processes.Extensions; using Ryujinx.Ui.App.Common; using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Common.Helper; @@ -23,7 +24,6 @@ using Ryujinx.Ui.Windows; using System; using System.Buffers; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Reflection; using System.Threading; @@ -36,17 +36,13 @@ namespace Ryujinx.Ui.Widgets private readonly VirtualFileSystem _virtualFileSystem; private readonly AccountManager _accountManager; private readonly HorizonClient _horizonClient; - private readonly BlitStruct _controlData; - private readonly string _titleFilePath; - private readonly string _titleName; - private readonly string _titleIdText; - private readonly ulong _titleId; + private readonly ApplicationData _title; private MessageDialog _dialog; private bool _cancel; - public GameTableContextMenu(MainWindow parent, VirtualFileSystem virtualFileSystem, AccountManager accountManager, HorizonClient horizonClient, string titleFilePath, string titleName, string titleId, BlitStruct controlData) + public GameTableContextMenu(MainWindow parent, VirtualFileSystem virtualFileSystem, AccountManager accountManager, HorizonClient horizonClient, ApplicationData applicationData) { _parent = parent; @@ -55,23 +51,13 @@ namespace Ryujinx.Ui.Widgets _virtualFileSystem = virtualFileSystem; _accountManager = accountManager; _horizonClient = horizonClient; - _titleFilePath = titleFilePath; - _titleName = titleName; - _titleIdText = titleId; - _controlData = controlData; + _title = applicationData; - if (!ulong.TryParse(_titleIdText, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _titleId)) - { - GtkDialog.CreateErrorDialog("The selected game did not have a valid Title Id"); + _openSaveUserDirMenuItem.Sensitive = !Utilities.IsZeros(_title.ControlHolder.ByteSpan) && _title.ControlHolder.Value.UserAccountSaveDataSize > 0; + _openSaveDeviceDirMenuItem.Sensitive = !Utilities.IsZeros(_title.ControlHolder.ByteSpan) && _title.ControlHolder.Value.DeviceSaveDataSize > 0; + _openSaveBcatDirMenuItem.Sensitive = !Utilities.IsZeros(_title.ControlHolder.ByteSpan) && _title.ControlHolder.Value.BcatDeliveryCacheStorageSize > 0; - return; - } - - _openSaveUserDirMenuItem.Sensitive = !Utilities.IsZeros(controlData.ByteSpan) && controlData.Value.UserAccountSaveDataSize > 0; - _openSaveDeviceDirMenuItem.Sensitive = !Utilities.IsZeros(controlData.ByteSpan) && controlData.Value.DeviceSaveDataSize > 0; - _openSaveBcatDirMenuItem.Sensitive = !Utilities.IsZeros(controlData.ByteSpan) && controlData.Value.BcatDeliveryCacheStorageSize > 0; - - string fileExt = System.IO.Path.GetExtension(_titleFilePath).ToLower(); + string fileExt = System.IO.Path.GetExtension(_title.Path).ToLower(); bool hasNca = fileExt == ".nca" || fileExt == ".nsp" || fileExt == ".pfs0" || fileExt == ".xci"; _extractRomFsMenuItem.Sensitive = hasNca; @@ -137,7 +123,7 @@ namespace Ryujinx.Ui.Widgets private void OpenSaveDir(in SaveDataFilter saveDataFilter) { - if (!TryFindSaveData(_titleName, _titleId, _controlData, in saveDataFilter, out ulong saveDataId)) + if (!TryFindSaveData(_title.Name, _title.Id, _title.ControlHolder, in saveDataFilter, out ulong saveDataId)) { return; } @@ -190,7 +176,7 @@ namespace Ryujinx.Ui.Widgets { Title = "Ryujinx - NCA Section Extractor", Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png"), - SecondaryText = $"Extracting {ncaSectionType} section from {System.IO.Path.GetFileName(_titleFilePath)}...", + SecondaryText = $"Extracting {ncaSectionType} section from {System.IO.Path.GetFileName(_title.Path)}...", WindowPosition = WindowPosition.Center, }; @@ -202,18 +188,18 @@ namespace Ryujinx.Ui.Widgets } }); - using FileStream file = new(_titleFilePath, FileMode.Open, FileAccess.Read); + using FileStream file = new(_title.Path, FileMode.Open, FileAccess.Read); Nca mainNca = null; Nca patchNca = null; - if ((System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".nsp") || - (System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".pfs0") || - (System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".xci")) + if ((System.IO.Path.GetExtension(_title.Path).ToLower() == ".nsp") || + (System.IO.Path.GetExtension(_title.Path).ToLower() == ".pfs0") || + (System.IO.Path.GetExtension(_title.Path).ToLower() == ".xci")) { IFileSystem pfs; - if (System.IO.Path.GetExtension(_titleFilePath) == ".xci") + if (System.IO.Path.GetExtension(_title.Path).ToLower() == ".xci") { Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage()); @@ -249,7 +235,7 @@ namespace Ryujinx.Ui.Widgets } } } - else if (System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".nca") + else if (System.IO.Path.GetExtension(_title.Path).ToLower() == ".nca") { mainNca = new Nca(_virtualFileSystem.KeySet, file.AsStorage()); } @@ -266,7 +252,11 @@ namespace Ryujinx.Ui.Widgets return; } - (Nca updatePatchNca, _) = ApplicationLibrary.GetGameUpdateData(_virtualFileSystem, mainNca.Header.TitleId.ToString("x16"), programIndex, out _); + IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks + ? IntegrityCheckLevel.ErrorOnInvalid + : IntegrityCheckLevel.None; + + (Nca updatePatchNca, _) = mainNca.GetUpdateData(_virtualFileSystem, checkLevel, programIndex, out _); if (updatePatchNca != null) { @@ -460,44 +450,44 @@ namespace Ryujinx.Ui.Widgets private void OpenSaveUserDir_Clicked(object sender, EventArgs args) { var userId = new LibHac.Fs.UserId((ulong)_accountManager.LastOpenedUser.UserId.High, (ulong)_accountManager.LastOpenedUser.UserId.Low); - var saveDataFilter = SaveDataFilter.Make(_titleId, saveType: default, userId, saveDataId: default, index: default); + var saveDataFilter = SaveDataFilter.Make(_title.Id, saveType: default, userId, saveDataId: default, index: default); OpenSaveDir(in saveDataFilter); } private void OpenSaveDeviceDir_Clicked(object sender, EventArgs args) { - var saveDataFilter = SaveDataFilter.Make(_titleId, SaveDataType.Device, userId: default, saveDataId: default, index: default); + var saveDataFilter = SaveDataFilter.Make(_title.Id, SaveDataType.Device, userId: default, saveDataId: default, index: default); OpenSaveDir(in saveDataFilter); } private void OpenSaveBcatDir_Clicked(object sender, EventArgs args) { - var saveDataFilter = SaveDataFilter.Make(_titleId, SaveDataType.Bcat, userId: default, saveDataId: default, index: default); + var saveDataFilter = SaveDataFilter.Make(_title.Id, SaveDataType.Bcat, userId: default, saveDataId: default, index: default); OpenSaveDir(in saveDataFilter); } private void ManageTitleUpdates_Clicked(object sender, EventArgs args) { - new TitleUpdateWindow(_parent, _virtualFileSystem, _titleIdText, _titleName).Show(); + new TitleUpdateWindow(_parent, _virtualFileSystem, _title).Show(); } private void ManageDlc_Clicked(object sender, EventArgs args) { - new DlcWindow(_virtualFileSystem, _titleIdText, _titleName).Show(); + new DlcWindow(_virtualFileSystem, _title.IdString, _title).Show(); } private void ManageCheats_Clicked(object sender, EventArgs args) { - new CheatWindow(_virtualFileSystem, _titleId, _titleName, _titleFilePath).Show(); + new CheatWindow(_virtualFileSystem, _title.Id, _title.Name, _title.Path).Show(); } private void OpenTitleModDir_Clicked(object sender, EventArgs args) { string modsBasePath = ModLoader.GetModsBasePath(); - string titleModsPath = ModLoader.GetTitleDir(modsBasePath, _titleIdText); + string titleModsPath = ModLoader.GetTitleDir(modsBasePath, _title.IdString); OpenHelper.OpenFolder(titleModsPath); } @@ -505,7 +495,7 @@ namespace Ryujinx.Ui.Widgets private void OpenTitleSdModDir_Clicked(object sender, EventArgs args) { string sdModsBasePath = ModLoader.GetSdModsBasePath(); - string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, _titleIdText); + string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, _title.IdString); OpenHelper.OpenFolder(titleModsPath); } @@ -527,7 +517,7 @@ namespace Ryujinx.Ui.Widgets private void OpenPtcDir_Clicked(object sender, EventArgs args) { - string ptcDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "cpu"); + string ptcDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _title.IdString, "cache", "cpu"); string mainPath = System.IO.Path.Combine(ptcDir, "0"); string backupPath = System.IO.Path.Combine(ptcDir, "1"); @@ -544,7 +534,7 @@ namespace Ryujinx.Ui.Widgets private void OpenShaderCacheDir_Clicked(object sender, EventArgs args) { - string shaderCacheDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "shader"); + string shaderCacheDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _title.IdString, "cache", "shader"); if (!Directory.Exists(shaderCacheDir)) { @@ -556,10 +546,10 @@ namespace Ryujinx.Ui.Widgets private void PurgePtcCache_Clicked(object sender, EventArgs args) { - DirectoryInfo mainDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "cpu", "0")); - DirectoryInfo backupDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "cpu", "1")); + DirectoryInfo mainDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _title.IdString, "cache", "cpu", "0")); + DirectoryInfo backupDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _title.IdString, "cache", "cpu", "1")); - MessageDialog warningDialog = GtkDialog.CreateConfirmationDialog("Warning", $"You are about to queue a PPTC rebuild on the next boot of:\n\n{_titleName}\n\nAre you sure you want to proceed?"); + MessageDialog warningDialog = GtkDialog.CreateConfirmationDialog("Warning", $"You are about to queue a PPTC rebuild on the next boot of:\n\n{_title.Name}\n\nAre you sure you want to proceed?"); List cacheFiles = new(); @@ -593,9 +583,9 @@ namespace Ryujinx.Ui.Widgets private void PurgeShaderCache_Clicked(object sender, EventArgs args) { - DirectoryInfo shaderCacheDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "shader")); + DirectoryInfo shaderCacheDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _title.IdString, "cache", "shader")); - using MessageDialog warningDialog = GtkDialog.CreateConfirmationDialog("Warning", $"You are about to delete the shader cache for :\n\n{_titleName}\n\nAre you sure you want to proceed?"); + using MessageDialog warningDialog = GtkDialog.CreateConfirmationDialog("Warning", $"You are about to delete the shader cache for :\n\n{_title.Name}\n\nAre you sure you want to proceed?"); List oldCacheDirectories = new(); List newCacheFiles = new(); @@ -637,8 +627,11 @@ namespace Ryujinx.Ui.Widgets private void CreateShortcut_Clicked(object sender, EventArgs args) { - byte[] appIcon = new ApplicationLibrary(_virtualFileSystem).GetApplicationIcon(_titleFilePath, ConfigurationState.Instance.System.Language); - ShortcutHelper.CreateAppShortcut(_titleFilePath, _titleName, _titleIdText, appIcon); + IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks + ? IntegrityCheckLevel.ErrorOnInvalid + : IntegrityCheckLevel.None; + byte[] appIcon = new ApplicationLibrary(_virtualFileSystem, checkLevel).GetApplicationIcon(_title.Path, ConfigurationState.Instance.System.Language, _title.Id); + ShortcutHelper.CreateAppShortcut(_title.Path, _title.Name, _title.IdString, appIcon); } } } diff --git a/src/Ryujinx/Ui/Windows/CheatWindow.cs b/src/Ryujinx/Ui/Windows/CheatWindow.cs index 1eca732b2..9bbae1c6c 100644 --- a/src/Ryujinx/Ui/Windows/CheatWindow.cs +++ b/src/Ryujinx/Ui/Windows/CheatWindow.cs @@ -1,7 +1,9 @@ using Gtk; +using LibHac.Tools.FsSystem; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS; using Ryujinx.Ui.App.Common; +using Ryujinx.Ui.Common.Configuration; using System; using System.Collections.Generic; using System.IO; @@ -27,8 +29,13 @@ namespace Ryujinx.Ui.Windows private CheatWindow(Builder builder, VirtualFileSystem virtualFileSystem, ulong titleId, string titleName, string titlePath) : base(builder.GetRawOwnedObject("_cheatWindow")) { builder.Autoconnect(this); + + IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks + ? IntegrityCheckLevel.ErrorOnInvalid + : IntegrityCheckLevel.None; + _baseTitleInfoLabel.Text = $"Cheats Available for {titleName} [{titleId:X16}]"; - _buildIdTextView.Buffer.Text = $"BuildId: {ApplicationData.GetApplicationBuildId(virtualFileSystem, titlePath)}"; + _buildIdTextView.Buffer.Text = $"BuildId: {ApplicationData.GetBuildId(virtualFileSystem, checkLevel, titlePath)}"; string modsBasePath = ModLoader.GetModsBasePath(); string titleModsPath = ModLoader.GetTitleDir(modsBasePath, titleId.ToString("X16")); diff --git a/src/Ryujinx/Ui/Windows/DlcWindow.cs b/src/Ryujinx/Ui/Windows/DlcWindow.cs index 9f7179467..dbffc4209 100644 --- a/src/Ryujinx/Ui/Windows/DlcWindow.cs +++ b/src/Ryujinx/Ui/Windows/DlcWindow.cs @@ -9,9 +9,12 @@ using LibHac.Tools.FsSystem.NcaUtils; using Ryujinx.Common.Configuration; using Ryujinx.Common.Utilities; using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.Loaders.Processes.Extensions; +using Ryujinx.Ui.App.Common; using Ryujinx.Ui.Widgets; using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using GUI = Gtk.Builder.ObjectAttribute; @@ -20,7 +23,7 @@ namespace Ryujinx.Ui.Windows public class DlcWindow : Window { private readonly VirtualFileSystem _virtualFileSystem; - private readonly string _titleId; + private readonly string _applicationId; private readonly string _dlcJsonPath; private readonly List _dlcContainerList; @@ -32,16 +35,16 @@ namespace Ryujinx.Ui.Windows [GUI] TreeSelection _dlcTreeSelection; #pragma warning restore CS0649, IDE0044 - public DlcWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName) : this(new Builder("Ryujinx.Ui.Windows.DlcWindow.glade"), virtualFileSystem, titleId, titleName) { } + public DlcWindow(VirtualFileSystem virtualFileSystem, string titleId, ApplicationData title) : this(new Builder("Ryujinx.Ui.Windows.DlcWindow.glade"), virtualFileSystem, titleId, title) { } - private DlcWindow(Builder builder, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : base(builder.GetRawOwnedObject("_dlcWindow")) + private DlcWindow(Builder builder, VirtualFileSystem virtualFileSystem, string applicationId, ApplicationData title) : base(builder.GetRawOwnedObject("_dlcWindow")) { builder.Autoconnect(this); - _titleId = titleId; + _applicationId = applicationId; _virtualFileSystem = virtualFileSystem; - _dlcJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleId, "dlc.json"); - _baseTitleInfoLabel.Text = $"DLC Available for {titleName} [{titleId.ToUpper()}]"; + _dlcJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, _applicationId, "dlc.json"); + _baseTitleInfoLabel.Text = $"DLC Available for {title.Name} [{applicationId.ToUpper()}]"; try { @@ -72,9 +75,12 @@ namespace Ryujinx.Ui.Windows }; _dlcTreeView.AppendColumn("Enabled", enableToggle, "active", 0); - _dlcTreeView.AppendColumn("TitleId", new CellRendererText(), "text", 1); + _dlcTreeView.AppendColumn("ApplicationId", new CellRendererText(), "text", 1); _dlcTreeView.AppendColumn("Path", new CellRendererText(), "text", 2); + // NOTE: Try to load downloadable contents from PFS first. + AddDlc(title.Path, true); + foreach (DownloadableContentContainer dlcContainer in _dlcContainerList) { if (File.Exists(dlcContainer.ContainerPath)) @@ -89,7 +95,10 @@ namespace Ryujinx.Ui.Windows using FileStream containerFile = File.OpenRead(dlcContainer.ContainerPath); PartitionFileSystem pfs = new(); - pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure(); + if (pfs.Initialize(containerFile.AsStorage()).IsFailure()) + { + continue; + } _virtualFileSystem.ImportTickets(pfs); @@ -128,6 +137,57 @@ namespace Ryujinx.Ui.Windows return null; } + private void AddDlc(string path, bool ignoreNotFound = false) + { + if (!File.Exists(path)) + { + return; + } + + using FileStream containerFile = File.OpenRead(path); + + PartitionFileSystem pfs = new(); + pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure(); + + bool containsDlc = false; + + _virtualFileSystem.ImportTickets(pfs); + + TreeIter? parentIter = null; + + foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) + { + using var ncaFile = new UniqueRef(); + + pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), path); + + if (nca == null) + { + continue; + } + + if (nca.Header.ContentType == NcaContentType.PublicData) + { + if (nca.GetProgramIdBase() != (ulong.Parse(_applicationId, NumberStyles.HexNumber) & ~0x1FFFUL)) + { + break; + } + + parentIter ??= ((TreeStore)_dlcTreeView.Model).AppendValues(true, "", path); + + ((TreeStore)_dlcTreeView.Model).AppendValues(parentIter.Value, true, nca.Header.TitleId.ToString("X16"), fileEntry.FullPath); + containsDlc = true; + } + } + + if (!containsDlc && !ignoreNotFound) + { + GtkDialog.CreateErrorDialog("The specified file does not contain DLC for the selected title!"); + } + } + private void AddButton_Clicked(object sender, EventArgs args) { FileChooserNative fileChooser = new("Select DLC files", this, FileChooserAction.Open, "Add", "Cancel") @@ -147,52 +207,7 @@ namespace Ryujinx.Ui.Windows { foreach (string containerPath in fileChooser.Filenames) { - if (!File.Exists(containerPath)) - { - return; - } - - using FileStream containerFile = File.OpenRead(containerPath); - - PartitionFileSystem pfs = new(); - pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure(); - bool containsDlc = false; - - _virtualFileSystem.ImportTickets(pfs); - - TreeIter? parentIter = null; - - foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) - { - using var ncaFile = new UniqueRef(); - - pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - - Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), containerPath); - - if (nca == null) - { - continue; - } - - if (nca.Header.ContentType == NcaContentType.PublicData) - { - if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000).ToString("x16") != _titleId) - { - break; - } - - parentIter ??= ((TreeStore)_dlcTreeView.Model).AppendValues(true, "", containerPath); - - ((TreeStore)_dlcTreeView.Model).AppendValues(parentIter.Value, true, nca.Header.TitleId.ToString("X16"), fileEntry.FullPath); - containsDlc = true; - } - } - - if (!containsDlc) - { - GtkDialog.CreateErrorDialog("The specified file does not contain DLC for the selected title!"); - } + AddDlc(containerPath); } } diff --git a/src/Ryujinx/Ui/Windows/TitleUpdateWindow.cs b/src/Ryujinx/Ui/Windows/TitleUpdateWindow.cs index 51918eeab..2f7f14f1f 100644 --- a/src/Ryujinx/Ui/Windows/TitleUpdateWindow.cs +++ b/src/Ryujinx/Ui/Windows/TitleUpdateWindow.cs @@ -4,12 +4,15 @@ using LibHac.Fs; using LibHac.Fs.Fsa; using LibHac.FsSystem; using LibHac.Ns; +using LibHac.Tools.Fs; using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.NcaUtils; using Ryujinx.Common.Configuration; using Ryujinx.Common.Utilities; using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.Loaders.Processes.Extensions; using Ryujinx.Ui.App.Common; +using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Widgets; using System; using System.Collections.Generic; @@ -24,7 +27,7 @@ namespace Ryujinx.Ui.Windows { private readonly MainWindow _parent; private readonly VirtualFileSystem _virtualFileSystem; - private readonly string _titleId; + private readonly ApplicationData _title; private readonly string _updateJsonPath; private TitleUpdateMetadata _titleUpdateWindowData; @@ -38,17 +41,17 @@ namespace Ryujinx.Ui.Windows [GUI] RadioButton _noUpdateRadioButton; #pragma warning restore CS0649, IDE0044 - public TitleUpdateWindow(MainWindow parent, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : this(new Builder("Ryujinx.Ui.Windows.TitleUpdateWindow.glade"), parent, virtualFileSystem, titleId, titleName) { } + public TitleUpdateWindow(MainWindow parent, VirtualFileSystem virtualFileSystem, ApplicationData applicationData) : this(new Builder("Ryujinx.Ui.Windows.TitleUpdateWindow.glade"), parent, virtualFileSystem, applicationData) { } - private TitleUpdateWindow(Builder builder, MainWindow parent, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : base(builder.GetRawOwnedObject("_titleUpdateWindow")) + private TitleUpdateWindow(Builder builder, MainWindow parent, VirtualFileSystem virtualFileSystem, ApplicationData applicationData) : base(builder.GetRawOwnedObject("_titleUpdateWindow")) { _parent = parent; builder.Autoconnect(this); - _titleId = titleId; + _title = applicationData; _virtualFileSystem = virtualFileSystem; - _updateJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleId, "updates.json"); + _updateJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, applicationData.IdString, "updates.json"); _radioButtonToPathDictionary = new Dictionary(); try @@ -64,7 +67,10 @@ namespace Ryujinx.Ui.Windows }; } - _baseTitleInfoLabel.Text = $"Updates Available for {titleName} [{titleId.ToUpper()}]"; + _baseTitleInfoLabel.Text = $"Updates Available for {applicationData.Name} [{applicationData.IdString}]"; + + // Try to get updates from PFS first + AddUpdate(_title.Path, true); foreach (string path in _titleUpdateWindowData.Paths) { @@ -84,18 +90,41 @@ namespace Ryujinx.Ui.Windows } } - private void AddUpdate(string path) + private void AddUpdate(string path, bool ignoreNotFound = false) { if (File.Exists(path)) { + IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks + ? IntegrityCheckLevel.ErrorOnInvalid + : IntegrityCheckLevel.None; + using FileStream file = new(path, FileMode.Open, FileAccess.Read); - PartitionFileSystem nsp = new(); - nsp.Initialize(file.AsStorage()).ThrowIfFailure(); + IFileSystem pfs; try { - (Nca patchNca, Nca controlNca) = ApplicationLibrary.GetGameUpdateDataFromPartition(_virtualFileSystem, nsp, _titleId, 0); + if (System.IO.Path.GetExtension(path).ToLower() == ".xci") + { + pfs = new Xci(_virtualFileSystem.KeySet, file.AsStorage()).OpenPartition(XciPartitionType.Secure); + } + else + { + var pfsTemp = new PartitionFileSystem(); + pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure(); + pfs = pfsTemp; + } + + Dictionary updates = pfs.GetUpdateData(_virtualFileSystem, checkLevel); + + Nca patchNca = null; + Nca controlNca = null; + + if (updates.TryGetValue(_title.Id, out ContentCollection update)) + { + patchNca = update.GetNcaByType(_virtualFileSystem.KeySet, LibHac.Ncm.ContentType.Program); + controlNca = update.GetNcaByType(_virtualFileSystem.KeySet, LibHac.Ncm.ContentType.Control); + } if (controlNca != null && patchNca != null) { @@ -106,7 +135,14 @@ namespace Ryujinx.Ui.Windows controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(ref nacpFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure(); nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure(); - RadioButton radioButton = new($"Version {controlData.DisplayVersionString.ToString()} - {path}"); + string radioLabel = $"Version {controlData.DisplayVersionString.ToString()} - {path}"; + + if (System.IO.Path.GetExtension(path).ToLower() == ".xci") + { + radioLabel = "Bundled: " + radioLabel; + } + + RadioButton radioButton = new(radioLabel); radioButton.JoinGroup(_noUpdateRadioButton); _availableUpdatesBox.Add(radioButton); @@ -117,7 +153,10 @@ namespace Ryujinx.Ui.Windows } else { - GtkDialog.CreateErrorDialog("The specified file does not contain an update for the selected title!"); + if (!ignoreNotFound) + { + GtkDialog.CreateErrorDialog("The specified file does not contain an update for the selected title!"); + } } } catch (Exception exception) From 98e7c33630af5a47246e3aa53c528528dbcc79bc Mon Sep 17 00:00:00 2001 From: Mary Guillemard Date: Sat, 11 Nov 2023 22:35:58 +0100 Subject: [PATCH 08/23] infra: Update to LLVM 15 for macOS release --- .github/workflows/release.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 988264a31..6089c2fd6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -156,11 +156,11 @@ jobs: with: global-json-file: global.json - - name: Setup LLVM 14 + - name: Setup LLVM 15 run: | wget https://apt.llvm.org/llvm.sh chmod +x llvm.sh - sudo ./llvm.sh 14 + sudo ./llvm.sh 15 - name: Install rcodesign run: | @@ -215,4 +215,4 @@ jobs: needs: release with: ryujinx_version: "1.1.${{ github.run_number }}" - secrets: inherit \ No newline at end of file + secrets: inherit From 6228331fd1fb63a32d929bf1cae7f709bc9fd271 Mon Sep 17 00:00:00 2001 From: Mary Guillemard Date: Sat, 11 Nov 2023 22:38:54 +0100 Subject: [PATCH 09/23] infra: switch back to ubuntu 20.04 LTS for macOS release --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6089c2fd6..008561f9c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,7 +25,7 @@ env: jobs: tag: name: Create tag - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Get version info id: version_info From 51065d91290e41a9d2518f44c9bdf83a9b0017ab Mon Sep 17 00:00:00 2001 From: gdkchan Date: Sat, 11 Nov 2023 23:35:30 -0300 Subject: [PATCH 10/23] Revert "Add support for multi game XCIs (#5638)" (#5914) This reverts commit 5c3cfb84c09b0566da677425915afa0b2d76da55. --- src/Ryujinx.Ava/AppHost.cs | 9 +- src/Ryujinx.Ava/Assets/Locales/en_US.json | 2 - src/Ryujinx.Ava/Common/ApplicationHelper.cs | 9 +- .../Controls/ApplicationContextMenu.axaml.cs | 54 +- .../UI/Controls/ApplicationGridView.axaml | 2 +- .../UI/Controls/ApplicationListView.axaml | 4 +- .../UI/Models/DownloadableContentModel.cs | 6 +- src/Ryujinx.Ava/UI/Models/SaveModel.cs | 4 +- src/Ryujinx.Ava/UI/Models/TitleUpdateModel.cs | 5 +- .../DownloadableContentManagerViewModel.cs | 60 +- .../UI/ViewModels/MainWindowViewModel.cs | 47 +- .../UI/ViewModels/TitleUpdateViewModel.cs | 56 +- .../UI/Views/Main/MainMenuBarView.axaml.cs | 10 +- .../UI/Views/Main/MainViewControls.axaml | 2 +- .../UI/Windows/CheatWindow.axaml.cs | 7 +- .../DownloadableContentManagerWindow.axaml | 2 +- .../DownloadableContentManagerWindow.axaml.cs | 12 +- .../UI/Windows/MainWindow.axaml.cs | 19 +- .../UI/Windows/TitleUpdateWindow.axaml.cs | 14 +- .../FileSystem/ContentCollection.cs | 61 -- .../Processes/Extensions/NcaExtensions.cs | 100 +- .../PartitionFileSystemExtensions.cs | 153 ++- .../Loaders/Processes/ProcessLoader.cs | 8 +- .../Loaders/Processes/ProcessLoaderHelper.cs | 9 +- src/Ryujinx.HLE/Switch.cs | 8 +- src/Ryujinx.Ui.Common/App/ApplicationData.cs | 18 +- .../App/ApplicationLibrary.cs | 922 +++++++++--------- src/Ryujinx/Program.cs | 8 +- src/Ryujinx/Ui/MainWindow.cs | 95 +- .../Ui/Widgets/GameTableContextMenu.cs | 89 +- src/Ryujinx/Ui/Windows/CheatWindow.cs | 9 +- src/Ryujinx/Ui/Windows/DlcWindow.cs | 123 +-- src/Ryujinx/Ui/Windows/TitleUpdateWindow.cs | 63 +- 33 files changed, 819 insertions(+), 1171 deletions(-) delete mode 100644 src/Ryujinx.HLE/FileSystem/ContentCollection.cs diff --git a/src/Ryujinx.Ava/AppHost.cs b/src/Ryujinx.Ava/AppHost.cs index 053d5b521..4d751e2a9 100644 --- a/src/Ryujinx.Ava/AppHost.cs +++ b/src/Ryujinx.Ava/AppHost.cs @@ -54,6 +54,8 @@ using System.Threading.Tasks; using static Ryujinx.Ava.UI.Helpers.Win32NativeInterop; using AntiAliasing = Ryujinx.Common.Configuration.AntiAliasing; using Image = SixLabors.ImageSharp.Image; +using InputManager = Ryujinx.Input.HLE.InputManager; +using IRenderer = Ryujinx.Graphics.GAL.IRenderer; using Key = Ryujinx.Input.Key; using MouseButton = Ryujinx.Input.MouseButton; using ScalingFilter = Ryujinx.Common.Configuration.ScalingFilter; @@ -121,14 +123,12 @@ namespace Ryujinx.Ava public int Width { get; private set; } public int Height { get; private set; } public string ApplicationPath { get; private set; } - public ulong ApplicationId { get; private set; } public bool ScreenshotRequested { get; set; } public AppHost( RendererHost renderer, InputManager inputManager, string applicationPath, - ulong applicationId, VirtualFileSystem virtualFileSystem, ContentManager contentManager, AccountManager accountManager, @@ -152,7 +152,6 @@ namespace Ryujinx.Ava NpadManager = _inputManager.CreateNpadManager(); TouchScreenManager = _inputManager.CreateTouchScreenManager(); ApplicationPath = applicationPath; - ApplicationId = applicationId; VirtualFileSystem = virtualFileSystem; ContentManager = contentManager; @@ -642,7 +641,7 @@ namespace Ryujinx.Ava { Logger.Info?.Print(LogClass.Application, "Loading as XCI."); - if (!Device.LoadXci(ApplicationPath, ApplicationId)) + if (!Device.LoadXci(ApplicationPath)) { Device.Dispose(); @@ -669,7 +668,7 @@ namespace Ryujinx.Ava { Logger.Info?.Print(LogClass.Application, "Loading as NSP."); - if (!Device.LoadNsp(ApplicationPath, ApplicationId)) + if (!Device.LoadNsp(ApplicationPath)) { Device.Dispose(); diff --git a/src/Ryujinx.Ava/Assets/Locales/en_US.json b/src/Ryujinx.Ava/Assets/Locales/en_US.json index be3e35a9c..bc2bbfe82 100644 --- a/src/Ryujinx.Ava/Assets/Locales/en_US.json +++ b/src/Ryujinx.Ava/Assets/Locales/en_US.json @@ -539,8 +539,6 @@ "OpenSetupGuideMessage": "Open the Setup Guide", "NoUpdate": "No Update", "TitleUpdateVersionLabel": "Version {0}", - "TitleBundledUpdateVersionLabel": "Bundled: Version {0}", - "TitleBundledDlcLabel": "Bundled:", "RyujinxInfo": "Ryujinx - Info", "RyujinxConfirm": "Ryujinx - Confirmation", "FileDialogAllTypes": "All types", diff --git a/src/Ryujinx.Ava/Common/ApplicationHelper.cs b/src/Ryujinx.Ava/Common/ApplicationHelper.cs index dd4643297..91ca8f4d5 100644 --- a/src/Ryujinx.Ava/Common/ApplicationHelper.cs +++ b/src/Ryujinx.Ava/Common/ApplicationHelper.cs @@ -18,8 +18,7 @@ using Ryujinx.Ava.UI.Helpers; using Ryujinx.Common.Logging; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS.Services.Account.Acc; -using Ryujinx.HLE.Loaders.Processes.Extensions; -using Ryujinx.Ui.Common.Configuration; +using Ryujinx.Ui.App.Common; using Ryujinx.Ui.Common.Helper; using System; using System.Buffers; @@ -227,11 +226,7 @@ namespace Ryujinx.Ava.Common return; } - IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks - ? IntegrityCheckLevel.ErrorOnInvalid - : IntegrityCheckLevel.None; - - (Nca updatePatchNca, _) = mainNca.GetUpdateData(_virtualFileSystem, checkLevel, programIndex, out _); + (Nca updatePatchNca, _) = ApplicationLibrary.GetGameUpdateData(_virtualFileSystem, mainNca.Header.TitleId.ToString("x16"), programIndex, out _); if (updatePatchNca != null) { patchNca = updatePatchNca; diff --git a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs index 69465c7c7..0f0071065 100644 --- a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs +++ b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs @@ -1,6 +1,7 @@ using Avalonia.Controls; using Avalonia.Interactivity; using Avalonia.Markup.Xaml; +using Avalonia.Threading; using LibHac.Fs; using LibHac.Tools.FsSystem.NcaUtils; using Ryujinx.Ava.Common; @@ -14,6 +15,7 @@ using Ryujinx.Ui.App.Common; using Ryujinx.Ui.Common.Helper; using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using Path = System.IO.Path; @@ -39,7 +41,7 @@ namespace Ryujinx.Ava.UI.Controls { viewModel.SelectedApplication.Favorite = !viewModel.SelectedApplication.Favorite; - ApplicationLibrary.LoadAndSaveMetaData(viewModel.SelectedApplication.IdString, appMetadata => + ApplicationLibrary.LoadAndSaveMetaData(viewModel.SelectedApplication.TitleId, appMetadata => { appMetadata.Favorite = viewModel.SelectedApplication.Favorite; }); @@ -74,9 +76,19 @@ namespace Ryujinx.Ava.UI.Controls { if (viewModel?.SelectedApplication != null) { - var saveDataFilter = SaveDataFilter.Make(viewModel.SelectedApplication.Id, saveDataType, userId, saveDataId: default, index: default); + if (!ulong.TryParse(viewModel.SelectedApplication.TitleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdNumber)) + { + Dispatcher.UIThread.InvokeAsync(async () => + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogRyujinxErrorMessage], LocaleManager.Instance[LocaleKeys.DialogInvalidTitleIdErrorMessage]); + }); - ApplicationHelper.OpenSaveDir(in saveDataFilter, viewModel.SelectedApplication.Id, viewModel.SelectedApplication.ControlHolder, viewModel.SelectedApplication.Name); + return; + } + + var saveDataFilter = SaveDataFilter.Make(titleIdNumber, saveDataType, userId, saveDataId: default, index: default); + + ApplicationHelper.OpenSaveDir(in saveDataFilter, titleIdNumber, viewModel.SelectedApplication.ControlHolder, viewModel.SelectedApplication.TitleName); } } @@ -86,7 +98,7 @@ namespace Ryujinx.Ava.UI.Controls if (viewModel?.SelectedApplication != null) { - await TitleUpdateWindow.Show(viewModel.VirtualFileSystem, viewModel.SelectedApplication); + await TitleUpdateWindow.Show(viewModel.VirtualFileSystem, ulong.Parse(viewModel.SelectedApplication.TitleId, NumberStyles.HexNumber), viewModel.SelectedApplication.TitleName); } } @@ -96,7 +108,7 @@ namespace Ryujinx.Ava.UI.Controls if (viewModel?.SelectedApplication != null) { - await DownloadableContentManagerWindow.Show(viewModel.VirtualFileSystem, viewModel.SelectedApplication); + await DownloadableContentManagerWindow.Show(viewModel.VirtualFileSystem, ulong.Parse(viewModel.SelectedApplication.TitleId, NumberStyles.HexNumber), viewModel.SelectedApplication.TitleName); } } @@ -108,8 +120,8 @@ namespace Ryujinx.Ava.UI.Controls { await new CheatWindow( viewModel.VirtualFileSystem, - viewModel.SelectedApplication.IdString, - viewModel.SelectedApplication.Name, + viewModel.SelectedApplication.TitleId, + viewModel.SelectedApplication.TitleName, viewModel.SelectedApplication.Path).ShowDialog(viewModel.TopLevel as Window); } } @@ -121,7 +133,7 @@ namespace Ryujinx.Ava.UI.Controls if (viewModel?.SelectedApplication != null) { string modsBasePath = ModLoader.GetModsBasePath(); - string titleModsPath = ModLoader.GetTitleDir(modsBasePath, viewModel.SelectedApplication.IdString); + string titleModsPath = ModLoader.GetTitleDir(modsBasePath, viewModel.SelectedApplication.TitleId); OpenHelper.OpenFolder(titleModsPath); } @@ -134,7 +146,7 @@ namespace Ryujinx.Ava.UI.Controls if (viewModel?.SelectedApplication != null) { string sdModsBasePath = ModLoader.GetSdModsBasePath(); - string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, viewModel.SelectedApplication.IdString); + string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, viewModel.SelectedApplication.TitleId); OpenHelper.OpenFolder(titleModsPath); } @@ -148,15 +160,15 @@ namespace Ryujinx.Ava.UI.Controls { UserResult result = await ContentDialogHelper.CreateConfirmationDialog( LocaleManager.Instance[LocaleKeys.DialogWarning], - LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionMessage, viewModel.SelectedApplication.Name), + LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionMessage, viewModel.SelectedApplication.TitleName), LocaleManager.Instance[LocaleKeys.InputDialogYes], LocaleManager.Instance[LocaleKeys.InputDialogNo], LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); if (result == UserResult.Yes) { - DirectoryInfo mainDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu", "0")); - DirectoryInfo backupDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu", "1")); + DirectoryInfo mainDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "cpu", "0")); + DirectoryInfo backupDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "cpu", "1")); List cacheFiles = new(); @@ -196,14 +208,14 @@ namespace Ryujinx.Ava.UI.Controls { UserResult result = await ContentDialogHelper.CreateConfirmationDialog( LocaleManager.Instance[LocaleKeys.DialogWarning], - LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogShaderDeletionMessage, viewModel.SelectedApplication.Name), + LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogShaderDeletionMessage, viewModel.SelectedApplication.TitleName), LocaleManager.Instance[LocaleKeys.InputDialogYes], LocaleManager.Instance[LocaleKeys.InputDialogNo], LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); if (result == UserResult.Yes) { - DirectoryInfo shaderCacheDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "shader")); + DirectoryInfo shaderCacheDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "shader")); List oldCacheDirectories = new(); List newCacheFiles = new(); @@ -251,7 +263,7 @@ namespace Ryujinx.Ava.UI.Controls if (viewModel?.SelectedApplication != null) { - string ptcDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu"); + string ptcDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "cpu"); string mainDir = Path.Combine(ptcDir, "0"); string backupDir = Path.Combine(ptcDir, "1"); @@ -272,7 +284,7 @@ namespace Ryujinx.Ava.UI.Controls if (viewModel?.SelectedApplication != null) { - string shaderCacheDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "shader"); + string shaderCacheDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "shader"); if (!Directory.Exists(shaderCacheDir)) { @@ -293,7 +305,7 @@ namespace Ryujinx.Ava.UI.Controls viewModel.StorageProvider, NcaSectionType.Code, viewModel.SelectedApplication.Path, - viewModel.SelectedApplication.Name); + viewModel.SelectedApplication.TitleName); } } @@ -307,7 +319,7 @@ namespace Ryujinx.Ava.UI.Controls viewModel.StorageProvider, NcaSectionType.Data, viewModel.SelectedApplication.Path, - viewModel.SelectedApplication.Name); + viewModel.SelectedApplication.TitleName); } } @@ -321,7 +333,7 @@ namespace Ryujinx.Ava.UI.Controls viewModel.StorageProvider, NcaSectionType.Logo, viewModel.SelectedApplication.Path, - viewModel.SelectedApplication.Name); + viewModel.SelectedApplication.TitleName); } } @@ -332,7 +344,7 @@ namespace Ryujinx.Ava.UI.Controls if (viewModel?.SelectedApplication != null) { ApplicationData selectedApplication = viewModel.SelectedApplication; - ShortcutHelper.CreateAppShortcut(selectedApplication.Path, selectedApplication.Name, selectedApplication.IdString, selectedApplication.Icon); + ShortcutHelper.CreateAppShortcut(selectedApplication.Path, selectedApplication.TitleName, selectedApplication.TitleId, selectedApplication.Icon); } } @@ -342,7 +354,7 @@ namespace Ryujinx.Ava.UI.Controls if (viewModel?.SelectedApplication != null) { - await viewModel.LoadApplication(viewModel.SelectedApplication); + await viewModel.LoadApplication(viewModel.SelectedApplication.Path); } } } diff --git a/src/Ryujinx.Ava/UI/Controls/ApplicationGridView.axaml b/src/Ryujinx.Ava/UI/Controls/ApplicationGridView.axaml index 5919652e2..bbdb4c4a7 100644 --- a/src/Ryujinx.Ava/UI/Controls/ApplicationGridView.axaml +++ b/src/Ryujinx.Ava/UI/Controls/ApplicationGridView.axaml @@ -82,7 +82,7 @@ diff --git a/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml b/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml index 24ec2b357..9004f7518 100644 --- a/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml +++ b/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml @@ -85,7 +85,7 @@ Path.GetFileName(ContainerPath); - public string Label => - Path.GetExtension(FileName)?.ToLower() == ".xci" ? $"{LocaleManager.Instance[LocaleKeys.TitleBundledDlcLabel]} {FileName}" : FileName; - public DownloadableContentModel(string titleId, string containerPath, string fullPath, bool enabled) { TitleId = titleId; diff --git a/src/Ryujinx.Ava/UI/Models/SaveModel.cs b/src/Ryujinx.Ava/UI/Models/SaveModel.cs index 2e3ed3bae..7b476932b 100644 --- a/src/Ryujinx.Ava/UI/Models/SaveModel.cs +++ b/src/Ryujinx.Ava/UI/Models/SaveModel.cs @@ -46,14 +46,14 @@ namespace Ryujinx.Ava.UI.Models TitleId = info.ProgramId; UserId = info.UserId; - var appData = MainWindow.MainWindowViewModel.Applications.FirstOrDefault(x => x.IdString.ToUpper() == TitleIdString); + var appData = MainWindow.MainWindowViewModel.Applications.FirstOrDefault(x => x.TitleId.ToUpper() == TitleIdString); InGameList = appData != null; if (InGameList) { Icon = appData.Icon; - Title = appData.Name; + Title = appData.TitleName; } else { diff --git a/src/Ryujinx.Ava/UI/Models/TitleUpdateModel.cs b/src/Ryujinx.Ava/UI/Models/TitleUpdateModel.cs index fae2a08d0..3b44e8ee6 100644 --- a/src/Ryujinx.Ava/UI/Models/TitleUpdateModel.cs +++ b/src/Ryujinx.Ava/UI/Models/TitleUpdateModel.cs @@ -8,10 +8,7 @@ namespace Ryujinx.Ava.UI.Models public ApplicationControlProperty Control { get; } public string Path { get; } - public string Label => LocaleManager.Instance.UpdateAndGetDynamicValue( - System.IO.Path.GetExtension(Path)?.ToLower() == ".xci" ? LocaleKeys.TitleBundledUpdateVersionLabel : LocaleKeys.TitleUpdateVersionLabel, - Control.DisplayVersionString.ToString() - ); + public string Label => LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.TitleUpdateVersionLabel, Control.DisplayVersionString.ToString()); public TitleUpdateModel(ApplicationControlProperty control, string path) { diff --git a/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs index 9f3a0045d..cdecae77d 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs @@ -17,12 +17,11 @@ using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using Ryujinx.Common.Utilities; using Ryujinx.HLE.FileSystem; -using Ryujinx.HLE.Loaders.Processes.Extensions; -using Ryujinx.Ui.App.Common; using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading.Tasks; using Application = Avalonia.Application; using Path = System.IO.Path; @@ -39,7 +38,7 @@ namespace Ryujinx.Ava.UI.ViewModels private AvaloniaList _selectedDownloadableContents = new(); private string _search; - private readonly ApplicationData _applicationData; + private readonly ulong _titleId; private static readonly DownloadableContentJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); @@ -93,25 +92,18 @@ namespace Ryujinx.Ava.UI.ViewModels public IStorageProvider StorageProvider; - public DownloadableContentManagerViewModel(VirtualFileSystem virtualFileSystem, ApplicationData applicationData) + public DownloadableContentManagerViewModel(VirtualFileSystem virtualFileSystem, ulong titleId) { _virtualFileSystem = virtualFileSystem; - _applicationData = applicationData; + _titleId = titleId; if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { StorageProvider = desktop.MainWindow.StorageProvider; } - _downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, applicationData.IdString, "dlc.json"); - - if (!File.Exists(_downloadableContentJsonPath)) - { - _downloadableContentContainerList = new List(); - - Save(); - } + _downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "dlc.json"); try { @@ -128,9 +120,6 @@ namespace Ryujinx.Ava.UI.ViewModels private void LoadDownloadableContents() { - // NOTE: Try to load downloadable contents from PFS first. - AddDownloadableContent(_applicationData.Path); - foreach (DownloadableContentContainer downloadableContentContainer in _downloadableContentContainerList) { if (File.Exists(downloadableContentContainer.ContainerPath)) @@ -138,11 +127,7 @@ namespace Ryujinx.Ava.UI.ViewModels using FileStream containerFile = File.OpenRead(downloadableContentContainer.ContainerPath); PartitionFileSystem partitionFileSystem = new(); - - if (partitionFileSystem.Initialize(containerFile.AsStorage()).IsFailure()) - { - continue; - } + partitionFileSystem.Initialize(containerFile.AsStorage()).ThrowIfFailure(); _virtualFileSystem.ImportTickets(partitionFileSystem); @@ -235,34 +220,22 @@ namespace Ryujinx.Ava.UI.ViewModels foreach (var file in result) { - if (!AddDownloadableContent(file.Path.LocalPath)) - { - await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogDlcNoDlcErrorMessage]); - } + await AddDownloadableContent(file.Path.LocalPath); } } - private bool AddDownloadableContent(string path) + private async Task AddDownloadableContent(string path) { if (!File.Exists(path) || DownloadableContents.FirstOrDefault(x => x.ContainerPath == path) != null) { - return true; + return; } using FileStream containerFile = File.OpenRead(path); - IFileSystem partitionFileSystem; - - if (Path.GetExtension(path).ToLower() == ".xci") - { - partitionFileSystem = new Xci(_virtualFileSystem.KeySet, containerFile.AsStorage()).OpenPartition(XciPartitionType.Secure); - } - else - { - var pfsTemp = new PartitionFileSystem(); - pfsTemp.Initialize(containerFile.AsStorage()).ThrowIfFailure(); - partitionFileSystem = pfsTemp; - } + PartitionFileSystem partitionFileSystem = new(); + partitionFileSystem.Initialize(containerFile.AsStorage()).ThrowIfFailure(); + bool containsDownloadableContent = false; _virtualFileSystem.ImportTickets(partitionFileSystem); @@ -280,7 +253,7 @@ namespace Ryujinx.Ava.UI.ViewModels if (nca.Header.ContentType == NcaContentType.PublicData) { - if (nca.GetProgramIdBase() != _applicationData.IdBase) + if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000) != _titleId) { break; } @@ -292,11 +265,14 @@ namespace Ryujinx.Ava.UI.ViewModels OnPropertyChanged(nameof(UpdateCount)); Sort(); - return true; + containsDownloadableContent = true; } } - return false; + if (!containsDownloadableContent) + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogDlcNoDlcErrorMessage]); + } } public void Remove(DownloadableContentModel model) diff --git a/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs index 692df483d..80df5d398 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs @@ -95,7 +95,7 @@ namespace Ryujinx.Ava.UI.ViewModels private bool _canUpdate = true; private Cursor _cursor; private string _title; - private ApplicationData _currentApplicationData; + private string _currentEmulatedGamePath; private readonly AutoResetEvent _rendererWaitEvent; private WindowState _windowState; private double _windowWidth; @@ -106,6 +106,7 @@ namespace Ryujinx.Ava.UI.ViewModels public ApplicationData ListSelectedApplication; public ApplicationData GridSelectedApplication; + private string TitleName { get; set; } internal AppHost AppHost { get; set; } public MainWindowViewModel() @@ -929,8 +930,8 @@ namespace Ryujinx.Ava.UI.ViewModels return SortMode switch { #pragma warning disable IDE0055 // Disable formatting - ApplicationSort.Title => IsAscending ? SortExpressionComparer.Ascending(app => app.Name) - : SortExpressionComparer.Descending(app => app.Name), + ApplicationSort.Title => IsAscending ? SortExpressionComparer.Ascending(app => app.TitleName) + : SortExpressionComparer.Descending(app => app.TitleName), ApplicationSort.Developer => IsAscending ? SortExpressionComparer.Ascending(app => app.Developer) : SortExpressionComparer.Descending(app => app.Developer), ApplicationSort.LastPlayed => new LastPlayedSortComparer(IsAscending), @@ -967,7 +968,7 @@ namespace Ryujinx.Ava.UI.ViewModels { if (arg is ApplicationData app) { - return string.IsNullOrWhiteSpace(_searchText) || app.Name.ToLower().Contains(_searchText.ToLower()); + return string.IsNullOrWhiteSpace(_searchText) || app.TitleName.ToLower().Contains(_searchText.ToLower()); } return false; @@ -1096,7 +1097,7 @@ namespace Ryujinx.Ava.UI.ViewModels IsLoadingIndeterminate = false; break; case LoadState.Loaded: - LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, _currentApplicationData.Name); + LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, TitleName); IsLoadingIndeterminate = true; CacheLoadStatus = ""; break; @@ -1116,7 +1117,7 @@ namespace Ryujinx.Ava.UI.ViewModels IsLoadingIndeterminate = false; break; case ShaderCacheLoadingState.Loaded: - LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, _currentApplicationData.Name); + LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, TitleName); IsLoadingIndeterminate = true; CacheLoadStatus = ""; break; @@ -1167,13 +1168,13 @@ namespace Ryujinx.Ava.UI.ViewModels { UserChannelPersistence.ShouldRestart = false; - await LoadApplication(_currentApplicationData); + await LoadApplication(_currentEmulatedGamePath); } else { // Otherwise, clear state. UserChannelPersistence = new UserChannelPersistence(); - _currentApplicationData = null; + _currentEmulatedGamePath = null; } } @@ -1450,12 +1451,7 @@ namespace Ryujinx.Ava.UI.ViewModels if (result.Count > 0) { - ApplicationData applicationData = new() - { - Path = result[0].Path.LocalPath, - }; - - await LoadApplication(applicationData); + await LoadApplication(result[0].Path.LocalPath); } } @@ -1469,17 +1465,11 @@ namespace Ryujinx.Ava.UI.ViewModels if (result.Count > 0) { - ApplicationData applicationData = new() - { - Name = Path.GetFileNameWithoutExtension(result[0].Path.LocalPath), - Path = result[0].Path.LocalPath, - }; - - await LoadApplication(applicationData); + await LoadApplication(result[0].Path.LocalPath); } } - public async Task LoadApplication(ApplicationData application, bool startFullscreen = false) + public async Task LoadApplication(string path, bool startFullscreen = false, string titleName = "") { if (AppHost != null) { @@ -1499,7 +1489,7 @@ namespace Ryujinx.Ava.UI.ViewModels Logger.RestartTime(); - SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(application.Path, ConfigurationState.Instance.System.Language, application.Id); + SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(path, ConfigurationState.Instance.System.Language); PrepareLoadScreen(); @@ -1508,8 +1498,7 @@ namespace Ryujinx.Ava.UI.ViewModels AppHost = new AppHost( RendererHostControl, InputManager, - application.Path, - application.Id, + path, VirtualFileSystem, ContentManager, AccountManager, @@ -1527,17 +1516,17 @@ namespace Ryujinx.Ava.UI.ViewModels CanUpdate = false; - LoadHeading = application.Name; + LoadHeading = TitleName = titleName; - if (string.IsNullOrWhiteSpace(application.Name)) + if (string.IsNullOrWhiteSpace(titleName)) { LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, AppHost.Device.Processes.ActiveApplication.Name); - application.Name = AppHost.Device.Processes.ActiveApplication.Name; + TitleName = AppHost.Device.Processes.ActiveApplication.Name; } SwitchToRenderer(startFullscreen); - _currentApplicationData = application; + _currentEmulatedGamePath = path; Thread gameThread = new(InitializeGame) { Name = "GUI.WindowThread" }; gameThread.Start(); diff --git a/src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs index 7bb96131d..5090a8c70 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs @@ -1,3 +1,4 @@ +using Avalonia; using Avalonia.Collections; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Platform.Storage; @@ -7,7 +8,6 @@ using LibHac.Fs; using LibHac.Fs.Fsa; using LibHac.FsSystem; using LibHac.Ns; -using LibHac.Tools.Fs; using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.NcaUtils; using Ryujinx.Ava.Common.Locale; @@ -17,16 +17,12 @@ using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using Ryujinx.Common.Utilities; using Ryujinx.HLE.FileSystem; -using Ryujinx.HLE.Loaders.Processes.Extensions; using Ryujinx.Ui.App.Common; -using Ryujinx.Ui.Common.Configuration; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; -using Application = Avalonia.Application; -using ContentType = LibHac.Ncm.ContentType; using Path = System.IO.Path; using SpanHelpers = LibHac.Common.SpanHelpers; @@ -37,7 +33,7 @@ namespace Ryujinx.Ava.UI.ViewModels public TitleUpdateMetadata TitleUpdateWindowData; public readonly string TitleUpdateJsonPath; private VirtualFileSystem VirtualFileSystem { get; } - private ApplicationData ApplicationData { get; } + private ulong TitleId { get; } private AvaloniaList _titleUpdates = new(); private AvaloniaList _views = new(); @@ -77,18 +73,18 @@ namespace Ryujinx.Ava.UI.ViewModels public IStorageProvider StorageProvider; - public TitleUpdateViewModel(VirtualFileSystem virtualFileSystem, ApplicationData applicationData) + public TitleUpdateViewModel(VirtualFileSystem virtualFileSystem, ulong titleId) { VirtualFileSystem = virtualFileSystem; - ApplicationData = applicationData; + TitleId = titleId; if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { StorageProvider = desktop.MainWindow.StorageProvider; } - TitleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, ApplicationData.IdString, "updates.json"); + TitleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "updates.json"); try { @@ -96,7 +92,7 @@ namespace Ryujinx.Ava.UI.ViewModels } catch { - Logger.Warning?.Print(LogClass.Application, $"Failed to deserialize title update data for {ApplicationData.IdString} at {TitleUpdateJsonPath}"); + Logger.Warning?.Print(LogClass.Application, $"Failed to deserialize title update data for {TitleId} at {TitleUpdateJsonPath}"); TitleUpdateWindowData = new TitleUpdateMetadata { @@ -112,9 +108,6 @@ namespace Ryujinx.Ava.UI.ViewModels private void LoadUpdates() { - // Try to load updates from PFS first - AddUpdate(ApplicationData.Path, true); - foreach (string path in TitleUpdateWindowData.Paths) { AddUpdate(path); @@ -169,41 +162,17 @@ namespace Ryujinx.Ava.UI.ViewModels } } - private void AddUpdate(string path, bool ignoreNotFound = false) + private void AddUpdate(string path) { if (File.Exists(path) && TitleUpdates.All(x => x.Path != path)) { - IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks - ? IntegrityCheckLevel.ErrorOnInvalid - : IntegrityCheckLevel.None; - using FileStream file = new(path, FileMode.Open, FileAccess.Read); - IFileSystem pfs; - try { - if (Path.GetExtension(path).ToLower() == ".xci") - { - pfs = new Xci(VirtualFileSystem.KeySet, file.AsStorage()).OpenPartition(XciPartitionType.Secure); - } - else - { - var pfsTemp = new PartitionFileSystem(); - pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure(); - pfs = pfsTemp; - } - - Dictionary updates = pfs.GetUpdateData(VirtualFileSystem, checkLevel); - - Nca patchNca = null; - Nca controlNca = null; - - if (updates.TryGetValue(ApplicationData.Id, out ContentCollection content)) - { - patchNca = content.GetNcaByType(VirtualFileSystem.KeySet, ContentType.Program); - controlNca = content.GetNcaByType(VirtualFileSystem.KeySet, ContentType.Control); - } + var pfs = new PartitionFileSystem(); + pfs.Initialize(file.AsStorage()).ThrowIfFailure(); + (Nca patchNca, Nca controlNca) = ApplicationLibrary.GetGameUpdateDataFromPartition(VirtualFileSystem, pfs, TitleId.ToString("x16"), 0); if (controlNca != null && patchNca != null) { @@ -218,10 +187,7 @@ namespace Ryujinx.Ava.UI.ViewModels } else { - if (!ignoreNotFound) - { - Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage])); - } + Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage])); } } catch (Exception ex) diff --git a/src/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs b/src/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs index af3c5deb6..4f2d262da 100644 --- a/src/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs +++ b/src/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs @@ -10,7 +10,6 @@ using Ryujinx.Ava.UI.Windows; using Ryujinx.Common; using Ryujinx.Common.Utilities; using Ryujinx.Modules; -using Ryujinx.Ui.App.Common; using Ryujinx.Ui.Common; using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Common.Helper; @@ -132,14 +131,7 @@ namespace Ryujinx.Ava.UI.Views.Main if (!string.IsNullOrEmpty(contentPath)) { - ApplicationData applicationData = new() - { - Name = "miiEdit", - Id = 0x0100000000001009, - Path = contentPath, - }; - - await ViewModel.LoadApplication(applicationData, ViewModel.IsFullScreen || ViewModel.StartGamesInFullscreen); + await ViewModel.LoadApplication(contentPath, false, "Mii Applet"); } } diff --git a/src/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml b/src/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml index 7a716fb2a..cc21b5c60 100644 --- a/src/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml +++ b/src/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml @@ -104,7 +104,7 @@ Content="{locale:Locale GameListHeaderApplication}" GroupName="Sort" IsChecked="{Binding IsSortedByTitle, Mode=OneTime}" - Tag="Application" /> + Tag="Title" /> (); - IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks - ? IntegrityCheckLevel.ErrorOnInvalid - : IntegrityCheckLevel.None; Heading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.CheatWindowHeading, titleName, titleId.ToUpper()); - BuildId = ApplicationData.GetBuildId(virtualFileSystem, checkLevel, titlePath); + BuildId = ApplicationData.GetApplicationBuildId(virtualFileSystem, titlePath); InitializeComponent(); diff --git a/src/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml b/src/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml index 98aac09ce..99cf28e77 100644 --- a/src/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml +++ b/src/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml @@ -97,7 +97,7 @@ MaxLines="2" TextWrapping="Wrap" TextTrimming="CharacterEllipsis" - Text="{Binding Label}" /> + Text="{Binding FileName}" /> x.OfType().Name("DialogSpace").Child().OfType()); diff --git a/src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs index 352ac4e54..c78f4160d 100644 --- a/src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs +++ b/src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs @@ -4,7 +4,6 @@ using Avalonia.Controls.Primitives; using Avalonia.Interactivity; using Avalonia.Threading; using FluentAvalonia.UI.Controls; -using LibHac.Tools.FsSystem; using Ryujinx.Ava.Common; using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Input; @@ -24,6 +23,7 @@ using Ryujinx.Ui.Common; using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Common.Helper; using System; +using System.IO; using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; @@ -139,7 +139,9 @@ namespace Ryujinx.Ava.UI.Windows { ViewModel.SelectedIcon = args.Application.Icon; - ViewModel.LoadApplication(args.Application).Wait(); + string path = new FileInfo(args.Application.Path).FullName; + + ViewModel.LoadApplication(path).Wait(); } args.Handled = true; @@ -188,11 +190,7 @@ namespace Ryujinx.Ava.UI.Windows LibHacHorizonManager.InitializeBcatServer(); LibHacHorizonManager.InitializeSystemClients(); - IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks - ? IntegrityCheckLevel.ErrorOnInvalid - : IntegrityCheckLevel.None; - - ApplicationLibrary = new ApplicationLibrary(VirtualFileSystem, checkLevel); + ApplicationLibrary = new ApplicationLibrary(VirtualFileSystem); // 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 @@ -299,12 +297,7 @@ namespace Ryujinx.Ava.UI.Windows { _deferLoad = false; - ApplicationData applicationData = new() - { - Path = _launchPath, - }; - - ViewModel.LoadApplication(applicationData, _startFullscreen).Wait(); + ViewModel.LoadApplication(_launchPath, _startFullscreen).Wait(); } } else diff --git a/src/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml.cs index 8ecf165ce..7ece63355 100644 --- a/src/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml.cs +++ b/src/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml.cs @@ -7,15 +7,15 @@ using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Models; using Ryujinx.Ava.UI.ViewModels; using Ryujinx.HLE.FileSystem; -using Ryujinx.Ui.App.Common; using Ryujinx.Ui.Common.Helper; using System.Threading.Tasks; +using Button = Avalonia.Controls.Button; namespace Ryujinx.Ava.UI.Windows { public partial class TitleUpdateWindow : UserControl { - public readonly TitleUpdateViewModel ViewModel; + public TitleUpdateViewModel ViewModel; public TitleUpdateWindow() { @@ -24,22 +24,22 @@ namespace Ryujinx.Ava.UI.Windows InitializeComponent(); } - public TitleUpdateWindow(VirtualFileSystem virtualFileSystem, ApplicationData applicationData) + public TitleUpdateWindow(VirtualFileSystem virtualFileSystem, ulong titleId) { - DataContext = ViewModel = new TitleUpdateViewModel(virtualFileSystem, applicationData); + DataContext = ViewModel = new TitleUpdateViewModel(virtualFileSystem, titleId); InitializeComponent(); } - public static async Task Show(VirtualFileSystem virtualFileSystem, ApplicationData applicationData) + public static async Task Show(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName) { ContentDialog contentDialog = new() { PrimaryButtonText = "", SecondaryButtonText = "", CloseButtonText = "", - Content = new TitleUpdateWindow(virtualFileSystem, applicationData), - Title = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.GameUpdateWindowHeading, applicationData.Name, applicationData.IdString), + Content = new TitleUpdateWindow(virtualFileSystem, titleId), + Title = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.GameUpdateWindowHeading, titleName, titleId.ToString("X16")), }; Style bottomBorder = new(x => x.OfType().Name("DialogSpace").Child().OfType()); diff --git a/src/Ryujinx.HLE/FileSystem/ContentCollection.cs b/src/Ryujinx.HLE/FileSystem/ContentCollection.cs deleted file mode 100644 index 1c19887be..000000000 --- a/src/Ryujinx.HLE/FileSystem/ContentCollection.cs +++ /dev/null @@ -1,61 +0,0 @@ -using LibHac.Common.Keys; -using LibHac.Fs.Fsa; -using LibHac.Ncm; -using LibHac.Tools.FsSystem.NcaUtils; -using LibHac.Tools.Ncm; -using Ryujinx.HLE.Loaders.Processes.Extensions; -using System; - -namespace Ryujinx.HLE.FileSystem -{ - /// - /// Thin wrapper around - /// - public class ContentCollection - { - private readonly IFileSystem _pfs; - private readonly Cnmt _cnmt; - - public ulong Id => _cnmt.TitleId; - public TitleVersion Version => _cnmt.TitleVersion; - public ContentMetaType Type => _cnmt.Type; - public ulong ApplicationId => _cnmt.ApplicationTitleId; - public ulong PatchId => _cnmt.PatchTitleId; - public TitleVersion RequiredSystemVersion => _cnmt.MinimumSystemVersion; - public TitleVersion RequiredApplicationVersion => _cnmt.MinimumApplicationVersion; - public byte[] Digest => _cnmt.Hash; - - public ulong ProgramBaseId => Id & ~0x1FFFUL; - public bool IsSystemTitle => _cnmt.Type < ContentMetaType.Application; - - public ContentCollection(IFileSystem pfs, Cnmt cnmt) - { - _pfs = pfs; - _cnmt = cnmt; - } - - public Nca GetNcaByType(KeySet keySet, ContentType type, int programIndex = 0) - { - // TODO: Replace this with a check for IdOffset as soon as LibHac supports it: - // && entry.IdOffset == programIndex - - foreach (var entry in _cnmt.ContentEntries) - { - if (entry.Type != type) - { - continue; - } - - string ncaId = BitConverter.ToString(entry.NcaId).Replace("-", null).ToLower(); - Nca nca = _pfs.GetNca(keySet, $"/{ncaId}.nca"); - - if (nca.GetProgramIndex() == programIndex) - { - return nca; - } - } - - return null; - } - } -} diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs index 6863d1a7c..4568b44da 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs @@ -2,31 +2,21 @@ using LibHac.Common; using LibHac.Fs; using LibHac.Fs.Fsa; -using LibHac.FsSystem; using LibHac.Loader; using LibHac.Ncm; using LibHac.Ns; -using LibHac.Tools.Fs; using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.NcaUtils; -using LibHac.Tools.Ncm; -using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; -using Ryujinx.Common.Utilities; -using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS; using System.IO; using System.Linq; using ApplicationId = LibHac.Ncm.ApplicationId; -using ContentType = LibHac.Ncm.ContentType; -using Path = System.IO.Path; namespace Ryujinx.HLE.Loaders.Processes.Extensions { - public static class NcaExtensions + static class NcaExtensions { - private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); - public static ProcessResult Load(this Nca nca, Switch device, Nca patchNca, Nca controlNca) { // Extract RomFs and ExeFs from NCA. @@ -57,7 +47,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions nacpData = controlNca.GetNacp(device); } - /* TODO: Rework this since it's wrong and doesn't work as it takes the DisplayVersion from a "potential" non-existent update. + /* TODO: Rework this since it's wrong and doesn't work as it takes the DisplayVersion from a "potential" inexistant update. // Load program 0 control NCA as we are going to need it for display version. (_, Nca updateProgram0ControlNca) = GetGameUpdateData(_device.Configuration.VirtualFileSystem, mainNca.Header.TitleId.ToString("x16"), 0, out _); @@ -96,11 +86,6 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions return processResult; } - public static ulong GetProgramIdBase(this Nca nca) - { - return nca.Header.TitleId & ~0x1FFFUL; - } - public static int GetProgramIndex(this Nca nca) { return (int)(nca.Header.TitleId & 0xF); @@ -111,11 +96,6 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions return nca.Header.ContentType == NcaContentType.Program; } - public static bool IsMain(this Nca nca) - { - return nca.IsProgram() && !nca.IsPatch(); - } - public static bool IsPatch(this Nca nca) { int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); @@ -128,56 +108,6 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions return nca.Header.ContentType == NcaContentType.Control; } - public static (Nca, Nca) GetUpdateData(this Nca mainNca, VirtualFileSystem fileSystem, IntegrityCheckLevel checkLevel, int programIndex, out string updatePath) - { - updatePath = "(unknown)"; - - // Load Update NCAs. - Nca updatePatchNca = null; - Nca updateControlNca = null; - - // Clear the program index part. - ulong titleIdBase = mainNca.GetProgramIdBase(); - - // Load update information if exists. - string titleUpdateMetadataPath = Path.Combine(AppDataManager.GamesDirPath, mainNca.Header.TitleId.ToString("x16"), "updates.json"); - if (File.Exists(titleUpdateMetadataPath)) - { - updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected; - if (File.Exists(updatePath)) - { - var updateFile = new FileStream(updatePath, FileMode.Open, FileAccess.Read); - - IFileSystem updatePartitionFileSystem; - - if (Path.GetExtension(updatePath).ToLower() == ".xci") - { - updatePartitionFileSystem = new Xci(fileSystem.KeySet, updateFile.AsStorage()).OpenPartition(XciPartitionType.Secure); - } - else - { - PartitionFileSystem pfsTemp = new(); - pfsTemp.Initialize(updateFile.AsStorage()).ThrowIfFailure(); - updatePartitionFileSystem = pfsTemp; - } - - foreach ((ulong updateTitleId, ContentCollection content) in updatePartitionFileSystem.GetUpdateData(fileSystem, checkLevel)) - { - if ((updateTitleId & ~0x1FFFUL) != titleIdBase) - { - continue; - } - - updatePatchNca = content.GetNcaByType(fileSystem.KeySet, ContentType.Program, programIndex); - updateControlNca = content.GetNcaByType(fileSystem.KeySet, ContentType.Control, programIndex); - break; - } - } - } - - return (updatePatchNca, updateControlNca); - } - public static IFileSystem GetExeFs(this Nca nca, Switch device, Nca patchNca = null) { IFileSystem exeFs = null; @@ -242,31 +172,5 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions return nacpData; } - - public static Cnmt GetCnmt(this Nca cnmtNca, IntegrityCheckLevel checkLevel, ContentMetaType metaType) - { - string path = $"/{metaType}_{cnmtNca.Header.TitleId:x16}.cnmt"; - using var cnmtFile = new UniqueRef(); - - try - { - Result result = cnmtNca.OpenFileSystem(0, checkLevel) - .OpenFile(ref cnmtFile.Ref, path.ToU8Span(), OpenMode.Read); - - if (result.IsSuccess()) - { - return new Cnmt(cnmtFile.Release().AsStream()); - } - } - catch (HorizonResultException ex) - { - if (!ResultFs.PathNotFound.Includes(ex.ResultValue)) - { - Logger.Warning?.Print(LogClass.Application, $"Failed get cnmt for '{cnmtNca.Header.TitleId:x16}' from nca: {ex.Message}"); - } - } - - return null; - } } } diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs index 5f45cd459..50f7d5853 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs @@ -1,87 +1,26 @@ using LibHac.Common; -using LibHac.Common.Keys; using LibHac.Fs; using LibHac.Fs.Fsa; using LibHac.FsSystem; -using LibHac.Ncm; using LibHac.Tools.Fs; using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.NcaUtils; -using LibHac.Tools.Ncm; using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using Ryujinx.Common.Utilities; -using Ryujinx.HLE.FileSystem; using System; using System.Collections.Generic; +using System.Globalization; using System.IO; -using ContentType = LibHac.Ncm.ContentType; namespace Ryujinx.HLE.Loaders.Processes.Extensions { public static class PartitionFileSystemExtensions { private static readonly DownloadableContentJsonSerializerContext _contentSerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); - public static Dictionary GetApplicationData(this IFileSystem partitionFileSystem, - VirtualFileSystem fileSystem, IntegrityCheckLevel checkLevel) - { - fileSystem.ImportTickets(partitionFileSystem); - - var programs = new Dictionary(); - - foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.cnmt.nca")) - { - Cnmt cnmt = partitionFileSystem.GetNca(fileSystem.KeySet, fileEntry.FullPath).GetCnmt(checkLevel, ContentMetaType.Application); - - if (cnmt == null) - { - continue; - } - - ContentCollection content = new(partitionFileSystem, cnmt); - - if (content.Type != ContentMetaType.Application) - { - continue; - } - - programs.TryAdd(content.ApplicationId, content); - } - - return programs; - } - - public static Dictionary GetUpdateData(this IFileSystem partitionFileSystem, - VirtualFileSystem fileSystem, IntegrityCheckLevel checkLevel) - { - fileSystem.ImportTickets(partitionFileSystem); - - var programs = new Dictionary(); - - foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.cnmt.nca")) - { - Cnmt cnmt = partitionFileSystem.GetNca(fileSystem.KeySet, fileEntry.FullPath).GetCnmt(checkLevel, ContentMetaType.Patch); - - if (cnmt == null) - { - continue; - } - - ContentCollection content = new(partitionFileSystem, cnmt); - - if (content.Type != ContentMetaType.Patch) - { - continue; - } - - programs.TryAdd(content.ApplicationId, content); - } - - return programs; - } - - internal static (bool, ProcessResult) TryLoad(this PartitionFileSystemCore partitionFileSystem, Switch device, string path, ulong titleId, out string errorMessage) + internal static (bool, ProcessResult) TryLoad(this PartitionFileSystemCore partitionFileSystem, Switch device, string path, out string errorMessage) where TMetaData : PartitionFileSystemMetaCore, new() where TFormat : IPartitionFileSystemFormat where THeader : unmanaged, IPartitionFileSystemHeader @@ -96,21 +35,30 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions try { - Dictionary applications = partitionFileSystem.GetApplicationData(device.FileSystem, device.System.FsIntegrityCheckLevel); + device.Configuration.VirtualFileSystem.ImportTickets(partitionFileSystem); - if (titleId == 0) + // TODO: To support multi-games container, this should use CNMT NCA instead. + foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca")) { - foreach ((ulong _, ContentCollection content) in applications) + Nca nca = partitionFileSystem.GetNca(device, fileEntry.FullPath); + + if (nca.GetProgramIndex() != device.Configuration.UserChannelPersistence.Index) { - mainNca = content.GetNcaByType(device.FileSystem.KeySet, ContentType.Program, device.Configuration.UserChannelPersistence.Index); - controlNca = content.GetNcaByType(device.FileSystem.KeySet, ContentType.Control, device.Configuration.UserChannelPersistence.Index); - break; + continue; + } + + if (nca.IsPatch()) + { + patchNca = nca; + } + else if (nca.IsProgram()) + { + mainNca = nca; + } + else if (nca.IsControl()) + { + controlNca = nca; } - } - else if (applications.TryGetValue(titleId, out ContentCollection content)) - { - mainNca = content.GetNcaByType(device.FileSystem.KeySet, ContentType.Program, device.Configuration.UserChannelPersistence.Index); - controlNca = content.GetNcaByType(device.FileSystem.KeySet, ContentType.Control, device.Configuration.UserChannelPersistence.Index); } ProcessLoaderHelper.RegisterProgramMapInfo(device, partitionFileSystem).ThrowIfFailure(); @@ -131,7 +79,54 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions return (false, ProcessResult.Failed); } - (Nca updatePatchNca, Nca updateControlNca) = mainNca.GetUpdateData(device.FileSystem, device.System.FsIntegrityCheckLevel, device.Configuration.UserChannelPersistence.Index, out string _); + // Load Update NCAs. + Nca updatePatchNca = null; + Nca updateControlNca = null; + + if (ulong.TryParse(mainNca.Header.TitleId.ToString("x16"), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdBase)) + { + // Clear the program index part. + titleIdBase &= ~0xFUL; + + // Load update information if exists. + string titleUpdateMetadataPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, titleIdBase.ToString("x16"), "updates.json"); + if (File.Exists(titleUpdateMetadataPath)) + { + string updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected; + if (File.Exists(updatePath)) + { + PartitionFileSystem updatePartitionFileSystem = new(); + updatePartitionFileSystem.Initialize(new FileStream(updatePath, FileMode.Open, FileAccess.Read).AsStorage()).ThrowIfFailure(); + + device.Configuration.VirtualFileSystem.ImportTickets(updatePartitionFileSystem); + + // TODO: This should use CNMT NCA instead. + foreach (DirectoryEntryEx fileEntry in updatePartitionFileSystem.EnumerateEntries("/", "*.nca")) + { + Nca nca = updatePartitionFileSystem.GetNca(device, fileEntry.FullPath); + + if (nca.GetProgramIndex() != device.Configuration.UserChannelPersistence.Index) + { + continue; + } + + if ($"{nca.Header.TitleId.ToString("x16")[..^3]}000" != titleIdBase.ToString("x16")) + { + break; + } + + if (nca.IsProgram()) + { + updatePatchNca = nca; + } + else if (nca.IsControl()) + { + updateControlNca = nca; + } + } + } + } + } if (updatePatchNca != null) { @@ -173,18 +168,18 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions return (true, mainNca.Load(device, patchNca, controlNca)); } - errorMessage = $"Unable to load: Could not find Main NCA for title \"{titleId:X16}\""; + errorMessage = "Unable to load: Could not find Main NCA"; return (false, ProcessResult.Failed); } - public static Nca GetNca(this IFileSystem fileSystem, KeySet keySet, string path) + public static Nca GetNca(this IFileSystem fileSystem, Switch device, string path) { using var ncaFile = new UniqueRef(); fileSystem.OpenFile(ref ncaFile.Ref, path.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - return new Nca(keySet, ncaFile.Release().AsStorage()); + return new Nca(device.Configuration.VirtualFileSystem.KeySet, ncaFile.Release().AsStorage()); } } } diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs index 6b4a64be8..220b868db 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs @@ -32,7 +32,7 @@ namespace Ryujinx.HLE.Loaders.Processes _processesByPid = new ConcurrentDictionary(); } - public bool LoadXci(string path, ulong titleId) + public bool LoadXci(string path) { FileStream stream = new(path, FileMode.Open, FileAccess.Read); Xci xci = new(_device.Configuration.VirtualFileSystem.KeySet, stream.AsStorage()); @@ -44,7 +44,7 @@ namespace Ryujinx.HLE.Loaders.Processes return false; } - (bool success, ProcessResult processResult) = xci.OpenPartition(XciPartitionType.Secure).TryLoad(_device, path, titleId, out string errorMessage); + (bool success, ProcessResult processResult) = xci.OpenPartition(XciPartitionType.Secure).TryLoad(_device, path, out string errorMessage); if (!success) { @@ -66,13 +66,13 @@ namespace Ryujinx.HLE.Loaders.Processes return false; } - public bool LoadNsp(string path, ulong titleId) + public bool LoadNsp(string path) { FileStream file = new(path, FileMode.Open, FileAccess.Read); PartitionFileSystem partitionFileSystem = new(); partitionFileSystem.Initialize(file.AsStorage()).ThrowIfFailure(); - (bool success, ProcessResult processResult) = partitionFileSystem.TryLoad(_device, path, titleId, out string errorMessage); + (bool success, ProcessResult processResult) = partitionFileSystem.TryLoad(_device, path, out string errorMessage); if (processResult.ProcessId == 0) { diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs index 110bb0928..c229b1742 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs @@ -42,14 +42,15 @@ namespace Ryujinx.HLE.Loaders.Processes foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca")) { - Nca nca = partitionFileSystem.GetNca(device.FileSystem.KeySet, fileEntry.FullPath); + Nca nca = partitionFileSystem.GetNca(device, fileEntry.FullPath); - if (!nca.IsProgram()) + if (!nca.IsProgram() && nca.IsPatch()) { continue; } - ulong currentMainProgramId = nca.GetProgramIdBase(); + ulong currentProgramId = nca.Header.TitleId; + ulong currentMainProgramId = currentProgramId & ~0xFFFul; if (applicationId == 0 && currentMainProgramId != 0) { @@ -66,7 +67,7 @@ namespace Ryujinx.HLE.Loaders.Processes break; } - hasIndex[nca.GetProgramIndex()] = true; + hasIndex[(int)(currentProgramId & 0xF)] = true; } if (programCount == 0) diff --git a/src/Ryujinx.HLE/Switch.cs b/src/Ryujinx.HLE/Switch.cs index 3516049c9..ae063a47d 100644 --- a/src/Ryujinx.HLE/Switch.cs +++ b/src/Ryujinx.HLE/Switch.cs @@ -72,9 +72,9 @@ namespace Ryujinx.HLE return Processes.LoadUnpackedNca(exeFsDir, romFsFile); } - public bool LoadXci(string xciFile, ulong titleId = 0) + public bool LoadXci(string xciFile) { - return Processes.LoadXci(xciFile, titleId); + return Processes.LoadXci(xciFile); } public bool LoadNca(string ncaFile) @@ -82,9 +82,9 @@ namespace Ryujinx.HLE return Processes.LoadNca(ncaFile); } - public bool LoadNsp(string nspFile, ulong titleId = 0) + public bool LoadNsp(string nspFile) { - return Processes.LoadNsp(nspFile, titleId); + return Processes.LoadNsp(nspFile); } public bool LoadProgram(string fileName) diff --git a/src/Ryujinx.Ui.Common/App/ApplicationData.cs b/src/Ryujinx.Ui.Common/App/ApplicationData.cs index 7495ccb56..65ab01eeb 100644 --- a/src/Ryujinx.Ui.Common/App/ApplicationData.cs +++ b/src/Ryujinx.Ui.Common/App/ApplicationData.cs @@ -9,11 +9,9 @@ using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.NcaUtils; using Ryujinx.Common.Logging; using Ryujinx.HLE.FileSystem; -using Ryujinx.HLE.Loaders.Processes.Extensions; using Ryujinx.Ui.Common.Helper; using System; using System.IO; -using System.Text.Json.Serialization; namespace Ryujinx.Ui.App.Common { @@ -21,10 +19,10 @@ namespace Ryujinx.Ui.App.Common { public bool Favorite { get; set; } public byte[] Icon { get; set; } - public string Name { get; set; } = "Unknown"; - public ulong Id { get; set; } - public string Developer { get; set; } = "Unknown"; - public string Version { get; set; } = "0"; + public string TitleName { get; set; } + public string TitleId { get; set; } + public string Developer { get; set; } + public string Version { get; set; } public TimeSpan TimePlayed { get; set; } public DateTime? LastPlayed { get; set; } public string FileExtension { get; set; } @@ -38,11 +36,7 @@ namespace Ryujinx.Ui.App.Common public string FileSizeString => ValueFormatUtils.FormatFileSize(FileSize); - [JsonIgnore] public string IdString => Id.ToString("x16"); - - [JsonIgnore] public ulong IdBase => Id & ~0x1FFFUL; - - public static string GetBuildId(VirtualFileSystem virtualFileSystem, IntegrityCheckLevel checkLevel, string titleFilePath) + public static string GetApplicationBuildId(VirtualFileSystem virtualFileSystem, string titleFilePath) { using FileStream file = new(titleFilePath, FileMode.Open, FileAccess.Read); @@ -111,7 +105,7 @@ namespace Ryujinx.Ui.App.Common return string.Empty; } - (Nca updatePatchNca, _) = mainNca.GetUpdateData(virtualFileSystem, checkLevel, 0, out string _); + (Nca updatePatchNca, _) = ApplicationLibrary.GetGameUpdateData(virtualFileSystem, mainNca.Header.TitleId.ToString("x16"), 0, out _); if (updatePatchNca != null) { diff --git a/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs b/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs index 976129717..46f29851c 100644 --- a/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs +++ b/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs @@ -14,18 +14,17 @@ using Ryujinx.Common.Utilities; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS.SystemState; using Ryujinx.HLE.Loaders.Npdm; -using Ryujinx.HLE.Loaders.Processes.Extensions; using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Common.Configuration.System; using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Text.Json; using System.Threading; -using ContentType = LibHac.Ncm.ContentType; using Path = System.IO.Path; using TimeSpan = System.TimeSpan; @@ -43,16 +42,15 @@ namespace Ryujinx.Ui.App.Common private readonly byte[] _nsoIcon; private readonly VirtualFileSystem _virtualFileSystem; - private readonly IntegrityCheckLevel _checkLevel; private Language _desiredTitleLanguage; private CancellationTokenSource _cancellationToken; private static readonly ApplicationJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); - public ApplicationLibrary(VirtualFileSystem virtualFileSystem, IntegrityCheckLevel checkLevel) + public ApplicationLibrary(VirtualFileSystem virtualFileSystem) { _virtualFileSystem = virtualFileSystem; - _checkLevel = checkLevel; _nspIcon = GetResourceBytes("Ryujinx.Ui.Common.Resources.Icon_NSP.png"); _xciIcon = GetResourceBytes("Ryujinx.Ui.Common.Resources.Icon_XCI.png"); @@ -71,390 +69,6 @@ namespace Ryujinx.Ui.App.Common return resourceByteArray; } - private ApplicationData GetApplicationFromExeFs(PartitionFileSystem pfs, string filePath) - { - ApplicationData data = new() - { - Icon = _nspIcon, - }; - - using UniqueRef npdmFile = new(); - - try - { - Result result = pfs.OpenFile(ref npdmFile.Ref, "/main.npdm".ToU8Span(), OpenMode.Read); - - if (ResultFs.PathNotFound.Includes(result)) - { - Npdm npdm = new(npdmFile.Get.AsStream()); - - data.Name = npdm.TitleName; - data.Id = npdm.Aci0.TitleId; - } - - return data; - } - catch (Exception exception) - { - Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. File: '{filePath}' Error: {exception.Message}"); - - return null; - } - } - - private ApplicationData GetApplicationFromNsp(PartitionFileSystem pfs, string filePath) - { - bool isExeFs = false; - - // If the NSP doesn't have a main NCA, decrement the number of applications found and then continue to the next application. - bool hasMainNca = false; - - foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*")) - { - if (Path.GetExtension(fileEntry.FullPath)?.ToLower() == ".nca") - { - using UniqueRef ncaFile = new(); - - pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - - Nca nca = new(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage()); - int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); - - // Some main NCAs don't have a data partition, so check if the partition exists before opening it - if (nca.Header.ContentType == NcaContentType.Program && - !(nca.SectionExists(NcaSectionType.Data) && - nca.Header.GetFsHeader(dataIndex).IsPatchSection())) - { - hasMainNca = true; - - break; - } - } - else if (Path.GetFileNameWithoutExtension(fileEntry.FullPath) == "main") - { - isExeFs = true; - } - } - - if (hasMainNca) - { - List applications = GetApplicationsFromPfs(pfs, filePath); - - switch (applications.Count) - { - case 1: - return applications[0]; - case >= 1: - Logger.Warning?.Print(LogClass.Application, $"File '{filePath}' contains more applications than expected: {applications.Count}"); - return applications[0]; - default: - return null; - } - } - - if (isExeFs) - { - return GetApplicationFromExeFs(pfs, filePath); - } - - return null; - } - - private List GetApplicationsFromPfs(IFileSystem pfs, string filePath) - { - var applications = new List(); - string extension = Path.GetExtension(filePath).ToLower(); - - foreach ((ulong titleId, ContentCollection content) in pfs.GetApplicationData(_virtualFileSystem, _checkLevel)) - { - ApplicationData applicationData = new() - { - Id = titleId, - }; - - try - { - Nca mainNca = content.GetNcaByType(_virtualFileSystem.KeySet, ContentType.Program); - Nca controlNca = content.GetNcaByType(_virtualFileSystem.KeySet, ContentType.Control); - - BlitStruct controlHolder = new(1); - - IFileSystem controlFs = controlNca?.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None); - - // Check if there is an update available. - if (IsUpdateApplied(mainNca, out IFileSystem updatedControlFs)) - { - // Replace the original ControlFs by the updated one. - controlFs = updatedControlFs; - } - - ReadControlData(controlFs, controlHolder.ByteSpan); - - GetApplicationInformation(ref controlHolder.Value, ref applicationData); - - // Read the icon from the ControlFS and store it as a byte array - try - { - using UniqueRef icon = new(); - - controlFs.OpenFile(ref icon.Ref, $"/icon_{_desiredTitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure(); - - using MemoryStream stream = new(); - - icon.Get.AsStream().CopyTo(stream); - applicationData.Icon = stream.ToArray(); - } - catch (HorizonResultException) - { - foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*")) - { - if (entry.Name == "control.nacp") - { - continue; - } - - using var icon = new UniqueRef(); - - controlFs.OpenFile(ref icon.Ref, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - - using MemoryStream stream = new(); - - icon.Get.AsStream().CopyTo(stream); - applicationData.Icon = stream.ToArray(); - - if (applicationData.Icon != null) - { - break; - } - } - - applicationData.Icon ??= extension == ".xci" ? _xciIcon : _nspIcon; - } - - applicationData.ControlHolder = controlHolder; - - applications.Add(applicationData); - } - catch (MissingKeyException exception) - { - applicationData.Icon = extension == ".xci" ? _xciIcon : _nspIcon; - - Logger.Warning?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}"); - } - catch (InvalidDataException) - { - applicationData.Icon = extension == ".xci" ? _xciIcon : _nspIcon; - - Logger.Warning?.Print(LogClass.Application, $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {filePath}"); - } - catch (Exception exception) - { - Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. File: '{filePath}' Error: {exception}"); - } - } - - return applications; - } - - private bool TryGetApplicationsFromFile(string applicationPath, out List applications) - { - applications = new List(); - - long fileSizeBytes = new FileInfo(applicationPath).Length; - - double fileSize = fileSizeBytes * 0.000000000931; - - BlitStruct controlHolder = new(1); - - try - { - string extension = Path.GetExtension(applicationPath).ToLower(); - - using FileStream file = new(applicationPath, FileMode.Open, FileAccess.Read); - - switch (extension) - { - case ".xci": - { - Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage()); - - applications = GetApplicationsFromPfs(xci.OpenPartition(XciPartitionType.Secure), applicationPath); - - if (applications.Count == 0) - { - return false; - } - - break; - } - case ".nsp": - case ".pfs0": - var pfs = new PartitionFileSystem(); - pfs.Initialize(file.AsStorage()).ThrowIfFailure(); - - ApplicationData result = GetApplicationFromNsp(pfs, applicationPath); - - if (result == null) - { - return false; - } - - applications.Add(result); - - break; - case ".nro": - { - BinaryReader reader = new(file); - ApplicationData application = new(); - - byte[] Read(long position, int size) - { - file.Seek(position, SeekOrigin.Begin); - - return reader.ReadBytes(size); - } - - try - { - file.Seek(24, SeekOrigin.Begin); - - int assetOffset = reader.ReadInt32(); - - if (Encoding.ASCII.GetString(Read(assetOffset, 4)) == "ASET") - { - byte[] iconSectionInfo = Read(assetOffset + 8, 0x10); - - long iconOffset = BitConverter.ToInt64(iconSectionInfo, 0); - long iconSize = BitConverter.ToInt64(iconSectionInfo, 8); - - ulong nacpOffset = reader.ReadUInt64(); - ulong nacpSize = reader.ReadUInt64(); - - // Reads and stores game icon as byte array - if (iconSize > 0) - { - application.Icon = Read(assetOffset + iconOffset, (int)iconSize); - } - else - { - application.Icon = _nroIcon; - } - - // Read the NACP data - Read(assetOffset + (int)nacpOffset, (int)nacpSize).AsSpan().CopyTo(controlHolder.ByteSpan); - - GetApplicationInformation(ref controlHolder.Value, ref application); - } - else - { - application.Icon = _nroIcon; - application.Name = Path.GetFileNameWithoutExtension(applicationPath); - } - - application.ControlHolder = controlHolder; - applications.Add(application); - } - catch - { - Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}"); - - return false; - } - - break; - } - case ".nca": - { - try - { - ApplicationData application = new(); - - Nca nca = new(_virtualFileSystem.KeySet, new FileStream(applicationPath, FileMode.Open, FileAccess.Read).AsStorage()); - - if (!nca.IsProgram() || nca.IsPatch()) - { - return false; - } - - application.Icon = _ncaIcon; - application.Name = Path.GetFileNameWithoutExtension(applicationPath); - application.ControlHolder = controlHolder; - - applications.Add(application); - } - catch (InvalidDataException) - { - Logger.Warning?.Print(LogClass.Application, $"The NCA header content type check has failed. This is usually because the header key is incorrect or missing. Errored File: {applicationPath}"); - } - catch - { - Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}"); - - return false; - } - - break; - } - // If its an NSO we just set defaults - case ".nso": - { - ApplicationData application = new() - { - Icon = _nsoIcon, - Name = Path.GetFileNameWithoutExtension(applicationPath), - }; - - applications.Add(application); - break; - } - } - } - catch (IOException exception) - { - Logger.Warning?.Print(LogClass.Application, exception.Message); - - return false; - } - - foreach (var data in applications) - { - ApplicationMetadata appMetadata = LoadAndSaveMetaData(data.IdString, appMetadata => - { - 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) - { - 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.FileExtension = Path.GetExtension(applicationPath).TrimStart('.').ToUpper(); - data.FileSize = new FileInfo(applicationPath).Length; - data.Path = applicationPath; - } - - return true; - } - public void CancelLoading() { _cancellationToken?.Cancel(); @@ -478,7 +92,7 @@ namespace Ryujinx.Ui.App.Common _cancellationToken = new CancellationTokenSource(); // Builds the applications list with paths to found applications - List applicationPaths = new(); + List applications = new(); try { @@ -522,7 +136,7 @@ namespace Ryujinx.Ui.App.Common if (!fileInfo.Attributes.HasFlag(FileAttributes.Hidden) && extension is ".nsp" or ".pfs0" or ".xci" or ".nca" or ".nro" or ".nso") { var fullPath = fileInfo.ResolveLinkTarget(true)?.FullName ?? fileInfo.FullName; - applicationPaths.Add(fullPath); + applications.Add(fullPath); numApplicationsFound++; } } @@ -534,34 +148,327 @@ namespace Ryujinx.Ui.App.Common } // Loops through applications list, creating a struct and then firing an event containing the struct for each application - foreach (string applicationPath in applicationPaths) + foreach (string applicationPath in applications) { if (_cancellationToken.Token.IsCancellationRequested) { return; } - if (TryGetApplicationsFromFile(applicationPath, out List applications)) + long fileSize = new FileInfo(applicationPath).Length; + string titleName = "Unknown"; + string titleId = "0000000000000000"; + string developer = "Unknown"; + string version = "0"; + byte[] applicationIcon = null; + + BlitStruct controlHolder = new(1); + + try { - foreach (var application in applications) + string extension = Path.GetExtension(applicationPath).ToLower(); + + using FileStream file = new(applicationPath, FileMode.Open, FileAccess.Read); + + if (extension == ".nsp" || extension == ".pfs0" || extension == ".xci") { - OnApplicationAdded(new ApplicationAddedEventArgs + try { - AppData = application, - }); - } + IFileSystem pfs; - if (applications.Count > 1) + bool isExeFs = false; + + if (extension == ".xci") + { + Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage()); + + pfs = xci.OpenPartition(XciPartitionType.Secure); + } + else + { + var pfsTemp = new PartitionFileSystem(); + pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure(); + pfs = pfsTemp; + + // If the NSP doesn't have a main NCA, decrement the number of applications found and then continue to the next application. + bool hasMainNca = false; + + foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*")) + { + if (Path.GetExtension(fileEntry.FullPath).ToLower() == ".nca") + { + using UniqueRef ncaFile = new(); + + pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + Nca nca = new(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage()); + int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); + + // Some main NCAs don't have a data partition, so check if the partition exists before opening it + if (nca.Header.ContentType == NcaContentType.Program && !(nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection())) + { + hasMainNca = true; + + break; + } + } + else if (Path.GetFileNameWithoutExtension(fileEntry.FullPath) == "main") + { + isExeFs = true; + } + } + + if (!hasMainNca && !isExeFs) + { + numApplicationsFound--; + + continue; + } + } + + if (isExeFs) + { + applicationIcon = _nspIcon; + + using UniqueRef npdmFile = new(); + + Result result = pfs.OpenFile(ref npdmFile.Ref, "/main.npdm".ToU8Span(), OpenMode.Read); + + if (ResultFs.PathNotFound.Includes(result)) + { + Npdm npdm = new(npdmFile.Get.AsStream()); + + titleName = npdm.TitleName; + titleId = npdm.Aci0.TitleId.ToString("x16"); + } + } + else + { + GetControlFsAndTitleId(pfs, out IFileSystem controlFs, out titleId); + + // Check if there is an update available. + if (IsUpdateApplied(titleId, out IFileSystem updatedControlFs)) + { + // Replace the original ControlFs by the updated one. + controlFs = updatedControlFs; + } + + ReadControlData(controlFs, controlHolder.ByteSpan); + + GetGameInformation(ref controlHolder.Value, out titleName, out _, out developer, out version); + + // Read the icon from the ControlFS and store it as a byte array + try + { + using UniqueRef icon = new(); + + controlFs.OpenFile(ref icon.Ref, $"/icon_{_desiredTitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + using MemoryStream stream = new(); + + icon.Get.AsStream().CopyTo(stream); + applicationIcon = stream.ToArray(); + } + catch (HorizonResultException) + { + foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*")) + { + if (entry.Name == "control.nacp") + { + continue; + } + + using var icon = new UniqueRef(); + + controlFs.OpenFile(ref icon.Ref, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + using MemoryStream stream = new(); + + icon.Get.AsStream().CopyTo(stream); + applicationIcon = stream.ToArray(); + + if (applicationIcon != null) + { + break; + } + } + + applicationIcon ??= extension == ".xci" ? _xciIcon : _nspIcon; + } + } + } + catch (MissingKeyException exception) + { + applicationIcon = extension == ".xci" ? _xciIcon : _nspIcon; + + Logger.Warning?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}"); + } + catch (InvalidDataException) + { + applicationIcon = extension == ".xci" ? _xciIcon : _nspIcon; + + Logger.Warning?.Print(LogClass.Application, $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {applicationPath}"); + } + catch (Exception exception) + { + Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. File: '{applicationPath}' Error: {exception}"); + + numApplicationsFound--; + + continue; + } + } + else if (extension == ".nro") { - numApplicationsFound += applications.Count - 1; + BinaryReader reader = new(file); + + byte[] Read(long position, int size) + { + file.Seek(position, SeekOrigin.Begin); + + return reader.ReadBytes(size); + } + + try + { + file.Seek(24, SeekOrigin.Begin); + + int assetOffset = reader.ReadInt32(); + + if (Encoding.ASCII.GetString(Read(assetOffset, 4)) == "ASET") + { + byte[] iconSectionInfo = Read(assetOffset + 8, 0x10); + + long iconOffset = BitConverter.ToInt64(iconSectionInfo, 0); + long iconSize = BitConverter.ToInt64(iconSectionInfo, 8); + + ulong nacpOffset = reader.ReadUInt64(); + ulong nacpSize = reader.ReadUInt64(); + + // Reads and stores game icon as byte array + if (iconSize > 0) + { + applicationIcon = Read(assetOffset + iconOffset, (int)iconSize); + } + else + { + applicationIcon = _nroIcon; + } + + // Read the NACP data + Read(assetOffset + (int)nacpOffset, (int)nacpSize).AsSpan().CopyTo(controlHolder.ByteSpan); + + GetGameInformation(ref controlHolder.Value, out titleName, out titleId, out developer, out version); + } + else + { + applicationIcon = _nroIcon; + titleName = Path.GetFileNameWithoutExtension(applicationPath); + } + } + catch + { + Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}"); + + numApplicationsFound--; + + continue; + } + } + else if (extension == ".nca") + { + try + { + Nca nca = new(_virtualFileSystem.KeySet, new FileStream(applicationPath, FileMode.Open, FileAccess.Read).AsStorage()); + int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); + + if (nca.Header.ContentType != NcaContentType.Program || (nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection())) + { + numApplicationsFound--; + + continue; + } + } + catch (InvalidDataException) + { + Logger.Warning?.Print(LogClass.Application, $"The NCA header content type check has failed. This is usually because the header key is incorrect or missing. Errored File: {applicationPath}"); + } + catch + { + Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}"); + + numApplicationsFound--; + + continue; + } + + applicationIcon = _ncaIcon; + titleName = Path.GetFileNameWithoutExtension(applicationPath); + } + // If its an NSO we just set defaults + else if (extension == ".nso") + { + applicationIcon = _nsoIcon; + titleName = Path.GetFileNameWithoutExtension(applicationPath); + } + } + catch (IOException exception) + { + Logger.Warning?.Print(LogClass.Application, exception.Message); + + numApplicationsFound--; + + continue; + } + + ApplicationMetadata appMetadata = LoadAndSaveMetaData(titleId, appMetadata => + { + appMetadata.Title = titleName; + + // 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.TimePlayed = TimeSpan.FromSeconds(appMetadata.TimePlayedOld); + appMetadata.TimePlayedOld = default; } - numApplicationsLoaded += applications.Count; - } - else + // 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; + } + + } + }); + + ApplicationData data = new() { - numApplicationsFound--; - } + Favorite = appMetadata.Favorite, + Icon = applicationIcon, + TitleName = titleName, + TitleId = titleId, + Developer = developer, + Version = version, + TimePlayed = appMetadata.TimePlayed, + LastPlayed = appMetadata.LastPlayed, + FileExtension = Path.GetExtension(applicationPath).TrimStart('.').ToUpper(), + FileSize = fileSize, + Path = applicationPath, + ControlHolder = controlHolder, + }; + + numApplicationsLoaded++; + + OnApplicationAdded(new ApplicationAddedEventArgs + { + AppData = data, + }); OnApplicationCountUpdated(new ApplicationCountUpdatedEventArgs { @@ -593,6 +500,15 @@ namespace Ryujinx.Ui.App.Common ApplicationCountUpdated?.Invoke(null, e); } + private void GetControlFsAndTitleId(IFileSystem pfs, out IFileSystem controlFs, out string titleId) + { + (_, _, Nca controlNca) = GetGameData(_virtualFileSystem, pfs, 0); + + // Return the ControlFS + controlFs = controlNca?.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None); + titleId = controlNca?.Header.TitleId.ToString("x16"); + } + public static ApplicationMetadata LoadAndSaveMetaData(string titleId, Action modifyFunction = null) { string metadataFolder = Path.Combine(AppDataManager.GamesDirPath, titleId, "gui"); @@ -630,29 +546,10 @@ namespace Ryujinx.Ui.App.Common return appMetadata; } - public byte[] GetApplicationIcon(string applicationPath, Language desiredTitleLanguage, ulong titleId) + public byte[] GetApplicationIcon(string applicationPath, Language desiredTitleLanguage) { byte[] applicationIcon = null; - if (titleId == 0) - { - if (Directory.Exists(applicationPath)) - { - return _ncaIcon; - } - - return Path.GetExtension(applicationPath).ToLower() switch - { - ".nsp" => _nspIcon, - ".pfs0" => _nspIcon, - ".xci" => _xciIcon, - ".nso" => _nsoIcon, - ".nro" => _nroIcon, - ".nca" => _ncaIcon, - _ => _ncaIcon, - }; - } - try { // Look for icon only if applicationPath is not a directory @@ -698,16 +595,7 @@ namespace Ryujinx.Ui.App.Common else { // Store the ControlFS in variable called controlFs - Dictionary programs = pfs.GetApplicationData(_virtualFileSystem, _checkLevel); - IFileSystem controlFs = null; - - if (programs.ContainsKey(titleId)) - { - if (programs[titleId].GetNcaByType(_virtualFileSystem.KeySet, ContentType.Control) is { } controlNca) - { - controlFs = controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None); - } - } + GetControlFsAndTitleId(pfs, out IFileSystem controlFs, out _); // Read the icon from the ControlFS and store it as a byte array try @@ -734,11 +622,16 @@ namespace Ryujinx.Ui.App.Common controlFs.OpenFile(ref icon.Ref, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - using MemoryStream stream = new(); - icon.Get.AsStream().CopyTo(stream); - applicationIcon = stream.ToArray(); + using (MemoryStream stream = new()) + { + icon.Get.AsStream().CopyTo(stream); + applicationIcon = stream.ToArray(); + } - break; + if (applicationIcon != null) + { + break; + } } applicationIcon ??= extension == ".xci" ? _xciIcon : _nspIcon; @@ -821,41 +714,41 @@ namespace Ryujinx.Ui.App.Common return applicationIcon ?? _ncaIcon; } - private void GetApplicationInformation(ref ApplicationControlProperty controlData, ref ApplicationData data) + private void GetGameInformation(ref ApplicationControlProperty controlData, out string titleName, out string titleId, out string publisher, out string version) { _ = Enum.TryParse(_desiredTitleLanguage.ToString(), out TitleLanguage desiredTitleLanguage); if (controlData.Title.ItemsRo.Length > (int)desiredTitleLanguage) { - data.Name = controlData.Title[(int)desiredTitleLanguage].NameString.ToString(); - data.Developer = controlData.Title[(int)desiredTitleLanguage].PublisherString.ToString(); + titleName = controlData.Title[(int)desiredTitleLanguage].NameString.ToString(); + publisher = controlData.Title[(int)desiredTitleLanguage].PublisherString.ToString(); } else { - data.Name = null; - data.Developer = null; + titleName = null; + publisher = null; } - if (string.IsNullOrWhiteSpace(data.Name)) + if (string.IsNullOrWhiteSpace(titleName)) { foreach (ref readonly var controlTitle in controlData.Title.ItemsRo) { if (!controlTitle.NameString.IsEmpty()) { - data.Name = controlTitle.NameString.ToString(); + titleName = controlTitle.NameString.ToString(); break; } } } - if (string.IsNullOrWhiteSpace(data.Developer)) + if (string.IsNullOrWhiteSpace(publisher)) { foreach (ref readonly var controlTitle in controlData.Title.ItemsRo) { if (!controlTitle.PublisherString.IsEmpty()) { - data.Developer = controlTitle.PublisherString.ToString(); + publisher = controlTitle.PublisherString.ToString(); break; } @@ -864,21 +757,25 @@ namespace Ryujinx.Ui.App.Common if (controlData.PresenceGroupId != 0) { - data.Id = controlData.PresenceGroupId; + titleId = controlData.PresenceGroupId.ToString("x16"); } else if (controlData.SaveDataOwnerId != 0) { - data.Id = controlData.SaveDataOwnerId; + titleId = controlData.SaveDataOwnerId.ToString(); } else if (controlData.AddOnContentBaseId != 0) { - data.Id = (controlData.AddOnContentBaseId - 0x1000); + titleId = (controlData.AddOnContentBaseId - 0x1000).ToString("x16"); + } + else + { + titleId = "0000000000000000"; } - data.Version = controlData.DisplayVersionString.ToString(); + version = controlData.DisplayVersionString.ToString(); } - private bool IsUpdateApplied(Nca mainNca, out IFileSystem updatedControlFs) + private bool IsUpdateApplied(string titleId, out IFileSystem updatedControlFs) { updatedControlFs = null; @@ -886,11 +783,11 @@ namespace Ryujinx.Ui.App.Common try { - (Nca patchNca, Nca controlNca) = mainNca.GetUpdateData(_virtualFileSystem, _checkLevel, 0, out updatePath); + (Nca patchNca, Nca controlNca) = GetGameUpdateData(_virtualFileSystem, titleId, 0, out updatePath); if (patchNca != null && controlNca != null) { - updatedControlFs = controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None); + updatedControlFs = controlNca?.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None); return true; } @@ -906,5 +803,120 @@ namespace Ryujinx.Ui.App.Common return false; } + + public static (Nca main, Nca patch, Nca control) GetGameData(VirtualFileSystem fileSystem, IFileSystem pfs, int programIndex) + { + Nca mainNca = null; + Nca patchNca = null; + Nca controlNca = null; + + fileSystem.ImportTickets(pfs); + + foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) + { + using var ncaFile = new UniqueRef(); + + pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + Nca nca = new(fileSystem.KeySet, ncaFile.Release().AsStorage()); + + int ncaProgramIndex = (int)(nca.Header.TitleId & 0xF); + + if (ncaProgramIndex != programIndex) + { + continue; + } + + if (nca.Header.ContentType == NcaContentType.Program) + { + int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); + + if (nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection()) + { + patchNca = nca; + } + else + { + mainNca = nca; + } + } + else if (nca.Header.ContentType == NcaContentType.Control) + { + controlNca = nca; + } + } + + return (mainNca, patchNca, controlNca); + } + + public static (Nca patch, Nca control) GetGameUpdateDataFromPartition(VirtualFileSystem fileSystem, PartitionFileSystem pfs, string titleId, int programIndex) + { + Nca patchNca = null; + Nca controlNca = null; + + fileSystem.ImportTickets(pfs); + + foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) + { + using var ncaFile = new UniqueRef(); + + pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + Nca nca = new(fileSystem.KeySet, ncaFile.Release().AsStorage()); + + int ncaProgramIndex = (int)(nca.Header.TitleId & 0xF); + + if (ncaProgramIndex != programIndex) + { + continue; + } + + if ($"{nca.Header.TitleId.ToString("x16")[..^3]}000" != titleId) + { + break; + } + + if (nca.Header.ContentType == NcaContentType.Program) + { + patchNca = nca; + } + else if (nca.Header.ContentType == NcaContentType.Control) + { + controlNca = nca; + } + } + + return (patchNca, controlNca); + } + + public static (Nca patch, Nca control) GetGameUpdateData(VirtualFileSystem fileSystem, string titleId, int programIndex, out string updatePath) + { + updatePath = null; + + if (ulong.TryParse(titleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdBase)) + { + // Clear the program index part. + titleIdBase &= ~0xFUL; + + // Load update information if exists. + string titleUpdateMetadataPath = Path.Combine(AppDataManager.GamesDirPath, titleIdBase.ToString("x16"), "updates.json"); + + if (File.Exists(titleUpdateMetadataPath)) + { + updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected; + + if (File.Exists(updatePath)) + { + FileStream file = new(updatePath, FileMode.Open, FileAccess.Read); + PartitionFileSystem nsp = new(); + nsp.Initialize(file.AsStorage()).ThrowIfFailure(); + + return GetGameUpdateDataFromPartition(fileSystem, nsp, titleIdBase.ToString("x16"), programIndex); + } + } + } + + return (null, null); + } } } diff --git a/src/Ryujinx/Program.cs b/src/Ryujinx/Program.cs index 14062481a..afb6a9925 100644 --- a/src/Ryujinx/Program.cs +++ b/src/Ryujinx/Program.cs @@ -7,7 +7,6 @@ using Ryujinx.Common.SystemInterop; using Ryujinx.Modules; using Ryujinx.SDL2.Common; using Ryujinx.Ui; -using Ryujinx.Ui.App.Common; using Ryujinx.Ui.Common; using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Common.Helper; @@ -333,12 +332,7 @@ namespace Ryujinx if (CommandLineState.LaunchPathArg != null) { - ApplicationData applicationData = new() - { - Path = CommandLineState.LaunchPathArg, - }; - - mainWindow.RunApplication(applicationData, CommandLineState.StartFullscreenArg); + mainWindow.RunApplication(CommandLineState.LaunchPathArg, CommandLineState.StartFullscreenArg); } if (ConfigurationState.Instance.CheckUpdatesOnStart.Value && Updater.CanUpdate(false)) diff --git a/src/Ryujinx/Ui/MainWindow.cs b/src/Ryujinx/Ui/MainWindow.cs index 884f6687e..8b0b35e6c 100644 --- a/src/Ryujinx/Ui/MainWindow.cs +++ b/src/Ryujinx/Ui/MainWindow.cs @@ -39,7 +39,6 @@ using Silk.NET.Vulkan; using SPB.Graphics.Vulkan; using System; using System.Diagnostics; -using System.Globalization; using System.IO; using System.Reflection; using System.Threading; @@ -71,7 +70,7 @@ namespace Ryujinx.Ui private bool _gameLoaded; private bool _ending; - private ApplicationData _currentApplicationData = null; + private string _currentEmulatedGamePath = null; private string _lastScannedAmiiboId = ""; private bool _lastScannedAmiiboShowAll = false; @@ -182,12 +181,8 @@ namespace Ryujinx.Ui _accountManager = new AccountManager(_libHacHorizonManager.RyujinxClient, CommandLineState.Profile); _userChannelPersistence = new UserChannelPersistence(); - IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks - ? IntegrityCheckLevel.ErrorOnInvalid - : IntegrityCheckLevel.None; - // Instantiate GUI objects. - _applicationLibrary = new ApplicationLibrary(_virtualFileSystem, checkLevel); + _applicationLibrary = new ApplicationLibrary(_virtualFileSystem); _uiHandler = new GtkHostUiHandler(this); _deviceExitStatus = new AutoResetEvent(false); @@ -789,7 +784,7 @@ namespace Ryujinx.Ui } } - private bool LoadApplication(string path, ulong titleId, bool isFirmwareTitle) + private bool LoadApplication(string path, bool isFirmwareTitle) { SystemVersion firmwareVersion = _contentManager.GetCurrentFirmwareVersion(); @@ -863,7 +858,7 @@ namespace Ryujinx.Ui case ".xci": Logger.Info?.Print(LogClass.Application, "Loading as XCI."); - return _emulationContext.LoadXci(path, titleId); + return _emulationContext.LoadXci(path); case ".nca": Logger.Info?.Print(LogClass.Application, "Loading as NCA."); @@ -872,7 +867,7 @@ namespace Ryujinx.Ui case ".pfs0": Logger.Info?.Print(LogClass.Application, "Loading as NSP."); - return _emulationContext.LoadNsp(path, titleId); + return _emulationContext.LoadNsp(path); default: Logger.Info?.Print(LogClass.Application, "Loading as Homebrew."); try @@ -893,7 +888,7 @@ namespace Ryujinx.Ui return false; } - public void RunApplication(ApplicationData application, bool startFullscreen = false) + public void RunApplication(string path, bool startFullscreen = false) { if (_gameLoaded) { @@ -915,14 +910,14 @@ namespace Ryujinx.Ui bool isFirmwareTitle = false; - if (application.Path.StartsWith("@SystemContent")) + if (path.StartsWith("@SystemContent")) { - application.Path = VirtualFileSystem.SwitchPathToSystemPath(application.Path); + path = VirtualFileSystem.SwitchPathToSystemPath(path); isFirmwareTitle = true; } - if (!LoadApplication(application.Path, application.Id, isFirmwareTitle)) + if (!LoadApplication(path, isFirmwareTitle)) { _emulationContext.Dispose(); SwitchToGameTable(); @@ -932,7 +927,7 @@ namespace Ryujinx.Ui SetupProgressUiHandlers(); - _currentApplicationData = application; + _currentEmulatedGamePath = path; _deviceExitStatus.Reset(); @@ -1173,7 +1168,7 @@ namespace Ryujinx.Ui _tableStore.AppendValues( args.AppData.Favorite, new Gdk.Pixbuf(args.AppData.Icon, 75, 75), - $"{args.AppData.Name}\n{args.AppData.IdString.ToUpper()}", + $"{args.AppData.TitleName}\n{args.AppData.TitleId.ToUpper()}", args.AppData.Developer, args.AppData.Version, args.AppData.TimePlayedString, @@ -1261,22 +1256,9 @@ namespace Ryujinx.Ui { _gameTableSelection.GetSelected(out TreeIter treeIter); - ApplicationData application = new() - { - Favorite = (bool)_tableStore.GetValue(treeIter, 0), - Name = ((string)_tableStore.GetValue(treeIter, 2)).Split('\n')[0], - Id = ulong.Parse(((string)_tableStore.GetValue(treeIter, 2)).Split('\n')[1], NumberStyles.HexNumber), - Developer = (string)_tableStore.GetValue(treeIter, 3), - Version = (string)_tableStore.GetValue(treeIter, 4), - TimePlayed = ValueFormatUtils.ParseTimeSpan((string)_tableStore.GetValue(treeIter, 5)), - LastPlayed = ValueFormatUtils.ParseDateTime((string)_tableStore.GetValue(treeIter, 6)), - FileExtension = (string)_tableStore.GetValue(treeIter, 7), - FileSize = ValueFormatUtils.ParseFileSize((string)_tableStore.GetValue(treeIter, 8)), - Path = (string)_tableStore.GetValue(treeIter, 9), - ControlHolder = (BlitStruct)_tableStore.GetValue(treeIter, 10), - }; + string path = (string)_tableStore.GetValue(treeIter, 9); - RunApplication(application); + RunApplication(path); } private void VSyncStatus_Clicked(object sender, ButtonReleaseEventArgs args) @@ -1334,22 +1316,13 @@ namespace Ryujinx.Ui return; } - ApplicationData application = new() - { - Favorite = (bool)_tableStore.GetValue(treeIter, 0), - Name = ((string)_tableStore.GetValue(treeIter, 2)).Split('\n')[0], - Id = ulong.Parse(((string)_tableStore.GetValue(treeIter, 2)).Split('\n')[1], NumberStyles.HexNumber), - Developer = (string)_tableStore.GetValue(treeIter, 3), - Version = (string)_tableStore.GetValue(treeIter, 4), - TimePlayed = ValueFormatUtils.ParseTimeSpan((string)_tableStore.GetValue(treeIter, 5)), - LastPlayed = ValueFormatUtils.ParseDateTime((string)_tableStore.GetValue(treeIter, 6)), - FileExtension = (string)_tableStore.GetValue(treeIter, 7), - FileSize = ValueFormatUtils.ParseFileSize((string)_tableStore.GetValue(treeIter, 8)), - Path = (string)_tableStore.GetValue(treeIter, 9), - ControlHolder = (BlitStruct)_tableStore.GetValue(treeIter, 10), - }; + string titleFilePath = _tableStore.GetValue(treeIter, 9).ToString(); + string titleName = _tableStore.GetValue(treeIter, 2).ToString().Split("\n")[0]; + string titleId = _tableStore.GetValue(treeIter, 2).ToString().Split("\n")[1].ToLower(); - _ = new GameTableContextMenu(this, _virtualFileSystem, _accountManager, _libHacHorizonManager.RyujinxClient, application); + BlitStruct controlData = (BlitStruct)_tableStore.GetValue(treeIter, 10); + + _ = new GameTableContextMenu(this, _virtualFileSystem, _accountManager, _libHacHorizonManager.RyujinxClient, titleFilePath, titleName, titleId, controlData); } private void Load_Application_File(object sender, EventArgs args) @@ -1371,12 +1344,7 @@ namespace Ryujinx.Ui if (fileChooser.Run() == (int)ResponseType.Accept) { - ApplicationData applicationData = new() - { - Path = fileChooser.Filename, - }; - - RunApplication(applicationData); + RunApplication(fileChooser.Filename); } } @@ -1386,13 +1354,7 @@ namespace Ryujinx.Ui if (fileChooser.Run() == (int)ResponseType.Accept) { - ApplicationData applicationData = new() - { - Name = System.IO.Path.GetFileNameWithoutExtension(fileChooser.Filename), - Path = fileChooser.Filename, - }; - - RunApplication(applicationData); + RunApplication(fileChooser.Filename); } } @@ -1407,14 +1369,7 @@ namespace Ryujinx.Ui { string contentPath = _contentManager.GetInstalledContentPath(0x0100000000001009, StorageId.BuiltInSystem, NcaContentType.Program); - ApplicationData applicationData = new() - { - Name = "miiEdit", - Id = 0x0100000000001009ul, - Path = contentPath, - }; - - RunApplication(applicationData); + RunApplication(contentPath); } private void Open_Ryu_Folder(object sender, EventArgs args) @@ -1690,13 +1645,13 @@ namespace Ryujinx.Ui { _userChannelPersistence.ShouldRestart = false; - RunApplication(_currentApplicationData); + RunApplication(_currentEmulatedGamePath); } else { // otherwise, clear state. _userChannelPersistence = new UserChannelPersistence(); - _currentApplicationData = null; + _currentEmulatedGamePath = null; _actionMenu.Sensitive = false; _firmwareInstallFile.Sensitive = true; _firmwareInstallDirectory.Sensitive = true; @@ -1758,7 +1713,7 @@ namespace Ryujinx.Ui _emulationContext.Processes.ActiveApplication.ProgramId, _emulationContext.Processes.ActiveApplication.ApplicationControlProperties .Title[(int)_emulationContext.System.State.DesiredTitleLanguage].NameString.ToString(), - _currentApplicationData.Path); + _currentEmulatedGamePath); window.Destroyed += CheatWindow_Destroyed; window.Show(); diff --git a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs index 6903c9419..5af181b08 100644 --- a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs +++ b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs @@ -16,7 +16,6 @@ using Ryujinx.Common.Logging; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS; using Ryujinx.HLE.HOS.Services.Account.Acc; -using Ryujinx.HLE.Loaders.Processes.Extensions; using Ryujinx.Ui.App.Common; using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Common.Helper; @@ -24,6 +23,7 @@ using Ryujinx.Ui.Windows; using System; using System.Buffers; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Reflection; using System.Threading; @@ -36,13 +36,17 @@ namespace Ryujinx.Ui.Widgets private readonly VirtualFileSystem _virtualFileSystem; private readonly AccountManager _accountManager; private readonly HorizonClient _horizonClient; + private readonly BlitStruct _controlData; - private readonly ApplicationData _title; + private readonly string _titleFilePath; + private readonly string _titleName; + private readonly string _titleIdText; + private readonly ulong _titleId; private MessageDialog _dialog; private bool _cancel; - public GameTableContextMenu(MainWindow parent, VirtualFileSystem virtualFileSystem, AccountManager accountManager, HorizonClient horizonClient, ApplicationData applicationData) + public GameTableContextMenu(MainWindow parent, VirtualFileSystem virtualFileSystem, AccountManager accountManager, HorizonClient horizonClient, string titleFilePath, string titleName, string titleId, BlitStruct controlData) { _parent = parent; @@ -51,13 +55,23 @@ namespace Ryujinx.Ui.Widgets _virtualFileSystem = virtualFileSystem; _accountManager = accountManager; _horizonClient = horizonClient; - _title = applicationData; + _titleFilePath = titleFilePath; + _titleName = titleName; + _titleIdText = titleId; + _controlData = controlData; - _openSaveUserDirMenuItem.Sensitive = !Utilities.IsZeros(_title.ControlHolder.ByteSpan) && _title.ControlHolder.Value.UserAccountSaveDataSize > 0; - _openSaveDeviceDirMenuItem.Sensitive = !Utilities.IsZeros(_title.ControlHolder.ByteSpan) && _title.ControlHolder.Value.DeviceSaveDataSize > 0; - _openSaveBcatDirMenuItem.Sensitive = !Utilities.IsZeros(_title.ControlHolder.ByteSpan) && _title.ControlHolder.Value.BcatDeliveryCacheStorageSize > 0; + if (!ulong.TryParse(_titleIdText, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _titleId)) + { + GtkDialog.CreateErrorDialog("The selected game did not have a valid Title Id"); - string fileExt = System.IO.Path.GetExtension(_title.Path).ToLower(); + return; + } + + _openSaveUserDirMenuItem.Sensitive = !Utilities.IsZeros(controlData.ByteSpan) && controlData.Value.UserAccountSaveDataSize > 0; + _openSaveDeviceDirMenuItem.Sensitive = !Utilities.IsZeros(controlData.ByteSpan) && controlData.Value.DeviceSaveDataSize > 0; + _openSaveBcatDirMenuItem.Sensitive = !Utilities.IsZeros(controlData.ByteSpan) && controlData.Value.BcatDeliveryCacheStorageSize > 0; + + string fileExt = System.IO.Path.GetExtension(_titleFilePath).ToLower(); bool hasNca = fileExt == ".nca" || fileExt == ".nsp" || fileExt == ".pfs0" || fileExt == ".xci"; _extractRomFsMenuItem.Sensitive = hasNca; @@ -123,7 +137,7 @@ namespace Ryujinx.Ui.Widgets private void OpenSaveDir(in SaveDataFilter saveDataFilter) { - if (!TryFindSaveData(_title.Name, _title.Id, _title.ControlHolder, in saveDataFilter, out ulong saveDataId)) + if (!TryFindSaveData(_titleName, _titleId, _controlData, in saveDataFilter, out ulong saveDataId)) { return; } @@ -176,7 +190,7 @@ namespace Ryujinx.Ui.Widgets { Title = "Ryujinx - NCA Section Extractor", Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png"), - SecondaryText = $"Extracting {ncaSectionType} section from {System.IO.Path.GetFileName(_title.Path)}...", + SecondaryText = $"Extracting {ncaSectionType} section from {System.IO.Path.GetFileName(_titleFilePath)}...", WindowPosition = WindowPosition.Center, }; @@ -188,18 +202,18 @@ namespace Ryujinx.Ui.Widgets } }); - using FileStream file = new(_title.Path, FileMode.Open, FileAccess.Read); + using FileStream file = new(_titleFilePath, FileMode.Open, FileAccess.Read); Nca mainNca = null; Nca patchNca = null; - if ((System.IO.Path.GetExtension(_title.Path).ToLower() == ".nsp") || - (System.IO.Path.GetExtension(_title.Path).ToLower() == ".pfs0") || - (System.IO.Path.GetExtension(_title.Path).ToLower() == ".xci")) + if ((System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".nsp") || + (System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".pfs0") || + (System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".xci")) { IFileSystem pfs; - if (System.IO.Path.GetExtension(_title.Path).ToLower() == ".xci") + if (System.IO.Path.GetExtension(_titleFilePath) == ".xci") { Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage()); @@ -235,7 +249,7 @@ namespace Ryujinx.Ui.Widgets } } } - else if (System.IO.Path.GetExtension(_title.Path).ToLower() == ".nca") + else if (System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".nca") { mainNca = new Nca(_virtualFileSystem.KeySet, file.AsStorage()); } @@ -252,11 +266,7 @@ namespace Ryujinx.Ui.Widgets return; } - IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks - ? IntegrityCheckLevel.ErrorOnInvalid - : IntegrityCheckLevel.None; - - (Nca updatePatchNca, _) = mainNca.GetUpdateData(_virtualFileSystem, checkLevel, programIndex, out _); + (Nca updatePatchNca, _) = ApplicationLibrary.GetGameUpdateData(_virtualFileSystem, mainNca.Header.TitleId.ToString("x16"), programIndex, out _); if (updatePatchNca != null) { @@ -450,44 +460,44 @@ namespace Ryujinx.Ui.Widgets private void OpenSaveUserDir_Clicked(object sender, EventArgs args) { var userId = new LibHac.Fs.UserId((ulong)_accountManager.LastOpenedUser.UserId.High, (ulong)_accountManager.LastOpenedUser.UserId.Low); - var saveDataFilter = SaveDataFilter.Make(_title.Id, saveType: default, userId, saveDataId: default, index: default); + var saveDataFilter = SaveDataFilter.Make(_titleId, saveType: default, userId, saveDataId: default, index: default); OpenSaveDir(in saveDataFilter); } private void OpenSaveDeviceDir_Clicked(object sender, EventArgs args) { - var saveDataFilter = SaveDataFilter.Make(_title.Id, SaveDataType.Device, userId: default, saveDataId: default, index: default); + var saveDataFilter = SaveDataFilter.Make(_titleId, SaveDataType.Device, userId: default, saveDataId: default, index: default); OpenSaveDir(in saveDataFilter); } private void OpenSaveBcatDir_Clicked(object sender, EventArgs args) { - var saveDataFilter = SaveDataFilter.Make(_title.Id, SaveDataType.Bcat, userId: default, saveDataId: default, index: default); + var saveDataFilter = SaveDataFilter.Make(_titleId, SaveDataType.Bcat, userId: default, saveDataId: default, index: default); OpenSaveDir(in saveDataFilter); } private void ManageTitleUpdates_Clicked(object sender, EventArgs args) { - new TitleUpdateWindow(_parent, _virtualFileSystem, _title).Show(); + new TitleUpdateWindow(_parent, _virtualFileSystem, _titleIdText, _titleName).Show(); } private void ManageDlc_Clicked(object sender, EventArgs args) { - new DlcWindow(_virtualFileSystem, _title.IdString, _title).Show(); + new DlcWindow(_virtualFileSystem, _titleIdText, _titleName).Show(); } private void ManageCheats_Clicked(object sender, EventArgs args) { - new CheatWindow(_virtualFileSystem, _title.Id, _title.Name, _title.Path).Show(); + new CheatWindow(_virtualFileSystem, _titleId, _titleName, _titleFilePath).Show(); } private void OpenTitleModDir_Clicked(object sender, EventArgs args) { string modsBasePath = ModLoader.GetModsBasePath(); - string titleModsPath = ModLoader.GetTitleDir(modsBasePath, _title.IdString); + string titleModsPath = ModLoader.GetTitleDir(modsBasePath, _titleIdText); OpenHelper.OpenFolder(titleModsPath); } @@ -495,7 +505,7 @@ namespace Ryujinx.Ui.Widgets private void OpenTitleSdModDir_Clicked(object sender, EventArgs args) { string sdModsBasePath = ModLoader.GetSdModsBasePath(); - string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, _title.IdString); + string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, _titleIdText); OpenHelper.OpenFolder(titleModsPath); } @@ -517,7 +527,7 @@ namespace Ryujinx.Ui.Widgets private void OpenPtcDir_Clicked(object sender, EventArgs args) { - string ptcDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _title.IdString, "cache", "cpu"); + string ptcDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "cpu"); string mainPath = System.IO.Path.Combine(ptcDir, "0"); string backupPath = System.IO.Path.Combine(ptcDir, "1"); @@ -534,7 +544,7 @@ namespace Ryujinx.Ui.Widgets private void OpenShaderCacheDir_Clicked(object sender, EventArgs args) { - string shaderCacheDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _title.IdString, "cache", "shader"); + string shaderCacheDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "shader"); if (!Directory.Exists(shaderCacheDir)) { @@ -546,10 +556,10 @@ namespace Ryujinx.Ui.Widgets private void PurgePtcCache_Clicked(object sender, EventArgs args) { - DirectoryInfo mainDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _title.IdString, "cache", "cpu", "0")); - DirectoryInfo backupDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _title.IdString, "cache", "cpu", "1")); + DirectoryInfo mainDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "cpu", "0")); + DirectoryInfo backupDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "cpu", "1")); - MessageDialog warningDialog = GtkDialog.CreateConfirmationDialog("Warning", $"You are about to queue a PPTC rebuild on the next boot of:\n\n{_title.Name}\n\nAre you sure you want to proceed?"); + MessageDialog warningDialog = GtkDialog.CreateConfirmationDialog("Warning", $"You are about to queue a PPTC rebuild on the next boot of:\n\n{_titleName}\n\nAre you sure you want to proceed?"); List cacheFiles = new(); @@ -583,9 +593,9 @@ namespace Ryujinx.Ui.Widgets private void PurgeShaderCache_Clicked(object sender, EventArgs args) { - DirectoryInfo shaderCacheDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _title.IdString, "cache", "shader")); + DirectoryInfo shaderCacheDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "shader")); - using MessageDialog warningDialog = GtkDialog.CreateConfirmationDialog("Warning", $"You are about to delete the shader cache for :\n\n{_title.Name}\n\nAre you sure you want to proceed?"); + using MessageDialog warningDialog = GtkDialog.CreateConfirmationDialog("Warning", $"You are about to delete the shader cache for :\n\n{_titleName}\n\nAre you sure you want to proceed?"); List oldCacheDirectories = new(); List newCacheFiles = new(); @@ -627,11 +637,8 @@ namespace Ryujinx.Ui.Widgets private void CreateShortcut_Clicked(object sender, EventArgs args) { - IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks - ? IntegrityCheckLevel.ErrorOnInvalid - : IntegrityCheckLevel.None; - byte[] appIcon = new ApplicationLibrary(_virtualFileSystem, checkLevel).GetApplicationIcon(_title.Path, ConfigurationState.Instance.System.Language, _title.Id); - ShortcutHelper.CreateAppShortcut(_title.Path, _title.Name, _title.IdString, appIcon); + byte[] appIcon = new ApplicationLibrary(_virtualFileSystem).GetApplicationIcon(_titleFilePath, ConfigurationState.Instance.System.Language); + ShortcutHelper.CreateAppShortcut(_titleFilePath, _titleName, _titleIdText, appIcon); } } } diff --git a/src/Ryujinx/Ui/Windows/CheatWindow.cs b/src/Ryujinx/Ui/Windows/CheatWindow.cs index 9bbae1c6c..1eca732b2 100644 --- a/src/Ryujinx/Ui/Windows/CheatWindow.cs +++ b/src/Ryujinx/Ui/Windows/CheatWindow.cs @@ -1,9 +1,7 @@ using Gtk; -using LibHac.Tools.FsSystem; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS; using Ryujinx.Ui.App.Common; -using Ryujinx.Ui.Common.Configuration; using System; using System.Collections.Generic; using System.IO; @@ -29,13 +27,8 @@ namespace Ryujinx.Ui.Windows private CheatWindow(Builder builder, VirtualFileSystem virtualFileSystem, ulong titleId, string titleName, string titlePath) : base(builder.GetRawOwnedObject("_cheatWindow")) { builder.Autoconnect(this); - - IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks - ? IntegrityCheckLevel.ErrorOnInvalid - : IntegrityCheckLevel.None; - _baseTitleInfoLabel.Text = $"Cheats Available for {titleName} [{titleId:X16}]"; - _buildIdTextView.Buffer.Text = $"BuildId: {ApplicationData.GetBuildId(virtualFileSystem, checkLevel, titlePath)}"; + _buildIdTextView.Buffer.Text = $"BuildId: {ApplicationData.GetApplicationBuildId(virtualFileSystem, titlePath)}"; string modsBasePath = ModLoader.GetModsBasePath(); string titleModsPath = ModLoader.GetTitleDir(modsBasePath, titleId.ToString("X16")); diff --git a/src/Ryujinx/Ui/Windows/DlcWindow.cs b/src/Ryujinx/Ui/Windows/DlcWindow.cs index dbffc4209..9f7179467 100644 --- a/src/Ryujinx/Ui/Windows/DlcWindow.cs +++ b/src/Ryujinx/Ui/Windows/DlcWindow.cs @@ -9,12 +9,9 @@ using LibHac.Tools.FsSystem.NcaUtils; using Ryujinx.Common.Configuration; using Ryujinx.Common.Utilities; using Ryujinx.HLE.FileSystem; -using Ryujinx.HLE.Loaders.Processes.Extensions; -using Ryujinx.Ui.App.Common; using Ryujinx.Ui.Widgets; using System; using System.Collections.Generic; -using System.Globalization; using System.IO; using GUI = Gtk.Builder.ObjectAttribute; @@ -23,7 +20,7 @@ namespace Ryujinx.Ui.Windows public class DlcWindow : Window { private readonly VirtualFileSystem _virtualFileSystem; - private readonly string _applicationId; + private readonly string _titleId; private readonly string _dlcJsonPath; private readonly List _dlcContainerList; @@ -35,16 +32,16 @@ namespace Ryujinx.Ui.Windows [GUI] TreeSelection _dlcTreeSelection; #pragma warning restore CS0649, IDE0044 - public DlcWindow(VirtualFileSystem virtualFileSystem, string titleId, ApplicationData title) : this(new Builder("Ryujinx.Ui.Windows.DlcWindow.glade"), virtualFileSystem, titleId, title) { } + public DlcWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName) : this(new Builder("Ryujinx.Ui.Windows.DlcWindow.glade"), virtualFileSystem, titleId, titleName) { } - private DlcWindow(Builder builder, VirtualFileSystem virtualFileSystem, string applicationId, ApplicationData title) : base(builder.GetRawOwnedObject("_dlcWindow")) + private DlcWindow(Builder builder, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : base(builder.GetRawOwnedObject("_dlcWindow")) { builder.Autoconnect(this); - _applicationId = applicationId; + _titleId = titleId; _virtualFileSystem = virtualFileSystem; - _dlcJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, _applicationId, "dlc.json"); - _baseTitleInfoLabel.Text = $"DLC Available for {title.Name} [{applicationId.ToUpper()}]"; + _dlcJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleId, "dlc.json"); + _baseTitleInfoLabel.Text = $"DLC Available for {titleName} [{titleId.ToUpper()}]"; try { @@ -75,12 +72,9 @@ namespace Ryujinx.Ui.Windows }; _dlcTreeView.AppendColumn("Enabled", enableToggle, "active", 0); - _dlcTreeView.AppendColumn("ApplicationId", new CellRendererText(), "text", 1); + _dlcTreeView.AppendColumn("TitleId", new CellRendererText(), "text", 1); _dlcTreeView.AppendColumn("Path", new CellRendererText(), "text", 2); - // NOTE: Try to load downloadable contents from PFS first. - AddDlc(title.Path, true); - foreach (DownloadableContentContainer dlcContainer in _dlcContainerList) { if (File.Exists(dlcContainer.ContainerPath)) @@ -95,10 +89,7 @@ namespace Ryujinx.Ui.Windows using FileStream containerFile = File.OpenRead(dlcContainer.ContainerPath); PartitionFileSystem pfs = new(); - if (pfs.Initialize(containerFile.AsStorage()).IsFailure()) - { - continue; - } + pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure(); _virtualFileSystem.ImportTickets(pfs); @@ -137,57 +128,6 @@ namespace Ryujinx.Ui.Windows return null; } - private void AddDlc(string path, bool ignoreNotFound = false) - { - if (!File.Exists(path)) - { - return; - } - - using FileStream containerFile = File.OpenRead(path); - - PartitionFileSystem pfs = new(); - pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure(); - - bool containsDlc = false; - - _virtualFileSystem.ImportTickets(pfs); - - TreeIter? parentIter = null; - - foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) - { - using var ncaFile = new UniqueRef(); - - pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - - Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), path); - - if (nca == null) - { - continue; - } - - if (nca.Header.ContentType == NcaContentType.PublicData) - { - if (nca.GetProgramIdBase() != (ulong.Parse(_applicationId, NumberStyles.HexNumber) & ~0x1FFFUL)) - { - break; - } - - parentIter ??= ((TreeStore)_dlcTreeView.Model).AppendValues(true, "", path); - - ((TreeStore)_dlcTreeView.Model).AppendValues(parentIter.Value, true, nca.Header.TitleId.ToString("X16"), fileEntry.FullPath); - containsDlc = true; - } - } - - if (!containsDlc && !ignoreNotFound) - { - GtkDialog.CreateErrorDialog("The specified file does not contain DLC for the selected title!"); - } - } - private void AddButton_Clicked(object sender, EventArgs args) { FileChooserNative fileChooser = new("Select DLC files", this, FileChooserAction.Open, "Add", "Cancel") @@ -207,7 +147,52 @@ namespace Ryujinx.Ui.Windows { foreach (string containerPath in fileChooser.Filenames) { - AddDlc(containerPath); + if (!File.Exists(containerPath)) + { + return; + } + + using FileStream containerFile = File.OpenRead(containerPath); + + PartitionFileSystem pfs = new(); + pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure(); + bool containsDlc = false; + + _virtualFileSystem.ImportTickets(pfs); + + TreeIter? parentIter = null; + + foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) + { + using var ncaFile = new UniqueRef(); + + pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), containerPath); + + if (nca == null) + { + continue; + } + + if (nca.Header.ContentType == NcaContentType.PublicData) + { + if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000).ToString("x16") != _titleId) + { + break; + } + + parentIter ??= ((TreeStore)_dlcTreeView.Model).AppendValues(true, "", containerPath); + + ((TreeStore)_dlcTreeView.Model).AppendValues(parentIter.Value, true, nca.Header.TitleId.ToString("X16"), fileEntry.FullPath); + containsDlc = true; + } + } + + if (!containsDlc) + { + GtkDialog.CreateErrorDialog("The specified file does not contain DLC for the selected title!"); + } } } diff --git a/src/Ryujinx/Ui/Windows/TitleUpdateWindow.cs b/src/Ryujinx/Ui/Windows/TitleUpdateWindow.cs index 2f7f14f1f..51918eeab 100644 --- a/src/Ryujinx/Ui/Windows/TitleUpdateWindow.cs +++ b/src/Ryujinx/Ui/Windows/TitleUpdateWindow.cs @@ -4,15 +4,12 @@ using LibHac.Fs; using LibHac.Fs.Fsa; using LibHac.FsSystem; using LibHac.Ns; -using LibHac.Tools.Fs; using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.NcaUtils; using Ryujinx.Common.Configuration; using Ryujinx.Common.Utilities; using Ryujinx.HLE.FileSystem; -using Ryujinx.HLE.Loaders.Processes.Extensions; using Ryujinx.Ui.App.Common; -using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Widgets; using System; using System.Collections.Generic; @@ -27,7 +24,7 @@ namespace Ryujinx.Ui.Windows { private readonly MainWindow _parent; private readonly VirtualFileSystem _virtualFileSystem; - private readonly ApplicationData _title; + private readonly string _titleId; private readonly string _updateJsonPath; private TitleUpdateMetadata _titleUpdateWindowData; @@ -41,17 +38,17 @@ namespace Ryujinx.Ui.Windows [GUI] RadioButton _noUpdateRadioButton; #pragma warning restore CS0649, IDE0044 - public TitleUpdateWindow(MainWindow parent, VirtualFileSystem virtualFileSystem, ApplicationData applicationData) : this(new Builder("Ryujinx.Ui.Windows.TitleUpdateWindow.glade"), parent, virtualFileSystem, applicationData) { } + public TitleUpdateWindow(MainWindow parent, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : this(new Builder("Ryujinx.Ui.Windows.TitleUpdateWindow.glade"), parent, virtualFileSystem, titleId, titleName) { } - private TitleUpdateWindow(Builder builder, MainWindow parent, VirtualFileSystem virtualFileSystem, ApplicationData applicationData) : base(builder.GetRawOwnedObject("_titleUpdateWindow")) + private TitleUpdateWindow(Builder builder, MainWindow parent, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : base(builder.GetRawOwnedObject("_titleUpdateWindow")) { _parent = parent; builder.Autoconnect(this); - _title = applicationData; + _titleId = titleId; _virtualFileSystem = virtualFileSystem; - _updateJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, applicationData.IdString, "updates.json"); + _updateJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleId, "updates.json"); _radioButtonToPathDictionary = new Dictionary(); try @@ -67,10 +64,7 @@ namespace Ryujinx.Ui.Windows }; } - _baseTitleInfoLabel.Text = $"Updates Available for {applicationData.Name} [{applicationData.IdString}]"; - - // Try to get updates from PFS first - AddUpdate(_title.Path, true); + _baseTitleInfoLabel.Text = $"Updates Available for {titleName} [{titleId.ToUpper()}]"; foreach (string path in _titleUpdateWindowData.Paths) { @@ -90,41 +84,18 @@ namespace Ryujinx.Ui.Windows } } - private void AddUpdate(string path, bool ignoreNotFound = false) + private void AddUpdate(string path) { if (File.Exists(path)) { - IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks - ? IntegrityCheckLevel.ErrorOnInvalid - : IntegrityCheckLevel.None; - using FileStream file = new(path, FileMode.Open, FileAccess.Read); - IFileSystem pfs; + PartitionFileSystem nsp = new(); + nsp.Initialize(file.AsStorage()).ThrowIfFailure(); try { - if (System.IO.Path.GetExtension(path).ToLower() == ".xci") - { - pfs = new Xci(_virtualFileSystem.KeySet, file.AsStorage()).OpenPartition(XciPartitionType.Secure); - } - else - { - var pfsTemp = new PartitionFileSystem(); - pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure(); - pfs = pfsTemp; - } - - Dictionary updates = pfs.GetUpdateData(_virtualFileSystem, checkLevel); - - Nca patchNca = null; - Nca controlNca = null; - - if (updates.TryGetValue(_title.Id, out ContentCollection update)) - { - patchNca = update.GetNcaByType(_virtualFileSystem.KeySet, LibHac.Ncm.ContentType.Program); - controlNca = update.GetNcaByType(_virtualFileSystem.KeySet, LibHac.Ncm.ContentType.Control); - } + (Nca patchNca, Nca controlNca) = ApplicationLibrary.GetGameUpdateDataFromPartition(_virtualFileSystem, nsp, _titleId, 0); if (controlNca != null && patchNca != null) { @@ -135,14 +106,7 @@ namespace Ryujinx.Ui.Windows controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(ref nacpFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure(); nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure(); - string radioLabel = $"Version {controlData.DisplayVersionString.ToString()} - {path}"; - - if (System.IO.Path.GetExtension(path).ToLower() == ".xci") - { - radioLabel = "Bundled: " + radioLabel; - } - - RadioButton radioButton = new(radioLabel); + RadioButton radioButton = new($"Version {controlData.DisplayVersionString.ToString()} - {path}"); radioButton.JoinGroup(_noUpdateRadioButton); _availableUpdatesBox.Add(radioButton); @@ -153,10 +117,7 @@ namespace Ryujinx.Ui.Windows } else { - if (!ignoreNotFound) - { - GtkDialog.CreateErrorDialog("The specified file does not contain an update for the selected title!"); - } + GtkDialog.CreateErrorDialog("The specified file does not contain an update for the selected title!"); } } catch (Exception exception) From e6e58389164fe7cb6894dfd6e8ac1cc7d9ec7d11 Mon Sep 17 00:00:00 2001 From: gdkchan Date: Mon, 13 Nov 2023 18:07:05 -0300 Subject: [PATCH 11/23] Do not set modified flag again if texture was not modified (#5909) * Do not set modified flag again if texture was not modified * Formatting * Fix copy dep regression --- src/Ryujinx.Graphics.Gpu/Image/Texture.cs | 18 ++++++-------- .../Image/TextureGroup.cs | 6 ++--- .../Image/TextureManager.cs | 24 +++++++++++++++---- 3 files changed, 29 insertions(+), 19 deletions(-) diff --git a/src/Ryujinx.Graphics.Gpu/Image/Texture.cs b/src/Ryujinx.Graphics.Gpu/Image/Texture.cs index dca6263aa..326272e7c 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/Texture.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/Texture.cs @@ -102,9 +102,9 @@ namespace Ryujinx.Graphics.Gpu.Image public bool AlwaysFlushOnOverlap { get; private set; } /// - /// Indicates that the texture was fully unmapped since the modified flag was set, and flushes should be ignored until it is modified again. + /// Indicates that the texture was modified since the last time it was flushed. /// - public bool FlushStale { get; private set; } + public bool ModifiedSinceLastFlush { get; set; } /// /// Increments when the host texture is swapped, or when the texture is removed from all pools. @@ -1417,7 +1417,6 @@ namespace Ryujinx.Graphics.Gpu.Image /// public void SignalModified() { - FlushStale = false; _scaledSetScore = Math.Max(0, _scaledSetScore - 1); if (_modifiedStale || Group.HasCopyDependencies) @@ -1438,14 +1437,17 @@ namespace Ryujinx.Graphics.Gpu.Image { if (bound) { - FlushStale = false; _scaledSetScore = Math.Max(0, _scaledSetScore - 1); } if (_modifiedStale || Group.HasCopyDependencies || Group.HasFlushBuffer) { _modifiedStale = false; - Group.SignalModifying(this, bound); + + if (bound || ModifiedSinceLastFlush || Group.HasCopyDependencies || Group.HasFlushBuffer) + { + Group.SignalModifying(this, bound); + } } _physicalMemory.TextureCache.Lift(this); @@ -1703,12 +1705,6 @@ namespace Ryujinx.Graphics.Gpu.Image /// The range of memory being unmapped public void Unmapped(MultiRange unmapRange) { - if (unmapRange.Contains(Range)) - { - // If this is a full unmap, prevent flushes until the texture is mapped again. - FlushStale = true; - } - ChangedMapping = true; if (Group.Storage == this) diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs index 21d7939ad..d7de8a3cd 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs @@ -1660,13 +1660,13 @@ namespace Ryujinx.Graphics.Gpu.Image } // If size is zero, we have nothing to flush. - // If the flush is stale, we should ignore it because the texture was unmapped since the modified - // flag was set, and flushing it is not safe anymore as the GPU might no longer own the memory. - if (size == 0 || Storage.FlushStale) + if (size == 0) { return; } + Storage.ModifiedSinceLastFlush = false; + // There is a small gap here where the action is removed but _actionRegistered is still 1. // In this case it will skip registering the action, but here we are already handling it, // so there shouldn't be any issue as it's the same handler for all actions. diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureManager.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureManager.cs index ed181640a..e9f58314f 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureManager.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureManager.cs @@ -413,21 +413,35 @@ namespace Ryujinx.Graphics.Gpu.Image { bool anyChanged = false; - if (_rtHostDs != _rtDepthStencil?.HostTexture) - { - _rtHostDs = _rtDepthStencil?.HostTexture; + Texture dsTexture = _rtDepthStencil; + ITexture hostDsTexture = null; + if (dsTexture != null) + { + hostDsTexture = dsTexture.HostTexture; + dsTexture.ModifiedSinceLastFlush = true; + } + + if (_rtHostDs != hostDsTexture) + { + _rtHostDs = hostDsTexture; anyChanged = true; } for (int index = 0; index < _rtColors.Length; index++) { - ITexture hostTexture = _rtColors[index]?.HostTexture; + Texture texture = _rtColors[index]; + ITexture hostTexture = null; + + if (texture != null) + { + hostTexture = texture.HostTexture; + texture.ModifiedSinceLastFlush = true; + } if (_rtHostColors[index] != hostTexture) { _rtHostColors[index] = hostTexture; - anyChanged = true; } } From 6bce46621c9952bdc697c1977cb866b48cbfad58 Mon Sep 17 00:00:00 2001 From: shinra-electric <50119606+shinra-electric@users.noreply.github.com> Date: Tue, 14 Nov 2023 21:20:33 +0100 Subject: [PATCH 12/23] Change minimum OS to macOS 12 in Info.plist (#5925) This should prevent the app from opening on macOS 11 and lower, informing the user that their OS is unsupported. --- distribution/macos/Info.plist | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/distribution/macos/Info.plist b/distribution/macos/Info.plist index 6e068ba2d..51c71eaa1 100644 --- a/distribution/macos/Info.plist +++ b/distribution/macos/Info.plist @@ -43,7 +43,7 @@ LSApplicationCategoryType public.app-category.games LSMinimumSystemVersion - 11.0 + 12.0 UTExportedTypeDeclarations @@ -155,4 +155,4 @@ 200000 - \ No newline at end of file + From 1329c47ea46d3fa152dabe02cff82db84429545a Mon Sep 17 00:00:00 2001 From: gdkchan Date: Tue, 14 Nov 2023 22:24:54 -0300 Subject: [PATCH 13/23] Work around issue apparently caused by 5909 (#5926) --- src/Ryujinx.Graphics.Gpu/Image/Texture.cs | 6 +----- src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs | 5 +++-- src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs | 12 ++++++++++-- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/Ryujinx.Graphics.Gpu/Image/Texture.cs b/src/Ryujinx.Graphics.Gpu/Image/Texture.cs index 326272e7c..04c2b615f 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/Texture.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/Texture.cs @@ -1443,11 +1443,7 @@ namespace Ryujinx.Graphics.Gpu.Image if (_modifiedStale || Group.HasCopyDependencies || Group.HasFlushBuffer) { _modifiedStale = false; - - if (bound || ModifiedSinceLastFlush || Group.HasCopyDependencies || Group.HasFlushBuffer) - { - Group.SignalModifying(this, bound); - } + Group.SignalModifying(this, bound, bound || ModifiedSinceLastFlush || Group.HasCopyDependencies || Group.HasFlushBuffer); } _physicalMemory.TextureCache.Lift(this); diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs index d7de8a3cd..e828cb9f1 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs @@ -709,7 +709,8 @@ namespace Ryujinx.Graphics.Gpu.Image /// /// The texture that has been modified /// True if this texture is being bound, false if unbound - public void SignalModifying(Texture texture, bool bound) + /// Indicates if the modified flag should be set + public void SignalModifying(Texture texture, bool bound, bool setModified) { ModifiedSequence = _context.GetModifiedSequence(); @@ -721,7 +722,7 @@ namespace Ryujinx.Graphics.Gpu.Image { TextureGroupHandle group = _handles[baseHandle + i]; - group.SignalModifying(bound, _context); + group.SignalModifying(bound, _context, setModified); } }); } diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs index 84171c7a9..717792e04 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs @@ -304,9 +304,17 @@ namespace Ryujinx.Graphics.Gpu.Image /// /// True if this handle is being bound, false if unbound /// The GPU context to register a sync action on - public void SignalModifying(bool bound, GpuContext context) + /// Indicates if the modified flag should be set + public void SignalModifying(bool bound, GpuContext context, bool setModified) { - SignalModified(context); + if (setModified) + { + SignalModified(context); + } + else + { + RegisterSync(context); + } if (!bound && _syncActionRegistered && NextSyncCopies()) { From 5b3662b793b3a34acc12c45c3c1691b7302d4b1d Mon Sep 17 00:00:00 2001 From: gdkchan Date: Tue, 14 Nov 2023 23:24:42 -0300 Subject: [PATCH 14/23] Disable DMA GPU copy for block linear to linear copies (#5927) * Disable DMA GPU copy for block linear to linear copies * Simplify check * PR feedback --- src/Ryujinx.Graphics.Gpu/Engine/Dma/DmaClass.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Dma/DmaClass.cs b/src/Ryujinx.Graphics.Gpu/Engine/Dma/DmaClass.cs index e6557780b..199cc423e 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Dma/DmaClass.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Dma/DmaClass.cs @@ -279,7 +279,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Dma bool completeSource = IsTextureCopyComplete(src, srcLinear, srcBpp, srcStride, xCount, yCount); bool completeDest = IsTextureCopyComplete(dst, dstLinear, dstBpp, dstStride, xCount, yCount); - if (completeSource && completeDest) + // Try to set the texture data directly, + // but only if we are doing a complete copy, + // and not for block linear to linear copies, since those are typically accessed from the CPU. + + if (completeSource && completeDest && !(dstLinear && !srcLinear)) { var target = memoryManager.Physical.TextureCache.FindTexture( memoryManager, From 29e192f241136ce910071ff4fdedda5bd1d9b838 Mon Sep 17 00:00:00 2001 From: Zoltan Csizmadia Date: Wed, 15 Nov 2023 10:41:31 -0600 Subject: [PATCH 15/23] Migrate to .NET 8 (#5887) * Change TargetFramework to net8.0 * Disable info messages * Fix warings * Disable additional analyzer messages * Fix typo * Add whitespace * Fix ref vs in warnings * Use explicit [In] on array parameters * No need to guard Remove with Contains * Use 'ArgumentOutOfRangeException.ThrowIf...' instead of explicitly throwing a new exception instance * Bump .NET SDK version * Enable JsonSerializerIsReflectionEnabledByDefault * Use 8.0.100 GA release * Bump System package versions --------- Co-authored-by: Zoltan Csizmadia --- .editorconfig | 23 +++++++++++++++++ .github/workflows/build.yml | 4 +-- .github/workflows/release.yml | 2 +- Directory.Packages.props | 10 ++++---- README.md | 2 +- global.json | 4 +-- src/ARMeilleure/ARMeilleure.csproj | 2 +- .../Ryujinx.Audio.Backends.OpenAL.csproj | 2 +- .../Ryujinx.Audio.Backends.SDL2.csproj | 2 +- .../Ryujinx.Audio.Backends.SoundIo.csproj | 8 +++--- .../Renderer/Utils/SpanIOHelper.cs | 4 +-- src/Ryujinx.Audio/Ryujinx.Audio.csproj | 2 +- src/Ryujinx.Ava/Ryujinx.Ava.csproj | 16 +++++++++--- .../UI/Helpers/Win32NativeInterop.cs | 2 +- src/Ryujinx.Common/Ryujinx.Common.csproj | 2 +- src/Ryujinx.Cpu/Ryujinx.Cpu.csproj | 2 +- .../Ryujinx.Graphics.Device.csproj | 2 +- .../Ryujinx.Graphics.GAL.csproj | 2 +- .../Ryujinx.Graphics.Gpu.csproj | 2 +- .../Synchronization/SynchronizationManager.cs | 25 ++++--------------- .../Ryujinx.Graphics.Host1x.csproj | 2 +- .../Ryujinx.Graphics.Nvdec.FFmpeg.csproj | 2 +- .../Ryujinx.Graphics.Nvdec.Vp9.csproj | 2 +- .../Ryujinx.Graphics.Nvdec.csproj | 2 +- .../Ryujinx.Graphics.OpenGL.csproj | 2 +- .../IntermediateRepresentation/PhiNode.cs | 5 +--- .../Ryujinx.Graphics.Shader.csproj | 2 +- .../Ryujinx.Graphics.Texture.csproj | 2 +- .../Utils/RgbaColor32.cs | 9 ++++--- .../Ryujinx.Graphics.Vic.csproj | 2 +- .../Ryujinx.Graphics.Video.csproj | 2 +- src/Ryujinx.Graphics.Vulkan/PipelineBase.cs | 2 +- .../Ryujinx.Graphics.Vulkan.csproj | 2 +- .../VulkanException.cs | 4 --- .../ServiceNotImplementedException.cs | 2 -- src/Ryujinx.HLE/FileSystem/ContentManager.cs | 5 +--- .../NvHostCtrl/Types/NvHostSyncPt.cs | 5 +--- .../Services/Sockets/Bsd/Types/BsdMsgHdr.cs | 12 ++++----- .../Services/Sockets/Sfdnsres/IResolver.cs | 2 +- .../Sfdnsres/Types/AddrInfoSerialized.cs | 6 ++--- .../SslService/SslManagedSocketConnection.cs | 4 +-- src/Ryujinx.HLE/Ryujinx.HLE.csproj | 2 +- .../Ryujinx.Headless.SDL2.csproj | 8 +++--- .../Ryujinx.Horizon.Common.csproj | 2 +- src/Ryujinx.Horizon/Ryujinx.Horizon.csproj | 2 +- .../Ryujinx.Input.SDL2.csproj | 2 +- src/Ryujinx.Input/Ryujinx.Input.csproj | 2 +- src/Ryujinx.Memory/Range/MultiRange.cs | 10 ++------ src/Ryujinx.Memory/Ryujinx.Memory.csproj | 2 +- .../Ryujinx.SDL2.Common.csproj | 2 +- .../Ryujinx.ShaderTools.csproj | 2 +- .../Ryujinx.Tests.Memory.csproj | 2 +- .../Ryujinx.Tests.Unicorn.csproj | 2 +- src/Ryujinx.Tests/Ryujinx.Tests.csproj | 2 +- .../Ryujinx.Ui.Common.csproj | 2 +- src/Ryujinx/Ryujinx.csproj | 6 ++--- src/Spv.Generator/Spv.Generator.csproj | 2 +- 57 files changed, 121 insertions(+), 123 deletions(-) diff --git a/.editorconfig b/.editorconfig index 9d695c7fb..db08c67e2 100644 --- a/.editorconfig +++ b/.editorconfig @@ -233,6 +233,29 @@ dotnet_naming_style.IPascalCase.required_suffix = dotnet_naming_style.IPascalCase.word_separator = dotnet_naming_style.IPascalCase.capitalization = pascal_case +# TODO: +# .NET 8 migration (new warnings are caused by the NET 8 C# compiler and analyzer) +# The following info messages might need to be fixed in the source code instead of hiding the actual message +# Without the following lines, dotnet format would fail +# Disable "Collection initialization can be simplified" +dotnet_diagnostic.IDE0028.severity = none +dotnet_diagnostic.IDE0300.severity = none +dotnet_diagnostic.IDE0301.severity = none +dotnet_diagnostic.IDE0302.severity = none +dotnet_diagnostic.IDE0305.severity = none +# Disable "'new' expression can be simplified" +dotnet_diagnostic.IDE0090.severity = none +# Disable "Use primary constructor" +dotnet_diagnostic.IDE0290.severity = none +# Disable "Member '' does not access instance data and can be marked as static" +dotnet_diagnostic.CA1822.severity = none +# Disable "Change type of field '' from '' to '' for improved performance" +dotnet_diagnostic.CA1859.severity = none +# Disable "Prefer 'static readonly' fields over constant array arguments if the called method is called repeatedly and is not mutating the passed array" +dotnet_diagnostic.CA1861.severity = none +# Disable "Prefer using 'string.Equals(string, StringComparison)' to perform a case-insensitive comparison, but keep in mind that this might cause subtle changes in behavior, so make sure to conduct thorough testing after applying the suggestion, or if culturally sensitive comparison is not required, consider using 'StringComparison.OrdinalIgnoreCase'" +dotnet_diagnostic.CA1862.severity = none + [src/Ryujinx.HLE/HOS/Services/**.cs] # Disable "mark members as static" rule for services dotnet_diagnostic.CA1822.severity = none diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 16058d9f8..7d46adc2c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,7 +30,7 @@ jobs: - os: windows-latest OS_NAME: Windows x64 - DOTNET_RUNTIME_IDENTIFIER: win10-x64 + DOTNET_RUNTIME_IDENTIFIER: win-x64 RELEASE_ZIP_OS_NAME: win_x64 fail-fast: false @@ -155,4 +155,4 @@ jobs: with: name: sdl2-ryujinx-headless-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-macos_universal path: "publish_headless/*.tar.gz" - if: github.event_name == 'pull_request' \ No newline at end of file + if: github.event_name == 'pull_request' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 008561f9c..4dc1d091d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -59,7 +59,7 @@ jobs: - os: windows-latest OS_NAME: Windows x64 - DOTNET_RUNTIME_IDENTIFIER: win10-x64 + DOTNET_RUNTIME_IDENTIFIER: win-x64 RELEASE_ZIP_OS_NAME: win_x64 steps: - uses: actions/checkout@v4 diff --git a/Directory.Packages.props b/Directory.Packages.props index 009430f92..3d8aac1a7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -21,7 +21,7 @@ - + @@ -45,10 +45,10 @@ - - - - + + + + diff --git a/README.md b/README.md index b2a6646f5..b2f95cc1f 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ The latest automatic build for Windows, macOS, and Linux can be found on the [Of If you wish to build the emulator yourself, follow these steps: ### Step 1 -Install the X64 version of [.NET 7.0 (or higher) SDK](https://dotnet.microsoft.com/download/dotnet/7.0). +Install the X64 version of [.NET 8.0 (or higher) SDK](https://dotnet.microsoft.com/download/dotnet/8.0). ### Step 2 Either use `git clone https://github.com/Ryujinx/Ryujinx` on the command line to clone the repository or use Code --> Download zip button to get the files. diff --git a/global.json b/global.json index 39ccef0d0..391ba3c2a 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "7.0.200", + "version": "8.0.100", "rollForward": "latestFeature" } -} \ No newline at end of file +} diff --git a/src/ARMeilleure/ARMeilleure.csproj b/src/ARMeilleure/ARMeilleure.csproj index fa5551154..550e50c26 100644 --- a/src/ARMeilleure/ARMeilleure.csproj +++ b/src/ARMeilleure/ARMeilleure.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 true diff --git a/src/Ryujinx.Audio.Backends.OpenAL/Ryujinx.Audio.Backends.OpenAL.csproj b/src/Ryujinx.Audio.Backends.OpenAL/Ryujinx.Audio.Backends.OpenAL.csproj index 115a37601..3863e4439 100644 --- a/src/Ryujinx.Audio.Backends.OpenAL/Ryujinx.Audio.Backends.OpenAL.csproj +++ b/src/Ryujinx.Audio.Backends.OpenAL/Ryujinx.Audio.Backends.OpenAL.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 diff --git a/src/Ryujinx.Audio.Backends.SDL2/Ryujinx.Audio.Backends.SDL2.csproj b/src/Ryujinx.Audio.Backends.SDL2/Ryujinx.Audio.Backends.SDL2.csproj index 525f1f5b6..dd18e70a1 100644 --- a/src/Ryujinx.Audio.Backends.SDL2/Ryujinx.Audio.Backends.SDL2.csproj +++ b/src/Ryujinx.Audio.Backends.SDL2/Ryujinx.Audio.Backends.SDL2.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 true diff --git a/src/Ryujinx.Audio.Backends.SoundIo/Ryujinx.Audio.Backends.SoundIo.csproj b/src/Ryujinx.Audio.Backends.SoundIo/Ryujinx.Audio.Backends.SoundIo.csproj index 9f242dbe2..1d92d9d2e 100644 --- a/src/Ryujinx.Audio.Backends.SoundIo/Ryujinx.Audio.Backends.SoundIo.csproj +++ b/src/Ryujinx.Audio.Backends.SoundIo/Ryujinx.Audio.Backends.SoundIo.csproj @@ -1,9 +1,9 @@ - net7.0 + net8.0 true - win10-x64;linux-x64;osx-x64 + win-x64;osx-x64;linux-x64 @@ -15,11 +15,11 @@ PreserveNewest libsoundio.dll - + PreserveNewest libsoundio.dylib - + PreserveNewest libsoundio.so diff --git a/src/Ryujinx.Audio/Renderer/Utils/SpanIOHelper.cs b/src/Ryujinx.Audio/Renderer/Utils/SpanIOHelper.cs index 4771ae4dd..abbb6ea6c 100644 --- a/src/Ryujinx.Audio/Renderer/Utils/SpanIOHelper.cs +++ b/src/Ryujinx.Audio/Renderer/Utils/SpanIOHelper.cs @@ -25,7 +25,7 @@ namespace Ryujinx.Audio.Renderer.Utils throw new ArgumentOutOfRangeException(nameof(backingMemory), backingMemory.Length, null); } - MemoryMarshal.Write(backingMemory.Span[..size], ref data); + MemoryMarshal.Write(backingMemory.Span[..size], in data); backingMemory = backingMemory[size..]; } @@ -45,7 +45,7 @@ namespace Ryujinx.Audio.Renderer.Utils throw new ArgumentOutOfRangeException(nameof(backingMemory), backingMemory.Length, null); } - MemoryMarshal.Write(backingMemory[..size], ref data); + MemoryMarshal.Write(backingMemory[..size], in data); backingMemory = backingMemory[size..]; } diff --git a/src/Ryujinx.Audio/Ryujinx.Audio.csproj b/src/Ryujinx.Audio/Ryujinx.Audio.csproj index 4a159eb5c..fc20f4ec4 100644 --- a/src/Ryujinx.Audio/Ryujinx.Audio.csproj +++ b/src/Ryujinx.Audio/Ryujinx.Audio.csproj @@ -1,7 +1,7 @@  - net7.0 + net8.0 true diff --git a/src/Ryujinx.Ava/Ryujinx.Ava.csproj b/src/Ryujinx.Ava/Ryujinx.Ava.csproj index f0e99f427..6812e57c4 100644 --- a/src/Ryujinx.Ava/Ryujinx.Ava.csproj +++ b/src/Ryujinx.Ava/Ryujinx.Ava.csproj @@ -1,7 +1,7 @@  - net7.0 - win10-x64;osx-x64;linux-x64 + net8.0 + win-x64;osx-x64;linux-x64 Exe true 1.0.0-dirty @@ -25,6 +25,16 @@ partial + + + true + + @@ -40,7 +50,7 @@ - + diff --git a/src/Ryujinx.Ava/UI/Helpers/Win32NativeInterop.cs b/src/Ryujinx.Ava/UI/Helpers/Win32NativeInterop.cs index ca55d0399..35d16b9e0 100644 --- a/src/Ryujinx.Ava/UI/Helpers/Win32NativeInterop.cs +++ b/src/Ryujinx.Ava/UI/Helpers/Win32NativeInterop.cs @@ -86,7 +86,7 @@ namespace Ryujinx.Ava.UI.Helpers public static partial IntPtr SetCursor(IntPtr handle); [LibraryImport("user32.dll")] - public static partial IntPtr CreateCursor(IntPtr hInst, int xHotSpot, int yHotSpot, int nWidth, int nHeight, byte[] pvAndPlane, byte[] pvXorPlane); + public static partial IntPtr CreateCursor(IntPtr hInst, int xHotSpot, int yHotSpot, int nWidth, int nHeight, [In] byte[] pvAndPlane, [In] byte[] pvXorPlane); [LibraryImport("user32.dll", SetLastError = true, EntryPoint = "RegisterClassExW")] public static partial ushort RegisterClassEx(ref WndClassEx param); diff --git a/src/Ryujinx.Common/Ryujinx.Common.csproj b/src/Ryujinx.Common/Ryujinx.Common.csproj index c02b11e0c..da2f13a21 100644 --- a/src/Ryujinx.Common/Ryujinx.Common.csproj +++ b/src/Ryujinx.Common/Ryujinx.Common.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 true $(DefineConstants);$(ExtraDefineConstants) diff --git a/src/Ryujinx.Cpu/Ryujinx.Cpu.csproj b/src/Ryujinx.Cpu/Ryujinx.Cpu.csproj index 7da8da25a..5a6bf5c3d 100644 --- a/src/Ryujinx.Cpu/Ryujinx.Cpu.csproj +++ b/src/Ryujinx.Cpu/Ryujinx.Cpu.csproj @@ -1,7 +1,7 @@  - net7.0 + net8.0 true diff --git a/src/Ryujinx.Graphics.Device/Ryujinx.Graphics.Device.csproj b/src/Ryujinx.Graphics.Device/Ryujinx.Graphics.Device.csproj index 082dac9c2..ae2821edb 100644 --- a/src/Ryujinx.Graphics.Device/Ryujinx.Graphics.Device.csproj +++ b/src/Ryujinx.Graphics.Device/Ryujinx.Graphics.Device.csproj @@ -1,7 +1,7 @@  - net7.0 + net8.0 diff --git a/src/Ryujinx.Graphics.GAL/Ryujinx.Graphics.GAL.csproj b/src/Ryujinx.Graphics.GAL/Ryujinx.Graphics.GAL.csproj index 189108a39..d88b641a3 100644 --- a/src/Ryujinx.Graphics.GAL/Ryujinx.Graphics.GAL.csproj +++ b/src/Ryujinx.Graphics.GAL/Ryujinx.Graphics.GAL.csproj @@ -1,7 +1,7 @@  - net7.0 + net8.0 diff --git a/src/Ryujinx.Graphics.Gpu/Ryujinx.Graphics.Gpu.csproj b/src/Ryujinx.Graphics.Gpu/Ryujinx.Graphics.Gpu.csproj index 5255a6e00..6f1cce6ac 100644 --- a/src/Ryujinx.Graphics.Gpu/Ryujinx.Graphics.Gpu.csproj +++ b/src/Ryujinx.Graphics.Gpu/Ryujinx.Graphics.Gpu.csproj @@ -1,7 +1,7 @@  - net7.0 + net8.0 true diff --git a/src/Ryujinx.Graphics.Gpu/Synchronization/SynchronizationManager.cs b/src/Ryujinx.Graphics.Gpu/Synchronization/SynchronizationManager.cs index ccec763e3..2d5eede58 100644 --- a/src/Ryujinx.Graphics.Gpu/Synchronization/SynchronizationManager.cs +++ b/src/Ryujinx.Graphics.Gpu/Synchronization/SynchronizationManager.cs @@ -37,10 +37,7 @@ namespace Ryujinx.Graphics.Gpu.Synchronization /// The incremented value of the syncpoint public uint IncrementSyncpoint(uint id) { - if (id >= MaxHardwareSyncpoints) - { - throw new ArgumentOutOfRangeException(nameof(id)); - } + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(id, (uint)MaxHardwareSyncpoints); return _syncpoints[id].Increment(); } @@ -53,10 +50,7 @@ namespace Ryujinx.Graphics.Gpu.Synchronization /// The value of the syncpoint public uint GetSyncpointValue(uint id) { - if (id >= MaxHardwareSyncpoints) - { - throw new ArgumentOutOfRangeException(nameof(id)); - } + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(id, (uint)MaxHardwareSyncpoints); return _syncpoints[id].Value; } @@ -72,10 +66,7 @@ namespace Ryujinx.Graphics.Gpu.Synchronization /// The created SyncpointWaiterHandle object or null if already past threshold public SyncpointWaiterHandle RegisterCallbackOnSyncpoint(uint id, uint threshold, Action callback) { - if (id >= MaxHardwareSyncpoints) - { - throw new ArgumentOutOfRangeException(nameof(id)); - } + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(id, (uint)MaxHardwareSyncpoints); return _syncpoints[id].RegisterCallback(threshold, callback); } @@ -88,10 +79,7 @@ namespace Ryujinx.Graphics.Gpu.Synchronization /// Thrown when id >= MaxHardwareSyncpoints public void UnregisterCallback(uint id, SyncpointWaiterHandle waiterInformation) { - if (id >= MaxHardwareSyncpoints) - { - throw new ArgumentOutOfRangeException(nameof(id)); - } + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(id, (uint)MaxHardwareSyncpoints); _syncpoints[id].UnregisterCallback(waiterInformation); } @@ -107,10 +95,7 @@ namespace Ryujinx.Graphics.Gpu.Synchronization /// True if timed out public bool WaitOnSyncpoint(uint id, uint threshold, TimeSpan timeout) { - if (id >= MaxHardwareSyncpoints) - { - throw new ArgumentOutOfRangeException(nameof(id)); - } + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(id, (uint)MaxHardwareSyncpoints); // TODO: Remove this when GPU channel scheduling will be implemented. if (timeout == Timeout.InfiniteTimeSpan) diff --git a/src/Ryujinx.Graphics.Host1x/Ryujinx.Graphics.Host1x.csproj b/src/Ryujinx.Graphics.Host1x/Ryujinx.Graphics.Host1x.csproj index 3cff4061e..22959fad8 100644 --- a/src/Ryujinx.Graphics.Host1x/Ryujinx.Graphics.Host1x.csproj +++ b/src/Ryujinx.Graphics.Host1x/Ryujinx.Graphics.Host1x.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 diff --git a/src/Ryujinx.Graphics.Nvdec.FFmpeg/Ryujinx.Graphics.Nvdec.FFmpeg.csproj b/src/Ryujinx.Graphics.Nvdec.FFmpeg/Ryujinx.Graphics.Nvdec.FFmpeg.csproj index bff1e803b..d1a6358c2 100644 --- a/src/Ryujinx.Graphics.Nvdec.FFmpeg/Ryujinx.Graphics.Nvdec.FFmpeg.csproj +++ b/src/Ryujinx.Graphics.Nvdec.FFmpeg/Ryujinx.Graphics.Nvdec.FFmpeg.csproj @@ -1,7 +1,7 @@  - net7.0 + net8.0 true diff --git a/src/Ryujinx.Graphics.Nvdec.Vp9/Ryujinx.Graphics.Nvdec.Vp9.csproj b/src/Ryujinx.Graphics.Nvdec.Vp9/Ryujinx.Graphics.Nvdec.Vp9.csproj index bff1e803b..d1a6358c2 100644 --- a/src/Ryujinx.Graphics.Nvdec.Vp9/Ryujinx.Graphics.Nvdec.Vp9.csproj +++ b/src/Ryujinx.Graphics.Nvdec.Vp9/Ryujinx.Graphics.Nvdec.Vp9.csproj @@ -1,7 +1,7 @@  - net7.0 + net8.0 true diff --git a/src/Ryujinx.Graphics.Nvdec/Ryujinx.Graphics.Nvdec.csproj b/src/Ryujinx.Graphics.Nvdec/Ryujinx.Graphics.Nvdec.csproj index bfba98a73..fd49a7c80 100644 --- a/src/Ryujinx.Graphics.Nvdec/Ryujinx.Graphics.Nvdec.csproj +++ b/src/Ryujinx.Graphics.Nvdec/Ryujinx.Graphics.Nvdec.csproj @@ -1,7 +1,7 @@  - net7.0 + net8.0 true diff --git a/src/Ryujinx.Graphics.OpenGL/Ryujinx.Graphics.OpenGL.csproj b/src/Ryujinx.Graphics.OpenGL/Ryujinx.Graphics.OpenGL.csproj index 2313cc68f..3d64da99b 100644 --- a/src/Ryujinx.Graphics.OpenGL/Ryujinx.Graphics.OpenGL.csproj +++ b/src/Ryujinx.Graphics.OpenGL/Ryujinx.Graphics.OpenGL.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 true diff --git a/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/PhiNode.cs b/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/PhiNode.cs index 6c95c7bdd..f4c4fef42 100644 --- a/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/PhiNode.cs +++ b/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/PhiNode.cs @@ -69,10 +69,7 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation public Operand GetDest(int index) { - if (index != 0) - { - throw new ArgumentOutOfRangeException(nameof(index)); - } + ArgumentOutOfRangeException.ThrowIfNotEqual(index, 0); return _dest; } diff --git a/src/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj b/src/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj index ea9a7821b..8ccf5348f 100644 --- a/src/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj +++ b/src/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 diff --git a/src/Ryujinx.Graphics.Texture/Ryujinx.Graphics.Texture.csproj b/src/Ryujinx.Graphics.Texture/Ryujinx.Graphics.Texture.csproj index 70e3453c3..51721490e 100644 --- a/src/Ryujinx.Graphics.Texture/Ryujinx.Graphics.Texture.csproj +++ b/src/Ryujinx.Graphics.Texture/Ryujinx.Graphics.Texture.csproj @@ -1,6 +1,6 @@ - net7.0 + net8.0 true diff --git a/src/Ryujinx.Graphics.Texture/Utils/RgbaColor32.cs b/src/Ryujinx.Graphics.Texture/Utils/RgbaColor32.cs index de7c9262d..8ca3f89bc 100644 --- a/src/Ryujinx.Graphics.Texture/Utils/RgbaColor32.cs +++ b/src/Ryujinx.Graphics.Texture/Utils/RgbaColor32.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.Intrinsics; using System.Runtime.Intrinsics.X86; @@ -102,11 +103,11 @@ namespace Ryujinx.Graphics.Texture.Utils } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static RgbaColor32 operator <<(RgbaColor32 x, int shift) + public static RgbaColor32 operator <<(RgbaColor32 x, [ConstantExpected] byte shift) { if (Sse2.IsSupported) { - return new RgbaColor32(Sse2.ShiftLeftLogical(x._color, (byte)shift)); + return new RgbaColor32(Sse2.ShiftLeftLogical(x._color, shift)); } else { @@ -115,11 +116,11 @@ namespace Ryujinx.Graphics.Texture.Utils } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static RgbaColor32 operator >>(RgbaColor32 x, int shift) + public static RgbaColor32 operator >>(RgbaColor32 x, [ConstantExpected] byte shift) { if (Sse2.IsSupported) { - return new RgbaColor32(Sse2.ShiftRightLogical(x._color, (byte)shift)); + return new RgbaColor32(Sse2.ShiftRightLogical(x._color, shift)); } else { diff --git a/src/Ryujinx.Graphics.Vic/Ryujinx.Graphics.Vic.csproj b/src/Ryujinx.Graphics.Vic/Ryujinx.Graphics.Vic.csproj index 2a7cdd985..cfebcfa2a 100644 --- a/src/Ryujinx.Graphics.Vic/Ryujinx.Graphics.Vic.csproj +++ b/src/Ryujinx.Graphics.Vic/Ryujinx.Graphics.Vic.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 true diff --git a/src/Ryujinx.Graphics.Video/Ryujinx.Graphics.Video.csproj b/src/Ryujinx.Graphics.Video/Ryujinx.Graphics.Video.csproj index 9cf37670e..abff58a53 100644 --- a/src/Ryujinx.Graphics.Video/Ryujinx.Graphics.Video.csproj +++ b/src/Ryujinx.Graphics.Video/Ryujinx.Graphics.Video.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs b/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs index 156b3db16..7346d7891 100644 --- a/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs +++ b/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs @@ -189,7 +189,7 @@ namespace Ryujinx.Graphics.Vulkan PipelineStageFlags.AllCommandsBit, 0, 1, - new ReadOnlySpan(memoryBarrier), + new ReadOnlySpan(in memoryBarrier), 0, ReadOnlySpan.Empty, 0, diff --git a/src/Ryujinx.Graphics.Vulkan/Ryujinx.Graphics.Vulkan.csproj b/src/Ryujinx.Graphics.Vulkan/Ryujinx.Graphics.Vulkan.csproj index 8d30457e2..f6a7be91e 100644 --- a/src/Ryujinx.Graphics.Vulkan/Ryujinx.Graphics.Vulkan.csproj +++ b/src/Ryujinx.Graphics.Vulkan/Ryujinx.Graphics.Vulkan.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanException.cs b/src/Ryujinx.Graphics.Vulkan/VulkanException.cs index 983f03d4e..2d9dbc348 100644 --- a/src/Ryujinx.Graphics.Vulkan/VulkanException.cs +++ b/src/Ryujinx.Graphics.Vulkan/VulkanException.cs @@ -33,9 +33,5 @@ namespace Ryujinx.Graphics.Vulkan public VulkanException(string message, Exception innerException) : base(message, innerException) { } - - protected VulkanException(SerializationInfo info, StreamingContext context) : base(info, context) - { - } } } diff --git a/src/Ryujinx.HLE/Exceptions/ServiceNotImplementedException.cs b/src/Ryujinx.HLE/Exceptions/ServiceNotImplementedException.cs index e43c838a2..9cb1cf2c7 100644 --- a/src/Ryujinx.HLE/Exceptions/ServiceNotImplementedException.cs +++ b/src/Ryujinx.HLE/Exceptions/ServiceNotImplementedException.cs @@ -35,8 +35,6 @@ namespace Ryujinx.HLE.Exceptions Request = context.Request; } - protected ServiceNotImplementedException(SerializationInfo info, StreamingContext context) : base(info, context) { } - public override string Message { get diff --git a/src/Ryujinx.HLE/FileSystem/ContentManager.cs b/src/Ryujinx.HLE/FileSystem/ContentManager.cs index 724cb675c..b27eb5ead 100644 --- a/src/Ryujinx.HLE/FileSystem/ContentManager.cs +++ b/src/Ryujinx.HLE/FileSystem/ContentManager.cs @@ -420,10 +420,7 @@ namespace Ryujinx.HLE.FileSystem if (locationList != null) { - if (locationList.Contains(entry)) - { - locationList.Remove(entry); - } + locationList.Remove(entry); locationList.AddLast(entry); } diff --git a/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvHostCtrl/Types/NvHostSyncPt.cs b/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvHostCtrl/Types/NvHostSyncPt.cs index 9c6d025eb..b83c642e5 100644 --- a/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvHostCtrl/Types/NvHostSyncPt.cs +++ b/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvHostCtrl/Types/NvHostSyncPt.cs @@ -85,10 +85,7 @@ namespace Ryujinx.HLE.HOS.Services.Nv.NvDrvServices.NvHostCtrl public void SetSyncpointMinEqualSyncpointMax(uint id) { - if (id >= SynchronizationManager.MaxHardwareSyncpoints) - { - throw new ArgumentOutOfRangeException(nameof(id)); - } + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(id, (uint)SynchronizationManager.MaxHardwareSyncpoints); int value = (int)ReadSyncpointValue(id); diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Types/BsdMsgHdr.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Types/BsdMsgHdr.cs index 07c97182c..62a7ccb59 100644 --- a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Types/BsdMsgHdr.cs +++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Types/BsdMsgHdr.cs @@ -27,7 +27,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Types int controlLength = message.Control == null ? 0 : message.Control.Length; BsdSocketFlags flags = message.Flags; - if (!MemoryMarshal.TryWrite(rawData, ref msgNameLength)) + if (!MemoryMarshal.TryWrite(rawData, in msgNameLength)) { return LinuxError.EFAULT; } @@ -45,7 +45,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Types rawData = rawData[msgNameLength..]; } - if (!MemoryMarshal.TryWrite(rawData, ref iovCount)) + if (!MemoryMarshal.TryWrite(rawData, in iovCount)) { return LinuxError.EFAULT; } @@ -58,7 +58,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Types { ulong iovLength = (ulong)message.Iov[index].Length; - if (!MemoryMarshal.TryWrite(rawData, ref iovLength)) + if (!MemoryMarshal.TryWrite(rawData, in iovLength)) { return LinuxError.EFAULT; } @@ -78,7 +78,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Types } } - if (!MemoryMarshal.TryWrite(rawData, ref controlLength)) + if (!MemoryMarshal.TryWrite(rawData, in controlLength)) { return LinuxError.EFAULT; } @@ -96,14 +96,14 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Types rawData = rawData[controlLength..]; } - if (!MemoryMarshal.TryWrite(rawData, ref flags)) + if (!MemoryMarshal.TryWrite(rawData, in flags)) { return LinuxError.EFAULT; } rawData = rawData[sizeof(BsdSocketFlags)..]; - if (!MemoryMarshal.TryWrite(rawData, ref message.Length)) + if (!MemoryMarshal.TryWrite(rawData, in message.Length)) { return LinuxError.EFAULT; } diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/IResolver.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/IResolver.cs index d0fb6675a..39af90383 100644 --- a/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/IResolver.cs +++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/IResolver.cs @@ -654,7 +654,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Sfdnsres } uint sentinel = 0; - MemoryMarshal.Write(data, ref sentinel); + MemoryMarshal.Write(data, in sentinel); data = data[sizeof(uint)..]; return region.Memory.Span.Length - data.Length; diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/Types/AddrInfoSerialized.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/Types/AddrInfoSerialized.cs index a0613d7bc..b57b0d5ca 100644 --- a/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/Types/AddrInfoSerialized.cs +++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/Types/AddrInfoSerialized.cs @@ -94,7 +94,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Sfdnsres.Types Header.ToNetworkOrder(); - MemoryMarshal.Write(buffer, ref Header); + MemoryMarshal.Write(buffer, in Header); buffer = buffer[Unsafe.SizeOf()..]; @@ -103,7 +103,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Sfdnsres.Types AddrInfo4 socketAddress = SocketAddress.Value; socketAddress.ToNetworkOrder(); - MemoryMarshal.Write(buffer, ref socketAddress); + MemoryMarshal.Write(buffer, in socketAddress); buffer = buffer[Unsafe.SizeOf()..]; } @@ -117,7 +117,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Sfdnsres.Types Array4 rawIPv4Address = RawIPv4Address.Value; AddrInfo4.RawIpv4AddressNetworkEndianSwap(ref rawIPv4Address); - MemoryMarshal.Write(buffer, ref rawIPv4Address); + MemoryMarshal.Write(buffer, in rawIPv4Address); buffer = buffer[Unsafe.SizeOf>()..]; } diff --git a/src/Ryujinx.HLE/HOS/Services/Ssl/SslService/SslManagedSocketConnection.cs b/src/Ryujinx.HLE/HOS/Services/Ssl/SslService/SslManagedSocketConnection.cs index dab099aab..e3c05df51 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ssl/SslService/SslManagedSocketConnection.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ssl/SslService/SslManagedSocketConnection.cs @@ -161,7 +161,7 @@ namespace Ryujinx.HLE.HOS.Services.Ssl.SslService } else { - throw exception; + throw; } } finally @@ -206,7 +206,7 @@ namespace Ryujinx.HLE.HOS.Services.Ssl.SslService } else { - throw exception; + throw; } } finally diff --git a/src/Ryujinx.HLE/Ryujinx.HLE.csproj b/src/Ryujinx.HLE/Ryujinx.HLE.csproj index f3439cc8f..370933ccf 100644 --- a/src/Ryujinx.HLE/Ryujinx.HLE.csproj +++ b/src/Ryujinx.HLE/Ryujinx.HLE.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 diff --git a/src/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj b/src/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj index d2585c563..7b13df736 100644 --- a/src/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj +++ b/src/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj @@ -1,8 +1,8 @@  - net7.0 - win10-x64;osx-x64;linux-x64 + net8.0 + win-x64;osx-x64;linux-x64 Exe true 1.0.0-dirty @@ -34,7 +34,7 @@ - + @@ -69,4 +69,4 @@ true partial - \ No newline at end of file + diff --git a/src/Ryujinx.Horizon.Common/Ryujinx.Horizon.Common.csproj b/src/Ryujinx.Horizon.Common/Ryujinx.Horizon.Common.csproj index d04c5a9b6..fa1544c4f 100644 --- a/src/Ryujinx.Horizon.Common/Ryujinx.Horizon.Common.csproj +++ b/src/Ryujinx.Horizon.Common/Ryujinx.Horizon.Common.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 diff --git a/src/Ryujinx.Horizon/Ryujinx.Horizon.csproj b/src/Ryujinx.Horizon/Ryujinx.Horizon.csproj index 0139c367f..ae40f7b5e 100644 --- a/src/Ryujinx.Horizon/Ryujinx.Horizon.csproj +++ b/src/Ryujinx.Horizon/Ryujinx.Horizon.csproj @@ -1,7 +1,7 @@  - net7.0 + net8.0 diff --git a/src/Ryujinx.Input.SDL2/Ryujinx.Input.SDL2.csproj b/src/Ryujinx.Input.SDL2/Ryujinx.Input.SDL2.csproj index 817a96e2e..1ab79d08a 100644 --- a/src/Ryujinx.Input.SDL2/Ryujinx.Input.SDL2.csproj +++ b/src/Ryujinx.Input.SDL2/Ryujinx.Input.SDL2.csproj @@ -1,7 +1,7 @@  - net7.0 + net8.0 true diff --git a/src/Ryujinx.Input/Ryujinx.Input.csproj b/src/Ryujinx.Input/Ryujinx.Input.csproj index df462734f..59a9eeb61 100644 --- a/src/Ryujinx.Input/Ryujinx.Input.csproj +++ b/src/Ryujinx.Input/Ryujinx.Input.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 true diff --git a/src/Ryujinx.Memory/Range/MultiRange.cs b/src/Ryujinx.Memory/Range/MultiRange.cs index 7011e528e..5a0b4178a 100644 --- a/src/Ryujinx.Memory/Range/MultiRange.cs +++ b/src/Ryujinx.Memory/Range/MultiRange.cs @@ -52,10 +52,7 @@ namespace Ryujinx.Memory.Range { if (HasSingleRange) { - if (_singleRange.Size - offset < size) - { - throw new ArgumentOutOfRangeException(nameof(size)); - } + ArgumentOutOfRangeException.ThrowIfGreaterThan(size, _singleRange.Size - offset); return new MultiRange(_singleRange.Address + offset, size); } @@ -108,10 +105,7 @@ namespace Ryujinx.Memory.Range { if (HasSingleRange) { - if (index != 0) - { - throw new ArgumentOutOfRangeException(nameof(index)); - } + ArgumentOutOfRangeException.ThrowIfNotEqual(index, 0); return _singleRange; } diff --git a/src/Ryujinx.Memory/Ryujinx.Memory.csproj b/src/Ryujinx.Memory/Ryujinx.Memory.csproj index 91e46e48e..8310a3e5c 100644 --- a/src/Ryujinx.Memory/Ryujinx.Memory.csproj +++ b/src/Ryujinx.Memory/Ryujinx.Memory.csproj @@ -1,7 +1,7 @@  - net7.0 + net8.0 true diff --git a/src/Ryujinx.SDL2.Common/Ryujinx.SDL2.Common.csproj b/src/Ryujinx.SDL2.Common/Ryujinx.SDL2.Common.csproj index 955e6d3f1..8e7953045 100644 --- a/src/Ryujinx.SDL2.Common/Ryujinx.SDL2.Common.csproj +++ b/src/Ryujinx.SDL2.Common/Ryujinx.SDL2.Common.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 diff --git a/src/Ryujinx.ShaderTools/Ryujinx.ShaderTools.csproj b/src/Ryujinx.ShaderTools/Ryujinx.ShaderTools.csproj index 74b4ec2f7..ab89fb5c7 100644 --- a/src/Ryujinx.ShaderTools/Ryujinx.ShaderTools.csproj +++ b/src/Ryujinx.ShaderTools/Ryujinx.ShaderTools.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 Exe Debug;Release diff --git a/src/Ryujinx.Tests.Memory/Ryujinx.Tests.Memory.csproj b/src/Ryujinx.Tests.Memory/Ryujinx.Tests.Memory.csproj index 4dcb69623..f05060838 100644 --- a/src/Ryujinx.Tests.Memory/Ryujinx.Tests.Memory.csproj +++ b/src/Ryujinx.Tests.Memory/Ryujinx.Tests.Memory.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 false diff --git a/src/Ryujinx.Tests.Unicorn/Ryujinx.Tests.Unicorn.csproj b/src/Ryujinx.Tests.Unicorn/Ryujinx.Tests.Unicorn.csproj index d925546fe..befacfb22 100644 --- a/src/Ryujinx.Tests.Unicorn/Ryujinx.Tests.Unicorn.csproj +++ b/src/Ryujinx.Tests.Unicorn/Ryujinx.Tests.Unicorn.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 true Debug;Release diff --git a/src/Ryujinx.Tests/Ryujinx.Tests.csproj b/src/Ryujinx.Tests/Ryujinx.Tests.csproj index ab331ce58..3be9787a3 100644 --- a/src/Ryujinx.Tests/Ryujinx.Tests.csproj +++ b/src/Ryujinx.Tests/Ryujinx.Tests.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 Exe false diff --git a/src/Ryujinx.Ui.Common/Ryujinx.Ui.Common.csproj b/src/Ryujinx.Ui.Common/Ryujinx.Ui.Common.csproj index 3da47431f..7aff09ff6 100644 --- a/src/Ryujinx.Ui.Common/Ryujinx.Ui.Common.csproj +++ b/src/Ryujinx.Ui.Common/Ryujinx.Ui.Common.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 true diff --git a/src/Ryujinx/Ryujinx.csproj b/src/Ryujinx/Ryujinx.csproj index 5b5ed4637..9890b761b 100644 --- a/src/Ryujinx/Ryujinx.csproj +++ b/src/Ryujinx/Ryujinx.csproj @@ -1,8 +1,8 @@ - net7.0 - win10-x64;osx-x64;linux-x64 + net8.0 + win-x64;osx-x64;linux-x64 Exe true 1.0.0-dirty @@ -25,7 +25,7 @@ - + diff --git a/src/Spv.Generator/Spv.Generator.csproj b/src/Spv.Generator/Spv.Generator.csproj index 082dac9c2..ae2821edb 100644 --- a/src/Spv.Generator/Spv.Generator.csproj +++ b/src/Spv.Generator/Spv.Generator.csproj @@ -1,7 +1,7 @@  - net7.0 + net8.0 From 388446c255566d67240905bc6efa7af5a71c34b5 Mon Sep 17 00:00:00 2001 From: Mary Guillemard Date: Wed, 15 Nov 2023 18:12:19 +0100 Subject: [PATCH 16/23] infra: Workaround Microsoft.NET.ILLink.Tasks restore failure on Flathub This package seems to be required for triming now but isn't restored by default. This changes the flatpak pusher to publish so we are sure that the package is in the cache. Signed-off-by: Mary Guillemard --- .github/workflows/flatpak.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index 4c8ba3e17..c1ae9fe8f 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -49,7 +49,9 @@ jobs: run: python -m pip install PyYAML lxml - name: Restore Nuget packages - run: dotnet restore Ryujinx/${{ env.RYUJINX_PROJECT_FILE }} + # With .NET 8.0.100, Microsoft.NET.ILLink.Tasks isn't restored by default and only seems to appears when publishing. + # So we just publish to grab the dependencies + run: dotnet publish -c Release -r linux-x64 Ryujinx/${{ env.RYUJINX_PROJECT_FILE }} --self-contained - name: Generate nuget_sources.json shell: python From cdc8fed64fc8e51fe626b0a369902932db0ec49c Mon Sep 17 00:00:00 2001 From: Mary Guillemard Date: Wed, 15 Nov 2023 19:08:46 +0100 Subject: [PATCH 17/23] chore: Update OpenTK to 4.8.1 (#5912) OpenTK.OpenAL was renamed to OpenTK.Audio.OpenAL. Signed-off-by: Mary Guillemard --- Directory.Packages.props | 8 ++++---- .../Ryujinx.Audio.Backends.OpenAL.csproj | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 3d8aac1a7..44f9b5d40 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -27,10 +27,10 @@ - - - - + + + + diff --git a/src/Ryujinx.Audio.Backends.OpenAL/Ryujinx.Audio.Backends.OpenAL.csproj b/src/Ryujinx.Audio.Backends.OpenAL/Ryujinx.Audio.Backends.OpenAL.csproj index 3863e4439..b5fd8f9e7 100644 --- a/src/Ryujinx.Audio.Backends.OpenAL/Ryujinx.Audio.Backends.OpenAL.csproj +++ b/src/Ryujinx.Audio.Backends.OpenAL/Ryujinx.Audio.Backends.OpenAL.csproj @@ -5,7 +5,7 @@ - + From dcf10561b996cdba111c5a3c3fe128781ab44021 Mon Sep 17 00:00:00 2001 From: gdkchan Date: Wed, 15 Nov 2023 21:36:25 -0300 Subject: [PATCH 18/23] Fix missing texture flush for draw then DMA copy sequence without render target change (#5933) * Unbind render targets before DMA copy * Move DirtyAction to TextureGroupHandle * Fix lost copy dependency bug * XML doc --- .../Engine/Dma/DmaClass.cs | 1 + src/Ryujinx.Graphics.Gpu/Image/Texture.cs | 7 +- .../Image/TextureGroup.cs | 38 +--------- .../Image/TextureGroupHandle.cs | 45 ++++++++---- .../Image/TextureManager.cs | 72 +++++++++++++++++-- 5 files changed, 102 insertions(+), 61 deletions(-) diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Dma/DmaClass.cs b/src/Ryujinx.Graphics.Gpu/Engine/Dma/DmaClass.cs index 199cc423e..b1921bd51 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Dma/DmaClass.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Dma/DmaClass.cs @@ -211,6 +211,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Dma int xCount = (int)_state.State.LineLengthIn; int yCount = (int)_state.State.LineCount; + _channel.TextureManager.RefreshModifiedTextures(); _3dEngine.CreatePendingSyncs(); _3dEngine.FlushUboDirty(); diff --git a/src/Ryujinx.Graphics.Gpu/Image/Texture.cs b/src/Ryujinx.Graphics.Gpu/Image/Texture.cs index 04c2b615f..f1615b388 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/Texture.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/Texture.cs @@ -101,11 +101,6 @@ namespace Ryujinx.Graphics.Gpu.Image /// public bool AlwaysFlushOnOverlap { get; private set; } - /// - /// Indicates that the texture was modified since the last time it was flushed. - /// - public bool ModifiedSinceLastFlush { get; set; } - /// /// Increments when the host texture is swapped, or when the texture is removed from all pools. /// @@ -1443,7 +1438,7 @@ namespace Ryujinx.Graphics.Gpu.Image if (_modifiedStale || Group.HasCopyDependencies || Group.HasFlushBuffer) { _modifiedStale = false; - Group.SignalModifying(this, bound, bound || ModifiedSinceLastFlush || Group.HasCopyDependencies || Group.HasFlushBuffer); + Group.SignalModifying(this, bound); } _physicalMemory.TextureCache.Lift(this); diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs index e828cb9f1..b93f3832d 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs @@ -709,8 +709,7 @@ namespace Ryujinx.Graphics.Gpu.Image /// /// The texture that has been modified /// True if this texture is being bound, false if unbound - /// Indicates if the modified flag should be set - public void SignalModifying(Texture texture, bool bound, bool setModified) + public void SignalModifying(Texture texture, bool bound) { ModifiedSequence = _context.GetModifiedSequence(); @@ -722,7 +721,7 @@ namespace Ryujinx.Graphics.Gpu.Image { TextureGroupHandle group = _handles[baseHandle + i]; - group.SignalModifying(bound, _context, setModified); + group.SignalModifying(bound, _context); } }); } @@ -993,26 +992,6 @@ namespace Ryujinx.Graphics.Gpu.Image } } - /// - /// The action to perform when a memory tracking handle is flipped to dirty. - /// This notifies overlapping textures that the memory needs to be synchronized. - /// - /// The handle that a dirty flag was set on - private void DirtyAction(TextureGroupHandle groupHandle) - { - // Notify all textures that belong to this handle. - - Storage.SignalGroupDirty(); - - lock (groupHandle.Overlaps) - { - foreach (Texture overlap in groupHandle.Overlaps) - { - overlap.SignalGroupDirty(); - } - } - } - /// /// Generate a CpuRegionHandle for a given address and size range in CPU VA. /// @@ -1084,11 +1063,6 @@ namespace Ryujinx.Graphics.Gpu.Image views, result.ToArray()); - foreach (RegionHandle handle in result) - { - handle.RegisterDirtyEvent(() => DirtyAction(groupHandle)); - } - return groupHandle; } @@ -1360,11 +1334,6 @@ namespace Ryujinx.Graphics.Gpu.Image var groupHandle = new TextureGroupHandle(this, 0, Storage.Size, _views, 0, 0, 0, _allOffsets.Length, cpuRegionHandles); - foreach (RegionHandle handle in cpuRegionHandles) - { - handle.RegisterDirtyEvent(() => DirtyAction(groupHandle)); - } - handles = new TextureGroupHandle[] { groupHandle }; } else @@ -1620,6 +1589,7 @@ namespace Ryujinx.Graphics.Gpu.Image if ((ignore == null || !handle.HasDependencyTo(ignore)) && handle.Modified) { handle.Modified = false; + handle.DeferredCopy = null; Storage.SignalModifiedDirty(); lock (handle.Overlaps) @@ -1666,8 +1636,6 @@ namespace Ryujinx.Graphics.Gpu.Image return; } - Storage.ModifiedSinceLastFlush = false; - // There is a small gap here where the action is removed but _actionRegistered is still 1. // In this case it will skip registering the action, but here we are already handling it, // so there shouldn't be any issue as it's the same handler for all actions. diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs index 717792e04..72b743906 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs @@ -152,6 +152,32 @@ namespace Ryujinx.Graphics.Gpu.Image // Linear textures are presumed to be used for readback initially. _flushBalance = FlushBalanceThreshold + FlushBalanceIncrement; } + + foreach (RegionHandle handle in handles) + { + handle.RegisterDirtyEvent(DirtyAction); + } + } + + /// + /// The action to perform when a memory tracking handle is flipped to dirty. + /// This notifies overlapping textures that the memory needs to be synchronized. + /// + private void DirtyAction() + { + // Notify all textures that belong to this handle. + + _group.Storage.SignalGroupDirty(); + + lock (Overlaps) + { + foreach (Texture overlap in Overlaps) + { + overlap.SignalGroupDirty(); + } + } + + DeferredCopy = null; } /// @@ -304,17 +330,9 @@ namespace Ryujinx.Graphics.Gpu.Image /// /// True if this handle is being bound, false if unbound /// The GPU context to register a sync action on - /// Indicates if the modified flag should be set - public void SignalModifying(bool bound, GpuContext context, bool setModified) + public void SignalModifying(bool bound, GpuContext context) { - if (setModified) - { - SignalModified(context); - } - else - { - RegisterSync(context); - } + SignalModified(context); if (!bound && _syncActionRegistered && NextSyncCopies()) { @@ -457,7 +475,6 @@ namespace Ryujinx.Graphics.Gpu.Image public void DeferCopy(TextureGroupHandle copyFrom) { Modified = false; - DeferredCopy = copyFrom; _group.Storage.SignalGroupDirty(); @@ -514,7 +531,7 @@ namespace Ryujinx.Graphics.Gpu.Image { existing.Other.Handle.CreateCopyDependency(this); - if (copyToOther) + if (copyToOther && Modified) { existing.Other.Handle.DeferCopy(this); } @@ -558,10 +575,10 @@ namespace Ryujinx.Graphics.Gpu.Image if (fromHandle != null) { // Only copy if the copy texture is still modified. - // It will be set as unmodified if new data is written from CPU, as the data previously in the texture will flush. + // DeferredCopy will be set to null if new data is written from CPU (see the DirtyAction method). // It will also set as unmodified if a copy is deferred to it. - shouldCopy = fromHandle.Modified; + shouldCopy = true; if (fromHandle._bindCount == 0) { diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureManager.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureManager.cs index e9f58314f..fa278bbd9 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureManager.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureManager.cs @@ -20,8 +20,10 @@ namespace Ryujinx.Graphics.Gpu.Image private readonly Texture[] _rtColors; private readonly ITexture[] _rtHostColors; + private readonly bool[] _rtColorsBound; private Texture _rtDepthStencil; private ITexture _rtHostDs; + private bool _rtDsBound; public int ClipRegionWidth { get; private set; } public int ClipRegionHeight { get; private set; } @@ -51,6 +53,7 @@ namespace Ryujinx.Graphics.Gpu.Image _rtColors = new Texture[Constants.TotalRenderTargets]; _rtHostColors = new ITexture[Constants.TotalRenderTargets]; + _rtColorsBound = new bool[Constants.TotalRenderTargets]; } /// @@ -154,7 +157,14 @@ namespace Ryujinx.Graphics.Gpu.Image if (_rtColors[index] != color) { - _rtColors[index]?.SignalModifying(false); + if (_rtColorsBound[index]) + { + _rtColors[index]?.SignalModifying(false); + } + else + { + _rtColorsBound[index] = true; + } if (color != null) { @@ -180,7 +190,14 @@ namespace Ryujinx.Graphics.Gpu.Image if (_rtDepthStencil != depthStencil) { - _rtDepthStencil?.SignalModifying(false); + if (_rtDsBound) + { + _rtDepthStencil?.SignalModifying(false); + } + else + { + _rtDsBound = true; + } if (depthStencil != null) { @@ -419,7 +436,12 @@ namespace Ryujinx.Graphics.Gpu.Image if (dsTexture != null) { hostDsTexture = dsTexture.HostTexture; - dsTexture.ModifiedSinceLastFlush = true; + + if (!_rtDsBound) + { + dsTexture.SignalModifying(true); + _rtDsBound = true; + } } if (_rtHostDs != hostDsTexture) @@ -436,7 +458,12 @@ namespace Ryujinx.Graphics.Gpu.Image if (texture != null) { hostTexture = texture.HostTexture; - texture.ModifiedSinceLastFlush = true; + + if (!_rtColorsBound[index]) + { + texture.SignalModifying(true); + _rtColorsBound[index] = true; + } } if (_rtHostColors[index] != hostTexture) @@ -466,6 +493,31 @@ namespace Ryujinx.Graphics.Gpu.Image _context.Renderer.Pipeline.SetRenderTargets(_rtHostColors, _rtHostDs); } + /// + /// Marks all currently bound render target textures as modified, and also makes them be set as modified again on next use. + /// + public void RefreshModifiedTextures() + { + Texture dsTexture = _rtDepthStencil; + + if (dsTexture != null && _rtDsBound) + { + dsTexture.SignalModifying(false); + _rtDsBound = false; + } + + for (int index = 0; index < _rtColors.Length; index++) + { + Texture texture = _rtColors[index]; + + if (texture != null && _rtColorsBound[index]) + { + texture.SignalModifying(false); + _rtColorsBound[index] = false; + } + } + } + /// /// Forces the texture and sampler pools to be re-loaded from the cache on next use. /// @@ -502,11 +554,19 @@ namespace Ryujinx.Graphics.Gpu.Image for (int i = 0; i < _rtColors.Length; i++) { - _rtColors[i]?.DecrementReferenceCount(); + if (_rtColorsBound[i]) + { + _rtColors[i]?.DecrementReferenceCount(); + } + _rtColors[i] = null; } - _rtDepthStencil?.DecrementReferenceCount(); + if (_rtDsBound) + { + _rtDepthStencil?.DecrementReferenceCount(); + } + _rtDepthStencil = null; } } From d11fe26aa319b352112c434167e5c43f7b6ee636 Mon Sep 17 00:00:00 2001 From: Isaac Marovitz <42140194+IsaacMarovitz@users.noreply.github.com> Date: Thu, 16 Nov 2023 14:09:15 -0500 Subject: [PATCH 19/23] Fix macOS Path (#5941) --- src/Ryujinx.Common/Configuration/AppDataManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ryujinx.Common/Configuration/AppDataManager.cs b/src/Ryujinx.Common/Configuration/AppDataManager.cs index 1dbc1f0ce..2b4a594d3 100644 --- a/src/Ryujinx.Common/Configuration/AppDataManager.cs +++ b/src/Ryujinx.Common/Configuration/AppDataManager.cs @@ -48,7 +48,7 @@ namespace Ryujinx.Common.Configuration string appDataPath; if (OperatingSystem.IsMacOS()) { - appDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "Library", "Application Support"); + appDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support"); } else { From 82a638230e4b10c099d8517b8ef7b602f22a6887 Mon Sep 17 00:00:00 2001 From: gdkchan Date: Thu, 16 Nov 2023 17:52:21 -0300 Subject: [PATCH 20/23] Fix JitCache.Unmap called with the same address freeing memory in use (#5937) --- src/ARMeilleure/Translation/Cache/JitCache.cs | 30 +++++-------------- .../Translation/Cache/JitUnwindWindows.cs | 2 +- 2 files changed, 9 insertions(+), 23 deletions(-) diff --git a/src/ARMeilleure/Translation/Cache/JitCache.cs b/src/ARMeilleure/Translation/Cache/JitCache.cs index 91a054123..e2b5e2d10 100644 --- a/src/ARMeilleure/Translation/Cache/JitCache.cs +++ b/src/ARMeilleure/Translation/Cache/JitCache.cs @@ -117,12 +117,11 @@ namespace ARMeilleure.Translation.Cache int funcOffset = (int)(pointer.ToInt64() - _jitRegion.Pointer.ToInt64()); - bool result = TryFind(funcOffset, out CacheEntry entry); - Debug.Assert(result); - - _cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size)); - - Remove(funcOffset); + if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset) + { + _cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size)); + _cacheEntries.RemoveAt(entryIndex); + } } } @@ -181,22 +180,7 @@ namespace ARMeilleure.Translation.Cache _cacheEntries.Insert(index, entry); } - private static void Remove(int offset) - { - int index = _cacheEntries.BinarySearch(new CacheEntry(offset, 0, default)); - - if (index < 0) - { - index = ~index - 1; - } - - if (index >= 0) - { - _cacheEntries.RemoveAt(index); - } - } - - public static bool TryFind(int offset, out CacheEntry entry) + public static bool TryFind(int offset, out CacheEntry entry, out int entryIndex) { lock (_lock) { @@ -210,11 +194,13 @@ namespace ARMeilleure.Translation.Cache if (index >= 0) { entry = _cacheEntries[index]; + entryIndex = index; return true; } } entry = default; + entryIndex = 0; return false; } } diff --git a/src/ARMeilleure/Translation/Cache/JitUnwindWindows.cs b/src/ARMeilleure/Translation/Cache/JitUnwindWindows.cs index 91fd19c25..3957a7559 100644 --- a/src/ARMeilleure/Translation/Cache/JitUnwindWindows.cs +++ b/src/ARMeilleure/Translation/Cache/JitUnwindWindows.cs @@ -95,7 +95,7 @@ namespace ARMeilleure.Translation.Cache { int offset = (int)((long)controlPc - context.ToInt64()); - if (!JitCache.TryFind(offset, out CacheEntry funcEntry)) + if (!JitCache.TryFind(offset, out CacheEntry funcEntry, out _)) { return null; // Not found. } From aa96dcb1bede3693877e2f1eca3e169d8ee13ef1 Mon Sep 17 00:00:00 2001 From: MutantAura <44103205+MutantAura@users.noreply.github.com> Date: Sat, 18 Nov 2023 20:42:45 +0000 Subject: [PATCH 21/23] misc: Default to Vulkan if available or running on macOS (#5913) * Addition of default backend check. Vulkan is preferred if available or macOS. * import ordering format fix * Update src/Ryujinx/Program.cs Co-authored-by: gdkchan * remove redundant load types --------- Co-authored-by: gdkchan --- .../Configuration/ConfigurationLoadResult.cs | 9 ---- .../Configuration/ConfigurationState.cs | 23 ++++++---- .../Ryujinx.Ui.Common.csproj | 1 + src/Ryujinx/Program.cs | 44 +------------------ 4 files changed, 16 insertions(+), 61 deletions(-) delete mode 100644 src/Ryujinx.Ui.Common/Configuration/ConfigurationLoadResult.cs diff --git a/src/Ryujinx.Ui.Common/Configuration/ConfigurationLoadResult.cs b/src/Ryujinx.Ui.Common/Configuration/ConfigurationLoadResult.cs deleted file mode 100644 index 71366ba7c..000000000 --- a/src/Ryujinx.Ui.Common/Configuration/ConfigurationLoadResult.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Ryujinx.Ui.Common.Configuration -{ - public enum ConfigurationLoadResult - { - Success = 0, - NotLoaded = 1, - MigratedFromPreVulkan = 1 << 8, - } -} diff --git a/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs b/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs index c79fa56c6..b017d384c 100644 --- a/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs +++ b/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs @@ -5,6 +5,7 @@ using Ryujinx.Common.Configuration.Hid.Controller; using Ryujinx.Common.Configuration.Hid.Keyboard; using Ryujinx.Common.Configuration.Multiplayer; using Ryujinx.Common.Logging; +using Ryujinx.Graphics.Vulkan; using Ryujinx.Ui.Common.Configuration.System; using Ryujinx.Ui.Common.Configuration.Ui; using Ryujinx.Ui.Common.Helper; @@ -763,7 +764,7 @@ namespace Ryujinx.Ui.Common.Configuration Graphics.ResScaleCustom.Value = 1.0f; Graphics.MaxAnisotropy.Value = -1.0f; Graphics.AspectRatio.Value = AspectRatio.Fixed16x9; - Graphics.GraphicsBackend.Value = OperatingSystem.IsMacOS() ? GraphicsBackend.Vulkan : GraphicsBackend.OpenGl; + Graphics.GraphicsBackend.Value = DefaultGraphicsBackend(); Graphics.PreferredGpu.Value = ""; Graphics.ShadersDumpPath.Value = ""; Logger.EnableDebug.Value = false; @@ -907,7 +908,7 @@ namespace Ryujinx.Ui.Common.Configuration }; } - public ConfigurationLoadResult Load(ConfigurationFileFormat configurationFileFormat, string configurationFilePath) + public void Load(ConfigurationFileFormat configurationFileFormat, string configurationFilePath) { bool configurationFileUpdated = false; @@ -916,12 +917,8 @@ namespace Ryujinx.Ui.Common.Configuration Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Unsupported configuration version {configurationFileFormat.Version}, loading default."); LoadDefault(); - - return ConfigurationLoadResult.NotLoaded; } - ConfigurationLoadResult result = ConfigurationLoadResult.Success; - if (configurationFileFormat.Version < 2) { Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 2."); @@ -1336,8 +1333,6 @@ namespace Ryujinx.Ui.Common.Configuration configurationFileFormat.GraphicsBackend = GraphicsBackend.OpenGl; - result |= ConfigurationLoadResult.MigratedFromPreVulkan; - configurationFileUpdated = true; } @@ -1535,8 +1530,18 @@ namespace Ryujinx.Ui.Common.Configuration Ryujinx.Common.Logging.Logger.Notice.Print(LogClass.Application, $"Configuration file updated to version {ConfigurationFileFormat.CurrentVersion}"); } + } - return result; + private static GraphicsBackend DefaultGraphicsBackend() + { + // Any system running macOS or returning any amount of valid Vulkan devices should default to Vulkan. + // Checks for if the Vulkan version and featureset is compatible should be performed within VulkanRenderer. + if (OperatingSystem.IsMacOS() || VulkanRenderer.GetPhysicalDevices().Length > 0) + { + return GraphicsBackend.Vulkan; + } + + return GraphicsBackend.OpenGl; } private static void LogValueChange(ReactiveEventArgs eventArgs, string valueName) diff --git a/src/Ryujinx.Ui.Common/Ryujinx.Ui.Common.csproj b/src/Ryujinx.Ui.Common/Ryujinx.Ui.Common.csproj index 7aff09ff6..74331fdef 100644 --- a/src/Ryujinx.Ui.Common/Ryujinx.Ui.Common.csproj +++ b/src/Ryujinx.Ui.Common/Ryujinx.Ui.Common.csproj @@ -62,6 +62,7 @@ + diff --git a/src/Ryujinx/Program.cs b/src/Ryujinx/Program.cs index afb6a9925..597d00f30 100644 --- a/src/Ryujinx/Program.cs +++ b/src/Ryujinx/Program.cs @@ -177,8 +177,6 @@ namespace Ryujinx ? appDataConfigurationPath : null; - bool showVulkanPrompt = false; - if (ConfigurationPath == null) { // No configuration, we load the default values and save it to disk @@ -186,26 +184,17 @@ namespace Ryujinx ConfigurationState.Instance.LoadDefault(); ConfigurationState.Instance.ToFileFormat().SaveConfig(ConfigurationPath); - - showVulkanPrompt = true; } else { if (ConfigurationFileFormat.TryLoad(ConfigurationPath, out ConfigurationFileFormat configurationFileFormat)) { - ConfigurationLoadResult result = ConfigurationState.Instance.Load(configurationFileFormat, ConfigurationPath); - - if ((result & ConfigurationLoadResult.MigratedFromPreVulkan) != 0) - { - showVulkanPrompt = true; - } + ConfigurationState.Instance.Load(configurationFileFormat, ConfigurationPath); } else { ConfigurationState.Instance.LoadDefault(); - showVulkanPrompt = true; - Logger.Warning?.PrintMsg(LogClass.Application, $"Failed to load config! Loading the default config instead.\nFailed config location {ConfigurationPath}"); } } @@ -216,12 +205,10 @@ namespace Ryujinx if (CommandLineState.OverrideGraphicsBackend.ToLower() == "opengl") { ConfigurationState.Instance.Graphics.GraphicsBackend.Value = GraphicsBackend.OpenGl; - showVulkanPrompt = false; } else if (CommandLineState.OverrideGraphicsBackend.ToLower() == "vulkan") { ConfigurationState.Instance.Graphics.GraphicsBackend.Value = GraphicsBackend.Vulkan; - showVulkanPrompt = false; } } @@ -343,35 +330,6 @@ namespace Ryujinx }, TaskContinuationOptions.OnlyOnFaulted); } - if (showVulkanPrompt) - { - var buttonTexts = new Dictionary() - { - { 0, "Yes (Vulkan)" }, - { 1, "No (OpenGL)" }, - }; - - ResponseType response = GtkDialog.CreateCustomDialog( - "Ryujinx - Default graphics backend", - "Use Vulkan as default graphics backend?", - "Ryujinx now supports the Vulkan API. " + - "Vulkan greatly improves shader compilation performance, " + - "and fixes some graphical glitches; however, since it is a new feature, " + - "you may experience some issues that did not occur with OpenGL.\n\n" + - "Note that you will also lose any existing shader cache the first time you start a game " + - "on version 1.1.200 onwards, because Vulkan required changes to the shader cache that makes it incompatible with previous versions.\n\n" + - "Would you like to set Vulkan as the default graphics backend? " + - "You can change this at any time on the settings window.", - buttonTexts, - MessageType.Question); - - ConfigurationState.Instance.Graphics.GraphicsBackend.Value = response == 0 - ? GraphicsBackend.Vulkan - : GraphicsBackend.OpenGl; - - ConfigurationState.Instance.ToFileFormat().SaveConfig(ConfigurationPath); - } - Application.Run(); } From 0b58f462668694db1a035e8be40d2a6d366635e1 Mon Sep 17 00:00:00 2001 From: gdkchan Date: Sun, 19 Nov 2023 15:10:44 -0300 Subject: [PATCH 22/23] Extend bindless elimination to see through Phis with the same results (#5957) * Extend bindless elimination to see through Phis with the same results * Shader cache version bump --- .../Shader/DiskCache/DiskCacheHostStorage.cs | 2 +- .../Optimizations/BindlessElimination.cs | 58 ++++++++++++++++++- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs index 0dc4b1a72..403e039a4 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs @@ -22,7 +22,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache private const ushort FileFormatVersionMajor = 1; private const ushort FileFormatVersionMinor = 2; private const uint FileFormatVersionPacked = ((uint)FileFormatVersionMajor << 16) | FileFormatVersionMinor; - private const uint CodeGenVersion = 5791; + private const uint CodeGenVersion = 5957; private const string SharedTocFileName = "shared.toc"; private const string SharedDataFileName = "shared.data"; diff --git a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs index 19b7999a7..c955f5b5f 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs @@ -55,7 +55,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations continue; } - if (bindlessHandle.AsgOp is not Operation handleCombineOp) + if (!TryGetOperation(bindlessHandle.AsgOp, out Operation handleCombineOp)) { continue; } @@ -199,9 +199,64 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations } } + private static bool TryGetOperation(INode asgOp, out Operation outOperation) + { + if (asgOp is PhiNode phi) + { + // If we have a phi, let's check if all inputs are effectively the same value. + // If so, we can "see through" the phi and pick any of the inputs (since they are all the same). + + Operand firstSrc = phi.GetSource(0); + + for (int index = 1; index < phi.SourcesCount; index++) + { + if (!IsSameOperand(firstSrc, phi.GetSource(index))) + { + outOperation = null; + + return false; + } + } + + asgOp = firstSrc.AsgOp; + } + + if (asgOp is Operation operation) + { + outOperation = operation; + + return true; + } + + outOperation = null; + + return false; + } + + private static bool IsSameOperand(Operand x, Operand y) + { + if (x.Type == y.Type && x.Type == OperandType.LocalVariable) + { + return x.AsgOp is Operation xOp && + y.AsgOp is Operation yOp && + xOp.Inst == Instruction.BitwiseOr && + yOp.Inst == Instruction.BitwiseOr && + AreBothEqualConstantBuffers(xOp.GetSource(0), yOp.GetSource(0)) && + AreBothEqualConstantBuffers(xOp.GetSource(1), yOp.GetSource(1)); + } + + return false; + } + + private static bool AreBothEqualConstantBuffers(Operand x, Operand y) + { + return x.Type == y.Type && x.Value == y.Value && x.Type == OperandType.ConstantBuffer; + } + private static Operand GetSourceForMaskedHandle(Operation asgOp, uint mask) { // Assume it was already checked that the operation is bitwise AND. + Operand src0 = asgOp.GetSource(0); Operand src1 = asgOp.GetSource(1); @@ -210,6 +265,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations // We can't check if the mask matches here as both operands are from a constant buffer. // Be optimistic and assume it matches. Avoid constant buffer 1 as official drivers // uses this one to store compiler constants. + return src0.GetCbufSlot() == 1 ? src1 : src0; } else if (src0.Type == OperandType.ConstantBuffer && src1.Type == OperandType.Constant) From 70d65d3d8e77e66226ebab7f23d9b6e8c271319f Mon Sep 17 00:00:00 2001 From: gdkchan Date: Sun, 19 Nov 2023 15:27:34 -0300 Subject: [PATCH 23/23] Enable copy dependency between RGBA8 and RGBA32 formats (#5943) * Enable copy dependency between RGBA8 and RGBA32 formats * Take packed flag into account for texture formats * Account for widths not being a multiple of each other * Don't try to alias depth textures as color, fix log condition * PR feedback --- src/Ryujinx.Graphics.Gpu/Image/FormatTable.cs | 30 +++++++++++++++++-- .../Image/TextureCompatibility.cs | 20 +++++++++++-- src/Ryujinx.Graphics.Gpu/Image/TexturePool.cs | 2 +- 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/src/Ryujinx.Graphics.Gpu/Image/FormatTable.cs b/src/Ryujinx.Graphics.Gpu/Image/FormatTable.cs index 1b517e63f..0af0725a2 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/FormatTable.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/FormatTable.cs @@ -651,9 +651,35 @@ namespace Ryujinx.Graphics.Gpu.Image /// True if the format is valid, false otherwise public static bool TryGetTextureFormat(uint encoded, bool isSrgb, out FormatInfo format) { - encoded |= (isSrgb ? 1u << 19 : 0u); + bool isPacked = (encoded & 0x80000000u) != 0; + if (isPacked) + { + encoded &= ~0x80000000u; + } - return _textureFormats.TryGetValue((TextureFormat)encoded, out format); + encoded |= isSrgb ? 1u << 19 : 0u; + + bool found = _textureFormats.TryGetValue((TextureFormat)encoded, out format); + + if (found && isPacked && !format.Format.IsDepthOrStencil()) + { + // If the packed flag is set, then the components of the pixel are tightly packed into the + // GPU registers on the shader. + // We can get the same behaviour by aliasing the texture as a format with the same amount of + // bytes per pixel, but only a single or the lowest possible number of components. + + format = format.BytesPerPixel switch + { + 1 => new FormatInfo(Format.R8Unorm, 1, 1, 1, 1), + 2 => new FormatInfo(Format.R16Unorm, 1, 1, 2, 1), + 4 => new FormatInfo(Format.R32Float, 1, 1, 4, 1), + 8 => new FormatInfo(Format.R32G32Float, 1, 1, 8, 2), + 16 => new FormatInfo(Format.R32G32B32A32Float, 1, 1, 16, 4), + _ => format, + }; + } + + return found; } /// diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs index 5af0471c0..eef38948d 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs @@ -2,6 +2,8 @@ using Ryujinx.Common; using Ryujinx.Graphics.GAL; using Ryujinx.Graphics.Texture; using System; +using System.Diagnostics; +using System.Numerics; namespace Ryujinx.Graphics.Gpu.Image { @@ -339,7 +341,20 @@ namespace Ryujinx.Graphics.Gpu.Image if (lhs.FormatInfo.BytesPerPixel != rhs.FormatInfo.BytesPerPixel && IsIncompatibleFormatAliasingAllowed(lhs.FormatInfo, rhs.FormatInfo)) { - alignedWidthMatches = lhsSize.Width * lhs.FormatInfo.BytesPerPixel == rhsSize.Width * rhs.FormatInfo.BytesPerPixel; + // If the formats are incompatible, but the texture strides match, + // we might allow them to be copy compatible depending on the format. + // The strides are aligned because the format with higher bytes per pixel + // might need a bit of padding at the end due to one width not being a multiple of the other. + + Debug.Assert((1 << BitOperations.Log2((uint)lhs.FormatInfo.BytesPerPixel)) == lhs.FormatInfo.BytesPerPixel); + Debug.Assert((1 << BitOperations.Log2((uint)rhs.FormatInfo.BytesPerPixel)) == rhs.FormatInfo.BytesPerPixel); + + int alignment = Math.Max(lhs.FormatInfo.BytesPerPixel, rhs.FormatInfo.BytesPerPixel); + + int lhsStride = BitUtils.AlignUp(lhsSize.Width * lhs.FormatInfo.BytesPerPixel, alignment); + int rhsStride = BitUtils.AlignUp(rhsSize.Width * rhs.FormatInfo.BytesPerPixel, alignment); + + alignedWidthMatches = lhsStride == rhsStride; } TextureViewCompatibility result = TextureViewCompatibility.Full; @@ -718,7 +733,8 @@ namespace Ryujinx.Graphics.Gpu.Image (lhsFormat, rhsFormat) = (rhsFormat, lhsFormat); } - return lhsFormat.Format == Format.R8Unorm && rhsFormat.Format == Format.R8G8B8A8Unorm; + return (lhsFormat.Format == Format.R8G8B8A8Unorm && rhsFormat.Format == Format.R32G32B32A32Float) || + (lhsFormat.Format == Format.R8Unorm && rhsFormat.Format == Format.R8G8B8A8Unorm); } /// diff --git a/src/Ryujinx.Graphics.Gpu/Image/TexturePool.cs b/src/Ryujinx.Graphics.Gpu/Image/TexturePool.cs index 0fdb6cd64..a4035577d 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TexturePool.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TexturePool.cs @@ -430,7 +430,7 @@ namespace Ryujinx.Graphics.Gpu.Image if (!FormatTable.TryGetTextureFormat(format, srgb, out FormatInfo formatInfo)) { - if (gpuVa != 0 && (int)format > 0) + if (gpuVa != 0 && format != 0) { Logger.Error?.Print(LogClass.Gpu, $"Invalid texture format 0x{format:X} (sRGB: {srgb})."); }