diff --git a/src/Ryujinx.Common/Configuration/DirtyHacks.cs b/src/Ryujinx.Common/Configuration/DirtyHacks.cs new file mode 100644 index 000000000..6a6d4949c --- /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 b75ee1299..82a08b24e 100644 --- a/src/Ryujinx.Common/TitleIDs.cs +++ b/src/Ryujinx.Common/TitleIDs.cs @@ -8,6 +8,21 @@ namespace Ryujinx.Common { public static class TitleIDs { + private static string _currentApplication; + + public static Optional CurrentApplication + { + get => _currentApplication; + set + { + _currentApplication = value.OrElse(null); + + CurrentApplicationChanged?.Invoke(_currentApplication); + } + } + + public static event Action> CurrentApplicationChanged; + public static GraphicsBackend SelectGraphicsBackend(string titleId, GraphicsBackend currentBackend) { switch (currentBackend) @@ -33,6 +48,7 @@ namespace Ryujinx.Common "010028600EBDA000", // Mario 3D World "0100152000022000", // Mario Kart 8 Deluxe "01005CA01580E000", // Persona 5 + "01001f5010dfa000", // Pokemon Legends Arceus "01008C0016544000", // Sea of Stars "01006A800016E000", // Smash Ultimate "0100000000010000", // Super Mario Odyessy diff --git a/src/Ryujinx.Graphics.Gpu/GraphicsConfig.cs b/src/Ryujinx.Graphics.Gpu/GraphicsConfig.cs index fbb7399ca..066ac28f7 100644 --- a/src/Ryujinx.Graphics.Gpu/GraphicsConfig.cs +++ b/src/Ryujinx.Graphics.Gpu/GraphicsConfig.cs @@ -46,7 +46,7 @@ namespace Ryujinx.Graphics.Gpu /// Enables or disables high-level emulation of common GPU Macro code. /// public static bool EnableMacroHLE = true; - + /// /// Title id of the current running game. /// Used by the shader cache. diff --git a/src/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs b/src/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs index 0924c60f8..3a02eb615 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; diff --git a/src/Ryujinx.Graphics.Metal/Window.cs b/src/Ryujinx.Graphics.Metal/Window.cs index 7d9a51a9e..1823c0b9a 100644 --- a/src/Ryujinx.Graphics.Metal/Window.cs +++ b/src/Ryujinx.Graphics.Metal/Window.cs @@ -22,13 +22,15 @@ namespace Ryujinx.Graphics.Metal private int _requestedWidth; private int _requestedHeight; - - // private bool _vsyncEnabled; + private AntiAliasing _currentAntiAliasing; private bool _updateEffect; private IPostProcessingEffect _effect; private IScalingFilter _scalingFilter; private bool _isLinear; + + public bool IsVSyncEnabled => _metalLayer.DisplaySyncEnabled; + // private float _scalingFilterLevel; private bool _updateScalingFilter; private ScalingFilter _currentScalingFilter; @@ -40,7 +42,7 @@ namespace Ryujinx.Graphics.Metal _metalLayer = metalLayer; } - private unsafe void ResizeIfNeeded() + private void ResizeIfNeeded() { if (_requestedWidth != 0 && _requestedHeight != 0) { @@ -54,7 +56,7 @@ namespace Ryujinx.Graphics.Metal } } - public unsafe void Present(ITexture texture, ImageCrop crop, Action swapBuffersCallback) + public void Present(ITexture texture, ImageCrop crop, Action swapBuffersCallback) { if (_renderer.Pipeline is Pipeline pipeline && texture is Texture tex) { @@ -141,15 +143,7 @@ namespace Ryujinx.Graphics.Metal public void ChangeVSyncMode(VSyncMode vSyncMode) { - switch (vSyncMode) - { - case VSyncMode.Unbounded: - _metalLayer.DisplaySyncEnabled = false; - break; - case VSyncMode.Switch: - _metalLayer.DisplaySyncEnabled = true; - break; - } + _metalLayer.DisplaySyncEnabled = vSyncMode is VSyncMode.Switch; } public void SetAntiAliasing(AntiAliasing effect) diff --git a/src/Ryujinx.Graphics.Nvdec.Vp9/BitDepth.cs b/src/Ryujinx.Graphics.Nvdec.Vp9/BitDepth.cs index a43c83580..fdf0aff9c 100644 --- a/src/Ryujinx.Graphics.Nvdec.Vp9/BitDepth.cs +++ b/src/Ryujinx.Graphics.Nvdec.Vp9/BitDepth.cs @@ -2,8 +2,8 @@ namespace Ryujinx.Graphics.Nvdec.Vp9 { internal enum BitDepth { - Bits8 = 8, /**< 8 bits */ - Bits10 = 10, /**< 10 bits */ - Bits12 = 12, /**< 12 bits */ + Bits8 = 8, // < 8 bits + Bits10 = 10, // < 10 bits + Bits12 = 12, // < 12 bits } } diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs index ad4b18e50..a4fcf5353 100644 --- a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs +++ b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs @@ -1,3 +1,4 @@ +using Gommon; using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using Ryujinx.Graphics.GAL; @@ -890,7 +891,12 @@ namespace Ryujinx.Graphics.Vulkan private void PrintGpuInformation() { - Logger.Notice.Print(LogClass.Gpu, $"{GpuVendor} {GpuRenderer} ({GpuVersion})"); + string gpuInfoMessage = $"{GpuRenderer} ({GpuVersion})"; + if (!GpuRenderer.StartsWithIgnoreCase(GpuVendor)) + gpuInfoMessage = gpuInfoMessage.Prepend(GpuVendor); + + Logger.Notice.Print(LogClass.Gpu, gpuInfoMessage); + Logger.Notice.Print(LogClass.Gpu, $"GPU Memory: {GetTotalGPUMemory() / (1024 * 1024)} MiB"); } diff --git a/src/Ryujinx.HLE/HLEConfiguration.cs b/src/Ryujinx.HLE/HLEConfiguration.cs index 52c2b3da4..b44a09b22 100644 --- a/src/Ryujinx.HLE/HLEConfiguration.cs +++ b/src/Ryujinx.HLE/HLEConfiguration.cs @@ -188,6 +188,11 @@ namespace Ryujinx.HLE /// 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 @@ namespace Ryujinx.HLE bool multiplayerDisableP2p, string multiplayerLdnPassphrase, string multiplayerLdnServer, - int customVSyncInterval) + int customVSyncInterval, + DirtyHacks dirtyHacks = DirtyHacks.None) { VirtualFileSystem = virtualFileSystem; LibHacHorizonManager = libHacHorizonManager; @@ -250,6 +256,7 @@ namespace Ryujinx.HLE 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 4299a6c74..6b542c16a 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 @@ namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy _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 @@ namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy 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 97284f3bb..d1d13b00f 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/FileSystemExtensions.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/FileSystemExtensions.cs @@ -4,8 +4,10 @@ using LibHac.Fs.Fsa; using LibHac.Loader; using LibHac.Ns; using LibHac.Tools.FsSystem; +using Ryujinx.Common; using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; +using Ryujinx.Graphics.Gpu; using Ryujinx.HLE.Loaders.Executables; using Ryujinx.Memory; using System; @@ -102,7 +104,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions } // Initialize GPU. - Graphics.Gpu.GraphicsConfig.TitleId = programId.ToString("X16"); + GraphicsConfig.TitleId = 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 fe8360f04..1b90f2707 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs @@ -6,7 +6,9 @@ using LibHac.Ns; using LibHac.Tools.Fs; using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.NcaUtils; +using Ryujinx.Common; using Ryujinx.Common.Logging; +using Ryujinx.Graphics.Gpu; using Ryujinx.HLE.Loaders.Executables; using Ryujinx.HLE.Loaders.Processes.Extensions; using System; @@ -59,6 +61,8 @@ namespace Ryujinx.HLE.Loaders.Processes { _latestPid = processResult.ProcessId; + TitleIDs.CurrentApplication = processResult.ProgramIdText; + return true; } } @@ -86,6 +90,8 @@ namespace Ryujinx.HLE.Loaders.Processes { _latestPid = processResult.ProcessId; + TitleIDs.CurrentApplication = processResult.ProgramIdText; + return true; } } @@ -113,6 +119,8 @@ namespace Ryujinx.HLE.Loaders.Processes if (processResult.ProgramId > 0x01000000000007FF) { _latestPid = processResult.ProcessId; + + TitleIDs.CurrentApplication = processResult.ProgramIdText; } return true; @@ -132,6 +140,8 @@ namespace Ryujinx.HLE.Loaders.Processes { _latestPid = processResult.ProcessId; + TitleIDs.CurrentApplication = processResult.ProgramIdText; + return true; } } @@ -183,14 +193,17 @@ namespace Ryujinx.HLE.Loaders.Processes if (nacpData.Value.PresenceGroupId != 0) { programId = nacpData.Value.PresenceGroupId; + TitleIDs.CurrentApplication = programId.ToString("X16"); } else if (nacpData.Value.SaveDataOwnerId != 0) { programId = nacpData.Value.SaveDataOwnerId; + TitleIDs.CurrentApplication = programId.ToString("X16"); } else if (nacpData.Value.AddOnContentBaseId != 0) { programId = nacpData.Value.AddOnContentBaseId - 0x1000; + TitleIDs.CurrentApplication = programId.ToString("X16"); } } @@ -204,7 +217,7 @@ namespace Ryujinx.HLE.Loaders.Processes } // Explicitly null TitleId to disable the shader cache. - Graphics.Gpu.GraphicsConfig.TitleId = null; + GraphicsConfig.TitleId = null; _device.Gpu.HostInitalized.Set(); ProcessResult processResult = ProcessLoaderHelper.LoadNsos(_device, diff --git a/src/Ryujinx.HLE/Switch.cs b/src/Ryujinx.HLE/Switch.cs index d0afdf173..f73776eda 100644 --- a/src/Ryujinx.HLE/Switch.cs +++ b/src/Ryujinx.HLE/Switch.cs @@ -2,6 +2,7 @@ using LibHac.Common; using LibHac.Ns; using Ryujinx.Audio.Backends.CompatLayer; using Ryujinx.Audio.Integration; +using Ryujinx.Common; using Ryujinx.Common.Configuration; using Ryujinx.Graphics.Gpu; using Ryujinx.HLE.FileSystem; @@ -17,6 +18,8 @@ namespace Ryujinx.HLE { public class Switch : IDisposable { + public static Switch Shared { get; private set; } + public HLEConfiguration Configuration { get; } public IHardwareDeviceDriver AudioDeviceDriver { get; } public MemoryBlock Memory { get; } @@ -37,6 +40,8 @@ namespace Ryujinx.HLE public bool IsFrameAvailable => Gpu.Window.IsFrameAvailable; + public DirtyHacks DirtyHacks { get; } + public Switch(HLEConfiguration configuration) { ArgumentNullException.ThrowIfNull(configuration.GpuRenderer); @@ -72,8 +77,11 @@ namespace Ryujinx.HLE System.EnablePtc = Configuration.EnablePtc; System.FsIntegrityCheckLevel = Configuration.FsIntegrityCheckLevel; System.GlobalAccessLogMode = Configuration.FsGlobalAccessLogMode; + DirtyHacks = Configuration.Hacks; UpdateVSyncInterval(); #pragma warning restore IDE0055 + + Shared = this; } public void ProcessFrame() @@ -142,6 +150,9 @@ namespace Ryujinx.HLE AudioDeviceDriver.Dispose(); FileSystem.Dispose(); Memory.Dispose(); + + TitleIDs.CurrentApplication = null; + Shared = null; } } } diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs index 027e1052b..8b123be01 100644 --- a/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs @@ -17,7 +17,7 @@ namespace Ryujinx.UI.Common.Configuration /// /// 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 @@ namespace Ryujinx.UI.Common.Configuration /// 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 a41ea2cd7..8652b4331 100644 --- a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Migration.cs +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Migration.cs @@ -735,6 +735,9 @@ namespace Ryujinx.UI.Common.Configuration 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 f28ce0348..2ae56d50a 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 @@ namespace Ryujinx.UI.Common.Configuration } } + 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 @@ namespace Ryujinx.UI.Common.Configuration /// 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 @@ namespace Ryujinx.UI.Common.Configuration 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 90bdc3409..8ae76ecc5 100644 --- a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs @@ -138,6 +138,8 @@ namespace Ryujinx.UI.Common.Configuration MultiplayerDisableP2p = Multiplayer.DisableP2p, MultiplayerLdnPassphrase = Multiplayer.LdnPassphrase, LdnServer = Multiplayer.LdnServer, + ShowDirtyHacks = Hacks.ShowDirtyHacks, + EnabledDirtyHacks = (int)Hacks.EnabledHacks, }; return configurationFile; diff --git a/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs b/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs index 574aaff87..de778a648 100644 --- a/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs +++ b/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs @@ -2,6 +2,7 @@ using DiscordRPC; using Humanizer; using Humanizer.Localisation; using Ryujinx.Common; +using Ryujinx.HLE; using Ryujinx.HLE.Loaders.Processes; using Ryujinx.UI.App.Common; using Ryujinx.UI.Common.Configuration; @@ -44,6 +45,16 @@ namespace Ryujinx.UI.Common }; ConfigurationState.Instance.EnableDiscordIntegration.Event += Update; + TitleIDs.CurrentApplicationChanged += titleId => + { + if (titleId) + SwitchToPlayingState( + ApplicationLibrary.LoadAndSaveMetaData(titleId), + Switch.Shared.Processes.ActiveApplication + ); + else + SwitchToMainState(); + }; } private static void Update(object sender, ReactiveEventArgs evnt) @@ -69,7 +80,7 @@ namespace Ryujinx.UI.Common } } - public static void SwitchToPlayingState(ApplicationMetadata appMeta, ProcessResult procRes) + private static void SwitchToPlayingState(ApplicationMetadata appMeta, ProcessResult procRes) { _discordClient?.SetPresence(new RichPresence { @@ -88,7 +99,7 @@ namespace Ryujinx.UI.Common }); } - public static void SwitchToMainState() => _discordClient?.SetPresence(_discordPresenceMain); + private static void SwitchToMainState() => _discordClient?.SetPresence(_discordPresenceMain); private static string TruncateToByteLength(string input) { diff --git a/src/Ryujinx/AppHost.cs b/src/Ryujinx/AppHost.cs index 909eb05d5..1f538868b 100644 --- a/src/Ryujinx/AppHost.cs +++ b/src/Ryujinx/AppHost.cs @@ -311,6 +311,7 @@ namespace Ryujinx.Ava Device.VSyncMode = e.NewValue; Device.UpdateVSyncInterval(); } + _renderer.Window?.ChangeVSyncMode(e.NewValue); _viewModel.ShowCustomVSyncIntervalPicker = (e.NewValue == VSyncMode.Custom); @@ -577,7 +578,6 @@ namespace Ryujinx.Ava public void Stop() { _isActive = false; - DiscordIntegrationModule.SwitchToMainState(); } private void Exit() @@ -861,13 +861,11 @@ namespace Ryujinx.Ava return false; } - - ApplicationMetadata appMeta = ApplicationLibrary.LoadAndSaveMetaData(Device.Processes.ActiveApplication.ProgramIdText, + + ApplicationLibrary.LoadAndSaveMetaData(Device.Processes.ActiveApplication.ProgramIdText, appMetadata => appMetadata.UpdatePreGame() ); - DiscordIntegrationModule.SwitchToPlayingState(appMeta, Device.Processes.ActiveApplication); - return true; } @@ -923,7 +921,7 @@ namespace Ryujinx.Ava // 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 +951,8 @@ namespace Ryujinx.Ava 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/Assets/locales.json b/src/Ryujinx/Assets/locales.json index 907a7d7bd..6bede7999 100644 --- a/src/Ryujinx/Assets/locales.json +++ b/src/Ryujinx/Assets/locales.json @@ -17426,25 +17426,25 @@ "ID": "TitleUpdateVersionLabel", "Translations": { "ar_SA": "الإصدار: {0}", - "de_DE": "Version {0} - {1}", - "el_GR": "Version {0} - {1}", - "en_US": "Version {0} - {1}", - "es_ES": "Versión {0} - {1}", + "de_DE": "", + "el_GR": "", + "en_US": "Version {0}", + "es_ES": "Versión {0}", "fr_FR": "", - "he_IL": "גרסה {0} - {1}", - "it_IT": "Versione {0} - {1}", - "ja_JP": "バージョン {0} - {1}", - "ko_KR": "버전 {0} - {1}", - "no_NO": "Versjon {0} - {1}", - "pl_PL": "Wersja {0} - {1}", - "pt_BR": "Versão {0} - {1}", - "ru_RU": "Версия {0} - {1}", - "sv_SE": "Version {0} - {1}", - "th_TH": "เวอร์ชั่น {0} - {1}", - "tr_TR": "Sürüm {0} - {1}", - "uk_UA": "Версія {0} - {1}", - "zh_CN": "游戏更新的版本 {0} - {1}", - "zh_TW": "版本 {0} - {1}" + "he_IL": "גרסה: {0}", + "it_IT": "Versione {0}", + "ja_JP": "バージョン {0}", + "ko_KR": "버전 {0}", + "no_NO": "Versjon {0}", + "pl_PL": "Wersja {0}", + "pt_BR": "Versão {0}", + "ru_RU": "Версия {0}", + "sv_SE": "Version {0}", + "th_TH": "เวอร์ชั่น {0}", + "tr_TR": "Sürüm {0}", + "uk_UA": "Версія {0}", + "zh_CN": "游戏更新的版本 {0}", + "zh_TW": "版本 {0}" } }, { @@ -22598,4 +22598,4 @@ } } ] -} \ No newline at end of file +} diff --git a/src/Ryujinx/Ryujinx.csproj b/src/Ryujinx/Ryujinx.csproj index d98e499c2..5e5adf2a0 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 d8f2e9096..e27c52867 100644 --- a/src/Ryujinx/UI/ViewModels/BaseModel.cs +++ b/src/Ryujinx/UI/ViewModels/BaseModel.cs @@ -13,8 +13,9 @@ namespace Ryujinx.Ava.UI.ViewModels 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 9feaaba9b..ecd2d40dd 100644 --- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs @@ -1,6 +1,7 @@ using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Threading; +using Gommon; using LibHac.Tools.FsSystem; using Ryujinx.Audio.Backends.OpenAL; using Ryujinx.Audio.Backends.SDL2; @@ -62,7 +63,9 @@ namespace Ryujinx.Ava.UI.ViewModels 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 +165,7 @@ namespace Ryujinx.Ava.UI.ViewModels 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 +259,8 @@ namespace Ryujinx.Ava.UI.ViewModels 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 +277,17 @@ namespace Ryujinx.Ava.UI.ViewModels } } + 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 +388,10 @@ namespace Ryujinx.Ava.UI.ViewModels public string LdnServer { - get => _LdnServer; + get => _ldnServer; set { - _LdnServer = value; + _ldnServer = value; OnPropertyChanged(); } } @@ -746,6 +760,9 @@ namespace Ryujinx.Ava.UI.ViewModels 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 +796,18 @@ namespace Ryujinx.Ava.UI.ViewModels RevertIfNotSaved(); CloseWindow?.Invoke(); } + + public static string Xc2MenuFixTooltip { get; } = Lambda.String(sb => + { + sb.AppendLine( + "This fix applies a 2ms delay (via 'Thread.Sleep(2)') every time the game tries to read data from the emulated Switch filesystem.") + .AppendLine(); + + sb.AppendLine("From the issue on GitHub:").AppendLine(); + sb.Append( + "When 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 000000000..b7817f064 --- /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 000000000..f9e0958ca --- /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 2bf5b55e7..59302b6fc 100644 --- a/src/Ryujinx/UI/Windows/SettingsWindow.axaml +++ b/src/Ryujinx/UI/Windows/SettingsWindow.axaml @@ -37,6 +37,7 @@ + +