From 9800da36a249e7f6a97a7376eaf87016226134bd Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Sun, 4 Jun 2023 19:14:55 +0100 Subject: [PATCH 01/43] =?UTF-8?q?Let=E2=80=99s=20start=20again?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Ryujinx.Ava/Assets/Locales/en_US.json | 3 + .../UI/Controls/ApplicationContextMenu.axaml | 4 + .../Controls/ApplicationContextMenu.axaml.cs | 10 + src/Ryujinx.Ava/UI/Models/ModModel.cs | 31 +++ .../UI/ViewModels/ModManagerViewModel.cs | 124 ++++++++++++ .../UI/Windows/ModManagerWindow.axaml | 180 ++++++++++++++++++ .../UI/Windows/ModManagerWindow.axaml.cs | 115 +++++++++++ 7 files changed, 467 insertions(+) create mode 100644 src/Ryujinx.Ava/UI/Models/ModModel.cs create mode 100644 src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs create mode 100644 src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml create mode 100644 src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs diff --git a/src/Ryujinx.Ava/Assets/Locales/en_US.json b/src/Ryujinx.Ava/Assets/Locales/en_US.json index 72b5e8e3c..40fbb6eec 100644 --- a/src/Ryujinx.Ava/Assets/Locales/en_US.json +++ b/src/Ryujinx.Ava/Assets/Locales/en_US.json @@ -506,6 +506,8 @@ "EnableInternetAccessTooltip": "Allows the emulated application to connect to the Internet.\n\nGames with a LAN mode can connect to each other when this is enabled and the systems are connected to the same access point. This includes real consoles as well.\n\nDoes NOT allow connecting to Nintendo servers. May cause crashing in certain games that try to connect to the Internet.\n\nLeave OFF if unsure.", "GameListContextMenuManageCheatToolTip": "Manage Cheats", "GameListContextMenuManageCheat": "Manage Cheats", + "GameListContextMenuManageModToolTip": "Manage Mods", + "GameListContextMenuManageMod": "Manage Mods", "ControllerSettingsStickRange": "Range:", "DialogStopEmulationTitle": "Ryujinx - Stop Emulation", "DialogStopEmulationMessage": "Are you sure you want to stop emulation?", @@ -597,6 +599,7 @@ "CheatWindowHeading": "Cheats Available for {0} [{1}]", "BuildId": "BuildId:", "DlcWindowHeading": "{0} Downloadable Content(s)", + "ModWindowHeading": "{0} Mod(s)", "UserProfilesEditProfile": "Edit Selected", "Cancel": "Cancel", "Save": "Save", diff --git a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml index b8fe7e76f..46c37ef2a 100644 --- a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml +++ b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml @@ -46,6 +46,10 @@ Click="OpenCheatManager_Click" Header="{locale:Locale GameListContextMenuManageCheat}" ToolTip.Tip="{locale:Locale GameListContextMenuManageCheatToolTip}" /> + _enabled; + set + { + _enabled = value; + OnPropertyChanged(); + } + } + + public string ContainerPath { get; } + public string FullPath { get; } + public string FileName => Path.GetFileName(ContainerPath); + + public ModModel(string containerPath, string fullPath, bool enabled) + { + ContainerPath = containerPath; + FullPath = fullPath; + Enabled = enabled; + } + } +} \ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs new file mode 100644 index 000000000..2ad3ddd64 --- /dev/null +++ b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs @@ -0,0 +1,124 @@ +using Avalonia.Collections; +using DynamicData; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Models; + +namespace Ryujinx.Ava.UI.ViewModels +{ + public class ModManagerViewModel : BaseModel + { + public AvaloniaList _mods = new(); + public AvaloniaList _views = new(); + public AvaloniaList _selectedMods = new(); + + private string _search; + + public AvaloniaList Mods + { + get => _mods; + set + { + _mods = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(ModCount)); + Sort(); + } + } + + public AvaloniaList Views + { + get => _views; + set + { + _views = value; + OnPropertyChanged(); + } + } + + public AvaloniaList SelectedMods + { + get => _selectedMods; + set + { + _selectedMods = value; + OnPropertyChanged(); + } + } + + public string Search + { + get => _search; + set + { + _search = value; + OnPropertyChanged(); + Sort(); + } + } + + public string ModCount + { + get => string.Format(LocaleManager.Instance[LocaleKeys.ModWindowHeading], Mods.Count); + } + + public ModManagerViewModel() + { + LoadMods(); + } + + private void LoadMods() + { + + } + + public void Sort() + { + Mods.AsObservableChangeSet() + .Filter(Filter) + .Bind(out var view).AsObservableList(); + + _views.Clear(); + _views.AddRange(view); + OnPropertyChanged(nameof(ModCount)); + } + + private bool Filter(object arg) + { + if (arg is ModModel content) + { + return string.IsNullOrWhiteSpace(_search) || content.FileName.ToLower().Contains(_search.ToLower()); + } + + return false; + } + + public void Save() + { + + } + + public void Remove(ModModel model) + { + Mods.Remove(model); + OnPropertyChanged(nameof(ModCount)); + Sort(); + } + + public void RemoveAll() + { + Mods.Clear(); + OnPropertyChanged(nameof(ModCount)); + Sort(); + } + + public void EnableAll() + { + SelectedMods = new(Mods); + } + + public void DisableAll() + { + SelectedMods.Clear(); + } + } +} \ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml b/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml new file mode 100644 index 000000000..d98cf0bde --- /dev/null +++ b/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs new file mode 100644 index 000000000..3e64c2c96 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs @@ -0,0 +1,115 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Styling; +using FluentAvalonia.UI.Controls; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.Models; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.HLE.FileSystem; +using Ryujinx.Ui.Common.Helper; +using System.Threading.Tasks; +using Button = Avalonia.Controls.Button; + +namespace Ryujinx.Ava.UI.Windows +{ + public partial class ModManagerWindow : UserControl + { + public ModManagerViewModel ViewModel; + + public ModManagerWindow() + { + DataContext = this; + + InitializeComponent(); + } + + public ModManagerWindow(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName) + { + DataContext = ViewModel = new ModManagerViewModel(); + + InitializeComponent(); + } + + public static async Task Show(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName) + { + ContentDialog contentDialog = new() + { + PrimaryButtonText = "", + SecondaryButtonText = "", + CloseButtonText = "", + Content = new ModManagerWindow(virtualFileSystem, titleId, titleName), + Title = string.Format(LocaleManager.Instance[LocaleKeys.ModWindowHeading], titleName, titleId.ToString("X16")) + }; + + Style bottomBorder = new(x => x.OfType().Name("DialogSpace").Child().OfType()); + bottomBorder.Setters.Add(new Setter(IsVisibleProperty, false)); + + contentDialog.Styles.Add(bottomBorder); + + await ContentDialogHelper.ShowAsync(contentDialog); + } + + private void SaveAndClose(object sender, RoutedEventArgs e) + { + ViewModel.Save(); + ((ContentDialog)Parent).Hide(); + } + + private void Close(object sender, RoutedEventArgs e) + { + ((ContentDialog)Parent).Hide(); + } + + private void RemoveMod(object sender, RoutedEventArgs e) + { + if (sender is Button button) + { + if (button.DataContext is ModModel model) + { + ViewModel.Remove(model); + } + } + } + + private void OpenLocation(object sender, RoutedEventArgs e) + { + if (sender is Button button) + { + if (button.DataContext is ModModel model) + { + OpenHelper.LocateFile(model.ContainerPath); + } + } + } + + private void OnSelectionChanged(object sender, SelectionChangedEventArgs e) + { + foreach (var content in e.AddedItems) + { + if (content is ModModel model) + { + var index = ViewModel.Mods.IndexOf(model); + + if (index != -1) + { + ViewModel.Mods[index].Enabled = true; + } + } + } + + foreach (var content in e.RemovedItems) + { + if (content is ModModel model) + { + var index = ViewModel.Mods.IndexOf(model); + + if (index != -1) + { + ViewModel.Mods[index].Enabled = false; + } + } + } + } + } +} \ No newline at end of file From fe74d3b773f0b0dc29c909835627d1f93956cb96 Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Sun, 4 Jun 2023 22:08:30 +0100 Subject: [PATCH 02/43] Read folders and such --- src/Ryujinx.Ava/Assets/Locales/en_US.json | 2 + src/Ryujinx.Ava/UI/Models/ModModel.cs | 11 +- .../UI/ViewModels/ModManagerViewModel.cs | 127 +++++++++++++++++- .../UI/Windows/ModManagerWindow.axaml | 2 +- .../UI/Windows/ModManagerWindow.axaml.cs | 4 +- src/Ryujinx.Common/Configuration/Mod.cs | 8 ++ .../Configuration/ModMetadata.cs | 9 ++ .../ModMetadataJsonSerializerContext.cs | 10 ++ 8 files changed, 160 insertions(+), 13 deletions(-) create mode 100644 src/Ryujinx.Common/Configuration/Mod.cs create mode 100644 src/Ryujinx.Common/Configuration/ModMetadata.cs create mode 100644 src/Ryujinx.Common/Configuration/ModMetadataJsonSerializerContext.cs diff --git a/src/Ryujinx.Ava/Assets/Locales/en_US.json b/src/Ryujinx.Ava/Assets/Locales/en_US.json index 40fbb6eec..41ff5b1ec 100644 --- a/src/Ryujinx.Ava/Assets/Locales/en_US.json +++ b/src/Ryujinx.Ava/Assets/Locales/en_US.json @@ -388,6 +388,7 @@ "DialogControllerSettingsModifiedConfirmMessage": "The current controller settings has been updated.", "DialogControllerSettingsModifiedConfirmSubMessage": "Do you want to save?", "DialogLoadNcaErrorMessage": "{0}. Errored File: {1}", + "DialogLoadModErrorMessage": "{0}. Errored File: {1}", "DialogDlcNoDlcErrorMessage": "The specified file does not contain a DLC for the selected title!", "DialogPerformanceCheckLoggingEnabledMessage": "You have trace logging enabled, which is designed to be used by developers only.", "DialogPerformanceCheckLoggingEnabledConfirmMessage": "For optimal performance, it's recommended to disable trace logging. Would you like to disable trace logging now?", @@ -592,6 +593,7 @@ "Writable": "Writable", "SelectDlcDialogTitle": "Select DLC files", "SelectUpdateDialogTitle": "Select update files", + "SelectModDialogTitle": "Select mod directory", "UserProfileWindowTitle": "User Profiles Manager", "CheatWindowTitle": "Cheats Manager", "DlcWindowTitle": "Manage Downloadable Content for {0} ({1})", diff --git a/src/Ryujinx.Ava/UI/Models/ModModel.cs b/src/Ryujinx.Ava/UI/Models/ModModel.cs index 35b035b7f..4b3394dd4 100644 --- a/src/Ryujinx.Ava/UI/Models/ModModel.cs +++ b/src/Ryujinx.Ava/UI/Models/ModModel.cs @@ -17,14 +17,13 @@ public bool Enabled } } - public string ContainerPath { get; } - public string FullPath { get; } - public string FileName => Path.GetFileName(ContainerPath); + public string Path { get; } + public string Name { get; } - public ModModel(string containerPath, string fullPath, bool enabled) + public ModModel(string path, string name, bool enabled) { - ContainerPath = containerPath; - FullPath = fullPath; + Path = path; + Name = name; Enabled = enabled; } } diff --git a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs index 2ad3ddd64..44c4a564c 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs @@ -1,18 +1,38 @@ using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Threading; using DynamicData; using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Models; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Logging; +using Ryujinx.Common.Utilities; +using Ryujinx.HLE.HOS; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; namespace Ryujinx.Ava.UI.ViewModels { public class ModManagerViewModel : BaseModel { + public ModMetadata _modData; + public readonly string _modJsonPath; + public AvaloniaList _mods = new(); public AvaloniaList _views = new(); public AvaloniaList _selectedMods = new(); + private ulong _titleId { get; } + private string _titleName { get; } + private string _search; + private static readonly ModMetadataJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + public AvaloniaList Mods { get => _mods; @@ -61,14 +81,64 @@ public string ModCount get => string.Format(LocaleManager.Instance[LocaleKeys.ModWindowHeading], Mods.Count); } - public ModManagerViewModel() + public ModManagerViewModel(ulong titleId, string titleName) { - LoadMods(); + _titleId = titleId; + _titleName = titleName; + + _modJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "mods.json"); + + try + { + _modData = JsonHelper.DeserializeFromFile(_modJsonPath, SerializerContext.ModMetadata); + } + catch + { + Logger.Warning?.Print(LogClass.Application, $"Failed to deserialize mod data for {_titleId} at {_modJsonPath}"); + + _modData = new ModMetadata + { + Mods = new List() + }; + + Save(); + } + + LoadMods(titleId); } - private void LoadMods() + private void LoadMods(ulong titleId) { + string modsBasePath = ModLoader.GetSdModsBasePath(); + + var modCache = new ModLoader.ModCache(); + ModLoader.QueryContentsDir(modCache, new DirectoryInfo(Path.Combine(modsBasePath, "contents")), titleId); + + foreach (var mod in modCache.RomfsDirs) + { + var enabled = _modData.Mods.Exists(x => x.Path == mod.Path.FullName); + Mods.Add(new ModModel(mod.Path.FullName, mod.Name, enabled)); + } + + foreach (var mod in modCache.RomfsContainers) + { + var enabled = _modData.Mods.Exists(x => x.Path == mod.Path.FullName); + Mods.Add(new ModModel(mod.Path.FullName, mod.Name, enabled)); + } + + foreach (var mod in modCache.ExefsDirs) + { + var enabled = _modData.Mods.Exists(x => x.Path == mod.Path.FullName); + Mods.Add(new ModModel(mod.Path.FullName, mod.Name, enabled)); + } + + foreach (var mod in modCache.ExefsContainers) + { + var enabled = _modData.Mods.Exists(x => x.Path == mod.Path.FullName); + Mods.Add(new ModModel(mod.Path.FullName, mod.Name, enabled)); + } + Sort(); } public void Sort() @@ -86,7 +156,7 @@ private bool Filter(object arg) { if (arg is ModModel content) { - return string.IsNullOrWhiteSpace(_search) || content.FileName.ToLower().Contains(_search.ToLower()); + return string.IsNullOrWhiteSpace(_search) || content.Name.ToLower().Contains(_search.ToLower()); } return false; @@ -94,7 +164,18 @@ private bool Filter(object arg) public void Save() { + _modData.Mods.Clear(); + foreach (ModModel mod in SelectedMods) + { + _modData.Mods.Add(new Mod + { + Name = mod.Name, + Path = mod.Path, + }); + } + + JsonHelper.SerializeToFile(_modJsonPath, _modData, SerializerContext.ModMetadata); } public void Remove(ModModel model) @@ -104,6 +185,44 @@ public void Remove(ModModel model) Sort(); } + private void AddMod(string directory) + { + if (Directory.Exists(directory) && Mods.All(x => !x.Path.Contains(directory))) + { + using FileStream file = new(directory, FileMode.Open, FileAccess.Read); + + try + { + + } + catch (Exception ex) + { + Dispatcher.UIThread.Post(async () => + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogLoadModErrorMessage, ex.Message, directory)); + }); + } + } + } + + public async void Add() + { + OpenFolderDialog dialog = new() + { + Title = LocaleManager.Instance[LocaleKeys.SelectModDialogTitle] + }; + + if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + string directory = await dialog.ShowAsync(desktop.MainWindow); + + if (directory != null) + { + AddMod(directory); + } + } + } + public void RemoveAll() { Mods.Clear(); diff --git a/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml b/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml index d98cf0bde..ad054ed52 100644 --- a/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml +++ b/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml @@ -92,7 +92,7 @@ MaxLines="2" TextWrapping="Wrap" TextTrimming="CharacterEllipsis" - Text="{Binding FileName}" /> + Text="{Binding Name}" /> Mods { get; set; } + } +} \ No newline at end of file diff --git a/src/Ryujinx.Common/Configuration/ModMetadataJsonSerializerContext.cs b/src/Ryujinx.Common/Configuration/ModMetadataJsonSerializerContext.cs new file mode 100644 index 000000000..f1cd2cf21 --- /dev/null +++ b/src/Ryujinx.Common/Configuration/ModMetadataJsonSerializerContext.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Serialization; + +namespace Ryujinx.Common.Configuration +{ + [JsonSourceGenerationOptions(WriteIndented = true)] + [JsonSerializable(typeof(ModMetadata))] + public partial class ModMetadataJsonSerializerContext : JsonSerializerContext + { + } +} \ No newline at end of file From 98b3cd1fe181287ebdfd0dabb1f1ea43ca4cc326 Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Mon, 5 Jun 2023 00:33:45 +0100 Subject: [PATCH 03/43] Remove Open Mod Folder menu items --- src/Ryujinx.Ava/Assets/Locales/en_US.json | 4 --- .../UI/Controls/ApplicationContextMenu.axaml | 8 ------ .../Controls/ApplicationContextMenu.axaml.cs | 26 ------------------- 3 files changed, 38 deletions(-) diff --git a/src/Ryujinx.Ava/Assets/Locales/en_US.json b/src/Ryujinx.Ava/Assets/Locales/en_US.json index 41ff5b1ec..33c33299f 100644 --- a/src/Ryujinx.Ava/Assets/Locales/en_US.json +++ b/src/Ryujinx.Ava/Assets/Locales/en_US.json @@ -54,8 +54,6 @@ "GameListContextMenuManageTitleUpdatesToolTip": "Opens the Title Update management window", "GameListContextMenuManageDlc": "Manage DLC", "GameListContextMenuManageDlcToolTip": "Opens the DLC management window", - "GameListContextMenuOpenModsDirectory": "Open Mods Directory", - "GameListContextMenuOpenModsDirectoryToolTip": "Opens the directory which contains Application's Mods", "GameListContextMenuCacheManagement": "Cache Management", "GameListContextMenuCacheManagementPurgePptc": "Queue PPTC Rebuild", "GameListContextMenuCacheManagementPurgePptcToolTip": "Trigger PPTC to rebuild at boot time on the next game launch", @@ -520,8 +518,6 @@ "SettingsTabCpuMemory": "CPU Mode", "DialogUpdaterFlatpakNotSupportedMessage": "Please update Ryujinx via FlatHub.", "UpdaterDisabledWarningTitle": "Updater Disabled!", - "GameListContextMenuOpenSdModsDirectory": "Open Atmosphere Mods Directory", - "GameListContextMenuOpenSdModsDirectoryToolTip": "Opens the alternative SD card Atmosphere directory which contains Application's Mods. Useful for mods that are packaged for real hardware.", "ControllerSettingsRotate90": "Rotate 90° Clockwise", "IconSize": "Icon Size", "IconSizeTooltip": "Change the size of game icons", diff --git a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml index 46c37ef2a..7f786cf38 100644 --- a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml +++ b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml @@ -50,14 +50,6 @@ Click="OpenModManager_Click" Header="{locale:Locale GameListContextMenuManageMod}" ToolTip.Tip="{locale:Locale GameListContextMenuManageModToolTip}" /> - - Date: Mon, 5 Jun 2023 00:44:52 +0100 Subject: [PATCH 04/43] Fix folder opening, Selecting/deselecting --- src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs | 2 ++ src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs index 44c4a564c..364b565c9 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs @@ -138,6 +138,8 @@ private void LoadMods(ulong titleId) Mods.Add(new ModModel(mod.Path.FullName, mod.Name, enabled)); } + SelectedMods = new(Mods.Where(x => x.Enabled)); + Sort(); } diff --git a/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs index 18f25f997..bd8c9f4c3 100644 --- a/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs +++ b/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs @@ -78,7 +78,7 @@ private void OpenLocation(object sender, RoutedEventArgs e) { if (button.DataContext is ModModel model) { - OpenHelper.LocateFile(model.Path); + OpenHelper.OpenFolder(model.Path); } } } From 99b7fe27c58a50e8eebac92b90c06a6da9814395 Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Mon, 5 Jun 2023 14:02:24 +0100 Subject: [PATCH 05/43] She works --- .../UI/ViewModels/ModManagerViewModel.cs | 14 +-- .../Configuration/AppDataManager.cs | 4 +- src/Ryujinx.HLE/HOS/ModLoader.cs | 103 +++++++++++++----- .../Extensions/LocalFileSystemExtensions.cs | 5 +- .../Processes/Extensions/NcaExtensions.cs | 4 +- 5 files changed, 86 insertions(+), 44 deletions(-) diff --git a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs index 364b565c9..431f14dbc 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs @@ -109,33 +109,29 @@ public ModManagerViewModel(ulong titleId, string titleName) private void LoadMods(ulong titleId) { - string modsBasePath = ModLoader.GetSdModsBasePath(); + string modsBasePath = ModLoader.GetModsBasePath(); var modCache = new ModLoader.ModCache(); ModLoader.QueryContentsDir(modCache, new DirectoryInfo(Path.Combine(modsBasePath, "contents")), titleId); foreach (var mod in modCache.RomfsDirs) { - var enabled = _modData.Mods.Exists(x => x.Path == mod.Path.FullName); - Mods.Add(new ModModel(mod.Path.FullName, mod.Name, enabled)); + Mods.Add(new ModModel(mod.Path.FullName, mod.Name, mod.Enabled)); } foreach (var mod in modCache.RomfsContainers) { - var enabled = _modData.Mods.Exists(x => x.Path == mod.Path.FullName); - Mods.Add(new ModModel(mod.Path.FullName, mod.Name, enabled)); + Mods.Add(new ModModel(mod.Path.FullName, mod.Name, mod.Enabled)); } foreach (var mod in modCache.ExefsDirs) { - var enabled = _modData.Mods.Exists(x => x.Path == mod.Path.FullName); - Mods.Add(new ModModel(mod.Path.FullName, mod.Name, enabled)); + Mods.Add(new ModModel(mod.Path.FullName, mod.Name, mod.Enabled)); } foreach (var mod in modCache.ExefsContainers) { - var enabled = _modData.Mods.Exists(x => x.Path == mod.Path.FullName); - Mods.Add(new ModModel(mod.Path.FullName, mod.Name, enabled)); + Mods.Add(new ModModel(mod.Path.FullName, mod.Name, mod.Enabled)); } SelectedMods = new(Mods.Where(x => x.Enabled)); diff --git a/src/Ryujinx.Common/Configuration/AppDataManager.cs b/src/Ryujinx.Common/Configuration/AppDataManager.cs index 2b4a594d3..350a37a98 100644 --- a/src/Ryujinx.Common/Configuration/AppDataManager.cs +++ b/src/Ryujinx.Common/Configuration/AppDataManager.cs @@ -31,7 +31,6 @@ public enum LaunchMode public const string DefaultNandDir = "bis"; public const string DefaultSdcardDir = "sdcard"; - private const string DefaultModsDir = "mods"; public static string CustomModsPath { get; set; } public static string CustomSdModsPath { get; set; } @@ -151,7 +150,6 @@ private static void CopyDirectory(string sourceDir, string destinationDir) } } - public static string GetModsPath() => CustomModsPath ?? Directory.CreateDirectory(Path.Combine(BaseDirPath, DefaultModsDir)).FullName; - public static string GetSdModsPath() => CustomSdModsPath ?? Directory.CreateDirectory(Path.Combine(BaseDirPath, DefaultSdcardDir, "atmosphere")).FullName; + public static string GetModsPath() => CustomModsPath ?? Directory.CreateDirectory(Path.Combine(BaseDirPath, DefaultSdcardDir, "atmosphere")).FullName; } } diff --git a/src/Ryujinx.HLE/HOS/ModLoader.cs b/src/Ryujinx.HLE/HOS/ModLoader.cs index 834bc0595..d2e945880 100644 --- a/src/Ryujinx.HLE/HOS/ModLoader.cs +++ b/src/Ryujinx.HLE/HOS/ModLoader.cs @@ -7,6 +7,7 @@ using LibHac.Tools.FsSystem.RomFs; using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; +using Ryujinx.Common.Utilities; using Ryujinx.HLE.HOS.Kernel.Process; using Ryujinx.HLE.Loaders.Executables; using Ryujinx.HLE.Loaders.Mods; @@ -37,15 +38,19 @@ public class ModLoader private const string AmsNroPatchDir = "nro_patches"; private const string AmsKipPatchDir = "kip_patches"; + private static readonly ModMetadataJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + public readonly struct Mod where T : FileSystemInfo { public readonly string Name; public readonly T Path; + public readonly bool Enabled; - public Mod(string name, T path) + public Mod(string name, T path, bool enabled) { Name = name; Path = path; + Enabled = enabled; } } @@ -138,7 +143,6 @@ private void Clear() private static bool StrEquals(string s1, string s2) => string.Equals(s1, s2, StringComparison.OrdinalIgnoreCase); public static string GetModsBasePath() => EnsureBaseDirStructure(AppDataManager.GetModsPath()); - public static string GetSdModsBasePath() => EnsureBaseDirStructure(AppDataManager.GetSdModsPath()); private static string EnsureBaseDirStructure(string modsBasePath) { @@ -160,19 +164,35 @@ private static void AddModsFromDirectory(ModCache mods, DirectoryInfo dir, strin { System.Text.StringBuilder types = new(); + string ModJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId, "mods.json"); + ModMetadata? ModData = null; + + try + { + ModData = JsonHelper.DeserializeFromFile(ModJsonPath, SerializerContext.ModMetadata); + } + catch + { + Logger.Warning?.Print(LogClass.ModLoader, $"Failed to deserialize mod data for {titleId} at {ModJsonPath}"); + } + foreach (var modDir in dir.EnumerateDirectories()) { types.Clear(); - Mod mod = new("", null); + Mod mod = new("", null, true); if (StrEquals(RomfsDir, modDir.Name)) { - mods.RomfsDirs.Add(mod = new Mod(dir.Name, modDir)); + bool enabled = !ModData.HasValue || ModData.Value.Mods.Exists(x => x.Path == modDir.FullName); + + mods.RomfsDirs.Add(mod = new Mod(dir.Name, modDir, enabled)); types.Append('R'); } else if (StrEquals(ExefsDir, modDir.Name)) { - mods.ExefsDirs.Add(mod = new Mod(dir.Name, modDir)); + bool enabled = !ModData.HasValue || ModData.Value.Mods.Exists(x => x.Path == modDir.FullName); + + mods.ExefsDirs.Add(mod = new Mod(dir.Name, modDir, enabled)); types.Append('E'); } else if (StrEquals(CheatDir, modDir.Name)) @@ -238,28 +258,44 @@ private static void QueryPatchDirs(PatchCache cache, DirectoryInfo patchDir) foreach (var modDir in patchDir.EnumerateDirectories()) { - patches.Add(new Mod(modDir.Name, modDir)); + patches.Add(new Mod(modDir.Name, modDir, true)); Logger.Info?.Print(LogClass.ModLoader, $"Found {type} patch '{modDir.Name}'"); } } - private static void QueryTitleDir(ModCache mods, DirectoryInfo titleDir) + private static void QueryTitleDir(ModCache mods, DirectoryInfo titleDir, ulong titleId) { if (!titleDir.Exists) { return; } + string ModJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "mods.json"); + ModMetadata? ModData = null; + + try + { + ModData = JsonHelper.DeserializeFromFile(ModJsonPath, SerializerContext.ModMetadata); + } + catch + { + Logger.Warning?.Print(LogClass.ModLoader, $"Failed to deserialize mod data for {titleId} at {ModJsonPath}"); + } + var fsFile = new FileInfo(Path.Combine(titleDir.FullName, RomfsContainer)); if (fsFile.Exists) { - mods.RomfsContainers.Add(new Mod($"<{titleDir.Name} RomFs>", fsFile)); + bool enabled = !ModData.HasValue || ModData.Value.Mods.Exists(x => x.Path == fsFile.FullName); + + mods.RomfsContainers.Add(new Mod($"<{titleDir.Name} RomFs>", fsFile, enabled)); } fsFile = new FileInfo(Path.Combine(titleDir.FullName, ExefsContainer)); if (fsFile.Exists) { - mods.ExefsContainers.Add(new Mod($"<{titleDir.Name} ExeFs>", fsFile)); + bool enabled = !ModData.HasValue || ModData.Value.Mods.Exists(x => x.Path == fsFile.FullName); + + mods.ExefsContainers.Add(new Mod($"<{titleDir.Name} ExeFs>", fsFile, enabled)); } AddModsFromDirectory(mods, titleDir, titleDir.Name); @@ -278,7 +314,7 @@ public static void QueryContentsDir(ModCache mods, DirectoryInfo contentsDir, ul if (titleDir != null) { - QueryTitleDir(mods, titleDir); + QueryTitleDir(mods, titleDir, titleId); } } @@ -375,7 +411,7 @@ private static IEnumerable GetCheatsInFile(FileInfo cheatFile) } // Assumes searchDirPaths don't overlap - private static void CollectMods(Dictionary modCaches, PatchCache patches, params string[] searchDirPaths) + private static void CollectMods(Dictionary modCaches, PatchCache patches) { static bool IsPatchesDir(string name) => StrEquals(AmsNsoPatchDir, name) || StrEquals(AmsNroPatchDir, name) || @@ -404,28 +440,25 @@ static bool TryQuery(DirectoryInfo searchDir, PatchCache patches, Dictionary titles, params string[] searchDirPaths) + public void CollectMods(IEnumerable titles) { Clear(); @@ -434,7 +467,7 @@ public void CollectMods(IEnumerable titles, params string[] searchDirPath _appMods[titleId] = new ModCache(); } - CollectMods(_appMods, _patches, searchDirPaths); + CollectMods(_appMods, _patches); } internal IStorage ApplyRomFsMods(ulong titleId, IStorage baseStorage) @@ -453,6 +486,11 @@ internal IStorage ApplyRomFsMods(ulong titleId, IStorage baseStorage) // Prioritize loose files first foreach (var mod in mods.RomfsDirs) { + if (!mod.Enabled) + { + continue; + } + using (IFileSystem fs = new LocalFileSystem(mod.Path.FullName)) { AddFiles(fs, mod.Name, fileSet, builder); @@ -463,6 +501,11 @@ internal IStorage ApplyRomFsMods(ulong titleId, IStorage baseStorage) // Then files inside images foreach (var mod in mods.RomfsContainers) { + if (!mod.Enabled) + { + continue; + } + Logger.Info?.Print(LogClass.ModLoader, $"Found 'romfs.bin' for Title {titleId:X16}"); using (IFileSystem fs = new RomFsFileSystem(mod.Path.OpenRead().AsStorage())) { @@ -571,6 +614,11 @@ internal ModLoadResult ApplyExefsMods(ulong titleId, NsoExecutable[] nsos) foreach (var mod in exeMods) { + if (!mod.Enabled) + { + continue; + } + for (int i = 0; i < ProcessConst.ExeFsPrefixes.Length; ++i) { var nsoName = ProcessConst.ExeFsPrefixes[i]; @@ -724,6 +772,11 @@ private static bool ApplyProgramPatches(IEnumerable> mods, in // Collect patches foreach (var mod in mods) { + if (!mod.Enabled) + { + continue; + } + var patchDir = mod.Path; foreach (var patchFile in patchDir.EnumerateFiles()) { diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/LocalFileSystemExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/LocalFileSystemExtensions.cs index 798a9261e..831c33ea6 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/LocalFileSystemExtensions.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/LocalFileSystemExtensions.cs @@ -16,10 +16,7 @@ public static ProcessResult Load(this LocalFileSystem exeFs, Switch device, stri var nacpData = new BlitStruct(1); ulong programId = metaLoader.GetProgramId(); - device.Configuration.VirtualFileSystem.ModLoader.CollectMods( - new[] { programId }, - ModLoader.GetModsBasePath(), - ModLoader.GetSdModsBasePath()); + device.Configuration.VirtualFileSystem.ModLoader.CollectMods(new[] { programId }); if (programId != 0) { diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs index e369f4b04..de396ae9d 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs @@ -35,9 +35,7 @@ public static ProcessResult Load(this Nca nca, Switch device, Nca patchNca, Nca // Collecting mods related to AocTitleIds and ProgramId. device.Configuration.VirtualFileSystem.ModLoader.CollectMods( - device.Configuration.ContentManager.GetAocTitleIds().Prepend(metaLoader.GetProgramId()), - ModLoader.GetModsBasePath(), - ModLoader.GetSdModsBasePath()); + device.Configuration.ContentManager.GetAocTitleIds().Prepend(metaLoader.GetProgramId())); // Load Nacp file. var nacpData = new BlitStruct(1); From a4b69c5fb883e14ef9e970ca906bed2639c8d7ee Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Mon, 5 Jun 2023 14:06:01 +0100 Subject: [PATCH 06/43] Fix GTK --- .../Ui/Widgets/GameTableContextMenu.Designer.cs | 11 ----------- src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs | 10 +--------- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs index 734437eea..370d22640 100644 --- a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs +++ b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs @@ -11,7 +11,6 @@ public partial class GameTableContextMenu : Menu private MenuItem _manageDlcMenuItem; private MenuItem _manageCheatMenuItem; private MenuItem _openTitleModDirMenuItem; - private MenuItem _openTitleSdModDirMenuItem; private Menu _extractSubMenu; private MenuItem _extractMenuItem; private MenuItem _extractRomFsMenuItem; @@ -90,15 +89,6 @@ private void InitializeComponent() }; _openTitleModDirMenuItem.Activated += OpenTitleModDir_Clicked; - // - // _openTitleSdModDirMenuItem - // - _openTitleSdModDirMenuItem = new MenuItem("Open Atmosphere Mods Directory") - { - TooltipText = "Open the alternative SD card atmosphere directory which contains the Application's Mods.", - }; - _openTitleSdModDirMenuItem.Activated += OpenTitleSdModDir_Clicked; - // // _extractSubMenu // @@ -221,7 +211,6 @@ private void ShowComponent() Add(_manageDlcMenuItem); Add(_manageCheatMenuItem); Add(_openTitleModDirMenuItem); - Add(_openTitleSdModDirMenuItem); Add(new SeparatorMenuItem()); Add(_manageCacheMenuItem); Add(_extractMenuItem); diff --git a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs index eb048b00d..425766b5a 100644 --- a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs +++ b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs @@ -496,15 +496,7 @@ private void ManageCheats_Clicked(object sender, EventArgs args) private void OpenTitleModDir_Clicked(object sender, EventArgs args) { - string modsBasePath = ModLoader.GetModsBasePath(); - string titleModsPath = ModLoader.GetTitleDir(modsBasePath, _titleIdText); - - OpenHelper.OpenFolder(titleModsPath); - } - - private void OpenTitleSdModDir_Clicked(object sender, EventArgs args) - { - string sdModsBasePath = ModLoader.GetSdModsBasePath(); + string sdModsBasePath = ModLoader.GetModsBasePath(); string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, _titleIdText); OpenHelper.OpenFolder(titleModsPath); From e8d523b8b56af10d115563e24144ac5a34dd476c Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Mon, 5 Jun 2023 16:17:08 +0100 Subject: [PATCH 07/43] AddMod --- .../UI/ViewModels/ModManagerViewModel.cs | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs index 431f14dbc..691252dde 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs @@ -183,24 +183,37 @@ public void Remove(ModModel model) Sort(); } - private void AddMod(string directory) + private void AddMod(DirectoryInfo directory) { - if (Directory.Exists(directory) && Mods.All(x => !x.Path.Contains(directory))) - { - using FileStream file = new(directory, FileMode.Open, FileAccess.Read); + var directories = Directory.GetDirectories(directory.ToString(), "*", SearchOption.AllDirectories); + var destinationDir = ModLoader.GetTitleDir(ModLoader.GetModsBasePath(), _titleId.ToString("x16")); - try - { + foreach (var dir in directories) + { + string dirToCreate = dir.Replace(directory.Parent.ToString(), destinationDir); - } - catch (Exception ex) + // Mod already exists + if (Directory.Exists(dirToCreate)) { Dispatcher.UIThread.Post(async () => { - await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogLoadModErrorMessage, ex.Message, directory)); + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogLoadModErrorMessage, "Director", dirToCreate)); }); + + return; } + + Directory.CreateDirectory(dirToCreate); } + + var files = Directory.GetFiles(directory.ToString(), "*", SearchOption.AllDirectories); + + foreach (var file in files) + { + File.Copy(file, file.Replace(directory.Parent.ToString(), destinationDir), true); + } + + LoadMods(_titleId); } public async void Add() @@ -216,7 +229,7 @@ public async void Add() if (directory != null) { - AddMod(directory); + AddMod(new DirectoryInfo(directory)); } } } From 83ef3315a3558f6c6d63114f0be4ffc4fd48f0f6 Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Mon, 5 Jun 2023 16:23:00 +0100 Subject: [PATCH 08/43] Delete --- src/Ryujinx.Ava/Assets/Locales/en_US.json | 1 + .../UI/ViewModels/ModManagerViewModel.cs | 13 +++++++++++-- src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml | 6 +++--- .../UI/Windows/ModManagerWindow.axaml.cs | 4 ++-- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/Ryujinx.Ava/Assets/Locales/en_US.json b/src/Ryujinx.Ava/Assets/Locales/en_US.json index 33c33299f..07c843142 100644 --- a/src/Ryujinx.Ava/Assets/Locales/en_US.json +++ b/src/Ryujinx.Ava/Assets/Locales/en_US.json @@ -431,6 +431,7 @@ "DlcManagerRemoveAllButton": "Remove All", "DlcManagerEnableAllButton": "Enable All", "DlcManagerDisableAllButton": "Disable All", + "ModManagerDeleteAllButton": "Delete All", "MenuBarOptionsChangeLanguage": "Change Language", "MenuBarShowFileTypes": "Show File Types", "CommonSort": "Sort", diff --git a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs index 691252dde..560aa33f6 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs @@ -109,6 +109,8 @@ public ModManagerViewModel(ulong titleId, string titleName) private void LoadMods(ulong titleId) { + Mods.Clear(); + string modsBasePath = ModLoader.GetModsBasePath(); var modCache = new ModLoader.ModCache(); @@ -176,8 +178,10 @@ public void Save() JsonHelper.SerializeToFile(_modJsonPath, _modData, SerializerContext.ModMetadata); } - public void Remove(ModModel model) + public void Delete(ModModel model) { + Directory.Delete(model.Path, true); + Mods.Remove(model); OnPropertyChanged(nameof(ModCount)); Sort(); @@ -234,8 +238,13 @@ public async void Add() } } - public void RemoveAll() + public void DeleteAll() { + foreach (var mod in Mods) + { + Directory.Delete(mod.Path, true); + } + Mods.Clear(); OnPropertyChanged(nameof(ModCount)); Sort(); diff --git a/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml b/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml index ad054ed52..d56d43ea3 100644 --- a/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml +++ b/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml @@ -116,7 +116,7 @@ Padding="10" MinWidth="0" MinHeight="0" - Click="RemoveMod"> + Click="DeleteMod"> - + Command="{ReflectionBinding DeleteAll}"> + Date: Tue, 27 Jun 2023 01:03:16 +0100 Subject: [PATCH 09/43] Fix duplicate entries --- src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs index 560aa33f6..489d6d3a8 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs @@ -118,7 +118,11 @@ private void LoadMods(ulong titleId) foreach (var mod in modCache.RomfsDirs) { - Mods.Add(new ModModel(mod.Path.FullName, mod.Name, mod.Enabled)); + var modModel = new ModModel(mod.Path.Parent.FullName, mod.Name, mod.Enabled); + if (Mods.All(x => x.Path != mod.Path.Parent.FullName)) + { + Mods.Add(modModel); + } } foreach (var mod in modCache.RomfsContainers) @@ -128,7 +132,11 @@ private void LoadMods(ulong titleId) foreach (var mod in modCache.ExefsDirs) { - Mods.Add(new ModModel(mod.Path.FullName, mod.Name, mod.Enabled)); + var modModel = new ModModel(mod.Path.Parent.FullName, mod.Name, mod.Enabled); + if (Mods.All(x => x.Path != mod.Path.Parent.FullName)) + { + Mods.Add(modModel); + } } foreach (var mod in modCache.ExefsContainers) From 4b64616932f9ef51ea4e397c0bcd69c1bad0fb0d Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Tue, 27 Jun 2023 01:06:26 +0100 Subject: [PATCH 10/43] Fix file check --- src/Ryujinx.HLE/HOS/ModLoader.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Ryujinx.HLE/HOS/ModLoader.cs b/src/Ryujinx.HLE/HOS/ModLoader.cs index d2e945880..b67302470 100644 --- a/src/Ryujinx.HLE/HOS/ModLoader.cs +++ b/src/Ryujinx.HLE/HOS/ModLoader.cs @@ -285,7 +285,7 @@ private static void QueryTitleDir(ModCache mods, DirectoryInfo titleDir, ulong t var fsFile = new FileInfo(Path.Combine(titleDir.FullName, RomfsContainer)); if (fsFile.Exists) { - bool enabled = !ModData.HasValue || ModData.Value.Mods.Exists(x => x.Path == fsFile.FullName); + bool enabled = !ModData.HasValue || ModData.Value.Mods.Exists(x => fsFile.FullName.Contains(x.Path)); mods.RomfsContainers.Add(new Mod($"<{titleDir.Name} RomFs>", fsFile, enabled)); } @@ -293,7 +293,7 @@ private static void QueryTitleDir(ModCache mods, DirectoryInfo titleDir, ulong t fsFile = new FileInfo(Path.Combine(titleDir.FullName, ExefsContainer)); if (fsFile.Exists) { - bool enabled = !ModData.HasValue || ModData.Value.Mods.Exists(x => x.Path == fsFile.FullName); + bool enabled = !ModData.HasValue || ModData.Value.Mods.Exists(x => fsFile.FullName.Contains(x.Path)); mods.ExefsContainers.Add(new Mod($"<{titleDir.Name} ExeFs>", fsFile, enabled)); } From b14f8080f604ef67d85bbf6ebb100117cb76c4dd Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Mon, 14 Aug 2023 12:53:58 +0100 Subject: [PATCH 11/43] Avalonia 11 --- .../Controls/ApplicationContextMenu.axaml.cs | 2 +- src/Ryujinx.Ava/UI/Models/ModModel.cs | 2 +- .../DownloadableContentManagerViewModel.cs | 7 ++-- .../UI/ViewModels/ModManagerViewModel.cs | 32 +++++++++---------- .../UI/Windows/ModManagerWindow.axaml | 9 +++--- .../UI/Windows/ModManagerWindow.axaml.cs | 10 +++--- src/Ryujinx.Common/Configuration/Mod.cs | 2 +- .../Configuration/ModMetadata.cs | 2 +- .../ModMetadataJsonSerializerContext.cs | 2 +- 9 files changed, 32 insertions(+), 36 deletions(-) diff --git a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs index 81c087d27..01d977091 100644 --- a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs +++ b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs @@ -132,7 +132,7 @@ public async void OpenModManager_Click(object sender, RoutedEventArgs args) if (viewModel?.SelectedApplication != null) { - await ModManagerWindow.Show(viewModel.VirtualFileSystem, ulong.Parse(viewModel.SelectedApplication.TitleId, NumberStyles.HexNumber), viewModel.SelectedApplication.TitleName); + await ModManagerWindow.Show(ulong.Parse(viewModel.SelectedApplication.TitleId, NumberStyles.HexNumber), viewModel.SelectedApplication.TitleName); } } diff --git a/src/Ryujinx.Ava/UI/Models/ModModel.cs b/src/Ryujinx.Ava/UI/Models/ModModel.cs index 4b3394dd4..f68e15939 100644 --- a/src/Ryujinx.Ava/UI/Models/ModModel.cs +++ b/src/Ryujinx.Ava/UI/Models/ModModel.cs @@ -27,4 +27,4 @@ public ModModel(string path, string name, bool enabled) Enabled = enabled; } } -} \ No newline at end of file +} diff --git a/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs index cdecae77d..d1401e53f 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs @@ -39,6 +39,7 @@ public class DownloadableContentManagerViewModel : BaseModel private string _search; private readonly ulong _titleId; + private IStorageProvider _storageProvider; private static readonly DownloadableContentJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); @@ -90,8 +91,6 @@ public string UpdateCount get => string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowHeading], DownloadableContents.Count); } - public IStorageProvider StorageProvider; - public DownloadableContentManagerViewModel(VirtualFileSystem virtualFileSystem, ulong titleId) { _virtualFileSystem = virtualFileSystem; @@ -100,7 +99,7 @@ public DownloadableContentManagerViewModel(VirtualFileSystem virtualFileSystem, if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { - StorageProvider = desktop.MainWindow.StorageProvider; + _storageProvider = desktop.MainWindow.StorageProvider; } _downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "dlc.json"); @@ -203,7 +202,7 @@ private Nca TryOpenNca(IStorage ncaStorage, string containerPath) public async void Add() { - var result = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions + var result = await _storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions { Title = LocaleManager.Instance[LocaleKeys.SelectDlcDialogTitle], AllowMultiple = true, diff --git a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs index 489d6d3a8..ed16a2aba 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs @@ -1,6 +1,7 @@ +using Avalonia; using Avalonia.Collections; -using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Platform.Storage; using Avalonia.Threading; using DynamicData; using Ryujinx.Ava.Common.Locale; @@ -10,7 +11,6 @@ using Ryujinx.Common.Logging; using Ryujinx.Common.Utilities; using Ryujinx.HLE.HOS; -using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -26,10 +26,9 @@ public class ModManagerViewModel : BaseModel public AvaloniaList _views = new(); public AvaloniaList _selectedMods = new(); - private ulong _titleId { get; } - private string _titleName { get; } - private string _search; + private ulong _titleId { get; } + private IStorageProvider _storageProvider; private static readonly ModMetadataJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); @@ -81,13 +80,17 @@ public string ModCount get => string.Format(LocaleManager.Instance[LocaleKeys.ModWindowHeading], Mods.Count); } - public ModManagerViewModel(ulong titleId, string titleName) + public ModManagerViewModel(ulong titleId) { _titleId = titleId; - _titleName = titleName; _modJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "mods.json"); + if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + _storageProvider = desktop.MainWindow.StorageProvider; + } + try { _modData = JsonHelper.DeserializeFromFile(_modJsonPath, SerializerContext.ModMetadata); @@ -230,19 +233,14 @@ private void AddMod(DirectoryInfo directory) public async void Add() { - OpenFolderDialog dialog = new() + var result = await _storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions { Title = LocaleManager.Instance[LocaleKeys.SelectModDialogTitle] - }; + }); - if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + foreach (var folder in result) { - string directory = await dialog.ShowAsync(desktop.MainWindow); - - if (directory != null) - { - AddMod(new DirectoryInfo(directory)); - } + AddMod(new DirectoryInfo(folder.Path.LocalPath)); } } @@ -268,4 +266,4 @@ public void DisableAll() SelectedMods.Clear(); } } -} \ No newline at end of file +} diff --git a/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml b/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml index d56d43ea3..69372cb34 100644 --- a/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml +++ b/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml @@ -71,12 +71,11 @@ Padding="2.5"> + ItemsSource="{Binding Views}"> @@ -145,14 +144,14 @@ Name="AddButton" MinWidth="90" Margin="5" - Command="{ReflectionBinding Add}"> + Command="{Binding Add}"> @@ -177,4 +176,4 @@ - \ No newline at end of file + diff --git a/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs index c7c2ded6f..70b1f4a55 100644 --- a/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs +++ b/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs @@ -24,21 +24,21 @@ public ModManagerWindow() InitializeComponent(); } - public ModManagerWindow(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName) + public ModManagerWindow(ulong titleId) { - DataContext = ViewModel = new ModManagerViewModel(titleId, titleName); + DataContext = ViewModel = new ModManagerViewModel(titleId); InitializeComponent(); } - public static async Task Show(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName) + public static async Task Show(ulong titleId, string titleName) { ContentDialog contentDialog = new() { PrimaryButtonText = "", SecondaryButtonText = "", CloseButtonText = "", - Content = new ModManagerWindow(virtualFileSystem, titleId, titleName), + Content = new ModManagerWindow(titleId), Title = string.Format(LocaleManager.Instance[LocaleKeys.ModWindowHeading], titleName, titleId.ToString("X16")) }; @@ -112,4 +112,4 @@ private void OnSelectionChanged(object sender, SelectionChangedEventArgs e) } } } -} \ No newline at end of file +} diff --git a/src/Ryujinx.Common/Configuration/Mod.cs b/src/Ryujinx.Common/Configuration/Mod.cs index 970e59d26..a513a3994 100644 --- a/src/Ryujinx.Common/Configuration/Mod.cs +++ b/src/Ryujinx.Common/Configuration/Mod.cs @@ -5,4 +5,4 @@ public class Mod public string Name { get; set; } public string Path { get; set; } } -} \ No newline at end of file +} diff --git a/src/Ryujinx.Common/Configuration/ModMetadata.cs b/src/Ryujinx.Common/Configuration/ModMetadata.cs index 1b9366bb8..1999fdbca 100644 --- a/src/Ryujinx.Common/Configuration/ModMetadata.cs +++ b/src/Ryujinx.Common/Configuration/ModMetadata.cs @@ -6,4 +6,4 @@ public struct ModMetadata { public List Mods { get; set; } } -} \ No newline at end of file +} diff --git a/src/Ryujinx.Common/Configuration/ModMetadataJsonSerializerContext.cs b/src/Ryujinx.Common/Configuration/ModMetadataJsonSerializerContext.cs index f1cd2cf21..8c1e242ad 100644 --- a/src/Ryujinx.Common/Configuration/ModMetadataJsonSerializerContext.cs +++ b/src/Ryujinx.Common/Configuration/ModMetadataJsonSerializerContext.cs @@ -7,4 +7,4 @@ namespace Ryujinx.Common.Configuration public partial class ModMetadataJsonSerializerContext : JsonSerializerContext { } -} \ No newline at end of file +} From 0aec0c501537c3c309109761d1056fda7f6afc9c Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Mon, 14 Aug 2023 13:01:47 +0100 Subject: [PATCH 12/43] Style fixes --- .../UI/ViewModels/ModManagerViewModel.cs | 20 +++++++++---------- src/Ryujinx.HLE/HOS/ModLoader.cs | 6 +++--- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs index ed16a2aba..4d4ac1878 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs @@ -19,18 +19,18 @@ namespace Ryujinx.Ava.UI.ViewModels { public class ModManagerViewModel : BaseModel { - public ModMetadata _modData; - public readonly string _modJsonPath; + private readonly ModMetadata _modData; + private readonly string _modJsonPath; - public AvaloniaList _mods = new(); - public AvaloniaList _views = new(); - public AvaloniaList _selectedMods = new(); + private AvaloniaList _mods = new(); + private AvaloniaList _views = new(); + private AvaloniaList _selectedMods = new(); private string _search; - private ulong _titleId { get; } - private IStorageProvider _storageProvider; + private ulong _titleId; + private readonly IStorageProvider _storageProvider; - private static readonly ModMetadataJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + private static readonly ModMetadataJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); public AvaloniaList Mods { @@ -93,7 +93,7 @@ public ModManagerViewModel(ulong titleId) try { - _modData = JsonHelper.DeserializeFromFile(_modJsonPath, SerializerContext.ModMetadata); + _modData = JsonHelper.DeserializeFromFile(_modJsonPath, _serializerContext.ModMetadata); } catch { @@ -186,7 +186,7 @@ public void Save() }); } - JsonHelper.SerializeToFile(_modJsonPath, _modData, SerializerContext.ModMetadata); + JsonHelper.SerializeToFile(_modJsonPath, _modData, _serializerContext.ModMetadata); } public void Delete(ModModel model) diff --git a/src/Ryujinx.HLE/HOS/ModLoader.cs b/src/Ryujinx.HLE/HOS/ModLoader.cs index b67302470..169fc9413 100644 --- a/src/Ryujinx.HLE/HOS/ModLoader.cs +++ b/src/Ryujinx.HLE/HOS/ModLoader.cs @@ -38,7 +38,7 @@ public class ModLoader private const string AmsNroPatchDir = "nro_patches"; private const string AmsKipPatchDir = "kip_patches"; - private static readonly ModMetadataJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + private static readonly ModMetadataJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); public readonly struct Mod where T : FileSystemInfo { @@ -169,7 +169,7 @@ private static void AddModsFromDirectory(ModCache mods, DirectoryInfo dir, strin try { - ModData = JsonHelper.DeserializeFromFile(ModJsonPath, SerializerContext.ModMetadata); + ModData = JsonHelper.DeserializeFromFile(ModJsonPath, _serializerContext.ModMetadata); } catch { @@ -275,7 +275,7 @@ private static void QueryTitleDir(ModCache mods, DirectoryInfo titleDir, ulong t try { - ModData = JsonHelper.DeserializeFromFile(ModJsonPath, SerializerContext.ModMetadata); + ModData = JsonHelper.DeserializeFromFile(ModJsonPath, _serializerContext.ModMetadata); } catch { From 78eae90cb7456db9ae98d5b5780201e7e5474828 Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Mon, 14 Aug 2023 13:06:23 +0100 Subject: [PATCH 13/43] Final style fixes --- .../UI/ViewModels/DownloadableContentManagerViewModel.cs | 2 +- src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs index d1401e53f..a0d0c060c 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs @@ -39,7 +39,7 @@ public class DownloadableContentManagerViewModel : BaseModel private string _search; private readonly ulong _titleId; - private IStorageProvider _storageProvider; + private readonly IStorageProvider _storageProvider; private static readonly DownloadableContentJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); diff --git a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs index 4d4ac1878..0a9767735 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs @@ -27,7 +27,7 @@ public class ModManagerViewModel : BaseModel private AvaloniaList _selectedMods = new(); private string _search; - private ulong _titleId; + private readonly ulong _titleId; private readonly IStorageProvider _storageProvider; private static readonly ModMetadataJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); From 73439b9647099830416beb97eb6623cf61af2a7c Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Mon, 14 Aug 2023 14:24:34 +0100 Subject: [PATCH 14/43] Might be too general --- src/Ryujinx.HLE/HOS/ModLoader.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Ryujinx.HLE/HOS/ModLoader.cs b/src/Ryujinx.HLE/HOS/ModLoader.cs index 169fc9413..5ca91f416 100644 --- a/src/Ryujinx.HLE/HOS/ModLoader.cs +++ b/src/Ryujinx.HLE/HOS/ModLoader.cs @@ -183,14 +183,14 @@ private static void AddModsFromDirectory(ModCache mods, DirectoryInfo dir, strin if (StrEquals(RomfsDir, modDir.Name)) { - bool enabled = !ModData.HasValue || ModData.Value.Mods.Exists(x => x.Path == modDir.FullName); + bool enabled = !ModData.HasValue || ModData.Value.Mods.Exists(x => modDir.FullName.Contains(x.Path)); mods.RomfsDirs.Add(mod = new Mod(dir.Name, modDir, enabled)); types.Append('R'); } else if (StrEquals(ExefsDir, modDir.Name)) { - bool enabled = !ModData.HasValue || ModData.Value.Mods.Exists(x => x.Path == modDir.FullName); + bool enabled = !ModData.HasValue || ModData.Value.Mods.Exists(x => modDir.FullName.Contains(x.Path)); mods.ExefsDirs.Add(mod = new Mod(dir.Name, modDir, enabled)); types.Append('E'); From 3f2c2a1465ef1a55327748005e6d325fc4e4bf13 Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Mon, 14 Aug 2023 14:39:54 +0100 Subject: [PATCH 15/43] Remove unnecessary using --- src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs index 70b1f4a55..72dcb1139 100644 --- a/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs +++ b/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs @@ -6,7 +6,6 @@ using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Models; using Ryujinx.Ava.UI.ViewModels; -using Ryujinx.HLE.FileSystem; using Ryujinx.Ui.Common.Helper; using System.Threading.Tasks; using Button = Avalonia.Controls.Button; From 9979193bfd3e27fdad4a1f8794eaee199efa5475 Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Mon, 14 Aug 2023 14:48:58 +0100 Subject: [PATCH 16/43] Enable new mods by default --- .../UI/ViewModels/ModManagerViewModel.cs | 1 + src/Ryujinx.Common/Configuration/Mod.cs | 1 + src/Ryujinx.HLE/HOS/ModLoader.cs | 48 +++++++++++++++++-- 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs index 0a9767735..f11a6a32a 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs @@ -183,6 +183,7 @@ public void Save() { Name = mod.Name, Path = mod.Path, + Enabled = mod.Enabled, }); } diff --git a/src/Ryujinx.Common/Configuration/Mod.cs b/src/Ryujinx.Common/Configuration/Mod.cs index a513a3994..052c7c8d1 100644 --- a/src/Ryujinx.Common/Configuration/Mod.cs +++ b/src/Ryujinx.Common/Configuration/Mod.cs @@ -4,5 +4,6 @@ public class Mod { public string Name { get; set; } public string Path { get; set; } + public bool Enabled { get; set; } } } diff --git a/src/Ryujinx.HLE/HOS/ModLoader.cs b/src/Ryujinx.HLE/HOS/ModLoader.cs index 5ca91f416..eb6ea3e20 100644 --- a/src/Ryujinx.HLE/HOS/ModLoader.cs +++ b/src/Ryujinx.HLE/HOS/ModLoader.cs @@ -183,14 +183,34 @@ private static void AddModsFromDirectory(ModCache mods, DirectoryInfo dir, strin if (StrEquals(RomfsDir, modDir.Name)) { - bool enabled = !ModData.HasValue || ModData.Value.Mods.Exists(x => modDir.FullName.Contains(x.Path)); + bool enabled; + + if (ModData.Value.Mods.Find(x => modDir.FullName.Contains(x.Path)) is Mod modData) + { + enabled = modData.Enabled; + } + else + { + // Mod is not in the list yet. New mods should be enabled by default. + enabled = true; + } mods.RomfsDirs.Add(mod = new Mod(dir.Name, modDir, enabled)); types.Append('R'); } else if (StrEquals(ExefsDir, modDir.Name)) { - bool enabled = !ModData.HasValue || ModData.Value.Mods.Exists(x => modDir.FullName.Contains(x.Path)); + bool enabled; + + if (ModData.Value.Mods.Find(x => modDir.FullName.Contains(x.Path)) is Mod modData) + { + enabled = modData.Enabled; + } + else + { + // Mod is not in the list yet. New mods should be enabled by default. + enabled = true; + } mods.ExefsDirs.Add(mod = new Mod(dir.Name, modDir, enabled)); types.Append('E'); @@ -285,7 +305,17 @@ private static void QueryTitleDir(ModCache mods, DirectoryInfo titleDir, ulong t var fsFile = new FileInfo(Path.Combine(titleDir.FullName, RomfsContainer)); if (fsFile.Exists) { - bool enabled = !ModData.HasValue || ModData.Value.Mods.Exists(x => fsFile.FullName.Contains(x.Path)); + bool enabled; + + if (ModData.Value.Mods.Find(x => fsFile.FullName.Contains(x.Path)) is Mod mod) + { + enabled = mod.Enabled; + } + else + { + // Mod is not in the list yet. New mods should be enabled by default. + enabled = true; + } mods.RomfsContainers.Add(new Mod($"<{titleDir.Name} RomFs>", fsFile, enabled)); } @@ -293,7 +323,17 @@ private static void QueryTitleDir(ModCache mods, DirectoryInfo titleDir, ulong t fsFile = new FileInfo(Path.Combine(titleDir.FullName, ExefsContainer)); if (fsFile.Exists) { - bool enabled = !ModData.HasValue || ModData.Value.Mods.Exists(x => fsFile.FullName.Contains(x.Path)); + bool enabled; + + if (ModData.Value.Mods.Find(x => fsFile.FullName.Contains(x.Path)) is Mod mod) + { + enabled = mod.Enabled; + } + else + { + // Mod is not in the list yet. New mods should be enabled by default. + enabled = true; + } mods.ExefsContainers.Add(new Mod($"<{titleDir.Name} ExeFs>", fsFile, enabled)); } From 928b91b3f6dc0bdb4771394c573c0e558d6fc8d6 Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Mon, 14 Aug 2023 15:00:11 +0100 Subject: [PATCH 17/43] More cleanup --- .../UI/ViewModels/ModManagerViewModel.cs | 26 ++---------- .../Configuration/ModMetadata.cs | 5 +++ src/Ryujinx.HLE/HOS/ModLoader.cs | 40 ++++++++++--------- 3 files changed, 31 insertions(+), 40 deletions(-) diff --git a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs index f11a6a32a..f8cfba087 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs @@ -8,10 +8,8 @@ using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Models; using Ryujinx.Common.Configuration; -using Ryujinx.Common.Logging; using Ryujinx.Common.Utilities; using Ryujinx.HLE.HOS; -using System.Collections.Generic; using System.IO; using System.Linq; @@ -19,7 +17,6 @@ namespace Ryujinx.Ava.UI.ViewModels { public class ModManagerViewModel : BaseModel { - private readonly ModMetadata _modData; private readonly string _modJsonPath; private AvaloniaList _mods = new(); @@ -91,28 +88,13 @@ public ModManagerViewModel(ulong titleId) _storageProvider = desktop.MainWindow.StorageProvider; } - try - { - _modData = JsonHelper.DeserializeFromFile(_modJsonPath, _serializerContext.ModMetadata); - } - catch - { - Logger.Warning?.Print(LogClass.Application, $"Failed to deserialize mod data for {_titleId} at {_modJsonPath}"); - - _modData = new ModMetadata - { - Mods = new List() - }; - - Save(); - } - LoadMods(titleId); } private void LoadMods(ulong titleId) { Mods.Clear(); + SelectedMods.Clear(); string modsBasePath = ModLoader.GetModsBasePath(); @@ -175,11 +157,11 @@ private bool Filter(object arg) public void Save() { - _modData.Mods.Clear(); + ModMetadata modData = new(); foreach (ModModel mod in SelectedMods) { - _modData.Mods.Add(new Mod + modData.Mods.Add(new Mod { Name = mod.Name, Path = mod.Path, @@ -187,7 +169,7 @@ public void Save() }); } - JsonHelper.SerializeToFile(_modJsonPath, _modData, _serializerContext.ModMetadata); + JsonHelper.SerializeToFile(_modJsonPath, modData, _serializerContext.ModMetadata); } public void Delete(ModModel model) diff --git a/src/Ryujinx.Common/Configuration/ModMetadata.cs b/src/Ryujinx.Common/Configuration/ModMetadata.cs index 1999fdbca..174320d0a 100644 --- a/src/Ryujinx.Common/Configuration/ModMetadata.cs +++ b/src/Ryujinx.Common/Configuration/ModMetadata.cs @@ -5,5 +5,10 @@ namespace Ryujinx.Common.Configuration public struct ModMetadata { public List Mods { get; set; } + + public ModMetadata() + { + Mods = new List(); + } } } diff --git a/src/Ryujinx.HLE/HOS/ModLoader.cs b/src/Ryujinx.HLE/HOS/ModLoader.cs index eb6ea3e20..a74bb7ee1 100644 --- a/src/Ryujinx.HLE/HOS/ModLoader.cs +++ b/src/Ryujinx.HLE/HOS/ModLoader.cs @@ -164,16 +164,16 @@ private static void AddModsFromDirectory(ModCache mods, DirectoryInfo dir, strin { System.Text.StringBuilder types = new(); - string ModJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId, "mods.json"); - ModMetadata? ModData = null; + string modJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId, "mods.json"); + ModMetadata modMetadata = new(); try { - ModData = JsonHelper.DeserializeFromFile(ModJsonPath, _serializerContext.ModMetadata); + modMetadata = JsonHelper.DeserializeFromFile(modJsonPath, _serializerContext.ModMetadata); } catch { - Logger.Warning?.Print(LogClass.ModLoader, $"Failed to deserialize mod data for {titleId} at {ModJsonPath}"); + Logger.Warning?.Print(LogClass.ModLoader, $"Failed to deserialize mod data for {titleId} at {modJsonPath}"); } foreach (var modDir in dir.EnumerateDirectories()) @@ -185,11 +185,12 @@ private static void AddModsFromDirectory(ModCache mods, DirectoryInfo dir, strin { bool enabled; - if (ModData.Value.Mods.Find(x => modDir.FullName.Contains(x.Path)) is Mod modData) + try { + var modData = modMetadata.Mods.Find(x => modDir.FullName.Contains(x.Path)); enabled = modData.Enabled; } - else + catch { // Mod is not in the list yet. New mods should be enabled by default. enabled = true; @@ -202,11 +203,12 @@ private static void AddModsFromDirectory(ModCache mods, DirectoryInfo dir, strin { bool enabled; - if (ModData.Value.Mods.Find(x => modDir.FullName.Contains(x.Path)) is Mod modData) + try { + var modData = modMetadata.Mods.Find(x => modDir.FullName.Contains(x.Path)); enabled = modData.Enabled; } - else + catch { // Mod is not in the list yet. New mods should be enabled by default. enabled = true; @@ -290,16 +292,16 @@ private static void QueryTitleDir(ModCache mods, DirectoryInfo titleDir, ulong t return; } - string ModJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "mods.json"); - ModMetadata? ModData = null; + string modJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "mods.json"); + ModMetadata modMetadata = new(); try { - ModData = JsonHelper.DeserializeFromFile(ModJsonPath, _serializerContext.ModMetadata); + modMetadata = JsonHelper.DeserializeFromFile(modJsonPath, _serializerContext.ModMetadata); } catch { - Logger.Warning?.Print(LogClass.ModLoader, $"Failed to deserialize mod data for {titleId} at {ModJsonPath}"); + Logger.Warning?.Print(LogClass.ModLoader, $"Failed to deserialize mod data for {titleId} at {modJsonPath}"); } var fsFile = new FileInfo(Path.Combine(titleDir.FullName, RomfsContainer)); @@ -307,11 +309,12 @@ private static void QueryTitleDir(ModCache mods, DirectoryInfo titleDir, ulong t { bool enabled; - if (ModData.Value.Mods.Find(x => fsFile.FullName.Contains(x.Path)) is Mod mod) + try { - enabled = mod.Enabled; + var modData = modMetadata.Mods.Find(x => fsFile.FullName.Contains(x.Path)); + enabled = modData.Enabled; } - else + catch { // Mod is not in the list yet. New mods should be enabled by default. enabled = true; @@ -325,11 +328,12 @@ private static void QueryTitleDir(ModCache mods, DirectoryInfo titleDir, ulong t { bool enabled; - if (ModData.Value.Mods.Find(x => fsFile.FullName.Contains(x.Path)) is Mod mod) + try { - enabled = mod.Enabled; + var modData = modMetadata.Mods.Find(x => fsFile.FullName.Contains(x.Path)); + enabled = modData.Enabled; } - else + catch { // Mod is not in the list yet. New mods should be enabled by default. enabled = true; From 243654efadaf9cc388bf6c2e0c28c231ef61b3a8 Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Mon, 14 Aug 2023 15:01:43 +0100 Subject: [PATCH 18/43] Fix saving metadata --- src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs index f8cfba087..256222cb5 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs @@ -159,13 +159,13 @@ public void Save() { ModMetadata modData = new(); - foreach (ModModel mod in SelectedMods) + foreach (ModModel mod in Mods) { modData.Mods.Add(new Mod { Name = mod.Name, Path = mod.Path, - Enabled = mod.Enabled, + Enabled = SelectedMods.Contains(mod), }); } From 6dc8355edb712c98585cc45a1825c1df7ae84137 Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Mon, 23 Oct 2023 14:59:50 -0400 Subject: [PATCH 19/43] Dont deseralise ModMetadata several times --- src/Ryujinx.HLE/HOS/ModLoader.cs | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/src/Ryujinx.HLE/HOS/ModLoader.cs b/src/Ryujinx.HLE/HOS/ModLoader.cs index a74bb7ee1..6b6eae512 100644 --- a/src/Ryujinx.HLE/HOS/ModLoader.cs +++ b/src/Ryujinx.HLE/HOS/ModLoader.cs @@ -160,22 +160,10 @@ private static string EnsureBaseDirStructure(string modsBasePath) private static DirectoryInfo FindTitleDir(DirectoryInfo contentsDir, string titleId) => contentsDir.EnumerateDirectories(titleId, _dirEnumOptions).FirstOrDefault(); - private static void AddModsFromDirectory(ModCache mods, DirectoryInfo dir, string titleId) + private static void AddModsFromDirectory(ModCache mods, DirectoryInfo dir, ModMetadata modMetadata) { System.Text.StringBuilder types = new(); - string modJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId, "mods.json"); - ModMetadata modMetadata = new(); - - try - { - modMetadata = JsonHelper.DeserializeFromFile(modJsonPath, _serializerContext.ModMetadata); - } - catch - { - Logger.Warning?.Print(LogClass.ModLoader, $"Failed to deserialize mod data for {titleId} at {modJsonPath}"); - } - foreach (var modDir in dir.EnumerateDirectories()) { types.Clear(); @@ -223,7 +211,7 @@ private static void AddModsFromDirectory(ModCache mods, DirectoryInfo dir, strin } else { - AddModsFromDirectory(mods, modDir, titleId); + AddModsFromDirectory(mods, modDir, modMetadata); } if (types.Length > 0) @@ -342,7 +330,7 @@ private static void QueryTitleDir(ModCache mods, DirectoryInfo titleDir, ulong t mods.ExefsContainers.Add(new Mod($"<{titleDir.Name} ExeFs>", fsFile, enabled)); } - AddModsFromDirectory(mods, titleDir, titleDir.Name); + AddModsFromDirectory(mods, titleDir, modMetadata); } public static void QueryContentsDir(ModCache mods, DirectoryInfo contentsDir, ulong titleId) From 288ad6f9372bf8da08aec1ea49cc4f7464d609fa Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Mon, 23 Oct 2023 15:25:58 -0400 Subject: [PATCH 20/43] Avalonia I hate you --- src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs index 256222cb5..ad3bcd95e 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs @@ -129,8 +129,6 @@ private void LoadMods(ulong titleId) Mods.Add(new ModModel(mod.Path.FullName, mod.Name, mod.Enabled)); } - SelectedMods = new(Mods.Where(x => x.Enabled)); - Sort(); } @@ -142,7 +140,12 @@ public void Sort() _views.Clear(); _views.AddRange(view); + + SelectedMods = new(Views.Where(x => x.Enabled)); + OnPropertyChanged(nameof(ModCount)); + OnPropertyChanged(nameof(Views)); + OnPropertyChanged(nameof(SelectedMods)); } private bool Filter(object arg) From 48d5bbff4d9ac5aab5453ed7d020b6603d74d9e4 Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Mon, 23 Oct 2023 15:45:26 -0400 Subject: [PATCH 21/43] Confirmation dialgoues --- src/Ryujinx.Ava/Assets/Locales/en_US.json | 2 ++ .../UI/Windows/ModManagerWindow.axaml | 2 +- .../UI/Windows/ModManagerWindow.axaml.cs | 31 +++++++++++++++++-- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/Ryujinx.Ava/Assets/Locales/en_US.json b/src/Ryujinx.Ava/Assets/Locales/en_US.json index 07c843142..340d488c6 100644 --- a/src/Ryujinx.Ava/Assets/Locales/en_US.json +++ b/src/Ryujinx.Ava/Assets/Locales/en_US.json @@ -397,6 +397,8 @@ "DialogUpdateAddUpdateErrorMessage": "The specified file does not contain an update for the selected title!", "DialogSettingsBackendThreadingWarningTitle": "Warning - Backend Threading", "DialogSettingsBackendThreadingWarningMessage": "Ryujinx must be restarted after changing this option for it to apply fully. Depending on your platform, you may need to manually disable your driver's own multithreading when using Ryujinx's.", + "DialogModManagerDeletionWarningMessage": "You are about to delete the mod: {0}\n\nAre you sure you want to proceed?", + "DialogModManagerDeletionAllWarningMessage": "You are about to delete all mods for this title.\n\nAre you sure you want to proceed?", "SettingsTabGraphicsFeaturesOptions": "Features", "SettingsTabGraphicsBackendMultithreading": "Graphics Backend Multithreading:", "CommonAuto": "Auto", diff --git a/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml b/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml index 69372cb34..d9f586408 100644 --- a/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml +++ b/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml @@ -151,7 +151,7 @@ Name="RemoveAllButton" MinWidth="90" Margin="5" - Command="{Binding DeleteAll}"> + Click="DeleteAll"> diff --git a/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs index 72dcb1139..44fd363f2 100644 --- a/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs +++ b/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs @@ -46,7 +46,7 @@ public static async Task Show(ulong titleId, string titleName) contentDialog.Styles.Add(bottomBorder); - await ContentDialogHelper.ShowAsync(contentDialog); + await contentDialog.ShowAsync(); } private void SaveAndClose(object sender, RoutedEventArgs e) @@ -60,17 +60,42 @@ private void Close(object sender, RoutedEventArgs e) ((ContentDialog)Parent).Hide(); } - private void DeleteMod(object sender, RoutedEventArgs e) + private async void DeleteMod(object sender, RoutedEventArgs e) { if (sender is Button button) { if (button.DataContext is ModModel model) { - ViewModel.Delete(model); + var result = await ContentDialogHelper.CreateConfirmationDialog( + LocaleManager.Instance[LocaleKeys.DialogWarning], + LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogModManagerDeletionWarningMessage, model.Name), + LocaleManager.Instance[LocaleKeys.InputDialogYes], + LocaleManager.Instance[LocaleKeys.InputDialogNo], + LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); + + if (result == UserResult.Yes) + { + ViewModel.Delete(model); + } } } } + private async void DeleteAll(object sender, RoutedEventArgs e) + { + var result = await ContentDialogHelper.CreateConfirmationDialog( + LocaleManager.Instance[LocaleKeys.DialogWarning], + LocaleManager.Instance[LocaleKeys.DialogModManagerDeletionAllWarningMessage], + LocaleManager.Instance[LocaleKeys.InputDialogYes], + LocaleManager.Instance[LocaleKeys.InputDialogNo], + LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); + + if (result == UserResult.Yes) + { + ViewModel.DeleteAll(); + } + } + private void OpenLocation(object sender, RoutedEventArgs e) { if (sender is Button button) From c910c1127e63dc6d079542460a2b77283a01f6ba Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Tue, 24 Oct 2023 19:03:24 -0400 Subject: [PATCH 22/43] Allow selecting multiple folders --- src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs index ad3bcd95e..5a05766a9 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs @@ -221,7 +221,8 @@ public async void Add() { var result = await _storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions { - Title = LocaleManager.Instance[LocaleKeys.SelectModDialogTitle] + Title = LocaleManager.Instance[LocaleKeys.SelectModDialogTitle], + AllowMultiple = true }); foreach (var folder in result) From fc8f81954bd9f019c0b3b1a9d6b3ce3c78118f1a Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Thu, 11 Jan 2024 16:03:58 -0500 Subject: [PATCH 23/43] Add back secondary folder --- .../UI/ViewModels/ModManagerViewModel.cs | 4 +-- .../Configuration/AppDataManager.cs | 4 ++- src/Ryujinx.HLE/HOS/ModLoader.cs | 28 +++++++++++-------- .../Processes/Extensions/NcaExtensions.cs | 4 ++- .../Widgets/GameTableContextMenu.Designer.cs | 11 ++++++++ .../Ui/Widgets/GameTableContextMenu.cs | 9 +++++- 6 files changed, 43 insertions(+), 17 deletions(-) diff --git a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs index 5a05766a9..e1050211a 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs @@ -96,7 +96,7 @@ private void LoadMods(ulong titleId) Mods.Clear(); SelectedMods.Clear(); - string modsBasePath = ModLoader.GetModsBasePath(); + string modsBasePath = ModLoader.GetSdModsBasePath(); var modCache = new ModLoader.ModCache(); ModLoader.QueryContentsDir(modCache, new DirectoryInfo(Path.Combine(modsBasePath, "contents")), titleId); @@ -187,7 +187,7 @@ public void Delete(ModModel model) private void AddMod(DirectoryInfo directory) { var directories = Directory.GetDirectories(directory.ToString(), "*", SearchOption.AllDirectories); - var destinationDir = ModLoader.GetTitleDir(ModLoader.GetModsBasePath(), _titleId.ToString("x16")); + var destinationDir = ModLoader.GetTitleDir(ModLoader.GetSdModsBasePath(), _titleId.ToString("x16")); foreach (var dir in directories) { diff --git a/src/Ryujinx.Common/Configuration/AppDataManager.cs b/src/Ryujinx.Common/Configuration/AppDataManager.cs index 350a37a98..2b4a594d3 100644 --- a/src/Ryujinx.Common/Configuration/AppDataManager.cs +++ b/src/Ryujinx.Common/Configuration/AppDataManager.cs @@ -31,6 +31,7 @@ public enum LaunchMode public const string DefaultNandDir = "bis"; public const string DefaultSdcardDir = "sdcard"; + private const string DefaultModsDir = "mods"; public static string CustomModsPath { get; set; } public static string CustomSdModsPath { get; set; } @@ -150,6 +151,7 @@ private static void CopyDirectory(string sourceDir, string destinationDir) } } - public static string GetModsPath() => CustomModsPath ?? Directory.CreateDirectory(Path.Combine(BaseDirPath, DefaultSdcardDir, "atmosphere")).FullName; + public static string GetModsPath() => CustomModsPath ?? Directory.CreateDirectory(Path.Combine(BaseDirPath, DefaultModsDir)).FullName; + public static string GetSdModsPath() => CustomSdModsPath ?? Directory.CreateDirectory(Path.Combine(BaseDirPath, DefaultSdcardDir, "atmosphere")).FullName; } } diff --git a/src/Ryujinx.HLE/HOS/ModLoader.cs b/src/Ryujinx.HLE/HOS/ModLoader.cs index 6b6eae512..e9ec4d59e 100644 --- a/src/Ryujinx.HLE/HOS/ModLoader.cs +++ b/src/Ryujinx.HLE/HOS/ModLoader.cs @@ -143,6 +143,7 @@ private void Clear() private static bool StrEquals(string s1, string s2) => string.Equals(s1, s2, StringComparison.OrdinalIgnoreCase); public static string GetModsBasePath() => EnsureBaseDirStructure(AppDataManager.GetModsPath()); + public static string GetSdModsBasePath() => EnsureBaseDirStructure(AppDataManager.GetSdModsPath()); private static string EnsureBaseDirStructure(string modsBasePath) { @@ -443,7 +444,7 @@ private static IEnumerable GetCheatsInFile(FileInfo cheatFile) } // Assumes searchDirPaths don't overlap - private static void CollectMods(Dictionary modCaches, PatchCache patches) + private static void CollectMods(Dictionary modCaches, PatchCache patches, params string[] searchDirPaths) { static bool IsPatchesDir(string name) => StrEquals(AmsNsoPatchDir, name) || StrEquals(AmsNroPatchDir, name) || @@ -472,25 +473,28 @@ static bool TryQuery(DirectoryInfo searchDir, PatchCache patches, Dictionary titles) + public void CollectMods(IEnumerable titles, params string[] searchDirPaths) { Clear(); @@ -499,7 +503,7 @@ public void CollectMods(IEnumerable titles) _appMods[titleId] = new ModCache(); } - CollectMods(_appMods, _patches); + CollectMods(_appMods, _patches, searchDirPaths); } internal IStorage ApplyRomFsMods(ulong titleId, IStorage baseStorage) diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs index de396ae9d..e369f4b04 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs @@ -35,7 +35,9 @@ public static ProcessResult Load(this Nca nca, Switch device, Nca patchNca, Nca // Collecting mods related to AocTitleIds and ProgramId. device.Configuration.VirtualFileSystem.ModLoader.CollectMods( - device.Configuration.ContentManager.GetAocTitleIds().Prepend(metaLoader.GetProgramId())); + device.Configuration.ContentManager.GetAocTitleIds().Prepend(metaLoader.GetProgramId()), + ModLoader.GetModsBasePath(), + ModLoader.GetSdModsBasePath()); // Load Nacp file. var nacpData = new BlitStruct(1); diff --git a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs index 370d22640..734437eea 100644 --- a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs +++ b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs @@ -11,6 +11,7 @@ public partial class GameTableContextMenu : Menu private MenuItem _manageDlcMenuItem; private MenuItem _manageCheatMenuItem; private MenuItem _openTitleModDirMenuItem; + private MenuItem _openTitleSdModDirMenuItem; private Menu _extractSubMenu; private MenuItem _extractMenuItem; private MenuItem _extractRomFsMenuItem; @@ -89,6 +90,15 @@ private void InitializeComponent() }; _openTitleModDirMenuItem.Activated += OpenTitleModDir_Clicked; + // + // _openTitleSdModDirMenuItem + // + _openTitleSdModDirMenuItem = new MenuItem("Open Atmosphere Mods Directory") + { + TooltipText = "Open the alternative SD card atmosphere directory which contains the Application's Mods.", + }; + _openTitleSdModDirMenuItem.Activated += OpenTitleSdModDir_Clicked; + // // _extractSubMenu // @@ -211,6 +221,7 @@ private void ShowComponent() Add(_manageDlcMenuItem); Add(_manageCheatMenuItem); Add(_openTitleModDirMenuItem); + Add(_openTitleSdModDirMenuItem); Add(new SeparatorMenuItem()); Add(_manageCacheMenuItem); Add(_extractMenuItem); diff --git a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs index 425766b5a..82acb9ac4 100644 --- a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs +++ b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs @@ -496,7 +496,14 @@ private void ManageCheats_Clicked(object sender, EventArgs args) private void OpenTitleModDir_Clicked(object sender, EventArgs args) { - string sdModsBasePath = ModLoader.GetModsBasePath(); + string modsBasePath = ModLoader.GetModsBasePath(); + string titleModsPath = ModLoader.GetTitleDir(modsBasePath, _titleIdText); + + OpenHelper.OpenFolder(titleModsPath); + } + + private void OpenTitleSdModDir_Clicked(object sender, EventArgs args) { + string sdModsBasePath = ModLoader.GetSdModsBasePath(); string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, _titleIdText); OpenHelper.OpenFolder(titleModsPath); From a986fe3ed0ee37cf51c18ec9cc4f87a378a014f2 Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Thu, 11 Jan 2024 16:07:06 -0500 Subject: [PATCH 24/43] Search both paths --- .../UI/ViewModels/ModManagerViewModel.cs | 47 ++++++++++--------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs index e1050211a..b125e7a37 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs @@ -96,37 +96,40 @@ private void LoadMods(ulong titleId) Mods.Clear(); SelectedMods.Clear(); - string modsBasePath = ModLoader.GetSdModsBasePath(); + string[] modsBasePaths = [ModLoader.GetSdModsBasePath(), ModLoader.GetModsBasePath()]; - var modCache = new ModLoader.ModCache(); - ModLoader.QueryContentsDir(modCache, new DirectoryInfo(Path.Combine(modsBasePath, "contents")), titleId); - - foreach (var mod in modCache.RomfsDirs) + foreach (var path in modsBasePaths) { - var modModel = new ModModel(mod.Path.Parent.FullName, mod.Name, mod.Enabled); - if (Mods.All(x => x.Path != mod.Path.Parent.FullName)) + var modCache = new ModLoader.ModCache(); + ModLoader.QueryContentsDir(modCache, new DirectoryInfo(Path.Combine(path, "contents")), titleId); + + foreach (var mod in modCache.RomfsDirs) { - Mods.Add(modModel); + var modModel = new ModModel(mod.Path.Parent.FullName, mod.Name, mod.Enabled); + if (Mods.All(x => x.Path != mod.Path.Parent.FullName)) + { + Mods.Add(modModel); + } } - } - foreach (var mod in modCache.RomfsContainers) - { - Mods.Add(new ModModel(mod.Path.FullName, mod.Name, mod.Enabled)); - } + foreach (var mod in modCache.RomfsContainers) + { + Mods.Add(new ModModel(mod.Path.FullName, mod.Name, mod.Enabled)); + } - foreach (var mod in modCache.ExefsDirs) - { - var modModel = new ModModel(mod.Path.Parent.FullName, mod.Name, mod.Enabled); - if (Mods.All(x => x.Path != mod.Path.Parent.FullName)) + foreach (var mod in modCache.ExefsDirs) { - Mods.Add(modModel); + var modModel = new ModModel(mod.Path.Parent.FullName, mod.Name, mod.Enabled); + if (Mods.All(x => x.Path != mod.Path.Parent.FullName)) + { + Mods.Add(modModel); + } } - } - foreach (var mod in modCache.ExefsContainers) - { - Mods.Add(new ModModel(mod.Path.FullName, mod.Name, mod.Enabled)); + foreach (var mod in modCache.ExefsContainers) + { + Mods.Add(new ModModel(mod.Path.FullName, mod.Name, mod.Enabled)); + } } Sort(); From e0acdc4d89a5346b5968995224d260a7e18c9ce2 Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Thu, 11 Jan 2024 16:13:21 -0500 Subject: [PATCH 25/43] Fix formatting --- src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs index 82acb9ac4..eb048b00d 100644 --- a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs +++ b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs @@ -502,7 +502,8 @@ private void OpenTitleModDir_Clicked(object sender, EventArgs args) OpenHelper.OpenFolder(titleModsPath); } - private void OpenTitleSdModDir_Clicked(object sender, EventArgs args) { + private void OpenTitleSdModDir_Clicked(object sender, EventArgs args) + { string sdModsBasePath = ModLoader.GetSdModsBasePath(); string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, _titleIdText); From 0bae0d1da65dbd65e35dc86654f6b1ebf42ca5bd Mon Sep 17 00:00:00 2001 From: jcm Date: Thu, 21 Sep 2023 20:29:54 -0600 Subject: [PATCH 26/43] Add custom refresh interval target --- src/Ryujinx.Ava/AppHost.cs | 100 ++++++++++- src/Ryujinx.Ava/Assets/Locales/en_US.json | 18 +- src/Ryujinx.Ava/Assets/Styles/Themes.xaml | 5 +- src/Ryujinx.Ava/Common/KeyboardHotkeyState.cs | 4 +- .../UI/Models/StatusUpdatedEventArgs.cs | 6 +- .../UI/ViewModels/MainWindowViewModel.cs | 160 +++++++++++++++++- .../UI/ViewModels/SettingsViewModel.cs | 86 +++++++++- .../UI/Views/Main/MainStatusBarView.axaml | 66 +++++++- .../UI/Views/Main/MainStatusBarView.axaml.cs | 8 +- .../Views/Settings/SettingsHotkeysView.axaml | 22 ++- .../Views/Settings/SettingsSystemView.axaml | 66 +++++++- .../UI/Windows/MainWindow.axaml.cs | 1 + .../Configuration/Hid/KeyboardHotkeys.cs | 4 +- .../Configuration/PresentIntervalState.cs | 15 ++ src/Ryujinx.Graphics.GAL/IWindow.cs | 3 +- .../Multithreading/ThreadedWindow.cs | 3 +- .../PresentIntervalState.cs | 9 + src/Ryujinx.Graphics.OpenGL/Window.cs | 2 +- src/Ryujinx.Graphics.Vulkan/Window.cs | 12 +- src/Ryujinx.Graphics.Vulkan/WindowBase.cs | 2 +- src/Ryujinx.HLE/HLEConfiguration.cs | 20 ++- .../Services/SurfaceFlinger/SurfaceFlinger.cs | 32 ++-- src/Ryujinx.HLE/Switch.cs | 58 ++++++- src/Ryujinx.Headless.SDL2/Options.cs | 8 +- src/Ryujinx.Headless.SDL2/Program.cs | 5 +- .../StatusUpdatedEventArgs.cs | 6 +- src/Ryujinx.Headless.SDL2/WindowBase.cs | 2 +- .../Configuration/ConfigurationFileFormat.cs | 20 ++- .../Configuration/ConfigurationState.cs | 76 +++++++-- src/Ryujinx/Ui/MainWindow.cs | 8 +- src/Ryujinx/Ui/RendererWidgetBase.cs | 7 +- src/Ryujinx/Ui/StatusUpdatedEventArgs.cs | 6 +- 32 files changed, 732 insertions(+), 108 deletions(-) create mode 100644 src/Ryujinx.Common/Configuration/PresentIntervalState.cs create mode 100644 src/Ryujinx.Graphics.GAL/PresentIntervalState.cs diff --git a/src/Ryujinx.Ava/AppHost.cs b/src/Ryujinx.Ava/AppHost.cs index 2fd9ce00d..dd77a08af 100644 --- a/src/Ryujinx.Ava/AppHost.cs +++ b/src/Ryujinx.Ava/AppHost.cs @@ -58,6 +58,7 @@ using IRenderer = Ryujinx.Graphics.GAL.IRenderer; using Key = Ryujinx.Input.Key; using MouseButton = Ryujinx.Input.MouseButton; +using PresentIntervalState = Ryujinx.Common.Configuration.PresentIntervalState; using ScalingFilter = Ryujinx.Common.Configuration.ScalingFilter; using Size = Avalonia.Size; using Switch = Ryujinx.HLE.Switch; @@ -189,6 +190,9 @@ public AppHost( ConfigurationState.Instance.Graphics.ScalingFilter.Event += UpdateScalingFilter; ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event += UpdateScalingFilterLevel; ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Event += UpdateColorSpacePassthrough; + ConfigurationState.Instance.Graphics.PresentIntervalState.Event += UpdatePresentIntervalState; + ConfigurationState.Instance.Graphics.CustomPresentInterval.Event += UpdateCustomPresentIntervalValue; + ConfigurationState.Instance.Graphics.EnableCustomPresentInterval.Event += UpdateCustomPresentIntervalEnabled; ConfigurationState.Instance.System.EnableInternetAccess.Event += UpdateEnableInternetAccessState; ConfigurationState.Instance.Multiplayer.LanInterfaceId.Event += UpdateLanInterfaceIdState; @@ -236,6 +240,37 @@ private void UpdateColorSpacePassthrough(object sender, ReactiveEventArgs _renderer.Window?.SetColorSpacePassthrough((bool)ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Value); } + private void UpdatePresentIntervalState(object sender, ReactiveEventArgs e) + { + if (Device != null) + { + Device.PresentIntervalState = e.NewValue; + Device.UpdatePresentInterval(); + } + //vulkan present mode may change in response, so recreate the swapchain + _renderer.Window?.ChangePresentIntervalState((Ryujinx.Graphics.GAL.PresentIntervalState)e.NewValue); + ConfigurationState.Instance.Graphics.PresentIntervalState.Value = e.NewValue; + ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); + } + + private void UpdateCustomPresentIntervalValue(object sender, ReactiveEventArgs e) + { + if (Device != null) + { + Device.TargetPresentInterval = e.NewValue; + Device.UpdatePresentInterval(); + } + } + + private void UpdateCustomPresentIntervalEnabled(object sender, ReactiveEventArgs e) + { + if (Device != null) + { + Device.CustomPresentIntervalEnabled = e.NewValue; + Device.UpdatePresentInterval(); + } + } + private void ShowCursor() { Dispatcher.UIThread.Post(() => @@ -515,6 +550,12 @@ private void HideCursorState_Changed(object sender, ReactiveEventArgs(oldState, presentIntervalState)); + } + public async Task LoadGuestApplication() { InitializeSwitchInstance(); @@ -778,7 +819,7 @@ private void InitializeSwitchInstance() _viewModel.UiHandler, (SystemLanguage)ConfigurationState.Instance.System.Language.Value, (RegionCode)ConfigurationState.Instance.System.Region.Value, - ConfigurationState.Instance.Graphics.EnableVsync, + ConfigurationState.Instance.Graphics.PresentIntervalState, ConfigurationState.Instance.System.EnableDockedMode, ConfigurationState.Instance.System.EnablePtc, ConfigurationState.Instance.System.EnableInternetAccess, @@ -792,7 +833,8 @@ private void InitializeSwitchInstance() ConfigurationState.Instance.System.AudioVolume, ConfigurationState.Instance.System.UseHypervisor, ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value, - ConfigurationState.Instance.Multiplayer.Mode); + ConfigurationState.Instance.Multiplayer.Mode, + ConfigurationState.Instance.Graphics.CustomPresentInterval.Value); Device = new Switch(configuration); } @@ -918,7 +960,7 @@ private void RenderLoop() Device.Gpu.InitializeShaderCache(_gpuCancellationTokenSource.Token); Translator.IsReadyForTranslation.Set(); - _renderer.Window.ChangeVSyncMode(Device.EnableDeviceVsync); + _renderer.Window.ChangePresentIntervalState((Ryujinx.Graphics.GAL.PresentIntervalState)Device.PresentIntervalState); while (_isActive) { @@ -966,6 +1008,7 @@ public void UpdateStatus() { // Run a status update only when a frame is to be drawn. This prevents from updating the ui and wasting a render when no frame is queued. string dockedMode = ConfigurationState.Instance.System.EnableDockedMode ? LocaleManager.Instance[LocaleKeys.Docked] : LocaleManager.Instance[LocaleKeys.Handheld]; + string presentIntervalState = Device.PresentIntervalState.ToString(); if (GraphicsConfig.ResScale != 1) { @@ -973,7 +1016,7 @@ public void UpdateStatus() } StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs( - Device.EnableDeviceVsync, + presentIntervalState, LocaleManager.Instance[LocaleKeys.VolumeShort] + $": {(int)(Device.GetVolume() * 100)}%", ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.Vulkan ? "Vulkan" : "OpenGL", dockedMode, @@ -1065,9 +1108,40 @@ private bool UpdateFrame() { switch (currentHotkeyState) { - case KeyboardHotkeyState.ToggleVSync: - Device.EnableDeviceVsync = !Device.EnableDeviceVsync; - + //todo default + case KeyboardHotkeyState.TogglePresentIntervalState: + PresentIntervalState oldState = Device.PresentIntervalState; + PresentIntervalState newState; + if (oldState == PresentIntervalState.Switch) + { + newState = PresentIntervalState.Unbounded; + } + else if (oldState == PresentIntervalState.Unbounded) + { + if (ConfigurationState.Instance.Graphics.EnableCustomPresentInterval) + { + newState = PresentIntervalState.Custom; + } + else + { + newState = PresentIntervalState.Switch; + } + } + else + { + newState = PresentIntervalState.Switch; + } + UpdatePresentIntervalState(this, new ReactiveEventArgs(oldState, newState)); + _viewModel.ShowCustomPresentIntervalPicker = + (newState == PresentIntervalState.Custom); + break; + case KeyboardHotkeyState.CustomPresentIntervalDecrement: + Device.DecrementCustomPresentInterval(); + _viewModel.CustomPresentInterval -= 1; + break; + case KeyboardHotkeyState.CustomPresentIntervalIncrement: + Device.IncrementCustomPresentInterval(); + _viewModel.CustomPresentInterval += 1; break; case KeyboardHotkeyState.Screenshot: ScreenshotRequested = true; @@ -1154,9 +1228,9 @@ private KeyboardHotkeyState GetHotkeyState() { KeyboardHotkeyState state = KeyboardHotkeyState.None; - if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ToggleVsync)) + if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.PresentIntervalState)) { - state = KeyboardHotkeyState.ToggleVSync; + state = KeyboardHotkeyState.TogglePresentIntervalState; } else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.Screenshot)) { @@ -1190,6 +1264,14 @@ private KeyboardHotkeyState GetHotkeyState() { state = KeyboardHotkeyState.VolumeDown; } + else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CustomPresentIntervalIncrement)) + { + state = KeyboardHotkeyState.CustomPresentIntervalIncrement; + } + else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CustomPresentIntervalDecrement)) + { + state = KeyboardHotkeyState.CustomPresentIntervalDecrement; + } return state; } diff --git a/src/Ryujinx.Ava/Assets/Locales/en_US.json b/src/Ryujinx.Ava/Assets/Locales/en_US.json index 72b5e8e3c..1b2705b7a 100644 --- a/src/Ryujinx.Ava/Assets/Locales/en_US.json +++ b/src/Ryujinx.Ava/Assets/Locales/en_US.json @@ -124,9 +124,18 @@ "SettingsTabSystemSystemLanguageLatinAmericanSpanish": "Latin American Spanish", "SettingsTabSystemSystemLanguageSimplifiedChinese": "Simplified Chinese", "SettingsTabSystemSystemLanguageTraditionalChinese": "Traditional Chinese", - "SettingsTabSystemSystemTimeZone": "System TimeZone:", + "SettingsTabSystemSystemTimeZone": "System Time Zone:", "SettingsTabSystemSystemTime": "System Time:", - "SettingsTabSystemEnableVsync": "VSync", + "SettingsTabSystemPresentIntervalState": "Present Interval Mode (VSync):", + "SettingsTabSystemEnableCustomPresentInterval": "Enable toggle for custom present interval", + "SettingsTabSystemPresentIntervalStateSwitch": "60 (VSync ON)", + "SettingsTabSystemPresentIntervalStateUnbounded": "Unbounded (VSync OFF)", + "SettingsTabSystemPresentIntervalStateCustom": "Custom", + "SettingsTabSystemPresentIntervalStateTooltip": "Previously VSync. Use the Custom mode to set a specific refresh interval target. In some titles, the custom mode will act as an FPS cap. In others, it may lead to unpredictable behavior or do nothing at all. \n\nLeave at 60 (VSync ON) if unsure.", + "SettingsTabSystemEnableCustomPresentIntervalTooltip": "The present interval mode toggle will also cycle through the custom interval mode.", + "SettingsTabSystemCustomPresentIntervalValueTooltip": "The custom present interval target value.", + "SettingsTabSystemCustomPresentIntervalSliderTooltip": "The custom present interval target, as a percentage of the normal Switch interval.", + "SettingsTabSystemCustomPresentIntervalValue": "Custom Present Interval Value:", "SettingsTabSystemEnablePptc": "PPTC (Profiled Persistent Translation Cache)", "SettingsTabSystemEnableFsIntegrityChecks": "FS Integrity Checks", "SettingsTabSystemAudioBackend": "Audio Backend:", @@ -134,6 +143,7 @@ "SettingsTabSystemAudioBackendOpenAL": "OpenAL", "SettingsTabSystemAudioBackendSoundIO": "SoundIO", "SettingsTabSystemAudioBackendSDL2": "SDL2", + "SettingsTabSystemCustomPresentInterval": "Interval", "SettingsTabSystemHacks": "Hacks", "SettingsTabSystemHacksNote": "May cause instability", "SettingsTabSystemExpandDramSize": "Use alternative memory layout (Developers)", @@ -574,11 +584,13 @@ "RyujinxUpdater": "Ryujinx Updater", "SettingsTabHotkeys": "Keyboard Hotkeys", "SettingsTabHotkeysHotkeys": "Keyboard Hotkeys", - "SettingsTabHotkeysToggleVsyncHotkey": "Toggle VSync:", + "SettingsTabHotkeysTogglePresentIntervalStateHotkey": "Toggle Present Interval state:", "SettingsTabHotkeysScreenshotHotkey": "Screenshot:", "SettingsTabHotkeysShowUiHotkey": "Show UI:", "SettingsTabHotkeysPauseHotkey": "Pause:", "SettingsTabHotkeysToggleMuteHotkey": "Mute:", + "SettingsTabHotkeysIncrementCustomPresentIntervalHotkey": "Raise custom refresh interval", + "SettingsTabHotkeysDecrementCustomPresentIntervalHotkey": "Lower custom refresh interval", "ControllerMotionTitle": "Motion Control Settings", "ControllerRumbleTitle": "Rumble Settings", "SettingsSelectThemeFileDialogTitle": "Select Theme File", diff --git a/src/Ryujinx.Ava/Assets/Styles/Themes.xaml b/src/Ryujinx.Ava/Assets/Styles/Themes.xaml index 0f323f84b..056eba228 100644 --- a/src/Ryujinx.Ava/Assets/Styles/Themes.xaml +++ b/src/Ryujinx.Ava/Assets/Styles/Themes.xaml @@ -26,8 +26,9 @@ #b3ffffff #80cccccc #A0000000 - #FF2EEAC9 - #FFFF4554 + #FF2EEAC9 + #FFFF4554 + #6483F5 _vsyncColor; + get => _presentIntervalStateColor; set { - _vsyncColor = value; + _presentIntervalStateColor = value; OnPropertyChanged(); } } + public bool ShowCustomPresentIntervalPicker + { + get + { + if (_isGameRunning) + { + return AppHost.Device.PresentIntervalState == + PresentIntervalState.Custom; + } + else + { + return false; + } + } + set + { + OnPropertyChanged(); + } + } + + public int CustomPresentIntervalPercentageProxy + { + get => _customPresentIntervalPercentageProxy; + set + { + int newInterval = (int)(((decimal)value / 100) * 60); + _customPresentInterval = newInterval; + _customPresentIntervalPercentageProxy = value; + ConfigurationState.Instance.Graphics.CustomPresentInterval.Value = newInterval; + if (_isGameRunning) + { + AppHost.Device.CustomPresentInterval = newInterval; + AppHost.Device.UpdatePresentInterval(); + } + OnPropertyChanged((nameof(CustomPresentInterval))); + OnPropertyChanged((nameof(CustomPresentIntervalPercentageText))); + } + } + + public string CustomPresentIntervalPercentageText + { + get + { + string text = CustomPresentIntervalPercentageProxy.ToString() + "%"; + return text; + } + set + { + + } + } + + public int CustomPresentInterval + { + get => _customPresentInterval; + set + { + _customPresentInterval = value; + int newPercent = (int)(((decimal)value / 60) * 100); + _customPresentIntervalPercentageProxy = newPercent; + ConfigurationState.Instance.Graphics.CustomPresentInterval.Value = value; + if (_isGameRunning) + { + AppHost.Device.CustomPresentInterval = value; + AppHost.Device.UpdatePresentInterval(); + } + OnPropertyChanged(nameof(CustomPresentIntervalPercentageProxy)); + OnPropertyChanged(nameof(CustomPresentIntervalPercentageText)); + OnPropertyChanged(); + } + } + public byte[] SelectedIcon { get => _selectedIcon; @@ -502,6 +581,17 @@ public string BackendText } } + public string PresentIntervalStateText + { + get => _presentIntervalStateText; + set + { + _presentIntervalStateText = value; + + OnPropertyChanged(); + } + } + public string DockedStatusText { get => _dockedStatusText; @@ -1196,17 +1286,18 @@ private void Update_StatusBar(object sender, StatusUpdatedEventArgs args) { Dispatcher.UIThread.InvokeAsync(() => { - Application.Current.Styles.TryGetResource(args.VSyncEnabled - ? "VsyncEnabled" - : "VsyncDisabled", - Application.Current.ActualThemeVariant, + Application.Current.Styles.TryGetResource(args.PresentIntervalState, + Avalonia.Application.Current.ActualThemeVariant, out object color); if (color is not null) { - VsyncColor = new SolidColorBrush((Color)color); + PresentIntervalStateColor = new SolidColorBrush((Color)color); } + PresentIntervalStateText = args.PresentIntervalState; + ShowCustomPresentIntervalPicker = + args.PresentIntervalState == PresentIntervalState.Custom.ToString(); DockedStatusText = args.DockedMode; AspectRatioStatusText = args.AspectRatio; GameStatusText = args.GameStatus; @@ -1365,6 +1456,57 @@ public void ToggleDockMode() } } + public void UpdatePresentIntervalState() + { + PresentIntervalState oldPresentInterval = AppHost.Device.PresentIntervalState; + PresentIntervalState newPresentInterval = PresentIntervalState.Switch; + bool customPresentIntervalEnabled = ConfigurationState.Instance.Graphics.EnableCustomPresentInterval.Value; + + switch (oldPresentInterval) + { + case PresentIntervalState.Switch: + newPresentInterval = PresentIntervalState.Unbounded; + break; + case PresentIntervalState.Unbounded: + if (customPresentIntervalEnabled) + { + newPresentInterval = PresentIntervalState.Custom; + } + else + { + newPresentInterval = PresentIntervalState.Switch; + } + break; + case PresentIntervalState.Custom: + newPresentInterval = PresentIntervalState.Switch; + break; + } + + ConfigurationState.Instance.Graphics.PresentIntervalState.Value = newPresentInterval; + + if (_isGameRunning) + { + AppHost.UpdatePresentInterval(newPresentInterval); + } + + OnPropertyChanged(nameof(ShowCustomPresentIntervalPicker)); + } + + public void PresentIntervalStateSettingChanged() + { + if (_isGameRunning) + { + AppHost.Device.CustomPresentInterval = ConfigurationState.Instance.Graphics.CustomPresentInterval.Value; + AppHost.Device.UpdatePresentInterval(); + } + + CustomPresentInterval = ConfigurationState.Instance.Graphics.CustomPresentInterval.Value; + OnPropertyChanged(nameof(ShowCustomPresentIntervalPicker)); + OnPropertyChanged(nameof(CustomPresentIntervalPercentageProxy)); + OnPropertyChanged(nameof(CustomPresentIntervalPercentageText)); + OnPropertyChanged(nameof(CustomPresentInterval)); + } + public async Task ExitCurrentState() { if (WindowState == WindowState.FullScreen) diff --git a/src/Ryujinx.Ava/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/SettingsViewModel.cs index 604e34067..564677bec 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/SettingsViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/SettingsViewModel.cs @@ -7,6 +7,7 @@ using Ryujinx.Audio.Backends.SoundIo; using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.Views.Main; using Ryujinx.Ava.UI.Windows; using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration.Hid; @@ -51,6 +52,10 @@ public class SettingsViewModel : BaseModel private string _customThemePath; private int _scalingFilter; private int _scalingFilterLevel; + private int _customPresentInterval; + private bool _enableCustomPresentInterval; + private int _customPresentIntervalPercentageProxy; + private PresentIntervalState _presentIntervalState; public event Action CloseWindow; public event Action SaveSettingsEvent; @@ -137,7 +142,75 @@ public bool DirectoryChanged public bool EnableDockedMode { get; set; } public bool EnableKeyboard { get; set; } public bool EnableMouse { get; set; } - public bool EnableVsync { get; set; } + public PresentIntervalState PresentIntervalState + { + get => _presentIntervalState; + set + { + if (value == PresentIntervalState.Custom) + { + EnableCustomPresentInterval = true; + } + _presentIntervalState = value; + OnPropertyChanged(); + OnPropertyChanged((nameof(EnableCustomPresentInterval))); + } + } + + public int CustomPresentIntervalPercentageProxy + { + get => _customPresentIntervalPercentageProxy; + set + { + int newInterval = (int)(((decimal)value / 100) * 60); + _customPresentInterval = newInterval; + _customPresentIntervalPercentageProxy = value; + OnPropertyChanged((nameof(CustomPresentInterval))); + OnPropertyChanged((nameof(CustomPresentIntervalPercentageText))); + } + } + + public string CustomPresentIntervalPercentageText + { + get + { + string text = CustomPresentIntervalPercentageProxy.ToString() + "%"; + return text; + } + set + { + + } + } + + public bool EnableCustomPresentInterval + { + get => _enableCustomPresentInterval; + set + { + _enableCustomPresentInterval = value; + if (_presentIntervalState == PresentIntervalState.Custom && value == false) + { + PresentIntervalState = PresentIntervalState.Switch; + OnPropertyChanged(nameof(PresentIntervalState)); + } + OnPropertyChanged(); + } + } + + public int CustomPresentInterval + { + get => _customPresentInterval; + set + { + _customPresentInterval = value; + int newPercent = (int)(((decimal)value / 60) * 100); + _customPresentIntervalPercentageProxy = newPercent; + OnPropertyChanged(nameof(CustomPresentIntervalPercentageProxy)); + OnPropertyChanged(nameof(CustomPresentIntervalPercentageText)); + OnPropertyChanged(); + } + } public bool EnablePptc { get; set; } public bool EnableInternetAccess { get; set; } public bool EnableFsIntegrityChecks { get; set; } @@ -448,7 +521,9 @@ public void LoadCurrentConfiguration() CurrentDate = currentDateTime.Date; CurrentTime = currentDateTime.TimeOfDay.Add(TimeSpan.FromSeconds(config.System.SystemTimeOffset)); - EnableVsync = config.Graphics.EnableVsync; + EnableCustomPresentInterval = config.Graphics.EnableCustomPresentInterval.Value; + CustomPresentInterval = config.Graphics.CustomPresentInterval; + PresentIntervalState = config.Graphics.PresentIntervalState; EnableFsIntegrityChecks = config.System.EnableFsIntegrityChecks; ExpandDramSize = config.System.ExpandRam; IgnoreMissingServices = config.System.IgnoreMissingServices; @@ -460,7 +535,7 @@ public void LoadCurrentConfiguration() // Graphics GraphicsBackendIndex = (int)config.Graphics.GraphicsBackend.Value; - // Physical devices are queried asynchronously hence the prefered index config value is loaded in LoadAvailableGpus(). + // Physical devices are queried asynchronously hence the preferred index config value is loaded in LoadAvailableGpus(). EnableShaderCache = config.Graphics.EnableShaderCache; EnableTextureRecompression = config.Graphics.EnableTextureRecompression; EnableMacroHLE = config.Graphics.EnableMacroHLE; @@ -537,7 +612,9 @@ public void SaveSettings() } config.System.SystemTimeOffset.Value = Convert.ToInt64((CurrentDate.ToUnixTimeSeconds() + CurrentTime.TotalSeconds) - DateTimeOffset.Now.ToUnixTimeSeconds()); - config.Graphics.EnableVsync.Value = EnableVsync; + config.Graphics.PresentIntervalState.Value = PresentIntervalState; + config.Graphics.EnableCustomPresentInterval.Value = EnableCustomPresentInterval; + config.Graphics.CustomPresentInterval.Value = CustomPresentInterval; config.System.EnableFsIntegrityChecks.Value = EnableFsIntegrityChecks; config.System.ExpandRam.Value = ExpandDramSize; config.System.IgnoreMissingServices.Value = IgnoreMissingServices; @@ -603,6 +680,7 @@ public void SaveSettings() config.ToFileFormat().SaveConfig(Program.ConfigurationPath); MainWindow.UpdateGraphicsConfig(); + MainWindow.MainWindowViewModel.PresentIntervalStateSettingChanged(); SaveSettingsEvent?.Invoke(); diff --git a/src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml b/src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml index f9e192e62..c53281c2a 100644 --- a/src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml @@ -1,4 +1,4 @@ - + PointerReleased="PresentIntervalState_PointerReleased" + Text="{Binding PresentIntervalStateText}" + TextAlignment="Left"/> + - + @@ -97,7 +97,23 @@ TextAlignment="Center" /> + + + + + + + + + + + + - \ No newline at end of file + diff --git a/src/Ryujinx.Ava/UI/Views/Settings/SettingsSystemView.axaml b/src/Ryujinx.Ava/UI/Views/Settings/SettingsSystemView.axaml index e6f7c6e46..3ded1e384 100644 --- a/src/Ryujinx.Ava/UI/Views/Settings/SettingsSystemView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Settings/SettingsSystemView.axaml @@ -4,6 +4,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" @@ -181,10 +182,67 @@ Width="350" ToolTip.Tip="{locale:Locale TimeTooltip}" /> - + + VerticalAlignment="Center" + Text="{locale:Locale SettingsTabSystemPresentIntervalState}" + ToolTip.Tip="{locale:Locale SettingsTabSystemPresentIntervalStateTooltip}" + Width="250" /> + + + + + + + + + + + + + + + + + + + + - \ No newline at end of file + diff --git a/src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs index 1aecbd041..5da60d256 100644 --- a/src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs +++ b/src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs @@ -10,6 +10,7 @@ using Ryujinx.Ava.UI.Applet; using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Ava.UI.Views.Main; using Ryujinx.Common.Logging; using Ryujinx.Graphics.Gpu; using Ryujinx.HLE.FileSystem; diff --git a/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs b/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs index b4f2f9468..458ea5ca5 100644 --- a/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs +++ b/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs @@ -4,7 +4,7 @@ namespace Ryujinx.Common.Configuration.Hid // This breaks Avalonia's TwoWay binding, which makes us unable to save new KeyboardHotkeys. public class KeyboardHotkeys { - public Key ToggleVsync { get; set; } + public Key PresentIntervalState { get; set; } public Key Screenshot { get; set; } public Key ShowUi { get; set; } public Key Pause { get; set; } @@ -13,5 +13,7 @@ public class KeyboardHotkeys public Key ResScaleDown { get; set; } public Key VolumeUp { get; set; } public Key VolumeDown { get; set; } + public Key CustomPresentIntervalIncrement { get; set; } + public Key CustomPresentIntervalDecrement { get; set; } } } diff --git a/src/Ryujinx.Common/Configuration/PresentIntervalState.cs b/src/Ryujinx.Common/Configuration/PresentIntervalState.cs new file mode 100644 index 000000000..98c3e88c7 --- /dev/null +++ b/src/Ryujinx.Common/Configuration/PresentIntervalState.cs @@ -0,0 +1,15 @@ +using Ryujinx.Common.Utilities; +using System; +using System.Text.Json.Serialization; + +namespace Ryujinx.Common.Configuration +{ + [JsonConverter(typeof(TypedStringEnumConverter))] + [Flags] + public enum PresentIntervalState + { + Switch = 0, + Unbounded = 1 << 0, + Custom = 1 << 1 + } +} diff --git a/src/Ryujinx.Graphics.GAL/IWindow.cs b/src/Ryujinx.Graphics.GAL/IWindow.cs index 83418e709..285aa1d5f 100644 --- a/src/Ryujinx.Graphics.GAL/IWindow.cs +++ b/src/Ryujinx.Graphics.GAL/IWindow.cs @@ -1,3 +1,4 @@ +using Ryujinx.Common.Configuration; using System; namespace Ryujinx.Graphics.GAL @@ -8,7 +9,7 @@ public interface IWindow void SetSize(int width, int height); - void ChangeVSyncMode(bool vsyncEnabled); + void ChangePresentIntervalState(PresentIntervalState presentIntervalState); void SetAntiAliasing(AntiAliasing antialiasing); void SetScalingFilter(ScalingFilter type); diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedWindow.cs b/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedWindow.cs index acda37ef3..f506d08c7 100644 --- a/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedWindow.cs +++ b/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedWindow.cs @@ -1,3 +1,4 @@ +using Ryujinx.Common.Configuration; using Ryujinx.Graphics.GAL.Multithreading.Commands.Window; using Ryujinx.Graphics.GAL.Multithreading.Model; using Ryujinx.Graphics.GAL.Multithreading.Resources; @@ -31,7 +32,7 @@ public void SetSize(int width, int height) _impl.Window.SetSize(width, height); } - public void ChangeVSyncMode(bool vsyncEnabled) { } + public void ChangePresentIntervalState(PresentIntervalState presentIntervalState) { } public void SetAntiAliasing(AntiAliasing effect) { } diff --git a/src/Ryujinx.Graphics.GAL/PresentIntervalState.cs b/src/Ryujinx.Graphics.GAL/PresentIntervalState.cs new file mode 100644 index 000000000..8c4cca19e --- /dev/null +++ b/src/Ryujinx.Graphics.GAL/PresentIntervalState.cs @@ -0,0 +1,9 @@ +namespace Ryujinx.Graphics.GAL +{ + public enum PresentIntervalState + { + Switch = 0, + Unbounded = 1 << 0, + Custom = 1 << 1 + } +} diff --git a/src/Ryujinx.Graphics.OpenGL/Window.cs b/src/Ryujinx.Graphics.OpenGL/Window.cs index 6bcfefa4e..c85e29e6a 100644 --- a/src/Ryujinx.Graphics.OpenGL/Window.cs +++ b/src/Ryujinx.Graphics.OpenGL/Window.cs @@ -54,7 +54,7 @@ public void Present(ITexture texture, ImageCrop crop, Action swapBuffersCallback GL.PixelStore(PixelStoreParameter.UnpackAlignment, 4); } - public void ChangeVSyncMode(bool vsyncEnabled) { } + public void ChangePresentIntervalState(PresentIntervalState presentIntervalState) { } public void SetSize(int width, int height) { diff --git a/src/Ryujinx.Graphics.Vulkan/Window.cs b/src/Ryujinx.Graphics.Vulkan/Window.cs index 2c5764a99..7edc804fd 100644 --- a/src/Ryujinx.Graphics.Vulkan/Window.cs +++ b/src/Ryujinx.Graphics.Vulkan/Window.cs @@ -29,7 +29,7 @@ class Window : WindowBase, IDisposable private int _width; private int _height; - private bool _vsyncEnabled; + private PresentIntervalState _presentIntervalState; private bool _swapchainIsDirty; private VkFormat _format; private AntiAliasing _currentAntiAliasing; @@ -139,7 +139,7 @@ private unsafe void CreateSwapchain() ImageArrayLayers = 1, PreTransform = capabilities.CurrentTransform, CompositeAlpha = ChooseCompositeAlpha(capabilities.SupportedCompositeAlpha), - PresentMode = ChooseSwapPresentMode(presentModes, _vsyncEnabled), + PresentMode = ChooseSwapPresentMode(presentModes, _presentIntervalState), Clipped = true, }; @@ -261,9 +261,9 @@ private static CompositeAlphaFlagsKHR ChooseCompositeAlpha(CompositeAlphaFlagsKH } } - private static PresentModeKHR ChooseSwapPresentMode(PresentModeKHR[] availablePresentModes, bool vsyncEnabled) + private static PresentModeKHR ChooseSwapPresentMode(PresentModeKHR[] availablePresentModes, PresentIntervalState presentIntervalState) { - if (!vsyncEnabled && availablePresentModes.Contains(PresentModeKHR.ImmediateKhr)) + if (presentIntervalState == PresentIntervalState.Unbounded && availablePresentModes.Contains(PresentModeKHR.ImmediateKhr)) { return PresentModeKHR.ImmediateKhr; } @@ -612,9 +612,9 @@ public override void SetSize(int width, int height) // Not needed as we can get the size from the surface. } - public override void ChangeVSyncMode(bool vsyncEnabled) + public override void ChangePresentIntervalState(PresentIntervalState presentIntervalState) { - _vsyncEnabled = vsyncEnabled; + _presentIntervalState = presentIntervalState; _swapchainIsDirty = true; } diff --git a/src/Ryujinx.Graphics.Vulkan/WindowBase.cs b/src/Ryujinx.Graphics.Vulkan/WindowBase.cs index edb9c688c..ecbf4493f 100644 --- a/src/Ryujinx.Graphics.Vulkan/WindowBase.cs +++ b/src/Ryujinx.Graphics.Vulkan/WindowBase.cs @@ -10,7 +10,7 @@ internal abstract class WindowBase : IWindow public abstract void Dispose(); public abstract void Present(ITexture texture, ImageCrop crop, Action swapBuffersCallback); public abstract void SetSize(int width, int height); - public abstract void ChangeVSyncMode(bool vsyncEnabled); + public abstract void ChangePresentIntervalState(PresentIntervalState presentIntervalState); public abstract void SetAntiAliasing(AntiAliasing effect); public abstract void SetScalingFilter(ScalingFilter scalerType); public abstract void SetScalingFilterLevel(float scale); diff --git a/src/Ryujinx.HLE/HLEConfiguration.cs b/src/Ryujinx.HLE/HLEConfiguration.cs index f589bfdda..07d0fa095 100644 --- a/src/Ryujinx.HLE/HLEConfiguration.cs +++ b/src/Ryujinx.HLE/HLEConfiguration.cs @@ -9,6 +9,7 @@ using Ryujinx.HLE.HOS.SystemState; using Ryujinx.HLE.Ui; using System; +using PresentIntervalState = Ryujinx.Common.Configuration.PresentIntervalState; namespace Ryujinx.HLE { @@ -84,9 +85,14 @@ public class HLEConfiguration internal readonly RegionCode Region; /// - /// Control the initial state of the vertical sync in the SurfaceFlinger service. + /// Control the initial state of the present interval in the SurfaceFlinger service (previously Vsync). /// - internal readonly bool EnableVsync; + internal readonly PresentIntervalState PresentIntervalState; + + /// + /// Control the custom present interval, if enabled and active. + /// + internal readonly int CustomPresentInterval; /// /// Control the initial state of the docked mode. @@ -180,7 +186,7 @@ public HLEConfiguration(VirtualFileSystem virtualFileSystem, IHostUiHandler hostUiHandler, SystemLanguage systemLanguage, RegionCode region, - bool enableVsync, + PresentIntervalState presentIntervalState, bool enableDockedMode, bool enablePtc, bool enableInternetAccess, @@ -194,7 +200,9 @@ public HLEConfiguration(VirtualFileSystem virtualFileSystem, float audioVolume, bool useHypervisor, string multiplayerLanInterfaceId, - MultiplayerMode multiplayerMode) + MultiplayerMode multiplayerMode, + //bool enableCustomPresentInterval, + int customPresentInterval) { VirtualFileSystem = virtualFileSystem; LibHacHorizonManager = libHacHorizonManager; @@ -207,7 +215,9 @@ public HLEConfiguration(VirtualFileSystem virtualFileSystem, HostUiHandler = hostUiHandler; SystemLanguage = systemLanguage; Region = region; - EnableVsync = enableVsync; + PresentIntervalState = presentIntervalState; + //EnableCustomPresentInterval = enableCustomPresentInterval; + CustomPresentInterval = customPresentInterval; EnableDockedMode = enableDockedMode; EnablePtc = enablePtc; EnableInternetAccess = enableInternetAccess; diff --git a/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/SurfaceFlinger.cs b/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/SurfaceFlinger.cs index fd517b1ae..b79b85306 100644 --- a/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/SurfaceFlinger.cs +++ b/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/SurfaceFlinger.cs @@ -10,13 +10,12 @@ using System.Diagnostics; using System.Linq; using System.Threading; +using PresentIntervalState = Ryujinx.Common.Configuration.PresentIntervalState; namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger { class SurfaceFlinger : IConsumerListener, IDisposable { - private const int TargetFps = 60; - private readonly Switch _device; private readonly Dictionary _layers; @@ -34,6 +33,7 @@ class SurfaceFlinger : IConsumerListener, IDisposable private int _swapInterval; private int _swapIntervalDelay; + private bool _targetPresentIntervalChanged = true; private readonly object _lock = new(); @@ -58,6 +58,7 @@ private class TextureCallbackInformation public SurfaceFlinger(Switch device) { _device = device; + _device.TargetPresentIntervalChanged += () => _targetPresentIntervalChanged = true; _layers = new Dictionary(); RenderLayerId = 0; @@ -81,14 +82,18 @@ private void UpdateSwapInterval(int swapInterval) _swapInterval = swapInterval; // If the swap interval is 0, Game VSync is disabled. - if (_swapInterval == 0) - { - _nextFrameEvent.Set(); - _ticksPerFrame = 1; - } - else + if (_targetPresentIntervalChanged) { - _ticksPerFrame = Stopwatch.Frequency / TargetFps; + _targetPresentIntervalChanged = false; + if (_swapInterval == 0) + { + _nextFrameEvent.Set(); + _ticksPerFrame = 1; + } + else + { + _ticksPerFrame = Stopwatch.Frequency / (long)_device.TargetPresentInterval; + } } } @@ -371,14 +376,11 @@ public void Compose() if (acquireStatus == Status.Success) { // If device vsync is disabled, reflect the change. - if (!_device.EnableDeviceVsync) + if (_device.PresentIntervalState == PresentIntervalState.Unbounded) { - if (_swapInterval != 0) - { - UpdateSwapInterval(0); - } + UpdateSwapInterval(0); } - else if (item.SwapInterval != _swapInterval) + else { UpdateSwapInterval(item.SwapInterval); } diff --git a/src/Ryujinx.HLE/Switch.cs b/src/Ryujinx.HLE/Switch.cs index ae063a47d..8ad263115 100644 --- a/src/Ryujinx.HLE/Switch.cs +++ b/src/Ryujinx.HLE/Switch.cs @@ -27,7 +27,24 @@ public class Switch : IDisposable public TamperMachine TamperMachine { get; } public IHostUiHandler UiHandler { get; } - public bool EnableDeviceVsync { get; set; } = true; + public PresentIntervalState PresentIntervalState { get; set; } = PresentIntervalState.Switch; + public bool CustomPresentIntervalEnabled { get; set; } = false; + public int CustomPresentInterval { get; set; } + + public long TargetPresentInterval { get; set; } = 60; + public Action TargetPresentIntervalChanged { get; set; } + + public bool EnableDeviceVsync + { + get + { + return PresentIntervalState == PresentIntervalState.Switch; + } + set + { + PresentIntervalState = value ? PresentIntervalState.Switch : PresentIntervalState.Unbounded; + } + } public bool IsFrameAvailable => Gpu.Window.IsFrameAvailable; @@ -58,12 +75,20 @@ public Switch(HLEConfiguration configuration) System.State.SetLanguage(Configuration.SystemLanguage); System.State.SetRegion(Configuration.Region); - EnableDeviceVsync = Configuration.EnableVsync; + EnableDeviceVsync = Configuration.PresentIntervalState == PresentIntervalState.Switch; + PresentIntervalState = Configuration.PresentIntervalState; + CustomPresentInterval = Configuration.CustomPresentInterval; System.State.DockedMode = Configuration.EnableDockedMode; System.PerformanceState.PerformanceMode = System.State.DockedMode ? PerformanceMode.Boost : PerformanceMode.Default; System.EnablePtc = Configuration.EnablePtc; System.FsIntegrityCheckLevel = Configuration.FsIntegrityCheckLevel; System.GlobalAccessLogMode = Configuration.FsGlobalAccessLogMode; + UpdatePresentInterval(); //todo remove these comments before committing. + // this call seems awkward. maybe this should be part of HOS.Horizon, + // where surfaceflinger is initialized? not sure though since it isn't a genuine + // Switch system setting. either way, if not, we need this call here because + // SurfaceFlinger was initialized with the default target interval + // of 60. #pragma warning restore IDE0055 } @@ -114,6 +139,35 @@ public void PresentFrame(Action swapBuffersCallback) Gpu.Window.Present(swapBuffersCallback); } + public void IncrementCustomPresentInterval() + { + CustomPresentInterval += 1; + UpdatePresentInterval(); + } + + public void DecrementCustomPresentInterval() + { + CustomPresentInterval -= 1; + UpdatePresentInterval(); + } + + public void UpdatePresentInterval() + { + switch (PresentIntervalState) + { + case PresentIntervalState.Custom: + TargetPresentInterval = CustomPresentInterval; + break; + case PresentIntervalState.Switch: + TargetPresentInterval = 60; + break; + case PresentIntervalState.Unbounded: + TargetPresentInterval = 1; + break; + } + TargetPresentIntervalChanged.Invoke(); + } + public void SetVolume(float volume) { System.SetVolume(Math.Clamp(volume, 0, 1)); diff --git a/src/Ryujinx.Headless.SDL2/Options.cs b/src/Ryujinx.Headless.SDL2/Options.cs index ea2063758..f80fe3eeb 100644 --- a/src/Ryujinx.Headless.SDL2/Options.cs +++ b/src/Ryujinx.Headless.SDL2/Options.cs @@ -114,9 +114,15 @@ public class Options [Option("fs-global-access-log-mode", Required = false, Default = 0, HelpText = "Enables FS access log output to the console.")] public int FsGlobalAccessLogMode { get; set; } - [Option("disable-vsync", Required = false, HelpText = "Disables Vertical Sync.")] + [Option("disable-vsync", Required = false, HelpText = "Disables Vertical Sync. Deprecated. Use present-interval-mode instead.")] public bool DisableVSync { get; set; } + [Option("present-interval-mode", Required = false, Default = PresentIntervalState.Switch, HelpText = "Sets the present interval mode (Switch, Unbounded, or Custom).")] + public PresentIntervalState PresentIntervalState { get; set; } + + [Option("custom-present-interval", Required = false, Default = 90, HelpText = "Sets the custom present interval target value (integer).")] + public int CustomPresentInterval { get; set; } + [Option("disable-shader-cache", Required = false, HelpText = "Disables Shader cache.")] public bool DisableShaderCache { get; set; } diff --git a/src/Ryujinx.Headless.SDL2/Program.cs b/src/Ryujinx.Headless.SDL2/Program.cs index 7e3c79f54..729ca2a52 100644 --- a/src/Ryujinx.Headless.SDL2/Program.cs +++ b/src/Ryujinx.Headless.SDL2/Program.cs @@ -543,7 +543,7 @@ private static Switch InitializeEmulationContext(WindowBase window, IRenderer re window, options.SystemLanguage, options.SystemRegion, - !options.DisableVSync, + options.PresentIntervalState, !options.DisableDockedMode, !options.DisablePTC, options.EnableInternetAccess, @@ -557,7 +557,8 @@ private static Switch InitializeEmulationContext(WindowBase window, IRenderer re options.AudioVolume, options.UseHypervisor ?? true, options.MultiplayerLanInterfaceId, - Common.Configuration.Multiplayer.MultiplayerMode.Disabled); + Common.Configuration.Multiplayer.MultiplayerMode.Disabled, + options.CustomPresentInterval); return new Switch(configuration); } diff --git a/src/Ryujinx.Headless.SDL2/StatusUpdatedEventArgs.cs b/src/Ryujinx.Headless.SDL2/StatusUpdatedEventArgs.cs index 0b199d128..2e6d9b9f0 100644 --- a/src/Ryujinx.Headless.SDL2/StatusUpdatedEventArgs.cs +++ b/src/Ryujinx.Headless.SDL2/StatusUpdatedEventArgs.cs @@ -4,16 +4,16 @@ namespace Ryujinx.Headless.SDL2 { class StatusUpdatedEventArgs : EventArgs { - public bool VSyncEnabled; + public string PresentIntervalState; public string DockedMode; public string AspectRatio; public string GameStatus; public string FifoStatus; public string GpuName; - public StatusUpdatedEventArgs(bool vSyncEnabled, string dockedMode, string aspectRatio, string gameStatus, string fifoStatus, string gpuName) + public StatusUpdatedEventArgs(string presentIntervalState, string dockedMode, string aspectRatio, string gameStatus, string fifoStatus, string gpuName) { - VSyncEnabled = vSyncEnabled; + PresentIntervalState = presentIntervalState; DockedMode = dockedMode; AspectRatio = aspectRatio; GameStatus = gameStatus; diff --git a/src/Ryujinx.Headless.SDL2/WindowBase.cs b/src/Ryujinx.Headless.SDL2/WindowBase.cs index adab07641..24f25fd82 100644 --- a/src/Ryujinx.Headless.SDL2/WindowBase.cs +++ b/src/Ryujinx.Headless.SDL2/WindowBase.cs @@ -311,7 +311,7 @@ public void Render() } StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs( - Device.EnableDeviceVsync, + Device.PresentIntervalState.ToString(), dockedMode, Device.Configuration.AspectRatio.ToText(), $"Game: {Device.Statistics.GetGameFrameRate():00.00} FPS ({Device.Statistics.GetGameFrameTime():00.00} ms)", diff --git a/src/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormat.cs b/src/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormat.cs index 8a4db1fe7..46dc50c13 100644 --- a/src/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormat.cs +++ b/src/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormat.cs @@ -1,3 +1,4 @@ +using Ryujinx.Common; using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Multiplayer; @@ -15,7 +16,7 @@ public class ConfigurationFileFormat /// /// The current version of the file format /// - public const int CurrentVersion = 48; + public const int CurrentVersion = 49; /// /// Version of the configuration file format @@ -170,8 +171,25 @@ public class ConfigurationFileFormat /// /// Enables or disables Vertical Sync /// + /// Kept for file format compatibility (to avoid possible failure when parsing configuration on old versions) + /// TODO: Remove this when those older versions aren't in use anymore. public bool EnableVsync { get; set; } + /// + /// Current present interval state; 60 (Switch), unbounded ("Vsync off"), or custom + /// + public PresentIntervalState PresentIntervalState { get; set; } + + /// + /// Enables or disables the custom present interval + /// + public bool EnableCustomPresentInterval { get; set; } + + /// + /// The custom present interval value + /// + public int CustomPresentInterval { get; set; } + /// /// Enables or disables Shader cache /// diff --git a/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs b/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs index b017d384c..b91a4e8e3 100644 --- a/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs +++ b/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs @@ -468,10 +468,25 @@ public class GraphicsSection public ReactiveObject ShadersDumpPath { get; private set; } /// - /// Enables or disables Vertical Sync + /// Toggles VSync on or off. Deprecated, included for GTK compatibility. /// public ReactiveObject EnableVsync { get; private set; } + /// + /// Toggles the present interval mode. Options are Switch (60Hz), Unbounded (previously Vsync off), and Custom, if enabled. + /// + public ReactiveObject PresentIntervalState { get; private set; } + + /// + /// Enables or disables the custom present interval mode. + /// + public ReactiveObject EnableCustomPresentInterval { get; private set; } + + /// + /// Changes the custom present interval. + /// + public ReactiveObject CustomPresentInterval { get; private set; } + /// /// Enables or disables Shader cache /// @@ -530,8 +545,14 @@ public GraphicsSection() AspectRatio = new ReactiveObject(); AspectRatio.Event += static (sender, e) => LogValueChange(e, nameof(AspectRatio)); ShadersDumpPath = new ReactiveObject(); + PresentIntervalState = new ReactiveObject(); + PresentIntervalState.Event += static (sender, e) => LogValueChange(e, nameof(PresentIntervalState)); + EnableCustomPresentInterval = new ReactiveObject(); + EnableCustomPresentInterval.Event += static (sender, e) => LogValueChange(e, nameof(EnableCustomPresentInterval)); EnableVsync = new ReactiveObject(); EnableVsync.Event += static (sender, e) => LogValueChange(e, nameof(EnableVsync)); + CustomPresentInterval = new ReactiveObject(); + CustomPresentInterval.Event += static (sender, e) => LogValueChange(e, nameof(CustomPresentInterval)); EnableShaderCache = new ReactiveObject(); EnableShaderCache.Event += static (sender, e) => LogValueChange(e, nameof(EnableShaderCache)); EnableTextureRecompression = new ReactiveObject(); @@ -680,6 +701,9 @@ public ConfigurationFileFormat ToFileFormat() ShowConfirmExit = ShowConfirmExit, HideCursor = HideCursor, EnableVsync = Graphics.EnableVsync, + PresentIntervalState = Graphics.PresentIntervalState, + EnableCustomPresentInterval = Graphics.EnableCustomPresentInterval, + CustomPresentInterval = Graphics.CustomPresentInterval, EnableShaderCache = Graphics.EnableShaderCache, EnableTextureRecompression = Graphics.EnableTextureRecompression, EnableMacroHLE = Graphics.EnableMacroHLE, @@ -787,6 +811,9 @@ public void LoadDefault() ShowConfirmExit.Value = true; HideCursor.Value = HideCursorMode.OnIdle; Graphics.EnableVsync.Value = true; + Graphics.PresentIntervalState.Value = PresentIntervalState.Switch; + Graphics.CustomPresentInterval.Value = 120; + Graphics.EnableCustomPresentInterval.Value = false; Graphics.EnableShaderCache.Value = true; Graphics.EnableTextureRecompression.Value = false; Graphics.EnableMacroHLE.Value = true; @@ -845,7 +872,7 @@ public void LoadDefault() Hid.EnableMouse.Value = false; Hid.Hotkeys.Value = new KeyboardHotkeys { - ToggleVsync = Key.F1, + PresentIntervalState = Key.F1, ToggleMute = Key.F2, Screenshot = Key.F8, ShowUi = Key.F4, @@ -976,7 +1003,7 @@ public void Load(ConfigurationFileFormat configurationFileFormat, string configu configurationFileFormat.Hotkeys = new KeyboardHotkeys { - ToggleVsync = Key.F1, + PresentIntervalState = Key.F1, }; configurationFileUpdated = true; @@ -1170,7 +1197,7 @@ public void Load(ConfigurationFileFormat configurationFileFormat, string configu configurationFileFormat.Hotkeys = new KeyboardHotkeys { - ToggleVsync = Key.F1, + PresentIntervalState = Key.F1, Screenshot = Key.F8, }; @@ -1183,7 +1210,7 @@ public void Load(ConfigurationFileFormat configurationFileFormat, string configu configurationFileFormat.Hotkeys = new KeyboardHotkeys { - ToggleVsync = Key.F1, + PresentIntervalState = Key.F1, Screenshot = Key.F8, ShowUi = Key.F4, }; @@ -1226,7 +1253,7 @@ public void Load(ConfigurationFileFormat configurationFileFormat, string configu configurationFileFormat.Hotkeys = new KeyboardHotkeys { - ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, + PresentIntervalState = Key.F1, Screenshot = configurationFileFormat.Hotkeys.Screenshot, ShowUi = configurationFileFormat.Hotkeys.ShowUi, Pause = Key.F5, @@ -1241,7 +1268,7 @@ public void Load(ConfigurationFileFormat configurationFileFormat, string configu configurationFileFormat.Hotkeys = new KeyboardHotkeys { - ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, + PresentIntervalState = Key.F1, Screenshot = configurationFileFormat.Hotkeys.Screenshot, ShowUi = configurationFileFormat.Hotkeys.ShowUi, Pause = configurationFileFormat.Hotkeys.Pause, @@ -1315,7 +1342,7 @@ public void Load(ConfigurationFileFormat configurationFileFormat, string configu configurationFileFormat.Hotkeys = new KeyboardHotkeys { - ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, + PresentIntervalState = Key.F1, Screenshot = configurationFileFormat.Hotkeys.Screenshot, ShowUi = configurationFileFormat.Hotkeys.ShowUi, Pause = configurationFileFormat.Hotkeys.Pause, @@ -1342,7 +1369,7 @@ public void Load(ConfigurationFileFormat configurationFileFormat, string configu configurationFileFormat.Hotkeys = new KeyboardHotkeys { - ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, + PresentIntervalState = Key.F1, Screenshot = configurationFileFormat.Hotkeys.Screenshot, ShowUi = configurationFileFormat.Hotkeys.ShowUi, Pause = configurationFileFormat.Hotkeys.Pause, @@ -1430,6 +1457,32 @@ public void Load(ConfigurationFileFormat configurationFileFormat, string configu configurationFileUpdated = true; } + if (configurationFileFormat.Version < 49) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 49."); + + configurationFileFormat.PresentIntervalState = PresentIntervalState.Switch; + configurationFileFormat.EnableCustomPresentInterval = false; + + configurationFileFormat.Hotkeys = new KeyboardHotkeys + { + PresentIntervalState = Key.F1, + Screenshot = configurationFileFormat.Hotkeys.Screenshot, + ShowUi = configurationFileFormat.Hotkeys.ShowUi, + Pause = configurationFileFormat.Hotkeys.Pause, + ToggleMute = configurationFileFormat.Hotkeys.ToggleMute, + ResScaleUp = configurationFileFormat.Hotkeys.ResScaleUp, + ResScaleDown = configurationFileFormat.Hotkeys.ResScaleDown, + VolumeUp = configurationFileFormat.Hotkeys.VolumeUp, + VolumeDown = configurationFileFormat.Hotkeys.VolumeDown, + CustomPresentIntervalIncrement = Key.Unbound, + CustomPresentIntervalDecrement = Key.Unbound, + }; + configurationFileFormat.CustomPresentInterval = 90; + + configurationFileUpdated = true; + } + Logger.EnableFileLog.Value = configurationFileFormat.EnableFileLog; Graphics.ResScale.Value = configurationFileFormat.ResScale; Graphics.ResScaleCustom.Value = configurationFileFormat.ResScaleCustom; @@ -1461,7 +1514,10 @@ public void Load(ConfigurationFileFormat configurationFileFormat, string configu CheckUpdatesOnStart.Value = configurationFileFormat.CheckUpdatesOnStart; ShowConfirmExit.Value = configurationFileFormat.ShowConfirmExit; HideCursor.Value = configurationFileFormat.HideCursor; - Graphics.EnableVsync.Value = configurationFileFormat.EnableVsync; + Graphics.EnableVsync.Value = configurationFileFormat.PresentIntervalState == PresentIntervalState.Switch; + Graphics.PresentIntervalState.Value = configurationFileFormat.PresentIntervalState; + Graphics.EnableCustomPresentInterval.Value = configurationFileFormat.EnableCustomPresentInterval; + Graphics.CustomPresentInterval.Value = configurationFileFormat.CustomPresentInterval; Graphics.EnableShaderCache.Value = configurationFileFormat.EnableShaderCache; Graphics.EnableTextureRecompression.Value = configurationFileFormat.EnableTextureRecompression; Graphics.EnableMacroHLE.Value = configurationFileFormat.EnableMacroHLE; diff --git a/src/Ryujinx/Ui/MainWindow.cs b/src/Ryujinx/Ui/MainWindow.cs index 2a088f561..f33abb395 100644 --- a/src/Ryujinx/Ui/MainWindow.cs +++ b/src/Ryujinx/Ui/MainWindow.cs @@ -44,6 +44,7 @@ using System.Threading; using System.Threading.Tasks; using GUI = Gtk.Builder.ObjectAttribute; +using PresentIntervalState = Ryujinx.Common.Configuration.PresentIntervalState; using ShaderCacheLoadingState = Ryujinx.Graphics.Gpu.Shader.ShaderCacheState; namespace Ryujinx.Ui @@ -656,7 +657,7 @@ private void InitializeSwitchInstance() _uiHandler, (SystemLanguage)ConfigurationState.Instance.System.Language.Value, (RegionCode)ConfigurationState.Instance.System.Region.Value, - ConfigurationState.Instance.Graphics.EnableVsync, + ConfigurationState.Instance.Graphics.EnableVsync ? PresentIntervalState.Switch : PresentIntervalState.Unbounded, ConfigurationState.Instance.System.EnableDockedMode, ConfigurationState.Instance.System.EnablePtc, ConfigurationState.Instance.System.EnableInternetAccess, @@ -670,7 +671,8 @@ private void InitializeSwitchInstance() ConfigurationState.Instance.System.AudioVolume, ConfigurationState.Instance.System.UseHypervisor, ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value, - ConfigurationState.Instance.Multiplayer.Mode); + ConfigurationState.Instance.Multiplayer.Mode, + ConfigurationState.Instance.Graphics.CustomPresentInterval); _emulationContext = new HLE.Switch(configuration); } @@ -1214,7 +1216,7 @@ private void Update_StatusBar(object sender, StatusUpdatedEventArgs args) _gpuBackend.Text = args.GpuBackend; _volumeStatus.Text = GetVolumeLabelText(args.Volume); - if (args.VSyncEnabled) + if (args.PresentIntervalState == PresentIntervalState.Switch.ToString()) { _vSyncStatus.Attributes = new Pango.AttrList(); _vSyncStatus.Attributes.Insert(new Pango.AttrForeground(11822, 60138, 51657)); diff --git a/src/Ryujinx/Ui/RendererWidgetBase.cs b/src/Ryujinx/Ui/RendererWidgetBase.cs index 6ae122a0a..efefaa51b 100644 --- a/src/Ryujinx/Ui/RendererWidgetBase.cs +++ b/src/Ryujinx/Ui/RendererWidgetBase.cs @@ -24,6 +24,7 @@ using System.Threading.Tasks; using Image = SixLabors.ImageSharp.Image; using Key = Ryujinx.Input.Key; +using PresentIntervalState = Ryujinx.Graphics.GAL.PresentIntervalState; using ScalingFilter = Ryujinx.Graphics.GAL.ScalingFilter; using Switch = Ryujinx.HLE.Switch; @@ -452,7 +453,7 @@ public void Render() Device.Gpu.InitializeShaderCache(_gpuCancellationTokenSource.Token); Translator.IsReadyForTranslation.Set(); - Renderer.Window.ChangeVSyncMode(Device.EnableDeviceVsync); + Renderer.Window.ChangePresentIntervalState(Device.EnableDeviceVsync ? PresentIntervalState.Switch : PresentIntervalState.Unbounded); (Toplevel as MainWindow)?.ActivatePauseMenu(); @@ -489,7 +490,7 @@ public void Render() } StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs( - Device.EnableDeviceVsync, + Device.EnableDeviceVsync ? PresentIntervalState.Switch.ToString() : PresentIntervalState.Unbounded.ToString(), Device.GetVolume(), _gpuBackendName, dockedMode, @@ -754,7 +755,7 @@ private KeyboardHotkeyState GetHotkeyState() { KeyboardHotkeyState state = KeyboardHotkeyState.None; - if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ToggleVsync)) + if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.PresentIntervalState)) { state |= KeyboardHotkeyState.ToggleVSync; } diff --git a/src/Ryujinx/Ui/StatusUpdatedEventArgs.cs b/src/Ryujinx/Ui/StatusUpdatedEventArgs.cs index 72e7d7f5b..0a746768f 100644 --- a/src/Ryujinx/Ui/StatusUpdatedEventArgs.cs +++ b/src/Ryujinx/Ui/StatusUpdatedEventArgs.cs @@ -4,7 +4,7 @@ namespace Ryujinx.Ui { public class StatusUpdatedEventArgs : EventArgs { - public bool VSyncEnabled; + public string PresentIntervalState; public float Volume; public string DockedMode; public string AspectRatio; @@ -13,9 +13,9 @@ public class StatusUpdatedEventArgs : EventArgs public string GpuName; public string GpuBackend; - public StatusUpdatedEventArgs(bool vSyncEnabled, float volume, string gpuBackend, string dockedMode, string aspectRatio, string gameStatus, string fifoStatus, string gpuName) + public StatusUpdatedEventArgs(string presentIntervalState, float volume, string gpuBackend, string dockedMode, string aspectRatio, string gameStatus, string fifoStatus, string gpuName) { - VSyncEnabled = vSyncEnabled; + PresentIntervalState = presentIntervalState; Volume = volume; GpuBackend = gpuBackend; DockedMode = dockedMode; From 4089ba99ec23389e6283c5149fb969fc1a1a6621 Mon Sep 17 00:00:00 2001 From: jcm Date: Sat, 7 Oct 2023 21:14:37 -0500 Subject: [PATCH 27/43] revise naming, UX to conform closer to legacy --- src/Ryujinx.Ava/Assets/Locales/en_US.json | 20 +++++++++---------- .../UI/ViewModels/MainWindowViewModel.cs | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Ryujinx.Ava/Assets/Locales/en_US.json b/src/Ryujinx.Ava/Assets/Locales/en_US.json index 1b2705b7a..b92f3ff6d 100644 --- a/src/Ryujinx.Ava/Assets/Locales/en_US.json +++ b/src/Ryujinx.Ava/Assets/Locales/en_US.json @@ -126,16 +126,16 @@ "SettingsTabSystemSystemLanguageTraditionalChinese": "Traditional Chinese", "SettingsTabSystemSystemTimeZone": "System Time Zone:", "SettingsTabSystemSystemTime": "System Time:", - "SettingsTabSystemPresentIntervalState": "Present Interval Mode (VSync):", - "SettingsTabSystemEnableCustomPresentInterval": "Enable toggle for custom present interval", - "SettingsTabSystemPresentIntervalStateSwitch": "60 (VSync ON)", - "SettingsTabSystemPresentIntervalStateUnbounded": "Unbounded (VSync OFF)", - "SettingsTabSystemPresentIntervalStateCustom": "Custom", - "SettingsTabSystemPresentIntervalStateTooltip": "Previously VSync. Use the Custom mode to set a specific refresh interval target. In some titles, the custom mode will act as an FPS cap. In others, it may lead to unpredictable behavior or do nothing at all. \n\nLeave at 60 (VSync ON) if unsure.", - "SettingsTabSystemEnableCustomPresentIntervalTooltip": "The present interval mode toggle will also cycle through the custom interval mode.", - "SettingsTabSystemCustomPresentIntervalValueTooltip": "The custom present interval target value.", - "SettingsTabSystemCustomPresentIntervalSliderTooltip": "The custom present interval target, as a percentage of the normal Switch interval.", - "SettingsTabSystemCustomPresentIntervalValue": "Custom Present Interval Value:", + "SettingsTabSystemPresentIntervalState": "VSync:", + "SettingsTabSystemEnableCustomPresentInterval": "Enable toggle through custom refresh rate", + "SettingsTabSystemPresentIntervalStateSwitch": "On", + "SettingsTabSystemPresentIntervalStateUnbounded": "Off", + "SettingsTabSystemPresentIntervalStateCustom": "Custom Refresh Rate", + "SettingsTabSystemPresentIntervalStateTooltip": "Emulated Vertical Sync. 'On' emulates the Switch's refresh rate of 60Hz. 'Off' is an unbounded refresh rate. 'Custom' allows the user to specify a custom emulated refresh rate. In some titles, the custom rate will act as an FPS cap. In others, it may lead to unpredictable behavior. \n\nLeave ON if unsure.", + "SettingsTabSystemEnableCustomPresentIntervalTooltip": "The VSync toggle will also cycle through the custom refresh rate mode.", + "SettingsTabSystemCustomPresentIntervalValueTooltip": "The custom refresh rate target value.", + "SettingsTabSystemCustomPresentIntervalSliderTooltip": "The custom refresh rate, as a percentage of the normal Switch refresh rate.", + "SettingsTabSystemCustomPresentIntervalValue": "Custom Refresh Rate Value:", "SettingsTabSystemEnablePptc": "PPTC (Profiled Persistent Translation Cache)", "SettingsTabSystemEnableFsIntegrityChecks": "FS Integrity Checks", "SettingsTabSystemAudioBackend": "Audio Backend:", diff --git a/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs index 9bf1561d8..e34eb72b8 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs @@ -1295,7 +1295,7 @@ private void Update_StatusBar(object sender, StatusUpdatedEventArgs args) PresentIntervalStateColor = new SolidColorBrush((Color)color); } - PresentIntervalStateText = args.PresentIntervalState; + PresentIntervalStateText = args.PresentIntervalState == "Custom" ? "Custom" : "VSync"; ShowCustomPresentIntervalPicker = args.PresentIntervalState == PresentIntervalState.Custom.ToString(); DockedStatusText = args.DockedMode; From e6916ea0febe78949019a48dfbe81b29114ff744 Mon Sep 17 00:00:00 2001 From: jcm Date: Sun, 8 Oct 2023 11:54:14 -0500 Subject: [PATCH 28/43] move setting to 'Hacks', tweak UI and tooltips accordingly --- src/Ryujinx.Ava/Assets/Locales/en_US.json | 7 ++--- .../UI/ViewModels/SettingsViewModel.cs | 15 ++++++----- .../Views/Settings/SettingsSystemView.axaml | 27 ++++++++++++++----- 3 files changed, 34 insertions(+), 15 deletions(-) diff --git a/src/Ryujinx.Ava/Assets/Locales/en_US.json b/src/Ryujinx.Ava/Assets/Locales/en_US.json index b92f3ff6d..849604577 100644 --- a/src/Ryujinx.Ava/Assets/Locales/en_US.json +++ b/src/Ryujinx.Ava/Assets/Locales/en_US.json @@ -127,12 +127,13 @@ "SettingsTabSystemSystemTimeZone": "System Time Zone:", "SettingsTabSystemSystemTime": "System Time:", "SettingsTabSystemPresentIntervalState": "VSync:", - "SettingsTabSystemEnableCustomPresentInterval": "Enable toggle through custom refresh rate", + "SettingsTabSystemEnableCustomPresentInterval": "Enable custom refresh rate (Experimental)", "SettingsTabSystemPresentIntervalStateSwitch": "On", "SettingsTabSystemPresentIntervalStateUnbounded": "Off", "SettingsTabSystemPresentIntervalStateCustom": "Custom Refresh Rate", - "SettingsTabSystemPresentIntervalStateTooltip": "Emulated Vertical Sync. 'On' emulates the Switch's refresh rate of 60Hz. 'Off' is an unbounded refresh rate. 'Custom' allows the user to specify a custom emulated refresh rate. In some titles, the custom rate will act as an FPS cap. In others, it may lead to unpredictable behavior. \n\nLeave ON if unsure.", - "SettingsTabSystemEnableCustomPresentIntervalTooltip": "The VSync toggle will also cycle through the custom refresh rate mode.", + "SettingsTabSystemPresentIntervalStateTooltip": "Emulated Vertical Sync. 'On' emulates the Switch's refresh rate of 60Hz. 'Off' is an unbounded refresh rate.", + "SettingsTabSystemPresentIntervalStateTooltipCustom": "Emulated Vertical Sync. 'On' emulates the Switch's refresh rate of 60Hz. 'Off' is an unbounded refresh rate. 'Custom' emulates the specified custom refresh rate.", + "SettingsTabSystemEnableCustomPresentIntervalTooltip": "Allows the user to specify an emulated refresh rate. In some titles, this may speed up or slow down the rate of gameplay logic. In other titles, it may allow for capping FPS at some multiple of the refresh rate, or lead to unpredictable behavior. This is an experimental feature, with no guarantees for how gameplay will be affected. \n\nLeave OFF if unsure.", "SettingsTabSystemCustomPresentIntervalValueTooltip": "The custom refresh rate target value.", "SettingsTabSystemCustomPresentIntervalSliderTooltip": "The custom refresh rate, as a percentage of the normal Switch refresh rate.", "SettingsTabSystemCustomPresentIntervalValue": "Custom Refresh Rate Value:", diff --git a/src/Ryujinx.Ava/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/SettingsViewModel.cs index 564677bec..5ff1871bd 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/SettingsViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/SettingsViewModel.cs @@ -147,13 +147,13 @@ public PresentIntervalState PresentIntervalState get => _presentIntervalState; set { - if (value == PresentIntervalState.Custom) + if (value == PresentIntervalState.Custom || + value == PresentIntervalState.Switch || + value == PresentIntervalState.Unbounded) { - EnableCustomPresentInterval = true; + _presentIntervalState = value; + OnPropertyChanged(); } - _presentIntervalState = value; - OnPropertyChanged(); - OnPropertyChanged((nameof(EnableCustomPresentInterval))); } } @@ -192,7 +192,10 @@ public bool EnableCustomPresentInterval if (_presentIntervalState == PresentIntervalState.Custom && value == false) { PresentIntervalState = PresentIntervalState.Switch; - OnPropertyChanged(nameof(PresentIntervalState)); + } + else if (value) + { + PresentIntervalState = PresentIntervalState.Custom; } OnPropertyChanged(); } diff --git a/src/Ryujinx.Ava/UI/Views/Settings/SettingsSystemView.axaml b/src/Ryujinx.Ava/UI/Views/Settings/SettingsSystemView.axaml index 3ded1e384..b143c1072 100644 --- a/src/Ryujinx.Ava/UI/Views/Settings/SettingsSystemView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Settings/SettingsSystemView.axaml @@ -190,8 +190,9 @@ ToolTip.Tip="{locale:Locale SettingsTabSystemPresentIntervalStateTooltip}" Width="250" /> @@ -204,6 +205,19 @@ + + + + + + + + - - - + + + From 30552cc2caf043fb33227cba4832adbfb15c38ac Mon Sep 17 00:00:00 2001 From: jcm Date: Sat, 13 Jan 2024 11:32:59 -0600 Subject: [PATCH 29/43] Rename to VSyncMode internally, code cleanup --- src/Ryujinx.Ava/AppHost.cs | 130 +++++++++--------- src/Ryujinx.Ava/Assets/Locales/en_US.json | 30 ++-- src/Ryujinx.Ava/Common/KeyboardHotkeyState.cs | 6 +- .../UI/Models/StatusUpdatedEventArgs.cs | 6 +- .../UI/ViewModels/MainWindowViewModel.cs | 130 +++++++----------- .../UI/ViewModels/SettingsViewModel.cs | 74 +++++----- .../UI/Views/Main/MainStatusBarView.axaml | 22 +-- .../UI/Views/Main/MainStatusBarView.axaml.cs | 6 +- .../Views/Settings/SettingsHotkeysView.axaml | 12 +- .../Views/Settings/SettingsSystemView.axaml | 50 +++---- .../Configuration/Hid/KeyboardHotkeys.cs | 6 +- .../{PresentIntervalState.cs => VSyncMode.cs} | 4 +- src/Ryujinx.Graphics.GAL/IWindow.cs | 2 +- .../Multithreading/ThreadedWindow.cs | 2 +- .../{PresentIntervalState.cs => VSyncMode.cs} | 2 +- src/Ryujinx.Graphics.OpenGL/Window.cs | 2 +- src/Ryujinx.Graphics.Vulkan/Window.cs | 12 +- src/Ryujinx.Graphics.Vulkan/WindowBase.cs | 2 +- src/Ryujinx.HLE/HLEConfiguration.cs | 18 ++- .../Services/SurfaceFlinger/SurfaceFlinger.cs | 14 +- src/Ryujinx.HLE/Switch.cs | 52 +++---- src/Ryujinx.Headless.SDL2/Options.cs | 10 +- src/Ryujinx.Headless.SDL2/Program.cs | 4 +- .../StatusUpdatedEventArgs.cs | 6 +- src/Ryujinx.Headless.SDL2/WindowBase.cs | 2 +- .../Configuration/ConfigurationFileFormat.cs | 8 +- .../Configuration/ConfigurationState.cs | 66 ++++----- src/Ryujinx/Ui/MainWindow.cs | 8 +- src/Ryujinx/Ui/RendererWidgetBase.cs | 8 +- src/Ryujinx/Ui/StatusUpdatedEventArgs.cs | 6 +- 30 files changed, 335 insertions(+), 365 deletions(-) rename src/Ryujinx.Common/Configuration/{PresentIntervalState.cs => VSyncMode.cs} (66%) rename src/Ryujinx.Graphics.GAL/{PresentIntervalState.cs => VSyncMode.cs} (76%) diff --git a/src/Ryujinx.Ava/AppHost.cs b/src/Ryujinx.Ava/AppHost.cs index dd77a08af..47fa1c32e 100644 --- a/src/Ryujinx.Ava/AppHost.cs +++ b/src/Ryujinx.Ava/AppHost.cs @@ -58,10 +58,10 @@ using IRenderer = Ryujinx.Graphics.GAL.IRenderer; using Key = Ryujinx.Input.Key; using MouseButton = Ryujinx.Input.MouseButton; -using PresentIntervalState = Ryujinx.Common.Configuration.PresentIntervalState; using ScalingFilter = Ryujinx.Common.Configuration.ScalingFilter; using Size = Avalonia.Size; using Switch = Ryujinx.HLE.Switch; +using VSyncMode = Ryujinx.Common.Configuration.VSyncMode; namespace Ryujinx.Ava { @@ -190,9 +190,9 @@ public AppHost( ConfigurationState.Instance.Graphics.ScalingFilter.Event += UpdateScalingFilter; ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event += UpdateScalingFilterLevel; ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Event += UpdateColorSpacePassthrough; - ConfigurationState.Instance.Graphics.PresentIntervalState.Event += UpdatePresentIntervalState; - ConfigurationState.Instance.Graphics.CustomPresentInterval.Event += UpdateCustomPresentIntervalValue; - ConfigurationState.Instance.Graphics.EnableCustomPresentInterval.Event += UpdateCustomPresentIntervalEnabled; + ConfigurationState.Instance.Graphics.VSyncMode.Event += UpdateVSyncMode; + ConfigurationState.Instance.Graphics.CustomVSyncInterval.Event += UpdateCustomVSyncIntervalValue; + ConfigurationState.Instance.Graphics.EnableCustomVSyncInterval.Event += UpdateCustomVSyncIntervalEnabled; ConfigurationState.Instance.System.EnableInternetAccess.Event += UpdateEnableInternetAccessState; ConfigurationState.Instance.Multiplayer.LanInterfaceId.Event += UpdateLanInterfaceIdState; @@ -240,34 +240,66 @@ private void UpdateColorSpacePassthrough(object sender, ReactiveEventArgs _renderer.Window?.SetColorSpacePassthrough((bool)ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Value); } - private void UpdatePresentIntervalState(object sender, ReactiveEventArgs e) + public void UpdateVSyncMode(object sender, ReactiveEventArgs e) { if (Device != null) { - Device.PresentIntervalState = e.NewValue; - Device.UpdatePresentInterval(); + Device.VSyncMode = e.NewValue; + Device.UpdateVSyncInterval(); } //vulkan present mode may change in response, so recreate the swapchain - _renderer.Window?.ChangePresentIntervalState((Ryujinx.Graphics.GAL.PresentIntervalState)e.NewValue); - ConfigurationState.Instance.Graphics.PresentIntervalState.Value = e.NewValue; + _renderer.Window?.ChangeVSyncMode((Ryujinx.Graphics.GAL.VSyncMode)e.NewValue); + + _viewModel.ShowCustomVSyncIntervalPicker = (e.NewValue == VSyncMode.Custom); + + ConfigurationState.Instance.Graphics.VSyncMode.Value = e.NewValue; ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); } - private void UpdateCustomPresentIntervalValue(object sender, ReactiveEventArgs e) + public void VSyncModeToggle() + { + VSyncMode oldVSyncMode = Device.VSyncMode; + VSyncMode newVSyncMode = VSyncMode.Switch; + bool customVSyncIntervalEnabled = ConfigurationState.Instance.Graphics.EnableCustomVSyncInterval.Value; + + switch (oldVSyncMode) + { + case VSyncMode.Switch: + newVSyncMode = VSyncMode.Unbounded; + break; + case VSyncMode.Unbounded: + if (customVSyncIntervalEnabled) + { + newVSyncMode = VSyncMode.Custom; + } + else + { + newVSyncMode = VSyncMode.Switch; + } + + break; + case VSyncMode.Custom: + newVSyncMode = VSyncMode.Switch; + break; + } + UpdateVSyncMode(this, new ReactiveEventArgs(oldVSyncMode, newVSyncMode)); + } + + private void UpdateCustomVSyncIntervalValue(object sender, ReactiveEventArgs e) { if (Device != null) { - Device.TargetPresentInterval = e.NewValue; - Device.UpdatePresentInterval(); + Device.TargetVSyncInterval = e.NewValue; + Device.UpdateVSyncInterval(); } } - private void UpdateCustomPresentIntervalEnabled(object sender, ReactiveEventArgs e) + private void UpdateCustomVSyncIntervalEnabled(object sender, ReactiveEventArgs e) { if (Device != null) { - Device.CustomPresentIntervalEnabled = e.NewValue; - Device.UpdatePresentInterval(); + Device.CustomVSyncIntervalEnabled = e.NewValue; + Device.UpdateVSyncInterval(); } } @@ -550,12 +582,6 @@ private void HideCursorState_Changed(object sender, ReactiveEventArgs(oldState, presentIntervalState)); - } - public async Task LoadGuestApplication() { InitializeSwitchInstance(); @@ -819,7 +845,7 @@ private void InitializeSwitchInstance() _viewModel.UiHandler, (SystemLanguage)ConfigurationState.Instance.System.Language.Value, (RegionCode)ConfigurationState.Instance.System.Region.Value, - ConfigurationState.Instance.Graphics.PresentIntervalState, + ConfigurationState.Instance.Graphics.VSyncMode, ConfigurationState.Instance.System.EnableDockedMode, ConfigurationState.Instance.System.EnablePtc, ConfigurationState.Instance.System.EnableInternetAccess, @@ -834,7 +860,7 @@ private void InitializeSwitchInstance() ConfigurationState.Instance.System.UseHypervisor, ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value, ConfigurationState.Instance.Multiplayer.Mode, - ConfigurationState.Instance.Graphics.CustomPresentInterval.Value); + ConfigurationState.Instance.Graphics.CustomVSyncInterval.Value); Device = new Switch(configuration); } @@ -960,7 +986,7 @@ private void RenderLoop() Device.Gpu.InitializeShaderCache(_gpuCancellationTokenSource.Token); Translator.IsReadyForTranslation.Set(); - _renderer.Window.ChangePresentIntervalState((Ryujinx.Graphics.GAL.PresentIntervalState)Device.PresentIntervalState); + _renderer.Window.ChangeVSyncMode((Ryujinx.Graphics.GAL.VSyncMode)Device.VSyncMode); while (_isActive) { @@ -1008,7 +1034,7 @@ public void UpdateStatus() { // Run a status update only when a frame is to be drawn. This prevents from updating the ui and wasting a render when no frame is queued. string dockedMode = ConfigurationState.Instance.System.EnableDockedMode ? LocaleManager.Instance[LocaleKeys.Docked] : LocaleManager.Instance[LocaleKeys.Handheld]; - string presentIntervalState = Device.PresentIntervalState.ToString(); + string vSyncMode = Device.VSyncMode.ToString(); if (GraphicsConfig.ResScale != 1) { @@ -1016,7 +1042,7 @@ public void UpdateStatus() } StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs( - presentIntervalState, + vSyncMode, LocaleManager.Instance[LocaleKeys.VolumeShort] + $": {(int)(Device.GetVolume() * 100)}%", ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.Vulkan ? "Vulkan" : "OpenGL", dockedMode, @@ -1108,40 +1134,16 @@ private bool UpdateFrame() { switch (currentHotkeyState) { - //todo default - case KeyboardHotkeyState.TogglePresentIntervalState: - PresentIntervalState oldState = Device.PresentIntervalState; - PresentIntervalState newState; - if (oldState == PresentIntervalState.Switch) - { - newState = PresentIntervalState.Unbounded; - } - else if (oldState == PresentIntervalState.Unbounded) - { - if (ConfigurationState.Instance.Graphics.EnableCustomPresentInterval) - { - newState = PresentIntervalState.Custom; - } - else - { - newState = PresentIntervalState.Switch; - } - } - else - { - newState = PresentIntervalState.Switch; - } - UpdatePresentIntervalState(this, new ReactiveEventArgs(oldState, newState)); - _viewModel.ShowCustomPresentIntervalPicker = - (newState == PresentIntervalState.Custom); + case KeyboardHotkeyState.ToggleVSyncMode: + VSyncModeToggle(); break; - case KeyboardHotkeyState.CustomPresentIntervalDecrement: - Device.DecrementCustomPresentInterval(); - _viewModel.CustomPresentInterval -= 1; + case KeyboardHotkeyState.CustomVSyncIntervalDecrement: + Device.DecrementCustomVSyncInterval(); + _viewModel.CustomVSyncInterval -= 1; break; - case KeyboardHotkeyState.CustomPresentIntervalIncrement: - Device.IncrementCustomPresentInterval(); - _viewModel.CustomPresentInterval += 1; + case KeyboardHotkeyState.CustomVSyncIntervalIncrement: + Device.IncrementCustomVSyncInterval(); + _viewModel.CustomVSyncInterval += 1; break; case KeyboardHotkeyState.Screenshot: ScreenshotRequested = true; @@ -1228,9 +1230,9 @@ private KeyboardHotkeyState GetHotkeyState() { KeyboardHotkeyState state = KeyboardHotkeyState.None; - if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.PresentIntervalState)) + if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.VSyncMode)) { - state = KeyboardHotkeyState.TogglePresentIntervalState; + state = KeyboardHotkeyState.ToggleVSyncMode; } else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.Screenshot)) { @@ -1264,13 +1266,13 @@ private KeyboardHotkeyState GetHotkeyState() { state = KeyboardHotkeyState.VolumeDown; } - else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CustomPresentIntervalIncrement)) + else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CustomVSyncIntervalIncrement)) { - state = KeyboardHotkeyState.CustomPresentIntervalIncrement; + state = KeyboardHotkeyState.CustomVSyncIntervalIncrement; } - else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CustomPresentIntervalDecrement)) + else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CustomVSyncIntervalDecrement)) { - state = KeyboardHotkeyState.CustomPresentIntervalDecrement; + state = KeyboardHotkeyState.CustomVSyncIntervalDecrement; } return state; diff --git a/src/Ryujinx.Ava/Assets/Locales/en_US.json b/src/Ryujinx.Ava/Assets/Locales/en_US.json index 849604577..1d80f5161 100644 --- a/src/Ryujinx.Ava/Assets/Locales/en_US.json +++ b/src/Ryujinx.Ava/Assets/Locales/en_US.json @@ -126,17 +126,17 @@ "SettingsTabSystemSystemLanguageTraditionalChinese": "Traditional Chinese", "SettingsTabSystemSystemTimeZone": "System Time Zone:", "SettingsTabSystemSystemTime": "System Time:", - "SettingsTabSystemPresentIntervalState": "VSync:", - "SettingsTabSystemEnableCustomPresentInterval": "Enable custom refresh rate (Experimental)", - "SettingsTabSystemPresentIntervalStateSwitch": "On", - "SettingsTabSystemPresentIntervalStateUnbounded": "Off", - "SettingsTabSystemPresentIntervalStateCustom": "Custom Refresh Rate", - "SettingsTabSystemPresentIntervalStateTooltip": "Emulated Vertical Sync. 'On' emulates the Switch's refresh rate of 60Hz. 'Off' is an unbounded refresh rate.", - "SettingsTabSystemPresentIntervalStateTooltipCustom": "Emulated Vertical Sync. 'On' emulates the Switch's refresh rate of 60Hz. 'Off' is an unbounded refresh rate. 'Custom' emulates the specified custom refresh rate.", - "SettingsTabSystemEnableCustomPresentIntervalTooltip": "Allows the user to specify an emulated refresh rate. In some titles, this may speed up or slow down the rate of gameplay logic. In other titles, it may allow for capping FPS at some multiple of the refresh rate, or lead to unpredictable behavior. This is an experimental feature, with no guarantees for how gameplay will be affected. \n\nLeave OFF if unsure.", - "SettingsTabSystemCustomPresentIntervalValueTooltip": "The custom refresh rate target value.", - "SettingsTabSystemCustomPresentIntervalSliderTooltip": "The custom refresh rate, as a percentage of the normal Switch refresh rate.", - "SettingsTabSystemCustomPresentIntervalValue": "Custom Refresh Rate Value:", + "SettingsTabSystemVSyncMode": "VSync:", + "SettingsTabSystemEnableCustomVSyncInterval": "Enable custom refresh rate (Experimental)", + "SettingsTabSystemVSyncModeSwitch": "On", + "SettingsTabSystemVSyncModeUnbounded": "Off", + "SettingsTabSystemVSyncModeCustom": "Custom Refresh Rate", + "SettingsTabSystemVSyncModeTooltip": "Emulated Vertical Sync. 'On' emulates the Switch's refresh rate of 60Hz. 'Off' is an unbounded refresh rate.", + "SettingsTabSystemVSyncModeTooltipCustom": "Emulated Vertical Sync. 'On' emulates the Switch's refresh rate of 60Hz. 'Off' is an unbounded refresh rate. 'Custom' emulates the specified custom refresh rate.", + "SettingsTabSystemEnableCustomVSyncIntervalTooltip": "Allows the user to specify an emulated refresh rate. In some titles, this may speed up or slow down the rate of gameplay logic. In other titles, it may allow for capping FPS at some multiple of the refresh rate, or lead to unpredictable behavior. This is an experimental feature, with no guarantees for how gameplay will be affected. \n\nLeave OFF if unsure.", + "SettingsTabSystemCustomVSyncIntervalValueTooltip": "The custom refresh rate target value.", + "SettingsTabSystemCustomVSyncIntervalSliderTooltip": "The custom refresh rate, as a percentage of the normal Switch refresh rate.", + "SettingsTabSystemCustomVSyncIntervalValue": "Custom Refresh Rate Value:", "SettingsTabSystemEnablePptc": "PPTC (Profiled Persistent Translation Cache)", "SettingsTabSystemEnableFsIntegrityChecks": "FS Integrity Checks", "SettingsTabSystemAudioBackend": "Audio Backend:", @@ -144,7 +144,7 @@ "SettingsTabSystemAudioBackendOpenAL": "OpenAL", "SettingsTabSystemAudioBackendSoundIO": "SoundIO", "SettingsTabSystemAudioBackendSDL2": "SDL2", - "SettingsTabSystemCustomPresentInterval": "Interval", + "SettingsTabSystemCustomVSyncInterval": "Interval", "SettingsTabSystemHacks": "Hacks", "SettingsTabSystemHacksNote": "May cause instability", "SettingsTabSystemExpandDramSize": "Use alternative memory layout (Developers)", @@ -585,13 +585,13 @@ "RyujinxUpdater": "Ryujinx Updater", "SettingsTabHotkeys": "Keyboard Hotkeys", "SettingsTabHotkeysHotkeys": "Keyboard Hotkeys", - "SettingsTabHotkeysTogglePresentIntervalStateHotkey": "Toggle Present Interval state:", + "SettingsTabHotkeysToggleVSyncModeHotkey": "Toggle VSync mode:", "SettingsTabHotkeysScreenshotHotkey": "Screenshot:", "SettingsTabHotkeysShowUiHotkey": "Show UI:", "SettingsTabHotkeysPauseHotkey": "Pause:", "SettingsTabHotkeysToggleMuteHotkey": "Mute:", - "SettingsTabHotkeysIncrementCustomPresentIntervalHotkey": "Raise custom refresh interval", - "SettingsTabHotkeysDecrementCustomPresentIntervalHotkey": "Lower custom refresh interval", + "SettingsTabHotkeysIncrementCustomVSyncIntervalHotkey": "Raise custom refresh rate", + "SettingsTabHotkeysDecrementCustomVSyncIntervalHotkey": "Lower custom refresh rate", "ControllerMotionTitle": "Motion Control Settings", "ControllerRumbleTitle": "Rumble Settings", "SettingsSelectThemeFileDialogTitle": "Select Theme File", diff --git a/src/Ryujinx.Ava/Common/KeyboardHotkeyState.cs b/src/Ryujinx.Ava/Common/KeyboardHotkeyState.cs index 6429dda05..5c5507ffc 100644 --- a/src/Ryujinx.Ava/Common/KeyboardHotkeyState.cs +++ b/src/Ryujinx.Ava/Common/KeyboardHotkeyState.cs @@ -3,7 +3,7 @@ namespace Ryujinx.Ava.Common public enum KeyboardHotkeyState { None, - TogglePresentIntervalState, + ToggleVSyncMode, Screenshot, ShowUi, Pause, @@ -12,7 +12,7 @@ public enum KeyboardHotkeyState ResScaleDown, VolumeUp, VolumeDown, - CustomPresentIntervalIncrement, - CustomPresentIntervalDecrement, + CustomVSyncIntervalIncrement, + CustomVSyncIntervalDecrement, } } diff --git a/src/Ryujinx.Ava/UI/Models/StatusUpdatedEventArgs.cs b/src/Ryujinx.Ava/UI/Models/StatusUpdatedEventArgs.cs index ce975f8c5..e14bff25e 100644 --- a/src/Ryujinx.Ava/UI/Models/StatusUpdatedEventArgs.cs +++ b/src/Ryujinx.Ava/UI/Models/StatusUpdatedEventArgs.cs @@ -4,7 +4,7 @@ namespace Ryujinx.Ava.UI.Models { internal class StatusUpdatedEventArgs : EventArgs { - public string PresentIntervalState { get; } + public string VSyncMode { get; } public string VolumeStatus { get; } public string GpuBackend { get; } public string AspectRatio { get; } @@ -13,9 +13,9 @@ internal class StatusUpdatedEventArgs : EventArgs public string GameStatus { get; } public string GpuName { get; } - public StatusUpdatedEventArgs(string presentIntervalState, string volumeStatus, string gpuBackend, string dockedMode, string aspectRatio, string gameStatus, string fifoStatus, string gpuName) + public StatusUpdatedEventArgs(string vSyncMode, string volumeStatus, string gpuBackend, string dockedMode, string aspectRatio, string gameStatus, string fifoStatus, string gpuName) { - PresentIntervalState = presentIntervalState; + VSyncMode = vSyncMode; VolumeStatus = volumeStatus; GpuBackend = gpuBackend; DockedMode = dockedMode; diff --git a/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs index e34eb72b8..ec0bb3fc5 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs @@ -59,7 +59,7 @@ public class MainWindowViewModel : BaseModel private string _searchText; private Timer _searchTimer; private string _dockedStatusText; - private string _presentIntervalStateText; + private string _vSyncModeText; private string _fifoStatusText; private string _gameStatusText; private string _volumeStatusText; @@ -75,7 +75,7 @@ public class MainWindowViewModel : BaseModel private bool _showStatusSeparator; private Brush _progressBarForegroundColor; private Brush _progressBarBackgroundColor; - private Brush _presentIntervalStateColor; + private Brush _vSyncModeColor; private byte[] _selectedIcon; private bool _isAppletMenuActive; private int _statusBarProgressMaximum; @@ -103,8 +103,8 @@ public class MainWindowViewModel : BaseModel private WindowState _windowState; private double _windowWidth; private double _windowHeight; - private int _customPresentInterval; - private int _customPresentIntervalPercentageProxy; + private int _customVSyncInterval; + private int _customVSyncIntervalPercentageProxy; @@ -133,7 +133,7 @@ public MainWindowViewModel() Volume = ConfigurationState.Instance.System.AudioVolume; } - CustomPresentInterval = ConfigurationState.Instance.Graphics.CustomPresentInterval.Value; + CustomVSyncInterval = ConfigurationState.Instance.Graphics.CustomVSyncInterval.Value; } public void Initialize( @@ -410,25 +410,25 @@ public Brush ProgressBarForegroundColor } } - public Brush PresentIntervalStateColor + public Brush VSyncModeColor { - get => _presentIntervalStateColor; + get => _vSyncModeColor; set { - _presentIntervalStateColor = value; + _vSyncModeColor = value; OnPropertyChanged(); } } - public bool ShowCustomPresentIntervalPicker + public bool ShowCustomVSyncIntervalPicker { get { if (_isGameRunning) { - return AppHost.Device.PresentIntervalState == - PresentIntervalState.Custom; + return AppHost.Device.VSyncMode == + VSyncMode.Custom; } else { @@ -441,30 +441,30 @@ public bool ShowCustomPresentIntervalPicker } } - public int CustomPresentIntervalPercentageProxy + public int CustomVSyncIntervalPercentageProxy { - get => _customPresentIntervalPercentageProxy; + get => _customVSyncIntervalPercentageProxy; set { int newInterval = (int)(((decimal)value / 100) * 60); - _customPresentInterval = newInterval; - _customPresentIntervalPercentageProxy = value; - ConfigurationState.Instance.Graphics.CustomPresentInterval.Value = newInterval; + _customVSyncInterval = newInterval; + _customVSyncIntervalPercentageProxy = value; + ConfigurationState.Instance.Graphics.CustomVSyncInterval.Value = newInterval; if (_isGameRunning) { - AppHost.Device.CustomPresentInterval = newInterval; - AppHost.Device.UpdatePresentInterval(); + AppHost.Device.CustomVSyncInterval = newInterval; + AppHost.Device.UpdateVSyncInterval(); } - OnPropertyChanged((nameof(CustomPresentInterval))); - OnPropertyChanged((nameof(CustomPresentIntervalPercentageText))); + OnPropertyChanged((nameof(CustomVSyncInterval))); + OnPropertyChanged((nameof(CustomVSyncIntervalPercentageText))); } } - public string CustomPresentIntervalPercentageText + public string CustomVSyncIntervalPercentageText { get { - string text = CustomPresentIntervalPercentageProxy.ToString() + "%"; + string text = CustomVSyncIntervalPercentageProxy.ToString() + "%"; return text; } set @@ -473,22 +473,22 @@ public string CustomPresentIntervalPercentageText } } - public int CustomPresentInterval + public int CustomVSyncInterval { - get => _customPresentInterval; + get => _customVSyncInterval; set { - _customPresentInterval = value; + _customVSyncInterval = value; int newPercent = (int)(((decimal)value / 60) * 100); - _customPresentIntervalPercentageProxy = newPercent; - ConfigurationState.Instance.Graphics.CustomPresentInterval.Value = value; + _customVSyncIntervalPercentageProxy = newPercent; + ConfigurationState.Instance.Graphics.CustomVSyncInterval.Value = value; if (_isGameRunning) { - AppHost.Device.CustomPresentInterval = value; - AppHost.Device.UpdatePresentInterval(); + AppHost.Device.CustomVSyncInterval = value; + AppHost.Device.UpdateVSyncInterval(); } - OnPropertyChanged(nameof(CustomPresentIntervalPercentageProxy)); - OnPropertyChanged(nameof(CustomPresentIntervalPercentageText)); + OnPropertyChanged(nameof(CustomVSyncIntervalPercentageProxy)); + OnPropertyChanged(nameof(CustomVSyncIntervalPercentageText)); OnPropertyChanged(); } } @@ -581,12 +581,12 @@ public string BackendText } } - public string PresentIntervalStateText + public string VSyncModeText { - get => _presentIntervalStateText; + get => _vSyncModeText; set { - _presentIntervalStateText = value; + _vSyncModeText = value; OnPropertyChanged(); } @@ -1286,18 +1286,18 @@ private void Update_StatusBar(object sender, StatusUpdatedEventArgs args) { Dispatcher.UIThread.InvokeAsync(() => { - Application.Current.Styles.TryGetResource(args.PresentIntervalState, + Application.Current.Styles.TryGetResource(args.VSyncMode, Avalonia.Application.Current.ActualThemeVariant, out object color); if (color is not null) { - PresentIntervalStateColor = new SolidColorBrush((Color)color); + VSyncModeColor = new SolidColorBrush((Color)color); } - PresentIntervalStateText = args.PresentIntervalState == "Custom" ? "Custom" : "VSync"; - ShowCustomPresentIntervalPicker = - args.PresentIntervalState == PresentIntervalState.Custom.ToString(); + VSyncModeText = args.VSyncMode == "Custom" ? "Custom" : "VSync"; + ShowCustomVSyncIntervalPicker = + args.VSyncMode == VSyncMode.Custom.ToString(); DockedStatusText = args.DockedMode; AspectRatioStatusText = args.AspectRatio; GameStatusText = args.GameStatus; @@ -1456,55 +1456,25 @@ public void ToggleDockMode() } } - public void UpdatePresentIntervalState() + public void UpdateVSyncMode() { - PresentIntervalState oldPresentInterval = AppHost.Device.PresentIntervalState; - PresentIntervalState newPresentInterval = PresentIntervalState.Switch; - bool customPresentIntervalEnabled = ConfigurationState.Instance.Graphics.EnableCustomPresentInterval.Value; - - switch (oldPresentInterval) - { - case PresentIntervalState.Switch: - newPresentInterval = PresentIntervalState.Unbounded; - break; - case PresentIntervalState.Unbounded: - if (customPresentIntervalEnabled) - { - newPresentInterval = PresentIntervalState.Custom; - } - else - { - newPresentInterval = PresentIntervalState.Switch; - } - break; - case PresentIntervalState.Custom: - newPresentInterval = PresentIntervalState.Switch; - break; - } - - ConfigurationState.Instance.Graphics.PresentIntervalState.Value = newPresentInterval; - - if (_isGameRunning) - { - AppHost.UpdatePresentInterval(newPresentInterval); - } - - OnPropertyChanged(nameof(ShowCustomPresentIntervalPicker)); + AppHost.VSyncModeToggle(); + OnPropertyChanged(nameof(ShowCustomVSyncIntervalPicker)); } - public void PresentIntervalStateSettingChanged() + public void VSyncModeSettingChanged() { if (_isGameRunning) { - AppHost.Device.CustomPresentInterval = ConfigurationState.Instance.Graphics.CustomPresentInterval.Value; - AppHost.Device.UpdatePresentInterval(); + AppHost.Device.CustomVSyncInterval = ConfigurationState.Instance.Graphics.CustomVSyncInterval.Value; + AppHost.Device.UpdateVSyncInterval(); } - CustomPresentInterval = ConfigurationState.Instance.Graphics.CustomPresentInterval.Value; - OnPropertyChanged(nameof(ShowCustomPresentIntervalPicker)); - OnPropertyChanged(nameof(CustomPresentIntervalPercentageProxy)); - OnPropertyChanged(nameof(CustomPresentIntervalPercentageText)); - OnPropertyChanged(nameof(CustomPresentInterval)); + CustomVSyncInterval = ConfigurationState.Instance.Graphics.CustomVSyncInterval.Value; + OnPropertyChanged(nameof(ShowCustomVSyncIntervalPicker)); + OnPropertyChanged(nameof(CustomVSyncIntervalPercentageProxy)); + OnPropertyChanged(nameof(CustomVSyncIntervalPercentageText)); + OnPropertyChanged(nameof(CustomVSyncInterval)); } public async Task ExitCurrentState() diff --git a/src/Ryujinx.Ava/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/SettingsViewModel.cs index 5ff1871bd..4333f4974 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/SettingsViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/SettingsViewModel.cs @@ -52,10 +52,10 @@ public class SettingsViewModel : BaseModel private string _customThemePath; private int _scalingFilter; private int _scalingFilterLevel; - private int _customPresentInterval; - private bool _enableCustomPresentInterval; - private int _customPresentIntervalPercentageProxy; - private PresentIntervalState _presentIntervalState; + private int _customVSyncInterval; + private bool _enableCustomVSyncInterval; + private int _customVSyncIntervalPercentageProxy; + private VSyncMode _vSyncMode; public event Action CloseWindow; public event Action SaveSettingsEvent; @@ -142,39 +142,39 @@ public bool DirectoryChanged public bool EnableDockedMode { get; set; } public bool EnableKeyboard { get; set; } public bool EnableMouse { get; set; } - public PresentIntervalState PresentIntervalState + public VSyncMode VSyncMode { - get => _presentIntervalState; + get => _vSyncMode; set { - if (value == PresentIntervalState.Custom || - value == PresentIntervalState.Switch || - value == PresentIntervalState.Unbounded) + if (value == VSyncMode.Custom || + value == VSyncMode.Switch || + value == VSyncMode.Unbounded) { - _presentIntervalState = value; + _vSyncMode = value; OnPropertyChanged(); } } } - public int CustomPresentIntervalPercentageProxy + public int CustomVSyncIntervalPercentageProxy { - get => _customPresentIntervalPercentageProxy; + get => _customVSyncIntervalPercentageProxy; set { int newInterval = (int)(((decimal)value / 100) * 60); - _customPresentInterval = newInterval; - _customPresentIntervalPercentageProxy = value; - OnPropertyChanged((nameof(CustomPresentInterval))); - OnPropertyChanged((nameof(CustomPresentIntervalPercentageText))); + _customVSyncInterval = newInterval; + _customVSyncIntervalPercentageProxy = value; + OnPropertyChanged((nameof(CustomVSyncInterval))); + OnPropertyChanged((nameof(CustomVSyncIntervalPercentageText))); } } - public string CustomPresentIntervalPercentageText + public string CustomVSyncIntervalPercentageText { get { - string text = CustomPresentIntervalPercentageProxy.ToString() + "%"; + string text = CustomVSyncIntervalPercentageProxy.ToString() + "%"; return text; } set @@ -183,34 +183,34 @@ public string CustomPresentIntervalPercentageText } } - public bool EnableCustomPresentInterval + public bool EnableCustomVSyncInterval { - get => _enableCustomPresentInterval; + get => _enableCustomVSyncInterval; set { - _enableCustomPresentInterval = value; - if (_presentIntervalState == PresentIntervalState.Custom && value == false) + _enableCustomVSyncInterval = value; + if (_vSyncMode == VSyncMode.Custom && value == false) { - PresentIntervalState = PresentIntervalState.Switch; + VSyncMode = VSyncMode.Switch; } else if (value) { - PresentIntervalState = PresentIntervalState.Custom; + VSyncMode = VSyncMode.Custom; } OnPropertyChanged(); } } - public int CustomPresentInterval + public int CustomVSyncInterval { - get => _customPresentInterval; + get => _customVSyncInterval; set { - _customPresentInterval = value; + _customVSyncInterval = value; int newPercent = (int)(((decimal)value / 60) * 100); - _customPresentIntervalPercentageProxy = newPercent; - OnPropertyChanged(nameof(CustomPresentIntervalPercentageProxy)); - OnPropertyChanged(nameof(CustomPresentIntervalPercentageText)); + _customVSyncIntervalPercentageProxy = newPercent; + OnPropertyChanged(nameof(CustomVSyncIntervalPercentageProxy)); + OnPropertyChanged(nameof(CustomVSyncIntervalPercentageText)); OnPropertyChanged(); } } @@ -524,9 +524,9 @@ public void LoadCurrentConfiguration() CurrentDate = currentDateTime.Date; CurrentTime = currentDateTime.TimeOfDay.Add(TimeSpan.FromSeconds(config.System.SystemTimeOffset)); - EnableCustomPresentInterval = config.Graphics.EnableCustomPresentInterval.Value; - CustomPresentInterval = config.Graphics.CustomPresentInterval; - PresentIntervalState = config.Graphics.PresentIntervalState; + EnableCustomVSyncInterval = config.Graphics.EnableCustomVSyncInterval.Value; + CustomVSyncInterval = config.Graphics.CustomVSyncInterval; + VSyncMode = config.Graphics.VSyncMode; EnableFsIntegrityChecks = config.System.EnableFsIntegrityChecks; ExpandDramSize = config.System.ExpandRam; IgnoreMissingServices = config.System.IgnoreMissingServices; @@ -615,9 +615,9 @@ public void SaveSettings() } config.System.SystemTimeOffset.Value = Convert.ToInt64((CurrentDate.ToUnixTimeSeconds() + CurrentTime.TotalSeconds) - DateTimeOffset.Now.ToUnixTimeSeconds()); - config.Graphics.PresentIntervalState.Value = PresentIntervalState; - config.Graphics.EnableCustomPresentInterval.Value = EnableCustomPresentInterval; - config.Graphics.CustomPresentInterval.Value = CustomPresentInterval; + config.Graphics.VSyncMode.Value = VSyncMode; + config.Graphics.EnableCustomVSyncInterval.Value = EnableCustomVSyncInterval; + config.Graphics.CustomVSyncInterval.Value = CustomVSyncInterval; config.System.EnableFsIntegrityChecks.Value = EnableFsIntegrityChecks; config.System.ExpandRam.Value = ExpandDramSize; config.System.IgnoreMissingServices.Value = IgnoreMissingServices; @@ -683,7 +683,7 @@ public void SaveSettings() config.ToFileFormat().SaveConfig(Program.ConfigurationPath); MainWindow.UpdateGraphicsConfig(); - MainWindow.MainWindowViewModel.PresentIntervalStateSettingChanged(); + MainWindow.MainWindowViewModel.VSyncModeSettingChanged(); SaveSettingsEvent?.Invoke(); diff --git a/src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml b/src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml index c53281c2a..4bc5130f2 100644 --- a/src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml @@ -80,18 +80,18 @@ MaxHeight="18" Orientation="Horizontal"> + PointerReleased="VSyncMode_PointerReleased" + Text="{Binding VSyncModeText}" + TextAlignment="Start"/> diff --git a/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs index c282aebf0..ac59f0d92 100644 --- a/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs +++ b/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs @@ -1,29 +1,18 @@ -using Avalonia.Collections; -using Avalonia.Controls; +using Avalonia.Controls; +using Avalonia.Interactivity; using Avalonia.Styling; using FluentAvalonia.UI.Controls; using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.UI.Helpers; -using Ryujinx.Ava.UI.Models; +using Ryujinx.Ava.UI.ViewModels; using Ryujinx.HLE.FileSystem; -using Ryujinx.HLE.HOS; -using Ryujinx.Ui.App.Common; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; using System.Threading.Tasks; namespace Ryujinx.Ava.UI.Windows { public partial class CheatWindow : UserControl { - private readonly string _enabledCheatsPath; - public bool NoCheatsFound { get; } - - public AvaloniaList LoadedCheats { get; } - public string BuildId { get; } + public CheatWindowViewModel ViewModel; public CheatWindow() { @@ -32,66 +21,14 @@ public CheatWindow() InitializeComponent(); } - public CheatWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName, string titlePath) + public CheatWindow(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName, string titlePath) { - LoadedCheats = new AvaloniaList(); - - BuildId = ApplicationData.GetApplicationBuildId(virtualFileSystem, titlePath); + DataContext = ViewModel = new CheatWindowViewModel(virtualFileSystem, titleId, titlePath); InitializeComponent(); - - string modsBasePath = ModLoader.GetModsBasePath(); - string titleModsPath = ModLoader.GetTitleDir(modsBasePath, titleId); - ulong titleIdValue = ulong.Parse(titleId, NumberStyles.HexNumber); - - _enabledCheatsPath = Path.Combine(titleModsPath, "cheats", "enabled.txt"); - - string[] enabled = Array.Empty(); - - if (File.Exists(_enabledCheatsPath)) - { - enabled = File.ReadAllLines(_enabledCheatsPath); - } - - int cheatAdded = 0; - - var mods = new ModLoader.ModCache(); - - ModLoader.QueryContentsDir(mods, new DirectoryInfo(Path.Combine(modsBasePath, "contents")), titleIdValue); - - string currentCheatFile = string.Empty; - string buildId = string.Empty; - - CheatNode currentGroup = null; - - foreach (var cheat in mods.Cheats) - { - if (cheat.Path.FullName != currentCheatFile) - { - currentCheatFile = cheat.Path.FullName; - string parentPath = currentCheatFile.Replace(titleModsPath, ""); - - buildId = Path.GetFileNameWithoutExtension(currentCheatFile).ToUpper(); - currentGroup = new CheatNode("", buildId, parentPath, true); - - LoadedCheats.Add(currentGroup); - } - - var model = new CheatNode(cheat.Name, buildId, "", false, enabled.Contains($"{buildId}-{cheat.Name}")); - currentGroup?.SubNodes.Add(model); - - cheatAdded++; - } - - if (cheatAdded == 0) - { - NoCheatsFound = true; - } - - DataContext = this; } - public static async Task Show(VirtualFileSystem virtualFileSystem, string titleId, string titleName, string titlePath) + public static async Task Show(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName, string titlePath) { ContentDialog contentDialog = new() { @@ -99,7 +36,7 @@ public static async Task Show(VirtualFileSystem virtualFileSystem, string titleI SecondaryButtonText = "", CloseButtonText = "", Content = new CheatWindow(virtualFileSystem, titleId, titleName, titlePath), - Title = string.Format(LocaleManager.Instance[LocaleKeys.CheatWindowHeading], titleName, titleId.ToUpper()), + Title = string.Format(LocaleManager.Instance[LocaleKeys.CheatWindowHeading], titleName, titleId.ToString("X16")), }; Style bottomBorder = new(x => x.OfType().Name("DialogSpace").Child().OfType()); @@ -110,34 +47,13 @@ public static async Task Show(VirtualFileSystem virtualFileSystem, string titleI await ContentDialogHelper.ShowAsync(contentDialog); } - public void Save() + private void SaveAndClose(object sender, RoutedEventArgs e) { - if (NoCheatsFound) - { - return; - } - - List enabledCheats = new(); - - foreach (var cheats in LoadedCheats) - { - foreach (var cheat in cheats.SubNodes) - { - if (cheat.IsEnabled) - { - enabledCheats.Add(cheat.BuildIdKey); - } - } - } - - Directory.CreateDirectory(Path.GetDirectoryName(_enabledCheatsPath)); - - File.WriteAllLines(_enabledCheatsPath, enabledCheats); - + ViewModel.Save(); ((ContentDialog)Parent).Hide(); } - public void Close() + private void Close(object sender, RoutedEventArgs e) { ((ContentDialog)Parent).Hide(); } From a665f92127fcbb165faddf69bdd37ae6c50b2a16 Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Mon, 30 Oct 2023 00:42:05 -0400 Subject: [PATCH 33/43] initial Layout --- src/Ryujinx.Ava/Assets/Locales/en_US.json | 2 +- .../UI/ViewModels/CheatWindowViewModel.cs | 10 ++ src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml | 106 +++++++++++------- 3 files changed, 75 insertions(+), 43 deletions(-) diff --git a/src/Ryujinx.Ava/Assets/Locales/en_US.json b/src/Ryujinx.Ava/Assets/Locales/en_US.json index c0493c719..75327185a 100644 --- a/src/Ryujinx.Ava/Assets/Locales/en_US.json +++ b/src/Ryujinx.Ava/Assets/Locales/en_US.json @@ -594,7 +594,7 @@ "DlcWindowTitle": "Manage Downloadable Content for {0} ({1})", "UpdateWindowTitle": "Title Update Manager", "CheatWindowHeading": "Manage Cheats for {0} ({1})", - "BuildId": "BuildId:", + "BuildId": "Build ID:", "DlcWindowHeading": "{0} Downloadable Content(s)", "UserProfilesEditProfile": "Edit Selected", "Cancel": "Cancel", diff --git a/src/Ryujinx.Ava/UI/ViewModels/CheatWindowViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/CheatWindowViewModel.cs index 89b243104..d7de58bb2 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/CheatWindowViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/CheatWindowViewModel.cs @@ -1,4 +1,6 @@ +using Avalonia; using Avalonia.Collections; +using Avalonia.Controls.ApplicationLifetimes; using Ryujinx.Ava.UI.Models; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS; @@ -59,6 +61,14 @@ public CheatWindowViewModel(VirtualFileSystem virtualFileSystem, ulong titleId, } } + public async void CopyToClipboard() + { + if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + await desktop.MainWindow.Clipboard.SetTextAsync(BuildId); + } + } + public void Save() { List enabledCheats = new(); diff --git a/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml b/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml index a0290ebe9..f75e8a8bf 100644 --- a/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml +++ b/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml @@ -11,50 +11,53 @@ x:DataType="viewModels:CheatWindowViewModel" mc:Ignorable="d" Focusable="True"> - + - - - - - - - - + + + + + + + + + + + + + + BorderBrush="{DynamicResource AppListHoverBackgroundColor}" + BorderThickness="1" + CornerRadius="5" + Padding="2.5"> @@ -77,13 +80,32 @@ - - + + + + + - - + + From d4622c5f6aff847f4b6b7a9dbfa9608cbeeea040 Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Mon, 30 Oct 2023 00:46:48 -0400 Subject: [PATCH 34/43] Tweak layout --- src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml b/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml index f75e8a8bf..21c7c8013 100644 --- a/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml +++ b/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml @@ -24,6 +24,7 @@ + + Text="" /> Date: Mon, 15 Jan 2024 20:51:07 -0500 Subject: [PATCH 35/43] Format --- src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs index ac59f0d92..0cb7b8b22 100644 --- a/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs +++ b/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs @@ -1,4 +1,4 @@ -using Avalonia.Controls; +using Avalonia.Controls; using Avalonia.Interactivity; using Avalonia.Styling; using FluentAvalonia.UI.Controls; From e5817a2ed508e79e1827220d27f7771b42bedc62 Mon Sep 17 00:00:00 2001 From: riperiperi Date: Fri, 19 Jan 2024 13:51:48 +0000 Subject: [PATCH 36/43] Fix Push Descriptors --- .../DescriptorSetUpdater.cs | 62 +++++++++++++-- .../HardwareCapabilities.cs | 3 + .../ShaderCollection.cs | 76 ++++++++++++++++++- .../VulkanConfiguration.cs | 2 +- src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs | 39 +++++++++- 5 files changed, 171 insertions(+), 11 deletions(-) diff --git a/src/Ryujinx.Graphics.Vulkan/DescriptorSetUpdater.cs b/src/Ryujinx.Graphics.Vulkan/DescriptorSetUpdater.cs index 6615d8ce0..487fdb21a 100644 --- a/src/Ryujinx.Graphics.Vulkan/DescriptorSetUpdater.cs +++ b/src/Ryujinx.Graphics.Vulkan/DescriptorSetUpdater.cs @@ -61,6 +61,8 @@ public BufferRef(Auto buffer, ref BufferRange range) private BitMapStruct> _storageSet; private BitMapStruct> _uniformMirrored; private BitMapStruct> _storageMirrored; + private int[] _uniformSetPd; + private int _pdSequence = 1; private bool _updateDescriptorCacheCbIndex; @@ -106,6 +108,8 @@ public DescriptorSetUpdater(VulkanRenderer gd, Device device, PipelineBase pipel _bufferTextures = new BufferView[Constants.MaxTexturesPerStage]; _bufferImages = new BufferView[Constants.MaxImagesPerStage]; + _uniformSetPd = new int[Constants.MaxUniformBufferBindings]; + var initialImageInfo = new DescriptorImageInfo { ImageLayout = ImageLayout.General, @@ -193,6 +197,7 @@ internal void Rebind(Auto buffer, int offset, int size) if (BindingOverlaps(ref info, bindingOffset, offset, size)) { _uniformSet.Clear(binding); + _uniformSetPd[binding] = 0; SignalDirty(DirtyFlags.Uniform); } } @@ -223,8 +228,23 @@ internal void Rebind(Auto buffer, int offset, int size) }); } + public void AdvancePdSequence() + { + if (++_pdSequence == 0) + { + _pdSequence = 1; + } + } + public void SetProgram(ShaderCollection program) { + if (!program.HasSameLayout(_program)) + { + // When the pipeline layout changes, push descriptor bindings are invalidated. + + AdvancePdSequence(); + } + _program = program; _updateDescriptorCacheCbIndex = true; _dirty = DirtyFlags.All; @@ -402,6 +422,7 @@ public void SetUniformBuffers(CommandBuffer commandBuffer, ReadOnlySpan uniformBuffers = _uniformBuffers; for (int i = 0; i < count; i++) { @@ -688,15 +716,36 @@ private void UpdateAndBindUniformBufferPd(CommandBufferScoped cbs, PipelineBindP if (_uniformSet.Set(index)) { ref BufferRef buffer = ref _uniformBufferRefs[index]; - UpdateBuffer(cbs, ref _uniformBuffers[index], ref buffer, dummyBuffer, true); - doUpdate = true; + + bool mirrored = UpdateBuffer(cbs, ref _uniformBuffers[index], ref buffer, dummyBuffer, true); + + _uniformMirrored.Set(index, mirrored); + } + + if (_uniformSetPd[index] != sequence) + { + // Need to set this push descriptor (even if the buffer binding has not changed) + + _uniformSetPd[index] = sequence; + + if (updateFrom == -1) + { + updateFrom = index; + } + } + else if (updateFrom != -1) + { + // Need to push updates that have been queued. + + UpdateBuffers(cbs, pbp, updateFrom, uniformBuffers.Slice(updateFrom, index - updateFrom), DescriptorType.UniformBuffer); + + updateFrom = -1; } } - if (doUpdate) + if (updateFrom != -1) { - ReadOnlySpan uniformBuffers = _uniformBuffers; - UpdateBuffers(cbs, pbp, binding, uniformBuffers.Slice(binding, count), DescriptorType.UniformBuffer); + UpdateBuffers(cbs, pbp, updateFrom, uniformBuffers.Slice(updateFrom, binding + count - updateFrom), DescriptorType.UniformBuffer); } } } @@ -724,6 +773,7 @@ public void SignalCommandBufferChange() _uniformSet.Clear(); _storageSet.Clear(); + AdvancePdSequence(); } private static void SwapBuffer(BufferRef[] list, Auto from, Auto to) diff --git a/src/Ryujinx.Graphics.Vulkan/HardwareCapabilities.cs b/src/Ryujinx.Graphics.Vulkan/HardwareCapabilities.cs index 98c777eed..b6694bcb3 100644 --- a/src/Ryujinx.Graphics.Vulkan/HardwareCapabilities.cs +++ b/src/Ryujinx.Graphics.Vulkan/HardwareCapabilities.cs @@ -34,6 +34,7 @@ readonly struct HardwareCapabilities public readonly bool SupportsMultiView; public readonly bool SupportsNullDescriptors; public readonly bool SupportsPushDescriptors; + public readonly uint MaxPushDescriptors; public readonly bool SupportsPrimitiveTopologyListRestart; public readonly bool SupportsPrimitiveTopologyPatchListRestart; public readonly bool SupportsTransformFeedback; @@ -71,6 +72,7 @@ public HardwareCapabilities( bool supportsMultiView, bool supportsNullDescriptors, bool supportsPushDescriptors, + uint maxPushDescriptors, bool supportsPrimitiveTopologyListRestart, bool supportsPrimitiveTopologyPatchListRestart, bool supportsTransformFeedback, @@ -107,6 +109,7 @@ public HardwareCapabilities( SupportsMultiView = supportsMultiView; SupportsNullDescriptors = supportsNullDescriptors; SupportsPushDescriptors = supportsPushDescriptors; + MaxPushDescriptors = maxPushDescriptors; SupportsPrimitiveTopologyListRestart = supportsPrimitiveTopologyListRestart; SupportsPrimitiveTopologyPatchListRestart = supportsPrimitiveTopologyPatchListRestart; SupportsTransformFeedback = supportsTransformFeedback; diff --git a/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs b/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs index 0d6da0391..fd0738dd2 100644 --- a/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs +++ b/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs @@ -108,16 +108,23 @@ public ShaderCollection( _shaders = internalShaders; - bool usePushDescriptors = !isMinimal && VulkanConfiguration.UsePushDescriptors && _gd.Capabilities.SupportsPushDescriptors; + bool usePushDescriptors = !isMinimal && + VulkanConfiguration.UsePushDescriptors && + _gd.Capabilities.SupportsPushDescriptors && + !IsCompute && + CanUsePushDescriptors(gd, resourceLayout, IsCompute); - _plce = gd.PipelineLayoutCache.GetOrCreate(gd, device, resourceLayout.Sets, usePushDescriptors); + ReadOnlyCollection sets = usePushDescriptors ? + BuildPushDescriptorSets(gd, resourceLayout.Sets) : resourceLayout.Sets; + + _plce = gd.PipelineLayoutCache.GetOrCreate(gd, device, sets, usePushDescriptors); HasMinimalLayout = isMinimal; UsePushDescriptors = usePushDescriptors; Stages = stages; - ClearSegments = BuildClearSegments(resourceLayout.Sets); + ClearSegments = BuildClearSegments(sets); BindingSegments = BuildBindingSegments(resourceLayout.SetUsages); Templates = BuildTemplates(); @@ -139,6 +146,64 @@ public ShaderCollection( _firstBackgroundUse = !fromCache; } + private static bool CanUsePushDescriptors(VulkanRenderer gd, ResourceLayout layout, bool isCompute) + { + // If binding 3 is immediately used, use an alternate set of reserved bindings. + ReadOnlyCollection uniformUsage = layout.SetUsages[0].Usages; + bool hasBinding3 = uniformUsage.Any(x => x.Binding == 3); + int[] reserved = isCompute ? Array.Empty() : gd.GetPushDescriptorReservedBindings(hasBinding3); + + // Can't use any of the reserved usages. + for (int i = 0; i < uniformUsage.Count; i++) + { + var binding = uniformUsage[i].Binding; + + if (reserved.Contains(binding) || binding >= gd.Capabilities.MaxPushDescriptors + reserved.Length) + { + return false; + } + } + + return true; + } + + private static ReadOnlyCollection BuildPushDescriptorSets( + VulkanRenderer gd, + ReadOnlyCollection sets) + { + // The reserved bindings were selected when determining if push descriptors could be used. + int[] reserved = gd.GetPushDescriptorReservedBindings(false); + + var result = new ResourceDescriptorCollection[sets.Count]; + + for (int i = 0; i < sets.Count; i++) + { + if (i == 0) + { + // Push descriptors apply here. Remove reserved bindings. + ResourceDescriptorCollection original = sets[i]; + + var pdUniforms = new List(); + + foreach (ResourceDescriptor descriptor in original.Descriptors) + { + if (!reserved.Contains(descriptor.Binding)) + { + pdUniforms.Add(descriptor); + } + } + + result[i] = new ResourceDescriptorCollection(new(pdUniforms)); + } + else + { + result[i] = sets[i]; + } + } + + return new(result); + } + private static ResourceBindingSegment[][] BuildClearSegments(ReadOnlyCollection sets) { ResourceBindingSegment[][] segments = new ResourceBindingSegment[sets.Count][]; @@ -493,6 +558,11 @@ public Auto GetNewDescriptorSetCollection(int setIndex, return _plce.GetNewDescriptorSetCollection(setIndex, out isNew); } + public bool HasSameLayout(ShaderCollection other) + { + return other != null && _plce == other._plce; + } + protected virtual void Dispose(bool disposing) { if (disposing) diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanConfiguration.cs b/src/Ryujinx.Graphics.Vulkan/VulkanConfiguration.cs index a1fdc4aed..596c0e176 100644 --- a/src/Ryujinx.Graphics.Vulkan/VulkanConfiguration.cs +++ b/src/Ryujinx.Graphics.Vulkan/VulkanConfiguration.cs @@ -4,7 +4,7 @@ static class VulkanConfiguration { public const bool UseFastBufferUpdates = true; public const bool UseUnsafeBlit = true; - public const bool UsePushDescriptors = false; + public const bool UsePushDescriptors = true; public const bool ForceD24S8Unsupported = false; public const bool ForceRGB16IntFloatUnsupported = false; diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs index 641ac844f..43f27ad2b 100644 --- a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs +++ b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs @@ -76,6 +76,10 @@ public sealed class VulkanRenderer : IRenderer private readonly Func _getRequiredExtensions; private readonly string _preferredGpuId; + private int[] _pdReservedBindings; + private readonly static int[] _pdReservedBindingsNvn = { 3, 18, 21, 36, 30 }; + private readonly static int[] _pdReservedBindingsOgl = { 17, 18, 34, 35, 36 }; + internal Vendor Vendor { get; private set; } internal bool IsAmdWindows { get; private set; } internal bool IsIntelWindows { get; private set; } @@ -190,6 +194,19 @@ private unsafe void LoadFeatures(uint maxQueueCount, uint queueFamilyIndex) SType = StructureType.PhysicalDevicePortabilitySubsetPropertiesKhr, }; + bool supportsPushDescriptors = _physicalDevice.IsDeviceExtensionPresent(KhrPushDescriptor.ExtensionName); + + PhysicalDevicePushDescriptorPropertiesKHR propertiesPushDescriptor = new PhysicalDevicePushDescriptorPropertiesKHR() + { + SType = StructureType.PhysicalDevicePushDescriptorPropertiesKhr + }; + + if (supportsPushDescriptors) + { + propertiesPushDescriptor.PNext = properties2.PNext; + properties2.PNext = &propertiesPushDescriptor; + } + PhysicalDeviceFeatures2 features2 = new() { SType = StructureType.PhysicalDeviceFeatures2, @@ -319,7 +336,8 @@ private unsafe void LoadFeatures(uint maxQueueCount, uint queueFamilyIndex) _physicalDevice.IsDeviceExtensionPresent(ExtExtendedDynamicState.ExtensionName), features2.Features.MultiViewport && !(IsMoltenVk && Vendor == Vendor.Amd), // Workaround for AMD on MoltenVK issue featuresRobustness2.NullDescriptor || IsMoltenVk, - _physicalDevice.IsDeviceExtensionPresent(KhrPushDescriptor.ExtensionName), + supportsPushDescriptors, + propertiesPushDescriptor.MaxPushDescriptors, featuresPrimitiveTopologyListRestart.PrimitiveTopologyListRestart, featuresPrimitiveTopologyListRestart.PrimitiveTopologyPatchListRestart, supportsTransformFeedback, @@ -399,6 +417,25 @@ private void SetupContext(GraphicsDebugLevel logLevel) _initialized = true; } + public int[] GetPushDescriptorReservedBindings(bool isOgl) + { + // The first call of this method determines what push descriptor layout is used for all shaders on this renderer. + // This is chosen to minimize shaders that can't fit on 32 entry push descriptor sets. + if (_pdReservedBindings == null) + { + if (Capabilities.MaxPushDescriptors == 32) + { + _pdReservedBindings = isOgl ? _pdReservedBindingsOgl : _pdReservedBindingsNvn; + } + else + { + _pdReservedBindings = Array.Empty(); + } + } + + return _pdReservedBindings; + } + public BufferHandle CreateBuffer(int size, BufferAccess access) { return BufferManager.CreateWithHandle(this, size, access.HasFlag(BufferAccess.SparseCompatible), access.Convert(), default, access == BufferAccess.Stream); From d76a023e28c85037520975986b8c8c64890d03af Mon Sep 17 00:00:00 2001 From: riperiperi Date: Sat, 20 Jan 2024 01:48:04 +0000 Subject: [PATCH 37/43] Use push descriptor templates --- .../DescriptorSetTemplate.cs | 87 +++++++++++++++++++ .../DescriptorSetTemplateUpdater.cs | 12 +++ .../DescriptorSetUpdater.cs | 35 +++----- .../PipelineLayoutCacheEntry.cs | 40 +++++++++ .../ShaderCollection.cs | 15 +++- 5 files changed, 164 insertions(+), 25 deletions(-) diff --git a/src/Ryujinx.Graphics.Vulkan/DescriptorSetTemplate.cs b/src/Ryujinx.Graphics.Vulkan/DescriptorSetTemplate.cs index 0c0004b95..09f95ad92 100644 --- a/src/Ryujinx.Graphics.Vulkan/DescriptorSetTemplate.cs +++ b/src/Ryujinx.Graphics.Vulkan/DescriptorSetTemplate.cs @@ -1,12 +1,19 @@ using Ryujinx.Graphics.GAL; using Silk.NET.Vulkan; using System; +using System.Numerics; using System.Runtime.CompilerServices; namespace Ryujinx.Graphics.Vulkan { class DescriptorSetTemplate : IDisposable { + /// + /// Renderdoc seems to crash when doing a templated uniform update with count > 1 on a push descriptor. + /// When this is true, consecutive buffers are always updated individually. + /// + private const bool RenderdocPushCountBug = true; + private readonly VulkanRenderer _gd; private readonly Device _device; @@ -137,6 +144,86 @@ public unsafe DescriptorSetTemplate(VulkanRenderer gd, Device device, ResourceBi Template = result; } + public unsafe DescriptorSetTemplate(VulkanRenderer gd, Device device, ResourceDescriptorCollection descriptors, long updateMask, PipelineLayoutCacheEntry plce, PipelineBindPoint pbp, int setIndex) + { + _gd = gd; + _device = device; + + // Create a template from the set usages. Assumes the descriptor set is updated in segment order then binding order. + int segmentCount = BitOperations.PopCount((ulong)updateMask); + + DescriptorUpdateTemplateEntry* entries = stackalloc DescriptorUpdateTemplateEntry[segmentCount]; + int entry = 0; + nuint structureOffset = 0; + + void addBinding(int binding, int count) + { + entries[entry++] = new DescriptorUpdateTemplateEntry() + { + DescriptorType = DescriptorType.UniformBuffer, + DstBinding = (uint)binding, + DescriptorCount = (uint)count, + Offset = structureOffset, + Stride = (nuint)Unsafe.SizeOf() + }; + + structureOffset += (nuint)(Unsafe.SizeOf() * count); + } + + int startBinding = 0; + int bindingCount = 0; + + foreach (ResourceDescriptor descriptor in descriptors.Descriptors) + { + for (int i = 0; i < descriptor.Count; i++) + { + int binding = descriptor.Binding + i; + + if ((updateMask & (1L << binding)) != 0) + { + if (bindingCount > 0 && (RenderdocPushCountBug || startBinding + bindingCount != binding)) + { + addBinding(startBinding, bindingCount); + + bindingCount = 0; + } + + if (bindingCount == 0) + { + startBinding = binding; + } + + bindingCount++; + } + } + } + + if (bindingCount > 0) + { + addBinding(startBinding, bindingCount); + } + + Size = (int)structureOffset; + + var info = new DescriptorUpdateTemplateCreateInfo() + { + SType = StructureType.DescriptorUpdateTemplateCreateInfo, + DescriptorUpdateEntryCount = (uint)entry, + PDescriptorUpdateEntries = entries, + + TemplateType = DescriptorUpdateTemplateType.PushDescriptorsKhr, + DescriptorSetLayout = plce.DescriptorSetLayouts[setIndex], + PipelineBindPoint = pbp, + PipelineLayout = plce.PipelineLayout, + Set = (uint)setIndex, + }; + + DescriptorUpdateTemplate result; + gd.Api.CreateDescriptorUpdateTemplate(device, &info, null, &result).ThrowOnError(); + + Template = result; + } + public unsafe void Dispose() { _gd.Api.DestroyDescriptorUpdateTemplate(_device, Template, null); diff --git a/src/Ryujinx.Graphics.Vulkan/DescriptorSetTemplateUpdater.cs b/src/Ryujinx.Graphics.Vulkan/DescriptorSetTemplateUpdater.cs index 1eb9dce75..88db7e769 100644 --- a/src/Ryujinx.Graphics.Vulkan/DescriptorSetTemplateUpdater.cs +++ b/src/Ryujinx.Graphics.Vulkan/DescriptorSetTemplateUpdater.cs @@ -52,11 +52,23 @@ public DescriptorSetTemplateWriter Begin(DescriptorSetTemplate template) return new DescriptorSetTemplateWriter(new Span(_data.Pointer, template.Size)); } + public DescriptorSetTemplateWriter Begin(int maxSize) + { + EnsureSize(maxSize); + + return new DescriptorSetTemplateWriter(new Span(_data.Pointer, maxSize)); + } + public void Commit(VulkanRenderer gd, Device device, DescriptorSet set) { gd.Api.UpdateDescriptorSetWithTemplate(device, set, _activeTemplate.Template, _data.Pointer); } + public void CommitPushDescriptor(VulkanRenderer gd, CommandBufferScoped cbs, DescriptorSetTemplate template, PipelineLayout layout) + { + gd.PushDescriptorApi.CmdPushDescriptorSetWithTemplate(cbs.CommandBuffer, template.Template, layout, 0, _data.Pointer); + } + public void Dispose() { _data?.Dispose(); diff --git a/src/Ryujinx.Graphics.Vulkan/DescriptorSetUpdater.cs b/src/Ryujinx.Graphics.Vulkan/DescriptorSetUpdater.cs index 487fdb21a..9f6e5d55f 100644 --- a/src/Ryujinx.Graphics.Vulkan/DescriptorSetUpdater.cs +++ b/src/Ryujinx.Graphics.Vulkan/DescriptorSetUpdater.cs @@ -4,6 +4,7 @@ using Silk.NET.Vulkan; using System; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using CompareOp = Ryujinx.Graphics.GAL.CompareOp; using Format = Ryujinx.Graphics.GAL.Format; using SamplerCreateInfo = Ryujinx.Graphics.GAL.SamplerCreateInfo; @@ -61,7 +62,7 @@ public BufferRef(Auto buffer, ref BufferRange range) private BitMapStruct> _storageSet; private BitMapStruct> _uniformMirrored; private BitMapStruct> _storageMirrored; - private int[] _uniformSetPd; + private readonly int[] _uniformSetPd; private int _pdSequence = 1; private bool _updateDescriptorCacheCbIndex; @@ -696,17 +697,14 @@ private void UpdateAndBindUniformBufferPd(CommandBufferScoped cbs, PipelineBindP var bindingSegments = _program.BindingSegments[PipelineBase.UniformSetIndex]; var dummyBuffer = _dummyBuffer?.GetBuffer(); + long updatedBindings = 0; + DescriptorSetTemplateWriter writer = _templateUpdater.Begin(32 * Unsafe.SizeOf()); + foreach (ResourceBindingSegment segment in bindingSegments) { - // Important: - // Not all bindings may update in a push descriptor update, - // But we still want to group them when consecutive if possible. - int binding = segment.Binding; int count = segment.Count; - int updateFrom = -1; - ReadOnlySpan uniformBuffers = _uniformBuffers; for (int i = 0; i < count; i++) @@ -727,26 +725,17 @@ private void UpdateAndBindUniformBufferPd(CommandBufferScoped cbs, PipelineBindP // Need to set this push descriptor (even if the buffer binding has not changed) _uniformSetPd[index] = sequence; + updatedBindings |= 1L << index; - if (updateFrom == -1) - { - updateFrom = index; - } - } - else if (updateFrom != -1) - { - // Need to push updates that have been queued. - - UpdateBuffers(cbs, pbp, updateFrom, uniformBuffers.Slice(updateFrom, index - updateFrom), DescriptorType.UniformBuffer); - - updateFrom = -1; + writer.Push(MemoryMarshal.CreateReadOnlySpan(ref _uniformBuffers[index], 1)); } } + } - if (updateFrom != -1) - { - UpdateBuffers(cbs, pbp, updateFrom, uniformBuffers.Slice(updateFrom, binding + count - updateFrom), DescriptorType.UniformBuffer); - } + if (updatedBindings > 0) + { + DescriptorSetTemplate template = _program.GetPushDescriptorTemplate(updatedBindings); + _templateUpdater.CommitPushDescriptor(_gd, cbs, template, _program.PipelineLayout); } } diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineLayoutCacheEntry.cs b/src/Ryujinx.Graphics.Vulkan/PipelineLayoutCacheEntry.cs index 2840dda0f..f388d9e88 100644 --- a/src/Ryujinx.Graphics.Vulkan/PipelineLayoutCacheEntry.cs +++ b/src/Ryujinx.Graphics.Vulkan/PipelineLayoutCacheEntry.cs @@ -31,6 +31,11 @@ class PipelineLayoutCacheEntry private int _dsLastCbIndex; private int _dsLastSubmissionCount; + private readonly Dictionary _pdTemplates; + private readonly ResourceDescriptorCollection _pdDescriptors; + private long _lastPdUsage; + private DescriptorSetTemplate _lastPdTemplate; + private PipelineLayoutCacheEntry(VulkanRenderer gd, Device device, int setsCount) { _gd = gd; @@ -72,6 +77,12 @@ public PipelineLayoutCacheEntry( _consumedDescriptorsPerSet[setIndex] = count; } + + if (usePushDescriptors) + { + _pdDescriptors = setDescriptors[0]; + _pdTemplates = new(); + } } public void UpdateCommandBufferIndex(int commandBufferIndex) @@ -143,10 +154,39 @@ private static Span GetDescriptorPoolSizes(Span 0) @@ -498,6 +504,11 @@ public byte[] GetBinary() return null; } + public DescriptorSetTemplate GetPushDescriptorTemplate(long updateMask) + { + return _plce.GetPushDescriptorTemplate(IsCompute ? PipelineBindPoint.Compute : PipelineBindPoint.Graphics, updateMask); + } + public void AddComputePipeline(ref SpecData key, Auto pipeline) { (_computePipelineCache ??= new()).Add(ref key, pipeline); From 29a13566a101dd40a88ae94a667be6fc11e49254 Mon Sep 17 00:00:00 2001 From: riperiperi Date: Sat, 20 Jan 2024 15:17:02 +0000 Subject: [PATCH 38/43] Use reserved bindings --- src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs b/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs index 38c0fc54e..552beadbb 100644 --- a/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs +++ b/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs @@ -183,13 +183,23 @@ private static ReadOnlyCollection BuildPushDescrip // Push descriptors apply here. Remove reserved bindings. ResourceDescriptorCollection original = sets[i]; - var pdUniforms = new List(); + var pdUniforms = new ResourceDescriptor[original.Descriptors.Count]; + int j = 0; foreach (ResourceDescriptor descriptor in original.Descriptors) { - if (!reserved.Contains(descriptor.Binding)) + if (reserved.Contains(descriptor.Binding)) { - pdUniforms.Add(descriptor); + // If the binding is reserved, set its descriptor count to 0. + pdUniforms[j++] = new ResourceDescriptor( + descriptor.Binding, + 0, + descriptor.Type, + descriptor.Stages); + } + else + { + pdUniforms[j++] = descriptor; } } From 61b66fd6812fbab2fc756a1a1c4af4f07338434f Mon Sep 17 00:00:00 2001 From: riperiperi Date: Sat, 20 Jan 2024 22:31:05 +0000 Subject: [PATCH 39/43] Formatting --- src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs b/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs index 552beadbb..71a260746 100644 --- a/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs +++ b/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs @@ -114,7 +114,7 @@ public ShaderCollection( !IsCompute && CanUsePushDescriptors(gd, resourceLayout, IsCompute); - ReadOnlyCollection sets = usePushDescriptors ? + ReadOnlyCollection sets = usePushDescriptors ? BuildPushDescriptorSets(gd, resourceLayout.Sets) : resourceLayout.Sets; _plce = gd.PipelineLayoutCache.GetOrCreate(gd, device, sets, usePushDescriptors); From 961e73f9c7ddd2e6f4769869423d4c1878d6a610 Mon Sep 17 00:00:00 2001 From: riperiperi Date: Mon, 22 Jan 2024 21:12:36 +0000 Subject: [PATCH 40/43] Vulkan: Enumerate Query Pool properly Turns out that ElementAt for Queue runs the default implementation as it doesn't implement IList, which enumerates elements of the queue up to the given index. This code was creating `count` enumerators and iterating way more queue items than it needed to at higher counts. The solution is just to use one enumerator and break out of the loop when we get the count that we need. 3.5% of backend time was being spent _just_ enumerating at the usual spot in SMO. --- src/Ryujinx.Graphics.Vulkan/Queries/CounterQueue.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Ryujinx.Graphics.Vulkan/Queries/CounterQueue.cs b/src/Ryujinx.Graphics.Vulkan/Queries/CounterQueue.cs index 3984e2826..0d133e50e 100644 --- a/src/Ryujinx.Graphics.Vulkan/Queries/CounterQueue.cs +++ b/src/Ryujinx.Graphics.Vulkan/Queries/CounterQueue.cs @@ -67,9 +67,18 @@ public void ResetFutureCounters(CommandBuffer cmd, int count) lock (_queryPool) { count = Math.Min(count, _queryPool.Count); - for (int i = 0; i < count; i++) + + if (count > 0) { - _queryPool.ElementAt(i).PoolReset(cmd, ResetSequence); + foreach (BufferedQuery query in _queryPool) + { + query.PoolReset(cmd, ResetSequence); + + if (--count == 0) + { + break; + } + } } } } From bd83221391aead8ce86b966d66907fe2898f1f75 Mon Sep 17 00:00:00 2001 From: riperiperi Date: Tue, 23 Jan 2024 19:58:40 +0000 Subject: [PATCH 41/43] Vulkan: Use staging buffer for temporary constants Helper shaders and post processing effects typically need some parameters to tell them what to do, which we pass via constant buffers that are created and destroyed each time. This can vary in cost between different Vulkan drivers. It shows up on profiles on mesa and MoltenVK, so it's worth avoiding. Some games only do it once (BlitColor for present), others multiple times. It's also done for post processing filters and FSR upscaling, which creates two buffers. For mirrors, I added the ability to reserve a range on the staging buffer for use as any type of binding. This PR allows these constant buffers to be instead temporarily allocated on the staging buffer, skipping allocation and buffer management costs entirely. Two temporary allocations do remain: - DrawTexture, because it doesn't have access to the command buffer scope - Index buffer indirect conversion, because one of them is a storage buffer and thus is a little more complicated. There's a small cost in that the uniform buffer takes up more space due to alignment requirements. At worst that's 256 bytes (on a GTX 1070) but more modern GPUs should have a better time. Worth testing across different games and post effects to make sure they still work. --- src/Ryujinx.Graphics.Vulkan/BufferHolder.cs | 2 +- src/Ryujinx.Graphics.Vulkan/BufferManager.cs | 48 ++++++++++++- .../Effects/FsrScalingFilter.cs | 19 ++--- .../Effects/FxaaPostProcessingEffect.cs | 8 +-- .../Effects/SmaaPostProcessingEffect.cs | 9 +-- src/Ryujinx.Graphics.Vulkan/HelperShader.cs | 72 +++++++------------ src/Ryujinx.Graphics.Vulkan/StagingBuffer.cs | 20 ++++-- 7 files changed, 103 insertions(+), 75 deletions(-) diff --git a/src/Ryujinx.Graphics.Vulkan/BufferHolder.cs b/src/Ryujinx.Graphics.Vulkan/BufferHolder.cs index bdd5d3856..e8153c627 100644 --- a/src/Ryujinx.Graphics.Vulkan/BufferHolder.cs +++ b/src/Ryujinx.Graphics.Vulkan/BufferHolder.cs @@ -384,7 +384,7 @@ private unsafe bool TryGetMirror(CommandBufferScoped cbs, ref int offset, int si var baseData = new Span((void*)(_map + offset), size); var modData = _pendingData.AsSpan(offset, size); - StagingBufferReserved? newMirror = _gd.BufferManager.StagingBuffer.TryReserveData(cbs, size, (int)_gd.Capabilities.MinResourceAlignment); + StagingBufferReserved? newMirror = _gd.BufferManager.StagingBuffer.TryReserveData(cbs, size); if (newMirror != null) { diff --git a/src/Ryujinx.Graphics.Vulkan/BufferManager.cs b/src/Ryujinx.Graphics.Vulkan/BufferManager.cs index e9ac98847..1ed295ce8 100644 --- a/src/Ryujinx.Graphics.Vulkan/BufferManager.cs +++ b/src/Ryujinx.Graphics.Vulkan/BufferManager.cs @@ -9,6 +9,34 @@ namespace Ryujinx.Graphics.Vulkan { + readonly struct ScopedTemporaryBuffer : IDisposable + { + private readonly BufferManager _bufferManager; + private readonly bool _isReserved; + + public readonly BufferRange Range; + + public BufferHandle Handle => Range.Handle; + public int Offset => Range.Offset; + + public ScopedTemporaryBuffer(BufferManager bufferManager, BufferHandle handle, int offset, int size, bool isReserved) + { + _bufferManager = bufferManager; + + Range = new BufferRange(handle, offset, size); + + _isReserved = isReserved; + } + + public void Dispose() + { + if (!_isReserved) + { + _bufferManager.Delete(Range.Handle); + } + } + } + class BufferManager : IDisposable { public const MemoryPropertyFlags DefaultBufferMemoryFlags = @@ -238,6 +266,23 @@ public BufferHandle CreateWithHandle( return Unsafe.As(ref handle64); } + public ScopedTemporaryBuffer ReserveOrCreate(VulkanRenderer gd, CommandBufferScoped cbs, int size, int alignment = -1) + { + StagingBufferReserved? result = StagingBuffer.TryReserveData(cbs, size, alignment); + + if (result.HasValue) + { + return new ScopedTemporaryBuffer(this, StagingBuffer.Handle, result.Value.Offset, result.Value.Size, true); + } + else + { + // Create a temporary buffer. + BufferHandle handle = CreateWithHandle(gd, size); + + return new ScopedTemporaryBuffer(this, handle, 0, size, false); + } + } + public unsafe MemoryRequirements GetHostImportedUsageRequirements(VulkanRenderer gd) { var usage = HostImportedBufferUsageFlags; @@ -635,13 +680,14 @@ protected virtual void Dispose(bool disposing) { if (disposing) { + StagingBuffer.Dispose(); + foreach (BufferHolder buffer in _buffers) { buffer.Dispose(); } _buffers.Clear(); - StagingBuffer.Dispose(); } } diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/FsrScalingFilter.cs b/src/Ryujinx.Graphics.Vulkan/Effects/FsrScalingFilter.cs index 23acdcf8f..979a36838 100644 --- a/src/Ryujinx.Graphics.Vulkan/Effects/FsrScalingFilter.cs +++ b/src/Ryujinx.Graphics.Vulkan/Effects/FsrScalingFilter.cs @@ -142,19 +142,18 @@ public void Run( }; int rangeSize = dimensionsBuffer.Length * sizeof(float); - var bufferHandle = _renderer.BufferManager.CreateWithHandle(_renderer, rangeSize); - _renderer.BufferManager.SetData(bufferHandle, 0, dimensionsBuffer); + using var buffer = _renderer.BufferManager.ReserveOrCreate(_renderer, cbs, rangeSize); + _renderer.BufferManager.SetData(buffer.Handle, buffer.Offset, dimensionsBuffer); - ReadOnlySpan sharpeningBuffer = stackalloc float[] { 1.5f - (Level * 0.01f * 1.5f) }; - var sharpeningBufferHandle = _renderer.BufferManager.CreateWithHandle(_renderer, sizeof(float)); - _renderer.BufferManager.SetData(sharpeningBufferHandle, 0, sharpeningBuffer); + ReadOnlySpan sharpeningBufferData = stackalloc float[] { 1.5f - (Level * 0.01f * 1.5f) }; + using var sharpeningBuffer = _renderer.BufferManager.ReserveOrCreate(_renderer, cbs, sizeof(float)); + _renderer.BufferManager.SetData(sharpeningBuffer.Handle, sharpeningBuffer.Offset, sharpeningBufferData); int threadGroupWorkRegionDim = 16; int dispatchX = (width + (threadGroupWorkRegionDim - 1)) / threadGroupWorkRegionDim; int dispatchY = (height + (threadGroupWorkRegionDim - 1)) / threadGroupWorkRegionDim; - var bufferRanges = new BufferRange(bufferHandle, 0, rangeSize); - _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(2, bufferRanges) }); + _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(2, buffer.Range) }); _pipeline.SetImage(0, _intermediaryTexture, FormatTable.ConvertRgba8SrgbToUnorm(view.Info.Format)); _pipeline.DispatchCompute(dispatchX, dispatchY, 1); _pipeline.ComputeBarrier(); @@ -162,16 +161,12 @@ public void Run( // Sharpening pass _pipeline.SetProgram(_sharpeningProgram); _pipeline.SetTextureAndSampler(ShaderStage.Compute, 1, _intermediaryTexture, _sampler); - var sharpeningRange = new BufferRange(sharpeningBufferHandle, 0, sizeof(float)); - _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(4, sharpeningRange) }); + _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(4, sharpeningBuffer.Range) }); _pipeline.SetImage(0, destinationTexture); _pipeline.DispatchCompute(dispatchX, dispatchY, 1); _pipeline.ComputeBarrier(); _pipeline.Finish(); - - _renderer.BufferManager.Delete(bufferHandle); - _renderer.BufferManager.Delete(sharpeningBufferHandle); } } } diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/FxaaPostProcessingEffect.cs b/src/Ryujinx.Graphics.Vulkan/Effects/FxaaPostProcessingEffect.cs index 67e461e51..0984823c0 100644 --- a/src/Ryujinx.Graphics.Vulkan/Effects/FxaaPostProcessingEffect.cs +++ b/src/Ryujinx.Graphics.Vulkan/Effects/FxaaPostProcessingEffect.cs @@ -66,12 +66,11 @@ public TextureView Run(TextureView view, CommandBufferScoped cbs, int width, int ReadOnlySpan resolutionBuffer = stackalloc float[] { view.Width, view.Height }; int rangeSize = resolutionBuffer.Length * sizeof(float); - var bufferHandle = _renderer.BufferManager.CreateWithHandle(_renderer, rangeSize); + using var buffer = _renderer.BufferManager.ReserveOrCreate(_renderer, cbs, rangeSize); - _renderer.BufferManager.SetData(bufferHandle, 0, resolutionBuffer); + _renderer.BufferManager.SetData(buffer.Handle, buffer.Offset, resolutionBuffer); - var bufferRanges = new BufferRange(bufferHandle, 0, rangeSize); - _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(2, bufferRanges) }); + _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(2, buffer.Range) }); var dispatchX = BitUtils.DivRoundUp(view.Width, IPostProcessingEffect.LocalGroupSize); var dispatchY = BitUtils.DivRoundUp(view.Height, IPostProcessingEffect.LocalGroupSize); @@ -79,7 +78,6 @@ public TextureView Run(TextureView view, CommandBufferScoped cbs, int width, int _pipeline.SetImage(0, _texture, FormatTable.ConvertRgba8SrgbToUnorm(view.Info.Format)); _pipeline.DispatchCompute(dispatchX, dispatchY, 1); - _renderer.BufferManager.Delete(bufferHandle); _pipeline.ComputeBarrier(); _pipeline.Finish(); diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/SmaaPostProcessingEffect.cs b/src/Ryujinx.Graphics.Vulkan/Effects/SmaaPostProcessingEffect.cs index c521f2273..7b850e2bc 100644 --- a/src/Ryujinx.Graphics.Vulkan/Effects/SmaaPostProcessingEffect.cs +++ b/src/Ryujinx.Graphics.Vulkan/Effects/SmaaPostProcessingEffect.cs @@ -215,11 +215,10 @@ public TextureView Run(TextureView view, CommandBufferScoped cbs, int width, int ReadOnlySpan resolutionBuffer = stackalloc float[] { view.Width, view.Height }; int rangeSize = resolutionBuffer.Length * sizeof(float); - var bufferHandle = _renderer.BufferManager.CreateWithHandle(_renderer, rangeSize); + using var buffer = _renderer.BufferManager.ReserveOrCreate(_renderer, cbs, rangeSize); - _renderer.BufferManager.SetData(bufferHandle, 0, resolutionBuffer); - var bufferRanges = new BufferRange(bufferHandle, 0, rangeSize); - _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(2, bufferRanges) }); + _renderer.BufferManager.SetData(buffer.Handle, buffer.Offset, resolutionBuffer); + _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(2, buffer.Range) }); _pipeline.SetImage(0, _edgeOutputTexture, FormatTable.ConvertRgba8SrgbToUnorm(view.Info.Format)); _pipeline.DispatchCompute(dispatchX, dispatchY, 1); _pipeline.ComputeBarrier(); @@ -245,8 +244,6 @@ public TextureView Run(TextureView view, CommandBufferScoped cbs, int width, int _pipeline.Finish(); - _renderer.BufferManager.Delete(bufferHandle); - return _outputTexture; } diff --git a/src/Ryujinx.Graphics.Vulkan/HelperShader.cs b/src/Ryujinx.Graphics.Vulkan/HelperShader.cs index deaf81625..247556db9 100644 --- a/src/Ryujinx.Graphics.Vulkan/HelperShader.cs +++ b/src/Ryujinx.Graphics.Vulkan/HelperShader.cs @@ -430,11 +430,11 @@ public void BlitColor( (region[2], region[3]) = (region[3], region[2]); } - var bufferHandle = gd.BufferManager.CreateWithHandle(gd, RegionBufferSize); + using var buffer = gd.BufferManager.ReserveOrCreate(gd, cbs, RegionBufferSize); - gd.BufferManager.SetData(bufferHandle, 0, region); + gd.BufferManager.SetData(buffer.Handle, buffer.Offset, region); - _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(1, new BufferRange(bufferHandle, 0, RegionBufferSize)) }); + _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(1, buffer.Range) }); Span viewports = stackalloc Viewport[1]; @@ -490,8 +490,6 @@ public void BlitColor( } _pipeline.Finish(gd, cbs); - - gd.BufferManager.Delete(bufferHandle); } private void BlitDepthStencil( @@ -527,11 +525,11 @@ private void BlitDepthStencil( (region[2], region[3]) = (region[3], region[2]); } - var bufferHandle = gd.BufferManager.CreateWithHandle(gd, RegionBufferSize); + using var buffer = gd.BufferManager.ReserveOrCreate(gd, cbs, RegionBufferSize); - gd.BufferManager.SetData(bufferHandle, 0, region); + gd.BufferManager.SetData(buffer.Handle, buffer.Offset, region); - _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(1, new BufferRange(bufferHandle, 0, RegionBufferSize)) }); + _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(1, buffer.Range) }); Span viewports = stackalloc Viewport[1]; @@ -582,8 +580,6 @@ private void BlitDepthStencil( } _pipeline.Finish(gd, cbs); - - gd.BufferManager.Delete(bufferHandle); } private static TextureView CreateDepthOrStencilView(TextureView depthStencilTexture, DepthStencilMode depthStencilMode) @@ -681,11 +677,11 @@ public void Clear( _pipeline.SetCommandBuffer(cbs); - var bufferHandle = gd.BufferManager.CreateWithHandle(gd, ClearColorBufferSize); + using var buffer = gd.BufferManager.ReserveOrCreate(gd, cbs, ClearColorBufferSize); - gd.BufferManager.SetData(bufferHandle, 0, clearColor); + gd.BufferManager.SetData(buffer.Handle, buffer.Offset, clearColor); - _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(1, new BufferRange(bufferHandle, 0, ClearColorBufferSize)) }); + _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(1, buffer.Range) }); Span viewports = stackalloc Viewport[1]; @@ -721,8 +717,6 @@ public void Clear( _pipeline.SetPrimitiveTopology(PrimitiveTopology.TriangleStrip); _pipeline.Draw(4, 1, 0, 0); _pipeline.Finish(); - - gd.BufferManager.Delete(bufferHandle); } public void Clear( @@ -745,11 +739,11 @@ public void Clear( _pipeline.SetCommandBuffer(cbs); - var bufferHandle = gd.BufferManager.CreateWithHandle(gd, ClearColorBufferSize); + using var buffer = gd.BufferManager.ReserveOrCreate(gd, cbs, ClearColorBufferSize); - gd.BufferManager.SetData(bufferHandle, 0, stackalloc float[] { depthValue }); + gd.BufferManager.SetData(buffer.Handle, buffer.Offset, stackalloc float[] { depthValue }); - _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(1, new BufferRange(bufferHandle, 0, ClearColorBufferSize)) }); + _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(1, buffer.Range) }); Span viewports = stackalloc Viewport[1]; @@ -771,8 +765,6 @@ public void Clear( _pipeline.SetStencilTest(CreateStencilTestDescriptor(stencilMask != 0, stencilValue, 0xff, stencilMask)); _pipeline.Draw(4, 1, 0, 0); _pipeline.Finish(); - - gd.BufferManager.Delete(bufferHandle); } public void DrawTexture( @@ -878,13 +870,13 @@ public unsafe void ChangeStride(VulkanRenderer gd, CommandBufferScoped cbs, Buff shaderParams[2] = size; shaderParams[3] = srcOffset; - var bufferHandle = gd.BufferManager.CreateWithHandle(gd, ParamsBufferSize); + using var buffer = gd.BufferManager.ReserveOrCreate(gd, cbs, ParamsBufferSize); - gd.BufferManager.SetData(bufferHandle, 0, shaderParams); + gd.BufferManager.SetData(buffer.Handle, buffer.Offset, shaderParams); _pipeline.SetCommandBuffer(cbs); - _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(0, new BufferRange(bufferHandle, 0, ParamsBufferSize)) }); + _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(0, buffer.Range) }); Span> sbRanges = new Auto[2]; @@ -896,8 +888,6 @@ public unsafe void ChangeStride(VulkanRenderer gd, CommandBufferScoped cbs, Buff _pipeline.SetProgram(_programStrideChange); _pipeline.DispatchCompute(1 + elems / ConvertElementsPerWorkgroup, 1, 1); - gd.BufferManager.Delete(bufferHandle); - _pipeline.Finish(gd, cbs); } else @@ -1034,9 +1024,9 @@ public void CopyIncompatibleFormats( shaderParams[0] = BitOperations.Log2((uint)ratio); - var bufferHandle = gd.BufferManager.CreateWithHandle(gd, ParamsBufferSize); + using var buffer = gd.BufferManager.ReserveOrCreate(gd, cbs, ParamsBufferSize); - gd.BufferManager.SetData(bufferHandle, 0, shaderParams); + gd.BufferManager.SetData(buffer.Handle, buffer.Offset, shaderParams); TextureView.InsertImageBarrier( gd.Api, @@ -1064,7 +1054,7 @@ public void CopyIncompatibleFormats( var srcFormat = GetFormat(componentSize, srcBpp / componentSize); var dstFormat = GetFormat(componentSize, dstBpp / componentSize); - _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(0, new BufferRange(bufferHandle, 0, ParamsBufferSize)) }); + _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(0, buffer.Range) }); for (int l = 0; l < levels; l++) { @@ -1093,8 +1083,6 @@ public void CopyIncompatibleFormats( } } - gd.BufferManager.Delete(bufferHandle); - _pipeline.Finish(gd, cbs); TextureView.InsertImageBarrier( @@ -1128,9 +1116,9 @@ public void CopyMSToNonMS(VulkanRenderer gd, CommandBufferScoped cbs, TextureVie (shaderParams[0], shaderParams[1]) = GetSampleCountXYLog2(samples); (shaderParams[2], shaderParams[3]) = GetSampleCountXYLog2((int)TextureStorage.ConvertToSampleCountFlags(gd.Capabilities.SupportedSampleCounts, (uint)samples)); - var bufferHandle = gd.BufferManager.CreateWithHandle(gd, ParamsBufferSize); + using var buffer = gd.BufferManager.ReserveOrCreate(gd, cbs, ParamsBufferSize); - gd.BufferManager.SetData(bufferHandle, 0, shaderParams); + gd.BufferManager.SetData(buffer.Handle, buffer.Offset, shaderParams); TextureView.InsertImageBarrier( gd.Api, @@ -1147,7 +1135,7 @@ public void CopyMSToNonMS(VulkanRenderer gd, CommandBufferScoped cbs, TextureVie 1); _pipeline.SetCommandBuffer(cbs); - _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(0, new BufferRange(bufferHandle, 0, ParamsBufferSize)) }); + _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(0, buffer.Range) }); if (isDepthOrStencil) { @@ -1226,8 +1214,6 @@ public void CopyMSToNonMS(VulkanRenderer gd, CommandBufferScoped cbs, TextureVie } } - gd.BufferManager.Delete(bufferHandle); - _pipeline.Finish(gd, cbs); TextureView.InsertImageBarrier( @@ -1261,9 +1247,9 @@ public void CopyNonMSToMS(VulkanRenderer gd, CommandBufferScoped cbs, TextureVie (shaderParams[0], shaderParams[1]) = GetSampleCountXYLog2(samples); (shaderParams[2], shaderParams[3]) = GetSampleCountXYLog2((int)TextureStorage.ConvertToSampleCountFlags(gd.Capabilities.SupportedSampleCounts, (uint)samples)); - var bufferHandle = gd.BufferManager.CreateWithHandle(gd, ParamsBufferSize); + using var buffer = gd.BufferManager.ReserveOrCreate(gd, cbs, ParamsBufferSize); - gd.BufferManager.SetData(bufferHandle, 0, shaderParams); + gd.BufferManager.SetData(buffer.Handle, buffer.Offset, shaderParams); TextureView.InsertImageBarrier( gd.Api, @@ -1299,7 +1285,7 @@ public void CopyNonMSToMS(VulkanRenderer gd, CommandBufferScoped cbs, TextureVie _pipeline.SetViewports(viewports); _pipeline.SetPrimitiveTopology(PrimitiveTopology.TriangleStrip); - _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(0, new BufferRange(bufferHandle, 0, ParamsBufferSize)) }); + _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(0, buffer.Range) }); if (isDepthOrStencil) { @@ -1364,8 +1350,6 @@ public void CopyNonMSToMS(VulkanRenderer gd, CommandBufferScoped cbs, TextureVie } } - gd.BufferManager.Delete(bufferHandle); - _pipeline.Finish(gd, cbs); TextureView.InsertImageBarrier( @@ -1726,13 +1710,13 @@ public unsafe void ConvertD32S8ToD24S8(VulkanRenderer gd, CommandBufferScoped cb shaderParams[0] = pixelCount; shaderParams[1] = dstOffset; - var bufferHandle = gd.BufferManager.CreateWithHandle(gd, ParamsBufferSize); + using var buffer = gd.BufferManager.ReserveOrCreate(gd, cbs, ParamsBufferSize); - gd.BufferManager.SetData(bufferHandle, 0, shaderParams); + gd.BufferManager.SetData(buffer.Handle, buffer.Offset, shaderParams); _pipeline.SetCommandBuffer(cbs); - _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(0, new BufferRange(bufferHandle, 0, ParamsBufferSize)) }); + _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(0, buffer.Range) }); Span> sbRanges = new Auto[2]; @@ -1744,8 +1728,6 @@ public unsafe void ConvertD32S8ToD24S8(VulkanRenderer gd, CommandBufferScoped cb _pipeline.SetProgram(_programConvertD32S8ToD24S8); _pipeline.DispatchCompute(1 + inSize / ConvertElementsPerWorkgroup, 1, 1); - gd.BufferManager.Delete(bufferHandle); - _pipeline.Finish(gd, cbs); BufferHolder.InsertBufferBarrier( diff --git a/src/Ryujinx.Graphics.Vulkan/StagingBuffer.cs b/src/Ryujinx.Graphics.Vulkan/StagingBuffer.cs index 3a02a28dc..4f3d0137c 100644 --- a/src/Ryujinx.Graphics.Vulkan/StagingBuffer.cs +++ b/src/Ryujinx.Graphics.Vulkan/StagingBuffer.cs @@ -1,5 +1,6 @@ using Ryujinx.Common; using Ryujinx.Common.Logging; +using Ryujinx.Graphics.GAL; using System; using System.Collections.Generic; using System.Diagnostics; @@ -29,6 +30,9 @@ class StagingBuffer : IDisposable private readonly VulkanRenderer _gd; private readonly BufferHolder _buffer; + private readonly int _resourceAlignment; + + public readonly BufferHandle Handle; private readonly struct PendingCopy { @@ -48,9 +52,10 @@ public PendingCopy(FenceHolder fence, int size) public StagingBuffer(VulkanRenderer gd, BufferManager bufferManager) { _gd = gd; - _buffer = bufferManager.Create(gd, BufferSize); + Handle = bufferManager.CreateWithHandle(gd, BufferSize, out _buffer); _pendingCopies = new Queue(); _freeSize = BufferSize; + _resourceAlignment = (int)gd.Capabilities.MinResourceAlignment; } public void PushData(CommandBufferPool cbp, CommandBufferScoped? cbs, Action endRenderPass, BufferHolder dst, int dstOffset, ReadOnlySpan data) @@ -197,16 +202,21 @@ private int GetContiguousFreeSize(int alignment) /// Reserve a range on the staging buffer for the current command buffer and upload data to it. /// /// Command buffer to reserve the data on - /// The data to upload - /// The required alignment for the buffer offset + /// The minimum size the reserved data requires + /// The required alignment for the buffer offset. -1 uses the most permissive alignment /// The reserved range of the staging buffer - public unsafe StagingBufferReserved? TryReserveData(CommandBufferScoped cbs, int size, int alignment) + public unsafe StagingBufferReserved? TryReserveData(CommandBufferScoped cbs, int size, int alignment = -1) { if (size > BufferSize) { return null; } + if (alignment == -1) + { + alignment = _resourceAlignment; + } + // Temporary reserved data cannot be fragmented. if (GetContiguousFreeSize(alignment) < size) @@ -263,7 +273,7 @@ protected virtual void Dispose(bool disposing) { if (disposing) { - _buffer.Dispose(); + _gd.BufferManager.Delete(Handle); while (_pendingCopies.TryDequeue(out var pc)) { From 1b17ecc2d2ea26d426f793b234169427527887ac Mon Sep 17 00:00:00 2001 From: riperiperi Date: Tue, 23 Jan 2024 20:16:31 +0000 Subject: [PATCH 42/43] Use temporary buffer for ConvertIndexBufferIndirect --- src/Ryujinx.Graphics.Vulkan/BufferManager.cs | 10 ++++++++-- src/Ryujinx.Graphics.Vulkan/HelperShader.cs | 17 ++++++++--------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/Ryujinx.Graphics.Vulkan/BufferManager.cs b/src/Ryujinx.Graphics.Vulkan/BufferManager.cs index 1ed295ce8..6774f0f6c 100644 --- a/src/Ryujinx.Graphics.Vulkan/BufferManager.cs +++ b/src/Ryujinx.Graphics.Vulkan/BufferManager.cs @@ -266,23 +266,29 @@ public BufferHandle CreateWithHandle( return Unsafe.As(ref handle64); } - public ScopedTemporaryBuffer ReserveOrCreate(VulkanRenderer gd, CommandBufferScoped cbs, int size, int alignment = -1) + public ScopedTemporaryBuffer ReserveOrCreate(VulkanRenderer gd, CommandBufferScoped cbs, int size, out BufferHolder holder, int alignment = -1) { StagingBufferReserved? result = StagingBuffer.TryReserveData(cbs, size, alignment); if (result.HasValue) { + holder = result.Value.Buffer; return new ScopedTemporaryBuffer(this, StagingBuffer.Handle, result.Value.Offset, result.Value.Size, true); } else { // Create a temporary buffer. - BufferHandle handle = CreateWithHandle(gd, size); + BufferHandle handle = CreateWithHandle(gd, size, out holder); return new ScopedTemporaryBuffer(this, handle, 0, size, false); } } + public ScopedTemporaryBuffer ReserveOrCreate(VulkanRenderer gd, CommandBufferScoped cbs, int size, int alignment = -1) + { + return ReserveOrCreate(gd, cbs, size, out _, alignment); + } + public unsafe MemoryRequirements GetHostImportedUsageRequirements(VulkanRenderer gd) { var usage = HostImportedBufferUsageFlags; diff --git a/src/Ryujinx.Graphics.Vulkan/HelperShader.cs b/src/Ryujinx.Graphics.Vulkan/HelperShader.cs index 247556db9..ffe537f38 100644 --- a/src/Ryujinx.Graphics.Vulkan/HelperShader.cs +++ b/src/Ryujinx.Graphics.Vulkan/HelperShader.cs @@ -1600,10 +1600,10 @@ public void ConvertIndexBufferIndirect( pattern.OffsetIndex.CopyTo(shaderParams[..pattern.OffsetIndex.Length]); - var patternBufferHandle = gd.BufferManager.CreateWithHandle(gd, ParamsBufferSize, out var patternBuffer); + using var patternScoped = gd.BufferManager.ReserveOrCreate(gd, cbs, ParamsBufferSize, out var patternBuffer); var patternBufferAuto = patternBuffer.GetBuffer(); - gd.BufferManager.SetData(patternBufferHandle, 0, shaderParams); + gd.BufferManager.SetData(patternScoped.Handle, patternScoped.Offset, shaderParams); _pipeline.SetCommandBuffer(cbs); @@ -1619,7 +1619,8 @@ public void ConvertIndexBufferIndirect( indirectDataSize); _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(0, drawCountBufferAligned) }); - _pipeline.SetStorageBuffers(1, new[] { srcIndirectBuffer.GetBuffer(), dstIndirectBuffer.GetBuffer(), patternBuffer.GetBuffer() }); + _pipeline.SetStorageBuffers(1, new[] { srcIndirectBuffer.GetBuffer(), dstIndirectBuffer.GetBuffer() }); + _pipeline.SetStorageBuffers(stackalloc[] { new BufferAssignment(3, patternScoped.Range) }); _pipeline.SetProgram(_programConvertIndirectData); _pipeline.DispatchCompute(1, 1, 1); @@ -1627,12 +1628,12 @@ public void ConvertIndexBufferIndirect( BufferHolder.InsertBufferBarrier( gd, cbs.CommandBuffer, - patternBufferAuto.Get(cbs, ParamsIndirectDispatchOffset, ParamsIndirectDispatchSize).Value, + patternBufferAuto.Get(cbs, patternScoped.Offset + ParamsIndirectDispatchOffset, ParamsIndirectDispatchSize).Value, AccessFlags.ShaderWriteBit, AccessFlags.IndirectCommandReadBit, PipelineStageFlags.ComputeShaderBit, PipelineStageFlags.DrawIndirectBit, - ParamsIndirectDispatchOffset, + patternScoped.Offset + ParamsIndirectDispatchOffset, ParamsIndirectDispatchSize); BufferHolder.InsertBufferBarrier( @@ -1646,11 +1647,11 @@ public void ConvertIndexBufferIndirect( 0, convertedCount * outputIndexSize); - _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(0, new BufferRange(patternBufferHandle, 0, ParamsBufferSize)) }); + _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(0, new BufferRange(patternScoped.Handle, patternScoped.Offset, ParamsBufferSize)) }); _pipeline.SetStorageBuffers(1, new[] { srcIndexBuffer.GetBuffer(), dstIndexBuffer.GetBuffer() }); _pipeline.SetProgram(_programConvertIndexBuffer); - _pipeline.DispatchComputeIndirect(patternBufferAuto, ParamsIndirectDispatchOffset); + _pipeline.DispatchComputeIndirect(patternBufferAuto, patternScoped.Offset + ParamsIndirectDispatchOffset); BufferHolder.InsertBufferBarrier( gd, @@ -1663,8 +1664,6 @@ public void ConvertIndexBufferIndirect( 0, convertedCount * outputIndexSize); - gd.BufferManager.Delete(patternBufferHandle); - _pipeline.Finish(gd, cbs); } From f2b895fba38a75e82240b79d566c6373d5d4466e Mon Sep 17 00:00:00 2001 From: riperiperi Date: Wed, 24 Jan 2024 00:39:01 +0000 Subject: [PATCH 43/43] Simplify alignment passing for now --- src/Ryujinx.Graphics.Vulkan/BufferManager.cs | 8 ++++---- src/Ryujinx.Graphics.Vulkan/StagingBuffer.cs | 21 +++++++++++++------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/Ryujinx.Graphics.Vulkan/BufferManager.cs b/src/Ryujinx.Graphics.Vulkan/BufferManager.cs index 6774f0f6c..97acfff8a 100644 --- a/src/Ryujinx.Graphics.Vulkan/BufferManager.cs +++ b/src/Ryujinx.Graphics.Vulkan/BufferManager.cs @@ -266,9 +266,9 @@ public BufferHandle CreateWithHandle( return Unsafe.As(ref handle64); } - public ScopedTemporaryBuffer ReserveOrCreate(VulkanRenderer gd, CommandBufferScoped cbs, int size, out BufferHolder holder, int alignment = -1) + public ScopedTemporaryBuffer ReserveOrCreate(VulkanRenderer gd, CommandBufferScoped cbs, int size, out BufferHolder holder) { - StagingBufferReserved? result = StagingBuffer.TryReserveData(cbs, size, alignment); + StagingBufferReserved? result = StagingBuffer.TryReserveData(cbs, size); if (result.HasValue) { @@ -284,9 +284,9 @@ public ScopedTemporaryBuffer ReserveOrCreate(VulkanRenderer gd, CommandBufferSco } } - public ScopedTemporaryBuffer ReserveOrCreate(VulkanRenderer gd, CommandBufferScoped cbs, int size, int alignment = -1) + public ScopedTemporaryBuffer ReserveOrCreate(VulkanRenderer gd, CommandBufferScoped cbs, int size) { - return ReserveOrCreate(gd, cbs, size, out _, alignment); + return ReserveOrCreate(gd, cbs, size, out _); } public unsafe MemoryRequirements GetHostImportedUsageRequirements(VulkanRenderer gd) diff --git a/src/Ryujinx.Graphics.Vulkan/StagingBuffer.cs b/src/Ryujinx.Graphics.Vulkan/StagingBuffer.cs index 4f3d0137c..90a47bb67 100644 --- a/src/Ryujinx.Graphics.Vulkan/StagingBuffer.cs +++ b/src/Ryujinx.Graphics.Vulkan/StagingBuffer.cs @@ -203,20 +203,15 @@ private int GetContiguousFreeSize(int alignment) /// /// Command buffer to reserve the data on /// The minimum size the reserved data requires - /// The required alignment for the buffer offset. -1 uses the most permissive alignment + /// The required alignment for the buffer offset /// The reserved range of the staging buffer - public unsafe StagingBufferReserved? TryReserveData(CommandBufferScoped cbs, int size, int alignment = -1) + public unsafe StagingBufferReserved? TryReserveData(CommandBufferScoped cbs, int size, int alignment) { if (size > BufferSize) { return null; } - if (alignment == -1) - { - alignment = _resourceAlignment; - } - // Temporary reserved data cannot be fragmented. if (GetContiguousFreeSize(alignment) < size) @@ -233,6 +228,18 @@ private int GetContiguousFreeSize(int alignment) return ReserveDataImpl(cbs, size, alignment); } + /// + /// Reserve a range on the staging buffer for the current command buffer and upload data to it. + /// Uses the most permissive byte alignment. + /// + /// Command buffer to reserve the data on + /// The minimum size the reserved data requires + /// The reserved range of the staging buffer + public unsafe StagingBufferReserved? TryReserveData(CommandBufferScoped cbs, int size) + { + return TryReserveData(cbs, size, _resourceAlignment); + } + private bool WaitFreeCompleted(CommandBufferPool cbp) { if (_pendingCopies.TryPeek(out var pc))