Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -2634,7 +2634,7 @@ They appear in the Frame Limiter section of the settings UI.
<tr>
<td>Description</td>
<td colspan="2">
Optional FPS limit to apply while streaming. Set to 0 to use the stream's requested FPS.
Optional FPS limit to apply while streaming. Set to 0 to use the stream's requested FPS minus @code{}frame_limiter_fps_offset@endcode.
</td>
</tr>
<tr>
Expand All @@ -2649,6 +2649,33 @@ They appear in the Frame Limiter section of the settings UI.
</tr>
</table>

### frame_limiter_fps_offset

<table>
<tr>
<td>Description</td>
<td colspan="2">
FPS value subtracted from the stream's requested FPS when @code{}frame_limiter_fps_limit@endcode is 0. This keeps the stream container at the client-requested refresh while applying a lower host-side frame cap.
<br><br>
For example, a 120 FPS stream with @code{}frame_limiter_fps_offset = 3@endcode applies a 117 FPS limiter.
</td>
</tr>
<tr>
<td>Default</td>
<td colspan="2">@code{}0@endcode</td>
</tr>
<tr>
<td>Range</td>
<td colspan="2">@code{}0-1000@endcode</td>
</tr>
<tr>
<td>Example</td>
<td colspan="2">@code{}
frame_limiter_fps_offset = 3
@endcode</td>
</tr>
</table>

### rtss_install_path

<table>
Expand Down
3 changes: 3 additions & 0 deletions src/config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -901,6 +901,7 @@ namespace config {
false, // enable
"auto", // provider
0, // fps_limit
0, // fps_offset
false // disable_vsync
};

