Merge pull request #2 from TheRooferDev/vrr-cap-clean

VRR: optional per-game FPS cap
This commit is contained in:
2026-06-19 10:45:33 -04:00
committed by GitHub
7 changed files with 122 additions and 47 deletions
@@ -13,10 +13,10 @@ namespace Ryujinx.Common.Configuration
public static VSyncMode Next(this VSyncMode vsync, bool customEnabled = false) => public static VSyncMode Next(this VSyncMode vsync, bool customEnabled = false) =>
vsync switch vsync switch
{ {
VSyncMode.Switch => customEnabled ? VSyncMode.Custom : VSyncMode.Unbounded, VSyncMode.Switch => VSyncMode.Unbounded,
VSyncMode.Custom => VSyncMode.Unbounded,
VSyncMode.Unbounded => VSyncMode.Vrr, VSyncMode.Unbounded => VSyncMode.Vrr,
VSyncMode.Vrr => VSyncMode.Switch, VSyncMode.Vrr => VSyncMode.Switch,
VSyncMode.Custom => VSyncMode.Switch, // legacy mode, no longer in the cycle
_ => VSyncMode.Switch _ => VSyncMode.Switch
}; };
} }
@@ -372,7 +372,25 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger
if (acquireStatus == Status.Success) 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) if (_swapInterval != 0)
{ {
+7
View File
@@ -93,6 +93,11 @@ namespace Ryujinx.HLE
/// </summary> /// </summary>
internal readonly int CustomVSyncInterval; 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> /// <summary>
/// Control the initial state of the docked mode. /// Control the initial state of the docked mode.
/// </summary> /// </summary>
@@ -241,6 +246,7 @@ namespace Ryujinx.HLE
ushort gdbStubPort, ushort gdbStubPort,
bool debuggerSuspendOnStart, bool debuggerSuspendOnStart,
int customVSyncInterval, int customVSyncInterval,
bool enableCustomVSyncInterval = false,
EnabledDirtyHack[] dirtyHacks = null) EnabledDirtyHack[] dirtyHacks = null)
{ {
MemoryConfiguration = memoryConfiguration; MemoryConfiguration = memoryConfiguration;
@@ -248,6 +254,7 @@ namespace Ryujinx.HLE
Region = region; Region = region;
VSyncMode = vSyncMode; VSyncMode = vSyncMode;
CustomVSyncInterval = customVSyncInterval; CustomVSyncInterval = customVSyncInterval;
EnableCustomVSyncInterval = enableCustomVSyncInterval;
EnableDockedMode = enableDockedMode; EnableDockedMode = enableDockedMode;
EnablePtc = enablePtc; EnablePtc = enablePtc;
TickScalar = tickScalar; TickScalar = tickScalar;
+8
View File
@@ -98,6 +98,7 @@ namespace Ryujinx.HLE
VSyncMode = Configuration.VSyncMode; VSyncMode = Configuration.VSyncMode;
CustomVSyncInterval = Configuration.CustomVSyncInterval; CustomVSyncInterval = Configuration.CustomVSyncInterval;
CustomVSyncIntervalEnabled = Configuration.EnableCustomVSyncInterval;
TickScalar = TurboMode ? Configuration.TickScalar : ITickSource.RealityTickScalar; TickScalar = TurboMode ? Configuration.TickScalar : ITickSource.RealityTickScalar;
System.State.DockedMode = Configuration.EnableDockedMode; System.State.DockedMode = Configuration.EnableDockedMode;
System.PerformanceState.PerformanceMode = System.State.DockedMode ? PerformanceMode.Boost : PerformanceMode.Default; System.PerformanceState.PerformanceMode = System.State.DockedMode ? PerformanceMode.Boost : PerformanceMode.Default;
@@ -147,6 +148,13 @@ namespace Ryujinx.HLE
case VSyncMode.Unbounded: case VSyncMode.Unbounded:
TargetVSyncInterval = 1; TargetVSyncInterval = 1;
break; 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.GdbStubPort,
Debug.DebuggerSuspendOnStart, Debug.DebuggerSuspendOnStart,
Graphics.CustomVSyncInterval, Graphics.CustomVSyncInterval,
Graphics.EnableCustomVSyncInterval,
Hacks.ShowDirtyHacks ? Hacks.EnabledHacks : null); Hacks.ShowDirtyHacks ? Hacks.EnabledHacks : null);
} }
} }
+71 -5
View File
@@ -260,6 +260,45 @@ namespace Ryujinx.Ava.UI.ViewModels
OnPropertyChanged(); 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 EnablePptc { get; set; }
public bool EnableLowPowerPptc { get; set; } public bool EnableLowPowerPptc { get; set; }
@@ -743,9 +782,23 @@ namespace Ryujinx.Ava.UI.ViewModels
MatchSystemTime = config.System.MatchSystemTime; MatchSystemTime = config.System.MatchSystemTime;
EnableCustomVSyncInterval = config.Graphics.EnableCustomVSyncInterval;
CustomVSyncInterval = config.Graphics.CustomVSyncInterval; 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; EnableFsIntegrityChecks = config.System.EnableFsIntegrityChecks;
DramSize = config.System.DramSize; DramSize = config.System.DramSize;
IgnoreMissingServices = config.System.IgnoreMissingServices; IgnoreMissingServices = config.System.IgnoreMissingServices;
@@ -879,9 +932,22 @@ namespace Ryujinx.Ava.UI.ViewModels
config.System.TickScalar.Value = TurboMultiplier; config.System.TickScalar.Value = TurboMultiplier;
// Graphics // Graphics
config.Graphics.VSyncMode.Value = VSyncMode; config.Graphics.VSyncMode.Value = _vSyncMode;
config.Graphics.EnableCustomVSyncInterval.Value = EnableCustomVSyncInterval;
config.Graphics.CustomVSyncInterval.Value = CustomVSyncInterval; // 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.GraphicsBackend.Value = (GraphicsBackend)GraphicsBackendIndex;
config.Graphics.PreferredGpu.Value = _gpuIds.ElementAtOrDefault(PreferredGpuIndex); config.Graphics.PreferredGpu.Value = _gpuIds.ElementAtOrDefault(PreferredGpuIndex);
config.Graphics.EnableShaderCache.Value = EnableShaderCache; config.Graphics.EnableShaderCache.Value = EnableShaderCache;
@@ -178,21 +178,7 @@
ToolTip.Tip="{ext:Locale SettingsTabSystemVSyncModeTooltip}" ToolTip.Tip="{ext:Locale SettingsTabSystemVSyncModeTooltip}"
Width="250" /> Width="250" />
<ComboBox <ComboBox
IsVisible="{Binding EnableCustomVSyncInterval}" SelectedIndex="{Binding VSyncModeIndex}"
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}"
ToolTip.Tip="{ext:Locale SettingsTabSystemVSyncModeTooltip}" ToolTip.Tip="{ext:Locale SettingsTabSystemVSyncModeTooltip}"
HorizontalContentAlignment="Left" HorizontalContentAlignment="Left"
Width="350"> Width="350">
@@ -200,32 +186,26 @@
Content="{ext:Locale SettingsTabSystemVSyncModeSwitch}" /> Content="{ext:Locale SettingsTabSystemVSyncModeSwitch}" />
<ComboBoxItem <ComboBoxItem
Content="{ext:Locale SettingsTabSystemVSyncModeUnbounded}" /> Content="{ext:Locale SettingsTabSystemVSyncModeUnbounded}" />
<ComboBoxItem
Content="VRR" />
</ComboBox> </ComboBox>
</StackPanel> </StackPanel>
<StackPanel IsVisible="{Binding EnableCustomVSyncInterval}" <StackPanel IsVisible="{Binding ShowVrrCapPicker}"
Margin="0,0,0,10" Margin="0,0,0,10"
Orientation="Horizontal"> Orientation="Horizontal">
<TextBlock <TextBlock
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{ext:Locale SettingsTabSystemCustomVSyncIntervalPercentage}" Text="Plafond VRR (FPS)"
ToolTip.Tip="{ext:Locale SettingsTabSystemCustomVSyncIntervalValueTooltip}"
Width="250" /> Width="250" />
<Slider Value="{Binding CustomVSyncIntervalPercentageProxy}" <ComboBox
ToolTip.Tip="{ext:Locale SettingsTabSystemCustomVSyncIntervalSliderTooltip}" SelectedIndex="{Binding VrrFpsCapIndex}"
MinWidth="175" HorizontalContentAlignment="Left"
Margin="10,-3,0,0" Width="350">
Height="32" <ComboBoxItem Content="Off" />
Padding="0,-5" <ComboBoxItem Content="30" />
TickFrequency="1" <ComboBoxItem Content="60" />
IsSnapToTickEnabled="True" <ComboBoxItem Content="120" />
LargeChange="10" </ComboBox>
SmallChange="1"
VerticalAlignment="Center"
Minimum="10"
Maximum="400" />
<TextBlock Margin="5,0"
Width="40"
Text="{Binding CustomVSyncIntervalPercentageText}"/>
</StackPanel> </StackPanel>
<CheckBox IsChecked="{Binding EnableFsIntegrityChecks}"> <CheckBox IsChecked="{Binding EnableFsIntegrityChecks}">
<TextBlock <TextBlock
@@ -282,11 +262,6 @@
ToolTip.Tip="{ext:Locale IgnoreControllerAppletTooltip}"> ToolTip.Tip="{ext:Locale IgnoreControllerAppletTooltip}">
<TextBlock Text="{ext:Locale SettingsTabSystemIgnoreControllerApplet}" /> <TextBlock Text="{ext:Locale SettingsTabSystemIgnoreControllerApplet}" />
</CheckBox> </CheckBox>
<CheckBox
IsChecked="{Binding EnableCustomVSyncInterval}"
ToolTip.Tip="{ext:Locale SettingsTabSystemEnableCustomVSyncIntervalTooltip}">
<TextBlock Text="{ext:Locale SettingsTabSystemEnableCustomVSyncInterval}" />
</CheckBox>
<CheckBox <CheckBox
IsChecked="{Binding SkipUserProfiles}" IsChecked="{Binding SkipUserProfiles}"
ToolTip.Tip="{ext:Locale SkipUserProfilesTooltip}"> ToolTip.Tip="{ext:Locale SkipUserProfilesTooltip}">