VRR: optional per-game FPS cap
Adds a per-game FPS cap for the VRR present mode so games whose logic is tied to the frame rate (e.g. TOTK's menu cursor running wild at 200 fps) can be limited, while keeping G-Sync/FreeSync smoothing. The cap is off by default (VRR stays unbounded); 30/60/120 are exposed. Reuses the existing custom-VSync-interval plumbing: when VSyncMode is Vrr and the custom interval is enabled, SurfaceFlinger paces frames at that value (forcing a non-zero swap interval even when a Dynamic FPS mod submits 0) instead of running unbounded, while Window.cs keeps FifoRelaxed so the display still follows the frame rate. EnableCustomVSyncInterval is now read at Switch construction so the per-game setting applies at launch. UI: the VSync dropdown now offers Switch / Unbounded / VRR, with a VRR FPS cap picker shown only for VRR. The legacy Custom mode is hidden (kept in the enum for config compatibility) and the percentage slider / experimental checkbox are removed.
This commit is contained in:
@@ -13,10 +13,10 @@ namespace Ryujinx.Common.Configuration
|
||||
public static VSyncMode Next(this VSyncMode vsync, bool customEnabled = false) =>
|
||||
vsync switch
|
||||
{
|
||||
VSyncMode.Switch => customEnabled ? VSyncMode.Custom : VSyncMode.Unbounded,
|
||||
VSyncMode.Custom => VSyncMode.Unbounded,
|
||||
VSyncMode.Switch => VSyncMode.Unbounded,
|
||||
VSyncMode.Unbounded => VSyncMode.Vrr,
|
||||
VSyncMode.Vrr => VSyncMode.Switch,
|
||||
VSyncMode.Custom => VSyncMode.Switch, // legacy mode, no longer in the cycle
|
||||
_ => VSyncMode.Switch
|
||||
};
|
||||
}
|
||||
|
||||
@@ -372,7 +372,25 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger
|
||||
|
||||
if (acquireStatus == Status.Success)
|
||||
{
|
||||
if (_device.VSyncMode == VSyncMode.Unbounded)
|
||||
bool vrrCapped = _device.VSyncMode == VSyncMode.Vrr
|
||||
&& _device.CustomVSyncIntervalEnabled
|
||||
&& _device.CustomVSyncInterval > 0;
|
||||
|
||||
if (vrrCapped)
|
||||
{
|
||||
// VRR with a per-game FPS cap: force a paced interval (swap interval 1 ->
|
||||
// _ticksPerFrame derived from TargetVSyncInterval) instead of running
|
||||
// unbounded. The present mode stays FifoRelaxed (chosen from VSyncMode.Vrr
|
||||
// in Window.cs), so the display still follows the framerate, just capped.
|
||||
if (_swapInterval != 1
|
||||
|| _device.TargetVSyncInterval != _targetVSyncInterval
|
||||
|| _vSyncMode != _device.VSyncMode)
|
||||
{
|
||||
UpdateSwapInterval(1);
|
||||
_vSyncMode = _device.VSyncMode;
|
||||
}
|
||||
}
|
||||
else if (_device.VSyncMode == VSyncMode.Unbounded || _device.VSyncMode == VSyncMode.Vrr)
|
||||
{
|
||||
if (_swapInterval != 0)
|
||||
{
|
||||
|
||||
@@ -93,6 +93,11 @@ namespace Ryujinx.HLE
|
||||
/// </summary>
|
||||
internal readonly int CustomVSyncInterval;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the custom VSync interval is enabled. Also acts as the per-game VRR FPS cap toggle.
|
||||
/// </summary>
|
||||
internal readonly bool EnableCustomVSyncInterval;
|
||||
|
||||
/// <summary>
|
||||
/// Control the initial state of the docked mode.
|
||||
/// </summary>
|
||||
@@ -241,6 +246,7 @@ namespace Ryujinx.HLE
|
||||
ushort gdbStubPort,
|
||||
bool debuggerSuspendOnStart,
|
||||
int customVSyncInterval,
|
||||
bool enableCustomVSyncInterval = false,
|
||||
EnabledDirtyHack[] dirtyHacks = null)
|
||||
{
|
||||
MemoryConfiguration = memoryConfiguration;
|
||||
@@ -248,6 +254,7 @@ namespace Ryujinx.HLE
|
||||
Region = region;
|
||||
VSyncMode = vSyncMode;
|
||||
CustomVSyncInterval = customVSyncInterval;
|
||||
EnableCustomVSyncInterval = enableCustomVSyncInterval;
|
||||
EnableDockedMode = enableDockedMode;
|
||||
EnablePtc = enablePtc;
|
||||
TickScalar = tickScalar;
|
||||
|
||||
@@ -98,6 +98,7 @@ namespace Ryujinx.HLE
|
||||
|
||||
VSyncMode = Configuration.VSyncMode;
|
||||
CustomVSyncInterval = Configuration.CustomVSyncInterval;
|
||||
CustomVSyncIntervalEnabled = Configuration.EnableCustomVSyncInterval;
|
||||
TickScalar = TurboMode ? Configuration.TickScalar : ITickSource.RealityTickScalar;
|
||||
System.State.DockedMode = Configuration.EnableDockedMode;
|
||||
System.PerformanceState.PerformanceMode = System.State.DockedMode ? PerformanceMode.Boost : PerformanceMode.Default;
|
||||
@@ -147,6 +148,13 @@ namespace Ryujinx.HLE
|
||||
case VSyncMode.Unbounded:
|
||||
TargetVSyncInterval = 1;
|
||||
break;
|
||||
case VSyncMode.Vrr:
|
||||
// VRR runs unbounded by default (interval 1). If the per-game FPS cap is
|
||||
// enabled, pace to that value instead (e.g. TOTK capped at 60).
|
||||
TargetVSyncInterval = (CustomVSyncIntervalEnabled && CustomVSyncInterval > 0)
|
||||
? CustomVSyncInterval
|
||||
: 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -987,6 +987,7 @@ namespace Ryujinx.Ava.Systems.Configuration
|
||||
Debug.GdbStubPort,
|
||||
Debug.DebuggerSuspendOnStart,
|
||||
Graphics.CustomVSyncInterval,
|
||||
Graphics.EnableCustomVSyncInterval,
|
||||
Hacks.ShowDirtyHacks ? Hacks.EnabledHacks : null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,6 +260,45 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
// --- Clean VSync / VRR UI (Roofer Edition) ---
|
||||
// Dropdown positions: 0 = Switch, 1 = Unbounded, 2 = VRR. We translate position <-> enum
|
||||
// here so we can skip the legacy "Custom" enum value (2) and still bind by SelectedIndex.
|
||||
public int VSyncModeIndex
|
||||
{
|
||||
get => _vSyncMode switch
|
||||
{
|
||||
VSyncMode.Unbounded => 1,
|
||||
VSyncMode.Vrr => 2,
|
||||
_ => 0,
|
||||
};
|
||||
set
|
||||
{
|
||||
_vSyncMode = value switch
|
||||
{
|
||||
1 => VSyncMode.Unbounded,
|
||||
2 => VSyncMode.Vrr,
|
||||
_ => VSyncMode.Switch,
|
||||
};
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(ShowVrrCapPicker));
|
||||
}
|
||||
}
|
||||
|
||||
// The VRR FPS cap picker is only relevant when VRR is the selected mode.
|
||||
public bool ShowVrrCapPicker => _vSyncMode == VSyncMode.Vrr;
|
||||
|
||||
// 0 = Off (unbounded VRR), 1 = 30, 2 = 60, 3 = 120.
|
||||
public int VrrFpsCapIndex
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool EnablePptc { get; set; }
|
||||
public bool EnableLowPowerPptc { get; set; }
|
||||
|
||||
@@ -743,9 +782,23 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
MatchSystemTime = config.System.MatchSystemTime;
|
||||
|
||||
EnableCustomVSyncInterval = config.Graphics.EnableCustomVSyncInterval;
|
||||
CustomVSyncInterval = config.Graphics.CustomVSyncInterval;
|
||||
VSyncMode = config.Graphics.VSyncMode;
|
||||
|
||||
// VSync / VRR (Roofer clean UI): set the backing field directly so VRR loads (the
|
||||
// legacy VSyncMode setter rejects it), and derive the VRR cap picker from the stored
|
||||
// custom-interval values.
|
||||
_vSyncMode = config.Graphics.VSyncMode.Value;
|
||||
VrrFpsCapIndex = config.Graphics.EnableCustomVSyncInterval.Value
|
||||
? config.Graphics.CustomVSyncInterval.Value switch
|
||||
{
|
||||
30 => 1,
|
||||
60 => 2,
|
||||
120 => 3,
|
||||
_ => 0,
|
||||
}
|
||||
: 0;
|
||||
OnPropertyChanged(nameof(VSyncModeIndex));
|
||||
OnPropertyChanged(nameof(ShowVrrCapPicker));
|
||||
EnableFsIntegrityChecks = config.System.EnableFsIntegrityChecks;
|
||||
DramSize = config.System.DramSize;
|
||||
IgnoreMissingServices = config.System.IgnoreMissingServices;
|
||||
@@ -879,9 +932,22 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
config.System.TickScalar.Value = TurboMultiplier;
|
||||
|
||||
// Graphics
|
||||
config.Graphics.VSyncMode.Value = VSyncMode;
|
||||
config.Graphics.EnableCustomVSyncInterval.Value = EnableCustomVSyncInterval;
|
||||
config.Graphics.CustomVSyncInterval.Value = CustomVSyncInterval;
|
||||
config.Graphics.VSyncMode.Value = _vSyncMode;
|
||||
|
||||
// VRR FPS cap -> reuse the custom-interval plumbing the engine already reads
|
||||
// (EnableCustomVSyncInterval = cap on, CustomVSyncInterval = the capped fps).
|
||||
bool vrrCapOn = _vSyncMode == VSyncMode.Vrr && VrrFpsCapIndex > 0;
|
||||
config.Graphics.EnableCustomVSyncInterval.Value = vrrCapOn;
|
||||
if (vrrCapOn)
|
||||
{
|
||||
config.Graphics.CustomVSyncInterval.Value = VrrFpsCapIndex switch
|
||||
{
|
||||
1 => 30,
|
||||
2 => 60,
|
||||
3 => 120,
|
||||
_ => 60,
|
||||
};
|
||||
}
|
||||
config.Graphics.GraphicsBackend.Value = (GraphicsBackend)GraphicsBackendIndex;
|
||||
config.Graphics.PreferredGpu.Value = _gpuIds.ElementAtOrDefault(PreferredGpuIndex);
|
||||
config.Graphics.EnableShaderCache.Value = EnableShaderCache;
|
||||
|
||||
@@ -178,21 +178,7 @@
|
||||
ToolTip.Tip="{ext:Locale SettingsTabSystemVSyncModeTooltip}"
|
||||
Width="250" />
|
||||
<ComboBox
|
||||
IsVisible="{Binding EnableCustomVSyncInterval}"
|
||||
SelectedIndex="{Binding VSyncMode}"
|
||||
ToolTip.Tip="{ext:Locale SettingsTabSystemVSyncModeTooltipCustom}"
|
||||
HorizontalContentAlignment="Left"
|
||||
Width="350">
|
||||
<ComboBoxItem
|
||||
Content="{ext:Locale SettingsTabSystemVSyncModeSwitch}" />
|
||||
<ComboBoxItem
|
||||
Content="{ext:Locale SettingsTabSystemVSyncModeUnbounded}" />
|
||||
<ComboBoxItem
|
||||
Content="{ext:Locale SettingsTabSystemVSyncModeCustom}" />
|
||||
</ComboBox>
|
||||
<ComboBox
|
||||
IsVisible="{Binding !EnableCustomVSyncInterval}"
|
||||
SelectedIndex="{Binding VSyncMode}"
|
||||
SelectedIndex="{Binding VSyncModeIndex}"
|
||||
ToolTip.Tip="{ext:Locale SettingsTabSystemVSyncModeTooltip}"
|
||||
HorizontalContentAlignment="Left"
|
||||
Width="350">
|
||||
@@ -200,32 +186,26 @@
|
||||
Content="{ext:Locale SettingsTabSystemVSyncModeSwitch}" />
|
||||
<ComboBoxItem
|
||||
Content="{ext:Locale SettingsTabSystemVSyncModeUnbounded}" />
|
||||
<ComboBoxItem
|
||||
Content="VRR" />
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
<StackPanel IsVisible="{Binding EnableCustomVSyncInterval}"
|
||||
<StackPanel IsVisible="{Binding ShowVrrCapPicker}"
|
||||
Margin="0,0,0,10"
|
||||
Orientation="Horizontal">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
Text="{ext:Locale SettingsTabSystemCustomVSyncIntervalPercentage}"
|
||||
ToolTip.Tip="{ext:Locale SettingsTabSystemCustomVSyncIntervalValueTooltip}"
|
||||
Text="Plafond VRR (FPS)"
|
||||
Width="250" />
|
||||
<Slider Value="{Binding CustomVSyncIntervalPercentageProxy}"
|
||||
ToolTip.Tip="{ext:Locale SettingsTabSystemCustomVSyncIntervalSliderTooltip}"
|
||||
MinWidth="175"
|
||||
Margin="10,-3,0,0"
|
||||
Height="32"
|
||||
Padding="0,-5"
|
||||
TickFrequency="1"
|
||||
IsSnapToTickEnabled="True"
|
||||
LargeChange="10"
|
||||
SmallChange="1"
|
||||
VerticalAlignment="Center"
|
||||
Minimum="10"
|
||||
Maximum="400" />
|
||||
<TextBlock Margin="5,0"
|
||||
Width="40"
|
||||
Text="{Binding CustomVSyncIntervalPercentageText}"/>
|
||||
<ComboBox
|
||||
SelectedIndex="{Binding VrrFpsCapIndex}"
|
||||
HorizontalContentAlignment="Left"
|
||||
Width="350">
|
||||
<ComboBoxItem Content="Off" />
|
||||
<ComboBoxItem Content="30" />
|
||||
<ComboBoxItem Content="60" />
|
||||
<ComboBoxItem Content="120" />
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
<CheckBox IsChecked="{Binding EnableFsIntegrityChecks}">
|
||||
<TextBlock
|
||||
@@ -282,11 +262,6 @@
|
||||
ToolTip.Tip="{ext:Locale IgnoreControllerAppletTooltip}">
|
||||
<TextBlock Text="{ext:Locale SettingsTabSystemIgnoreControllerApplet}" />
|
||||
</CheckBox>
|
||||
<CheckBox
|
||||
IsChecked="{Binding EnableCustomVSyncInterval}"
|
||||
ToolTip.Tip="{ext:Locale SettingsTabSystemEnableCustomVSyncIntervalTooltip}">
|
||||
<TextBlock Text="{ext:Locale SettingsTabSystemEnableCustomVSyncInterval}" />
|
||||
</CheckBox>
|
||||
<CheckBox
|
||||
IsChecked="{Binding SkipUserProfiles}"
|
||||
ToolTip.Tip="{ext:Locale SkipUserProfilesTooltip}">
|
||||
|
||||
Reference in New Issue
Block a user