diff --git a/src/config.cpp b/src/config.cpp index fd9df4a2f06..ad015d5bc9a 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -405,6 +405,14 @@ namespace config { true, // nv_realtime_hags true, // nv_opengl_vulkan_on_dxgi true, // nv_sunshine_high_power_mode + false, // nv_optimize_game (opt-in) + true, // nv_force_vsync + true, // nv_lock_frame_rate + -2, // nv_frl_fps_offset + 0, // nv_frl_fps_override (0 = derive from client fps) + false, // nv_prefer_max_performance + false, // nv_low_latency_mode + false, // nv_apply_to_base_profile (opt-in, machine-wide) false, // vdd_keep_enabled false, // vdd_headless_create_enabled false, // vdd_reuse (default: recreate VDD for each client) @@ -1127,6 +1135,26 @@ namespace config { bool_f(vars, "nvenc_opengl_vulkan_on_dxgi", video.nv_opengl_vulkan_on_dxgi); bool_f(vars, "nvenc_latency_over_power", video.nv_sunshine_high_power_mode); + bool_f(vars, "nvenc_optimize_game", video.nv_optimize_game); + bool_f(vars, "nvenc_force_vsync", video.nv_force_vsync); + bool_f(vars, "nvenc_lock_frame_rate", video.nv_lock_frame_rate); + int_f(vars, "nvenc_frl_fps_offset", video.nv_frl_fps_offset); + int_f(vars, "nvenc_frl_fps_override", video.nv_frl_fps_override); + // Clamp to the same ranges enforced in the Web UI to defend against + // tampered config files (avoids signed overflow when later combined + // with the client framerate, and matches what the slider permits). + if (video.nv_frl_fps_offset < -30 || video.nv_frl_fps_offset > 30) { + BOOST_LOG(warning) << "nvenc_frl_fps_offset out of range [-30, 30]; clamping from " << video.nv_frl_fps_offset; + video.nv_frl_fps_offset = std::clamp(video.nv_frl_fps_offset, -30, 30); + } + if (video.nv_frl_fps_override < 0 || video.nv_frl_fps_override > 500) { + BOOST_LOG(warning) << "nvenc_frl_fps_override out of range [0, 500]; clamping from " << video.nv_frl_fps_override; + video.nv_frl_fps_override = std::clamp(video.nv_frl_fps_override, 0, 500); + } + bool_f(vars, "nvenc_prefer_max_performance", video.nv_prefer_max_performance); + bool_f(vars, "nvenc_low_latency_mode", video.nv_low_latency_mode); + bool_f(vars, "nvenc_apply_to_base_profile", video.nv_apply_to_base_profile); + #if !defined(__ANDROID__) && !defined(__APPLE__) video.nv_legacy.preset = video.nv.quality_preset + 11; video.nv_legacy.multipass = video.nv.two_pass == nvenc::nvenc_two_pass::quarter_resolution ? NV_ENC_TWO_PASS_QUARTER_RESOLUTION : diff --git a/src/config.h b/src/config.h index 748e7936e21..58f0b0e21d4 100644 --- a/src/config.h +++ b/src/config.h @@ -38,6 +38,20 @@ namespace config { bool nv_realtime_hags; bool nv_opengl_vulkan_on_dxgi; bool nv_sunshine_high_power_mode; + + // Stream-time NVIDIA driver optimizations applied via NvAPI to the game's + // application profile (and optionally the BASE / global profile). All + // changes are persisted to an undo manifest in %ProgramData%\Sunshine and + // rolled back when the stream stops, or on the next launch after a crash. + bool nv_optimize_game; // master switch for the per-game optimizations + bool nv_force_vsync; // VSYNCMODE -> FORCEON + bool nv_lock_frame_rate; // FRL_FPS -> client_fps + nv_frl_fps_offset + int nv_frl_fps_offset; // delta added to client fps for FRL target + int nv_frl_fps_override; // if > 0, use this value directly + bool nv_prefer_max_performance; // PREFERRED_PSTATE -> PREFER_MAX + bool nv_low_latency_mode; // PRERENDERLIMIT -> 1 (NVCP "Low Latency Mode = On") + bool nv_apply_to_base_profile; // also write the same settings to the BASE profile + bool vdd_keep_enabled; /** When true, after stream end if no display is found (headless), create Zako VDD automatically. Default false. */ bool vdd_headless_create_enabled; diff --git a/src/platform/common.h b/src/platform/common.h index 7bbbeb8d5c5..ebbe8dd6722 100644 --- a/src/platform/common.h +++ b/src/platform/common.h @@ -742,6 +742,28 @@ namespace platf { void streaming_will_stop(); + /** + * @brief Apply per-stream GPU driver optimizations for the launched game. + * On Windows this writes NVIDIA application/BASE profile entries + * (force VSync, FRL frame-rate cap, low latency, P-state) gated on + * the `nv_optimize_game` config switch and on the system actually + * being NVIDIA. On other platforms this is a no-op. + * @param game_cmd The command line of the running game (e.g. proc::proc + * .get_app_cmd()). Used to derive the EXE basename for + * the per-game profile leg. Empty string is allowed and + * disables only the per-game leg. + * @param client_fps Client refresh rate, used to derive the FRL target. + */ + void + apply_stream_optimizations(const std::string &game_cmd, int client_fps); + + /** + * @brief Roll back any changes made by apply_stream_optimizations(). + * Safe to call unconditionally; does nothing if nothing was applied. + */ + void + restore_stream_optimizations(); + /** * @brief Enter Away Mode - display turns off, system stays running for instant wake. * On Windows, this uses ES_AWAYMODE_REQUIRED + ES_SYSTEM_REQUIRED and turns off the monitor. diff --git a/src/platform/linux/misc.cpp b/src/platform/linux/misc.cpp index e126ac04385..fbccaee9aa1 100644 --- a/src/platform/linux/misc.cpp +++ b/src/platform/linux/misc.cpp @@ -315,6 +315,16 @@ namespace platf { // Nothing to do } + void + apply_stream_optimizations(const std::string &, int) { + // Nothing to do (Linux has no NVAPI driver-profile equivalent here yet) + } + + void + restore_stream_optimizations() { + // Nothing to do + } + void enter_away_mode() { // TODO: Linux implementation could use DPMS to turn off display diff --git a/src/platform/macos/misc.mm b/src/platform/macos/misc.mm index 9b08cc00f59..51d4f20ee28 100644 --- a/src/platform/macos/misc.mm +++ b/src/platform/macos/misc.mm @@ -243,6 +243,16 @@ // Nothing to do } + void + apply_stream_optimizations(const std::string &, int) { + // Nothing to do on macOS + } + + void + restore_stream_optimizations() { + // Nothing to do + } + void enter_away_mode() { BOOST_LOG(info) << "Away Mode is not yet implemented on macOS"sv; diff --git a/src/platform/windows/misc.cpp b/src/platform/windows/misc.cpp index fc04a0c2ac4..b5fff1e03bb 100644 --- a/src/platform/windows/misc.cpp +++ b/src/platform/windows/misc.cpp @@ -38,6 +38,7 @@ #include "misc.h" +#include "src/config.h" #include "src/entry_handler.h" #include "src/globals.h" #include "src/logging.h" @@ -1401,6 +1402,66 @@ namespace platf { } } + namespace { + // Forward declaration; defined later in this TU. + std::wstring from_utf8(const std::string &string); + + /** + * @brief Best-effort lower-case wide basename of a launch command line. + * e.g. "\"C:\\Games\\foo\\bar.exe\" --opt" -> L"bar.exe" + */ + std::wstring + extract_exe_basename_w(const std::string &cmd) { + if (cmd.empty()) return {}; + std::string token; + if (cmd.front() == '"') { + auto end = cmd.find('"', 1); + token = cmd.substr(1, end == std::string::npos ? std::string::npos : end - 1); + } + else { + auto end = cmd.find_first_of(" \t"); + token = cmd.substr(0, end); + } + if (token.empty()) return {}; + // Convert UTF-8 to UTF-16 first so that std::filesystem::path doesn't + // misinterpret the bytes via the legacy ANSI code page on Windows + // (breaks profile lookup for non-ASCII paths). + auto wide_token = from_utf8(token); + if (wide_token.empty()) return {}; + auto fname = std::filesystem::path(wide_token).filename().wstring(); + std::transform(fname.begin(), fname.end(), fname.begin(), [](wchar_t c) { + return static_cast(::towlower(c)); + }); + return fname; + } + } // namespace + + void + apply_stream_optimizations(const std::string &game_cmd, int client_fps) { + if (!config::video.nv_optimize_game) { + return; + } + auto exe_w = extract_exe_basename_w(game_cmd); + if (!nvprefs_instance.load()) { + // Either non-NV system or driver init failed -- silently skip. + return; + } + nvprefs_instance.apply_stream_optimizations(exe_w, client_fps); + nvprefs_instance.unload(); + } + + void + restore_stream_optimizations() { + if (!config::video.nv_optimize_game) { + return; + } + if (!nvprefs_instance.load()) { + return; + } + nvprefs_instance.restore_stream_optimizations(); + nvprefs_instance.unload(); + } + void enter_away_mode() { if (away_mode_active.exchange(true)) { diff --git a/src/platform/windows/nvprefs/driver_settings.cpp b/src/platform/windows/nvprefs/driver_settings.cpp index 2cea9fa4c58..7529246da3b 100644 --- a/src/platform/windows/nvprefs/driver_settings.cpp +++ b/src/platform/windows/nvprefs/driver_settings.cpp @@ -5,6 +5,7 @@ // local includes #include "driver_settings.h" #include "nvprefs_common.h" +#include "../misc.h" namespace { @@ -311,4 +312,412 @@ namespace nvprefs { return true; } + // ----------------------------------------------------------------- + // Stream-time game optimizations (per-game application profile + + // optional BASE profile mirror). See driver_settings.h for rationale. + // ----------------------------------------------------------------- + namespace { + + constexpr auto sunshine_game_profile_name = L"SunshineStreamGame"; + + // Compute target FRL (frame rate limiter) value for the current client. + NvU32 + compute_frl_fps(int client_fps, const nvprefs_options &opts) { + if (opts.nv_frl_fps_override > 0) { + return static_cast(opts.nv_frl_fps_override); + } + int v = client_fps + opts.nv_frl_fps_offset; + if (v < 1) v = 1; + return static_cast(v); + } + + struct desired_settings_t { + std::optional vsync; + std::optional frl; + std::optional pstate; + std::optional prerender; + }; + + desired_settings_t + compute_desired(const nvprefs_options &opts, int client_fps) { + desired_settings_t d; + if (opts.nv_force_vsync) d.vsync = VSYNCMODE_FORCEON; + if (opts.nv_lock_frame_rate) d.frl = compute_frl_fps(client_fps, opts); + if (opts.nv_prefer_max_performance) d.pstate = PREFERRED_PSTATE_PREFER_MAX; + // PRERENDERLIMIT == 1 matches NVIDIA Control Panel "Low Latency Mode = On". + // Value 0 means "use the 3D application setting" so leaving it untouched + // when the option is off is equivalent to default driver behavior. + if (opts.nv_low_latency_mode) d.prerender = 1; + return d; + } + + // Apply a single uint32 setting to a profile. Records the previous value + // (or std::nullopt if the setting wasn't set on the profile) into undo_out + // ONLY when we actually change the value. No-op when current == desired. + bool + apply_uint_setting(NvDRSSessionHandle session, + NvDRSProfileHandle profile, + NvU32 setting_id, + NvU32 desired_value, + std::optional &undo_out, + const std::wstring &log_label) { + NVDRS_SETTING setting = {}; + setting.version = NVDRS_SETTING_VER1; + NvAPI_Status status = NvAPI_DRS_GetSetting(session, profile, setting_id, &setting); + + std::optional previous; + bool already_at_desired = false; + if (status == NVAPI_OK) { + if (setting.settingLocation == NVDRS_CURRENT_PROFILE_LOCATION) { + previous = setting.u32CurrentValue; + } + if (setting.u32CurrentValue == desired_value && setting.settingLocation == NVDRS_CURRENT_PROFILE_LOCATION) { + already_at_desired = true; + } + } + else if (status != NVAPI_SETTING_NOT_FOUND) { + nvapi_error_message(status); + error_message(std::wstring(L"NvAPI_DRS_GetSetting() failed for ") + log_label); + return false; + } + + if (already_at_desired) { + // No change to make and nothing to undo. + return true; + } + + setting = {}; + setting.version = NVDRS_SETTING_VER1; + setting.settingId = setting_id; + setting.settingType = NVDRS_DWORD_TYPE; + setting.settingLocation = NVDRS_CURRENT_PROFILE_LOCATION; + setting.u32CurrentValue = desired_value; + status = NvAPI_DRS_SetSetting(session, profile, &setting); + if (status != NVAPI_OK) { + nvapi_error_message(status); + error_message(std::wstring(L"NvAPI_DRS_SetSetting() failed for ") + log_label); + return false; + } + + undo_out = undo_data_t::data_t::setting_undo_t { desired_value, previous }; + info_message(std::wstring(L"Set ") + log_label + L" on profile"); + return true; + } + + // Restore a single uint32 setting using saved undo data. + // Skips silently when the user has manually changed the value since we + // wrote it (the original value would be lost anyway). + bool + restore_uint_setting(NvDRSSessionHandle session, + NvDRSProfileHandle profile, + NvU32 setting_id, + const undo_data_t::data_t::setting_undo_t &undo, + const std::wstring &log_label) { + NVDRS_SETTING setting = {}; + setting.version = NVDRS_SETTING_VER1; + NvAPI_Status status = NvAPI_DRS_GetSetting(session, profile, setting_id, &setting); + if (status != NVAPI_OK && status != NVAPI_SETTING_NOT_FOUND) { + nvapi_error_message(status); + error_message(std::wstring(L"NvAPI_DRS_GetSetting() failed while restoring ") + log_label); + return false; + } + if (status == NVAPI_OK) { + const bool ours = setting.settingLocation == NVDRS_CURRENT_PROFILE_LOCATION && setting.u32CurrentValue == undo.our_value; + if (!ours) { + info_message(std::wstring(log_label) + L" was changed externally, skipping restore"); + return true; + } + } + + if (undo.undo_value) { + setting = {}; + setting.version = NVDRS_SETTING_VER1; + setting.settingId = setting_id; + setting.settingType = NVDRS_DWORD_TYPE; + setting.settingLocation = NVDRS_CURRENT_PROFILE_LOCATION; + setting.u32CurrentValue = *undo.undo_value; + status = NvAPI_DRS_SetSetting(session, profile, &setting); + if (status != NVAPI_OK) { + nvapi_error_message(status); + error_message(std::wstring(L"NvAPI_DRS_SetSetting() failed while restoring ") + log_label); + return false; + } + } + else { + status = NvAPI_DRS_DeleteProfileSetting(session, profile, setting_id); + if (status != NVAPI_OK && status != NVAPI_SETTING_NOT_FOUND) { + nvapi_error_message(status); + error_message(std::wstring(L"NvAPI_DRS_DeleteProfileSetting() failed while restoring ") + log_label); + return false; + } + } + info_message(std::wstring(L"Restored ") + log_label); + return true; + } + + // Apply the four configurable settings to the given profile. Returns false + // on a hard NvAPI failure (caller should bail without saving). + bool + apply_desired_to_profile(NvDRSSessionHandle session, + NvDRSProfileHandle profile, + const desired_settings_t &d, + std::optional &vsync_undo, + std::optional &frl_undo, + std::optional &pstate_undo, + std::optional &prerender_undo) { + if (d.vsync && !apply_uint_setting(session, profile, VSYNCMODE_ID, *d.vsync, vsync_undo, L"VSYNCMODE")) { + return false; + } + if (d.frl && !apply_uint_setting(session, profile, FRL_FPS_ID, *d.frl, frl_undo, L"FRL_FPS")) { + return false; + } + if (d.pstate && !apply_uint_setting(session, profile, PREFERRED_PSTATE_ID, *d.pstate, pstate_undo, L"PREFERRED_PSTATE")) { + return false; + } + if (d.prerender && !apply_uint_setting(session, profile, PRERENDERLIMIT_ID, *d.prerender, prerender_undo, L"PRERENDERLIMIT")) { + return false; + } + return true; + } + + } // namespace + + bool + driver_settings_t::check_and_modify_game_profile(const std::wstring &exe_name, int client_fps, std::optional &undo_out) { + undo_out.reset(); + if (!session_handle) return false; + + const auto opts = get_nvprefs_options(); + if (!opts.nv_optimize_game) { + // Game optimizations disabled by user, no-op. + return true; + } + if (exe_name.empty()) { + // Cannot match any profile without an exe — skip silently. + return true; + } + + const desired_settings_t desired = compute_desired(opts, client_fps); + if (!desired.vsync && !desired.frl && !desired.pstate && !desired.prerender) { + // All sub-options off, nothing to do. + return true; + } + + NvAPI_Status status; + + // 1. Try to find an existing profile that already owns this exe. + NvAPI_UnicodeString app_name = {}; + fill_nvapi_string(app_name, exe_name.c_str()); + + NvDRSProfileHandle profile_handle = 0; + bool profile_was_created = false; + bool application_was_added = false; + std::wstring profile_name_used; + + NVDRS_APPLICATION app_info = {}; + app_info.version = NVDRS_APPLICATION_VER; + status = NvAPI_DRS_FindApplicationByName(session_handle, app_name, &profile_handle, &app_info); + if (status == NVAPI_OK) { + // Found — fetch the profile name for the undo manifest. + NVDRS_PROFILE existing = {}; + existing.version = NVDRS_PROFILE_VER; + if (NvAPI_DRS_GetProfileInfo(session_handle, profile_handle, &existing) == NVAPI_OK) { + profile_name_used.assign(reinterpret_cast(existing.profileName)); + } + } + else { + // No profile owns this exe: get/create our SunshineStreamGame profile and add the exe to it. + NvAPI_UnicodeString profile_name = {}; + fill_nvapi_string(profile_name, sunshine_game_profile_name); + status = NvAPI_DRS_FindProfileByName(session_handle, profile_name, &profile_handle); + if (status != NVAPI_OK) { + NVDRS_PROFILE profile = {}; + profile.version = NVDRS_PROFILE_VER1; + fill_nvapi_string(profile.profileName, sunshine_game_profile_name); + status = NvAPI_DRS_CreateProfile(session_handle, &profile, &profile_handle); + if (status != NVAPI_OK) { + nvapi_error_message(status); + error_message("NvAPI_DRS_CreateProfile() SunshineStreamGame failed"); + return false; + } + profile_was_created = true; + } + profile_name_used = sunshine_game_profile_name; + + NVDRS_APPLICATION application = {}; + application.version = NVDRS_APPLICATION_VER_V1; + status = NvAPI_DRS_GetApplicationInfo(session_handle, profile_handle, app_name, &application); + if (status != NVAPI_OK) { + application = {}; + application.version = NVDRS_APPLICATION_VER_V1; + application.isPredefined = 0; + fill_nvapi_string(application.appName, exe_name.c_str()); + fill_nvapi_string(application.userFriendlyName, exe_name.c_str()); + fill_nvapi_string(application.launcher, L""); + + status = NvAPI_DRS_CreateApplication(session_handle, profile_handle, &application); + if (status != NVAPI_OK) { + nvapi_error_message(status); + error_message(std::wstring(L"NvAPI_DRS_CreateApplication() failed for ") + exe_name); + return false; + } + application_was_added = true; + } + } + + undo_data_t::data_t::game_profile_t pending; + pending.profile_name = platf::to_utf8(profile_name_used); + pending.exe_path = platf::to_utf8(exe_name); + pending.profile_was_created = profile_was_created; + pending.application_was_added = application_was_added; + + if (!apply_desired_to_profile(session_handle, profile_handle, desired, pending.vsync, pending.frl, pending.pstate, pending.prerender)) { + // A NvAPI write failed mid-way; populate undo_out so the caller can still + // attempt to roll back what we did manage to set. + undo_out = pending; + return false; + } + + undo_out = pending; + info_message(std::wstring(L"Applied stream optimizations to game profile for ") + exe_name); + return true; + } + + bool + driver_settings_t::restore_game_profile_to_undo(const undo_data_t::data_t::game_profile_t &undo_data) { + if (!session_handle) return false; + + NvAPI_Status status; + + // Locate the profile that should own this exe right now. Prefer + // FindApplicationByName because the user may have moved the application + // between profiles since we wrote the settings. + const std::wstring exe_name = platf::from_utf8(undo_data.exe_path); + if (exe_name.empty()) { + info_message("game_profile undo entry missing exe_path, skipping"); + return true; + } + + NvAPI_UnicodeString app_name = {}; + fill_nvapi_string(app_name, exe_name.c_str()); + + NvDRSProfileHandle profile_handle = 0; + NVDRS_APPLICATION app_info = {}; + app_info.version = NVDRS_APPLICATION_VER; + status = NvAPI_DRS_FindApplicationByName(session_handle, app_name, &profile_handle, &app_info); + if (status != NVAPI_OK) { + // Fall back to the named profile we (might have) created. + const std::wstring saved_name = platf::from_utf8(undo_data.profile_name); + NvAPI_UnicodeString profile_name = {}; + fill_nvapi_string(profile_name, saved_name.c_str()); + status = NvAPI_DRS_FindProfileByName(session_handle, profile_name, &profile_handle); + if (status != NVAPI_OK) { + info_message(std::wstring(L"No profile found for ") + exe_name + L" during restore, skipping"); + return true; + } + } + + // Best-effort restore: keep going even if individual setting writes fail, + // so that subsequent settings + profile/application cleanup still run. + bool ok = true; + if (undo_data.vsync && !restore_uint_setting(session_handle, profile_handle, VSYNCMODE_ID, *undo_data.vsync, L"VSYNCMODE")) { + ok = false; + } + if (undo_data.frl && !restore_uint_setting(session_handle, profile_handle, FRL_FPS_ID, *undo_data.frl, L"FRL_FPS")) { + ok = false; + } + if (undo_data.pstate && !restore_uint_setting(session_handle, profile_handle, PREFERRED_PSTATE_ID, *undo_data.pstate, L"PREFERRED_PSTATE")) { + ok = false; + } + if (undo_data.prerender && !restore_uint_setting(session_handle, profile_handle, PRERENDERLIMIT_ID, *undo_data.prerender, L"PRERENDERLIMIT")) { + ok = false; + } + + if (undo_data.application_was_added) { + status = NvAPI_DRS_DeleteApplication(session_handle, profile_handle, app_name); + if (status != NVAPI_OK && status != NVAPI_EXECUTABLE_NOT_FOUND) { + nvapi_error_message(status); + error_message(std::wstring(L"NvAPI_DRS_DeleteApplication() failed for ") + exe_name); + // Non-fatal: the user can clean up manually if it ever happens. + } + } + + if (undo_data.profile_was_created) { + // Only delete the profile we created if it has no other applications attached. + NVDRS_PROFILE info = {}; + info.version = NVDRS_PROFILE_VER; + if (NvAPI_DRS_GetProfileInfo(session_handle, profile_handle, &info) == NVAPI_OK && info.numOfApps == 0) { + status = NvAPI_DRS_DeleteProfile(session_handle, profile_handle); + if (status != NVAPI_OK) { + nvapi_error_message(status); + error_message("NvAPI_DRS_DeleteProfile() SunshineStreamGame failed"); + // Non-fatal. + } + } + } + + return ok; + } + + bool + driver_settings_t::check_and_modify_base_extras(int client_fps, std::optional &undo_out) { + undo_out.reset(); + if (!session_handle) return false; + + const auto opts = get_nvprefs_options(); + if (!opts.nv_optimize_game || !opts.nv_apply_to_base_profile) { + return true; + } + + const desired_settings_t desired = compute_desired(opts, client_fps); + if (!desired.vsync && !desired.frl && !desired.pstate && !desired.prerender) { + return true; + } + + NvDRSProfileHandle profile_handle = 0; + NvAPI_Status status = NvAPI_DRS_GetBaseProfile(session_handle, &profile_handle); + if (status != NVAPI_OK) { + nvapi_error_message(status); + error_message("NvAPI_DRS_GetBaseProfile() failed for base extras"); + return false; + } + + undo_data_t::data_t::base_extras_t pending; + if (!apply_desired_to_profile(session_handle, profile_handle, desired, pending.vsync, pending.frl, pending.pstate, pending.prerender)) { + undo_out = pending; + return false; + } + undo_out = pending; + info_message("Applied stream optimizations to BASE driver profile"); + return true; + } + + bool + driver_settings_t::restore_base_extras_to_undo(const undo_data_t::data_t::base_extras_t &undo_data) { + if (!session_handle) return false; + + NvDRSProfileHandle profile_handle = 0; + NvAPI_Status status = NvAPI_DRS_GetBaseProfile(session_handle, &profile_handle); + if (status != NVAPI_OK) { + nvapi_error_message(status); + error_message("NvAPI_DRS_GetBaseProfile() failed for base extras restore"); + return false; + } + + bool ok = true; + if (undo_data.vsync && !restore_uint_setting(session_handle, profile_handle, VSYNCMODE_ID, *undo_data.vsync, L"VSYNCMODE (base)")) { + ok = false; + } + if (undo_data.frl && !restore_uint_setting(session_handle, profile_handle, FRL_FPS_ID, *undo_data.frl, L"FRL_FPS (base)")) { + ok = false; + } + if (undo_data.pstate && !restore_uint_setting(session_handle, profile_handle, PREFERRED_PSTATE_ID, *undo_data.pstate, L"PREFERRED_PSTATE (base)")) { + ok = false; + } + if (undo_data.prerender && !restore_uint_setting(session_handle, profile_handle, PRERENDERLIMIT_ID, *undo_data.prerender, L"PRERENDERLIMIT (base)")) { + ok = false; + } + return ok; + } + } // namespace nvprefs diff --git a/src/platform/windows/nvprefs/driver_settings.h b/src/platform/windows/nvprefs/driver_settings.h index cf16c4d897b..0d341266ea1 100644 --- a/src/platform/windows/nvprefs/driver_settings.h +++ b/src/platform/windows/nvprefs/driver_settings.h @@ -61,6 +61,49 @@ namespace nvprefs { bool check_and_modify_application_profile(bool &modified); + /** + * @brief Apply per-game stream optimizations (force VSync, FRL, max-perf, + * low-latency) to the NVIDIA application profile that owns the given + * executable. If no profile owns it, a SunshineStream-Game profile is + * created on demand. + * @param exe_name Lower-cased basename of the running game executable + * (NvAPI matches applications by basename). + * @param client_fps Target frame rate reported by the streaming client; used + * to derive the FRL value when nv_lock_frame_rate is on. + * @param undo_out Out: filled with the data needed to restore the profile. + * Already-existing data is overwritten — caller must merge + * with any prior session's data. + * @return true on success (including the no-op case). + */ + bool + check_and_modify_game_profile(const std::wstring &exe_name, int client_fps, std::optional &undo_out); + + /** + * @brief Reverse a previous check_and_modify_game_profile() using the saved + * undo data: restore each touched setting to its original value (or + * delete it if it didn't exist), remove the application from the + * profile if we added it, and delete the profile if we created it + * and it has no other applications. + */ + bool + restore_game_profile_to_undo(const undo_data_t::data_t::game_profile_t &undo_data); + + /** + * @brief Apply the same stream optimizations to the BASE (global) driver + * profile, so apps Sunshine cannot detect also benefit. Restored on + * stream stop. + * @param client_fps See check_and_modify_game_profile. + */ + bool + check_and_modify_base_extras(int client_fps, std::optional &undo_out); + + /** + * @brief Reverse a previous check_and_modify_base_extras() using the saved + * undo data. + */ + bool + restore_base_extras_to_undo(const undo_data_t::data_t::base_extras_t &undo_data); + private: NvDRSSessionHandle session_handle = 0; }; diff --git a/src/platform/windows/nvprefs/nvapi_opensource_wrapper.cpp b/src/platform/windows/nvprefs/nvapi_opensource_wrapper.cpp index 4227882c48e..3d262754928 100644 --- a/src/platform/windows/nvprefs/nvapi_opensource_wrapper.cpp +++ b/src/platform/windows/nvprefs/nvapi_opensource_wrapper.cpp @@ -135,3 +135,23 @@ NVAPI_INTERFACE NvAPI_DRS_GetBaseProfile(NvDRSSessionHandle hSession, NvDRSProfileHandle *phProfile) { return call_interface("NvAPI_DRS_GetBaseProfile", hSession, phProfile); } + +NVAPI_INTERFACE +NvAPI_DRS_FindApplicationByName(NvDRSSessionHandle hSession, NvAPI_UnicodeString appName, NvDRSProfileHandle *phProfile, NVDRS_APPLICATION *pApplication) { + return call_interface("NvAPI_DRS_FindApplicationByName", hSession, appName, phProfile, pApplication); +} + +NVAPI_INTERFACE +NvAPI_DRS_GetProfileInfo(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NVDRS_PROFILE *pProfileInfo) { + return call_interface("NvAPI_DRS_GetProfileInfo", hSession, hProfile, pProfileInfo); +} + +NVAPI_INTERFACE +NvAPI_DRS_DeleteProfile(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile) { + return call_interface("NvAPI_DRS_DeleteProfile", hSession, hProfile); +} + +NVAPI_INTERFACE +NvAPI_DRS_DeleteApplication(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NvAPI_UnicodeString appName) { + return call_interface("NvAPI_DRS_DeleteApplication", hSession, hProfile, appName); +} diff --git a/src/platform/windows/nvprefs/nvprefs_common.cpp b/src/platform/windows/nvprefs/nvprefs_common.cpp index 902ff81ae65..6c2c649f755 100644 --- a/src/platform/windows/nvprefs/nvprefs_common.cpp +++ b/src/platform/windows/nvprefs/nvprefs_common.cpp @@ -36,6 +36,14 @@ namespace nvprefs { nvprefs_options options; options.opengl_vulkan_on_dxgi = config::video.nv_opengl_vulkan_on_dxgi; options.sunshine_high_power_mode = config::video.nv_sunshine_high_power_mode; + options.nv_optimize_game = config::video.nv_optimize_game; + options.nv_force_vsync = config::video.nv_force_vsync; + options.nv_lock_frame_rate = config::video.nv_lock_frame_rate; + options.nv_frl_fps_offset = config::video.nv_frl_fps_offset; + options.nv_frl_fps_override = config::video.nv_frl_fps_override; + options.nv_prefer_max_performance = config::video.nv_prefer_max_performance; + options.nv_low_latency_mode = config::video.nv_low_latency_mode; + options.nv_apply_to_base_profile = config::video.nv_apply_to_base_profile; return options; } diff --git a/src/platform/windows/nvprefs/nvprefs_common.h b/src/platform/windows/nvprefs/nvprefs_common.h index aa6c00fa3b3..d70748ea4f7 100644 --- a/src/platform/windows/nvprefs/nvprefs_common.h +++ b/src/platform/windows/nvprefs/nvprefs_common.h @@ -52,6 +52,23 @@ namespace nvprefs { struct nvprefs_options { bool opengl_vulkan_on_dxgi = true; bool sunshine_high_power_mode = true; + + // Stream-time game optimizations applied to the game's NVIDIA application + // profile. All restored when the stream stops or via the undo file on the + // next launch after a crash. + bool nv_optimize_game = false; // master switch for the per-game block + bool nv_force_vsync = true; // VSYNCMODE -> VSYNCMODE_FORCEON + bool nv_lock_frame_rate = true; // FRL_FPS -> client_fps + frl_offset (clamped >= 1) + int nv_frl_fps_offset = -2; // delta added to client fps to derive FRL target + int nv_frl_fps_override = 0; // if > 0, use this fps directly instead of client_fps + offset + bool nv_prefer_max_performance = false; // PREFERRED_PSTATE -> PREFERRED_PSTATE_PREFER_MAX + bool nv_low_latency_mode = false; // PRERENDERLIMIT -> 1 (matches NVIDIA "Low Latency Mode = On") + + // When true the same set of optimizations is also written to the BASE + // (global) driver profile, so games launched outside Sunshine — or + // sub-processes Sunshine cannot detect — also get the treatment until the + // stream stops. + bool nv_apply_to_base_profile = false; }; nvprefs_options diff --git a/src/platform/windows/nvprefs/nvprefs_interface.cpp b/src/platform/windows/nvprefs/nvprefs_interface.cpp index ad366cd548c..6a75f4fe84c 100644 --- a/src/platform/windows/nvprefs/nvprefs_interface.cpp +++ b/src/platform/windows/nvprefs/nvprefs_interface.cpp @@ -34,6 +34,9 @@ namespace nvprefs { nvprefs_interface::~nvprefs_interface() { if (owning_undo_file() && load()) { + // Roll back any leftover stream-time entries first so the undo file ends + // up empty before restore_global_profile() decides whether to delete it. + restore_stream_optimizations(); restore_global_profile(); } unload(); @@ -80,11 +83,18 @@ namespace nvprefs { // Try to restore from the undo file info_message("Opened undo file from previous improper termination"); if (auto undo_data = undo_file->read_undo_data()) { - if (pimpl->driver_settings.restore_global_profile_to_undo(*undo_data) && pimpl->driver_settings.save_settings()) { - info_message("Restored global profile settings from undo file - deleting the file"); + bool ok = pimpl->driver_settings.restore_global_profile_to_undo(*undo_data); + if (auto g = undo_data->get_game_profile()) { + ok = pimpl->driver_settings.restore_game_profile_to_undo(*g) && ok; + } + if (auto b = undo_data->get_base_extras()) { + ok = pimpl->driver_settings.restore_base_extras_to_undo(*b) && ok; + } + if (ok && pimpl->driver_settings.save_settings()) { + info_message("Restored driver settings from undo file - deleting the file"); } else { - error_message("Failed to restore global profile settings from undo file, deleting the file anyway"); + error_message("Failed to fully restore driver settings from undo file, deleting the file anyway"); } } else { @@ -210,13 +220,24 @@ namespace nvprefs { // Restore global profile settings with undo data if (pimpl->driver_settings.restore_global_profile_to_undo(*pimpl->undo_data) && pimpl->driver_settings.save_settings()) { - // Global profile settings sucessfully restored, can delete undo file - if (!pimpl->undo_file->delete_file()) { - error_message("Couldn't delete undo file"); - return false; + // Only nuke the undo file when there are no other branches still + // waiting to be restored (e.g. stream-time entries written by + // apply_stream_optimizations()). + if (!pimpl->undo_data->get_game_profile() && !pimpl->undo_data->get_base_extras()) { + if (!pimpl->undo_file->delete_file()) { + error_message("Couldn't delete undo file"); + return false; + } + pimpl->undo_data = std::nullopt; + pimpl->undo_file = std::nullopt; + } + else { + // Persist the trimmed manifest so a later run won't replay the + // already-restored global-profile entry. + if (!pimpl->undo_file->write_undo_data(*pimpl->undo_data)) { + error_message("Couldn't update undo file after restoring global profile"); + } } - pimpl->undo_data = std::nullopt; - pimpl->undo_file = std::nullopt; } else { error_message("Couldn't restore global profile settings"); @@ -226,4 +247,129 @@ namespace nvprefs { return true; } + // Helper used by both apply_stream_optimizations() and modify_global_profile() + // to commit a freshly merged undo_data to disk + driver. Caller is responsible + // for already populating pimpl->undo_data. + static bool + ensure_undo_dir_and_file(const std::filesystem::path &dir, const std::filesystem::path &file, std::optional &out) { + if (out) return true; + if (!CreateDirectoryW(dir.c_str(), nullptr) && GetLastError() != ERROR_ALREADY_EXISTS) { + error_message("Couldn't create undo folder"); + return false; + } + out = undo_file_t::create_new_file(file); + if (!out) { + error_message("Couldn't create undo file"); + return false; + } + return true; + } + + bool + nvprefs_interface::apply_stream_optimizations(const std::wstring &exe_name, int client_fps) { + if (!pimpl->loaded) return false; + + std::optional game_undo; + std::optional base_undo; + + const bool ok_game = pimpl->driver_settings.check_and_modify_game_profile(exe_name, client_fps, game_undo); + if (!ok_game) { + error_message("Couldn't fully apply game-profile optimizations"); + } + const bool ok_base = pimpl->driver_settings.check_and_modify_base_extras(client_fps, base_undo); + if (!ok_base) { + error_message("Couldn't fully apply base-profile optimizations"); + } + + if (!game_undo && !base_undo) { + // Either feature was disabled or no setting needed changing — nothing to persist. + return ok_game && ok_base; + } + + // Merge new undo data with anything already on file. + undo_data_t fresh; + if (game_undo) fresh.set_game_profile(*game_undo); + if (base_undo) fresh.set_base_extras(*base_undo); + + if (!pimpl->undo_data) { + pimpl->undo_data = undo_data_t {}; + } + pimpl->undo_data->merge(fresh); + + if (!ensure_undo_dir_and_file(pimpl->undo_folder_path, pimpl->undo_file_path, pimpl->undo_file)) { + pimpl->driver_settings.load_settings(); + return false; + } + if (!pimpl->undo_file->write_undo_data(*pimpl->undo_data)) { + error_message("Couldn't write stream-optimization undo data"); + pimpl->driver_settings.load_settings(); + return false; + } + if (!pimpl->driver_settings.save_settings()) { + error_message("Couldn't save driver settings after stream optimizations"); + return false; + } + + return ok_game && ok_base; + } + + bool + nvprefs_interface::restore_stream_optimizations() { + if (!pimpl->loaded || !pimpl->undo_data) return true; + + bool ok = true; + bool game_restored = false; + bool base_restored = false; + if (auto g = pimpl->undo_data->get_game_profile()) { + if (pimpl->driver_settings.restore_game_profile_to_undo(*g)) { + game_restored = true; + } + else { + ok = false; + } + } + if (auto b = pimpl->undo_data->get_base_extras()) { + if (pimpl->driver_settings.restore_base_extras_to_undo(*b)) { + base_restored = true; + } + else { + ok = false; + } + } + + if (!pimpl->driver_settings.save_settings()) { + // Don't trim the undo manifest — a later run (or crash recovery) needs + // to be able to replay the restore against the actual driver state. + error_message("Couldn't save driver settings after restoring stream optimizations"); + return false; + } + + // Only clear branches that we actually restored AND persisted, so a + // partial failure leaves the manifest intact for the next attempt. + if (game_restored) { + pimpl->undo_data->clear_game_profile(); + } + if (base_restored) { + pimpl->undo_data->clear_base_extras(); + } + + // Drop the undo file when nothing else still needs to be remembered. + if (pimpl->undo_file && !pimpl->undo_data->get_opengl_swapchain() && !pimpl->undo_data->get_game_profile() && !pimpl->undo_data->get_base_extras()) { + if (!pimpl->undo_file->delete_file()) { + error_message("Couldn't delete now-empty undo file"); + } + pimpl->undo_data.reset(); + pimpl->undo_file.reset(); + } + else if (pimpl->undo_file && (game_restored || base_restored)) { + // Persist the trimmed undo manifest so a later crash doesn't replay + // already-restored settings. + if (!pimpl->undo_file->write_undo_data(*pimpl->undo_data)) { + error_message("Couldn't update undo file after restoring stream optimizations"); + } + } + + return ok; + } + } // namespace nvprefs diff --git a/src/platform/windows/nvprefs/nvprefs_interface.h b/src/platform/windows/nvprefs/nvprefs_interface.h index 73235877db9..1e5e1f935a3 100644 --- a/src/platform/windows/nvprefs/nvprefs_interface.h +++ b/src/platform/windows/nvprefs/nvprefs_interface.h @@ -6,6 +6,7 @@ // standard library headers #include +#include namespace nvprefs { @@ -35,6 +36,26 @@ namespace nvprefs { bool restore_global_profile(); + /** + * @brief Apply per-stream NVIDIA driver optimizations: per-game profile + * (always when feature is on) and BASE profile (when the user + * opted into nv_apply_to_base_profile). Updates and persists the + * undo manifest so a Sunshine crash mid-stream still allows the + * next launch to roll the changes back. + * @param exe_name Lower-cased basename of the running game executable. + * Empty string skips the per-game profile leg silently. + * @param client_fps Client refresh rate, used to derive FRL target. + */ + bool + apply_stream_optimizations(const std::wstring &exe_name, int client_fps); + + /** + * @brief Reverse a previous apply_stream_optimizations(). Safe to call + * even if no optimizations were applied. + */ + bool + restore_stream_optimizations(); + private: struct impl; std::unique_ptr pimpl; diff --git a/src/platform/windows/nvprefs/undo_data.cpp b/src/platform/windows/nvprefs/undo_data.cpp index e75b92b880b..7252a22ea51 100644 --- a/src/platform/windows/nvprefs/undo_data.cpp +++ b/src/platform/windows/nvprefs/undo_data.cpp @@ -16,6 +16,9 @@ using json = nlohmann::json; namespace nlohmann { using data_t = nvprefs::undo_data_t::data_t; using opengl_swapchain_t = data_t::opengl_swapchain_t; + using setting_undo_t = data_t::setting_undo_t; + using game_profile_t = data_t::game_profile_t; + using base_extras_t = data_t::base_extras_t; template struct adl_serializer> { @@ -44,12 +47,23 @@ namespace nlohmann { struct adl_serializer { static void to_json(json &j, const data_t &data) { - j = json { { "opengl_swapchain", data.opengl_swapchain } }; + j = json { + { "opengl_swapchain", data.opengl_swapchain }, + { "game_profile", data.game_profile }, + { "base_extras", data.base_extras }, + }; } static void from_json(const json &j, data_t &data) { j.at("opengl_swapchain").get_to(data.opengl_swapchain); + // game_profile / base_extras are new fields, missing in legacy undo files. + if (j.contains("game_profile")) { + j.at("game_profile").get_to(data.game_profile); + } + if (j.contains("base_extras")) { + j.at("base_extras").get_to(data.base_extras); + } } }; @@ -69,6 +83,73 @@ namespace nlohmann { j.at("undo_value").get_to(opengl_swapchain.undo_value); } }; + + template <> + struct adl_serializer { + static void + to_json(json &j, const setting_undo_t &s) { + j = json { + { "our_value", s.our_value }, + { "undo_value", s.undo_value } + }; + } + + static void + from_json(const json &j, setting_undo_t &s) { + j.at("our_value").get_to(s.our_value); + j.at("undo_value").get_to(s.undo_value); + } + }; + + template <> + struct adl_serializer { + static void + to_json(json &j, const game_profile_t &g) { + j = json { + { "profile_name", g.profile_name }, + { "exe_path", g.exe_path }, + { "profile_was_created", g.profile_was_created }, + { "application_was_added", g.application_was_added }, + { "vsync", g.vsync }, + { "frl", g.frl }, + { "pstate", g.pstate }, + { "prerender", g.prerender }, + }; + } + + static void + from_json(const json &j, game_profile_t &g) { + j.at("profile_name").get_to(g.profile_name); + j.at("exe_path").get_to(g.exe_path); + j.at("profile_was_created").get_to(g.profile_was_created); + j.at("application_was_added").get_to(g.application_was_added); + if (j.contains("vsync")) j.at("vsync").get_to(g.vsync); + if (j.contains("frl")) j.at("frl").get_to(g.frl); + if (j.contains("pstate")) j.at("pstate").get_to(g.pstate); + if (j.contains("prerender")) j.at("prerender").get_to(g.prerender); + } + }; + + template <> + struct adl_serializer { + static void + to_json(json &j, const base_extras_t &b) { + j = json { + { "vsync", b.vsync }, + { "frl", b.frl }, + { "pstate", b.pstate }, + { "prerender", b.prerender }, + }; + } + + static void + from_json(const json &j, base_extras_t &b) { + if (j.contains("vsync")) j.at("vsync").get_to(b.vsync); + if (j.contains("frl")) j.at("frl").get_to(b.frl); + if (j.contains("pstate")) j.at("pstate").get_to(b.pstate); + if (j.contains("prerender")) j.at("prerender").get_to(b.prerender); + } + }; } // namespace nlohmann namespace nvprefs { @@ -86,6 +167,36 @@ namespace nvprefs { return data.opengl_swapchain; } + void + undo_data_t::set_game_profile(const data_t::game_profile_t &game_profile) { + data.game_profile = game_profile; + } + + std::optional + undo_data_t::get_game_profile() const { + return data.game_profile; + } + + void + undo_data_t::clear_game_profile() { + data.game_profile = std::nullopt; + } + + void + undo_data_t::set_base_extras(const data_t::base_extras_t &base_extras) { + data.base_extras = base_extras; + } + + std::optional + undo_data_t::get_base_extras() const { + return data.base_extras; + } + + void + undo_data_t::clear_base_extras() { + data.base_extras = std::nullopt; + } + std::string undo_data_t::write() const { try { @@ -117,6 +228,14 @@ namespace nvprefs { if (swapchain_data) { set_opengl_swapchain(swapchain_data->our_value, swapchain_data->undo_value); } + const auto &game = newer_data.get_game_profile(); + if (game) { + set_game_profile(*game); + } + const auto &base = newer_data.get_base_extras(); + if (base) { + set_base_extras(*base); + } } } // namespace nvprefs diff --git a/src/platform/windows/nvprefs/undo_data.h b/src/platform/windows/nvprefs/undo_data.h index 5bd10ad9e9b..f3368d5ab25 100644 --- a/src/platform/windows/nvprefs/undo_data.h +++ b/src/platform/windows/nvprefs/undo_data.h @@ -20,7 +20,45 @@ namespace nvprefs { std::optional undo_value; }; + // A single driver setting we touched: the value we wrote ("our_value") + // and the value the user originally had ("undo_value", nullopt means the + // setting was previously unset on this profile, so restore = delete). + struct setting_undo_t { + uint32_t our_value; + std::optional undo_value; + }; + + // Stream optimizations applied to a per-game application profile. + // - profile_name: the SunshineStream-* profile we created or reused + // - exe_path: the lower-cased executable basename added to the profile + // - profile_was_created: true if we created profile_name (delete on restore if no other apps) + // - application_was_added: true if we added exe_path to profile_name (remove on restore) + // - vsync/frl/pstate/prerender: the four settings we may have written + struct game_profile_t { + std::string profile_name; + std::string exe_path; + bool profile_was_created = false; + bool application_was_added = false; + std::optional vsync; + std::optional frl; + std::optional pstate; + std::optional prerender; + }; + + // Stream optimizations applied to the BASE (global) driver profile. + // Mirrors the four settings we may write at the global level. Restore + // semantics match game_profile_t: nullopt = not touched, undo_value + // nullopt = setting did not exist before us. + struct base_extras_t { + std::optional vsync; + std::optional frl; + std::optional pstate; + std::optional prerender; + }; + std::optional opengl_swapchain; + std::optional game_profile; + std::optional base_extras; }; void @@ -29,6 +67,24 @@ namespace nvprefs { std::optional get_opengl_swapchain() const; + void + set_game_profile(const data_t::game_profile_t &game_profile); + + std::optional + get_game_profile() const; + + void + clear_game_profile(); + + void + set_base_extras(const data_t::base_extras_t &base_extras); + + std::optional + get_base_extras() const; + + void + clear_base_extras(); + std::string write() const; diff --git a/src/stream.cpp b/src/stream.cpp index ca87d4e0c67..33986788296 100644 --- a/src/stream.cpp +++ b/src/stream.cpp @@ -13,6 +13,7 @@ #include #include + #include #include #include @@ -1455,7 +1456,24 @@ namespace stream { return; } - const int param_type = *reinterpret_cast(payload.data()); + // Wire layout is packed little-endian native-int values; the payload + // buffer is not guaranteed to be 4-byte aligned, so use memcpy to avoid + // unaligned reads (UB on ARM/PowerPC, OK on x86 but still UB per spec). + auto read_le_u32 = [](const char *src) { + std::uint32_t raw = 0; + std::memcpy(&raw, src, sizeof(raw)); + return boost::endian::little_to_native(raw); + }; + auto read_le_f32 = [](const char *src) { + std::uint32_t raw = 0; + std::memcpy(&raw, src, sizeof(raw)); + raw = boost::endian::little_to_native(raw); + float out; + std::memcpy(&out, &raw, sizeof(out)); + return out; + }; + + const int param_type = static_cast(read_le_u32(payload.data())); if (param_type < 0 || param_type >= static_cast(video::dynamic_param_type_e::MAX_PARAM_TYPE)) { BOOST_LOG(warning) << "Invalid parameter type: " << param_type; @@ -1473,8 +1491,9 @@ namespace stream { return; } - const auto *resolution_data = reinterpret_cast(payload.data()); - handle_resolution_change(session, resolution_data[1], resolution_data[2]); + const int width = static_cast(read_le_u32(payload.data() + sizeof(std::uint32_t))); + const int height = static_cast(read_le_u32(payload.data() + sizeof(std::uint32_t) * 2)); + handle_resolution_change(session, width, height); return; } @@ -1487,7 +1506,7 @@ namespace stream { return; } - const float new_fps = *reinterpret_cast(payload.data() + sizeof(int)); + const float new_fps = read_le_f32(payload.data() + sizeof(std::uint32_t)); if (new_fps <= 0.0f || new_fps > 1000.0f) { BOOST_LOG(warning) << "Invalid FPS value: " << new_fps; @@ -1514,7 +1533,7 @@ namespace stream { return; } - const int param_value = reinterpret_cast(payload.data())[1]; + const int param_value = static_cast(read_le_u32(payload.data() + sizeof(std::uint32_t))); video::dynamic_param_t param; param.type = param_type_enum; @@ -3026,6 +3045,7 @@ namespace stream { display_device::session_t::get().restore_state(); } + platf::restore_stream_optimizations(); platf::streaming_will_stop(); } else { @@ -3123,6 +3143,15 @@ namespace stream { } platf::streaming_will_start(); + // Apply per-stream GPU driver optimizations for the launched game + // (NVIDIA application/BASE profile on Windows; no-op elsewhere). + { + std::string game_cmd; + if (auto app_id = proc::proc.running()) { + game_cmd = proc::proc.get_app_cmd(app_id); + } + platf::apply_stream_optimizations(game_cmd, session.config.monitor.framerate); + } #if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 system_tray::update_tray_playing(proc::proc.get_last_run_app_name()); #endif diff --git a/src_assets/common/assets/web/composables/useConfig.js b/src_assets/common/assets/web/composables/useConfig.js index 3bbf5a302fd..87b97122d16 100644 --- a/src_assets/common/assets/web/composables/useConfig.js +++ b/src_assets/common/assets/web/composables/useConfig.js @@ -142,6 +142,15 @@ const DEFAULT_TABS = [ nvenc_latency_over_power: 'enabled', nvenc_opengl_vulkan_on_dxgi: 'enabled', nvenc_h264_cavlc: 'disabled', + // Stream-time NVIDIA Control Panel auto-optimizations. + nvenc_optimize_game: 'disabled', + nvenc_force_vsync: 'enabled', + nvenc_lock_frame_rate: 'enabled', + nvenc_frl_fps_offset: -2, + nvenc_frl_fps_override: 0, + nvenc_prefer_max_performance: 'disabled', + nvenc_low_latency_mode: 'disabled', + nvenc_apply_to_base_profile: 'disabled', }, }, { diff --git a/src_assets/common/assets/web/configs/tabs/encoders/NvidiaNvencEncoder.vue b/src_assets/common/assets/web/configs/tabs/encoders/NvidiaNvencEncoder.vue index 3b1f7292257..4c28ca63404 100644 --- a/src_assets/common/assets/web/configs/tabs/encoders/NvidiaNvencEncoder.vue +++ b/src_assets/common/assets/web/configs/tabs/encoders/NvidiaNvencEncoder.vue @@ -192,6 +192,98 @@ const config = ref(props.config) + + +
+

+ +

+
+
+ + +
+ + +
{{ $t('config.nvenc_optimize_game_desc') }}
+
+ + + +
+
+
+ diff --git a/src_assets/common/assets/web/public/assets/locale/en.json b/src_assets/common/assets/web/public/assets/locale/en.json index e1af5fcdbe2..fc090f3eff6 100644 --- a/src_assets/common/assets/web/public/assets/locale/en.json +++ b/src_assets/common/assets/web/public/assets/locale/en.json @@ -381,6 +381,23 @@ "nvenc_lookahead_level_disabled": "Disabled (same as level 0)", "nvenc_opengl_vulkan_on_dxgi": "Present OpenGL/Vulkan on top of DXGI", "nvenc_opengl_vulkan_on_dxgi_desc": "Sunshine can't capture fullscreen OpenGL and Vulkan programs at full frame rate unless they present on top of DXGI. This is system-wide setting that is reverted on sunshine program exit.", + "nvenc_optimize_game_section": "Stream-time NVIDIA Control Panel auto-optimizations", + "nvenc_optimize_game": "Auto-tune NVIDIA Control Panel during stream", + "nvenc_optimize_game_desc": "When streaming starts, write a per-game profile (and optionally the BASE profile) into NVIDIA Control Panel that forces V-Sync ON and caps the in-game frame rate slightly below the client refresh. This stops Frame Generation / unlocked-FPS games from desynchronising the V-Blank window NVENC captures, eliminating tearing and missed frames. All changes are rolled back when the stream ends or on the next launch after a crash.", + "nvenc_force_vsync": "Force V-Sync = On", + "nvenc_force_vsync_desc": "Set the driver V-Sync mode to ForceOn for the game's application profile. This is the single most important setting for Frame Generation games — it overrides the in-game V-Sync (which Frame Generation disables) and forces all generated frames back into the V-Blank window so NVENC can capture them.", + "nvenc_lock_frame_rate": "Limit game frame rate (FRL)", + "nvenc_lock_frame_rate_desc": "Use the driver's frame rate limiter to cap the game slightly below the client refresh rate. Keeping in-game FPS strictly below the streaming display's refresh rate avoids the high PCL latency that occurs at exactly = refresh, and prevents stutter from going above it.", + "nvenc_frl_fps_offset": "FRL offset (frames below client refresh)", + "nvenc_frl_fps_offset_desc": "Negative values cap the game below the client refresh rate (recommended -2). Positive values are allowed but rarely useful. Ignored when 'FRL override' is non-zero.", + "nvenc_frl_fps_override": "FRL absolute override (0 = use offset)", + "nvenc_frl_fps_override_desc": "If non-zero, the driver frame-rate limiter is locked to this exact value regardless of the client refresh. Set to 0 to derive the cap from the client refresh + offset above.", + "nvenc_prefer_max_performance": "Prefer Maximum Performance power state", + "nvenc_prefer_max_performance_desc": "Force the GPU power management mode to 'Prefer Maximum Performance' for the game profile, preventing P-state downclocks that introduce frame-time spikes during light scenes.", + "nvenc_low_latency_mode": "NVIDIA Low Latency Mode = On", + "nvenc_low_latency_mode_desc": "Set max pre-rendered frames to 1, equivalent to 'Low Latency Mode = On' in the NVIDIA Control Panel. Reduces input latency at the cost of slightly less consistent frame pacing in CPU-bound titles.", + "nvenc_apply_to_base_profile": "Also apply to BASE profile (machine-wide)", + "nvenc_apply_to_base_profile_desc": "In addition to the per-game profile, also write the same V-Sync / FRL / power / latency settings to the NVIDIA driver BASE profile. This catches games that don't have a dedicated EXE profile, but the changes affect the entire machine until the stream ends.", "nvenc_preset": "Performance preset", "nvenc_preset_1": "(fastest, default)", "nvenc_preset_7": "(slowest)", diff --git a/src_assets/common/assets/web/public/assets/locale/zh.json b/src_assets/common/assets/web/public/assets/locale/zh.json index 801394d0f12..674455216bb 100644 --- a/src_assets/common/assets/web/public/assets/locale/zh.json +++ b/src_assets/common/assets/web/public/assets/locale/zh.json @@ -381,6 +381,23 @@ "nvenc_lookahead_level_disabled": "禁用(等同于级别 0)", "nvenc_opengl_vulkan_on_dxgi": "在 DXGI 基础上呈现 OpenGL/Vulkan", "nvenc_opengl_vulkan_on_dxgi_desc": "Sunshine 无法以满帧率捕获不在 DXGI 顶部的全屏 OpenGL 和 Vulkan 程序。此为系统级设置,Sunshine 退出时会恢复。", + "nvenc_optimize_game_section": "串流时自动优化 NVIDIA 控制面板", + "nvenc_optimize_game": "串流时自动调优 NVIDIA 控制面板", + "nvenc_optimize_game_desc": "串流开始时,将 NVIDIA 控制面板的「游戏 EXE 配置文件」(可选同时写入 BASE 全局配置)改为强制开启垂直同步、并把游戏帧率锁到略低于客户端刷新率。这能让「帧生成 / 不锁帧」游戏的所有渲染帧重新落回 V-Blank 窗口,NVENC 才能正常捕获,从根本上消除画面撕裂和掉帧。串流结束或下次 Sunshine 启动检测到上次崩溃残留时,所有改动会自动回滚。", + "nvenc_force_vsync": "强制垂直同步 = 开", + "nvenc_force_vsync_desc": "把当前游戏的驱动 V-Sync 模式改为 ForceOn。这是帧生成游戏最关键的一项 —— 因为游戏内 V-Sync 在帧生成下会失效,只有驱动级强开才能把所有生成帧锁回 V-Blank 窗口供 NVENC 捕获。", + "nvenc_lock_frame_rate": "限制游戏帧率(FRL)", + "nvenc_lock_frame_rate_desc": "用驱动帧率限制器把游戏帧率锁到略低于客户端刷新率。等于刷新率会有很高的 PCL 延迟,高于刷新率串流画面会卡,所以推荐略低 1~2 帧。", + "nvenc_frl_fps_offset": "FRL 偏移(在客户端刷新率上的差值)", + "nvenc_frl_fps_offset_desc": "负数表示锁到比客户端刷新率低多少帧(推荐 -2)。当下方「FRL 绝对值覆盖」非 0 时本项被忽略。", + "nvenc_frl_fps_override": "FRL 绝对值覆盖(0 = 用偏移)", + "nvenc_frl_fps_override_desc": "非 0 时驱动帧率限制器会被锁定为这个固定值,不再随客户端刷新率变化。设为 0 表示按「客户端刷新率 + 偏移」自动计算。", + "nvenc_prefer_max_performance": "首选最高性能电源模式", + "nvenc_prefer_max_performance_desc": "把游戏配置文件的电源管理模式设为「首选最高性能」,避免 GPU 在轻负载场景降频导致的帧时间抖动。", + "nvenc_low_latency_mode": "NVIDIA 低延迟模式 = 开", + "nvenc_low_latency_mode_desc": "把最大预渲染帧数设为 1,等同于 NVIDIA 控制面板里的「低延迟模式 = 开」。能降低输入延迟,但 CPU 受限的游戏帧时间稳定性可能略下降。", + "nvenc_apply_to_base_profile": "同时写入 BASE 全局配置(机器级生效)", + "nvenc_apply_to_base_profile_desc": "在写入当前游戏配置文件的基础上,把同一套 V-Sync / FRL / 电源 / 延迟设置同时写入 NVIDIA 驱动 BASE 配置。可以兜住没有专属 EXE 配置文件的游戏,但在串流结束前这些改动会影响整台机器的所有 3D 程序。", "nvenc_preset": "性能预设", "nvenc_preset_1": "最快(默认)", "nvenc_preset_7": "最慢",