diff --git a/src/Ryujinx.Ava/AppHost.cs b/src/Ryujinx.Ava/AppHost.cs index c473cf562..4fa3b172c 100644 --- a/src/Ryujinx.Ava/AppHost.cs +++ b/src/Ryujinx.Ava/AppHost.cs @@ -1,4 +1,4 @@ -using ARMeilleure.Translation; +using ARMeilleure.Translation; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; @@ -58,6 +58,7 @@ using IRenderer = Ryujinx.Graphics.GAL.IRenderer; using Key = Ryujinx.Input.Key; using MouseButton = Ryujinx.Input.MouseButton; +using PresentIntervalState = Ryujinx.Common.Configuration.PresentIntervalState; using ScalingFilter = Ryujinx.Common.Configuration.ScalingFilter; using Size = Avalonia.Size; using Switch = Ryujinx.HLE.Switch; @@ -189,6 +190,9 @@ public AppHost( ConfigurationState.Instance.Graphics.ScalingFilter.Event += UpdateScalingFilter; ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event += UpdateScalingFilterLevel; ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Event += UpdateColorSpacePassthrough; + ConfigurationState.Instance.Graphics.PresentIntervalState.Event += UpdatePresentIntervalState; + ConfigurationState.Instance.Graphics.CustomPresentInterval.Event += UpdateCustomPresentIntervalValue; + ConfigurationState.Instance.Graphics.EnableCustomPresentInterval.Event += UpdateCustomPresentIntervalEnabled; ConfigurationState.Instance.Multiplayer.LanInterfaceId.Event += UpdateLanInterfaceIdState; ConfigurationState.Instance.Multiplayer.Mode.Event += UpdateMultiplayerModeState; @@ -235,6 +239,37 @@ private void UpdateColorSpacePassthrough(object sender, ReactiveEventArgs _renderer.Window?.SetColorSpacePassthrough((bool)ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Value); } + private void UpdatePresentIntervalState(object sender, ReactiveEventArgs e) + { + if (Device != null) + { + Device.PresentIntervalState = e.NewValue; + Device.UpdatePresentInterval(); + } + //vulkan present mode may change in response, so recreate the swapchain + _renderer.Window?.ChangePresentIntervalState((Ryujinx.Graphics.GAL.PresentIntervalState)e.NewValue); + ConfigurationState.Instance.Graphics.PresentIntervalState.Value = e.NewValue; + ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); + } + + private void UpdateCustomPresentIntervalValue(object sender, ReactiveEventArgs e) + { + if (Device != null) + { + Device.TargetPresentInterval = e.NewValue; + Device.UpdatePresentInterval(); + } + } + + private void UpdateCustomPresentIntervalEnabled(object sender, ReactiveEventArgs e) + { + if (Device != null) + { + Device.CustomPresentIntervalEnabled = e.NewValue; + Device.UpdatePresentInterval(); + } + } + private void ShowCursor() { Dispatcher.UIThread.Post(() => @@ -516,6 +551,12 @@ private void HideCursorState_Changed(object sender, ReactiveEventArgs(oldState, presentIntervalState)); + } + public async Task LoadGuestApplication() { InitializeSwitchInstance(); @@ -775,7 +816,7 @@ private void InitializeSwitchInstance() _viewModel.UiHandler, (SystemLanguage)ConfigurationState.Instance.System.Language.Value, (RegionCode)ConfigurationState.Instance.System.Region.Value, - ConfigurationState.Instance.Graphics.EnableVsync, + ConfigurationState.Instance.Graphics.PresentIntervalState, ConfigurationState.Instance.System.EnableDockedMode, ConfigurationState.Instance.System.EnablePtc, ConfigurationState.Instance.System.EnableInternetAccess, @@ -789,7 +830,8 @@ private void InitializeSwitchInstance() ConfigurationState.Instance.System.AudioVolume, ConfigurationState.Instance.System.UseHypervisor, ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value, - ConfigurationState.Instance.Multiplayer.Mode); + ConfigurationState.Instance.Multiplayer.Mode, + ConfigurationState.Instance.Graphics.CustomPresentInterval.Value); Device = new Switch(configuration); } @@ -915,7 +957,7 @@ private void RenderLoop() Device.Gpu.InitializeShaderCache(_gpuCancellationTokenSource.Token); Translator.IsReadyForTranslation.Set(); - _renderer.Window.ChangeVSyncMode(Device.EnableDeviceVsync); + _renderer.Window.ChangePresentIntervalState((Ryujinx.Graphics.GAL.PresentIntervalState)Device.PresentIntervalState); while (_isActive) { @@ -963,6 +1005,7 @@ public void UpdateStatus() { // Run a status update only when a frame is to be drawn. This prevents from updating the ui and wasting a render when no frame is queued. string dockedMode = ConfigurationState.Instance.System.EnableDockedMode ? LocaleManager.Instance[LocaleKeys.Docked] : LocaleManager.Instance[LocaleKeys.Handheld]; + string presentIntervalState = Device.PresentIntervalState.ToString(); if (GraphicsConfig.ResScale != 1) { @@ -970,7 +1013,7 @@ public void UpdateStatus() } StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs( - Device.EnableDeviceVsync, + presentIntervalState, LocaleManager.Instance[LocaleKeys.VolumeShort] + $": {(int)(Device.GetVolume() * 100)}%", ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.Vulkan ? "Vulkan" : "OpenGL", dockedMode, @@ -1062,9 +1105,40 @@ private bool UpdateFrame() { switch (currentHotkeyState) { - case KeyboardHotkeyState.ToggleVSync: - Device.EnableDeviceVsync = !Device.EnableDeviceVsync; - + //todo default + case KeyboardHotkeyState.TogglePresentIntervalState: + PresentIntervalState oldState = Device.PresentIntervalState; + PresentIntervalState newState; + if (oldState == PresentIntervalState.Switch) + { + newState = PresentIntervalState.Unbounded; + } + else if (oldState == PresentIntervalState.Unbounded) + { + if (ConfigurationState.Instance.Graphics.EnableCustomPresentInterval) + { + newState = PresentIntervalState.Custom; + } + else + { + newState = PresentIntervalState.Switch; + } + } + else + { + newState = PresentIntervalState.Switch; + } + UpdatePresentIntervalState(this, new ReactiveEventArgs(oldState, newState)); + _viewModel.ShowCustomPresentIntervalPicker = + (newState == PresentIntervalState.Custom); + break; + case KeyboardHotkeyState.CustomPresentIntervalDecrement: + Device.DecrementCustomPresentInterval(); + _viewModel.CustomPresentInterval -= 1; + break; + case KeyboardHotkeyState.CustomPresentIntervalIncrement: + Device.IncrementCustomPresentInterval(); + _viewModel.CustomPresentInterval += 1; break; case KeyboardHotkeyState.Screenshot: ScreenshotRequested = true; @@ -1150,9 +1224,9 @@ private KeyboardHotkeyState GetHotkeyState() { KeyboardHotkeyState state = KeyboardHotkeyState.None; - if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ToggleVsync)) + if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.PresentIntervalState)) { - state = KeyboardHotkeyState.ToggleVSync; + state = KeyboardHotkeyState.TogglePresentIntervalState; } else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.Screenshot)) { @@ -1186,6 +1260,14 @@ private KeyboardHotkeyState GetHotkeyState() { state = KeyboardHotkeyState.VolumeDown; } + else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CustomPresentIntervalIncrement)) + { + state = KeyboardHotkeyState.CustomPresentIntervalIncrement; + } + else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CustomPresentIntervalDecrement)) + { + state = KeyboardHotkeyState.CustomPresentIntervalDecrement; + } return state; } diff --git a/src/Ryujinx.Ava/Assets/Locales/en_US.json b/src/Ryujinx.Ava/Assets/Locales/en_US.json index 53e277ba9..bfe855e2f 100644 --- a/src/Ryujinx.Ava/Assets/Locales/en_US.json +++ b/src/Ryujinx.Ava/Assets/Locales/en_US.json @@ -122,9 +122,18 @@ "SettingsTabSystemSystemLanguageLatinAmericanSpanish": "Latin American Spanish", "SettingsTabSystemSystemLanguageSimplifiedChinese": "Simplified Chinese", "SettingsTabSystemSystemLanguageTraditionalChinese": "Traditional Chinese", - "SettingsTabSystemSystemTimeZone": "System TimeZone:", + "SettingsTabSystemSystemTimeZone": "System Time Zone:", "SettingsTabSystemSystemTime": "System Time:", - "SettingsTabSystemEnableVsync": "VSync", + "SettingsTabSystemPresentIntervalState": "Present Interval Mode (VSync):", + "SettingsTabSystemEnableCustomPresentInterval": "Enable toggle for custom present interval", + "SettingsTabSystemPresentIntervalStateSwitch": "60 (VSync ON)", + "SettingsTabSystemPresentIntervalStateUnbounded": "Unbounded (VSync OFF)", + "SettingsTabSystemPresentIntervalStateCustom": "Custom", + "SettingsTabSystemPresentIntervalStateTooltip": "Previously VSync. Use the Custom mode to set a specific refresh interval target. In some titles, the custom mode will act as an FPS cap. In others, it may lead to unpredictable behavior or do nothing at all. \n\nLeave at 60 (VSync ON) if unsure.", + "SettingsTabSystemEnableCustomPresentIntervalTooltip": "The present interval mode toggle will also cycle through the custom interval mode.", + "SettingsTabSystemCustomPresentIntervalValueTooltip": "The custom present interval target value.", + "SettingsTabSystemCustomPresentIntervalSliderTooltip": "The custom present interval target, as a percentage of the normal Switch interval.", + "SettingsTabSystemCustomPresentIntervalValue": "Custom Present Interval Value:", "SettingsTabSystemEnablePptc": "PPTC (Profiled Persistent Translation Cache)", "SettingsTabSystemEnableFsIntegrityChecks": "FS Integrity Checks", "SettingsTabSystemAudioBackend": "Audio Backend:", @@ -132,6 +141,7 @@ "SettingsTabSystemAudioBackendOpenAL": "OpenAL", "SettingsTabSystemAudioBackendSoundIO": "SoundIO", "SettingsTabSystemAudioBackendSDL2": "SDL2", + "SettingsTabSystemCustomPresentInterval": "Interval", "SettingsTabSystemHacks": "Hacks", "SettingsTabSystemHacksNote": "May cause instability", "SettingsTabSystemExpandDramSize": "Use alternative memory layout (Developers)", @@ -571,11 +581,13 @@ "RyujinxUpdater": "Ryujinx Updater", "SettingsTabHotkeys": "Keyboard Hotkeys", "SettingsTabHotkeysHotkeys": "Keyboard Hotkeys", - "SettingsTabHotkeysToggleVsyncHotkey": "Toggle VSync:", + "SettingsTabHotkeysTogglePresentIntervalStateHotkey": "Toggle Present Interval state:", "SettingsTabHotkeysScreenshotHotkey": "Screenshot:", "SettingsTabHotkeysShowUiHotkey": "Show UI:", "SettingsTabHotkeysPauseHotkey": "Pause:", "SettingsTabHotkeysToggleMuteHotkey": "Mute:", + "SettingsTabHotkeysIncrementCustomPresentIntervalHotkey": "Raise custom refresh interval", + "SettingsTabHotkeysDecrementCustomPresentIntervalHotkey": "Lower custom refresh interval", "ControllerMotionTitle": "Motion Control Settings", "ControllerRumbleTitle": "Rumble Settings", "SettingsSelectThemeFileDialogTitle": "Select Theme File", diff --git a/src/Ryujinx.Ava/Assets/Styles/Themes.xaml b/src/Ryujinx.Ava/Assets/Styles/Themes.xaml index 0f323f84b..056eba228 100644 --- a/src/Ryujinx.Ava/Assets/Styles/Themes.xaml +++ b/src/Ryujinx.Ava/Assets/Styles/Themes.xaml @@ -26,8 +26,9 @@ #b3ffffff #80cccccc #A0000000 - #FF2EEAC9 - #FFFF4554 + #FF2EEAC9 + #FFFF4554 + #6483F5 _vsyncColor; + get => _presentIntervalStateColor; set { - _vsyncColor = value; + _presentIntervalStateColor = value; OnPropertyChanged(); } } + public bool ShowCustomPresentIntervalPicker + { + get + { + if (_isGameRunning) + { + return AppHost.Device.PresentIntervalState == + PresentIntervalState.Custom; + } + else + { + return false; + } + } + set + { + OnPropertyChanged(); + } + } + + public int CustomPresentIntervalPercentageProxy + { + get => _customPresentIntervalPercentageProxy; + set + { + int newInterval = (int)(((decimal)value / 100) * 60); + _customPresentInterval = newInterval; + _customPresentIntervalPercentageProxy = value; + ConfigurationState.Instance.Graphics.CustomPresentInterval.Value = newInterval; + if (_isGameRunning) + { + AppHost.Device.CustomPresentInterval = newInterval; + AppHost.Device.UpdatePresentInterval(); + } + OnPropertyChanged((nameof(CustomPresentInterval))); + OnPropertyChanged((nameof(CustomPresentIntervalPercentageText))); + } + } + + public string CustomPresentIntervalPercentageText + { + get + { + string text = CustomPresentIntervalPercentageProxy.ToString() + "%"; + return text; + } + set + { + + } + } + + public int CustomPresentInterval + { + get => _customPresentInterval; + set + { + _customPresentInterval = value; + int newPercent = (int)(((decimal)value / 60) * 100); + _customPresentIntervalPercentageProxy = newPercent; + ConfigurationState.Instance.Graphics.CustomPresentInterval.Value = value; + if (_isGameRunning) + { + AppHost.Device.CustomPresentInterval = value; + AppHost.Device.UpdatePresentInterval(); + } + OnPropertyChanged(nameof(CustomPresentIntervalPercentageProxy)); + OnPropertyChanged(nameof(CustomPresentIntervalPercentageText)); + OnPropertyChanged(); + } + } + public byte[] SelectedIcon { get => _selectedIcon; @@ -499,6 +578,17 @@ public string BackendText } } + public string PresentIntervalStateText + { + get => _presentIntervalStateText; + set + { + _presentIntervalStateText = value; + + OnPropertyChanged(); + } + } + public string DockedStatusText { get => _dockedStatusText; @@ -1183,17 +1273,18 @@ private void Update_StatusBar(object sender, StatusUpdatedEventArgs args) { Dispatcher.UIThread.InvokeAsync(() => { - Application.Current.Styles.TryGetResource(args.VSyncEnabled - ? "VsyncEnabled" - : "VsyncDisabled", - Application.Current.ActualThemeVariant, + Application.Current.Styles.TryGetResource(args.PresentIntervalState, + Avalonia.Application.Current.ActualThemeVariant, out object color); if (color is not null) { - VsyncColor = new SolidColorBrush((Color)color); + PresentIntervalStateColor = new SolidColorBrush((Color)color); } + PresentIntervalStateText = args.PresentIntervalState; + ShowCustomPresentIntervalPicker = + args.PresentIntervalState == PresentIntervalState.Custom.ToString(); DockedStatusText = args.DockedMode; AspectRatioStatusText = args.AspectRatio; GameStatusText = args.GameStatus; @@ -1347,6 +1438,57 @@ public void ToggleDockMode() } } + public void UpdatePresentIntervalState() + { + PresentIntervalState oldPresentInterval = AppHost.Device.PresentIntervalState; + PresentIntervalState newPresentInterval = PresentIntervalState.Switch; + bool customPresentIntervalEnabled = ConfigurationState.Instance.Graphics.EnableCustomPresentInterval.Value; + + switch (oldPresentInterval) + { + case PresentIntervalState.Switch: + newPresentInterval = PresentIntervalState.Unbounded; + break; + case PresentIntervalState.Unbounded: + if (customPresentIntervalEnabled) + { + newPresentInterval = PresentIntervalState.Custom; + } + else + { + newPresentInterval = PresentIntervalState.Switch; + } + break; + case PresentIntervalState.Custom: + newPresentInterval = PresentIntervalState.Switch; + break; + } + + ConfigurationState.Instance.Graphics.PresentIntervalState.Value = newPresentInterval; + + if (_isGameRunning) + { + AppHost.UpdatePresentInterval(newPresentInterval); + } + + OnPropertyChanged(nameof(ShowCustomPresentIntervalPicker)); + } + + public void PresentIntervalStateSettingChanged() + { + if (_isGameRunning) + { + AppHost.Device.CustomPresentInterval = ConfigurationState.Instance.Graphics.CustomPresentInterval.Value; + AppHost.Device.UpdatePresentInterval(); + } + + CustomPresentInterval = ConfigurationState.Instance.Graphics.CustomPresentInterval.Value; + OnPropertyChanged(nameof(ShowCustomPresentIntervalPicker)); + OnPropertyChanged(nameof(CustomPresentIntervalPercentageProxy)); + OnPropertyChanged(nameof(CustomPresentIntervalPercentageText)); + OnPropertyChanged(nameof(CustomPresentInterval)); + } + public async Task ExitCurrentState() { if (WindowState == WindowState.FullScreen) diff --git a/src/Ryujinx.Ava/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/SettingsViewModel.cs index 604e34067..564677bec 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/SettingsViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/SettingsViewModel.cs @@ -7,6 +7,7 @@ using Ryujinx.Audio.Backends.SoundIo; using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.Views.Main; using Ryujinx.Ava.UI.Windows; using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration.Hid; @@ -51,6 +52,10 @@ public class SettingsViewModel : BaseModel private string _customThemePath; private int _scalingFilter; private int _scalingFilterLevel; + private int _customPresentInterval; + private bool _enableCustomPresentInterval; + private int _customPresentIntervalPercentageProxy; + private PresentIntervalState _presentIntervalState; public event Action CloseWindow; public event Action SaveSettingsEvent; @@ -137,7 +142,75 @@ public bool DirectoryChanged public bool EnableDockedMode { get; set; } public bool EnableKeyboard { get; set; } public bool EnableMouse { get; set; } - public bool EnableVsync { get; set; } + public PresentIntervalState PresentIntervalState + { + get => _presentIntervalState; + set + { + if (value == PresentIntervalState.Custom) + { + EnableCustomPresentInterval = true; + } + _presentIntervalState = value; + OnPropertyChanged(); + OnPropertyChanged((nameof(EnableCustomPresentInterval))); + } + } + + public int CustomPresentIntervalPercentageProxy + { + get => _customPresentIntervalPercentageProxy; + set + { + int newInterval = (int)(((decimal)value / 100) * 60); + _customPresentInterval = newInterval; + _customPresentIntervalPercentageProxy = value; + OnPropertyChanged((nameof(CustomPresentInterval))); + OnPropertyChanged((nameof(CustomPresentIntervalPercentageText))); + } + } + + public string CustomPresentIntervalPercentageText + { + get + { + string text = CustomPresentIntervalPercentageProxy.ToString() + "%"; + return text; + } + set + { + + } + } + + public bool EnableCustomPresentInterval + { + get => _enableCustomPresentInterval; + set + { + _enableCustomPresentInterval = value; + if (_presentIntervalState == PresentIntervalState.Custom && value == false) + { + PresentIntervalState = PresentIntervalState.Switch; + OnPropertyChanged(nameof(PresentIntervalState)); + } + OnPropertyChanged(); + } + } + + public int CustomPresentInterval + { + get => _customPresentInterval; + set + { + _customPresentInterval = value; + int newPercent = (int)(((decimal)value / 60) * 100); + _customPresentIntervalPercentageProxy = newPercent; + OnPropertyChanged(nameof(CustomPresentIntervalPercentageProxy)); + OnPropertyChanged(nameof(CustomPresentIntervalPercentageText)); + OnPropertyChanged(); + } + } public bool EnablePptc { get; set; } public bool EnableInternetAccess { get; set; } public bool EnableFsIntegrityChecks { get; set; } @@ -448,7 +521,9 @@ public void LoadCurrentConfiguration() CurrentDate = currentDateTime.Date; CurrentTime = currentDateTime.TimeOfDay.Add(TimeSpan.FromSeconds(config.System.SystemTimeOffset)); - EnableVsync = config.Graphics.EnableVsync; + EnableCustomPresentInterval = config.Graphics.EnableCustomPresentInterval.Value; + CustomPresentInterval = config.Graphics.CustomPresentInterval; + PresentIntervalState = config.Graphics.PresentIntervalState; EnableFsIntegrityChecks = config.System.EnableFsIntegrityChecks; ExpandDramSize = config.System.ExpandRam; IgnoreMissingServices = config.System.IgnoreMissingServices; @@ -460,7 +535,7 @@ public void LoadCurrentConfiguration() // Graphics GraphicsBackendIndex = (int)config.Graphics.GraphicsBackend.Value; - // Physical devices are queried asynchronously hence the prefered index config value is loaded in LoadAvailableGpus(). + // Physical devices are queried asynchronously hence the preferred index config value is loaded in LoadAvailableGpus(). EnableShaderCache = config.Graphics.EnableShaderCache; EnableTextureRecompression = config.Graphics.EnableTextureRecompression; EnableMacroHLE = config.Graphics.EnableMacroHLE; @@ -537,7 +612,9 @@ public void SaveSettings() } config.System.SystemTimeOffset.Value = Convert.ToInt64((CurrentDate.ToUnixTimeSeconds() + CurrentTime.TotalSeconds) - DateTimeOffset.Now.ToUnixTimeSeconds()); - config.Graphics.EnableVsync.Value = EnableVsync; + config.Graphics.PresentIntervalState.Value = PresentIntervalState; + config.Graphics.EnableCustomPresentInterval.Value = EnableCustomPresentInterval; + config.Graphics.CustomPresentInterval.Value = CustomPresentInterval; config.System.EnableFsIntegrityChecks.Value = EnableFsIntegrityChecks; config.System.ExpandRam.Value = ExpandDramSize; config.System.IgnoreMissingServices.Value = IgnoreMissingServices; @@ -603,6 +680,7 @@ public void SaveSettings() config.ToFileFormat().SaveConfig(Program.ConfigurationPath); MainWindow.UpdateGraphicsConfig(); + MainWindow.MainWindowViewModel.PresentIntervalStateSettingChanged(); SaveSettingsEvent?.Invoke(); diff --git a/src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml b/src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml index 58e06a1c2..d3a68b35d 100644 --- a/src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml @@ -78,15 +78,69 @@ MaxHeight="18" Orientation="Horizontal"> + PointerReleased="PresentIntervalState_PointerReleased" + Text="{Binding PresentIntervalStateText}" + TextAlignment="Left"/> + - + @@ -97,7 +97,23 @@ TextAlignment="Center" /> + + + + + + + + + + + + - \ No newline at end of file + diff --git a/src/Ryujinx.Ava/UI/Views/Settings/SettingsSystemView.axaml b/src/Ryujinx.Ava/UI/Views/Settings/SettingsSystemView.axaml index e6f7c6e46..3ded1e384 100644 --- a/src/Ryujinx.Ava/UI/Views/Settings/SettingsSystemView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Settings/SettingsSystemView.axaml @@ -4,6 +4,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" @@ -181,10 +182,67 @@ Width="350" ToolTip.Tip="{locale:Locale TimeTooltip}" /> - + + VerticalAlignment="Center" + Text="{locale:Locale SettingsTabSystemPresentIntervalState}" + ToolTip.Tip="{locale:Locale SettingsTabSystemPresentIntervalStateTooltip}" + Width="250" /> + + + + + + + + + + + + + + + + + + + + - \ No newline at end of file + diff --git a/src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs index c78f4160d..fae80bc94 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 f0707c73d..1b6de040d 100644 --- a/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs +++ b/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs @@ -4,7 +4,7 @@ // This breaks Avalonia's TwoWay binding, which makes us unable to save new KeyboardHotkeys. public class KeyboardHotkeys { - public Key ToggleVsync { get; set; } + public Key PresentIntervalState { get; set; } public Key Screenshot { get; set; } public Key ShowUi { get; set; } public Key Pause { get; set; } @@ -13,5 +13,7 @@ public class KeyboardHotkeys public Key ResScaleDown { get; set; } public Key VolumeUp { get; set; } public Key VolumeDown { get; set; } + public Key CustomPresentIntervalIncrement { get; set; } + public Key CustomPresentIntervalDecrement { get; set; } } } diff --git a/src/Ryujinx.Common/Configuration/PresentIntervalState.cs b/src/Ryujinx.Common/Configuration/PresentIntervalState.cs new file mode 100644 index 000000000..98c3e88c7 --- /dev/null +++ b/src/Ryujinx.Common/Configuration/PresentIntervalState.cs @@ -0,0 +1,15 @@ +using Ryujinx.Common.Utilities; +using System; +using System.Text.Json.Serialization; + +namespace Ryujinx.Common.Configuration +{ + [JsonConverter(typeof(TypedStringEnumConverter))] + [Flags] + public enum PresentIntervalState + { + Switch = 0, + Unbounded = 1 << 0, + Custom = 1 << 1 + } +} diff --git a/src/Ryujinx.Graphics.GAL/IWindow.cs b/src/Ryujinx.Graphics.GAL/IWindow.cs index 83418e709..285aa1d5f 100644 --- a/src/Ryujinx.Graphics.GAL/IWindow.cs +++ b/src/Ryujinx.Graphics.GAL/IWindow.cs @@ -1,3 +1,4 @@ +using Ryujinx.Common.Configuration; using System; namespace Ryujinx.Graphics.GAL @@ -8,7 +9,7 @@ public interface IWindow void SetSize(int width, int height); - void ChangeVSyncMode(bool vsyncEnabled); + void ChangePresentIntervalState(PresentIntervalState presentIntervalState); void SetAntiAliasing(AntiAliasing antialiasing); void SetScalingFilter(ScalingFilter type); diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedWindow.cs b/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedWindow.cs index 4f1b795a4..344c0d808 100644 --- a/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedWindow.cs +++ b/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedWindow.cs @@ -1,4 +1,5 @@ -using Ryujinx.Graphics.GAL.Multithreading.Commands.Window; +using Ryujinx.Common.Configuration; +using Ryujinx.Graphics.GAL.Multithreading.Commands.Window; using Ryujinx.Graphics.GAL.Multithreading.Model; using Ryujinx.Graphics.GAL.Multithreading.Resources; using System; @@ -31,7 +32,7 @@ public void SetSize(int width, int height) _impl.Window.SetSize(width, height); } - public void ChangeVSyncMode(bool vsyncEnabled) { } + public void ChangePresentIntervalState(PresentIntervalState presentIntervalState) { } public void SetAntiAliasing(AntiAliasing effect) { } diff --git a/src/Ryujinx.Graphics.GAL/PresentIntervalState.cs b/src/Ryujinx.Graphics.GAL/PresentIntervalState.cs new file mode 100644 index 000000000..8c4cca19e --- /dev/null +++ b/src/Ryujinx.Graphics.GAL/PresentIntervalState.cs @@ -0,0 +1,9 @@ +namespace Ryujinx.Graphics.GAL +{ + public enum PresentIntervalState + { + Switch = 0, + Unbounded = 1 << 0, + Custom = 1 << 1 + } +} diff --git a/src/Ryujinx.Graphics.OpenGL/Window.cs b/src/Ryujinx.Graphics.OpenGL/Window.cs index 6bcfefa4e..c85e29e6a 100644 --- a/src/Ryujinx.Graphics.OpenGL/Window.cs +++ b/src/Ryujinx.Graphics.OpenGL/Window.cs @@ -54,7 +54,7 @@ public void Present(ITexture texture, ImageCrop crop, Action swapBuffersCallback GL.PixelStore(PixelStoreParameter.UnpackAlignment, 4); } - public void ChangeVSyncMode(bool vsyncEnabled) { } + public void ChangePresentIntervalState(PresentIntervalState presentIntervalState) { } public void SetSize(int width, int height) { diff --git a/src/Ryujinx.Graphics.Vulkan/Window.cs b/src/Ryujinx.Graphics.Vulkan/Window.cs index afaa7defe..46b1d35b9 100644 --- a/src/Ryujinx.Graphics.Vulkan/Window.cs +++ b/src/Ryujinx.Graphics.Vulkan/Window.cs @@ -29,7 +29,7 @@ class Window : WindowBase, IDisposable private int _width; private int _height; - private bool _vsyncEnabled; + private PresentIntervalState _presentIntervalState; private bool _swapchainIsDirty; private VkFormat _format; private AntiAliasing _currentAntiAliasing; @@ -139,7 +139,7 @@ private unsafe void CreateSwapchain() ImageArrayLayers = 1, PreTransform = capabilities.CurrentTransform, CompositeAlpha = ChooseCompositeAlpha(capabilities.SupportedCompositeAlpha), - PresentMode = ChooseSwapPresentMode(presentModes, _vsyncEnabled), + PresentMode = ChooseSwapPresentMode(presentModes, _presentIntervalState), Clipped = true, }; @@ -261,9 +261,9 @@ private static CompositeAlphaFlagsKHR ChooseCompositeAlpha(CompositeAlphaFlagsKH } } - private static PresentModeKHR ChooseSwapPresentMode(PresentModeKHR[] availablePresentModes, bool vsyncEnabled) + private static PresentModeKHR ChooseSwapPresentMode(PresentModeKHR[] availablePresentModes, PresentIntervalState presentIntervalState) { - if (!vsyncEnabled && availablePresentModes.Contains(PresentModeKHR.ImmediateKhr)) + if (presentIntervalState == PresentIntervalState.Unbounded && availablePresentModes.Contains(PresentModeKHR.ImmediateKhr)) { return PresentModeKHR.ImmediateKhr; } @@ -612,9 +612,9 @@ public override void SetSize(int width, int height) // Not needed as we can get the size from the surface. } - public override void ChangeVSyncMode(bool vsyncEnabled) + public override void ChangePresentIntervalState(PresentIntervalState presentIntervalState) { - _vsyncEnabled = vsyncEnabled; + _presentIntervalState = presentIntervalState; _swapchainIsDirty = true; } diff --git a/src/Ryujinx.Graphics.Vulkan/WindowBase.cs b/src/Ryujinx.Graphics.Vulkan/WindowBase.cs index da1613f41..d8774cb75 100644 --- a/src/Ryujinx.Graphics.Vulkan/WindowBase.cs +++ b/src/Ryujinx.Graphics.Vulkan/WindowBase.cs @@ -10,7 +10,7 @@ internal abstract class WindowBase : IWindow public abstract void Dispose(); public abstract void Present(ITexture texture, ImageCrop crop, Action swapBuffersCallback); public abstract void SetSize(int width, int height); - public abstract void ChangeVSyncMode(bool vsyncEnabled); + public abstract void ChangePresentIntervalState(PresentIntervalState presentIntervalState); public abstract void SetAntiAliasing(AntiAliasing effect); public abstract void SetScalingFilter(ScalingFilter scalerType); public abstract void SetScalingFilterLevel(float scale); diff --git a/src/Ryujinx.HLE/HLEConfiguration.cs b/src/Ryujinx.HLE/HLEConfiguration.cs index b1ba11b59..3e330b727 100644 --- a/src/Ryujinx.HLE/HLEConfiguration.cs +++ b/src/Ryujinx.HLE/HLEConfiguration.cs @@ -1,4 +1,4 @@ -using LibHac.Tools.FsSystem; +using LibHac.Tools.FsSystem; using Ryujinx.Audio.Integration; using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration.Multiplayer; @@ -9,6 +9,7 @@ using Ryujinx.HLE.HOS.SystemState; using Ryujinx.HLE.Ui; using System; +using PresentIntervalState = Ryujinx.Common.Configuration.PresentIntervalState; namespace Ryujinx.HLE { @@ -84,9 +85,14 @@ public class HLEConfiguration internal readonly RegionCode Region; /// - /// Control the initial state of the vertical sync in the SurfaceFlinger service. + /// Control the initial state of the present interval in the SurfaceFlinger service (previously Vsync). /// - internal readonly bool EnableVsync; + internal readonly PresentIntervalState PresentIntervalState; + + /// + /// Control the custom present interval, if enabled and active. + /// + internal readonly int CustomPresentInterval; /// /// Control the initial state of the docked mode. @@ -180,7 +186,7 @@ public HLEConfiguration(VirtualFileSystem virtualFileSystem, IHostUiHandler hostUiHandler, SystemLanguage systemLanguage, RegionCode region, - bool enableVsync, + PresentIntervalState presentIntervalState, bool enableDockedMode, bool enablePtc, bool enableInternetAccess, @@ -194,7 +200,9 @@ public HLEConfiguration(VirtualFileSystem virtualFileSystem, float audioVolume, bool useHypervisor, string multiplayerLanInterfaceId, - MultiplayerMode multiplayerMode) + MultiplayerMode multiplayerMode, + //bool enableCustomPresentInterval, + int customPresentInterval) { VirtualFileSystem = virtualFileSystem; LibHacHorizonManager = libHacHorizonManager; @@ -207,7 +215,9 @@ public HLEConfiguration(VirtualFileSystem virtualFileSystem, HostUiHandler = hostUiHandler; SystemLanguage = systemLanguage; Region = region; - EnableVsync = enableVsync; + PresentIntervalState = presentIntervalState; + //EnableCustomPresentInterval = enableCustomPresentInterval; + CustomPresentInterval = customPresentInterval; EnableDockedMode = enableDockedMode; EnablePtc = enablePtc; EnableInternetAccess = enableInternetAccess; diff --git a/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/SurfaceFlinger.cs b/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/SurfaceFlinger.cs index d3d9dc030..0c91a5681 100644 --- a/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/SurfaceFlinger.cs +++ b/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/SurfaceFlinger.cs @@ -8,13 +8,12 @@ using System.Diagnostics; using System.Linq; using System.Threading; +using PresentIntervalState = Ryujinx.Common.Configuration.PresentIntervalState; namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger { class SurfaceFlinger : IConsumerListener, IDisposable { - private const int TargetFps = 60; - private readonly Switch _device; private readonly Dictionary _layers; @@ -34,6 +33,7 @@ class SurfaceFlinger : IConsumerListener, IDisposable private int _swapInterval; private int _swapIntervalDelay; + private bool _targetPresentIntervalChanged = true; private readonly object _lock = new(); @@ -58,6 +58,7 @@ private class TextureCallbackInformation public SurfaceFlinger(Switch device) { _device = device; + _device.TargetPresentIntervalChanged += () => _targetPresentIntervalChanged = true; _layers = new Dictionary(); RenderLayerId = 0; @@ -83,14 +84,18 @@ private void UpdateSwapInterval(int swapInterval) _swapInterval = swapInterval; // If the swap interval is 0, Game VSync is disabled. - if (_swapInterval == 0) - { - _nextFrameEvent.Set(); - _ticksPerFrame = 1; - } - else + if (_targetPresentIntervalChanged) { - _ticksPerFrame = Stopwatch.Frequency / TargetFps; + _targetPresentIntervalChanged = false; + if (_swapInterval == 0) + { + _nextFrameEvent.Set(); + _ticksPerFrame = 1; + } + else + { + _ticksPerFrame = Stopwatch.Frequency / (long)_device.TargetPresentInterval; + } } } @@ -378,14 +383,11 @@ public void Compose() if (acquireStatus == Status.Success) { // If device vsync is disabled, reflect the change. - if (!_device.EnableDeviceVsync) + if (_device.PresentIntervalState == PresentIntervalState.Unbounded) { - if (_swapInterval != 0) - { - UpdateSwapInterval(0); - } + UpdateSwapInterval(0); } - else if (item.SwapInterval != _swapInterval) + else { UpdateSwapInterval(item.SwapInterval); } diff --git a/src/Ryujinx.HLE/Switch.cs b/src/Ryujinx.HLE/Switch.cs index ae063a47d..8ad263115 100644 --- a/src/Ryujinx.HLE/Switch.cs +++ b/src/Ryujinx.HLE/Switch.cs @@ -27,7 +27,24 @@ public class Switch : IDisposable public TamperMachine TamperMachine { get; } public IHostUiHandler UiHandler { get; } - public bool EnableDeviceVsync { get; set; } = true; + public PresentIntervalState PresentIntervalState { get; set; } = PresentIntervalState.Switch; + public bool CustomPresentIntervalEnabled { get; set; } = false; + public int CustomPresentInterval { get; set; } + + public long TargetPresentInterval { get; set; } = 60; + public Action TargetPresentIntervalChanged { get; set; } + + public bool EnableDeviceVsync + { + get + { + return PresentIntervalState == PresentIntervalState.Switch; + } + set + { + PresentIntervalState = value ? PresentIntervalState.Switch : PresentIntervalState.Unbounded; + } + } public bool IsFrameAvailable => Gpu.Window.IsFrameAvailable; @@ -58,12 +75,20 @@ public Switch(HLEConfiguration configuration) System.State.SetLanguage(Configuration.SystemLanguage); System.State.SetRegion(Configuration.Region); - EnableDeviceVsync = Configuration.EnableVsync; + EnableDeviceVsync = Configuration.PresentIntervalState == PresentIntervalState.Switch; + PresentIntervalState = Configuration.PresentIntervalState; + CustomPresentInterval = Configuration.CustomPresentInterval; System.State.DockedMode = Configuration.EnableDockedMode; System.PerformanceState.PerformanceMode = System.State.DockedMode ? PerformanceMode.Boost : PerformanceMode.Default; System.EnablePtc = Configuration.EnablePtc; System.FsIntegrityCheckLevel = Configuration.FsIntegrityCheckLevel; System.GlobalAccessLogMode = Configuration.FsGlobalAccessLogMode; + UpdatePresentInterval(); //todo remove these comments before committing. + // this call seems awkward. maybe this should be part of HOS.Horizon, + // where surfaceflinger is initialized? not sure though since it isn't a genuine + // Switch system setting. either way, if not, we need this call here because + // SurfaceFlinger was initialized with the default target interval + // of 60. #pragma warning restore IDE0055 } @@ -114,6 +139,35 @@ public void PresentFrame(Action swapBuffersCallback) Gpu.Window.Present(swapBuffersCallback); } + public void IncrementCustomPresentInterval() + { + CustomPresentInterval += 1; + UpdatePresentInterval(); + } + + public void DecrementCustomPresentInterval() + { + CustomPresentInterval -= 1; + UpdatePresentInterval(); + } + + public void UpdatePresentInterval() + { + switch (PresentIntervalState) + { + case PresentIntervalState.Custom: + TargetPresentInterval = CustomPresentInterval; + break; + case PresentIntervalState.Switch: + TargetPresentInterval = 60; + break; + case PresentIntervalState.Unbounded: + TargetPresentInterval = 1; + break; + } + TargetPresentIntervalChanged.Invoke(); + } + public void SetVolume(float volume) { System.SetVolume(Math.Clamp(volume, 0, 1)); diff --git a/src/Ryujinx.Headless.SDL2/Options.cs b/src/Ryujinx.Headless.SDL2/Options.cs index a1adcd024..269da7867 100644 --- a/src/Ryujinx.Headless.SDL2/Options.cs +++ b/src/Ryujinx.Headless.SDL2/Options.cs @@ -114,9 +114,15 @@ public class Options [Option("fs-global-access-log-mode", Required = false, Default = 0, HelpText = "Enables FS access log output to the console.")] public int FsGlobalAccessLogMode { get; set; } - [Option("disable-vsync", Required = false, HelpText = "Disables Vertical Sync.")] + [Option("disable-vsync", Required = false, HelpText = "Disables Vertical Sync. Deprecated. Use present-interval-mode instead.")] public bool DisableVSync { get; set; } + [Option("present-interval-mode", Required = false, Default = PresentIntervalState.Switch, HelpText = "Sets the present interval mode (Switch, Unbounded, or Custom).")] + public PresentIntervalState PresentIntervalState { get; set; } + + [Option("custom-present-interval", Required = false, Default = 90, HelpText = "Sets the custom present interval target value (integer).")] + public int CustomPresentInterval { get; set; } + [Option("disable-shader-cache", Required = false, HelpText = "Disables Shader cache.")] public bool DisableShaderCache { get; set; } diff --git a/src/Ryujinx.Headless.SDL2/Program.cs b/src/Ryujinx.Headless.SDL2/Program.cs index 86d3e841d..bcf60e85b 100644 --- a/src/Ryujinx.Headless.SDL2/Program.cs +++ b/src/Ryujinx.Headless.SDL2/Program.cs @@ -543,7 +543,7 @@ private static Switch InitializeEmulationContext(WindowBase window, IRenderer re window, options.SystemLanguage, options.SystemRegion, - !options.DisableVSync, + options.PresentIntervalState, !options.DisableDockedMode, !options.DisablePTC, options.EnableInternetAccess, @@ -557,7 +557,8 @@ private static Switch InitializeEmulationContext(WindowBase window, IRenderer re options.AudioVolume, options.UseHypervisor ?? true, options.MultiplayerLanInterfaceId, - Common.Configuration.Multiplayer.MultiplayerMode.Disabled); + Common.Configuration.Multiplayer.MultiplayerMode.Disabled, + options.CustomPresentInterval); return new Switch(configuration); } diff --git a/src/Ryujinx.Headless.SDL2/StatusUpdatedEventArgs.cs b/src/Ryujinx.Headless.SDL2/StatusUpdatedEventArgs.cs index 62e161dfd..46455c5d2 100644 --- a/src/Ryujinx.Headless.SDL2/StatusUpdatedEventArgs.cs +++ b/src/Ryujinx.Headless.SDL2/StatusUpdatedEventArgs.cs @@ -4,16 +4,16 @@ namespace Ryujinx.Headless.SDL2 { class StatusUpdatedEventArgs : EventArgs { - public bool VSyncEnabled; + public string PresentIntervalState; public string DockedMode; public string AspectRatio; public string GameStatus; public string FifoStatus; public string GpuName; - public StatusUpdatedEventArgs(bool vSyncEnabled, string dockedMode, string aspectRatio, string gameStatus, string fifoStatus, string gpuName) + public StatusUpdatedEventArgs(string presentIntervalState, string dockedMode, string aspectRatio, string gameStatus, string fifoStatus, string gpuName) { - VSyncEnabled = vSyncEnabled; + PresentIntervalState = presentIntervalState; DockedMode = dockedMode; AspectRatio = aspectRatio; GameStatus = gameStatus; diff --git a/src/Ryujinx.Headless.SDL2/WindowBase.cs b/src/Ryujinx.Headless.SDL2/WindowBase.cs index 1b9556057..0d7e05e97 100644 --- a/src/Ryujinx.Headless.SDL2/WindowBase.cs +++ b/src/Ryujinx.Headless.SDL2/WindowBase.cs @@ -311,7 +311,7 @@ public void Render() } StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs( - Device.EnableDeviceVsync, + Device.PresentIntervalState.ToString(), dockedMode, Device.Configuration.AspectRatio.ToText(), $"Game: {Device.Statistics.GetGameFrameRate():00.00} FPS ({Device.Statistics.GetGameFrameTime():00.00} ms)", diff --git a/src/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormat.cs b/src/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormat.cs index 8a4db1fe7..46dc50c13 100644 --- a/src/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormat.cs +++ b/src/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormat.cs @@ -1,3 +1,4 @@ +using Ryujinx.Common; using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Multiplayer; @@ -15,7 +16,7 @@ public class ConfigurationFileFormat /// /// The current version of the file format /// - public const int CurrentVersion = 48; + public const int CurrentVersion = 49; /// /// Version of the configuration file format @@ -170,8 +171,25 @@ public class ConfigurationFileFormat /// /// Enables or disables Vertical Sync /// + /// Kept for file format compatibility (to avoid possible failure when parsing configuration on old versions) + /// TODO: Remove this when those older versions aren't in use anymore. public bool EnableVsync { get; set; } + /// + /// Current present interval state; 60 (Switch), unbounded ("Vsync off"), or custom + /// + public PresentIntervalState PresentIntervalState { get; set; } + + /// + /// Enables or disables the custom present interval + /// + public bool EnableCustomPresentInterval { get; set; } + + /// + /// The custom present interval value + /// + public int CustomPresentInterval { get; set; } + /// /// Enables or disables Shader cache /// diff --git a/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs b/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs index 9d2df5f03..8f88ada54 100644 --- a/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs +++ b/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs @@ -467,10 +467,25 @@ public class GraphicsSection public ReactiveObject ShadersDumpPath { get; private set; } /// - /// Enables or disables Vertical Sync + /// Toggles VSync on or off. Deprecated, included for GTK compatibility. /// public ReactiveObject EnableVsync { get; private set; } + /// + /// Toggles the present interval mode. Options are Switch (60Hz), Unbounded (previously Vsync off), and Custom, if enabled. + /// + public ReactiveObject PresentIntervalState { get; private set; } + + /// + /// Enables or disables the custom present interval mode. + /// + public ReactiveObject EnableCustomPresentInterval { get; private set; } + + /// + /// Changes the custom present interval. + /// + public ReactiveObject CustomPresentInterval { get; private set; } + /// /// Enables or disables Shader cache /// @@ -529,8 +544,14 @@ public GraphicsSection() AspectRatio = new ReactiveObject(); AspectRatio.Event += static (sender, e) => LogValueChange(e, nameof(AspectRatio)); ShadersDumpPath = new ReactiveObject(); + PresentIntervalState = new ReactiveObject(); + PresentIntervalState.Event += static (sender, e) => LogValueChange(e, nameof(PresentIntervalState)); + EnableCustomPresentInterval = new ReactiveObject(); + EnableCustomPresentInterval.Event += static (sender, e) => LogValueChange(e, nameof(EnableCustomPresentInterval)); EnableVsync = new ReactiveObject(); EnableVsync.Event += static (sender, e) => LogValueChange(e, nameof(EnableVsync)); + CustomPresentInterval = new ReactiveObject(); + CustomPresentInterval.Event += static (sender, e) => LogValueChange(e, nameof(CustomPresentInterval)); EnableShaderCache = new ReactiveObject(); EnableShaderCache.Event += static (sender, e) => LogValueChange(e, nameof(EnableShaderCache)); EnableTextureRecompression = new ReactiveObject(); @@ -678,6 +699,9 @@ public ConfigurationFileFormat ToFileFormat() ShowConfirmExit = ShowConfirmExit, HideCursor = HideCursor, EnableVsync = Graphics.EnableVsync, + PresentIntervalState = Graphics.PresentIntervalState, + EnableCustomPresentInterval = Graphics.EnableCustomPresentInterval, + CustomPresentInterval = Graphics.CustomPresentInterval, EnableShaderCache = Graphics.EnableShaderCache, EnableTextureRecompression = Graphics.EnableTextureRecompression, EnableMacroHLE = Graphics.EnableMacroHLE, @@ -785,6 +809,9 @@ public void LoadDefault() ShowConfirmExit.Value = true; HideCursor.Value = HideCursorMode.Never; Graphics.EnableVsync.Value = true; + Graphics.PresentIntervalState.Value = PresentIntervalState.Switch; + Graphics.CustomPresentInterval.Value = 120; + Graphics.EnableCustomPresentInterval.Value = false; Graphics.EnableShaderCache.Value = true; Graphics.EnableTextureRecompression.Value = false; Graphics.EnableMacroHLE.Value = true; @@ -843,7 +870,7 @@ public void LoadDefault() Hid.EnableMouse.Value = false; Hid.Hotkeys.Value = new KeyboardHotkeys { - ToggleVsync = Key.F1, + PresentIntervalState = Key.F1, ToggleMute = Key.F2, Screenshot = Key.F8, ShowUi = Key.F4, @@ -978,7 +1005,7 @@ public ConfigurationLoadResult Load(ConfigurationFileFormat configurationFileFor configurationFileFormat.Hotkeys = new KeyboardHotkeys { - ToggleVsync = Key.F1, + PresentIntervalState = Key.F1, }; configurationFileUpdated = true; @@ -1172,7 +1199,7 @@ public ConfigurationLoadResult Load(ConfigurationFileFormat configurationFileFor configurationFileFormat.Hotkeys = new KeyboardHotkeys { - ToggleVsync = Key.F1, + PresentIntervalState = Key.F1, Screenshot = Key.F8, }; @@ -1185,7 +1212,7 @@ public ConfigurationLoadResult Load(ConfigurationFileFormat configurationFileFor configurationFileFormat.Hotkeys = new KeyboardHotkeys { - ToggleVsync = Key.F1, + PresentIntervalState = Key.F1, Screenshot = Key.F8, ShowUi = Key.F4, }; @@ -1228,7 +1255,7 @@ public ConfigurationLoadResult Load(ConfigurationFileFormat configurationFileFor configurationFileFormat.Hotkeys = new KeyboardHotkeys { - ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, + PresentIntervalState = Key.F1, Screenshot = configurationFileFormat.Hotkeys.Screenshot, ShowUi = configurationFileFormat.Hotkeys.ShowUi, Pause = Key.F5, @@ -1243,7 +1270,7 @@ public ConfigurationLoadResult Load(ConfigurationFileFormat configurationFileFor configurationFileFormat.Hotkeys = new KeyboardHotkeys { - ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, + PresentIntervalState = Key.F1, Screenshot = configurationFileFormat.Hotkeys.Screenshot, ShowUi = configurationFileFormat.Hotkeys.ShowUi, Pause = configurationFileFormat.Hotkeys.Pause, @@ -1317,7 +1344,7 @@ public ConfigurationLoadResult Load(ConfigurationFileFormat configurationFileFor configurationFileFormat.Hotkeys = new KeyboardHotkeys { - ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, + PresentIntervalState = Key.F1, Screenshot = configurationFileFormat.Hotkeys.Screenshot, ShowUi = configurationFileFormat.Hotkeys.ShowUi, Pause = configurationFileFormat.Hotkeys.Pause, @@ -1346,7 +1373,7 @@ public ConfigurationLoadResult Load(ConfigurationFileFormat configurationFileFor configurationFileFormat.Hotkeys = new KeyboardHotkeys { - ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, + PresentIntervalState = Key.F1, Screenshot = configurationFileFormat.Hotkeys.Screenshot, ShowUi = configurationFileFormat.Hotkeys.ShowUi, Pause = configurationFileFormat.Hotkeys.Pause, @@ -1434,6 +1461,32 @@ public ConfigurationLoadResult Load(ConfigurationFileFormat configurationFileFor configurationFileUpdated = true; } + if (configurationFileFormat.Version < 49) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 49."); + + configurationFileFormat.PresentIntervalState = PresentIntervalState.Switch; + configurationFileFormat.EnableCustomPresentInterval = false; + + configurationFileFormat.Hotkeys = new KeyboardHotkeys + { + PresentIntervalState = Key.F1, + Screenshot = configurationFileFormat.Hotkeys.Screenshot, + ShowUi = configurationFileFormat.Hotkeys.ShowUi, + Pause = configurationFileFormat.Hotkeys.Pause, + ToggleMute = configurationFileFormat.Hotkeys.ToggleMute, + ResScaleUp = configurationFileFormat.Hotkeys.ResScaleUp, + ResScaleDown = configurationFileFormat.Hotkeys.ResScaleDown, + VolumeUp = configurationFileFormat.Hotkeys.VolumeUp, + VolumeDown = configurationFileFormat.Hotkeys.VolumeDown, + CustomPresentIntervalIncrement = Key.Unbound, + CustomPresentIntervalDecrement = Key.Unbound, + }; + configurationFileFormat.CustomPresentInterval = 90; + + configurationFileUpdated = true; + } + Logger.EnableFileLog.Value = configurationFileFormat.EnableFileLog; Graphics.ResScale.Value = configurationFileFormat.ResScale; Graphics.ResScaleCustom.Value = configurationFileFormat.ResScaleCustom; @@ -1465,7 +1518,10 @@ public ConfigurationLoadResult Load(ConfigurationFileFormat configurationFileFor CheckUpdatesOnStart.Value = configurationFileFormat.CheckUpdatesOnStart; ShowConfirmExit.Value = configurationFileFormat.ShowConfirmExit; HideCursor.Value = configurationFileFormat.HideCursor; - Graphics.EnableVsync.Value = configurationFileFormat.EnableVsync; + Graphics.EnableVsync.Value = configurationFileFormat.PresentIntervalState == PresentIntervalState.Switch; + Graphics.PresentIntervalState.Value = configurationFileFormat.PresentIntervalState; + Graphics.EnableCustomPresentInterval.Value = configurationFileFormat.EnableCustomPresentInterval; + Graphics.CustomPresentInterval.Value = configurationFileFormat.CustomPresentInterval; Graphics.EnableShaderCache.Value = configurationFileFormat.EnableShaderCache; Graphics.EnableTextureRecompression.Value = configurationFileFormat.EnableTextureRecompression; Graphics.EnableMacroHLE.Value = configurationFileFormat.EnableMacroHLE; diff --git a/src/Ryujinx/Ui/MainWindow.cs b/src/Ryujinx/Ui/MainWindow.cs index f4817277d..fff9d6559 100644 --- a/src/Ryujinx/Ui/MainWindow.cs +++ b/src/Ryujinx/Ui/MainWindow.cs @@ -44,6 +44,7 @@ using System.Threading; using System.Threading.Tasks; using GUI = Gtk.Builder.ObjectAttribute; +using PresentIntervalState = Ryujinx.Common.Configuration.PresentIntervalState; using ShaderCacheLoadingState = Ryujinx.Graphics.Gpu.Shader.ShaderCacheState; namespace Ryujinx.Ui @@ -656,7 +657,7 @@ private void InitializeSwitchInstance() _uiHandler, (SystemLanguage)ConfigurationState.Instance.System.Language.Value, (RegionCode)ConfigurationState.Instance.System.Region.Value, - ConfigurationState.Instance.Graphics.EnableVsync, + ConfigurationState.Instance.Graphics.EnableVsync ? PresentIntervalState.Switch : PresentIntervalState.Unbounded, ConfigurationState.Instance.System.EnableDockedMode, ConfigurationState.Instance.System.EnablePtc, ConfigurationState.Instance.System.EnableInternetAccess, @@ -670,7 +671,8 @@ private void InitializeSwitchInstance() ConfigurationState.Instance.System.AudioVolume, ConfigurationState.Instance.System.UseHypervisor, ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value, - ConfigurationState.Instance.Multiplayer.Mode); + ConfigurationState.Instance.Multiplayer.Mode, + ConfigurationState.Instance.Graphics.CustomPresentInterval); _emulationContext = new HLE.Switch(configuration); } @@ -1212,7 +1214,7 @@ private void Update_StatusBar(object sender, StatusUpdatedEventArgs args) _gpuBackend.Text = args.GpuBackend; _volumeStatus.Text = GetVolumeLabelText(args.Volume); - if (args.VSyncEnabled) + if (args.PresentIntervalState == PresentIntervalState.Switch.ToString()) { _vSyncStatus.Attributes = new Pango.AttrList(); _vSyncStatus.Attributes.Insert(new Pango.AttrForeground(11822, 60138, 51657)); diff --git a/src/Ryujinx/Ui/RendererWidgetBase.cs b/src/Ryujinx/Ui/RendererWidgetBase.cs index 0ee344434..a5f2e3598 100644 --- a/src/Ryujinx/Ui/RendererWidgetBase.cs +++ b/src/Ryujinx/Ui/RendererWidgetBase.cs @@ -23,6 +23,7 @@ using System.Threading.Tasks; using Image = SixLabors.ImageSharp.Image; using Key = Ryujinx.Input.Key; +using PresentIntervalState = Ryujinx.Graphics.GAL.PresentIntervalState; using ScalingFilter = Ryujinx.Graphics.GAL.ScalingFilter; using Switch = Ryujinx.HLE.Switch; @@ -451,7 +452,7 @@ public void Render() Device.Gpu.InitializeShaderCache(_gpuCancellationTokenSource.Token); Translator.IsReadyForTranslation.Set(); - Renderer.Window.ChangeVSyncMode(Device.EnableDeviceVsync); + Renderer.Window.ChangePresentIntervalState(Device.EnableDeviceVsync ? PresentIntervalState.Switch : PresentIntervalState.Unbounded); (Toplevel as MainWindow)?.ActivatePauseMenu(); @@ -488,7 +489,7 @@ public void Render() } StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs( - Device.EnableDeviceVsync, + Device.EnableDeviceVsync ? PresentIntervalState.Switch.ToString() : PresentIntervalState.Unbounded.ToString(), Device.GetVolume(), _gpuBackendName, dockedMode, @@ -758,7 +759,7 @@ private KeyboardHotkeyState GetHotkeyState() { KeyboardHotkeyState state = KeyboardHotkeyState.None; - if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ToggleVsync)) + if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.PresentIntervalState)) { state |= KeyboardHotkeyState.ToggleVSync; } diff --git a/src/Ryujinx/Ui/StatusUpdatedEventArgs.cs b/src/Ryujinx/Ui/StatusUpdatedEventArgs.cs index 949390caa..7b40ad6d1 100644 --- a/src/Ryujinx/Ui/StatusUpdatedEventArgs.cs +++ b/src/Ryujinx/Ui/StatusUpdatedEventArgs.cs @@ -4,7 +4,7 @@ namespace Ryujinx.Ui { public class StatusUpdatedEventArgs : EventArgs { - public bool VSyncEnabled; + public string PresentIntervalState; public float Volume; public string DockedMode; public string AspectRatio; @@ -13,9 +13,9 @@ public class StatusUpdatedEventArgs : EventArgs public string GpuName; public string GpuBackend; - public StatusUpdatedEventArgs(bool vSyncEnabled, float volume, string gpuBackend, string dockedMode, string aspectRatio, string gameStatus, string fifoStatus, string gpuName) + public StatusUpdatedEventArgs(string presentIntervalState, float volume, string gpuBackend, string dockedMode, string aspectRatio, string gameStatus, string fifoStatus, string gpuName) { - VSyncEnabled = vSyncEnabled; + PresentIntervalState = presentIntervalState; Volume = volume; GpuBackend = gpuBackend; DockedMode = dockedMode;