Expand Down Expand Up @@ -1670,6 +1671,7 @@ namespace config {
frame_limiter.provider = "auto";
}
int_between_f(vars, "frame_limiter_fps_limit", frame_limiter.fps_limit, {0, 1000});
int_between_f(vars, "frame_limiter_fps_offset", frame_limiter.fps_offset, {0, 1000});
bool_f(vars, "frame_limiter_disable_vsync", frame_limiter.disable_vsync);
bool_f(vars, "rtss_disable_vsync_ullm", frame_limiter.disable_vsync);
string_f(vars, "rtss_install_path", rtss.install_path);
Expand Down Expand Up @@ -2175,6 +2177,7 @@ namespace config {
"frame_limiter_enable",
"frame_limiter_provider",
"frame_limiter_fps_limit",
"frame_limiter_fps_offset",
"rtss_frame_limit_type",
"frame_limiter_disable_vsync",

Expand Down
3 changes: 3 additions & 0 deletions src/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,9 @@ namespace config {
// Optional FPS limit override. 0 uses the stream's requested FPS.
int fps_limit {0};

// Optional offset subtracted from the stream's requested FPS when fps_limit is 0.
int fps_offset {0};

// When enabled, Sunshine forces the NVIDIA driver VSYNC setting to Off during streams when available.
// When NVIDIA overrides are unavailable, the display helper falls back to the highest refresh rate instead.
// Restores the previous VSYNC state when streaming stops.
Expand Down
114 changes: 83 additions & 31 deletions src/platform/windows/frame_limiter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
#include <algorithm>
#include <array>
#include <cctype>
#include <optional>
#include <numeric>
#include <optional>
#include <string>
#include <vector>

Expand All @@ -29,13 +29,19 @@ namespace platf {
bool g_nvcp_started = false;
bool g_gen1_framegen_fix_active = false;
bool g_gen2_framegen_fix_active = false;
int g_last_effective_limit = 0;
bool g_prev_frame_limiter_enabled = false;
std::string g_prev_frame_limiter_provider;
bool g_prev_frame_limiter_provider_set = false;
bool g_prev_disable_vsync = false;
std::string g_prev_rtss_frame_limit_type;
bool g_prev_rtss_frame_limit_type_set = false;
int g_last_rtss_limit_value = 0;
int g_last_rtss_limit_denominator = 1;

struct rtss_limit_t {
int value = 0;
int denominator = 1;
};

frame_limiter_provider parse_provider(const std::string &value) {
std::string normalized;
Expand Down Expand Up @@ -100,6 +106,58 @@ namespace platf {
return "front edge sync";
}

int apply_stream_fps_offset(int requested_fps, int offset) {
if (requested_fps <= 0 || offset <= 0) {
return requested_fps;
}

return std::max(1, requested_fps - offset);
}

void reduce_rtss_limit(rtss_limit_t &limit) {
if (limit.denominator <= 1 || limit.value <= 0) {
limit.denominator = 1;
return;
}

if (limit.value % 1000 == 0) {
limit.value /= 1000;
limit.denominator = 1;
} else if (limit.value % 100 == 0) {
limit.value /= 100;
limit.denominator = 10;
} else if (limit.value % 10 == 0) {
limit.value /= 10;
limit.denominator = 100;
}

int gcd_value = std::gcd(limit.value, limit.denominator);
if (gcd_value > 1) {
limit.value /= gcd_value;
limit.denominator /= gcd_value;
}
}

rtss_limit_t apply_stream_fps_offset_scaled(int requested_fps, int requested_fps_scaled, int offset) {
if (requested_fps_scaled <= 0) {
return {apply_stream_fps_offset(requested_fps, offset), 1};
}

const int scaled_offset = offset > 0 ? offset * 1000 : 0;
int scaled_limit = requested_fps_scaled - scaled_offset;
if (offset > 0) {
scaled_limit = std::max(1000, scaled_limit);
} else {
scaled_limit = std::max(0, scaled_limit);
}
rtss_limit_t limit {
scaled_limit,
1000
};
reduce_rtss_limit(limit);
return limit;
}

} // namespace

const char *frame_limiter_provider_to_string(frame_limiter_provider provider) {
Expand Down Expand Up @@ -170,40 +228,33 @@ namespace platf {
const bool want_nv_vsync_override = (config::frame_limiter.disable_vsync || capture_fix_enabled) && nvidia_gpu_present && nvcp_ready;

bool nvcp_already_invoked = false;
int effective_limit = (lossless_rtss_limit && *lossless_rtss_limit > 0) ? *lossless_rtss_limit : fps;
const bool using_lossless_rtss_limit = lossless_rtss_limit && *lossless_rtss_limit > 0;
const bool using_manual_fps_limit = config::frame_limiter.fps_limit > 0;
const bool using_stream_fps_limit = !using_lossless_rtss_limit && !using_manual_fps_limit;

int effective_limit = using_lossless_rtss_limit ? *lossless_rtss_limit : fps;
if (using_stream_fps_limit) {
effective_limit = apply_stream_fps_offset(effective_limit, config::frame_limiter.fps_offset);
}
if (config::frame_limiter.fps_limit > 0) {
effective_limit = config::frame_limiter.fps_limit;
}
g_last_effective_limit = effective_limit;

int rtss_limit_value = effective_limit;
int rtss_limit_denominator = 1;

if ((!lossless_rtss_limit || *lossless_rtss_limit <= 0) && config::frame_limiter.fps_limit <= 0) {
if (fps_scaled > 0) {
rtss_limit_value = fps_scaled;
rtss_limit_denominator = 1000;

if (rtss_limit_value % 1000 == 0) {
rtss_limit_value /= 1000;
rtss_limit_denominator = 1;
} else if (rtss_limit_value % 100 == 0) {
rtss_limit_value /= 100;
rtss_limit_denominator = 10;
} else if (rtss_limit_value % 10 == 0) {
rtss_limit_value /= 10;
rtss_limit_denominator = 100;
}
if (using_stream_fps_limit) {
const auto rtss_limit = apply_stream_fps_offset_scaled(fps, fps_scaled, config::frame_limiter.fps_offset);
rtss_limit_value = rtss_limit.value;
rtss_limit_denominator = rtss_limit.denominator;
}
g_last_rtss_limit_value = rtss_limit_value;
g_last_rtss_limit_denominator = rtss_limit_denominator;

int gcd_value = std::gcd(rtss_limit_value, rtss_limit_denominator);
if (gcd_value > 1) {
rtss_limit_value /= gcd_value;
rtss_limit_denominator /= gcd_value;
}
} else {
rtss_limit_value = effective_limit;
rtss_limit_denominator = 1;
}
if (using_stream_fps_limit && config::frame_limiter.fps_offset > 0) {
BOOST_LOG(info) << "Frame limiter FPS offset applied: requested=" << fps
<< ", offset=" << config::frame_limiter.fps_offset
<< ", effective=" << effective_limit;
}

if (frame_limit_enabled) {
Expand Down Expand Up @@ -374,15 +425,16 @@ namespace platf {

g_active_provider = frame_limiter_provider::none;
g_nvcp_started = false;
g_last_effective_limit = 0;
g_last_rtss_limit_value = 0;
g_last_rtss_limit_denominator = 1;
}

void frame_limiter_streaming_refresh() {
if (g_active_provider != frame_limiter_provider::rtss || g_last_effective_limit <= 0) {
if (g_active_provider != frame_limiter_provider::rtss || g_last_rtss_limit_value <= 0) {
return;
}

if (rtss_streaming_refresh(g_last_effective_limit)) {
if (rtss_streaming_refresh(g_last_rtss_limit_value, g_last_rtss_limit_denominator)) {
BOOST_LOG(info) << "Frame limiter provider 'rtss' refreshed";
}
}
Expand Down
28 changes: 16 additions & 12 deletions src/platform/windows/rtss_integration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1013,13 +1013,13 @@ namespace platf {
return g_limit_active;
}

bool rtss_streaming_refresh(int fps) {
bool rtss_streaming_refresh(int scaled_limit, int denominator) {
if (!config::frame_limiter.enable) {
return false;
}

if (!g_limit_active && !g_settings_dirty) {
return rtss_streaming_start(fps, 1);
return rtss_streaming_start(scaled_limit, denominator);
}

g_rtss_root = resolve_rtss_root();
Expand Down Expand Up @@ -1051,7 +1051,7 @@ namespace platf {
}
}

int current_denominator = 1;
int current_denominator = denominator > 0 ? denominator : 1;
auto old_den = set_limit_denominator(g_rtss_root, current_denominator);
if (old_den.has_value() && *old_den != current_denominator) {
if (!g_original_denominator.has_value()) {
Expand Down Expand Up @@ -1099,19 +1099,23 @@ namespace platf {
}
}

int scaled_limit = fps;
bool applied_limit = false;
int applied_limit = scaled_limit;
if (applied_limit < 0) {
applied_limit = 0;
}
bool limit_applied = false;
if (g_hooks) {
set_profile_property_int("FramerateLimit", scaled_limit);
applied_limit = true;
} else if (write_profile_value_int(g_rtss_root, "FramerateLimit", scaled_limit)) {
applied_limit = true;
set_profile_property_int("FramerateLimit", applied_limit);
limit_applied = true;
} else if (write_profile_value_int(g_rtss_root, "FramerateLimit", applied_limit)) {
limit_applied = true;
}
if (applied_limit) {
if (limit_applied) {
g_limit_active = true;
g_limit_modified = true;
dirty = true;
BOOST_LOG(info) << "RTSS refreshed framerate limit=" << scaled_limit << " (denominator=" << current_denominator << ")";
double limit_fps = current_denominator > 0 ? (double) applied_limit / current_denominator : 0.0;
BOOST_LOG(info) << "RTSS refreshed framerate limit=" << limit_fps << "Hz (raw=" << applied_limit << ", denominator=" << current_denominator << ")";
}

if (dirty && !g_settings_dirty) {
Expand All @@ -1128,7 +1132,7 @@ namespace platf {
g_recovery_file_owned = write_overrides_file(snapshot);
}

return applied_limit;
return limit_applied;
}

void rtss_streaming_stop(bool keep_process_running) {
Expand Down
2 changes: 1 addition & 1 deletion src/platform/windows/rtss_integration.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ namespace platf {
bool rtss_streaming_start(int scaled_limit, int denominator);

// Re-apply RTSS frame limit and related settings without resetting originals.
bool rtss_streaming_refresh(int fps);
bool rtss_streaming_refresh(int scaled_limit, int denominator);

// Restore any RTSS settings modified at stream start.
// If keep_process_running is true, Sunshine leaves RTSS running for pause/resume scenarios.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -921,6 +921,7 @@ const ALLOWED_OVERRIDE_KEYS = new Set<string>([
'frame_limiter_enable',
'frame_limiter_provider',
'frame_limiter_fps_limit',
'frame_limiter_fps_offset',
'rtss_frame_limit_type',
'frame_limiter_disable_vsync',

Expand Down Expand Up @@ -1772,7 +1773,10 @@ const BOOL_STRING_PAIRS = [
['1', '0'],
] as const;

const NUMERIC_OVERRIDE_KEYS = new Set<string>(['frame_limiter_fps_limit']);
const NUMERIC_OVERRIDE_KEYS = new Set<string>([
'frame_limiter_fps_limit',
'frame_limiter_fps_offset',
]);

function boolPairFromValue(value: unknown): BoolPair | null {
if (value === true || value === false) return { truthy: true, falsy: false };
Expand Down
1 change: 1 addition & 0 deletions src_assets/common/assets/web/configs/configFieldSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const NUMBER_FIELD_OVERRIDES: Record<string, Partial<ConfigFieldDefinition>> = {
minimum_fps_target: { min: 0, max: 1000, placeholder: '0' },
nvenc_vbv_increase: { min: 0, max: 400, placeholder: '0' },
frame_limiter_fps_limit: { min: 0, max: 1000, step: 1, precision: 0, placeholder: '0' },
frame_limiter_fps_offset: { min: 0, max: 1000, step: 1, precision: 0, placeholder: '0' },
};

function isFiniteNumber(value: unknown): value is number {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -310,13 +310,23 @@ onMounted(() => {
/>
</div>

<ConfigFieldRenderer
setting-key="frame_limiter_fps_limit"
v-model="config.frame_limiter_fps_limit"
:label="t('frameLimiter.limitLabel')"
:desc="t('frameLimiter.limitHint')"
:placeholder="t('frameLimiter.limitPlaceholder')"
/>
<div class="grid gap-4 md:grid-cols-2">
<ConfigFieldRenderer
setting-key="frame_limiter_fps_limit"
v-model="config.frame_limiter_fps_limit"
:label="t('frameLimiter.limitLabel')"
:desc="t('frameLimiter.limitHint')"
:placeholder="t('frameLimiter.limitPlaceholder')"
/>

<ConfigFieldRenderer
setting-key="frame_limiter_fps_offset"
v-model="config.frame_limiter_fps_offset"
:label="t('frameLimiter.offsetLabel')"
:desc="t('frameLimiter.offsetHint')"
:placeholder="t('frameLimiter.offsetPlaceholder')"
/>
</div>

<ConfigFieldRenderer
setting-key="frame_limiter_disable_vsync"
Expand Down
Loading