diff --git a/src/Ryujinx.Ava/AppHost.cs b/src/Ryujinx.Ava/AppHost.cs index e434deb0a..640a0783a 100644 --- a/src/Ryujinx.Ava/AppHost.cs +++ b/src/Ryujinx.Ava/AppHost.cs @@ -60,6 +60,7 @@ using ScalingFilter = Ryujinx.Common.Configuration.ScalingFilter; using Size = Avalonia.Size; using Switch = Ryujinx.HLE.Switch; +using VSyncMode = Ryujinx.Common.Configuration.VSyncMode; namespace Ryujinx.Ava { @@ -188,6 +189,9 @@ public AppHost( 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; @@ -235,6 +239,69 @@ private void UpdateColorSpacePassthrough(object sender, ReactiveEventArgs _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(); + } + //vulkan present mode may change in response, so recreate the swapchain + _renderer.Window?.ChangeVSyncMode((Ryujinx.Graphics.GAL.VSyncMode)e.NewValue); + + _viewModel.ShowCustomVSyncIntervalPicker = (e.NewValue == VSyncMode.Custom); + + ConfigurationState.Instance.Graphics.VSyncMode.Value = e.NewValue; + ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); + } + + 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(() => @@ -777,7 +844,7 @@ private void InitializeSwitchInstance() _viewModel.UiHandler, (SystemLanguage)ConfigurationState.Instance.System.Language.Value, (RegionCode)ConfigurationState.Instance.System.Region.Value, - ConfigurationState.Instance.Graphics.EnableVsync, + ConfigurationState.Instance.Graphics.VSyncMode, ConfigurationState.Instance.System.EnableDockedMode, ConfigurationState.Instance.System.EnablePtc, ConfigurationState.Instance.System.EnableInternetAccess, @@ -791,7 +858,8 @@ private void InitializeSwitchInstance() ConfigurationState.Instance.System.AudioVolume, ConfigurationState.Instance.System.UseHypervisor, ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value, - ConfigurationState.Instance.Multiplayer.Mode); + ConfigurationState.Instance.Multiplayer.Mode, + ConfigurationState.Instance.Graphics.CustomVSyncInterval.Value); Device = new Switch(configuration); } @@ -916,7 +984,7 @@ private void RenderLoop() Device.Gpu.SetGpuThread(); Device.Gpu.InitializeShaderCache(_gpuCancellationTokenSource.Token); - _renderer.Window.ChangeVSyncMode(Device.EnableDeviceVsync); + _renderer.Window.ChangeVSyncMode((Ryujinx.Graphics.GAL.VSyncMode)Device.VSyncMode); while (_isActive) { @@ -964,6 +1032,7 @@ public void UpdateStatus() { // Run a status update only when a frame is to be drawn. This prevents from updating the ui and wasting a render when no frame is queued. string dockedMode = ConfigurationState.Instance.System.EnableDockedMode ? LocaleManager.Instance[LocaleKeys.Docked] : LocaleManager.Instance[LocaleKeys.Handheld]; + string vSyncMode = Device.VSyncMode.ToString(); if (GraphicsConfig.ResScale != 1) { @@ -971,7 +1040,7 @@ public void UpdateStatus() } StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs( - Device.EnableDeviceVsync, + vSyncMode, LocaleManager.Instance[LocaleKeys.VolumeShort] + $": {(int)(Device.GetVolume() * 100)}%", ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.Vulkan ? "Vulkan" : "OpenGL", dockedMode, @@ -1063,9 +1132,16 @@ private bool UpdateFrame() { switch (currentHotkeyState) { - case KeyboardHotkeyState.ToggleVSync: - Device.EnableDeviceVsync = !Device.EnableDeviceVsync; - + 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; @@ -1152,9 +1228,9 @@ private KeyboardHotkeyState GetHotkeyState() { KeyboardHotkeyState state = KeyboardHotkeyState.None; - if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ToggleVsync)) + if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.VSyncMode)) { - state = KeyboardHotkeyState.ToggleVSync; + state = KeyboardHotkeyState.ToggleVSyncMode; } else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.Screenshot)) { @@ -1188,6 +1264,14 @@ private KeyboardHotkeyState GetHotkeyState() { 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.Ava/Assets/Locales/en_US.json b/src/Ryujinx.Ava/Assets/Locales/en_US.json index 936e0a547..649f4dd3d 100644 --- a/src/Ryujinx.Ava/Assets/Locales/en_US.json +++ b/src/Ryujinx.Ava/Assets/Locales/en_US.json @@ -122,9 +122,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": "On", + "SettingsTabSystemVSyncModeUnbounded": "Off", + "SettingsTabSystemVSyncModeCustom": "Custom Refresh Rate", + "SettingsTabSystemVSyncModeTooltip": "Emulated Vertical Sync. 'On' emulates the Switch's refresh rate of 60Hz. 'Off' is an unbounded refresh rate.", + "SettingsTabSystemVSyncModeTooltipCustom": "Emulated Vertical Sync. 'On' emulates the Switch's refresh rate of 60Hz. 'Off' is an unbounded refresh rate. 'Custom' emulates the specified custom refresh rate.", + "SettingsTabSystemEnableCustomVSyncIntervalTooltip": "Allows the user to specify an emulated refresh rate. In some titles, this may speed up or slow down the rate of gameplay logic. In other titles, it may allow for capping FPS at some multiple of the refresh rate, or lead to unpredictable behavior. This is an experimental feature, with no guarantees for how gameplay will be affected. \n\nLeave OFF if unsure.", + "SettingsTabSystemCustomVSyncIntervalValueTooltip": "The custom refresh rate target value.", + "SettingsTabSystemCustomVSyncIntervalSliderTooltip": "The custom refresh rate, as a percentage of the normal Switch refresh rate.", + "SettingsTabSystemCustomVSyncIntervalValue": "Custom Refresh Rate Value:", "SettingsTabSystemEnablePptc": "PPTC (Profiled Persistent Translation Cache)", "SettingsTabSystemEnableFsIntegrityChecks": "FS Integrity Checks", "SettingsTabSystemAudioBackend": "Audio Backend:", @@ -132,6 +142,7 @@ "SettingsTabSystemAudioBackendOpenAL": "OpenAL", "SettingsTabSystemAudioBackendSoundIO": "SoundIO", "SettingsTabSystemAudioBackendSDL2": "SDL2", + "SettingsTabSystemCustomVSyncInterval": "Interval", "SettingsTabSystemHacks": "Hacks", "SettingsTabSystemHacksNote": "May cause instability", "SettingsTabSystemExpandDramSize": "Use alternative memory layout (Developers)", @@ -576,11 +587,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.Ava/Assets/Styles/Themes.xaml b/src/Ryujinx.Ava/Assets/Styles/Themes.xaml index 0f323f84b..056eba228 100644 --- a/src/Ryujinx.Ava/Assets/Styles/Themes.xaml +++ b/src/Ryujinx.Ava/Assets/Styles/Themes.xaml @@ -26,8 +26,9 @@ #b3ffffff #80cccccc #A0000000 - #FF2EEAC9 - #FFFF4554 + #FF2EEAC9 + #FFFF4554 + #6483F5 _vsyncColor; + get => _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)(((decimal)value / 100) * 60); + _customVSyncInterval = newInterval; + _customVSyncIntervalPercentageProxy = value; + ConfigurationState.Instance.Graphics.CustomVSyncInterval.Value = newInterval; + 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)(((decimal)value / 60) * 100); + _customVSyncIntervalPercentageProxy = newPercent; + ConfigurationState.Instance.Graphics.CustomVSyncInterval.Value = value; + if (_isGameRunning) + { + AppHost.Device.CustomVSyncInterval = value; + AppHost.Device.UpdateVSyncInterval(); + } + OnPropertyChanged(nameof(CustomVSyncIntervalPercentageProxy)); + OnPropertyChanged(nameof(CustomVSyncIntervalPercentageText)); OnPropertyChanged(); } } @@ -502,6 +581,17 @@ public string BackendText } } + public string VSyncModeText + { + get => _vSyncModeText; + set + { + _vSyncModeText = value; + + OnPropertyChanged(); + } + } + public string DockedStatusText { get => _dockedStatusText; @@ -1196,17 +1286,18 @@ private void Update_StatusBar(object sender, StatusUpdatedEventArgs args) { Dispatcher.UIThread.InvokeAsync(() => { - Application.Current.Styles.TryGetResource(args.VSyncEnabled - ? "VsyncEnabled" - : "VsyncDisabled", - Application.Current.ActualThemeVariant, + Application.Current.Styles.TryGetResource(args.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; @@ -1365,6 +1456,27 @@ public void ToggleDockMode() } } + public void UpdateVSyncMode() + { + 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.Ava/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/SettingsViewModel.cs index 604e34067..4333f4974 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/SettingsViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/SettingsViewModel.cs @@ -7,6 +7,7 @@ using Ryujinx.Audio.Backends.SoundIo; using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.Views.Main; using Ryujinx.Ava.UI.Windows; using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration.Hid; @@ -51,6 +52,10 @@ public class SettingsViewModel : BaseModel private string _customThemePath; private int _scalingFilter; private int _scalingFilterLevel; + private int _customVSyncInterval; + private bool _enableCustomVSyncInterval; + private int _customVSyncIntervalPercentageProxy; + private VSyncMode _vSyncMode; public event Action CloseWindow; public event Action SaveSettingsEvent; @@ -137,7 +142,78 @@ public bool DirectoryChanged 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)(((decimal)value / 100) * 60); + _customVSyncInterval = newInterval; + _customVSyncIntervalPercentageProxy = value; + OnPropertyChanged((nameof(CustomVSyncInterval))); + OnPropertyChanged((nameof(CustomVSyncIntervalPercentageText))); + } + } + + public string CustomVSyncIntervalPercentageText + { + get + { + string text = CustomVSyncIntervalPercentageProxy.ToString() + "%"; + return text; + } + set + { + + } + } + + public bool EnableCustomVSyncInterval + { + get => _enableCustomVSyncInterval; + set + { + _enableCustomVSyncInterval = value; + if (_vSyncMode == VSyncMode.Custom && value == false) + { + VSyncMode = VSyncMode.Switch; + } + else if (value) + { + VSyncMode = VSyncMode.Custom; + } + OnPropertyChanged(); + } + } + + public int CustomVSyncInterval + { + get => _customVSyncInterval; + set + { + _customVSyncInterval = value; + int newPercent = (int)(((decimal)value / 60) * 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; } @@ -448,7 +524,9 @@ public void LoadCurrentConfiguration() CurrentDate = currentDateTime.Date; CurrentTime = currentDateTime.TimeOfDay.Add(TimeSpan.FromSeconds(config.System.SystemTimeOffset)); - 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; @@ -460,7 +538,7 @@ public void LoadCurrentConfiguration() // Graphics GraphicsBackendIndex = (int)config.Graphics.GraphicsBackend.Value; - // Physical devices are queried asynchronously hence the prefered index config value is loaded in LoadAvailableGpus(). + // Physical devices are queried asynchronously hence the preferred index config value is loaded in LoadAvailableGpus(). EnableShaderCache = config.Graphics.EnableShaderCache; EnableTextureRecompression = config.Graphics.EnableTextureRecompression; EnableMacroHLE = config.Graphics.EnableMacroHLE; @@ -537,7 +615,9 @@ public void SaveSettings() } 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; @@ -603,6 +683,7 @@ public void SaveSettings() config.ToFileFormat().SaveConfig(Program.ConfigurationPath); MainWindow.UpdateGraphicsConfig(); + MainWindow.MainWindowViewModel.VSyncModeSettingChanged(); SaveSettingsEvent?.Invoke(); diff --git a/src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml b/src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml index f9e192e62..4bc5130f2 100644 --- a/src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml @@ -1,4 +1,4 @@ - + PointerReleased="VSyncMode_PointerReleased" + Text="{Binding VSyncModeText}" + TextAlignment="Start"/> + - + @@ -97,7 +97,23 @@ TextAlignment="Center" /> + + + + + + + + + + + + - \ No newline at end of file + diff --git a/src/Ryujinx.Ava/UI/Views/Settings/SettingsSystemView.axaml b/src/Ryujinx.Ava/UI/Views/Settings/SettingsSystemView.axaml index e6f7c6e46..d8cc86b67 100644 --- a/src/Ryujinx.Ava/UI/Views/Settings/SettingsSystemView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Settings/SettingsSystemView.axaml @@ -4,6 +4,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" @@ -181,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 + diff --git a/src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs index 1aecbd041..5da60d256 100644 --- a/src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs +++ b/src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs @@ -10,6 +10,7 @@ using Ryujinx.Ava.UI.Applet; using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Ava.UI.Views.Main; using Ryujinx.Common.Logging; using Ryujinx.Graphics.Gpu; using Ryujinx.HLE.FileSystem; diff --git a/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs b/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs index b4f2f9468..8b6b60e5c 100644 --- a/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs +++ b/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs @@ -4,7 +4,7 @@ namespace Ryujinx.Common.Configuration.Hid // This breaks Avalonia's TwoWay binding, which makes us unable to save new KeyboardHotkeys. public class KeyboardHotkeys { - public Key ToggleVsync { get; set; } + public Key VSyncMode { get; set; } public Key Screenshot { get; set; } public Key ShowUi { get; set; } public Key Pause { get; set; } @@ -13,5 +13,7 @@ public class KeyboardHotkeys public Key ResScaleDown { get; set; } public Key VolumeUp { get; set; } public Key VolumeDown { get; set; } + public Key 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 000000000..3d90bb98a --- /dev/null +++ b/src/Ryujinx.Common/Configuration/VSyncMode.cs @@ -0,0 +1,15 @@ +using Ryujinx.Common.Utilities; +using System; +using System.Text.Json.Serialization; + +namespace Ryujinx.Common.Configuration +{ + [JsonConverter(typeof(TypedStringEnumConverter))] + [Flags] + public enum VSyncMode + { + Switch = 0, + Unbounded = 1 << 0, + Custom = 1 << 1 + } +} diff --git a/src/Ryujinx.Graphics.GAL/IWindow.cs b/src/Ryujinx.Graphics.GAL/IWindow.cs index 83418e709..48144f0b0 100644 --- a/src/Ryujinx.Graphics.GAL/IWindow.cs +++ b/src/Ryujinx.Graphics.GAL/IWindow.cs @@ -1,3 +1,4 @@ +using Ryujinx.Common.Configuration; using System; namespace Ryujinx.Graphics.GAL @@ -8,7 +9,7 @@ public interface IWindow void SetSize(int width, int height); - void ChangeVSyncMode(bool vsyncEnabled); + void 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 acda37ef3..7a4836982 100644 --- a/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedWindow.cs +++ b/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedWindow.cs @@ -1,3 +1,4 @@ +using Ryujinx.Common.Configuration; using Ryujinx.Graphics.GAL.Multithreading.Commands.Window; using Ryujinx.Graphics.GAL.Multithreading.Model; using Ryujinx.Graphics.GAL.Multithreading.Resources; @@ -31,7 +32,7 @@ public void SetSize(int width, int height) _impl.Window.SetSize(width, height); } - public void ChangeVSyncMode(bool vsyncEnabled) { } + public void 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 000000000..880a5d900 --- /dev/null +++ b/src/Ryujinx.Graphics.GAL/VSyncMode.cs @@ -0,0 +1,9 @@ +namespace Ryujinx.Graphics.GAL +{ + public enum VSyncMode + { + Switch = 0, + Unbounded = 1 << 0, + Custom = 1 << 1 + } +} diff --git a/src/Ryujinx.Graphics.OpenGL/Window.cs b/src/Ryujinx.Graphics.OpenGL/Window.cs index 6bcfefa4e..f881d9530 100644 --- a/src/Ryujinx.Graphics.OpenGL/Window.cs +++ b/src/Ryujinx.Graphics.OpenGL/Window.cs @@ -54,7 +54,7 @@ public void Present(ITexture texture, ImageCrop crop, Action swapBuffersCallback GL.PixelStore(PixelStoreParameter.UnpackAlignment, 4); } - public void ChangeVSyncMode(bool vsyncEnabled) { } + public void 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 2c5764a99..5ad161ea4 100644 --- a/src/Ryujinx.Graphics.Vulkan/Window.cs +++ b/src/Ryujinx.Graphics.Vulkan/Window.cs @@ -29,7 +29,7 @@ class Window : WindowBase, IDisposable private int _width; private int _height; - private bool _vsyncEnabled; + private VSyncMode _vSyncMode; private bool _swapchainIsDirty; private VkFormat _format; private AntiAliasing _currentAntiAliasing; @@ -139,7 +139,7 @@ private unsafe void CreateSwapchain() ImageArrayLayers = 1, PreTransform = capabilities.CurrentTransform, CompositeAlpha = ChooseCompositeAlpha(capabilities.SupportedCompositeAlpha), - PresentMode = ChooseSwapPresentMode(presentModes, _vsyncEnabled), + PresentMode = ChooseSwapPresentMode(presentModes, _vSyncMode), Clipped = true, }; @@ -261,9 +261,9 @@ private static CompositeAlphaFlagsKHR ChooseCompositeAlpha(CompositeAlphaFlagsKH } } - private static PresentModeKHR ChooseSwapPresentMode(PresentModeKHR[] availablePresentModes, bool vsyncEnabled) + private static PresentModeKHR ChooseSwapPresentMode(PresentModeKHR[] availablePresentModes, VSyncMode vSyncMode) { - if (!vsyncEnabled && availablePresentModes.Contains(PresentModeKHR.ImmediateKhr)) + if (vSyncMode == VSyncMode.Unbounded && availablePresentModes.Contains(PresentModeKHR.ImmediateKhr)) { return PresentModeKHR.ImmediateKhr; } @@ -612,9 +612,9 @@ public override void SetSize(int width, int height) // Not needed as we can get the size from the surface. } - public override void ChangeVSyncMode(bool vsyncEnabled) + public override void ChangeVSyncMode(VSyncMode vSyncMode) { - _vsyncEnabled = vsyncEnabled; + _vSyncMode = vSyncMode; _swapchainIsDirty = true; } diff --git a/src/Ryujinx.Graphics.Vulkan/WindowBase.cs b/src/Ryujinx.Graphics.Vulkan/WindowBase.cs index edb9c688c..ca06ec0b8 100644 --- a/src/Ryujinx.Graphics.Vulkan/WindowBase.cs +++ b/src/Ryujinx.Graphics.Vulkan/WindowBase.cs @@ -10,7 +10,7 @@ internal abstract class WindowBase : IWindow public abstract void Dispose(); public abstract void Present(ITexture texture, ImageCrop crop, Action swapBuffersCallback); public abstract void SetSize(int width, int height); - public abstract void ChangeVSyncMode(bool vsyncEnabled); + public abstract void 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.HLE/HLEConfiguration.cs b/src/Ryujinx.HLE/HLEConfiguration.cs index f589bfdda..d336544f6 100644 --- a/src/Ryujinx.HLE/HLEConfiguration.cs +++ b/src/Ryujinx.HLE/HLEConfiguration.cs @@ -9,6 +9,7 @@ using Ryujinx.HLE.HOS.SystemState; using Ryujinx.HLE.Ui; using System; +using VSyncMode = Ryujinx.Common.Configuration.VSyncMode; namespace Ryujinx.HLE { @@ -84,9 +85,14 @@ public class HLEConfiguration internal readonly RegionCode Region; /// - /// Control the initial state of the vertical sync in the SurfaceFlinger service. + /// Control the initial state of the present interval in the SurfaceFlinger service (previously Vsync). /// - internal readonly bool EnableVsync; + internal readonly 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 @@ public HLEConfiguration(VirtualFileSystem virtualFileSystem, IHostUiHandler hostUiHandler, SystemLanguage systemLanguage, RegionCode region, - bool enableVsync, + VSyncMode vSyncMode, bool enableDockedMode, bool enablePtc, bool enableInternetAccess, @@ -194,7 +200,8 @@ public HLEConfiguration(VirtualFileSystem virtualFileSystem, float audioVolume, bool useHypervisor, string multiplayerLanInterfaceId, - MultiplayerMode multiplayerMode) + MultiplayerMode multiplayerMode, + int customVSyncInterval) { VirtualFileSystem = virtualFileSystem; LibHacHorizonManager = libHacHorizonManager; @@ -207,7 +214,8 @@ public HLEConfiguration(VirtualFileSystem virtualFileSystem, 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 fd517b1ae..779e1c5d4 100644 --- a/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/SurfaceFlinger.cs +++ b/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/SurfaceFlinger.cs @@ -10,13 +10,12 @@ using System.Diagnostics; using System.Linq; using System.Threading; +using 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; @@ -34,6 +33,7 @@ class SurfaceFlinger : IConsumerListener, IDisposable private int _swapInterval; private int _swapIntervalDelay; + private bool _targetVSyncIntervalChanged = true; private readonly object _lock = new(); @@ -58,6 +58,7 @@ private class TextureCallbackInformation public SurfaceFlinger(Switch device) { _device = device; + _device.TargetVSyncIntervalChanged += () => _targetVSyncIntervalChanged = true; _layers = new Dictionary(); RenderLayerId = 0; @@ -81,14 +82,18 @@ private void UpdateSwapInterval(int swapInterval) _swapInterval = swapInterval; // If the swap interval is 0, Game VSync is disabled. - if (_swapInterval == 0) - { - _nextFrameEvent.Set(); - _ticksPerFrame = 1; - } - else + if (_targetVSyncIntervalChanged) { - _ticksPerFrame = Stopwatch.Frequency / TargetFps; + _targetVSyncIntervalChanged = false; + if (_swapInterval == 0) + { + _nextFrameEvent.Set(); + _ticksPerFrame = 1; + } + else + { + _ticksPerFrame = Stopwatch.Frequency / (long)_device.TargetVSyncInterval; + } } } @@ -371,14 +376,11 @@ public void Compose() 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); - } + UpdateSwapInterval(0); } - else if (item.SwapInterval != _swapInterval) + else { UpdateSwapInterval(item.SwapInterval); } diff --git a/src/Ryujinx.HLE/Switch.cs b/src/Ryujinx.HLE/Switch.cs index ae063a47d..58e2952b5 100644 --- a/src/Ryujinx.HLE/Switch.cs +++ b/src/Ryujinx.HLE/Switch.cs @@ -27,7 +27,24 @@ public class Switch : IDisposable public TamperMachine TamperMachine { get; } public IHostUiHandler UiHandler { get; } - public bool EnableDeviceVsync { get; set; } = true; + public VSyncMode VSyncMode { get; set; } = VSyncMode.Switch; + public bool CustomVSyncIntervalEnabled { get; set; } = false; + public int CustomVSyncInterval { get; set; } + + public long TargetVSyncInterval { get; set; } = 60; + public Action TargetVSyncIntervalChanged { get; set; } + + public bool EnableDeviceVsync + { + get + { + return VSyncMode == VSyncMode.Switch; + } + set + { + VSyncMode = value ? VSyncMode.Switch : VSyncMode.Unbounded; + } + } public bool IsFrameAvailable => Gpu.Window.IsFrameAvailable; @@ -58,12 +75,20 @@ public Switch(HLEConfiguration configuration) System.State.SetLanguage(Configuration.SystemLanguage); System.State.SetRegion(Configuration.Region); - EnableDeviceVsync = Configuration.EnableVsync; + EnableDeviceVsync = Configuration.VSyncMode == VSyncMode.Switch; + 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(); //todo remove these comments before committing. + // this call seems awkward. maybe this should be part of HOS.Horizon, + // where surfaceflinger is initialized? not sure though since it isn't a genuine + // Switch system setting. either way, if not, we need this call here because + // SurfaceFlinger was initialized with the default target interval + // of 60. #pragma warning restore IDE0055 } @@ -114,6 +139,35 @@ public void PresentFrame(Action swapBuffersCallback) Gpu.Window.Present(swapBuffersCallback); } + public void 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; + } + TargetVSyncIntervalChanged.Invoke(); + } + public void SetVolume(float volume) { System.SetVolume(Math.Clamp(volume, 0, 1)); diff --git a/src/Ryujinx.Headless.SDL2/Options.cs b/src/Ryujinx.Headless.SDL2/Options.cs index ea2063758..751e0caa0 100644 --- a/src/Ryujinx.Headless.SDL2/Options.cs +++ b/src/Ryujinx.Headless.SDL2/Options.cs @@ -114,9 +114,15 @@ public class Options [Option("fs-global-access-log-mode", Required = false, Default = 0, HelpText = "Enables FS access log output to the console.")] public int FsGlobalAccessLogMode { get; set; } - [Option("disable-vsync", Required = false, HelpText = "Disables Vertical Sync.")] + [Option("disable-vsync", Required = false, HelpText = "Disables Vertical Sync. Deprecated. Use vsync-mode instead.")] 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 e545079b9..3c77f9382 100644 --- a/src/Ryujinx.Headless.SDL2/Program.cs +++ b/src/Ryujinx.Headless.SDL2/Program.cs @@ -542,7 +542,7 @@ private static Switch InitializeEmulationContext(WindowBase window, IRenderer re window, options.SystemLanguage, options.SystemRegion, - !options.DisableVSync, + options.VSyncMode, !options.DisableDockedMode, !options.DisablePTC, options.EnableInternetAccess, @@ -556,7 +556,8 @@ private static Switch InitializeEmulationContext(WindowBase window, IRenderer re options.AudioVolume, options.UseHypervisor ?? true, options.MultiplayerLanInterfaceId, - Common.Configuration.Multiplayer.MultiplayerMode.Disabled); + Common.Configuration.Multiplayer.MultiplayerMode.Disabled, + options.CustomVSyncInterval); return new Switch(configuration); } diff --git a/src/Ryujinx.Headless.SDL2/StatusUpdatedEventArgs.cs b/src/Ryujinx.Headless.SDL2/StatusUpdatedEventArgs.cs index 0b199d128..261796758 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 1bfe43121..44db19dc4 100644 --- a/src/Ryujinx.Headless.SDL2/WindowBase.cs +++ b/src/Ryujinx.Headless.SDL2/WindowBase.cs @@ -309,7 +309,7 @@ public void Render() } 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 8a4db1fe7..f17a14049 100644 --- a/src/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormat.cs +++ b/src/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormat.cs @@ -1,3 +1,4 @@ +using Ryujinx.Common; using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Multiplayer; @@ -15,7 +16,7 @@ public class ConfigurationFileFormat /// /// The current version of the file format /// - public const int CurrentVersion = 48; + public const int CurrentVersion = 49; /// /// Version of the configuration file format @@ -170,8 +171,25 @@ public class ConfigurationFileFormat /// /// Enables or disables Vertical Sync /// + /// Kept for file format compatibility (to avoid possible failure when parsing configuration on old versions) + /// TODO: Remove this when those older versions aren't in use anymore. public bool EnableVsync { get; set; } + /// + /// Current 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 b017d384c..4efd452cb 100644 --- a/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs +++ b/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs @@ -468,10 +468,25 @@ public class GraphicsSection public ReactiveObject ShadersDumpPath { get; private set; } /// - /// Enables or disables Vertical Sync + /// Toggles VSync on or off. Deprecated, included for GTK compatibility. /// public ReactiveObject EnableVsync { get; private set; } + /// + /// Toggles the present interval mode. Options are Switch (60Hz), Unbounded (previously Vsync off), and Custom, if enabled. + /// + public ReactiveObject 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 /// @@ -530,8 +545,14 @@ public GraphicsSection() AspectRatio = new ReactiveObject(); AspectRatio.Event += static (sender, e) => LogValueChange(e, nameof(AspectRatio)); ShadersDumpPath = new ReactiveObject(); + VSyncMode = new ReactiveObject(); + VSyncMode.Event += static (sender, e) => LogValueChange(e, nameof(VSyncMode)); + EnableCustomVSyncInterval = new ReactiveObject(); + EnableCustomVSyncInterval.Event += static (sender, e) => LogValueChange(e, nameof(EnableCustomVSyncInterval)); EnableVsync = new ReactiveObject(); EnableVsync.Event += static (sender, e) => LogValueChange(e, nameof(EnableVsync)); + 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(); @@ -680,6 +701,9 @@ public ConfigurationFileFormat ToFileFormat() ShowConfirmExit = ShowConfirmExit, HideCursor = HideCursor, EnableVsync = Graphics.EnableVsync, + VSyncMode = Graphics.VSyncMode, + EnableCustomVSyncInterval = Graphics.EnableCustomVSyncInterval, + CustomVSyncInterval = Graphics.CustomVSyncInterval, EnableShaderCache = Graphics.EnableShaderCache, EnableTextureRecompression = Graphics.EnableTextureRecompression, EnableMacroHLE = Graphics.EnableMacroHLE, @@ -787,6 +811,9 @@ public void LoadDefault() ShowConfirmExit.Value = true; HideCursor.Value = HideCursorMode.OnIdle; Graphics.EnableVsync.Value = true; + Graphics.VSyncMode.Value = VSyncMode.Switch; + Graphics.CustomVSyncInterval.Value = 120; + Graphics.EnableCustomVSyncInterval.Value = false; Graphics.EnableShaderCache.Value = true; Graphics.EnableTextureRecompression.Value = false; Graphics.EnableMacroHLE.Value = true; @@ -845,7 +872,7 @@ public void LoadDefault() Hid.EnableMouse.Value = false; Hid.Hotkeys.Value = new KeyboardHotkeys { - ToggleVsync = Key.F1, + VSyncMode = Key.F1, ToggleMute = Key.F2, Screenshot = Key.F8, ShowUi = Key.F4, @@ -976,7 +1003,7 @@ public void Load(ConfigurationFileFormat configurationFileFormat, string configu configurationFileFormat.Hotkeys = new KeyboardHotkeys { - ToggleVsync = Key.F1, + VSyncMode = Key.F1, }; configurationFileUpdated = true; @@ -1170,7 +1197,7 @@ public void Load(ConfigurationFileFormat configurationFileFormat, string configu configurationFileFormat.Hotkeys = new KeyboardHotkeys { - ToggleVsync = Key.F1, + VSyncMode = Key.F1, Screenshot = Key.F8, }; @@ -1183,7 +1210,7 @@ public void Load(ConfigurationFileFormat configurationFileFormat, string configu configurationFileFormat.Hotkeys = new KeyboardHotkeys { - ToggleVsync = Key.F1, + VSyncMode = Key.F1, Screenshot = Key.F8, ShowUi = Key.F4, }; @@ -1226,7 +1253,7 @@ public void Load(ConfigurationFileFormat configurationFileFormat, string configu configurationFileFormat.Hotkeys = new KeyboardHotkeys { - ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, + VSyncMode = Key.F1, Screenshot = configurationFileFormat.Hotkeys.Screenshot, ShowUi = configurationFileFormat.Hotkeys.ShowUi, Pause = Key.F5, @@ -1241,7 +1268,7 @@ public void Load(ConfigurationFileFormat configurationFileFormat, string configu configurationFileFormat.Hotkeys = new KeyboardHotkeys { - ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, + VSyncMode = Key.F1, Screenshot = configurationFileFormat.Hotkeys.Screenshot, ShowUi = configurationFileFormat.Hotkeys.ShowUi, Pause = configurationFileFormat.Hotkeys.Pause, @@ -1315,7 +1342,7 @@ public void Load(ConfigurationFileFormat configurationFileFormat, string configu configurationFileFormat.Hotkeys = new KeyboardHotkeys { - ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, + VSyncMode = Key.F1, Screenshot = configurationFileFormat.Hotkeys.Screenshot, ShowUi = configurationFileFormat.Hotkeys.ShowUi, Pause = configurationFileFormat.Hotkeys.Pause, @@ -1342,7 +1369,7 @@ public void Load(ConfigurationFileFormat configurationFileFormat, string configu configurationFileFormat.Hotkeys = new KeyboardHotkeys { - ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, + VSyncMode = Key.F1, Screenshot = configurationFileFormat.Hotkeys.Screenshot, ShowUi = configurationFileFormat.Hotkeys.ShowUi, Pause = configurationFileFormat.Hotkeys.Pause, @@ -1430,6 +1457,32 @@ public void Load(ConfigurationFileFormat configurationFileFormat, string configu configurationFileUpdated = true; } + if (configurationFileFormat.Version < 49) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 49."); + + configurationFileFormat.VSyncMode = VSyncMode.Switch; + configurationFileFormat.EnableCustomVSyncInterval = false; + + configurationFileFormat.Hotkeys = new KeyboardHotkeys + { + VSyncMode = 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 = 90; + + configurationFileUpdated = true; + } + Logger.EnableFileLog.Value = configurationFileFormat.EnableFileLog; Graphics.ResScale.Value = configurationFileFormat.ResScale; Graphics.ResScaleCustom.Value = configurationFileFormat.ResScaleCustom; @@ -1461,7 +1514,10 @@ public void Load(ConfigurationFileFormat configurationFileFormat, string configu CheckUpdatesOnStart.Value = configurationFileFormat.CheckUpdatesOnStart; ShowConfirmExit.Value = configurationFileFormat.ShowConfirmExit; HideCursor.Value = configurationFileFormat.HideCursor; - Graphics.EnableVsync.Value = configurationFileFormat.EnableVsync; + Graphics.EnableVsync.Value = configurationFileFormat.VSyncMode == VSyncMode.Switch; + 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/Ui/MainWindow.cs b/src/Ryujinx/Ui/MainWindow.cs index 6cce034b6..2cd10840b 100644 --- a/src/Ryujinx/Ui/MainWindow.cs +++ b/src/Ryujinx/Ui/MainWindow.cs @@ -44,6 +44,7 @@ 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 { @@ -655,7 +656,7 @@ private void InitializeSwitchInstance() _uiHandler, (SystemLanguage)ConfigurationState.Instance.System.Language.Value, (RegionCode)ConfigurationState.Instance.System.Region.Value, - ConfigurationState.Instance.Graphics.EnableVsync, + ConfigurationState.Instance.Graphics.EnableVsync ? VSyncMode.Switch : VSyncMode.Unbounded, ConfigurationState.Instance.System.EnableDockedMode, ConfigurationState.Instance.System.EnablePtc, ConfigurationState.Instance.System.EnableInternetAccess, @@ -669,7 +670,8 @@ private void InitializeSwitchInstance() ConfigurationState.Instance.System.AudioVolume, ConfigurationState.Instance.System.UseHypervisor, ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value, - ConfigurationState.Instance.Multiplayer.Mode); + ConfigurationState.Instance.Multiplayer.Mode, + ConfigurationState.Instance.Graphics.CustomVSyncInterval); _emulationContext = new HLE.Switch(configuration); } @@ -1211,7 +1213,7 @@ private void Update_StatusBar(object sender, StatusUpdatedEventArgs args) _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)); diff --git a/src/Ryujinx/Ui/RendererWidgetBase.cs b/src/Ryujinx/Ui/RendererWidgetBase.cs index 7660f190e..fa537745a 100644 --- a/src/Ryujinx/Ui/RendererWidgetBase.cs +++ b/src/Ryujinx/Ui/RendererWidgetBase.cs @@ -25,6 +25,7 @@ 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 { @@ -450,7 +451,7 @@ public void Render() Device.Gpu.SetGpuThread(); Device.Gpu.InitializeShaderCache(_gpuCancellationTokenSource.Token); - Renderer.Window.ChangeVSyncMode(Device.EnableDeviceVsync); + Renderer.Window.ChangeVSyncMode(Device.EnableDeviceVsync ? VSyncMode.Switch : VSyncMode.Unbounded); (Toplevel as MainWindow)?.ActivatePauseMenu(); @@ -487,7 +488,7 @@ public void Render() } StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs( - Device.EnableDeviceVsync, + Device.EnableDeviceVsync ? VSyncMode.Switch.ToString() : VSyncMode.Unbounded.ToString(), Device.GetVolume(), _gpuBackendName, dockedMode, @@ -752,7 +753,7 @@ private KeyboardHotkeyState GetHotkeyState() { KeyboardHotkeyState state = KeyboardHotkeyState.None; - if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ToggleVsync)) + if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.VSyncMode)) { state |= KeyboardHotkeyState.ToggleVSync; } diff --git a/src/Ryujinx/Ui/StatusUpdatedEventArgs.cs b/src/Ryujinx/Ui/StatusUpdatedEventArgs.cs index 72e7d7f5b..87b1d8d17 100644 --- a/src/Ryujinx/Ui/StatusUpdatedEventArgs.cs +++ b/src/Ryujinx/Ui/StatusUpdatedEventArgs.cs @@ -4,7 +4,7 @@ namespace Ryujinx.Ui { public class StatusUpdatedEventArgs : EventArgs { - public bool VSyncEnabled; + public string VSyncMode; public float Volume; public string DockedMode; public string AspectRatio; @@ -13,9 +13,9 @@ public class StatusUpdatedEventArgs : EventArgs public string GpuName; public string GpuBackend; - public StatusUpdatedEventArgs(bool vSyncEnabled, float volume, string gpuBackend, string dockedMode, string aspectRatio, string gameStatus, string fifoStatus, string gpuName) + public StatusUpdatedEventArgs(string 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;