diff --git a/src/Ryujinx.Common/Configuration/VSyncMode.cs b/src/Ryujinx.Common/Configuration/VSyncMode.cs
index b2818eb7a..3cfc9549e 100644
--- a/src/Ryujinx.Common/Configuration/VSyncMode.cs
+++ b/src/Ryujinx.Common/Configuration/VSyncMode.cs
@@ -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
};
}
diff --git a/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/SurfaceFlinger.cs b/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/SurfaceFlinger.cs
index 971778175..99e49af71 100644
--- a/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/SurfaceFlinger.cs
+++ b/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/SurfaceFlinger.cs
@@ -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)
{
diff --git a/src/Ryujinx.HLE/HleConfiguration.cs b/src/Ryujinx.HLE/HleConfiguration.cs
index e2f95ede7..f9999118a 100644
--- a/src/Ryujinx.HLE/HleConfiguration.cs
+++ b/src/Ryujinx.HLE/HleConfiguration.cs
@@ -93,6 +93,11 @@ namespace Ryujinx.HLE
///
internal readonly int CustomVSyncInterval;
+ ///
+ /// Whether the custom VSync interval is enabled. Also acts as the per-game VRR FPS cap toggle.
+ ///
+ internal readonly bool EnableCustomVSyncInterval;
+
///
/// Control the initial state of the docked mode.
///
@@ -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;
diff --git a/src/Ryujinx.HLE/Switch.cs b/src/Ryujinx.HLE/Switch.cs
index 90af47988..d60d7518f 100644
--- a/src/Ryujinx.HLE/Switch.cs
+++ b/src/Ryujinx.HLE/Switch.cs
@@ -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;
}
}
diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs b/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs
index c4d6ce273..66632e5d1 100644
--- a/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs
+++ b/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs
@@ -987,6 +987,7 @@ namespace Ryujinx.Ava.Systems.Configuration
Debug.GdbStubPort,
Debug.DebuggerSuspendOnStart,
Graphics.CustomVSyncInterval,
+ Graphics.EnableCustomVSyncInterval,
Hacks.ShowDirtyHacks ? Hacks.EnabledHacks : null);
}
}
diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs
index 0f6e473e3..68ed064b0 100644
--- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs
+++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs
@@ -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;
diff --git a/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml
index 2414fb7f6..cd0fded02 100644
--- a/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml
+++ b/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml
@@ -178,21 +178,7 @@
ToolTip.Tip="{ext:Locale SettingsTabSystemVSyncModeTooltip}"
Width="250" />
-
-
-
-
-
@@ -200,32 +186,26 @@
Content="{ext:Locale SettingsTabSystemVSyncModeSwitch}" />
+
-
-
-
+
+
+
+
+
+
-
-
-