diff --git a/src/Ryujinx.Common/Configuration/DirtyHacks.cs b/src/Ryujinx.Common/Configuration/DirtyHacks.cs new file mode 100644 index 0000000000..6a6d4949c4 --- /dev/null +++ b/src/Ryujinx.Common/Configuration/DirtyHacks.cs @@ -0,0 +1,11 @@ +using System; + +namespace Ryujinx.Common.Configuration +{ + [Flags] + public enum DirtyHacks + { + None = 0, + Xc2MenuSoftlockFix = 1 << 10 + } +} diff --git a/src/Ryujinx.Common/TitleIDs.cs b/src/Ryujinx.Common/TitleIDs.cs index b75ee1299b..2d4068e4e8 100644 --- a/src/Ryujinx.Common/TitleIDs.cs +++ b/src/Ryujinx.Common/TitleIDs.cs @@ -8,6 +8,8 @@ namespace Ryujinx.Common { public static class TitleIDs { + public static Optional CurrentApplication; + public static GraphicsBackend SelectGraphicsBackend(string titleId, GraphicsBackend currentBackend) { switch (currentBackend) diff --git a/src/Ryujinx.Graphics.Gpu/GraphicsConfig.cs b/src/Ryujinx.Graphics.Gpu/GraphicsConfig.cs index fbb7399cac..6b2a57ad92 100644 --- a/src/Ryujinx.Graphics.Gpu/GraphicsConfig.cs +++ b/src/Ryujinx.Graphics.Gpu/GraphicsConfig.cs @@ -47,12 +47,6 @@ public static class GraphicsConfig /// public static bool EnableMacroHLE = true; - /// - /// Title id of the current running game. - /// Used by the shader cache. - /// - public static string TitleId; - /// /// Enables or disables the shader cache. /// diff --git a/src/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs b/src/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs index 0924c60f8c..2f4d98b9b3 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs @@ -1,3 +1,4 @@ +using Ryujinx.Common; using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using Ryujinx.Graphics.GAL; @@ -116,8 +117,8 @@ public ShaderCache(GpuContext context) /// private static string GetDiskCachePath() { - return GraphicsConfig.EnableShaderCache && GraphicsConfig.TitleId != null - ? Path.Combine(AppDataManager.GamesDirPath, GraphicsConfig.TitleId, "cache", "shader") + return GraphicsConfig.EnableShaderCache && TitleIDs.CurrentApplication.HasValue + ? Path.Combine(AppDataManager.GamesDirPath, TitleIDs.CurrentApplication, "cache", "shader") : null; } diff --git a/src/Ryujinx.HLE/HLEConfiguration.cs b/src/Ryujinx.HLE/HLEConfiguration.cs index 52c2b3da47..3bfab0be47 100644 --- a/src/Ryujinx.HLE/HLEConfiguration.cs +++ b/src/Ryujinx.HLE/HLEConfiguration.cs @@ -188,6 +188,11 @@ public class HLEConfiguration /// An action called when HLE force a refresh of output after docked mode changed. /// public Action RefreshInputConfig { internal get; set; } + + /** + * The desired hacky workarounds. + */ + public DirtyHacks Hacks { internal get; set; } public HLEConfiguration(VirtualFileSystem virtualFileSystem, LibHacHorizonManager libHacHorizonManager, @@ -218,7 +223,8 @@ public HLEConfiguration(VirtualFileSystem virtualFileSystem, bool multiplayerDisableP2p, string multiplayerLdnPassphrase, string multiplayerLdnServer, - int customVSyncInterval) + int customVSyncInterval, + DirtyHacks dirtyHacks = DirtyHacks.None) { VirtualFileSystem = virtualFileSystem; LibHacHorizonManager = libHacHorizonManager; @@ -250,6 +256,7 @@ public HLEConfiguration(VirtualFileSystem virtualFileSystem, MultiplayerDisableP2p = multiplayerDisableP2p; MultiplayerLdnPassphrase = multiplayerLdnPassphrase; MultiplayerLdnServer = multiplayerLdnServer; + Hacks = dirtyHacks; } } } diff --git a/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/IStorage.cs b/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/IStorage.cs index 4299a6c74b..6b542c16a3 100644 --- a/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/IStorage.cs +++ b/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/IStorage.cs @@ -1,6 +1,9 @@ using LibHac; using LibHac.Common; using LibHac.Sf; +using Ryujinx.Common; +using Ryujinx.Common.Configuration; +using System.Threading; namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy { @@ -13,6 +16,8 @@ public IStorage(ref SharedRef baseStorage) _baseStorage = SharedRef.CreateMove(ref baseStorage); } + private const string Xc2TitleId = "0100e95004038000"; + [CommandCmif(0)] // Read(u64 offset, u64 length) -> buffer buffer public ResultCode Read(ServiceCtx context) @@ -33,6 +38,13 @@ public ResultCode Read(ServiceCtx context) using var region = context.Memory.GetWritableRegion(bufferAddress, (int)bufferLen, true); Result result = _baseStorage.Get.Read((long)offset, new OutBuffer(region.Memory.Span), (long)size); + + if (context.Device.DirtyHacks.HasFlag(DirtyHacks.Xc2MenuSoftlockFix) && TitleIDs.CurrentApplication == Xc2TitleId) + { + // Add a load-bearing sleep to avoid XC2 softlock + // https://web.archive.org/web/20240728045136/https://github.com/Ryujinx/Ryujinx/issues/2357 + Thread.Sleep(2); + } return (ResultCode)result.Value; } diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/FileSystemExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/FileSystemExtensions.cs index 97284f3bbd..e4a5a547d9 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/FileSystemExtensions.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/FileSystemExtensions.cs @@ -4,6 +4,7 @@ using LibHac.Loader; using LibHac.Ns; using LibHac.Tools.FsSystem; +using Ryujinx.Common; using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using Ryujinx.HLE.Loaders.Executables; @@ -102,7 +103,7 @@ public static ProcessResult Load(this IFileSystem exeFs, Switch device, BlitStru } // Initialize GPU. - Graphics.Gpu.GraphicsConfig.TitleId = programId.ToString("X16"); + TitleIDs.CurrentApplication = programId.ToString("X16"); device.Gpu.HostInitalized.Set(); if (!MemoryBlock.SupportsFlags(MemoryAllocationFlags.ViewCompatible)) diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs index fe8360f04e..3e19292048 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs @@ -6,6 +6,7 @@ using LibHac.Tools.Fs; using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.NcaUtils; +using Ryujinx.Common; using Ryujinx.Common.Logging; using Ryujinx.HLE.Loaders.Executables; using Ryujinx.HLE.Loaders.Processes.Extensions; @@ -204,7 +205,7 @@ public bool LoadNxo(string path) } // Explicitly null TitleId to disable the shader cache. - Graphics.Gpu.GraphicsConfig.TitleId = null; + TitleIDs.CurrentApplication = default; _device.Gpu.HostInitalized.Set(); ProcessResult processResult = ProcessLoaderHelper.LoadNsos(_device, diff --git a/src/Ryujinx.HLE/Switch.cs b/src/Ryujinx.HLE/Switch.cs index d0afdf1733..e0f94274ca 100644 --- a/src/Ryujinx.HLE/Switch.cs +++ b/src/Ryujinx.HLE/Switch.cs @@ -37,6 +37,8 @@ public class Switch : IDisposable public bool IsFrameAvailable => Gpu.Window.IsFrameAvailable; + public DirtyHacks DirtyHacks { get; } + public Switch(HLEConfiguration configuration) { ArgumentNullException.ThrowIfNull(configuration.GpuRenderer); @@ -72,6 +74,7 @@ public Switch(HLEConfiguration configuration) System.EnablePtc = Configuration.EnablePtc; System.FsIntegrityCheckLevel = Configuration.FsIntegrityCheckLevel; System.GlobalAccessLogMode = Configuration.FsGlobalAccessLogMode; + DirtyHacks = Configuration.Hacks; UpdateVSyncInterval(); #pragma warning restore IDE0055 } diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs index 027e1052b0..f5ce265ee8 100644 --- a/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs @@ -17,7 +17,7 @@ public class ConfigurationFileFormat /// /// The current version of the file format /// - public const int CurrentVersion = 57; + public const int CurrentVersion = 58; /// /// Version of the configuration file format @@ -429,7 +429,17 @@ public class ConfigurationFileFormat /// Uses Hypervisor over JIT if available /// public bool UseHypervisor { get; set; } - + + /** + * Show toggles for dirty hacks in the UI. + */ + public bool ShowDirtyHacks { get; set; } + + /** + * The packed value of the enabled dirty hacks. + */ + public int EnabledDirtyHacks { get; set; } + /// /// Loads a configuration file from disk /// diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Migration.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Migration.cs index a41ea2cd73..8652b4331a 100644 --- a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Migration.cs +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Migration.cs @@ -735,6 +735,9 @@ public void Load(ConfigurationFileFormat configurationFileFormat, string configu Multiplayer.DisableP2p.Value = configurationFileFormat.MultiplayerDisableP2p; Multiplayer.LdnPassphrase.Value = configurationFileFormat.MultiplayerLdnPassphrase; Multiplayer.LdnServer.Value = configurationFileFormat.LdnServer; + + Hacks.ShowDirtyHacks.Value = configurationFileFormat.ShowDirtyHacks; + Hacks.Xc2MenuSoftlockFix.Value = ((DirtyHacks)configurationFileFormat.EnabledDirtyHacks).HasFlag(DirtyHacks.Xc2MenuSoftlockFix); if (configurationFileUpdated) { diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Model.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Model.cs index f28ce0348c..c07ff3d71c 100644 --- a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Model.cs +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Model.cs @@ -1,4 +1,5 @@ using ARMeilleure; +using Gommon; using Ryujinx.Common; using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration.Hid; @@ -617,6 +618,49 @@ public MultiplayerSection() } } + public class HacksSection + { + /// + /// Show toggles for dirty hacks in the UI. + /// + public ReactiveObject ShowDirtyHacks { get; private set; } + + public ReactiveObject Xc2MenuSoftlockFix { get; private set; } + + public HacksSection() + { + ShowDirtyHacks = new ReactiveObject(); + Xc2MenuSoftlockFix = new ReactiveObject(); + Xc2MenuSoftlockFix.Event += HackChanged; + } + + private void HackChanged(object sender, ReactiveEventArgs rxe) + { + Ryujinx.Common.Logging.Logger.Info?.Print(LogClass.Configuration, $"EnabledDirtyHacks set to: {EnabledHacks}", "LogValueChange"); + } + + public DirtyHacks EnabledHacks + { + get + { + DirtyHacks dirtyHacks = DirtyHacks.None; + + if (Xc2MenuSoftlockFix) + Apply(DirtyHacks.Xc2MenuSoftlockFix); + + return dirtyHacks; + + void Apply(DirtyHacks hack) + { + if (dirtyHacks is not DirtyHacks.None) + dirtyHacks |= hack; + else + dirtyHacks = hack; + } + } + } + } + /// /// The default configuration instance /// @@ -651,6 +695,11 @@ public MultiplayerSection() /// The Multiplayer section /// public MultiplayerSection Multiplayer { get; private set; } + + /** + * The Dirty Hacks section + */ + public HacksSection Hacks { get; private set; } /// /// Enables or disables Discord Rich Presence @@ -700,6 +749,7 @@ private ConfigurationState() Graphics = new GraphicsSection(); Hid = new HidSection(); Multiplayer = new MultiplayerSection(); + Hacks = new HacksSection(); EnableDiscordIntegration = new ReactiveObject(); CheckUpdatesOnStart = new ReactiveObject(); ShowConfirmExit = new ReactiveObject(); diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs index 90bdc34091..8ae76ecc51 100644 --- a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs @@ -138,6 +138,8 @@ public ConfigurationFileFormat ToFileFormat() MultiplayerDisableP2p = Multiplayer.DisableP2p, MultiplayerLdnPassphrase = Multiplayer.LdnPassphrase, LdnServer = Multiplayer.LdnServer, + ShowDirtyHacks = Hacks.ShowDirtyHacks, + EnabledDirtyHacks = (int)Hacks.EnabledHacks, }; return configurationFile; diff --git a/src/Ryujinx/AppHost.cs b/src/Ryujinx/AppHost.cs index 909eb05d50..0f8b8030c9 100644 --- a/src/Ryujinx/AppHost.cs +++ b/src/Ryujinx/AppHost.cs @@ -311,6 +311,7 @@ public void UpdateVSyncMode(object sender, ReactiveEventArgs e) Device.VSyncMode = e.NewValue; Device.UpdateVSyncInterval(); } + _renderer.Window?.ChangeVSyncMode(e.NewValue); _viewModel.ShowCustomVSyncIntervalPicker = (e.NewValue == VSyncMode.Custom); @@ -923,7 +924,7 @@ private void InitializeSwitchInstance() // Initialize Configuration. var memoryConfiguration = ConfigurationState.Instance.System.DramSize.Value; - Device = new HLE.Switch(new HLEConfiguration( + Device = new Switch(new HLEConfiguration( VirtualFileSystem, _viewModel.LibHacHorizonManager, ContentManager, @@ -953,7 +954,8 @@ private void InitializeSwitchInstance() ConfigurationState.Instance.Multiplayer.DisableP2p, ConfigurationState.Instance.Multiplayer.LdnPassphrase, ConfigurationState.Instance.Multiplayer.LdnServer, - ConfigurationState.Instance.Graphics.CustomVSyncInterval.Value)); + ConfigurationState.Instance.Graphics.CustomVSyncInterval.Value, + ConfigurationState.Instance.Hacks.ShowDirtyHacks ? ConfigurationState.Instance.Hacks.EnabledHacks : DirtyHacks.None)); } private static IHardwareDeviceDriver InitializeAudio() diff --git a/src/Ryujinx/Ryujinx.csproj b/src/Ryujinx/Ryujinx.csproj index d98e499c28..5e5adf2a09 100644 --- a/src/Ryujinx/Ryujinx.csproj +++ b/src/Ryujinx/Ryujinx.csproj @@ -139,4 +139,10 @@ + + + SettingsHacksView.axaml + Code + + \ No newline at end of file diff --git a/src/Ryujinx/UI/ViewModels/BaseModel.cs b/src/Ryujinx/UI/ViewModels/BaseModel.cs index d8f2e90965..e27c528678 100644 --- a/src/Ryujinx/UI/ViewModels/BaseModel.cs +++ b/src/Ryujinx/UI/ViewModels/BaseModel.cs @@ -13,8 +13,9 @@ protected void OnPropertyChanged([CallerMemberName] string propertyName = null) PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } - protected void OnPropertiesChanged(params ReadOnlySpan propertyNames) + protected void OnPropertiesChanged(string firstPropertyName, params ReadOnlySpan propertyNames) { + OnPropertyChanged(firstPropertyName); foreach (var propertyName in propertyNames) { OnPropertyChanged(propertyName); diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs index 9feaaba9b4..aa4b994ef0 100644 --- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs @@ -62,7 +62,9 @@ public partial class SettingsViewModel : BaseModel private int _networkInterfaceIndex; private int _multiplayerModeIndex; private string _ldnPassphrase; - private string _LdnServer; + private string _ldnServer; + + private bool _xc2MenuSoftlockFix = ConfigurationState.Instance.Hacks.Xc2MenuSoftlockFix; public int ResolutionScale { @@ -162,9 +164,7 @@ public VSyncMode VSyncMode get => _vSyncMode; set { - if (value == VSyncMode.Custom || - value == VSyncMode.Switch || - value == VSyncMode.Unbounded) + if (value is VSyncMode.Custom or VSyncMode.Switch or VSyncMode.Unbounded) { _vSyncMode = value; OnPropertyChanged(); @@ -258,6 +258,8 @@ public int CustomVSyncInterval public bool UseHypervisor { get; set; } public bool DisableP2P { get; set; } + public bool ShowDirtyHacks => ConfigurationState.Instance.Hacks.ShowDirtyHacks; + public string TimeZone { get; set; } public string ShaderDumpPath { get; set; } @@ -274,6 +276,17 @@ public string LdnPassphrase } } + public bool Xc2MenuSoftlockFixEnabled + { + get => _xc2MenuSoftlockFix; + set + { + _xc2MenuSoftlockFix = value; + + OnPropertyChanged(); + } + } + public int Language { get; set; } public int Region { get; set; } public int FsGlobalAccessLogMode { get; set; } @@ -374,10 +387,10 @@ public int MultiplayerModeIndex public string LdnServer { - get => _LdnServer; + get => _ldnServer; set { - _LdnServer = value; + _ldnServer = value; OnPropertyChanged(); } } @@ -746,6 +759,9 @@ public void SaveSettings() config.Multiplayer.DisableP2p.Value = DisableP2P; config.Multiplayer.LdnPassphrase.Value = LdnPassphrase; config.Multiplayer.LdnServer.Value = LdnServer; + + // Dirty Hacks + config.Hacks.Xc2MenuSoftlockFix.Value = Xc2MenuSoftlockFixEnabled; config.ToFileFormat().SaveConfig(Program.ConfigurationPath); @@ -779,5 +795,8 @@ public void CancelButton() RevertIfNotSaved(); CloseWindow?.Invoke(); } + + public static string Xc2MenuFixTooltip => + "From the issue on GitHub:\n\nWhen clicking very fast from game main menu to 2nd submenu, there is a low chance that the game will softlock, the submenu won't show up, while background music is still there."; } } diff --git a/src/Ryujinx/UI/Views/Settings/SettingsHacksView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsHacksView.axaml new file mode 100644 index 0000000000..b7817f0647 --- /dev/null +++ b/src/Ryujinx/UI/Views/Settings/SettingsHacksView.axaml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Views/Settings/SettingsHacksView.axaml.cs b/src/Ryujinx/UI/Views/Settings/SettingsHacksView.axaml.cs new file mode 100644 index 0000000000..f9e0958ca0 --- /dev/null +++ b/src/Ryujinx/UI/Views/Settings/SettingsHacksView.axaml.cs @@ -0,0 +1,17 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.UI.Common.Configuration; + +namespace Ryujinx.Ava.UI.Views.Settings +{ + public partial class SettingsHacksView : UserControl + { + public SettingsViewModel ViewModel; + + public SettingsHacksView() + { + InitializeComponent(); + } + } +} diff --git a/src/Ryujinx/UI/Windows/SettingsWindow.axaml b/src/Ryujinx/UI/Windows/SettingsWindow.axaml index 2bf5b55e79..59302b6fc8 100644 --- a/src/Ryujinx/UI/Windows/SettingsWindow.axaml +++ b/src/Ryujinx/UI/Windows/SettingsWindow.axaml @@ -37,6 +37,7 @@ + +