diff --git a/CMakeLists.txt b/CMakeLists.txt index d9574c8e..fee22c8f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -134,10 +134,12 @@ pkg_check_modules(CURL REQUIRED libcurl) if(WIN32) set(Boost_USE_STATIC_LIBS ON) # cmake-lint: disable=C0103 + # 1.82.0+ is required for boost::json::value::set_at_pointer() support + find_package(Boost 1.82.0 COMPONENTS locale log filesystem program_options json REQUIRED) +else() + find_package(Boost COMPONENTS locale log filesystem program_options REQUIRED) endif() -find_package(Boost COMPONENTS locale log filesystem program_options REQUIRED) - list(APPEND SUNSHINE_COMPILE_OPTIONS -Wall -Wno-sign-compare) # enable system tray, we will disable this later if we cannot find the required package config on linux @@ -155,6 +157,12 @@ if(WIN32) add_compile_definitions(SUNSHINE_PLATFORM="windows") add_subdirectory(tools) # This is temporary, only tools for Windows are needed, for now + include_directories(SYSTEM third-party/nvapi-opensource) + file(GLOB NVPREFS_FILES CONFIGURE_DEPENDS + "third-party/nvapi-opensource/*.h" + "src/platform/windows/nvprefs/*.cpp" + "src/platform/windows/nvprefs/*.h") + include_directories(SYSTEM third-party/ViGEmClient/include) if(NOT DEFINED SUNSHINE_ICON_PATH) @@ -177,7 +185,8 @@ if(WIN32) third-party/ViGEmClient/include/ViGEm/Client.h third-party/ViGEmClient/include/ViGEm/Common.h third-party/ViGEmClient/include/ViGEm/Util.h - third-party/ViGEmClient/include/ViGEm/km/BusShared.h) + third-party/ViGEmClient/include/ViGEm/km/BusShared.h + ${NVPREFS_FILES}) set(OPENSSL_LIBRARIES libssl.a @@ -762,6 +771,7 @@ if(WIN32) # see options at: https://cmake.org/cmake/help/latest/cpack_gen/nsis.h "${CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS} nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\delete-firewall-rule.bat\\\"' nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\uninstall-service.bat\\\"' + nsExec::ExecToLog '\\\"$INSTDIR\\\\sunshine.exe\\\" --restore-nvprefs-undo' MessageBox MB_YESNO|MB_ICONQUESTION \ 'Do you want to remove ViGEmBus)?' \ /SD IDNO IDNO NoVigem diff --git a/src/main.cpp b/src/main.cpp index b347cc0a..9cd1c689 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -46,6 +46,11 @@ safe::mail_t mail::man; using namespace std::literals; namespace bl = boost::log; +#ifdef _WIN32 +// Define global singleton used for NVIDIA control panel modifications +nvprefs::nvprefs_interface nvprefs_instance; +#endif + thread_pool_util::ThreadPool task_pool; bl::sources::severity_logger verbose(0); // Dominating output bl::sources::severity_logger debug(1); // Follow what is happening @@ -112,6 +117,22 @@ namespace version { } } // namespace version +#ifdef _WIN32 +namespace restore_nvprefs_undo { + int + entry(const char *name, int argc, char *argv[]) { + // Restore global NVIDIA control panel settings to the undo file + // left by improper termination of sunshine.exe, if it exists. + // This entry point is typically called by the uninstaller. + if (nvprefs_instance.load()) { + nvprefs_instance.restore_from_and_delete_undo_file_if_exists(); + nvprefs_instance.unload(); + } + return 0; + } +} // namespace restore_nvprefs_undo +#endif + namespace lifetime { static char **argv; static std::atomic_int desired_exit_code; @@ -413,7 +434,10 @@ namespace gen_creds { std::map> cmd_to_func { { "creds"sv, gen_creds::entry }, { "help"sv, help::entry }, - { "version"sv, version::entry } + { "version"sv, version::entry }, +#ifdef _WIN32 + { "restore-nvprefs-undo"sv, restore_nvprefs_undo::entry }, +#endif }; #ifdef _WIN32 @@ -568,6 +592,21 @@ main(int argc, char *argv[]) { return fn->second(argv[0], config::sunshine.cmd.argc, config::sunshine.cmd.argv); } + +#ifdef WIN32 + // Modify relevant NVIDIA control panel settings if the system has corresponding gpu + if (nvprefs_instance.load()) { + // Restore global settings to the undo file left by improper termination of sunshine.exe + nvprefs_instance.restore_from_and_delete_undo_file_if_exists(); + // Modify application settings for sunshine.exe + nvprefs_instance.modify_application_profile(); + // Modify global settings, undo file is produced in the process to restore after improper termination + nvprefs_instance.modify_global_profile(); + // Unload dynamic library to survive driver reinstallation + nvprefs_instance.unload(); + } +#endif + BOOST_LOG(info) << PROJECT_NAME << " version: " << PROJECT_VER << std::endl; task_pool.start(1); @@ -675,6 +714,14 @@ main(int argc, char *argv[]) { system_tray::end_tray(); #endif +#ifdef WIN32 + // Restore global NVIDIA control panel settings + if (nvprefs_instance.owning_undo_file() && nvprefs_instance.load()) { + nvprefs_instance.restore_global_profile(); + nvprefs_instance.unload(); + } +#endif + return lifetime::desired_exit_code; } diff --git a/src/main.h b/src/main.h index d5a6eeb5..28b7d39e 100644 --- a/src/main.h +++ b/src/main.h @@ -17,6 +17,12 @@ #include "thread_pool.h" #include "thread_safe.h" +#ifdef _WIN32 + // Declare global singleton used for NVIDIA control panel modifications + #include "platform/windows/nvprefs/nvprefs_interface.h" +extern nvprefs::nvprefs_interface nvprefs_instance; +#endif + extern thread_pool_util::ThreadPool task_pool; extern bool display_cursor; diff --git a/src/platform/windows/misc.cpp b/src/platform/windows/misc.cpp index c5bdc4fc..07f438bd 100644 --- a/src/platform/windows/misc.cpp +++ b/src/platform/windows/misc.cpp @@ -33,6 +33,8 @@ #include "src/utility.h" #include +#include "nvprefs/nvprefs_interface.h" + // UDP_SEND_MSG_SIZE was added in the Windows 10 20H1 SDK #ifndef UDP_SEND_MSG_SIZE #define UDP_SEND_MSG_SIZE 2 @@ -740,6 +742,16 @@ namespace platf { // Promote ourselves to high priority class SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS); + // Modify NVIDIA control panel settings again, in case they have been changed externally since sunshine launch + if (nvprefs_instance.load()) { + if (!nvprefs_instance.owning_undo_file()) { + nvprefs_instance.restore_from_and_delete_undo_file_if_exists(); + } + nvprefs_instance.modify_application_profile(); + nvprefs_instance.modify_global_profile(); + nvprefs_instance.unload(); + } + // Enable low latency mode on all connected WLAN NICs if wlanapi.dll is available if (fn_WlanOpenHandle) { DWORD negotiated_version; diff --git a/src/platform/windows/nvprefs/driver_settings.cpp b/src/platform/windows/nvprefs/driver_settings.cpp new file mode 100644 index 00000000..54529fd5 --- /dev/null +++ b/src/platform/windows/nvprefs/driver_settings.cpp @@ -0,0 +1,289 @@ +#include "nvprefs_common.h" + +#include "driver_settings.h" + +namespace { + + const auto sunshine_application_profile_name = L"SunshineStream"; + const auto sunshine_application_path = L"sunshine.exe"; + + void + nvapi_error_message(NvAPI_Status status) { + NvAPI_ShortString message = {}; + NvAPI_GetErrorMessage(status, message); + nvprefs::error_message(std::string("NvAPI error: ") + message); + } + + void + fill_nvapi_string(NvAPI_UnicodeString &dest, const wchar_t *src) { + static_assert(sizeof(NvU16) == sizeof(wchar_t)); + memcpy_s(dest, NVAPI_UNICODE_STRING_MAX * sizeof(NvU16), src, (wcslen(src) + 1) * sizeof(wchar_t)); + } + +} // namespace + +namespace nvprefs { + + driver_settings_t::~driver_settings_t() { + if (session_handle) { + NvAPI_DRS_DestroySession(session_handle); + } + } + + bool + driver_settings_t::init() { + if (session_handle) return true; + + NvAPI_Status status; + + status = NvAPI_Initialize(); + if (status != NVAPI_OK) { + info_message("NvAPI_Initialize() failed, ignore if you don't have NVIDIA video card"); + return false; + } + + status = NvAPI_DRS_CreateSession(&session_handle); + if (status != NVAPI_OK) { + nvapi_error_message(status); + error_message("NvAPI_DRS_CreateSession() failed"); + return false; + } + + return load_settings(); + } + + void + driver_settings_t::destroy() { + if (session_handle) { + NvAPI_DRS_DestroySession(session_handle); + session_handle = 0; + } + NvAPI_Unload(); + } + + bool + driver_settings_t::load_settings() { + if (!session_handle) return false; + + NvAPI_Status status = NvAPI_DRS_LoadSettings(session_handle); + if (status != NVAPI_OK) { + nvapi_error_message(status); + error_message("NvAPI_DRS_LoadSettings() failed"); + destroy(); + return false; + } + + return true; + } + + bool + driver_settings_t::save_settings() { + if (!session_handle) return false; + + NvAPI_Status status = NvAPI_DRS_SaveSettings(session_handle); + if (status != NVAPI_OK) { + nvapi_error_message(status); + error_message("NvAPI_DRS_SaveSettings() failed"); + return false; + } + + return true; + } + + bool + driver_settings_t::restore_global_profile_to_undo(const undo_data_t &undo_data) { + if (!session_handle) return false; + + auto [opengl_swapchain_saved, opengl_swapchain_our_value, opengl_swapchain_undo_value] = undo_data.get_opengl_swapchain(); + + if (opengl_swapchain_saved) { + NvAPI_Status status; + + NvDRSProfileHandle profile_handle = 0; + status = NvAPI_DRS_GetBaseProfile(session_handle, &profile_handle); + if (status != NVAPI_OK) { + nvapi_error_message(status); + error_message("NvAPI_DRS_GetBaseProfile() failed"); + return false; + } + + NVDRS_SETTING setting = {}; + setting.version = NVDRS_SETTING_VER; + status = NvAPI_DRS_GetSetting(session_handle, profile_handle, OGL_CPL_PREFER_DXPRESENT_ID, &setting); + + if (status == NVAPI_OK && setting.settingLocation == NVDRS_CURRENT_PROFILE_LOCATION && setting.u32CurrentValue == opengl_swapchain_our_value) { + if (opengl_swapchain_undo_value) { + setting = {}; + setting.version = NVDRS_SETTING_VER1; + setting.settingId = OGL_CPL_PREFER_DXPRESENT_ID; + setting.settingType = NVDRS_DWORD_TYPE; + setting.settingLocation = NVDRS_CURRENT_PROFILE_LOCATION; + setting.u32CurrentValue = *opengl_swapchain_undo_value; + + status = NvAPI_DRS_SetSetting(session_handle, profile_handle, &setting); + + if (status != NVAPI_OK) { + nvapi_error_message(status); + error_message("NvAPI_DRS_SetSetting() OGL_CPL_PREFER_DXPRESENT failed"); + return false; + } + } + else { + status = NvAPI_DRS_DeleteProfileSetting(session_handle, profile_handle, OGL_CPL_PREFER_DXPRESENT_ID); + + if (status != NVAPI_OK && status != NVAPI_SETTING_NOT_FOUND) { + nvapi_error_message(status); + error_message("NvAPI_DRS_DeleteProfileSetting() OGL_CPL_PREFER_DXPRESENT failed"); + return false; + } + } + + info_message("Restored OGL_CPL_PREFER_DXPRESENT for base profile"); + } + else if (status == NVAPI_OK || status == NVAPI_SETTING_NOT_FOUND) { + info_message("OGL_CPL_PREFER_DXPRESENT has been changed from our value in base profile, not restoring"); + } + else { + error_message("NvAPI_DRS_GetSetting() OGL_CPL_PREFER_DXPRESENT failed"); + return false; + } + } + + return true; + } + + bool + driver_settings_t::check_and_modify_global_profile(std::optional &undo_data) { + if (!session_handle) return false; + + undo_data.reset(); + NvAPI_Status status; + + NvDRSProfileHandle profile_handle = 0; + status = NvAPI_DRS_GetBaseProfile(session_handle, &profile_handle); + if (status != NVAPI_OK) { + nvapi_error_message(status); + error_message("NvAPI_DRS_GetBaseProfile() failed"); + return false; + } + + NVDRS_SETTING setting = {}; + setting.version = NVDRS_SETTING_VER; + status = NvAPI_DRS_GetSetting(session_handle, profile_handle, OGL_CPL_PREFER_DXPRESENT_ID, &setting); + + // Remember current OpenGL/Vulkan DXGI swapchain setting and change it if needed + if (status == NVAPI_SETTING_NOT_FOUND || (status == NVAPI_OK && setting.u32CurrentValue != OGL_CPL_PREFER_DXPRESENT_PREFER_ENABLED)) { + undo_data = undo_data_t(); + if (status == NVAPI_OK) { + undo_data->set_opengl_swapchain(OGL_CPL_PREFER_DXPRESENT_PREFER_ENABLED, setting.u32CurrentValue); + } + else { + undo_data->set_opengl_swapchain(OGL_CPL_PREFER_DXPRESENT_PREFER_ENABLED, std::nullopt); + } + + setting = {}; + setting.version = NVDRS_SETTING_VER1; + setting.settingId = OGL_CPL_PREFER_DXPRESENT_ID; + setting.settingType = NVDRS_DWORD_TYPE; + setting.settingLocation = NVDRS_CURRENT_PROFILE_LOCATION; + setting.u32CurrentValue = OGL_CPL_PREFER_DXPRESENT_PREFER_ENABLED; + + status = NvAPI_DRS_SetSetting(session_handle, profile_handle, &setting); + if (status != NVAPI_OK) { + nvapi_error_message(status); + error_message("NvAPI_DRS_SetSetting() OGL_CPL_PREFER_DXPRESENT failed"); + return false; + } + + info_message("Changed OGL_CPL_PREFER_DXPRESENT to OGL_CPL_PREFER_DXPRESENT_PREFER_ENABLED for base profile"); + } + else if (status != NVAPI_OK) { + nvapi_error_message(status); + error_message("NvAPI_DRS_GetSetting() OGL_CPL_PREFER_DXPRESENT failed"); + return false; + } + + return true; + } + + bool + driver_settings_t::check_and_modify_application_profile(bool &modified) { + if (!session_handle) return false; + + modified = false; + NvAPI_Status status; + + NvAPI_UnicodeString profile_name = {}; + fill_nvapi_string(profile_name, sunshine_application_profile_name); + + NvDRSProfileHandle profile_handle = 0; + status = NvAPI_DRS_FindProfileByName(session_handle, profile_name, &profile_handle); + + if (status != NVAPI_OK) { + // Create application profile if missing + NVDRS_PROFILE profile = {}; + profile.version = NVDRS_PROFILE_VER1; + fill_nvapi_string(profile.profileName, sunshine_application_profile_name); + status = NvAPI_DRS_CreateProfile(session_handle, &profile, &profile_handle); + if (status != NVAPI_OK) { + nvapi_error_message(status); + error_message("NvAPI_DRS_CreateProfile() failed"); + return false; + } + modified = true; + } + + NvAPI_UnicodeString sunshine_path = {}; + fill_nvapi_string(sunshine_path, sunshine_application_path); + + NVDRS_APPLICATION application = {}; + application.version = NVDRS_APPLICATION_VER_V1; + status = NvAPI_DRS_GetApplicationInfo(session_handle, profile_handle, sunshine_path, &application); + + if (status != NVAPI_OK) { + // Add application to application profile if missing + application.version = NVDRS_APPLICATION_VER_V1; + application.isPredefined = 0; + fill_nvapi_string(application.appName, sunshine_application_path); + fill_nvapi_string(application.userFriendlyName, sunshine_application_path); + 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("NvAPI_DRS_CreateApplication() failed"); + return false; + } + modified = true; + } + + NVDRS_SETTING setting = {}; + setting.version = NVDRS_SETTING_VER1; + status = NvAPI_DRS_GetSetting(session_handle, profile_handle, PREFERRED_PSTATE_ID, &setting); + + if (status != NVAPI_OK || + setting.settingLocation != NVDRS_CURRENT_PROFILE_LOCATION || + setting.u32CurrentValue != PREFERRED_PSTATE_PREFER_MAX) { + // Set power setting if needed + setting = {}; + setting.version = NVDRS_SETTING_VER1; + setting.settingId = PREFERRED_PSTATE_ID; + setting.settingType = NVDRS_DWORD_TYPE; + setting.settingLocation = NVDRS_CURRENT_PROFILE_LOCATION; + setting.u32CurrentValue = PREFERRED_PSTATE_PREFER_MAX; + + status = NvAPI_DRS_SetSetting(session_handle, profile_handle, &setting); + if (status != NVAPI_OK) { + nvapi_error_message(status); + error_message("NvAPI_DRS_SetSetting() PREFERRED_PSTATE failed"); + return false; + } + modified = true; + + info_message(std::wstring(L"Changed PREFERRED_PSTATE to PREFERRED_PSTATE_PREFER_MAX for ") + sunshine_application_path); + } + + return true; + } + +} // namespace nvprefs diff --git a/src/platform/windows/nvprefs/driver_settings.h b/src/platform/windows/nvprefs/driver_settings.h new file mode 100644 index 00000000..fbbc9aa1 --- /dev/null +++ b/src/platform/windows/nvprefs/driver_settings.h @@ -0,0 +1,36 @@ +#pragma once + +#include "undo_data.h" + +namespace nvprefs { + + class driver_settings_t { + public: + ~driver_settings_t(); + + bool + init(); + + void + destroy(); + + bool + load_settings(); + + bool + save_settings(); + + bool + restore_global_profile_to_undo(const undo_data_t &undo_data); + + bool + check_and_modify_global_profile(std::optional &undo_data); + + bool + check_and_modify_application_profile(bool &modified); + + private: + NvDRSSessionHandle session_handle = 0; + }; + +} // namespace nvprefs diff --git a/src/platform/windows/nvprefs/nvapi_opensource_wrapper.cpp b/src/platform/windows/nvprefs/nvapi_opensource_wrapper.cpp new file mode 100644 index 00000000..23a85842 --- /dev/null +++ b/src/platform/windows/nvprefs/nvapi_opensource_wrapper.cpp @@ -0,0 +1,128 @@ +#include "nvprefs_common.h" + +#include + +namespace { + + std::map interfaces; + HMODULE dll = NULL; + + template + NvAPI_Status + call_interface(const char *name, Args... args) { + auto func = (Func *) interfaces[name]; + + if (!func) { + return interfaces.empty() ? NVAPI_API_NOT_INITIALIZED : NVAPI_NOT_SUPPORTED; + } + + return func(args...); + } + +} // namespace + +#undef NVAPI_INTERFACE +#define NVAPI_INTERFACE NvAPI_Status __cdecl + +extern void * +nvapi_QueryInterface(NvU32 id); + +NVAPI_INTERFACE +NvAPI_Initialize() { + if (dll) return NVAPI_OK; + +#ifdef _WIN64 + auto dll_name = "nvapi64.dll"; +#else + auto dll_name = "nvapi.dll"; +#endif + + if ((dll = LoadLibraryEx(dll_name, NULL, LOAD_LIBRARY_SEARCH_SYSTEM32))) { + if (auto query_interface = (decltype(nvapi_QueryInterface) *) GetProcAddress(dll, "nvapi_QueryInterface")) { + for (const auto &item : nvapi_interface_table) { + interfaces[item.func] = query_interface(item.id); + } + return NVAPI_OK; + } + } + + NvAPI_Unload(); + return NVAPI_LIBRARY_NOT_FOUND; +} + +NVAPI_INTERFACE +NvAPI_Unload() { + if (dll) { + interfaces.clear(); + FreeLibrary(dll); + dll = NULL; + } + return NVAPI_OK; +} + +NVAPI_INTERFACE +NvAPI_GetErrorMessage(NvAPI_Status nr, NvAPI_ShortString szDesc) { + return call_interface("NvAPI_GetErrorMessage", nr, szDesc); +} + +// This is only a subset of NvAPI_DRS_* functions, more can be added if needed + +NVAPI_INTERFACE +NvAPI_DRS_CreateSession(NvDRSSessionHandle *phSession) { + return call_interface("NvAPI_DRS_CreateSession", phSession); +} + +NVAPI_INTERFACE +NvAPI_DRS_DestroySession(NvDRSSessionHandle hSession) { + return call_interface("NvAPI_DRS_DestroySession", hSession); +} + +NVAPI_INTERFACE +NvAPI_DRS_LoadSettings(NvDRSSessionHandle hSession) { + return call_interface("NvAPI_DRS_LoadSettings", hSession); +} + +NVAPI_INTERFACE +NvAPI_DRS_SaveSettings(NvDRSSessionHandle hSession) { + return call_interface("NvAPI_DRS_SaveSettings", hSession); +} + +NVAPI_INTERFACE +NvAPI_DRS_CreateProfile(NvDRSSessionHandle hSession, NVDRS_PROFILE *pProfileInfo, NvDRSProfileHandle *phProfile) { + return call_interface("NvAPI_DRS_CreateProfile", hSession, pProfileInfo, phProfile); +} + +NVAPI_INTERFACE +NvAPI_DRS_FindProfileByName(NvDRSSessionHandle hSession, NvAPI_UnicodeString profileName, NvDRSProfileHandle *phProfile) { + return call_interface("NvAPI_DRS_FindProfileByName", hSession, profileName, phProfile); +} + +NVAPI_INTERFACE +NvAPI_DRS_CreateApplication(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NVDRS_APPLICATION *pApplication) { + return call_interface("NvAPI_DRS_CreateApplication", hSession, hProfile, pApplication); +} + +NVAPI_INTERFACE +NvAPI_DRS_GetApplicationInfo(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NvAPI_UnicodeString appName, NVDRS_APPLICATION *pApplication) { + return call_interface("NvAPI_DRS_GetApplicationInfo", hSession, hProfile, appName, pApplication); +} + +NVAPI_INTERFACE +NvAPI_DRS_SetSetting(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NVDRS_SETTING *pSetting) { + return call_interface("NvAPI_DRS_SetSetting", hSession, hProfile, pSetting); +} + +NVAPI_INTERFACE +NvAPI_DRS_GetSetting(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NvU32 settingId, NVDRS_SETTING *pSetting) { + return call_interface("NvAPI_DRS_GetSetting", hSession, hProfile, settingId, pSetting); +} + +NVAPI_INTERFACE +NvAPI_DRS_DeleteProfileSetting(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NvU32 settingId) { + return call_interface("NvAPI_DRS_DeleteProfileSetting", hSession, hProfile, settingId); +} + +NVAPI_INTERFACE +NvAPI_DRS_GetBaseProfile(NvDRSSessionHandle hSession, NvDRSProfileHandle *phProfile) { + return call_interface("NvAPI_DRS_GetBaseProfile", hSession, phProfile); +} diff --git a/src/platform/windows/nvprefs/nvprefs_common.cpp b/src/platform/windows/nvprefs/nvprefs_common.cpp new file mode 100644 index 00000000..ba15dfe3 --- /dev/null +++ b/src/platform/windows/nvprefs/nvprefs_common.cpp @@ -0,0 +1,25 @@ +#include "nvprefs_common.h" + +namespace nvprefs { + + void + info_message(const std::wstring &message) { + BOOST_LOG(info) << "nvprefs: " << message; + } + + void + info_message(const std::string &message) { + BOOST_LOG(info) << "nvprefs: " << message; + } + + void + error_message(const std::wstring &message) { + BOOST_LOG(error) << "nvprefs: " << message; + } + + void + error_message(const std::string &message) { + BOOST_LOG(error) << "nvprefs: " << message; + } + +} // namespace nvprefs diff --git a/src/platform/windows/nvprefs/nvprefs_common.h b/src/platform/windows/nvprefs/nvprefs_common.h new file mode 100644 index 00000000..7d4a6619 --- /dev/null +++ b/src/platform/windows/nvprefs/nvprefs_common.h @@ -0,0 +1,70 @@ +#pragma once + +// sunshine utility header for generic smart pointers +#include "src/utility.h" + +// sunshine boost::log severity levels +#include "src/main.h" + +// standard library headers +#include +#include +#include +#include +#include +#include +#include +#include + +// winapi headers +// disable clang-format header reordering +// clang-format off +#include +#include +// clang-format on + +// nvapi headers +// disable clang-format header reordering +// clang-format off +#include +#include +// clang-format on + +// boost headers +#include + +namespace nvprefs { + + struct safe_handle: public util::safe_ptr_v2 { + using util::safe_ptr_v2::safe_ptr_v2; + explicit operator bool() const { + auto handle = get(); + return handle != NULL && handle != INVALID_HANDLE_VALUE; + } + }; + + struct safe_hlocal_deleter { + void + operator()(void *p) { + LocalFree(p); + } + }; + + template + using safe_hlocal = util::uniq_ptr, safe_hlocal_deleter>; + + using safe_sid = util::safe_ptr_v2; + + void + info_message(const std::wstring &message); + + void + info_message(const std::string &message); + + void + error_message(const std::wstring &message); + + void + error_message(const std::string &message); + +} // namespace nvprefs diff --git a/src/platform/windows/nvprefs/nvprefs_interface.cpp b/src/platform/windows/nvprefs/nvprefs_interface.cpp new file mode 100644 index 00000000..961788ae --- /dev/null +++ b/src/platform/windows/nvprefs/nvprefs_interface.cpp @@ -0,0 +1,225 @@ +#include "nvprefs_common.h" + +#include "nvprefs_interface.h" + +#include "driver_settings.h" +#include "undo_data.h" +#include "undo_file.h" + +namespace { + + const auto sunshine_program_data_folder = "Sunshine"; + const auto nvprefs_undo_file_name = "nvprefs_undo.json"; + +} // namespace + +namespace nvprefs { + + struct nvprefs_interface::impl { + bool loaded = false; + driver_settings_t driver_settings; + std::filesystem::path undo_folder_path; + std::filesystem::path undo_file_path; + std::optional undo_data; + std::optional undo_file; + }; + + nvprefs_interface::nvprefs_interface(): + pimpl(new impl()) { + } + + nvprefs_interface::~nvprefs_interface() { + if (owning_undo_file() && load()) { + restore_global_profile(); + } + unload(); + } + + bool + nvprefs_interface::load() { + if (!pimpl->loaded) { + // Check %ProgramData% variable, need it for storing undo file + wchar_t program_data_env[MAX_PATH]; + auto get_env_result = GetEnvironmentVariableW(L"ProgramData", program_data_env, MAX_PATH); + if (get_env_result == 0 || get_env_result >= MAX_PATH || !std::filesystem::is_directory(program_data_env)) { + error_message("Missing or malformed %ProgramData% environment variable"); + return false; + } + + // Prepare undo file path variables + pimpl->undo_folder_path = std::filesystem::path(program_data_env) / sunshine_program_data_folder; + pimpl->undo_file_path = pimpl->undo_folder_path / nvprefs_undo_file_name; + + // Dynamically load nvapi library and load driver settings + pimpl->loaded = pimpl->driver_settings.init(); + } + + return pimpl->loaded; + } + + void + nvprefs_interface::unload() { + if (pimpl->loaded) { + // Unload dynamically loaded nvapi library + pimpl->driver_settings.destroy(); + pimpl->loaded = false; + } + } + + bool + nvprefs_interface::restore_from_and_delete_undo_file_if_exists() { + if (!pimpl->loaded) return false; + + // Check for undo file from previous improper termination + bool access_denied = false; + if (auto undo_file = undo_file_t::open_existing_file(pimpl->undo_file_path, access_denied)) { + // 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"); + } + else { + error_message("Failed to restore global profile settings from undo file, deleting the file anyway"); + } + } + else { + error_message("Coulnd't read undo file, deleting the file anyway"); + } + + if (!undo_file->delete_file()) { + error_message("Couldn't delete undo file"); + return false; + } + } + else if (access_denied) { + error_message("Couldn't open undo file from previous improper termination, or confirm that there's no such file"); + return false; + } + + return true; + } + + bool + nvprefs_interface::modify_application_profile() { + if (!pimpl->loaded) return false; + + // Modify and save sunshine.exe application profile settings, if needed + bool modified = false; + if (!pimpl->driver_settings.check_and_modify_application_profile(modified)) { + error_message("Failed to modify application profile settings"); + return false; + } + else if (modified) { + if (pimpl->driver_settings.save_settings()) { + info_message("Modified application profile settings"); + } + else { + error_message("Couldn't save application profile settings"); + return false; + } + } + else { + info_message("No need to modify application profile settings"); + } + + return true; + } + + bool + nvprefs_interface::modify_global_profile() { + if (!pimpl->loaded) return false; + + // Modify but not save global profile settings, if needed + std::optional undo_data; + if (!pimpl->driver_settings.check_and_modify_global_profile(undo_data)) { + error_message("Couldn't modify global profile settings"); + return false; + } + else if (!undo_data) { + info_message("No need to modify global profile settings"); + return true; + } + + auto make_undo_and_commit = [&]() -> bool { + // Create and lock undo file if it hasn't been done yet + if (!pimpl->undo_file) { + // Prepare Sunshine folder in ProgramData if it doesn't exist + if (!CreateDirectoryW(pimpl->undo_folder_path.c_str(), nullptr) && GetLastError() != ERROR_ALREADY_EXISTS) { + error_message("Couldn't create undo folder"); + return false; + } + + // Create undo file to handle improper termination of nvprefs.exe + pimpl->undo_file = undo_file_t::create_new_file(pimpl->undo_file_path); + if (!pimpl->undo_file) { + error_message("Couldn't create undo file"); + return false; + } + } + + assert(undo_data); + if (pimpl->undo_data) { + // Merge undo data if settings has been modified externally since our last modification + pimpl->undo_data->merge(*undo_data); + } + else { + pimpl->undo_data = undo_data; + } + + // Write undo data to undo file + if (!pimpl->undo_file->write_undo_data(*pimpl->undo_data)) { + error_message("Couldn't write to undo file - deleting the file"); + if (!pimpl->undo_file->delete_file()) { + error_message("Couldn't delete undo file"); + } + return false; + } + + // Save global profile settings + if (!pimpl->driver_settings.save_settings()) { + error_message("Couldn't save global profile settings"); + return false; + } + + return true; + }; + + if (!make_undo_and_commit()) { + // Revert settings modifications + pimpl->driver_settings.load_settings(); + return false; + } + + return true; + } + + bool + nvprefs_interface::owning_undo_file() { + return pimpl->undo_file.has_value(); + } + + bool + nvprefs_interface::restore_global_profile() { + if (!pimpl->loaded || !pimpl->undo_data || !pimpl->undo_file) return false; + + // 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; + } + pimpl->undo_data = std::nullopt; + pimpl->undo_file = std::nullopt; + } + else { + error_message("Couldn't restore global profile settings"); + return false; + } + + return true; + } + +} // namespace nvprefs diff --git a/src/platform/windows/nvprefs/nvprefs_interface.h b/src/platform/windows/nvprefs/nvprefs_interface.h new file mode 100644 index 00000000..43d588c3 --- /dev/null +++ b/src/platform/windows/nvprefs/nvprefs_interface.h @@ -0,0 +1,36 @@ +#pragma once + +namespace nvprefs { + + class nvprefs_interface { + public: + nvprefs_interface(); + ~nvprefs_interface(); + + bool + load(); + + void + unload(); + + bool + restore_from_and_delete_undo_file_if_exists(); + + bool + modify_application_profile(); + + bool + modify_global_profile(); + + bool + owning_undo_file(); + + bool + restore_global_profile(); + + private: + struct impl; + std::unique_ptr pimpl; + }; + +} // namespace nvprefs diff --git a/src/platform/windows/nvprefs/undo_data.cpp b/src/platform/windows/nvprefs/undo_data.cpp new file mode 100644 index 00000000..7abd81f7 --- /dev/null +++ b/src/platform/windows/nvprefs/undo_data.cpp @@ -0,0 +1,71 @@ +#include "nvprefs_common.h" + +#include "undo_data.h" + +namespace { + + const auto opengl_swapchain_our_value_key = "/opengl_swapchain/our_value"; + const auto opengl_swapchain_undo_value_key = "/opengl_swapchain/undo_value"; + +} // namespace + +namespace nvprefs { + + void + undo_data_t::set_opengl_swapchain(uint32_t our_value, std::optional undo_value) { + data.set_at_pointer(opengl_swapchain_our_value_key, our_value); + if (undo_value) { + data.set_at_pointer(opengl_swapchain_undo_value_key, *undo_value); + } + else { + data.set_at_pointer(opengl_swapchain_undo_value_key, nullptr); + } + } + + std::tuple> + undo_data_t::get_opengl_swapchain() const { + auto get_value = [this](const auto &key) -> std::tuple> { + try { + auto value = data.at_pointer(key); + if (value.is_null()) { + return { true, std::nullopt }; + } + else if (value.is_number()) { + return { true, value.template to_number() }; + } + } + catch (...) { + } + error_message(std::string("Couldn't find ") + key + " element"); + return { false, std::nullopt }; + }; + + auto [our_value_present, our_value] = get_value(opengl_swapchain_our_value_key); + auto [undo_value_present, undo_value] = get_value(opengl_swapchain_undo_value_key); + + if (!our_value_present || !undo_value_present || !our_value) { + return { false, 0, std::nullopt }; + } + + return { true, *our_value, undo_value }; + } + + std::string + undo_data_t::write() const { + return boost::json::serialize(data); + } + + void + undo_data_t::read(const std::vector &buffer) { + data = boost::json::parse(std::string_view(buffer.data(), buffer.size())); + } + + void + undo_data_t::merge(const undo_data_t &newer_data) { + auto [opengl_swapchain_saved, opengl_swapchain_our_value, opengl_swapchain_undo_value] = newer_data.get_opengl_swapchain(); + if (opengl_swapchain_saved) { + set_opengl_swapchain(opengl_swapchain_our_value, opengl_swapchain_undo_value); + } + } + +} // namespace nvprefs diff --git a/src/platform/windows/nvprefs/undo_data.h b/src/platform/windows/nvprefs/undo_data.h new file mode 100644 index 00000000..999483e1 --- /dev/null +++ b/src/platform/windows/nvprefs/undo_data.h @@ -0,0 +1,32 @@ +#pragma once + +namespace nvprefs { + + class undo_data_t { + public: + void + set_opengl_swapchain(uint32_t our_value, std::optional undo_value); + + std::tuple> + get_opengl_swapchain() const; + + void + write(std::ostream &stream) const; + + std::string + write() const; + + void + read(std::istream &stream); + + void + read(const std::vector &buffer); + + void + merge(const undo_data_t &newer_data); + + private: + boost::json::value data; + }; + +} // namespace nvprefs diff --git a/src/platform/windows/nvprefs/undo_file.cpp b/src/platform/windows/nvprefs/undo_file.cpp new file mode 100644 index 00000000..8a459550 --- /dev/null +++ b/src/platform/windows/nvprefs/undo_file.cpp @@ -0,0 +1,154 @@ +#include "nvprefs_common.h" + +#include "undo_file.h" + +namespace { + + using namespace nvprefs; + + DWORD + relax_permissions(HANDLE file_handle) { + PACL old_dacl = nullptr; + + safe_hlocal sd; + DWORD status = GetSecurityInfo(file_handle, SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, nullptr, nullptr, &old_dacl, nullptr, &sd); + if (status != ERROR_SUCCESS) return status; + + safe_sid users_sid; + SID_IDENTIFIER_AUTHORITY nt_authorithy = SECURITY_NT_AUTHORITY; + if (!AllocateAndInitializeSid(&nt_authorithy, 2, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_USERS, 0, 0, 0, 0, 0, 0, &users_sid)) { + return GetLastError(); + } + + EXPLICIT_ACCESS ea = {}; + ea.grfAccessPermissions = GENERIC_READ | GENERIC_WRITE | DELETE; + ea.grfAccessMode = GRANT_ACCESS; + ea.grfInheritance = NO_INHERITANCE; + ea.Trustee.TrusteeForm = TRUSTEE_IS_SID; + ea.Trustee.ptstrName = (LPTSTR) users_sid.get(); + + safe_hlocal new_dacl; + status = SetEntriesInAcl(1, &ea, old_dacl, &new_dacl); + if (status != ERROR_SUCCESS) return status; + + status = SetSecurityInfo(file_handle, SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, nullptr, nullptr, new_dacl.get(), nullptr); + if (status != ERROR_SUCCESS) return status; + + return 0; + } + +} // namespace + +namespace nvprefs { + + std::optional + undo_file_t::open_existing_file(std::filesystem::path file_path, bool &access_denied) { + undo_file_t file; + file.file_handle.reset(CreateFileW(file_path.c_str(), GENERIC_READ | DELETE, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL)); + if (file.file_handle) { + access_denied = false; + return file; + } + else { + auto last_error = GetLastError(); + access_denied = (last_error != ERROR_FILE_NOT_FOUND && last_error != ERROR_PATH_NOT_FOUND); + return std::nullopt; + } + } + + std::optional + undo_file_t::create_new_file(std::filesystem::path file_path) { + undo_file_t file; + file.file_handle.reset(CreateFileW(file_path.c_str(), GENERIC_WRITE | STANDARD_RIGHTS_ALL, 0, nullptr, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL)); + + if (file.file_handle) { + // give GENERIC_READ, GENERIC_WRITE and DELETE permissions to Users group + if (relax_permissions(file.file_handle.get()) != 0) { + error_message("Failed to relax permissions on undo file"); + } + return file; + } + else { + return std::nullopt; + } + } + + bool + undo_file_t::delete_file() { + if (!file_handle) return false; + + FILE_DISPOSITION_INFO delete_file_info = { TRUE }; + if (SetFileInformationByHandle(file_handle.get(), FileDispositionInfo, &delete_file_info, sizeof(delete_file_info))) { + file_handle.reset(); + return true; + } + else { + return false; + } + } + + bool + undo_file_t::write_undo_data(const undo_data_t &undo_data) { + if (!file_handle) return false; + + std::string buffer; + try { + buffer = undo_data.write(); + } + catch (...) { + error_message("Couldn't serialize undo data"); + return false; + } + + if (!SetFilePointerEx(file_handle.get(), {}, nullptr, FILE_BEGIN) || !SetEndOfFile(file_handle.get())) { + error_message("Couldn't clear undo file"); + return false; + } + + DWORD bytes_written = 0; + if (!WriteFile(file_handle.get(), buffer.data(), buffer.size(), &bytes_written, nullptr) || bytes_written != buffer.size()) { + error_message("Couldn't write undo file"); + return false; + } + + if (!FlushFileBuffers(file_handle.get())) { + error_message("Failed to flush undo file"); + } + + return true; + } + + std::optional + undo_file_t::read_undo_data() { + if (!file_handle) return std::nullopt; + + LARGE_INTEGER file_size; + if (!GetFileSizeEx(file_handle.get(), &file_size)) { + error_message("Couldn't get undo file size"); + return std::nullopt; + } + + if ((size_t) file_size.QuadPart > 1024) { + error_message("Undo file size is unexpectedly large, aborting"); + return std::nullopt; + } + + std::vector buffer(file_size.QuadPart); + DWORD bytes_read = 0; + if (!ReadFile(file_handle.get(), buffer.data(), buffer.size(), &bytes_read, nullptr) || bytes_read != buffer.size()) { + error_message("Couldn't read undo file"); + return std::nullopt; + } + + undo_data_t undo_data; + try { + undo_data.read(buffer); + } + catch (...) { + error_message("Couldn't parse undo file"); + return std::nullopt; + } + return undo_data; + } + +} // namespace nvprefs diff --git a/src/platform/windows/nvprefs/undo_file.h b/src/platform/windows/nvprefs/undo_file.h new file mode 100644 index 00000000..46dcba61 --- /dev/null +++ b/src/platform/windows/nvprefs/undo_file.h @@ -0,0 +1,29 @@ +#pragma once + +#include "undo_data.h" + +namespace nvprefs { + + class undo_file_t { + public: + static std::optional + open_existing_file(std::filesystem::path file_path, bool &access_denied); + + static std::optional + create_new_file(std::filesystem::path file_path); + + bool + delete_file(); + + bool + write_undo_data(const undo_data_t &undo_data); + + std::optional + read_undo_data(); + + private: + undo_file_t() = default; + safe_handle file_handle; + }; + +} // namespace nvprefs