diff --git a/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs b/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs index 0cb49ca8ce..6b8152b9db 100644 --- a/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs +++ b/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs @@ -2,7 +2,7 @@ namespace Ryujinx.Common.Configuration.Hid { public class KeyboardHotkeys { - public Key ToggleVsync { get; set; } + public Key ToggleVSyncMode { get; set; } public Key Screenshot { get; set; } public Key ShowUI { get; set; } public Key Pause { get; set; } @@ -11,5 +11,7 @@ namespace Ryujinx.Common.Configuration.Hid public Key ResScaleDown { get; set; } public Key VolumeUp { get; set; } public Key VolumeDown { get; set; } + public Key CustomVSyncIntervalIncrement { get; set; } + public Key CustomVSyncIntervalDecrement { get; set; } } } diff --git a/src/Ryujinx.Common/Configuration/VSyncMode.cs b/src/Ryujinx.Common/Configuration/VSyncMode.cs new file mode 100644 index 0000000000..ca93b5e1c2 --- /dev/null +++ b/src/Ryujinx.Common/Configuration/VSyncMode.cs @@ -0,0 +1,9 @@ +namespace Ryujinx.Common.Configuration +{ + public enum VSyncMode + { + Switch, + Unbounded, + Custom + } +} diff --git a/src/Ryujinx.Graphics.GAL/IWindow.cs b/src/Ryujinx.Graphics.GAL/IWindow.cs index 83418e7090..12686cb281 100644 --- a/src/Ryujinx.Graphics.GAL/IWindow.cs +++ b/src/Ryujinx.Graphics.GAL/IWindow.cs @@ -8,7 +8,7 @@ namespace Ryujinx.Graphics.GAL void SetSize(int width, int height); - void ChangeVSyncMode(bool vsyncEnabled); + void ChangeVSyncMode(VSyncMode vSyncMode); 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 acda37ef36..102fdb1bb3 100644 --- a/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedWindow.cs +++ b/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedWindow.cs @@ -31,7 +31,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading _impl.Window.SetSize(width, height); } - public void ChangeVSyncMode(bool vsyncEnabled) { } + public void ChangeVSyncMode(VSyncMode vSyncMode) { } public void SetAntiAliasing(AntiAliasing effect) { } diff --git a/src/Ryujinx.Graphics.GAL/VSyncMode.cs b/src/Ryujinx.Graphics.GAL/VSyncMode.cs new file mode 100644 index 0000000000..c5794b8f77 --- /dev/null +++ b/src/Ryujinx.Graphics.GAL/VSyncMode.cs @@ -0,0 +1,9 @@ +namespace Ryujinx.Graphics.GAL +{ + public enum VSyncMode + { + Switch, + Unbounded, + Custom + } +} diff --git a/src/Ryujinx.Graphics.OpenGL/Window.cs b/src/Ryujinx.Graphics.OpenGL/Window.cs index 285ab725e2..1dc8a51f60 100644 --- a/src/Ryujinx.Graphics.OpenGL/Window.cs +++ b/src/Ryujinx.Graphics.OpenGL/Window.cs @@ -54,7 +54,7 @@ namespace Ryujinx.Graphics.OpenGL GL.PixelStore(PixelStoreParameter.UnpackAlignment, 4); } - public void ChangeVSyncMode(bool vsyncEnabled) { } + public void ChangeVSyncMode(VSyncMode vSyncMode) { } public void SetSize(int width, int height) { diff --git a/src/Ryujinx.Graphics.Vulkan/Window.cs b/src/Ryujinx.Graphics.Vulkan/Window.cs index 3dc6d4e191..3e8d3b375a 100644 --- a/src/Ryujinx.Graphics.Vulkan/Window.cs +++ b/src/Ryujinx.Graphics.Vulkan/Window.cs @@ -29,7 +29,7 @@ namespace Ryujinx.Graphics.Vulkan private int _width; private int _height; - private bool _vsyncEnabled; + private VSyncMode _vSyncMode; private bool _swapchainIsDirty; private VkFormat _format; private AntiAliasing _currentAntiAliasing; @@ -139,7 +139,7 @@ namespace Ryujinx.Graphics.Vulkan ImageArrayLayers = 1, PreTransform = capabilities.CurrentTransform, CompositeAlpha = ChooseCompositeAlpha(capabilities.SupportedCompositeAlpha), - PresentMode = ChooseSwapPresentMode(presentModes, _vsyncEnabled), + PresentMode = ChooseSwapPresentMode(presentModes, _vSyncMode), Clipped = true, }; @@ -279,9 +279,9 @@ namespace Ryujinx.Graphics.Vulkan } } - private static PresentModeKHR ChooseSwapPresentMode(PresentModeKHR[] availablePresentModes, bool vsyncEnabled) + private static PresentModeKHR ChooseSwapPresentMode(PresentModeKHR[] availablePresentModes, VSyncMode vSyncMode) { - if (!vsyncEnabled && availablePresentModes.Contains(PresentModeKHR.ImmediateKhr)) + if (vSyncMode == VSyncMode.Unbounded && availablePresentModes.Contains(PresentModeKHR.ImmediateKhr)) { return PresentModeKHR.ImmediateKhr; } @@ -634,9 +634,10 @@ namespace Ryujinx.Graphics.Vulkan _swapchainIsDirty = true; } - public override void ChangeVSyncMode(bool vsyncEnabled) + public override void ChangeVSyncMode(VSyncMode vSyncMode) { - _vsyncEnabled = vsyncEnabled; + _vSyncMode = vSyncMode; + //present mode may change, so mark the swapchain for recreation _swapchainIsDirty = true; } diff --git a/src/Ryujinx.Graphics.Vulkan/WindowBase.cs b/src/Ryujinx.Graphics.Vulkan/WindowBase.cs index edb9c688c9..ca06ec0b86 100644 --- a/src/Ryujinx.Graphics.Vulkan/WindowBase.cs +++ b/src/Ryujinx.Graphics.Vulkan/WindowBase.cs @@ -10,7 +10,7 @@ namespace Ryujinx.Graphics.Vulkan 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 ChangeVSyncMode(VSyncMode vSyncMode); public abstract void SetAntiAliasing(AntiAliasing effect); public abstract void SetScalingFilter(ScalingFilter scalerType); public abstract void SetScalingFilterLevel(float scale); diff --git a/src/Ryujinx.Gtk3/UI/MainWindow.cs b/src/Ryujinx.Gtk3/UI/MainWindow.cs index b10dfe3f9e..2b2f4505de 100644 --- a/src/Ryujinx.Gtk3/UI/MainWindow.cs +++ b/src/Ryujinx.Gtk3/UI/MainWindow.cs @@ -46,6 +46,7 @@ using System.Threading; using System.Threading.Tasks; using GUI = Gtk.Builder.ObjectAttribute; using ShaderCacheLoadingState = Ryujinx.Graphics.Gpu.Shader.ShaderCacheState; +using VSyncMode = Ryujinx.Common.Configuration.VSyncMode; namespace Ryujinx.UI { @@ -663,7 +664,7 @@ namespace Ryujinx.UI _uiHandler, (SystemLanguage)ConfigurationState.Instance.System.Language.Value, (RegionCode)ConfigurationState.Instance.System.Region.Value, - ConfigurationState.Instance.Graphics.EnableVsync, + ConfigurationState.Instance.Graphics.VSyncMode, ConfigurationState.Instance.System.EnableDockedMode, ConfigurationState.Instance.System.EnablePtc, ConfigurationState.Instance.System.EnableInternetAccess, @@ -677,7 +678,8 @@ namespace Ryujinx.UI ConfigurationState.Instance.System.AudioVolume, ConfigurationState.Instance.System.UseHypervisor, ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value, - ConfigurationState.Instance.Multiplayer.Mode); + ConfigurationState.Instance.Multiplayer.Mode, + ConfigurationState.Instance.Graphics.CustomVSyncInterval); _emulationContext = new HLE.Switch(configuration); } @@ -1220,7 +1222,7 @@ namespace Ryujinx.UI _gpuBackend.Text = args.GpuBackend; _volumeStatus.Text = GetVolumeLabelText(args.Volume); - if (args.VSyncEnabled) + if (args.VSyncMode == VSyncMode.Switch.ToString()) { _vSyncStatus.Attributes = new Pango.AttrList(); _vSyncStatus.Attributes.Insert(new Pango.AttrForeground(11822, 60138, 51657)); @@ -1282,9 +1284,9 @@ namespace Ryujinx.UI private void VSyncStatus_Clicked(object sender, ButtonReleaseEventArgs args) { - _emulationContext.EnableDeviceVsync = !_emulationContext.EnableDeviceVsync; + _emulationContext.VSyncMode = _emulationContext.VSyncMode == VSyncMode.Switch ? VSyncMode.Unbounded : VSyncMode.Switch; - Logger.Info?.Print(LogClass.Application, $"VSync toggled to: {_emulationContext.EnableDeviceVsync}"); + Logger.Info?.Print(LogClass.Application, $"VSync toggled to: {_emulationContext.VSyncMode}"); } private void DockedMode_Clicked(object sender, ButtonReleaseEventArgs args) diff --git a/src/Ryujinx.Gtk3/UI/RendererWidgetBase.cs b/src/Ryujinx.Gtk3/UI/RendererWidgetBase.cs index 12139e87d9..974899441a 100644 --- a/src/Ryujinx.Gtk3/UI/RendererWidgetBase.cs +++ b/src/Ryujinx.Gtk3/UI/RendererWidgetBase.cs @@ -23,6 +23,7 @@ using System.Threading.Tasks; using Key = Ryujinx.Input.Key; using ScalingFilter = Ryujinx.Graphics.GAL.ScalingFilter; using Switch = Ryujinx.HLE.Switch; +using VSyncMode = Ryujinx.Graphics.GAL.VSyncMode; namespace Ryujinx.UI { @@ -460,7 +461,7 @@ namespace Ryujinx.UI Device.Gpu.SetGpuThread(); Device.Gpu.InitializeShaderCache(_gpuCancellationTokenSource.Token); - Renderer.Window.ChangeVSyncMode(Device.EnableDeviceVsync); + Renderer.Window.ChangeVSyncMode(Device.VSyncMode == (Ryujinx.Common.Configuration.VSyncMode)VSyncMode.Switch ? VSyncMode.Switch : VSyncMode.Unbounded); (Toplevel as MainWindow)?.ActivatePauseMenu(); @@ -497,7 +498,7 @@ namespace Ryujinx.UI } StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs( - Device.EnableDeviceVsync, + Device.VSyncMode == Ryujinx.Common.Configuration.VSyncMode.Switch ? VSyncMode.Switch.ToString() : VSyncMode.Unbounded.ToString(), Device.GetVolume(), _gpuBackendName, dockedMode, @@ -658,7 +659,7 @@ namespace Ryujinx.UI if (currentHotkeyState.HasFlag(KeyboardHotkeyState.ToggleVSync) && !_prevHotkeyState.HasFlag(KeyboardHotkeyState.ToggleVSync)) { - Device.EnableDeviceVsync = !Device.EnableDeviceVsync; + Device.VSyncMode = Device.VSyncMode == Ryujinx.Common.Configuration.VSyncMode.Switch ? Ryujinx.Common.Configuration.VSyncMode.Unbounded : Ryujinx.Common.Configuration.VSyncMode.Switch; } if ((currentHotkeyState.HasFlag(KeyboardHotkeyState.Screenshot) && @@ -762,7 +763,7 @@ namespace Ryujinx.UI { KeyboardHotkeyState state = KeyboardHotkeyState.None; - if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ToggleVsync)) + if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ToggleVSyncMode)) { state |= KeyboardHotkeyState.ToggleVSync; } diff --git a/src/Ryujinx.Gtk3/UI/StatusUpdatedEventArgs.cs b/src/Ryujinx.Gtk3/UI/StatusUpdatedEventArgs.cs index db467ebfca..59c99eaf28 100644 --- a/src/Ryujinx.Gtk3/UI/StatusUpdatedEventArgs.cs +++ b/src/Ryujinx.Gtk3/UI/StatusUpdatedEventArgs.cs @@ -4,7 +4,7 @@ namespace Ryujinx.UI { public class StatusUpdatedEventArgs : EventArgs { - public bool VSyncEnabled; + public string VSyncMode; public float Volume; public string DockedMode; public string AspectRatio; @@ -13,9 +13,9 @@ namespace Ryujinx.UI 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 vSyncMode, float volume, string gpuBackend, string dockedMode, string aspectRatio, string gameStatus, string fifoStatus, string gpuName) { - VSyncEnabled = vSyncEnabled; + VSyncMode = vSyncMode; Volume = volume; GpuBackend = gpuBackend; DockedMode = dockedMode; diff --git a/src/Ryujinx.Gtk3/UI/Windows/SettingsWindow.cs b/src/Ryujinx.Gtk3/UI/Windows/SettingsWindow.cs index dc467c0f21..953a030e13 100644 --- a/src/Ryujinx.Gtk3/UI/Windows/SettingsWindow.cs +++ b/src/Ryujinx.Gtk3/UI/Windows/SettingsWindow.cs @@ -243,7 +243,7 @@ namespace Ryujinx.UI.Windows break; } - if (ConfigurationState.Instance.Graphics.EnableVsync) + if (ConfigurationState.Instance.Graphics.VSyncMode.Value == VSyncMode.Switch) { _vSyncToggle.Click(); } @@ -628,7 +628,8 @@ namespace Ryujinx.UI.Windows ConfigurationState.Instance.CheckUpdatesOnStart.Value = _checkUpdatesToggle.Active; ConfigurationState.Instance.ShowConfirmExit.Value = _showConfirmExitToggle.Active; ConfigurationState.Instance.HideCursor.Value = hideCursor; - ConfigurationState.Instance.Graphics.EnableVsync.Value = _vSyncToggle.Active; + ConfigurationState.Instance.Graphics.VSyncMode.Value = + _vSyncToggle.Active ? VSyncMode.Switch : VSyncMode.Unbounded; ConfigurationState.Instance.Graphics.EnableShaderCache.Value = _shaderCacheToggle.Active; ConfigurationState.Instance.Graphics.EnableTextureRecompression.Value = _textureRecompressionToggle.Active; ConfigurationState.Instance.Graphics.EnableMacroHLE.Value = _macroHLEToggle.Active; diff --git a/src/Ryujinx.HLE/HLEConfiguration.cs b/src/Ryujinx.HLE/HLEConfiguration.cs index 955fee4b5f..fa945f9a11 100644 --- a/src/Ryujinx.HLE/HLEConfiguration.cs +++ b/src/Ryujinx.HLE/HLEConfiguration.cs @@ -9,6 +9,7 @@ using Ryujinx.HLE.HOS.Services.Account.Acc; using Ryujinx.HLE.HOS.SystemState; using Ryujinx.HLE.UI; using System; +using VSyncMode = Ryujinx.Common.Configuration.VSyncMode; namespace Ryujinx.HLE { @@ -84,9 +85,14 @@ namespace Ryujinx.HLE 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 VSyncMode VSyncMode; + + /// + /// Control the custom VSync interval, if enabled and active. + /// + internal readonly int CustomVSyncInterval; /// /// Control the initial state of the docked mode. @@ -180,7 +186,7 @@ namespace Ryujinx.HLE IHostUIHandler hostUIHandler, SystemLanguage systemLanguage, RegionCode region, - bool enableVsync, + VSyncMode vSyncMode, bool enableDockedMode, bool enablePtc, bool enableInternetAccess, @@ -194,7 +200,8 @@ namespace Ryujinx.HLE float audioVolume, bool useHypervisor, string multiplayerLanInterfaceId, - MultiplayerMode multiplayerMode) + MultiplayerMode multiplayerMode, + int customVSyncInterval) { VirtualFileSystem = virtualFileSystem; LibHacHorizonManager = libHacHorizonManager; @@ -207,7 +214,8 @@ namespace Ryujinx.HLE HostUIHandler = hostUIHandler; SystemLanguage = systemLanguage; Region = region; - EnableVsync = enableVsync; + VSyncMode = vSyncMode; + CustomVSyncInterval = customVSyncInterval; 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 4c17e7aedc..601e858674 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.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; +using VSyncMode = Ryujinx.Common.Configuration.VSyncMode; namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger { class SurfaceFlinger : IConsumerListener, IDisposable { - private const int TargetFps = 60; - private readonly Switch _device; private readonly Dictionary _layers; @@ -32,6 +31,9 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger private readonly long _spinTicks; private readonly long _1msTicks; + private VSyncMode _vSyncMode; + private long _targetVSyncInterval; + private int _swapInterval; private int _swapIntervalDelay; @@ -88,7 +90,8 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger } else { - _ticksPerFrame = Stopwatch.Frequency / TargetFps; + _ticksPerFrame = Stopwatch.Frequency / _device.TargetVSyncInterval; + _targetVSyncInterval = _device.TargetVSyncInterval; } } @@ -370,15 +373,20 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger if (acquireStatus == Status.Success) { - // If device vsync is disabled, reflect the change. - if (!_device.EnableDeviceVsync) + if (_device.VSyncMode == VSyncMode.Unbounded) { if (_swapInterval != 0) { UpdateSwapInterval(0); + _vSyncMode = _device.VSyncMode; } } - else if (item.SwapInterval != _swapInterval) + else if (_device.VSyncMode != _vSyncMode) + { + UpdateSwapInterval(_device.VSyncMode == VSyncMode.Unbounded ? 0 : item.SwapInterval); + _vSyncMode = _device.VSyncMode; + } + else if (item.SwapInterval != _swapInterval || _device.TargetVSyncInterval != _targetVSyncInterval) { UpdateSwapInterval(item.SwapInterval); } diff --git a/src/Ryujinx.HLE/Switch.cs b/src/Ryujinx.HLE/Switch.cs index 9dfc698923..99b15e33e3 100644 --- a/src/Ryujinx.HLE/Switch.cs +++ b/src/Ryujinx.HLE/Switch.cs @@ -27,7 +27,11 @@ namespace Ryujinx.HLE public TamperMachine TamperMachine { get; } public IHostUIHandler UIHandler { get; } - public bool EnableDeviceVsync { get; set; } = true; + public VSyncMode VSyncMode { get; set; } = VSyncMode.Switch; + public bool CustomVSyncIntervalEnabled { get; set; } = false; + public int CustomVSyncInterval { get; set; } + + public long TargetVSyncInterval { get; set; } = 60; public bool IsFrameAvailable => Gpu.Window.IsFrameAvailable; @@ -59,12 +63,14 @@ namespace Ryujinx.HLE System.State.SetLanguage(Configuration.SystemLanguage); System.State.SetRegion(Configuration.Region); - EnableDeviceVsync = Configuration.EnableVsync; + VSyncMode = Configuration.VSyncMode; + CustomVSyncInterval = Configuration.CustomVSyncInterval; 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; + UpdateVSyncInterval(); #pragma warning restore IDE0055 } @@ -115,6 +121,34 @@ namespace Ryujinx.HLE Gpu.Window.Present(swapBuffersCallback); } + public void IncrementCustomVSyncInterval() + { + CustomVSyncInterval += 1; + UpdateVSyncInterval(); + } + + public void DecrementCustomVSyncInterval() + { + CustomVSyncInterval -= 1; + UpdateVSyncInterval(); + } + + public void UpdateVSyncInterval() + { + switch (VSyncMode) + { + case VSyncMode.Custom: + TargetVSyncInterval = CustomVSyncInterval; + break; + case VSyncMode.Switch: + TargetVSyncInterval = 60; + break; + case VSyncMode.Unbounded: + TargetVSyncInterval = 1; + break; + } + } + public void SetVolume(float volume) { AudioDeviceDriver.Volume = Math.Clamp(volume, 0f, 1f); diff --git a/src/Ryujinx.Headless.SDL2/Options.cs b/src/Ryujinx.Headless.SDL2/Options.cs index ef8849eeaa..d5c1336b0d 100644 --- a/src/Ryujinx.Headless.SDL2/Options.cs +++ b/src/Ryujinx.Headless.SDL2/Options.cs @@ -114,8 +114,11 @@ namespace Ryujinx.Headless.SDL2 [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.")] - public bool DisableVSync { get; set; } + [Option("vsync-mode", Required = false, Default = VSyncMode.Switch, HelpText = "Sets the emulated VSync mode (Switch, Unbounded, or Custom).")] + public VSyncMode VSyncMode { get; set; } + + [Option("custom-refresh-rate", Required = false, Default = 90, HelpText = "Sets the custom refresh rate target value (integer).")] + public int CustomVSyncInterval { 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 4ee2712037..09df7b6cf3 100644 --- a/src/Ryujinx.Headless.SDL2/Program.cs +++ b/src/Ryujinx.Headless.SDL2/Program.cs @@ -566,7 +566,7 @@ namespace Ryujinx.Headless.SDL2 window, options.SystemLanguage, options.SystemRegion, - !options.DisableVSync, + options.VSyncMode, !options.DisableDockedMode, !options.DisablePTC, options.EnableInternetAccess, @@ -580,7 +580,8 @@ namespace Ryujinx.Headless.SDL2 options.AudioVolume, options.UseHypervisor ?? true, options.MultiplayerLanInterfaceId, - Common.Configuration.Multiplayer.MultiplayerMode.Disabled); + Common.Configuration.Multiplayer.MultiplayerMode.Disabled, + options.CustomVSyncInterval); return new Switch(configuration); } diff --git a/src/Ryujinx.Headless.SDL2/StatusUpdatedEventArgs.cs b/src/Ryujinx.Headless.SDL2/StatusUpdatedEventArgs.cs index 0b199d128c..2617967580 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 VSyncMode; 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 vSyncMode, string dockedMode, string aspectRatio, string gameStatus, string fifoStatus, string gpuName) { - VSyncEnabled = vSyncEnabled; + VSyncMode = vSyncMode; DockedMode = dockedMode; AspectRatio = aspectRatio; GameStatus = gameStatus; diff --git a/src/Ryujinx.Headless.SDL2/WindowBase.cs b/src/Ryujinx.Headless.SDL2/WindowBase.cs index 8768913f5a..9b071febf0 100644 --- a/src/Ryujinx.Headless.SDL2/WindowBase.cs +++ b/src/Ryujinx.Headless.SDL2/WindowBase.cs @@ -309,7 +309,7 @@ namespace Ryujinx.Headless.SDL2 } StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs( - Device.EnableDeviceVsync, + Device.VSyncMode.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 8a0be40283..cad8702a22 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 @@ namespace Ryujinx.UI.Common.Configuration /// /// The current version of the file format /// - public const int CurrentVersion = 51; + public const int CurrentVersion = 52; /// /// Version of the configuration file format @@ -180,8 +181,25 @@ namespace Ryujinx.UI.Common.Configuration /// /// 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 VSync mode; 60 (Switch), unbounded ("Vsync off"), or custom + /// + public VSyncMode VSyncMode { get; set; } + + /// + /// Enables or disables the custom present interval + /// + public bool EnableCustomVSyncInterval { get; set; } + + /// + /// The custom present interval value + /// + public int CustomVSyncInterval { 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 8420dc5d98..93e056300b 100644 --- a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs @@ -469,9 +469,19 @@ namespace Ryujinx.UI.Common.Configuration public ReactiveObject ShadersDumpPath { get; private set; } /// - /// Enables or disables Vertical Sync + /// Toggles the present interval mode. Options are Switch (60Hz), Unbounded (previously Vsync off), and Custom, if enabled. /// - public ReactiveObject EnableVsync { get; private set; } + public ReactiveObject VSyncMode { get; private set; } + + /// + /// Enables or disables the custom present interval mode. + /// + public ReactiveObject EnableCustomVSyncInterval { get; private set; } + + /// + /// Changes the custom present interval. + /// + public ReactiveObject CustomVSyncInterval { get; private set; } /// /// Enables or disables Shader cache @@ -531,8 +541,12 @@ namespace Ryujinx.UI.Common.Configuration AspectRatio = new ReactiveObject(); AspectRatio.Event += static (sender, e) => LogValueChange(e, nameof(AspectRatio)); ShadersDumpPath = new ReactiveObject(); - EnableVsync = new ReactiveObject(); - EnableVsync.Event += static (sender, e) => LogValueChange(e, nameof(EnableVsync)); + VSyncMode = new ReactiveObject(); + VSyncMode.Event += static (sender, e) => LogValueChange(e, nameof(VSyncMode)); + EnableCustomVSyncInterval = new ReactiveObject(); + EnableCustomVSyncInterval.Event += static (sender, e) => LogValueChange(e, nameof(EnableCustomVSyncInterval)); + CustomVSyncInterval = new ReactiveObject(); + CustomVSyncInterval.Event += static (sender, e) => LogValueChange(e, nameof(CustomVSyncInterval)); EnableShaderCache = new ReactiveObject(); EnableShaderCache.Event += static (sender, e) => LogValueChange(e, nameof(EnableShaderCache)); EnableTextureRecompression = new ReactiveObject(); @@ -694,7 +708,9 @@ namespace Ryujinx.UI.Common.Configuration RememberWindowState = RememberWindowState, EnableHardwareAcceleration = EnableHardwareAcceleration, HideCursor = HideCursor, - EnableVsync = Graphics.EnableVsync, + VSyncMode = Graphics.VSyncMode, + EnableCustomVSyncInterval = Graphics.EnableCustomVSyncInterval, + CustomVSyncInterval = Graphics.CustomVSyncInterval, EnableShaderCache = Graphics.EnableShaderCache, EnableTextureRecompression = Graphics.EnableTextureRecompression, EnableMacroHLE = Graphics.EnableMacroHLE, @@ -803,7 +819,9 @@ namespace Ryujinx.UI.Common.Configuration RememberWindowState.Value = true; EnableHardwareAcceleration.Value = true; HideCursor.Value = HideCursorMode.OnIdle; - Graphics.EnableVsync.Value = true; + Graphics.VSyncMode.Value = VSyncMode.Switch; + Graphics.CustomVSyncInterval.Value = 120; + Graphics.EnableCustomVSyncInterval.Value = false; Graphics.EnableShaderCache.Value = true; Graphics.EnableTextureRecompression.Value = false; Graphics.EnableMacroHLE.Value = true; @@ -862,7 +880,7 @@ namespace Ryujinx.UI.Common.Configuration Hid.EnableMouse.Value = false; Hid.Hotkeys.Value = new KeyboardHotkeys { - ToggleVsync = Key.F1, + ToggleVSyncMode = Key.F1, ToggleMute = Key.F2, Screenshot = Key.F8, ShowUI = Key.F4, @@ -993,7 +1011,7 @@ namespace Ryujinx.UI.Common.Configuration configurationFileFormat.Hotkeys = new KeyboardHotkeys { - ToggleVsync = Key.F1, + ToggleVSyncMode = Key.F1, }; configurationFileUpdated = true; @@ -1187,7 +1205,7 @@ namespace Ryujinx.UI.Common.Configuration configurationFileFormat.Hotkeys = new KeyboardHotkeys { - ToggleVsync = Key.F1, + ToggleVSyncMode = Key.F1, Screenshot = Key.F8, }; @@ -1200,7 +1218,7 @@ namespace Ryujinx.UI.Common.Configuration configurationFileFormat.Hotkeys = new KeyboardHotkeys { - ToggleVsync = Key.F1, + ToggleVSyncMode = Key.F1, Screenshot = Key.F8, ShowUI = Key.F4, }; @@ -1243,7 +1261,7 @@ namespace Ryujinx.UI.Common.Configuration configurationFileFormat.Hotkeys = new KeyboardHotkeys { - ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, + ToggleVSyncMode = Key.F1, Screenshot = configurationFileFormat.Hotkeys.Screenshot, ShowUI = configurationFileFormat.Hotkeys.ShowUI, Pause = Key.F5, @@ -1258,7 +1276,7 @@ namespace Ryujinx.UI.Common.Configuration configurationFileFormat.Hotkeys = new KeyboardHotkeys { - ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, + ToggleVSyncMode = Key.F1, Screenshot = configurationFileFormat.Hotkeys.Screenshot, ShowUI = configurationFileFormat.Hotkeys.ShowUI, Pause = configurationFileFormat.Hotkeys.Pause, @@ -1332,7 +1350,7 @@ namespace Ryujinx.UI.Common.Configuration configurationFileFormat.Hotkeys = new KeyboardHotkeys { - ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, + ToggleVSyncMode = Key.F1, Screenshot = configurationFileFormat.Hotkeys.Screenshot, ShowUI = configurationFileFormat.Hotkeys.ShowUI, Pause = configurationFileFormat.Hotkeys.Pause, @@ -1359,7 +1377,7 @@ namespace Ryujinx.UI.Common.Configuration configurationFileFormat.Hotkeys = new KeyboardHotkeys { - ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, + ToggleVSyncMode = Key.F1, Screenshot = configurationFileFormat.Hotkeys.Screenshot, ShowUI = configurationFileFormat.Hotkeys.ShowUI, Pause = configurationFileFormat.Hotkeys.Pause, @@ -1477,6 +1495,34 @@ namespace Ryujinx.UI.Common.Configuration configurationFileUpdated = true; } + if (configurationFileFormat.Version < 52) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 52."); + + configurationFileFormat.VSyncMode = VSyncMode.Switch; + configurationFileFormat.EnableCustomVSyncInterval = false; + + configurationFileFormat.Hotkeys = new KeyboardHotkeys + { + ToggleVSyncMode = 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, + CustomVSyncIntervalIncrement = Key.Unbound, + CustomVSyncIntervalDecrement = Key.Unbound, + }; + + configurationFileFormat.CustomVSyncInterval = 120; + + configurationFileUpdated = true; + } + + Logger.EnableFileLog.Value = configurationFileFormat.EnableFileLog; Graphics.ResScale.Value = configurationFileFormat.ResScale; Graphics.ResScaleCustom.Value = configurationFileFormat.ResScaleCustom; @@ -1510,7 +1556,9 @@ namespace Ryujinx.UI.Common.Configuration RememberWindowState.Value = configurationFileFormat.RememberWindowState; EnableHardwareAcceleration.Value = configurationFileFormat.EnableHardwareAcceleration; HideCursor.Value = configurationFileFormat.HideCursor; - Graphics.EnableVsync.Value = configurationFileFormat.EnableVsync; + Graphics.VSyncMode.Value = configurationFileFormat.VSyncMode; + Graphics.EnableCustomVSyncInterval.Value = configurationFileFormat.EnableCustomVSyncInterval; + Graphics.CustomVSyncInterval.Value = configurationFileFormat.CustomVSyncInterval; Graphics.EnableShaderCache.Value = configurationFileFormat.EnableShaderCache; Graphics.EnableTextureRecompression.Value = configurationFileFormat.EnableTextureRecompression; Graphics.EnableMacroHLE.Value = configurationFileFormat.EnableMacroHLE; diff --git a/src/Ryujinx/AppHost.cs b/src/Ryujinx/AppHost.cs index f4bfd1169e..198a9d695f 100644 --- a/src/Ryujinx/AppHost.cs +++ b/src/Ryujinx/AppHost.cs @@ -58,6 +58,7 @@ using MouseButton = Ryujinx.Input.MouseButton; using ScalingFilter = Ryujinx.Common.Configuration.ScalingFilter; using Size = Avalonia.Size; using Switch = Ryujinx.HLE.Switch; +using VSyncMode = Ryujinx.Common.Configuration.VSyncMode; namespace Ryujinx.Ava { @@ -201,6 +202,9 @@ namespace Ryujinx.Ava ConfigurationState.Instance.Graphics.ScalingFilter.Event += UpdateScalingFilter; ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event += UpdateScalingFilterLevel; ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Event += UpdateColorSpacePassthrough; + 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; @@ -290,6 +294,66 @@ namespace Ryujinx.Ava _renderer.Window?.SetColorSpacePassthrough((bool)ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Value); } + public void UpdateVSyncMode(object sender, ReactiveEventArgs e) + { + if (Device != null) + { + Device.VSyncMode = e.NewValue; + Device.UpdateVSyncInterval(); + } + _renderer.Window?.ChangeVSyncMode((Ryujinx.Graphics.GAL.VSyncMode)e.NewValue); + + _viewModel.ShowCustomVSyncIntervalPicker = (e.NewValue == VSyncMode.Custom); + } + + 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.TargetVSyncInterval = e.NewValue; + Device.UpdateVSyncInterval(); + } + } + + private void UpdateCustomVSyncIntervalEnabled(object sender, ReactiveEventArgs e) + { + if (Device != null) + { + Device.CustomVSyncIntervalEnabled = e.NewValue; + Device.UpdateVSyncInterval(); + } + } + private void ShowCursor() { Dispatcher.UIThread.Post(() => @@ -489,12 +553,6 @@ namespace Ryujinx.Ava Device.Configuration.MultiplayerMode = e.NewValue; } - public void ToggleVSync() - { - Device.EnableDeviceVsync = !Device.EnableDeviceVsync; - _renderer.Window.ChangeVSyncMode(Device.EnableDeviceVsync); - } - public void Stop() { _isActive = false; @@ -848,31 +906,32 @@ namespace Ryujinx.Ava var memoryConfiguration = ConfigurationState.Instance.System.ExpandRam.Value ? MemoryConfiguration.MemoryConfiguration8GiB : MemoryConfiguration.MemoryConfiguration4GiB; HLEConfiguration configuration = new(VirtualFileSystem, - _viewModel.LibHacHorizonManager, - ContentManager, - _accountManager, - _userChannelPersistence, - renderer, - InitializeAudio(), - memoryConfiguration, - _viewModel.UiHandler, - (SystemLanguage)ConfigurationState.Instance.System.Language.Value, - (RegionCode)ConfigurationState.Instance.System.Region.Value, - ConfigurationState.Instance.Graphics.EnableVsync, - ConfigurationState.Instance.System.EnableDockedMode, - ConfigurationState.Instance.System.EnablePtc, - ConfigurationState.Instance.System.EnableInternetAccess, - ConfigurationState.Instance.System.EnableFsIntegrityChecks ? IntegrityCheckLevel.ErrorOnInvalid : IntegrityCheckLevel.None, - ConfigurationState.Instance.System.FsGlobalAccessLogMode, - ConfigurationState.Instance.System.SystemTimeOffset, - ConfigurationState.Instance.System.TimeZone, - ConfigurationState.Instance.System.MemoryManagerMode, - ConfigurationState.Instance.System.IgnoreMissingServices, - ConfigurationState.Instance.Graphics.AspectRatio, - ConfigurationState.Instance.System.AudioVolume, - ConfigurationState.Instance.System.UseHypervisor, - ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value, - ConfigurationState.Instance.Multiplayer.Mode); + _viewModel.LibHacHorizonManager, + ContentManager, + _accountManager, + _userChannelPersistence, + renderer, + InitializeAudio(), + memoryConfiguration, + _viewModel.UiHandler, + (SystemLanguage)ConfigurationState.Instance.System.Language.Value, + (RegionCode)ConfigurationState.Instance.System.Region.Value, + ConfigurationState.Instance.Graphics.VSyncMode, + ConfigurationState.Instance.System.EnableDockedMode, + ConfigurationState.Instance.System.EnablePtc, + ConfigurationState.Instance.System.EnableInternetAccess, + ConfigurationState.Instance.System.EnableFsIntegrityChecks ? IntegrityCheckLevel.ErrorOnInvalid : IntegrityCheckLevel.None, + ConfigurationState.Instance.System.FsGlobalAccessLogMode, + ConfigurationState.Instance.System.SystemTimeOffset, + ConfigurationState.Instance.System.TimeZone, + ConfigurationState.Instance.System.MemoryManagerMode, + ConfigurationState.Instance.System.IgnoreMissingServices, + ConfigurationState.Instance.Graphics.AspectRatio, + ConfigurationState.Instance.System.AudioVolume, + ConfigurationState.Instance.System.UseHypervisor, + ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value, + ConfigurationState.Instance.Multiplayer.Mode, + ConfigurationState.Instance.Graphics.CustomVSyncInterval.Value); Device = new Switch(configuration); } @@ -997,7 +1056,7 @@ namespace Ryujinx.Ava Device.Gpu.SetGpuThread(); Device.Gpu.InitializeShaderCache(_gpuCancellationTokenSource.Token); - _renderer.Window.ChangeVSyncMode(Device.EnableDeviceVsync); + _renderer.Window.ChangeVSyncMode((Ryujinx.Graphics.GAL.VSyncMode)Device.VSyncMode); while (_isActive) { @@ -1058,6 +1117,7 @@ namespace Ryujinx.Ava { // 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 vSyncMode = Device.VSyncMode.ToString(); if (GraphicsConfig.ResScale != 1) { @@ -1065,7 +1125,7 @@ namespace Ryujinx.Ava } StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs( - Device.EnableDeviceVsync, + vSyncMode, LocaleManager.Instance[LocaleKeys.VolumeShort] + $": {(int)(Device.GetVolume() * 100)}%", dockedMode, ConfigurationState.Instance.Graphics.AspectRatio.Value.ToText(), @@ -1149,8 +1209,16 @@ namespace Ryujinx.Ava { switch (currentHotkeyState) { - case KeyboardHotkeyState.ToggleVSync: - ToggleVSync(); + case KeyboardHotkeyState.ToggleVSyncMode: + VSyncModeToggle(); + break; + case KeyboardHotkeyState.CustomVSyncIntervalDecrement: + Device.DecrementCustomVSyncInterval(); + _viewModel.CustomVSyncInterval -= 1; + break; + case KeyboardHotkeyState.CustomVSyncIntervalIncrement: + Device.IncrementCustomVSyncInterval(); + _viewModel.CustomVSyncInterval += 1; break; case KeyboardHotkeyState.Screenshot: ScreenshotRequested = true; @@ -1237,9 +1305,9 @@ namespace Ryujinx.Ava { KeyboardHotkeyState state = KeyboardHotkeyState.None; - if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ToggleVsync)) + if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ToggleVSyncMode)) { - state = KeyboardHotkeyState.ToggleVSync; + state = KeyboardHotkeyState.ToggleVSyncMode; } else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.Screenshot)) { @@ -1273,6 +1341,14 @@ namespace Ryujinx.Ava { state = KeyboardHotkeyState.VolumeDown; } + else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CustomVSyncIntervalIncrement)) + { + state = KeyboardHotkeyState.CustomVSyncIntervalIncrement; + } + else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CustomVSyncIntervalDecrement)) + { + state = KeyboardHotkeyState.CustomVSyncIntervalDecrement; + } return state; } diff --git a/src/Ryujinx/Assets/Locales/en_US.json b/src/Ryujinx/Assets/Locales/en_US.json index b3cab7f5f6..c8766ddd45 100644 --- a/src/Ryujinx/Assets/Locales/en_US.json +++ b/src/Ryujinx/Assets/Locales/en_US.json @@ -133,9 +133,19 @@ "SettingsTabSystemSystemLanguageLatinAmericanSpanish": "Latin American Spanish", "SettingsTabSystemSystemLanguageSimplifiedChinese": "Simplified Chinese", "SettingsTabSystemSystemLanguageTraditionalChinese": "Traditional Chinese", - "SettingsTabSystemSystemTimeZone": "System TimeZone:", + "SettingsTabSystemSystemTimeZone": "System Time Zone:", "SettingsTabSystemSystemTime": "System Time:", - "SettingsTabSystemEnableVsync": "VSync", + "SettingsTabSystemVSyncMode": "VSync:", + "SettingsTabSystemEnableCustomVSyncInterval": "Enable custom refresh rate (Experimental)", + "SettingsTabSystemVSyncModeSwitch": "Switch", + "SettingsTabSystemVSyncModeUnbounded": "Unbounded", + "SettingsTabSystemVSyncModeCustom": "Custom Refresh Rate", + "SettingsTabSystemVSyncModeTooltip": "Emulated Vertical Sync. 'Switch' emulates the Switch's refresh rate of 60Hz. 'Unbounded' is an unbounded refresh rate.", + "SettingsTabSystemVSyncModeTooltipCustom": "Emulated Vertical Sync. 'Switch' emulates the Switch's refresh rate of 60Hz. 'Unbounded' 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:", @@ -143,6 +153,7 @@ "SettingsTabSystemAudioBackendOpenAL": "OpenAL", "SettingsTabSystemAudioBackendSoundIO": "SoundIO", "SettingsTabSystemAudioBackendSDL2": "SDL2", + "SettingsTabSystemCustomVSyncInterval": "Interval", "SettingsTabSystemHacks": "Hacks", "SettingsTabSystemHacksNote": "May cause instability", "SettingsTabSystemExpandDramSize": "Expand DRAM to 8GiB", @@ -687,11 +698,13 @@ "RyujinxUpdater": "Ryujinx Updater", "SettingsTabHotkeys": "Keyboard Hotkeys", "SettingsTabHotkeysHotkeys": "Keyboard Hotkeys", - "SettingsTabHotkeysToggleVsyncHotkey": "Toggle VSync:", + "SettingsTabHotkeysToggleVSyncModeHotkey": "Toggle VSync mode:", "SettingsTabHotkeysScreenshotHotkey": "Screenshot:", "SettingsTabHotkeysShowUiHotkey": "Show UI:", "SettingsTabHotkeysPauseHotkey": "Pause:", "SettingsTabHotkeysToggleMuteHotkey": "Mute:", + "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/Assets/Styles/Themes.xaml b/src/Ryujinx/Assets/Styles/Themes.xaml index 0f323f84b2..056eba2282 100644 --- a/src/Ryujinx/Assets/Styles/Themes.xaml +++ b/src/Ryujinx/Assets/Styles/Themes.xaml @@ -26,8 +26,9 @@ #b3ffffff #80cccccc #A0000000 - #FF2EEAC9 - #FFFF4554 + #FF2EEAC9 + #FFFF4554 + #6483F5 _toggleVsync; + get => _toggleVSyncMode; set { - _toggleVsync = value; + _toggleVSyncMode = value; OnPropertyChanged(); } } @@ -104,11 +104,33 @@ namespace Ryujinx.Ava.UI.Models.Input } } + private Key _customVSyncIntervalIncrement; + public Key CustomVSyncIntervalIncrement + { + get => _customVSyncIntervalIncrement; + set + { + _customVSyncIntervalIncrement = value; + OnPropertyChanged(); + } + } + + private Key _customVSyncIntervalDecrement; + public Key CustomVSyncIntervalDecrement + { + get => _customVSyncIntervalDecrement; + set + { + _customVSyncIntervalDecrement = value; + OnPropertyChanged(); + } + } + public HotkeyConfig(KeyboardHotkeys config) { if (config != null) { - ToggleVsync = config.ToggleVsync; + ToggleVSyncMode = config.ToggleVSyncMode; Screenshot = config.Screenshot; ShowUI = config.ShowUI; Pause = config.Pause; @@ -117,6 +139,8 @@ namespace Ryujinx.Ava.UI.Models.Input ResScaleDown = config.ResScaleDown; VolumeUp = config.VolumeUp; VolumeDown = config.VolumeDown; + CustomVSyncIntervalIncrement = config.CustomVSyncIntervalIncrement; + CustomVSyncIntervalDecrement = config.CustomVSyncIntervalDecrement; } } @@ -124,7 +148,7 @@ namespace Ryujinx.Ava.UI.Models.Input { var config = new KeyboardHotkeys { - ToggleVsync = ToggleVsync, + ToggleVSyncMode = ToggleVSyncMode, Screenshot = Screenshot, ShowUI = ShowUI, Pause = Pause, @@ -133,6 +157,8 @@ namespace Ryujinx.Ava.UI.Models.Input ResScaleDown = ResScaleDown, VolumeUp = VolumeUp, VolumeDown = VolumeDown, + CustomVSyncIntervalIncrement = CustomVSyncIntervalIncrement, + CustomVSyncIntervalDecrement = CustomVSyncIntervalDecrement, }; return config; diff --git a/src/Ryujinx/UI/Models/StatusUpdatedEventArgs.cs b/src/Ryujinx/UI/Models/StatusUpdatedEventArgs.cs index ee5648faf8..7e68841b62 100644 --- a/src/Ryujinx/UI/Models/StatusUpdatedEventArgs.cs +++ b/src/Ryujinx/UI/Models/StatusUpdatedEventArgs.cs @@ -4,16 +4,16 @@ namespace Ryujinx.Ava.UI.Models { internal class StatusUpdatedEventArgs : EventArgs { - public bool VSyncEnabled { get; } + public string VSyncMode { get; } public string VolumeStatus { get; } public string AspectRatio { get; } public string DockedMode { get; } public string FifoStatus { get; } public string GameStatus { get; } - public StatusUpdatedEventArgs(bool vSyncEnabled, string volumeStatus, string dockedMode, string aspectRatio, string gameStatus, string fifoStatus) + public StatusUpdatedEventArgs(string vSyncMode, string volumeStatus, string dockedMode, string aspectRatio, string gameStatus, string fifoStatus) { - VSyncEnabled = vSyncEnabled; + VSyncMode = vSyncMode; VolumeStatus = volumeStatus; DockedMode = dockedMode; AspectRatio = aspectRatio; diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index bd9f165b92..f19bf183d7 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -58,6 +58,7 @@ namespace Ryujinx.Ava.UI.ViewModels private string _searchText; private Timer _searchTimer; private string _dockedStatusText; + private string _vSyncModeText; private string _fifoStatusText; private string _gameStatusText; private string _volumeStatusText; @@ -73,7 +74,7 @@ namespace Ryujinx.Ava.UI.ViewModels private bool _showStatusSeparator; private Brush _progressBarForegroundColor; private Brush _progressBarBackgroundColor; - private Brush _vsyncColor; + private Brush _vSyncModeColor; private byte[] _selectedIcon; private bool _isAppletMenuActive; private int _statusBarProgressMaximum; @@ -101,6 +102,8 @@ namespace Ryujinx.Ava.UI.ViewModels private WindowState _windowState; private double _windowWidth; private double _windowHeight; + private int _customVSyncInterval; + private int _customVSyncIntervalPercentageProxy; private bool _isActive; private bool _isSubMenuOpen; @@ -127,6 +130,7 @@ namespace Ryujinx.Ava.UI.ViewModels Volume = ConfigurationState.Instance.System.AudioVolume; } + CustomVSyncInterval = ConfigurationState.Instance.Graphics.CustomVSyncInterval.Value; } public void Initialize( @@ -414,17 +418,87 @@ namespace Ryujinx.Ava.UI.ViewModels } } - public Brush VsyncColor + public Brush VSyncModeColor { - get => _vsyncColor; + get => _vSyncModeColor; set { - _vsyncColor = value; + _vSyncModeColor = value; OnPropertyChanged(); } } + public bool ShowCustomVSyncIntervalPicker + { + get + { + if (_isGameRunning) + { + return AppHost.Device.VSyncMode == + VSyncMode.Custom; + } + else + { + return false; + } + } + set + { + OnPropertyChanged(); + } + } + + public int CustomVSyncIntervalPercentageProxy + { + get => _customVSyncIntervalPercentageProxy; + set + { + int newInterval = (int)((value / 100f) * 60); + _customVSyncInterval = newInterval; + _customVSyncIntervalPercentageProxy = value; + if (_isGameRunning) + { + AppHost.Device.CustomVSyncInterval = newInterval; + AppHost.Device.UpdateVSyncInterval(); + } + OnPropertyChanged((nameof(CustomVSyncInterval))); + OnPropertyChanged((nameof(CustomVSyncIntervalPercentageText))); + } + } + + public string CustomVSyncIntervalPercentageText + { + get + { + string text = CustomVSyncIntervalPercentageProxy.ToString() + "%"; + return text; + } + set + { + + } + } + + public int CustomVSyncInterval + { + get => _customVSyncInterval; + set + { + _customVSyncInterval = value; + int newPercent = (int)((value / 60f) * 100); + _customVSyncIntervalPercentageProxy = newPercent; + if (_isGameRunning) + { + AppHost.Device.CustomVSyncInterval = value; + AppHost.Device.UpdateVSyncInterval(); + } + OnPropertyChanged(nameof(CustomVSyncIntervalPercentageProxy)); + OnPropertyChanged(nameof(CustomVSyncIntervalPercentageText)); + OnPropertyChanged(); + } + } + public byte[] SelectedIcon { get => _selectedIcon; @@ -513,6 +587,17 @@ namespace Ryujinx.Ava.UI.ViewModels } } + public string VSyncModeText + { + get => _vSyncModeText; + set + { + _vSyncModeText = value; + + OnPropertyChanged(); + } + } + public string DockedStatusText { get => _dockedStatusText; @@ -1227,17 +1312,18 @@ namespace Ryujinx.Ava.UI.ViewModels { Dispatcher.UIThread.InvokeAsync(() => { - Application.Current.Styles.TryGetResource(args.VSyncEnabled - ? "VsyncEnabled" - : "VsyncDisabled", - Application.Current.ActualThemeVariant, + Application.Current.Styles.TryGetResource(args.VSyncMode, + Avalonia.Application.Current.ActualThemeVariant, out object color); if (color is not null) { - VsyncColor = new SolidColorBrush((Color)color); + VSyncModeColor = new SolidColorBrush((Color)color); } + VSyncModeText = args.VSyncMode == "Custom" ? "Custom" : "VSync"; + ShowCustomVSyncIntervalPicker = + args.VSyncMode == VSyncMode.Custom.ToString(); DockedStatusText = args.DockedMode; AspectRatioStatusText = args.AspectRatio; GameStatusText = args.GameStatus; @@ -1394,6 +1480,27 @@ namespace Ryujinx.Ava.UI.ViewModels } } + public void ToggleVSyncMode() + { + AppHost.VSyncModeToggle(); + OnPropertyChanged(nameof(ShowCustomVSyncIntervalPicker)); + } + + public void VSyncModeSettingChanged() + { + if (_isGameRunning) + { + AppHost.Device.CustomVSyncInterval = ConfigurationState.Instance.Graphics.CustomVSyncInterval.Value; + AppHost.Device.UpdateVSyncInterval(); + } + + CustomVSyncInterval = ConfigurationState.Instance.Graphics.CustomVSyncInterval.Value; + OnPropertyChanged(nameof(ShowCustomVSyncIntervalPicker)); + OnPropertyChanged(nameof(CustomVSyncIntervalPercentageProxy)); + OnPropertyChanged(nameof(CustomVSyncIntervalPercentageText)); + OnPropertyChanged(nameof(CustomVSyncInterval)); + } + public async Task ExitCurrentState() { if (WindowState == WindowState.FullScreen) diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs index 70e5fa5c74..825b282145 100644 --- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs @@ -49,6 +49,10 @@ namespace Ryujinx.Ava.UI.ViewModels private int _graphicsBackendIndex; private int _scalingFilter; private int _scalingFilterLevel; + private int _customVSyncInterval; + private bool _enableCustomVSyncInterval; + private int _customVSyncIntervalPercentageProxy; + private VSyncMode _vSyncMode; public event Action CloseWindow; public event Action SaveSettingsEvent; @@ -136,7 +140,74 @@ namespace Ryujinx.Ava.UI.ViewModels public bool EnableDockedMode { get; set; } public bool EnableKeyboard { get; set; } public bool EnableMouse { get; set; } - public bool EnableVsync { get; set; } + public VSyncMode VSyncMode + { + get => _vSyncMode; + set + { + if (value == VSyncMode.Custom || + value == VSyncMode.Switch || + value == VSyncMode.Unbounded) + { + _vSyncMode = value; + OnPropertyChanged(); + } + } + } + + public int CustomVSyncIntervalPercentageProxy + { + get => _customVSyncIntervalPercentageProxy; + set + { + int newInterval = (int)((value / 100f) * 60); + _customVSyncInterval = newInterval; + _customVSyncIntervalPercentageProxy = value; + OnPropertyChanged((nameof(CustomVSyncInterval))); + OnPropertyChanged((nameof(CustomVSyncIntervalPercentageText))); + } + } + + public string CustomVSyncIntervalPercentageText + { + get + { + string text = CustomVSyncIntervalPercentageProxy.ToString() + "%"; + return text; + } + } + + public bool EnableCustomVSyncInterval + { + get => _enableCustomVSyncInterval; + set + { + _enableCustomVSyncInterval = value; + if (_vSyncMode == VSyncMode.Custom && !value) + { + VSyncMode = VSyncMode.Switch; + } + else if (value) + { + VSyncMode = VSyncMode.Custom; + } + OnPropertyChanged(); + } + } + + public int CustomVSyncInterval + { + get => _customVSyncInterval; + set + { + _customVSyncInterval = value; + int newPercent = (int)((value / 60f) * 100); + _customVSyncIntervalPercentageProxy = newPercent; + OnPropertyChanged(nameof(CustomVSyncIntervalPercentageProxy)); + OnPropertyChanged(nameof(CustomVSyncIntervalPercentageText)); + OnPropertyChanged(); + } + } public bool EnablePptc { get; set; } public bool EnableInternetAccess { get; set; } public bool EnableFsIntegrityChecks { get; set; } @@ -424,7 +495,9 @@ namespace Ryujinx.Ava.UI.ViewModels CurrentDate = currentDateTime.Date; CurrentTime = currentDateTime.TimeOfDay; - EnableVsync = config.Graphics.EnableVsync; + 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; @@ -436,7 +509,7 @@ namespace Ryujinx.Ava.UI.ViewModels // 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; @@ -518,7 +591,9 @@ namespace Ryujinx.Ava.UI.ViewModels } config.System.SystemTimeOffset.Value = Convert.ToInt64((CurrentDate.ToUnixTimeSeconds() + CurrentTime.TotalSeconds) - DateTimeOffset.Now.ToUnixTimeSeconds()); - config.Graphics.EnableVsync.Value = EnableVsync; + 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; @@ -584,6 +659,7 @@ namespace Ryujinx.Ava.UI.ViewModels config.ToFileFormat().SaveConfig(Program.ConfigurationPath); MainWindow.UpdateGraphicsConfig(); + MainWindow.MainWindowViewModel.VSyncModeSettingChanged(); SaveSettingsEvent?.Invoke(); diff --git a/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml b/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml index f9e192e620..4bc5130f25 100644 --- a/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml +++ b/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml @@ -1,4 +1,4 @@ - + PointerReleased="VSyncMode_PointerReleased" + Text="{Binding VSyncModeText}" + TextAlignment="Start"/> + - - - + + + @@ -103,6 +103,18 @@ + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml.cs b/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml.cs index fb0fe2bb12..609f616335 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml.cs +++ b/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml.cs @@ -82,8 +82,8 @@ namespace Ryujinx.Ava.UI.Views.Settings switch (button.Name) { - case "ToggleVsync": - viewModel.KeyboardHotkey.ToggleVsync = buttonValue.AsHidType(); + case "ToggleVSyncMode": + viewModel.KeyboardHotkey.ToggleVSyncMode = buttonValue.AsHidType(); break; case "Screenshot": viewModel.KeyboardHotkey.Screenshot = buttonValue.AsHidType(); @@ -109,6 +109,12 @@ namespace Ryujinx.Ava.UI.Views.Settings case "VolumeDown": viewModel.KeyboardHotkey.VolumeDown = buttonValue.AsHidType(); break; + case "CustomVSyncIntervalIncrement": + viewModel.KeyboardHotkey.CustomVSyncIntervalIncrement = buttonValue.AsHidType(); + break; + case "CustomVSyncIntervalDecrement": + viewModel.KeyboardHotkey.CustomVSyncIntervalDecrement = buttonValue.AsHidType(); + break; } } }; diff --git a/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml index e6f7c6e463..d8cc86b671 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml +++ b/src/Ryujinx/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,11 +182,78 @@ Width="350" ToolTip.Tip="{locale:Locale TimeTooltip}" /> - + - + VerticalAlignment="Center" + Text="{locale:Locale SettingsTabSystemVSyncMode}" + ToolTip.Tip="{locale:Locale SettingsTabSystemVSyncModeTooltip}" + Width="250" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - \ No newline at end of file +