From ac4be8a79253c63284f6abf8d1d7a2b7f3c84958 Mon Sep 17 00:00:00 2001 From: qiin2333 <414382190@qq.com> Date: Wed, 6 May 2026 13:40:34 +0800 Subject: [PATCH 1/3] feat(nvprefs): per-game + BASE profile auto-tuning for NVENC streams Extend the nvprefs module so a Sunshine stream can apply NVIDIA driver profile overrides for the launched game and (optionally) the BASE profile, then roll them back when the stream ends. Background ---------- Frame-Generation games (and any title with unlocked FPS) disable the in-game V-Sync and let the GPU produce frames outside of the V-Blank window. NVENC's display capture only sees frames that land inside that window, so the streamed picture tears, drops frames, or exhibits very high PCL latency. The standard "fix" is manual: open NVIDIA Control Panel / NVApp, force V-Sync = On for the game EXE and lock the in-game FPS slightly below the streaming display's refresh rate via FRL. This change automates that procedure and ties it to the stream lifecycle so casual users get the optimal settings without touching NVCP, and the host machine is left untouched after the stream ends. What's added ------------ * undo_data: new typed entries `game_profile_t` and `base_extras_t` with nlohmann adl_serializer plumbing. Backward-compatible with pre-existing `nvprefs_undo.json` files (the new fields are optional in `from_json`). * nvprefs_common: 8 new options surfaced from `config::video.nv_*`: - nv_optimize_game (master switch) - nv_force_vsync (VSYNCMODE = ForceOn) - nv_lock_frame_rate (FRL_FPS = client_fps + offset) - nv_frl_fps_offset (default -2) - nv_frl_fps_override (absolute override, 0 = use offset) - nv_prefer_max_performance (PREFERRED_PSTATE = PreferMax) - nv_low_latency_mode (PRERENDERLIMIT = 1) - nv_apply_to_base_profile (also write the BASE profile) * driver_settings: four new methods that locate or create a shared `SunshineStreamGame` profile, attach the EXE to it, write the desired uint settings, and remember the pre-existing values for later restore. Each setting is idempotent: pre-write read-back skips the change (and the undo entry) when the driver already matches the desired value. Restore is also conservative: a setting is only rolled back when the current value still equals what we wrote, and the per-game profile is only deleted when we created it AND no application entries remain. * nvprefs_interface: two public entry points `apply_stream_optimizations(exe, fps)` and `restore_stream_optimizations()` that merge the new undo data into the existing `%ProgramData%/Sunshine/nvprefs_undo.json` manifest, so a Sunshine crash mid-stream still allows the next launch to roll back the changes via `restore_from_and_delete_undo_file_if_exists`. Lifecycle integration and Web UI exposure follow in the next commits. --- .../windows/nvprefs/driver_settings.cpp | 409 ++++++++++++++++++ .../windows/nvprefs/driver_settings.h | 43 ++ .../nvprefs/nvapi_opensource_wrapper.cpp | 20 + .../windows/nvprefs/nvprefs_common.cpp | 8 + src/platform/windows/nvprefs/nvprefs_common.h | 17 + .../windows/nvprefs/nvprefs_interface.cpp | 164 ++++++- .../windows/nvprefs/nvprefs_interface.h | 21 + src/platform/windows/nvprefs/undo_data.cpp | 121 +++++- src/platform/windows/nvprefs/undo_data.h | 56 +++ 9 files changed, 849 insertions(+), 10 deletions(-) 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; From fee8aafd048794ee84932de5856aa8c8574ef94d Mon Sep 17 00:00:00 2001 From: qiin2333 <414382190@qq.com> Date: Wed, 6 May 2026 13:40:57 +0800 Subject: [PATCH 2/3] feat(stream): apply NVIDIA driver optimizations on stream start Add a small platform abstraction so stream.cpp stays free of NV- specific code: platf::apply_stream_optimizations(game_cmd, client_fps); platf::restore_stream_optimizations(); * common.h declares the new pair. * linux/macos: no-op implementations. * windows: parses the launched game's command line into a lower-cased EXE basename, gates on `config::video.nv_optimize_game`, then drives the new nvprefs entry points behind a `load()/unload()` pair. * config: 8 new `video_t.nv_*` fields with parser entries (`nvenc_optimize_game`, `nvenc_force_vsync`, `nvenc_lock_frame_rate`, `nvenc_frl_fps_offset`, `nvenc_frl_fps_override`, `nvenc_prefer_max_performance`, `nvenc_low_latency_mode`, `nvenc_apply_to_base_profile`). Defaults: master switch off; when enabled, force V-Sync + FRL = client_fps - 2 are on, BASE profile write is off (per-game leg only, machine-safe). * stream.cpp wires the apply call into the first non-control-only session start (after `streaming_will_start`) and the restore call into the last session stop (before `streaming_will_stop`). --- src/config.cpp | 28 ++++++++++++++++ src/config.h | 14 ++++++++ src/platform/common.h | 22 +++++++++++++ src/platform/linux/misc.cpp | 10 ++++++ src/platform/macos/misc.mm | 10 ++++++ src/platform/windows/misc.cpp | 61 +++++++++++++++++++++++++++++++++++ src/stream.cpp | 39 +++++++++++++++++++--- 7 files changed, 179 insertions(+), 5 deletions(-) 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/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 From ef92775437def4122bd1733a269417716be19f39 Mon Sep 17 00:00:00 2001 From: qiin2333 <414382190@qq.com> Date: Wed, 6 May 2026 13:41:15 +0800 Subject: [PATCH 3/3] feat(web): expose NVIDIA stream-time auto-optimizations in NVENC tab Add a dedicated collapsible section "Stream-time NVIDIA Control Panel auto-optimizations" under the NVENC encoder configuration tab, visible on Windows only. The master switch hides the per-feature controls when disabled to keep the page clean. * useConfig.js: 8 new defaults aligned with config.cpp. * NvidiaNvencEncoder.vue: master switch + 7 dependent controls (force V-Sync, FRL on/off, FRL offset, FRL override, prefer max performance, low latency mode, apply to BASE profile). * en.json + zh.json: 18 i18n strings each, both validated as parseable JSON. --- .../assets/web/composables/useConfig.js | 9 ++ .../tabs/encoders/NvidiaNvencEncoder.vue | 92 +++++++++++++++++++ .../assets/web/public/assets/locale/en.json | 17 ++++ .../assets/web/public/assets/locale/zh.json | 17 ++++ 4 files changed, 135 insertions(+) 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": "最慢",