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) =>
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)
{
+7
View File
@@ -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;
+8
View File
@@ -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);
}
}
+71 -5
View File
@@ -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}">