settings: automatic framerate cap option and menu polish

This commit is contained in:
mchughalex 2026-07-19 15:33:59 +01:00
parent 649c9e73a7
commit 3770dcd71d

View file

@ -28,6 +28,7 @@
#include <rex/logging.h> #include <rex/logging.h>
#include <rex/audio/audio_driver.h> #include <rex/audio/audio_driver.h>
#include <rex/ui/presenter.h> #include <rex/ui/presenter.h>
#include <rex/ui/window.h>
#include <toml++/toml.hpp> #include <toml++/toml.hpp>
namespace rex::ui { namespace rex::ui {
@ -37,11 +38,10 @@ constexpr std::array<int32_t, 3> kResolutionScales = {1, 2, 3};
constexpr std::array<const char*, 3> kResolutionLabels = {"720p (1x)", "1440p (2x)", constexpr std::array<const char*, 3> kResolutionLabels = {"720p (1x)", "1440p (2x)",
"2160p (3x)"}; "2160p (3x)"};
constexpr std::array<const char*, 2> kAspectRatioLabels = {"16:9", "21:9 (Experimental)"}; constexpr std::array<const char*, 2> kAspectRatioLabels = {"16:9", "21:9 (Experimental)"};
constexpr std::array<double, 7> kFrameCapRates = {60.0, 90.0, 120.0, 140.0, constexpr std::array<double, 6> kFrameCapRates = {60.0, 90.0, 120.0, 144.0, 165.0, 240.0};
144.0, 165.0, 240.0}; constexpr std::array<const char*, 7> kFrameCapLabels = {"Unlimited", "60 FPS", "90 FPS",
constexpr std::array<const char*, 8> kFrameCapLabels = {"Unlimited", "60 FPS", "90 FPS", "120 FPS", "144 FPS", "165 FPS",
"120 FPS", "140 FPS", "144 FPS", "240 FPS"};
"165 FPS", "240 FPS"};
constexpr std::array<std::string_view, 7> kCoreSimpleSettingsCvars = { constexpr std::array<std::string_view, 7> kCoreSimpleSettingsCvars = {
"resolution_scale", "resolution_scale",
"draw_resolution_scale_x", "draw_resolution_scale_x",
@ -209,6 +209,9 @@ std::vector<std::string_view> GetSimpleSettingsCvars() {
if (HasCvar("skate3_guest_fps_cap")) { if (HasCvar("skate3_guest_fps_cap")) {
cvars.push_back("skate3_guest_fps_cap"); cvars.push_back("skate3_guest_fps_cap");
} }
if (HasCvar("skate3_guest_fps_cap_auto")) {
cvars.push_back("skate3_guest_fps_cap_auto");
}
if (HasCvar("d3d12_present_frame_limiter")) { if (HasCvar("d3d12_present_frame_limiter")) {
cvars.push_back("d3d12_present_frame_limiter"); cvars.push_back("d3d12_present_frame_limiter");
} }
@ -282,16 +285,30 @@ bool HasFrameCapControl() {
return UseGuestFrameCap() || HasHostFrameCapCvars(); return UseGuestFrameCap() || HasHostFrameCapCvars();
} }
// The refresh-derived automatic cap rides the guest pacer only (the host
// present limiter has no auto mode). Option-list order: [Auto,] <rates...>,
// Unlimited.
bool FrameCapHasAuto() {
return UseGuestFrameCap() && HasCvar("skate3_guest_fps_cap_auto");
}
int FrameCapRateBaseIndex() { return FrameCapHasAuto() ? 1 : 0; }
int FrameCapUnlimitedIndex() {
return FrameCapRateBaseIndex() + static_cast<int>(kFrameCapRates.size());
}
int FrameCapIndexFromRate(double rate) { int FrameCapIndexFromRate(double rate) {
if (rate < 1.0) { if (rate < 1.0) {
return 0; return FrameCapUnlimitedIndex();
} }
int best = 1; const int base = FrameCapRateBaseIndex();
int best = base;
double best_delta = 1000.0; double best_delta = 1000.0;
for (int i = 0; i < static_cast<int>(kFrameCapRates.size()); ++i) { for (int i = 0; i < static_cast<int>(kFrameCapRates.size()); ++i) {
double delta = std::abs(kFrameCapRates[i] - rate); double delta = std::abs(kFrameCapRates[i] - rate);
if (delta < best_delta) { if (delta < best_delta) {
best = i + 1; best = base + i;
best_delta = delta; best_delta = delta;
} }
} }
@ -299,6 +316,9 @@ int FrameCapIndexFromRate(double rate) {
} }
int FrameCapIndexFromCvar() { int FrameCapIndexFromCvar() {
if (FrameCapHasAuto() && rex::cvar::Query<bool>("skate3_guest_fps_cap_auto")) {
return 0;
}
double current = 0.0; double current = 0.0;
if (UseGuestFrameCap()) { if (UseGuestFrameCap()) {
current = rex::cvar::Query<double>("skate3_guest_fps_cap"); current = rex::cvar::Query<double>("skate3_guest_fps_cap");
@ -961,8 +981,7 @@ void SimpleSettingsDialog::ReloadProfiles() {
void SimpleSettingsDialog::SaveVideo() { void SimpleSettingsDialog::SaveVideo() {
resolution_scale_index_ = resolution_scale_index_ =
std::clamp(resolution_scale_index_, 0, static_cast<int>(kResolutionScales.size()) - 1); std::clamp(resolution_scale_index_, 0, static_cast<int>(kResolutionScales.size()) - 1);
frame_cap_index_ = frame_cap_index_ = std::clamp(frame_cap_index_, 0, FrameCapUnlimitedIndex());
std::clamp(frame_cap_index_, 0, static_cast<int>(kFrameCapLabels.size()) - 1);
aspect_ratio_index_ = aspect_ratio_index_ =
std::clamp(aspect_ratio_index_, 0, static_cast<int>(kAspectRatioLabels.size()) - 1); std::clamp(aspect_ratio_index_, 0, static_cast<int>(kAspectRatioLabels.size()) - 1);
field_of_view_ = std::clamp(field_of_view_, 40.0f, 120.0f); field_of_view_ = std::clamp(field_of_view_, 40.0f, 120.0f);
@ -981,7 +1000,16 @@ void SimpleSettingsDialog::SaveVideo() {
rex::cvar::SetFlagByName("draw_resolution_scale_x", scale); rex::cvar::SetFlagByName("draw_resolution_scale_x", scale);
rex::cvar::SetFlagByName("draw_resolution_scale_y", scale); rex::cvar::SetFlagByName("draw_resolution_scale_y", scale);
if (UseGuestFrameCap()) { if (UseGuestFrameCap()) {
const double cap_fps = frame_cap_index_ != 0 ? kFrameCapRates[frame_cap_index_ - 1] : 0.0; const int rate_base = FrameCapRateBaseIndex();
if (FrameCapHasAuto()) {
// Auto rides its own flag; the pacer derives the rate from the display
// refresh live (window moves between monitors keep working).
SetBoolCvar("skate3_guest_fps_cap_auto", frame_cap_index_ == 0);
}
const double cap_fps =
frame_cap_index_ >= rate_base && frame_cap_index_ < FrameCapUnlimitedIndex()
? kFrameCapRates[frame_cap_index_ - rate_base]
: 0.0;
rex::cvar::SetFlagByName("skate3_guest_fps_cap", std::to_string(cap_fps)); rex::cvar::SetFlagByName("skate3_guest_fps_cap", std::to_string(cap_fps));
// Never run both pacers: the host limiter waits on the paint thread // Never run both pacers: the host limiter waits on the paint thread
// without backpressuring the guest, so presents drop frames on an // without backpressuring the guest, so presents drop frames on an
@ -990,10 +1018,12 @@ void SimpleSettingsDialog::SaveVideo() {
SetBoolCvar("d3d12_present_frame_limiter", false); SetBoolCvar("d3d12_present_frame_limiter", false);
} }
} else if (HasHostFrameCapCvars()) { } else if (HasHostFrameCapCvars()) {
SetBoolCvar("d3d12_present_frame_limiter", frame_cap_index_ != 0); const bool capped = frame_cap_index_ < FrameCapUnlimitedIndex();
if (frame_cap_index_ != 0) { SetBoolCvar("d3d12_present_frame_limiter", capped);
rex::cvar::SetFlagByName("d3d12_present_frame_limiter_fps", if (capped) {
std::to_string(kFrameCapRates[frame_cap_index_ - 1])); rex::cvar::SetFlagByName(
"d3d12_present_frame_limiter_fps",
std::to_string(kFrameCapRates[frame_cap_index_ - FrameCapRateBaseIndex()]));
} }
} }
SetBoolCvar("fullscreen", fullscreen_); SetBoolCvar("fullscreen", fullscreen_);
@ -1135,12 +1165,27 @@ void SimpleSettingsDialog::BuildRows(std::vector<RowSpec>& rows, int category) {
row.label = "Framerate Cap"; row.label = "Framerate Cap";
row.desc = row.desc =
"Limit how fast the game runs. A steady cap slightly below your display's " "Limit how fast the game runs. A steady cap slightly below your display's "
"refresh rate gives the smoothest pacing on variable-refresh displays."; "refresh rate gives the smoothest pacing on variable-refresh displays; "
for (const char* label : kFrameCapLabels) { "Auto tracks the current display and does exactly that. Frames above the "
row.options.push_back(label); "refresh rate cannot be shown and only make steady motion judder.";
if (FrameCapHasAuto()) {
const float hz = rex::ui::Window::CachedDisplayRefreshHz();
row.options.push_back(
hz >= 30.0f
? "Auto (" + std::to_string(int(hz) - 4) + " FPS)"
: std::string("Auto"));
} }
for (size_t i = 1; i < kFrameCapLabels.size(); ++i) {
row.options.push_back(kFrameCapLabels[i]);
}
row.options.push_back(kFrameCapLabels[0]);
row.index = &frame_cap_index_; row.index = &frame_cap_index_;
row.reset = [this] { row.reset = [this] {
if (FrameCapHasAuto() &&
CvarDefaultBool("skate3_guest_fps_cap_auto", false)) {
frame_cap_index_ = 0;
return;
}
double rate = 0.0; double rate = 0.0;
if (UseGuestFrameCap()) { if (UseGuestFrameCap()) {
rate = CvarDefaultDouble("skate3_guest_fps_cap", 0.0); rate = CvarDefaultDouble("skate3_guest_fps_cap", 0.0);
@ -2062,9 +2107,17 @@ void SimpleSettingsDialog::OnDraw(ImGuiIO& io) {
std::min(frame_pos.y + 98.0f * s, columns_y_flow), content_bottom - content_view_h)); std::min(frame_pos.y + 98.0f * s, columns_y_flow), content_bottom - content_view_h));
const float title_y = Snap(columns_y - 74.0f * s); const float title_y = Snap(columns_y - 74.0f * s);
const float content_view_bottom = columns_y + content_view_h; const float content_view_bottom = columns_y + content_view_h;
// Description panel height - also anchors Close Game's bottom edge. // Description panel height - also anchors Close Game's bottom edge. When
const float desc_panel_h = // the category rail is long enough to push Close Game below the panel's
Snap(std::min(content_bottom - columns_y, 320.0f * s)); // tuned 320*s height, the panel stretches so its bottom stays level with
// the button's (the two columns read as one aligned baseline).
const float rail_close_bottom =
Snap(columns_y + category_count * (rail_item_h + row_gap) - row_gap +
45.0f * s) +
rail_item_h;
const float desc_panel_h = Snap(std::min(
content_bottom - columns_y,
std::max(320.0f * s, rail_close_bottom - columns_y)));
const ImVec2 mouse = io.MousePos; const ImVec2 mouse = io.MousePos;
const bool mouse_moved = const bool mouse_moved =