/** * @file src/process.cpp * @brief Definitions for the startup and shutdown of the apps started by a streaming Session. */ #define BOOST_BIND_GLOBAL_PLACEHOLDERS // standard includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // lib includes #include #include #include #include #include #include #include #include #include // local includes #include "app_catalog_policy.h" #include "app_framegen_config.h" #include "audio.h" #include "config.h" #include "crypto.h" #include "display_device.h" #include "deferred_action.h" #include "file_handler.h" #include "logging.h" #include "platform/common.h" #if defined(_WIN32) || defined(__linux__) #include "display_helper_integration.h" #include "remote_display_topology.h" #endif #ifdef _WIN32 #include "config_playnite.h" #include "platform/windows/display.h" #include "platform/windows/frame_limiter.h" #include "platform/windows/ipc/misc_utils.h" #include "platform/windows/lossless_scaling_paths.h" #include "platform/windows/misc.h" #include "platform/windows/playnite_integration.h" #include "platform/windows/display_helper_request_helpers.h" #include "platform/windows/virtual_display_cleanup.h" #include "tools/playnite_launcher/focus_utils.h" #include "tools/playnite_launcher/lossless_scaling.h" #include "tools/playnite_launcher/lossless_scaling_policy.h" #include #elif defined(__linux__) #include "platform/linux/gamescopegrab.h" #include "platform/linux/frame_limiter.h" #include "platform/linux/mangohud_policy.h" #include "platform/linux/mangohud_state.h" #include "platform/linux/misc.h" #include "platform/linux/secure_open.h" #include "platform/linux/smooth_motion_policy.h" #include "platform/linux/wayland_hdr_compatibility.h" #include "steam_integration.h" #include #include #include #include #include #endif #include "httpcommon.h" #include "nvhttp.h" #include "process.h" #ifdef _WIN32 #include "platform/windows/virtual_display.h" #include "platform/windows/virtual_display_legacy.h" #endif #include "rtsp.h" #include "state_storage.h" #include "stream.h" #include "system_tray.h" #include "utility.h" #include "uuid.h" #include "video.h" #include "webrtc_stream.h" #ifdef _WIN32 // from_utf8() string conversion function #include "platform/windows/misc.h" #include "platform/windows/utils.h" #include "platform/windows/utf_utils.h" // _SH constants for _wfsopen() #include #endif namespace proc { using namespace std::literals; namespace pt = boost::property_tree; std::optional resolve_app_from_snapshot(const std::vector &apps, const std::string &appid, const std::string &appuuid); namespace { constexpr const char *LOSSLESS_PROFILE_RECOMMENDED = "recommended"; constexpr const char *LOSSLESS_PROFILE_CUSTOM = "custom"; constexpr int LOSSLESS_DEFAULT_FLOW_SCALE = 50; constexpr int LOSSLESS_DEFAULT_RESOLUTION_SCALE = 100; constexpr bool LOSSLESS_DEFAULT_PERFORMANCE_MODE = true; constexpr int LOSSLESS_MIN_FLOW_SCALE = 0; constexpr int LOSSLESS_MAX_FLOW_SCALE = 100; constexpr int LOSSLESS_MIN_RESOLUTION_SCALE = 10; constexpr int LOSSLESS_MAX_RESOLUTION_SCALE = 100; constexpr int LOSSLESS_SHARPNESS_MIN = 1; constexpr int LOSSLESS_SHARPNESS_MAX = 10; constexpr const char *ENV_LOSSLESS_PROFILE = "SUNSHINE_LOSSLESS_SCALING_ACTIVE_PROFILE"; constexpr const char *ENV_LOSSLESS_ENABLED = "SUNSHINE_LOSSLESS_SCALING_ENABLED"; constexpr const char *ENV_LOSSLESS_CAPTURE_API = "SUNSHINE_LOSSLESS_SCALING_CAPTURE_API"; constexpr const char *ENV_LOSSLESS_QUEUE_TARGET = "SUNSHINE_LOSSLESS_SCALING_QUEUE_TARGET"; constexpr const char *ENV_LOSSLESS_HDR = "SUNSHINE_LOSSLESS_SCALING_HDR"; constexpr const char *ENV_LOSSLESS_FLOW_SCALE = "SUNSHINE_LOSSLESS_SCALING_FLOW_SCALE"; constexpr const char *ENV_LOSSLESS_PERFORMANCE_MODE = "SUNSHINE_LOSSLESS_SCALING_PERFORMANCE_MODE"; constexpr const char *ENV_LOSSLESS_RESOLUTION = "SUNSHINE_LOSSLESS_SCALING_RESOLUTION_SCALE"; constexpr const char *ENV_LOSSLESS_FRAMEGEN_MODE = "SUNSHINE_LOSSLESS_SCALING_FRAMEGEN_MODE"; constexpr const char *ENV_LOSSLESS_LSFG3_MODE = "SUNSHINE_LOSSLESS_SCALING_LSFG3_MODE"; constexpr const char *ENV_LOSSLESS_SCALING_TYPE = "SUNSHINE_LOSSLESS_SCALING_SCALING_TYPE"; constexpr const char *ENV_LOSSLESS_SHARPNESS = "SUNSHINE_LOSSLESS_SCALING_SHARPNESS"; constexpr const char *ENV_LOSSLESS_LS1_SHARPNESS = "SUNSHINE_LOSSLESS_SCALING_LS1_SHARPNESS"; constexpr const char *ENV_LOSSLESS_ANIME4K_TYPE = "SUNSHINE_LOSSLESS_SCALING_ANIME4K_TYPE"; constexpr const char *ENV_LOSSLESS_ANIME4K_VRS = "SUNSHINE_LOSSLESS_SCALING_ANIME4K_VRS"; constexpr const char *ENV_LOSSLESS_LAUNCH_DELAY = "SUNSHINE_LOSSLESS_SCALING_LAUNCH_DELAY"; constexpr const char *ENV_LOSSLESS_LEGACY_AUTO_DETECT = "SUNSHINE_LOSSLESS_SCALING_LEGACY_AUTO_DETECT"; constexpr std::array RTX_HDR_LIVE_KEYS { "rtx_hdr"sv, "rtx_hdr_sdr_brightness"sv, "rtx_hdr_contrast"sv, "rtx_hdr_saturation"sv, "rtx_hdr_middle_gray"sv, "rtx_hdr_peak_brightness"sv, }; #ifdef _WIN32 std::optional lossless_to_path(const std::string &utf8) { if (utf8.empty()) { return std::nullopt; } try { return std::filesystem::path(platf::dxgi::utf8_to_wide(utf8)); } catch (...) { } try { std::u8string utf8_bytes; utf8_bytes.reserve(utf8.size()); for (unsigned char ch : utf8) { utf8_bytes.push_back(static_cast(ch)); } return std::filesystem::path(utf8_bytes); } catch (...) { return std::nullopt; } } std::string lossless_path_to_utf8(const std::filesystem::path &path) { try { return platf::dxgi::wide_to_utf8(path.wstring()); } catch (...) { return std::string(); } } std::optional resolve_lossless_executable_path() { auto configured_path = lossless_to_path(config::lossless_scaling.exe_path); if (configured_path) { auto resolved_configured = lossless_paths::resolve_lossless_candidate(*configured_path); if (resolved_configured) { return resolved_configured; } std::error_code ec; auto ext = configured_path->extension().wstring(); std::transform(ext.begin(), ext.end(), ext.begin(), [](wchar_t ch) { return std::towlower(ch); }); if (ext == L".exe" && std::filesystem::is_regular_file(*configured_path, ec)) { return configured_path->lexically_normal(); } BOOST_LOG(warning) << "Lossless Scaling: configured executable path is invalid, not falling back to default: " << config::lossless_scaling.exe_path; return std::nullopt; } auto default_path = lossless_paths::default_steam_lossless_path(); std::optional default_opt; if (!default_path.empty()) { default_opt = default_path; } auto candidates = lossless_paths::discover_lossless_candidates(configured_path, std::nullopt, default_opt); if (!candidates.empty()) { return candidates.front(); } return std::nullopt; } #endif std::string normalize_frame_generation_provider(const std::string &value) { std::string normalized; normalized.reserve(value.size()); for (char ch : value) { if (std::isalnum(static_cast(ch))) { normalized.push_back(static_cast(std::tolower(static_cast(ch)))); } } if (normalized == "nvidia" || normalized == "smoothmotion" || normalized == "nvidiasmoothmotion") { return "nvidia-smooth-motion"; } if (normalized == "game" || normalized == "gameprovided" || normalized == "gameprovider") { return "game-provided"; } if (normalized == "lossless" || normalized == "losslessscaling") { return "lossless-scaling"; } return "lossless-scaling"; } #ifdef __linux__ bool nvidia_present_layer_installed() { static const bool installed = []() { constexpr std::array manifest_directories { "/etc/vulkan/implicit_layer.d", "/usr/local/share/vulkan/implicit_layer.d", "/usr/share/vulkan/implicit_layer.d", }; for (const auto directory : manifest_directories) { std::error_code ec; for (const auto &entry : std::filesystem::directory_iterator(directory, ec)) { if (ec) { break; } if (!entry.is_regular_file(ec) || entry.path().extension() != ".json") { continue; } std::ifstream manifest(entry.path()); std::string contents( (std::istreambuf_iterator(manifest)), std::istreambuf_iterator() ); if ( contents.find("VK_LAYER_NV_present") != std::string::npos && contents.find("libnvidia-present") != std::string::npos ) { return true; } } } return false; }(); return installed; } #endif struct lossless_profile_defaults_t { bool performance_mode; int flow_scale; int resolution_scale; std::string scaling_mode; int sharpening; std::string anime4k_size; bool anime4k_vrs; }; const lossless_profile_defaults_t LOSSLESS_DEFAULTS_RECOMMENDED { true, LOSSLESS_DEFAULT_FLOW_SCALE, LOSSLESS_DEFAULT_RESOLUTION_SCALE, "off", 5, "S", false, }; const lossless_profile_defaults_t LOSSLESS_DEFAULTS_CUSTOM { false, LOSSLESS_DEFAULT_FLOW_SCALE, LOSSLESS_DEFAULT_RESOLUTION_SCALE, "off", 5, "S", false, }; const std::array &lossless_scaling_modes() { static const std::array modes { "off", "ls1", "fsr", "nis", "sgsr", "bcas", "anime4k", "xbr", "sharp-bilinear", "integer", "nearest" }; return modes; } std::optional normalize_scaling_mode(const std::string &value) { std::string lower = boost::algorithm::to_lower_copy(value); const auto &modes = lossless_scaling_modes(); if (std::find(modes.begin(), modes.end(), lower) != modes.end()) { return lower; } return std::nullopt; } bool is_valid_env_key(const std::string &name) { if (name.empty()) { return false; } return name.find('=') == std::string::npos; } bool scaling_mode_requires_sharpening(const std::string &mode) { static const std::array sharpening_modes {"ls1", "fsr", "nis", "sgsr"}; return std::find(sharpening_modes.begin(), sharpening_modes.end(), mode) != sharpening_modes.end(); } bool scaling_mode_is_anime(const std::string &mode) { return mode == "anime4k"; } std::optional scaling_mode_to_lossless_value(const std::string &mode) { if (mode == "off") { return std::string("Off"); } if (mode == "ls1") { return std::string("LS1"); } if (mode == "fsr") { return std::string("FSR"); } if (mode == "nis") { return std::string("NIS"); } if (mode == "sgsr") { return std::string("SGSR"); } if (mode == "bcas") { return std::string("BicubicCAS"); } if (mode == "anime4k") { return std::string("Anime4k"); } if (mode == "xbr") { return std::string("XBR"); } if (mode == "sharp-bilinear") { return std::string("SharpBilinear"); } if (mode == "integer") { return std::string("Integer"); } if (mode == "nearest") { return std::string("Nearest"); } return std::nullopt; } int clamp_sharpness(int value) { return std::clamp(value, LOSSLESS_SHARPNESS_MIN, LOSSLESS_SHARPNESS_MAX); } struct lossless_runtime_values_t { std::string profile; std::optional performance_mode; std::optional flow_scale; std::optional resolution_scale_factor; std::optional capture_api; std::optional queue_target; std::optional hdr_enabled; std::optional frame_generation; std::optional lsfg3_mode; std::optional scaling_type; std::optional sharpness; std::optional ls1_sharpness; std::optional anime4k_type; std::optional anime4k_vrs; }; std::optional pt_get_optional_bool(const pt::ptree &node, const std::string &key) { auto child = node.get_child_optional(key); if (!child) { return std::nullopt; } try { return child->get_value(); } catch (...) { } try { auto text = child->get_value(); if (text.empty()) { return std::nullopt; } if (boost::iequals(text, "true") || text == "1") { return true; } if (boost::iequals(text, "false") || text == "0") { return false; } } catch (...) { } return std::nullopt; } std::optional pt_get_optional_int(const pt::ptree &node, const std::string &key) { auto child = node.get_child_optional(key); if (!child) { return std::nullopt; } try { return child->get_value(); } catch (...) { } try { auto text = child->get_value(); if (text.empty()) { return std::nullopt; } return std::stoi(text); } catch (...) { } return std::nullopt; } [[maybe_unused]] void populate_lossless_overrides(const pt::ptree &node, lossless_scaling_profile_overrides_t &target) { if (auto perf = pt_get_optional_bool(node, "performance-mode")) { target.performance_mode = *perf; } if (auto flow = pt_get_optional_int(node, "flow-scale")) { target.flow_scale = *flow; } if (auto res = pt_get_optional_int(node, "resolution-scale")) { target.resolution_scale = *res; } if (auto scaling = node.get_optional("scaling-type")) { if (auto normalized = normalize_scaling_mode(*scaling)) { target.scaling_type = *normalized; } } if (auto sharp = pt_get_optional_int(node, "sharpening")) { target.sharpening = clamp_sharpness(*sharp); } if (auto anime = node.get_optional("anime4k-size")) { std::string value = boost::algorithm::to_upper_copy(*anime); target.anime4k_size = std::move(value); } if (auto vrs = pt_get_optional_bool(node, "anime4k-vrs")) { target.anime4k_vrs = *vrs; } } void populate_lossless_overrides(const nlohmann::json &node, lossless_scaling_profile_overrides_t &target) { if (!node.is_object()) { return; } if (node.contains("performance-mode")) { target.performance_mode = util::get_non_string_json_value(node, "performance-mode", false); } if (node.contains("flow-scale")) { target.flow_scale = util::get_non_string_json_value(node, "flow-scale", 0); } if (node.contains("resolution-scale")) { target.resolution_scale = util::get_non_string_json_value(node, "resolution-scale", 0); } if (auto it = node.find("scaling-type"); it != node.end() && it->is_string()) { target.scaling_type = it->get(); } if (node.contains("sharpening")) { target.sharpening = util::get_non_string_json_value(node, "sharpening", 0); } if (auto it = node.find("anime4k-size"); it != node.end() && it->is_string()) { target.anime4k_size = it->get(); } if (node.contains("anime4k-vrs")) { target.anime4k_vrs = util::get_non_string_json_value(node, "anime4k-vrs", false); } } lossless_runtime_values_t compute_lossless_runtime(const ctx_t &ctx, bool frame_gen_enabled) { lossless_runtime_values_t result; const lossless_profile_defaults_t &defaults = boost::iequals(ctx.lossless_scaling_profile, LOSSLESS_PROFILE_RECOMMENDED) ? LOSSLESS_DEFAULTS_RECOMMENDED : LOSSLESS_DEFAULTS_CUSTOM; const lossless_scaling_profile_overrides_t &overrides = boost::iequals(ctx.lossless_scaling_profile, LOSSLESS_PROFILE_RECOMMENDED) ? ctx.lossless_scaling_recommended : ctx.lossless_scaling_custom; if (boost::iequals(ctx.lossless_scaling_profile, LOSSLESS_PROFILE_RECOMMENDED)) { result.profile = LOSSLESS_PROFILE_RECOMMENDED; result.capture_api = "WGC"; result.queue_target = 0; result.hdr_enabled = true; if (frame_gen_enabled) { result.frame_generation = "LSFG3"; result.lsfg3_mode = "ADAPTIVE"; } } else { result.profile = LOSSLESS_PROFILE_CUSTOM; if (frame_gen_enabled) { result.frame_generation = "LSFG3"; } } bool performance_mode = overrides.performance_mode.value_or(defaults.performance_mode); result.performance_mode = performance_mode; int flow_scale = overrides.flow_scale.value_or(defaults.flow_scale); flow_scale = std::clamp(flow_scale, LOSSLESS_MIN_FLOW_SCALE, LOSSLESS_MAX_FLOW_SCALE); result.flow_scale = flow_scale; std::string scaling_mode = overrides.scaling_type.has_value() ? *overrides.scaling_type : defaults.scaling_mode; auto normalized_mode = normalize_scaling_mode(scaling_mode); if (!normalized_mode) { normalized_mode = defaults.scaling_mode; } // Only apply resolution scaling if scaling type is not 'off' if (*normalized_mode != "off") { int resolution_scale = overrides.resolution_scale.value_or(defaults.resolution_scale); resolution_scale = std::clamp(resolution_scale, LOSSLESS_MIN_RESOLUTION_SCALE, LOSSLESS_MAX_RESOLUTION_SCALE); double factor = 100.0 / static_cast(resolution_scale); factor = std::clamp(factor, 1.0, 10.0); factor = std::round(factor * 100.0) / 100.0; result.resolution_scale_factor = factor; } else { // When scaling is off, use unity scale factor to disable custom scaling result.resolution_scale_factor = 1.0; } if (auto mapped = scaling_mode_to_lossless_value(*normalized_mode)) { result.scaling_type = *mapped; } if (scaling_mode_requires_sharpening(*normalized_mode)) { int sharpness = overrides.sharpening.value_or(defaults.sharpening); sharpness = clamp_sharpness(sharpness); result.sharpness = sharpness; if (*normalized_mode == "ls1") { result.ls1_sharpness = sharpness; } } if (scaling_mode_is_anime(*normalized_mode)) { std::string anime_type = overrides.anime4k_size.has_value() ? *overrides.anime4k_size : defaults.anime4k_size; boost::algorithm::to_upper(anime_type); result.anime4k_type = anime_type; bool vrs = overrides.anime4k_vrs.value_or(defaults.anime4k_vrs); result.anime4k_vrs = vrs; } return result; } #ifdef _WIN32 constexpr auto k_lossless_observation_duration = std::chrono::seconds(10); constexpr auto k_lossless_poll_interval = std::chrono::milliseconds(250); std::optional foreground_window_process_id() { HWND hwnd = GetForegroundWindow(); if (!hwnd) { return std::nullopt; } DWORD pid = 0; if (!GetWindowThreadProcessId(hwnd, &pid) || pid == 0) { return std::nullopt; } return pid; } std::vector process_group_pids(const bp::group &group) { std::vector pids; if (!group) { return pids; } const auto job_handle = (HANDLE) group.native_handle(); DWORD required_length = sizeof(JOBOBJECT_BASIC_PROCESS_ID_LIST); auto process_id_list = (PJOBOBJECT_BASIC_PROCESS_ID_LIST) calloc(1, required_length); auto fg = util::fail_guard([&process_id_list]() { free(process_id_list); }); while (!QueryInformationJobObject(job_handle, JobObjectBasicProcessIdList, process_id_list, required_length, &required_length) && GetLastError() == ERROR_MORE_DATA) { free(process_id_list); process_id_list = (PJOBOBJECT_BASIC_PROCESS_ID_LIST) calloc(1, required_length); if (!process_id_list) { return pids; } } if (!process_id_list) { return pids; } pids.reserve(process_id_list->NumberOfProcessIdsInList); for (DWORD i = 0; i < process_id_list->NumberOfProcessIdsInList; ++i) { pids.push_back(static_cast(process_id_list->ProcessIdList[i])); } return pids; } bool process_group_contains_pid(const bp::group &group, DWORD pid) { if (!group || pid == 0) { return false; } auto pids = process_group_pids(group); return std::find(pids.begin(), pids.end(), pid) != pids.end(); } struct lossless_process_candidate { DWORD pid = 0; ULONGLONG start_cpu = 0; ULONGLONG last_cpu = 0; SIZE_T peak_working_set = 0; std::wstring path; std::chrono::steady_clock::time_point first_seen; std::chrono::steady_clock::time_point last_seen; }; struct lossless_selection { DWORD pid = 0; std::wstring path_wide; std::string path_utf8; std::string directory_utf8; }; std::vector enumerate_process_ids_snapshot() { DWORD needed = 0; std::vector pids(1024); while (true) { if (!EnumProcesses(pids.data(), static_cast(pids.size() * sizeof(DWORD)), &needed)) { return {}; } if (needed < pids.size() * sizeof(DWORD)) { pids.resize(needed / sizeof(DWORD)); break; } pids.resize(pids.size() * 2); } return pids; } std::unordered_set capture_process_baseline_for_lossless() { std::unordered_set result; auto snapshot = enumerate_process_ids_snapshot(); result.reserve(snapshot.size()); for (auto pid : snapshot) { if (pid != 0) { result.insert(pid); } } return result; } bool sample_process_usage(DWORD pid, ULONGLONG &cpu_time, SIZE_T &working_set) { HANDLE handle = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid); if (!handle) { handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); } if (!handle) { return false; } FILETIME creation {}, exit_time {}, kernel {}, user {}; BOOL got_times = GetProcessTimes(handle, &creation, &exit_time, &kernel, &user); PROCESS_MEMORY_COUNTERS_EX pmc {}; BOOL got_mem = GetProcessMemoryInfo(handle, reinterpret_cast(&pmc), sizeof(pmc)); CloseHandle(handle); if (!got_times) { return false; } ULARGE_INTEGER kernel_int {}; kernel_int.HighPart = kernel.dwHighDateTime; kernel_int.LowPart = kernel.dwLowDateTime; ULARGE_INTEGER user_int {}; user_int.HighPart = user.dwHighDateTime; user_int.LowPart = user.dwLowDateTime; cpu_time = kernel_int.QuadPart + user_int.QuadPart; working_set = got_mem ? pmc.WorkingSetSize : 0; return true; } std::optional query_process_image_path_optional(DWORD pid) { std::wstring path; if (playnite_launcher::focus::get_process_image_path(pid, path)) { return path; } return std::nullopt; } std::wstring normalize_lowercase_path(const std::wstring &path) { std::wstring normalized = path; for (auto &ch : normalized) { if (ch == L'/') { ch = L'\\'; } ch = static_cast(std::towlower(ch)); } return normalized; } bool path_matches_preferred(const std::wstring &path, const std::wstring &preferred_normalized) { if (preferred_normalized.empty()) { return false; } auto normalized = normalize_lowercase_path(path); if (normalized.size() < preferred_normalized.size()) { return false; } if (normalized.compare(0, preferred_normalized.size(), preferred_normalized) != 0) { return false; } if (normalized.size() == preferred_normalized.size()) { return true; } return normalized[preferred_normalized.size()] == L'\\'; } std::optional detect_lossless_candidate(const std::unordered_set &baseline, DWORD root_pid, const std::wstring &preferred_normalized, std::atomic_bool &stop_flag) { std::unordered_map candidates; auto deadline = std::chrono::steady_clock::now() + k_lossless_observation_duration; SYSTEM_INFO sys_info {}; GetSystemInfo(&sys_info); double cpu_count = sys_info.dwNumberOfProcessors > 0 ? static_cast(sys_info.dwNumberOfProcessors) : 1.0; while (std::chrono::steady_clock::now() < deadline) { if (stop_flag.load(std::memory_order_acquire)) { return std::nullopt; } auto now = std::chrono::steady_clock::now(); auto snapshot = enumerate_process_ids_snapshot(); for (auto pid : snapshot) { if (pid == 0 || baseline.find(pid) != baseline.end()) { continue; } auto &entry = candidates[pid]; if (entry.pid == 0) { entry.pid = pid; entry.first_seen = now; entry.last_seen = now; } ULONGLONG cpu_time = 0; SIZE_T working_set = 0; if (!sample_process_usage(pid, cpu_time, working_set)) { if (entry.start_cpu == 0) { candidates.erase(pid); } continue; } if (entry.start_cpu == 0) { entry.start_cpu = cpu_time; } entry.last_cpu = cpu_time; entry.last_seen = now; if (working_set > entry.peak_working_set) { entry.peak_working_set = working_set; } if (entry.path.empty()) { if (auto path = query_process_image_path_optional(pid)) { entry.path = std::move(*path); } } } std::this_thread::sleep_for(k_lossless_poll_interval); } if (stop_flag.load(std::memory_order_acquire)) { return std::nullopt; } if (candidates.empty()) { return std::nullopt; } double max_cpu_ratio = 0.0; double max_mem = 0.0; struct candidate_score { DWORD pid; std::wstring path; double cpu_ratio; double mem_mb; bool preferred_match; }; std::vector scores; scores.reserve(candidates.size()); for (auto &[pid, candidate] : candidates) { if (candidate.start_cpu == 0 || candidate.last_cpu < candidate.start_cpu) { continue; } if (candidate.last_seen <= candidate.first_seen) { continue; } if (candidate.path.empty()) { if (auto refreshed = query_process_image_path_optional(pid)) { candidate.path = std::move(*refreshed); } } if (candidate.path.empty()) { continue; } double elapsed = std::chrono::duration(candidate.last_seen - candidate.first_seen).count(); if (elapsed <= 0.1) { elapsed = 0.1; } double cpu_seconds = static_cast(candidate.last_cpu - candidate.start_cpu) / 10000000.0; if (cpu_seconds < 0.0) { cpu_seconds = 0.0; } double cpu_ratio = cpu_seconds / (elapsed * cpu_count); if (cpu_ratio < 0.0) { cpu_ratio = 0.0; } double mem_mb = static_cast(candidate.peak_working_set) / (1024.0 * 1024.0); bool matches = path_matches_preferred(candidate.path, preferred_normalized); scores.push_back({pid, candidate.path, cpu_ratio, mem_mb, matches}); max_cpu_ratio = std::max(max_cpu_ratio, cpu_ratio); max_mem = std::max(max_mem, mem_mb); } if (scores.empty()) { return std::nullopt; } bool cpu_low = max_cpu_ratio < 0.08; double cpu_weight = cpu_low ? 0.5 : 0.7; double mem_weight = 1.0 - cpu_weight; auto ensure_dir_prefix = [](std::wstring value) { if (!value.empty() && value.back() != L'\\') { value.push_back(L'\\'); } return normalize_lowercase_path(value); }; std::wstring windows_dir_norm; { wchar_t windows_dir[MAX_PATH] = {}; UINT len = GetWindowsDirectoryW(windows_dir, ARRAYSIZE(windows_dir)); if (len > 0 && len < ARRAYSIZE(windows_dir)) { windows_dir_norm = ensure_dir_prefix(std::wstring(windows_dir, len)); } } auto has_prefix = [](const std::wstring &value, const std::wstring &prefix) { return !prefix.empty() && value.size() >= prefix.size() && value.compare(0, prefix.size(), prefix) == 0; }; const candidate_score *best = nullptr; double best_score = -1.0; for (const auto &score : scores) { double cpu_norm = max_cpu_ratio > 0.0 ? score.cpu_ratio / max_cpu_ratio : 0.0; double mem_norm = max_mem > 0.0 ? score.mem_mb / max_mem : 0.0; double combined = (cpu_weight * cpu_norm) + (mem_weight * mem_norm); if (score.preferred_match) { combined += 0.2; } if (score.pid == root_pid) { combined += score.preferred_match ? 0.05 : -0.05; } combined += std::min(score.cpu_ratio, 1.0) * 0.15; if (!windows_dir_norm.empty()) { auto normalized_path = normalize_lowercase_path(score.path); bool system_path = has_prefix(normalized_path, windows_dir_norm); if (system_path) { combined -= 0.2; } if (system_path && score.cpu_ratio < 0.02 && score.mem_mb < 48.0) { combined -= 0.05; } } else if (score.cpu_ratio < 0.015 && score.mem_mb < 32.0) { combined -= 0.05; } if (combined > best_score) { best_score = combined; best = &score; } } if (!best) { return std::nullopt; } lossless_selection selection; selection.pid = best->pid; selection.path_wide = best->path; selection.path_utf8 = platf::dxgi::wide_to_utf8(best->path); std::filesystem::path fs_path(best->path); auto parent = fs_path.parent_path(); if (!parent.empty()) { selection.directory_utf8 = platf::dxgi::wide_to_utf8(parent.wstring()); } BOOST_LOG(debug) << "Lossless Scaling: candidate PID=" << selection.pid << " exe=" << selection.path_utf8 << " cpu=" << best->cpu_ratio << " memMB=" << best->mem_mb; return selection; } #endif } // namespace proc_t proc; int input_only_app_id = -1; std::string input_only_app_id_str; int terminate_app_id = -1; std::string terminate_app_id_str; #ifdef _WIN32 std::atomic vDisplayDriverStatus {VDISPLAY::DRIVER_STATUS::UNKNOWN}; std::atomic vDisplayDriverSelection {VDISPLAY::DRIVER_SELECTION::UNKNOWN}; std::mutex vdisplay_driver_status_mutex; namespace { lifecycle::deferred_action_t deferred_display_revert; } void defer_display_revert() { deferred_display_revert.defer(); } bool consume_deferred_display_revert() { return deferred_display_revert.consume(); } void clear_deferred_display_revert() { deferred_display_revert.clear(); } vdisplay_driver_status_snapshot_t vDisplayDriverStatusSnapshot() { std::lock_guard lock(vdisplay_driver_status_mutex); return { vDisplayDriverStatus.load(std::memory_order_acquire), vDisplayDriverSelection.load(std::memory_order_acquire), }; } void setVDisplayDriverStatus(const VDISPLAY::DRIVER_STATUS status) { std::lock_guard lock(vdisplay_driver_status_mutex); vDisplayDriverStatus.store(status, std::memory_order_release); } void setVDisplayDriverStatus( const VDISPLAY::DRIVER_STATUS status, const VDISPLAY::DRIVER_SELECTION selection ) { std::lock_guard lock(vdisplay_driver_status_mutex); vDisplayDriverSelection.store(selection, std::memory_order_release); vDisplayDriverStatus.store(status, std::memory_order_release); } void onVDisplayWatchdogFailed() { setVDisplayDriverStatus(VDISPLAY::DRIVER_STATUS::WATCHDOG_FAILED); VDISPLAY::closeVDisplayDevice(); } void initVDisplayDriver() { VDISPLAY::ensureVirtualDisplayRegistryDefaults(); setVDisplayDriverStatus( VDISPLAY::DRIVER_STATUS::UNKNOWN, VDISPLAY::DRIVER_SELECTION::UNKNOWN ); if (!VDISPLAY::ensure_driver_is_ready()) { BOOST_LOG(warning) << "Sunshine virtual display driver reported unavailable during initialization; attempting to continue."; } const auto status = VDISPLAY::openVDisplayDevice(); if (status == VDISPLAY::DRIVER_STATUS::OK) { if (!VDISPLAY::startPingThread(onVDisplayWatchdogFailed)) { onVDisplayWatchdogFailed(); return; } } } #endif // Custom move operations to allow global proc replacement if ever needed proc_t::proc_t(proc_t &&other) noexcept: _app_id(other._app_id.load(std::memory_order_acquire)), _env(std::move(other._env)), _stream_owned_environment_keys(std::move(other._stream_owned_environment_keys)), _apps(std::move(other._apps)), _app(std::move(other._app)), _app_launch_time(other._app_launch_time), _active_client_uuid(std::move(other._active_client_uuid)), _active_client_vdd_identity_token(other._active_client_vdd_identity_token), placebo(other.placebo), _steam_tracker(std::move(other._steam_tracker)), _steam_process_controller(std::move(other._steam_process_controller)), _steam_tracking_active(other._steam_tracking_active), _steam_tracking_associated(other._steam_tracking_associated), _steam_tracking_deadline(other._steam_tracking_deadline), _steam_last_tracking_poll(other._steam_last_tracking_poll), _process(std::move(other._process)), _process_group(std::move(other._process_group)), #ifdef _WIN32 _virtual_display_guid(other._virtual_display_guid), _virtual_display_active(other._virtual_display_active), _runtime_output_override_lease(std::exchange(other._runtime_output_override_lease, std::nullopt)), #endif _pipe(std::move(other._pipe)), _app_prep_it(other._app_prep_it), _app_prep_begin(other._app_prep_begin) #ifdef _WIN32 , _lossless_thread(std::move(other._lossless_thread)), _lossless_profile_applied(other._lossless_profile_applied), _lossless_backup(other._lossless_backup), _lossless_last_install_dir(std::move(other._lossless_last_install_dir)), _lossless_last_exe_path(std::move(other._lossless_last_exe_path)) #endif { #ifdef _WIN32 _lossless_stop_requested.store(other._lossless_stop_requested.load(std::memory_order_acquire), std::memory_order_release); other._lossless_profile_applied = false; #endif } proc_t &proc_t::operator=(proc_t &&other) noexcept { if (this != &other) { std::scoped_lock lk(_apps_mutex, other._apps_mutex); #ifdef _WIN32 stop_lossless_scaling_support(); #endif _app_id.store(other._app_id.load(std::memory_order_acquire), std::memory_order_release); _env = std::move(other._env); _stream_owned_environment_keys = std::move(other._stream_owned_environment_keys); _apps = std::move(other._apps); _app = std::move(other._app); _app_launch_time = other._app_launch_time; _active_client_uuid = std::move(other._active_client_uuid); _active_client_vdd_identity_token = other._active_client_vdd_identity_token; placebo = other.placebo; _steam_tracker = std::move(other._steam_tracker); _steam_process_controller = std::move(other._steam_process_controller); _steam_tracking_active = other._steam_tracking_active; _steam_tracking_associated = other._steam_tracking_associated; _steam_tracking_deadline = other._steam_tracking_deadline; _steam_last_tracking_poll = other._steam_last_tracking_poll; _process = std::move(other._process); _process_group = std::move(other._process_group); _pipe = std::move(other._pipe); _app_prep_it = other._app_prep_it; _app_prep_begin = other._app_prep_begin; #ifdef _WIN32 if (_runtime_output_override_lease) { (void) config::clear_runtime_output_name_override_if_lease(*_runtime_output_override_lease); } _runtime_output_override_lease = std::exchange(other._runtime_output_override_lease, std::nullopt); _lossless_thread = std::move(other._lossless_thread); _lossless_stop_requested.store(other._lossless_stop_requested.load(std::memory_order_acquire), std::memory_order_release); _lossless_profile_applied = other._lossless_profile_applied; _lossless_backup = other._lossless_backup; _lossless_last_install_dir = std::move(other._lossless_last_install_dir); _lossless_last_exe_path = std::move(other._lossless_last_exe_path); _virtual_display_guid = other._virtual_display_guid; _virtual_display_active = other._virtual_display_active; other._lossless_profile_applied = false; #endif } return *this; } #ifdef _WIN32 void proc_t::start_lossless_scaling_support(std::unordered_set baseline_pids, const playnite_launcher::lossless::lossless_scaling_app_metadata &metadata, std::string install_dir_hint_utf8, DWORD root_pid) { stop_lossless_scaling_support(); _lossless_stop_requested.store(false, std::memory_order_release); _lossless_thread = std::thread([this, baseline = std::move(baseline_pids), metadata, install_dir_hint = std::move(install_dir_hint_utf8), root_pid]() mutable { try { std::wstring preferred_directory; if (!install_dir_hint.empty()) { preferred_directory = platf::dxgi::utf8_to_wide(install_dir_hint); preferred_directory = normalize_lowercase_path(preferred_directory); while (!preferred_directory.empty() && preferred_directory.back() == L'\\') { preferred_directory.pop_back(); } } BOOST_LOG(debug) << "Lossless Scaling: monitoring for new processes (baseline=" << baseline.size() << ", root_pid=" << root_pid << ")"; auto selection = detect_lossless_candidate(baseline, root_pid, preferred_directory, _lossless_stop_requested); if (!selection || _lossless_stop_requested.load(std::memory_order_acquire)) { BOOST_LOG(debug) << "Lossless Scaling: no candidate detected within bootstrap window"; return; } const int launch_delay_seconds = std::max(0, metadata.launch_delay_seconds); if (launch_delay_seconds > 0) { BOOST_LOG(info) << "Lossless Scaling: delaying launch by " << launch_delay_seconds << " seconds after game start"; auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(launch_delay_seconds); while (std::chrono::steady_clock::now() < deadline) { if (_lossless_stop_requested.load(std::memory_order_acquire)) { return; } std::this_thread::sleep_for(std::chrono::milliseconds(200)); } } auto options = playnite_launcher::lossless::read_lossless_scaling_options(metadata); if (!options.enabled) { BOOST_LOG(debug) << "Lossless Scaling: disabled by configuration, skipping auto launch"; return; } auto runtime = playnite_launcher::lossless::capture_lossless_scaling_state(); #ifdef _WIN32 if (!runtime.exe_path && metadata.configured_path) { try { runtime.exe_path = metadata.configured_path->wstring(); } catch (...) { } } #endif if (_lossless_stop_requested.load(std::memory_order_acquire)) { return; } if (!runtime.running_pids.empty()) { playnite_launcher::lossless::lossless_scaling_stop_processes(runtime); } playnite_launcher::lossless::lossless_scaling_profile_backup backup; std::string install_dir = install_dir_hint.empty() ? selection->directory_utf8 : install_dir_hint; bool changed = playnite_launcher::lossless::lossless_scaling_apply_global_profile(options, install_dir, selection->path_utf8, backup); { std::lock_guard lk(_lossless_mutex); _lossless_backup = backup; _lossless_profile_applied = backup.valid; if (_lossless_profile_applied) { _lossless_last_install_dir = install_dir; _lossless_last_exe_path = selection->path_utf8; } else { _lossless_last_install_dir.clear(); _lossless_last_exe_path.clear(); } } if (_lossless_stop_requested.load(std::memory_order_acquire)) { return; } playnite_launcher::lossless::lossless_scaling_restart_foreground( runtime, changed, install_dir, selection->path_utf8, selection->pid, metadata.legacy_auto_detect ); BOOST_LOG(info) << "Lossless Scaling: launched helper for PID=" << selection->pid << " (" << selection->path_utf8 << ')'; } catch (const std::exception &e) { BOOST_LOG(warning) << "Lossless Scaling: exception during auto launch: " << e.what(); } catch (...) { BOOST_LOG(warning) << "Lossless Scaling: unknown exception during auto launch"; } }); } void proc_t::stop_lossless_scaling_support() { _lossless_stop_requested.store(true, std::memory_order_release); if (_lossless_thread.joinable()) { _lossless_thread.join(); } _lossless_stop_requested.store(false, std::memory_order_release); bool restore = false; playnite_launcher::lossless::lossless_scaling_profile_backup backup; { std::lock_guard lk(_lossless_mutex); if (_lossless_profile_applied) { backup = _lossless_backup; restore = backup.valid; _lossless_profile_applied = false; } _lossless_last_install_dir.clear(); _lossless_last_exe_path.clear(); } if (restore) { auto runtime = playnite_launcher::lossless::capture_lossless_scaling_state(); if (!runtime.running_pids.empty()) { playnite_launcher::lossless::lossless_scaling_stop_processes(runtime); } if (playnite_launcher::lossless::lossless_scaling_restore_global_profile(backup)) { BOOST_LOG(info) << "Lossless Scaling: restored previous profile"; } } } #endif class deinit_t: public platf::deinit_t { public: deinit_t() { #ifdef _WIN32 playnite_integration_ = platf::playnite::start(); if (!playnite_integration_) { BOOST_LOG(error) << "Playnite integration failed to initialize"; } #endif } ~deinit_t() { proc.terminate(); } private: #ifdef _WIN32 std::unique_ptr playnite_integration_; #endif }; std::unique_ptr init() { return std::make_unique(); } void terminate_process_group(bp::child &proc, bp::group &group, std::chrono::seconds exit_timeout) { if (group.valid() && platf::process_group_running((std::uintptr_t) group.native_handle())) { if (exit_timeout.count() > 0) { // Request processes in the group to exit gracefully if (platf::request_process_group_exit((std::uintptr_t) group.native_handle())) { // If the request was successful, wait for a little while for them to exit. BOOST_LOG(info) << "Successfully requested the app to exit. Waiting up to "sv << exit_timeout.count() << " seconds for it to close."sv; // group::wait_for() and similar functions are broken and deprecated, so we use a simple polling loop while (platf::process_group_running((std::uintptr_t) group.native_handle()) && (--exit_timeout).count() >= 0) { std::this_thread::sleep_for(1s); } if (exit_timeout.count() < 0) { BOOST_LOG(warning) << "App did not fully exit within the timeout. Terminating the app's remaining processes."sv; } else { BOOST_LOG(info) << "All app processes have successfully exited."sv; } } else { BOOST_LOG(info) << "App did not respond to a graceful termination request. Forcefully terminating the app's processes."sv; } } else { BOOST_LOG(info) << "No graceful exit timeout was specified for this app. Forcefully terminating the app's processes."sv; } // We always call terminate() even if we waited successfully for all processes above. // This ensures the process group state is consistent with the OS in boost. std::error_code ec; group.terminate(ec); group.detach(); } if (proc.valid()) { // avoid zombie process proc.detach(); } } boost::filesystem::path find_working_directory(const std::string &cmd, const bp::environment &env [[maybe_unused]]) { // Parse the raw command string into parts to get the actual command portion #ifdef _WIN32 auto parts = boost::program_options::split_winmain(cmd); #else auto parts = boost::program_options::split_unix(cmd); #endif if (parts.empty()) { BOOST_LOG(error) << "Unable to parse command: "sv << cmd; return boost::filesystem::path(); } BOOST_LOG(debug) << "Parsed target ["sv << parts.at(0) << "] from command ["sv << cmd << ']'; // If the target is a URL, don't parse any further here if (parts.at(0).find("://") != std::string::npos) { return boost::filesystem::path(); } // If the cmd path is not an absolute path, resolve it using our PATH variable boost::filesystem::path cmd_path(parts.at(0)); if (!cmd_path.is_absolute()) { auto resolved = bp::search_path(parts.at(0)); cmd_path = boost::filesystem::path(resolved.string()); if (cmd_path.empty()) { BOOST_LOG(error) << "Unable to find executable ["sv << parts.at(0) << "]. Is it in your PATH?"sv; return boost::filesystem::path(); } } BOOST_LOG(debug) << "Resolved target ["sv << parts.at(0) << "] to path ["sv << cmd_path << ']'; // Now that we have a complete path, we can just use parent_path() return cmd_path.parent_path(); } void proc_t::launch_input_only() { _app_id = input_only_app_id; _app_name = "Remote Input"; _app.uuid = REMOTE_INPUT_UUID; _app.terminate_on_pause = true; allow_client_commands = false; placebo = true; #if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 system_tray::update_tray_playing(_app_name); #endif } int proc_t::execute(const ctx_t &app, std::shared_ptr launch_session) { #ifdef _WIN32 std::optional resolved_lossless_exe_path; std::string resolved_lossless_exe_utf8; _virtual_display_active = false; _virtual_display_guid = GUID {}; _deferred_launch = false; _lossless_should_start_support = false; _lossless_metadata = {}; #endif if (_app_id == input_only_app_id) { terminate(false, false, false, true); std::this_thread::sleep_for(1s); } else { // Ensure starting from a clean slate const bool skip_display_revert = launch_session && launch_session->display_config_preapplied; terminate(false, false, skip_display_revert, true); } #ifdef __linux__ // proc_t retains its parsed environment across launches. Remove only the // values that a prior stream supplied; never erase an apps.json value. for (const auto &key : _stream_owned_environment_keys) { _env[key] = ""; } _stream_owned_environment_keys.clear(); #endif _app = app; _app_id = util::from_view(app.id); audio::app_started(); #ifdef _WIN32 // A replacement app owns the streaming display configuration. Any // restore deferred by the previous app must not fire at this session's end. clear_deferred_display_revert(); #endif _app_name = app.name; _launch_session = launch_session; _active_client_uuid = launch_session ? launch_session->client_uuid : std::string(); _active_client_vdd_identity_token = launch_session ? launch_session->normal_vdd_identity_token : 0; allow_client_commands = app.allow_client_commands; launch_session->gen1_framegen_fix = _app.gen1_framegen_fix; launch_session->gen2_framegen_fix = _app.gen2_framegen_fix; launch_session->frame_generation_enabled = _app.frame_generation_enabled; launch_session->lossless_scaling_framegen = _app.lossless_scaling_framegen; launch_session->lossless_scaling_target_fps = _app.lossless_scaling_target_fps; launch_session->lossless_scaling_rtss_limit = _app.lossless_scaling_rtss_limit; launch_session->frame_generation_provider = _app.frame_generation_provider; // Web UI launches do not resolve the app through make_launch_session(). // Carry app display policy into the session here so every launch path makes // the same physical-versus-virtual decision before creating a display. if (_app.output_name_override) { launch_session->output_name_override = _app.output_name_override; } if (!launch_session->virtual_display_mode_override && _app.virtual_display_mode_override) { launch_session->virtual_display_mode_override = _app.virtual_display_mode_override; } if (!launch_session->dd_config_option_override && _app.dd_config_option_override) { launch_session->dd_config_option_override = _app.dd_config_option_override; } std::optional effective_lossless_target = launch_session->lossless_scaling_target_fps; if ( (!effective_lossless_target || *effective_lossless_target <= 0) && launch_session->fps > 0 ) { effective_lossless_target = (double) launch_session->fps / 1000.0; } launch_session->lossless_scaling_target_fps = effective_lossless_target; std::optional effective_lossless_rtss = launch_session->lossless_scaling_rtss_limit; if ( (!effective_lossless_rtss || *effective_lossless_rtss <= 0) && effective_lossless_target && *effective_lossless_target > 0 ) { int computed_limit = (int) std::lround(*effective_lossless_target * 0.5); if (computed_limit > 0) { effective_lossless_rtss = computed_limit; } else { effective_lossless_rtss.reset(); } } launch_session->lossless_scaling_rtss_limit = effective_lossless_rtss; _app_prep_begin = std::begin(_app.prep_cmds); _app_prep_it = _app_prep_begin; const bool has_resolution_override = launch_session->resolution_override.has_value(); uint32_t client_width = has_resolution_override ? static_cast(launch_session->resolution_override->width) : (launch_session->width ? launch_session->width : 1920); uint32_t client_height = has_resolution_override ? static_cast(launch_session->resolution_override->height) : (launch_session->height ? launch_session->height : 1080); uint32_t render_width = client_width; uint32_t render_height = client_height; int scale_factor = launch_session->scale_factor; if (_app.scale_factor != 100) { scale_factor = _app.scale_factor; } if (scale_factor <= 0) { scale_factor = 100; } if (!has_resolution_override && scale_factor != 100) { render_width *= ((float) scale_factor / 100); render_height *= ((float) scale_factor / 100); // Chop the last bit to ensure the scaled resolution is even numbered // Most odd resolutions won't work well render_width &= ~1; render_height &= ~1; } launch_session->width = render_width; launch_session->height = render_height; this->initial_display = config::video.output_name; if (!app.gamepad.empty()) { _saved_input_config = std::make_shared(config::input); if (app.gamepad == "disabled") { config::input.controller = false; } else { config::input.controller = true; config::input.gamepad = app.gamepad; } } #ifdef _WIN32 bool already_has_virtual_guid = std::any_of( launch_session->virtual_display_guid_bytes.begin(), launch_session->virtual_display_guid_bytes.end(), [](std::uint8_t b) { return b != 0; } ); // Preserve an upstream resolver's decision. For Web UI and WebRTC launches, // resolve the same app/client display policy locally before any VDD exists. bool should_use_virtual_display = launch_session->virtual_display; if (!launch_session->virtual_display_request_resolved) { using dd_config_option_e = config::video_t::dd_t::config_option_e; const auto dd_config_option = launch_session->dd_config_option_override.value_or(config::video.dd.configuration_option); const bool forced_sudavda_virtual_display = config::video.output_name == VDISPLAY::SUDOVDA_VIRTUAL_DISPLAY_SELECTION; const auto effective_virtual_display_mode = launch_session->virtual_display_mode_override.value_or(config::video.virtual_display_mode); const bool headless_mode = effective_virtual_display_mode != config::video_t::virtual_display_mode_e::disabled; const bool dd_conflicts_with_virtual_display = dd_config_option == dd_config_option_e::ensure_only_display && dd_config_option != dd_config_option_e::disabled && !headless_mode; const bool metadata_requests_virtual = launch_session->app_metadata && launch_session->app_metadata->virtual_screen; const bool app_requests_virtual = _app.virtual_display || _app.virtual_screen; const bool client_requests_virtual = launch_session->client_requests_virtual_display; const bool session_requests_virtual = launch_session->virtual_display; std::optional output_override; if (launch_session->output_name_override) { output_override = boost::algorithm::trim_copy(*launch_session->output_name_override); } const bool output_selects_virtual = output_override && !output_override->empty() && VDISPLAY::is_virtual_display_selection(*output_override); const bool output_selects_physical = output_override && (output_override->empty() || !output_selects_virtual); const auto framegen_policy = framegen::make_stream_start_policy({ .fps = launch_session->fps, .fps_scaled = launch_session->fps, .display_refresh_millihz = launch_session->client_display_refresh_millihz, .frame_generation_enabled = launch_session->frame_generation_enabled, .gen1_framegen_fix = launch_session->gen1_framegen_fix, .gen2_framegen_fix = launch_session->gen2_framegen_fix, .lossless_scaling_framegen = launch_session->lossless_scaling_framegen, .lossless_rtss_limit = launch_session->lossless_scaling_rtss_limit, .frame_generation_provider = launch_session->frame_generation_provider, .uses_virtual_display = output_selects_physical ? false : (session_requests_virtual || headless_mode || app_requests_virtual || output_selects_virtual), .capture_mode = config::video.capture, .auto_capture_uses_wgc = platf::dxgi::should_use_wgc_default(), .auto_virtual_framegen_limiter = config::frame_limiter.virtual_display_limiter_enabled(), .virtual_display_refresh_multiplier = config::frame_limiter.fixed_virtual_display_refresh_multiplier(), }); const bool framegen_requires_virtual = framegen_policy.requires_virtual_display; if (forced_sudavda_virtual_display || output_selects_virtual) { launch_session->virtual_display = true; } if (output_selects_physical && !framegen_requires_virtual) { launch_session->virtual_display = false; launch_session->virtual_display_failed = false; launch_session->virtual_display_guid_bytes.fill(0); launch_session->virtual_display_device_id.clear(); launch_session->virtual_display_ready_since.reset(); already_has_virtual_guid = false; should_use_virtual_display = false; _runtime_output_override_lease = config::set_runtime_output_name_override_with_lease(*output_override); } else { should_use_virtual_display = headless_mode || app_requests_virtual || metadata_requests_virtual || client_requests_virtual || session_requests_virtual || output_selects_virtual || framegen_requires_virtual || !video::allow_encoder_probing() || VDISPLAY::should_auto_enable_virtual_display(); if (should_use_virtual_display && dd_conflicts_with_virtual_display && !forced_sudavda_virtual_display) { if (session_requests_virtual || app_requests_virtual || client_requests_virtual) { BOOST_LOG(info) << "Skipping virtual display activation because display device configuration is set to ensure-only-display."; } launch_session->virtual_display = false; should_use_virtual_display = headless_mode || !video::allow_encoder_probing(); } } launch_session->virtual_display_request_resolved = true; } bool dd_api_handled = false; // Display helper APPLY is handled in nvhttp to avoid duplicate helper restarts. if (should_use_virtual_display && !dd_api_handled && !already_has_virtual_guid) { if (vDisplayDriverStatus.load(std::memory_order_acquire) != VDISPLAY::DRIVER_STATUS::OK) { initVDisplayDriver(); } if (vDisplayDriverStatus.load(std::memory_order_acquire) == VDISPLAY::DRIVER_STATUS::OK) { if (!config::video.adapter_name.empty()) { (void) VDISPLAY::setRenderAdapterByName(platf::from_utf8(config::video.adapter_name)); } else { (void) VDISPLAY::setRenderAdapterWithMostDedicatedMemory(); } std::string device_name; std::string device_uuid_str; uuid_util::uuid_t device_uuid; const auto effective_virtual_display_mode = launch_session->virtual_display_mode_override.value_or(config::video.virtual_display_mode); const bool use_shared_display = effective_virtual_display_mode == config::video_t::virtual_display_mode_e::shared; if (use_shared_display) { if (http::shared_virtual_display_guid.empty()) { device_uuid = uuid_util::uuid_t::generate(); device_uuid_str = device_uuid.string(); http::shared_virtual_display_guid = device_uuid_str; nvhttp::save_state(); BOOST_LOG(info) << "Generated new shared virtual display GUID: " << device_uuid_str; } else { device_uuid_str = http::shared_virtual_display_guid; device_uuid = uuid_util::uuid_t::parse(device_uuid_str); BOOST_LOG(info) << "Reusing shared virtual display GUID: " << device_uuid_str; } device_name = config::nvhttp.sunshine_name.empty() ? "Sunshine Shared Display" : config::nvhttp.sunshine_name + " Shared"; } else if (_app.use_app_identity) { device_name = _app.name; if (_app.per_client_app_identity) { device_uuid = uuid_util::uuid_t::parse(launch_session->unique_id); auto app_uuid = uuid_util::uuid_t::parse(_app.uuid); device_uuid.b64[0] ^= app_uuid.b64[0]; device_uuid.b64[1] ^= app_uuid.b64[1]; device_uuid_str = device_uuid.string(); } else { device_uuid_str = _app.uuid; device_uuid = uuid_util::uuid_t::parse(_app.uuid); } } else { device_name = !launch_session->device_name.empty() ? launch_session->device_name : config::nvhttp.sunshine_name; if (device_name.empty()) { device_name = "Sunshine"; } device_uuid_str = !launch_session->client_uuid.empty() ? launch_session->client_uuid : launch_session->unique_id; device_uuid = uuid_util::uuid_t::parse(device_uuid_str); } GUID display_guid {}; std::memcpy(&display_guid, device_uuid.b8, sizeof(display_guid)); std::copy_n(device_uuid.b8, launch_session->virtual_display_guid_bytes.size(), launch_session->virtual_display_guid_bytes.begin()); uint32_t target_fps = rtsp_stream::effective_display_refresh_millihz(*launch_session); if (target_fps == 0) { target_fps = 60000u; } const uint32_t base_fps_millihz = launch_session->client_display_refresh_millihz > 0 ? launch_session->client_display_refresh_millihz : framegen::normalize_refresh_millihz(launch_session->fps); const bool framegen_refresh_active = (launch_session->framegen_refresh_millihz && *launch_session->framegen_refresh_millihz > 0) || (launch_session->framegen_refresh_rate && *launch_session->framegen_refresh_rate > 0); // Virtual displays always run at 4x the requested refresh (or the highest the driver // can provide) so frame pacing stays smooth; frame generation reuses the same target. const int refresh_multiplier = std::max( 4, framegen_refresh_active ? rtsp_stream::framegen_refresh_multiplier(*launch_session) : 1 ); const char *hdr_profile = launch_session->hdr_profile ? launch_session->hdr_profile->c_str() : nullptr; auto display_info = VDISPLAY::createVirtualDisplay( device_uuid_str.c_str(), device_name.c_str(), hdr_profile, render_width, render_height, target_fps, display_guid, base_fps_millihz, framegen_refresh_active, refresh_multiplier ); if (display_info) { const std::wstring *display_name_w = display_info->display_name && !display_info->display_name->empty() ? &*display_info->display_name : nullptr; launch_session->virtual_display = true; this->virtual_display = true; if (display_name_w) { this->display_name = platf::to_utf8(*display_name_w); config::video.output_name = this->display_name; } else { this->display_name.clear(); config::video.output_name.clear(); } if (display_info->device_id && !display_info->device_id->empty()) { launch_session->virtual_display_device_id = *display_info->device_id; } else if (display_name_w) { if (auto resolved_device = VDISPLAY::resolveVirtualDisplayDeviceId(*display_name_w)) { launch_session->virtual_display_device_id = *resolved_device; } else { launch_session->virtual_display_device_id.clear(); } } else if (auto resolved_device = VDISPLAY::resolveAnyVirtualDisplayDeviceId()) { launch_session->virtual_display_device_id = *resolved_device; } else { launch_session->virtual_display_device_id.clear(); } std::memcpy(&_virtual_display_guid, &display_guid, sizeof(_virtual_display_guid)); _virtual_display_active = true; } else { BOOST_LOG(warning) << "Virtual display creation failed."; } } else { BOOST_LOG(warning) << "SudoVDA driver unavailable (status=" << static_cast(vDisplayDriverStatus.load(std::memory_order_acquire)) << ")"; } } else if (already_has_virtual_guid) { std::memcpy(&_virtual_display_guid, launch_session->virtual_display_guid_bytes.data(), sizeof(_virtual_display_guid)); _virtual_display_active = true; } if (this->virtual_display) { display_helper_integration::reset_persistence(); } #endif // _WIN32 std::string fps_str; char fps_buf[8]; snprintf(fps_buf, sizeof(fps_buf), "%.3f", (float) launch_session->fps / 1000.0f); fps_str = fps_buf; const std::string fps_scaled_str = std::to_string(launch_session->fps); // Add Stream-specific environment variables // Sunshine Compatibility _env["SUNSHINE_APP_ID"] = _app.id; _env["SUNSHINE_APP_NAME"] = _app.name; _env["SUNSHINE_CLIENT_WIDTH"] = std::to_string(render_width); _env["SUNSHINE_CLIENT_HEIGHT"] = std::to_string(render_height); _env["SUNSHINE_CLIENT_FPS"] = config::sunshine.envvar_compatibility_mode ? std::to_string(std::round((float) launch_session->fps / 1000.0f)) : fps_str; const bool stream_hdr = rtsp_stream::effective_hdr_requested(*launch_session); _env["SUNSHINE_CLIENT_HDR"] = stream_hdr ? "true" : "false"; #ifdef __linux__ // The launched app renders the *source*, not the stream. Under synthesized HDR the source // must stay SDR, otherwise TrueHDR converts pixels that are already HDR a second time. const bool source_hdr = rtsp_stream::hdr_capture_required(*launch_session, config::video); const auto wayland_hdr_compatibility = platf::wayland_hdr_compatibility::resolve( config::video.dd.wayland_hdr_compatibility, platf::wayland_hdr_compatibility::selected_session_is_wayland( window_system == window_system_e::WAYLAND ), source_hdr ); auto set_stream_environment_default = [&](const char *key, const char *value) { if (!_env[key].to_string().empty()) { return false; } _env[key] = value; _stream_owned_environment_keys.emplace(key); return true; }; if (config::input.proton_dualsense_compatibility) { (void) set_stream_environment_default("PROTON_KEEP_SONY_AUDIO_ENDPOINT_VISIBLE", "1"); (void) set_stream_environment_default("PROTON_SONY_WINDOWS_DEVICE_NAMES", "1"); } if (wayland_hdr_compatibility.enabled) { (void) set_stream_environment_default("ENABLE_HDR_WSI", "1"); BOOST_LOG(info) << "Wayland HDR compatibility enabled for application launch."; BOOST_LOG(debug) << "Wayland HDR compatibility applied ENABLE_HDR_WSI to the launch environment."; } else if (config::video.dd.wayland_hdr_compatibility) { if (launch_session->prefer_sdr_10bit) { BOOST_LOG(debug) << "Wayland HDR compatibility skipped: 10-bit SDR preferred."; } else if (launch_session->force_sdr) { BOOST_LOG(debug) << "Wayland HDR compatibility skipped: HDR disabled by the final session/display policy."; } else if (wayland_hdr_compatibility.suppression_reason == platf::wayland_hdr_compatibility::suppression_reason_e::not_wayland) { BOOST_LOG(debug) << "Wayland HDR compatibility skipped: the active session is not Wayland."; } else { BOOST_LOG(debug) << "Wayland HDR compatibility skipped: session resolved to SDR."; } } #endif _env["SUNSHINE_CLIENT_GCMAP"] = std::to_string(launch_session->gcmap); _env["SUNSHINE_CLIENT_HOST_AUDIO"] = launch_session->host_audio ? "true" : "false"; _env["SUNSHINE_CLIENT_ENABLE_SOPS"] = launch_session->enable_sops ? "true" : "false"; _env["APOLLO_APP_ID"] = _app.id; _env["APOLLO_APP_NAME"] = _app.name; _env["APOLLO_APP_UUID"] = _app.uuid; _env["APOLLO_APP_STATUS"] = "STARTING"; _env["APOLLO_CLIENT_UUID"] = !launch_session->client_uuid.empty() ? launch_session->client_uuid : launch_session->unique_id; _env["APOLLO_CLIENT_NAME"] = launch_session->device_name; _env["APOLLO_CLIENT_WIDTH"] = std::to_string(render_width); _env["APOLLO_CLIENT_HEIGHT"] = std::to_string(render_height); _env["APOLLO_CLIENT_RENDER_WIDTH"] = std::to_string(launch_session->width); _env["APOLLO_CLIENT_RENDER_HEIGHT"] = std::to_string(launch_session->height); _env["APOLLO_CLIENT_SCALE_FACTOR"] = std::to_string(scale_factor); _env["APOLLO_CLIENT_FPS"] = fps_scaled_str; _env["APOLLO_CLIENT_HDR"] = rtsp_stream::effective_hdr_requested(*launch_session) ? "true" : "false"; _env["APOLLO_CLIENT_GCMAP"] = std::to_string(launch_session->gcmap); _env["APOLLO_CLIENT_HOST_AUDIO"] = launch_session->host_audio ? "true" : "false"; _env["APOLLO_CLIENT_ENABLE_SOPS"] = launch_session->enable_sops ? "true" : "false"; #ifdef __linux__ const bool mangohud_uses_lossless_limit = _app.lossless_scaling_framegen && boost::iequals(_app.frame_generation_provider, "lossless-scaling"); const auto mangohud_stream_policy = rtsp_stream::make_framegen_stream_start_policy( *launch_session, mangohud_uses_lossless_limit ? launch_session->lossless_scaling_rtss_limit : std::nullopt, config::video.capture, false, config::frame_limiter.virtual_display_limiter_enabled(), config::frame_limiter.fixed_virtual_display_refresh_multiplier(), config::frame_limiter.fixed_virtual_display_refresh_millihz(launch_session->client_vrr_requested) ); const auto mangohud_policy = platf::mangohud::make_launch_policy( config::frame_limiter.provider, config::frame_limiter.enable, config::frame_limiter.virtual_display_limiter_enabled(), mangohud_stream_policy, config::frame_limiter.fps_limit_millihz ); const bool proton_limiter = platf::mangohud::proton_provider_selected(config::frame_limiter.provider); const bool proton_overlay_requested = platf::mangohud::proton_overlay_provider_selected(config::frame_limiter.provider); const bool mangohud_available = !bp::search_path("mangohud").empty(); const auto smooth_motion_policy = platf::smooth_motion::make_launch_policy( _app.frame_generation_enabled, _app.frame_generation_provider, mangohud_policy.enabled && !proton_limiter ); #ifdef SUNSHINE_BUILD_GAMESCOPE const bool gamescope_steam_launch = !_app.steam_id.empty() && platf::gamescope_capture_selected(); #else const bool gamescope_steam_launch = false; #endif if (gamescope_steam_launch) { // Catalog commands may have been generated in Desktop Mode and contain // a direct Proton launch. Resolve ownership at launch time so Steam can // manage the game's window, focus and runtime in Gaming Mode. _app.cmd = platf::steam::runtime_launch_command(_app.steam_id, _app.cmd, true); if (_app.cmd.empty()) { BOOST_LOG(error) << "Invalid Steam app ID for Gaming Mode launch: [" << _app.steam_id << "]."; return 400; } BOOST_LOG(info) << "Gaming Mode: handing Steam app " << _app.steam_id << " to the running Steam client."; if (smooth_motion_policy.enabled) { BOOST_LOG(warning) << "Smooth Motion cannot be injected through the running Gaming Mode Steam client."; } } bool inherited_steam_environment_ready = !gamescope_steam_launch; bool session_steam_direct_launch = false; bool steam_proton_launch = false; const bool effective_frame_limiter = mangohud_policy.enabled && (proton_limiter || mangohud_available); const bool direct_steam_launch_required = !gamescope_steam_launch && !_app.steam_id.empty() && (config::input.proton_dualsense_compatibility || platf::steam::requires_direct_environment_launch( effective_frame_limiter, smooth_motion_policy.enabled, stream_hdr )); if (direct_steam_launch_required) { inherited_steam_environment_ready = false; std::uint32_t steam_app_id = 0; const auto *steam_id_begin = _app.steam_id.data(); const auto *steam_id_end = steam_id_begin + _app.steam_id.size(); const auto parsed_steam_id = std::from_chars(steam_id_begin, steam_id_end, steam_app_id); if (parsed_steam_id.ec == std::errc {} && parsed_steam_id.ptr == steam_id_end && steam_app_id != 0) { const auto games = platf::steam::discover(); const auto game = std::find_if(games.begin(), games.end(), [steam_app_id](const auto &candidate) { return candidate.app_id == steam_app_id && candidate.installed; }); if (game != games.end()) { steam_proton_launch = game->launch_os == "windows"; if (std::getenv("ARTLIGHT_MACHINE_HOST")) { const bool proton_overlay_enabled = effective_frame_limiter && proton_limiter && proton_overlay_requested && mangohud_available; const bool mangohud_limiter_enabled = effective_frame_limiter && !proton_limiter; platf::steam::session_launch_policy_t policy { .provider = proton_overlay_enabled ? "mangohud-proton" : (effective_frame_limiter && proton_limiter ? "proton" : (mangohud_limiter_enabled ? "mangohud" : "disabled")), .limit_millihz = effective_frame_limiter ? mangohud_policy.limit_millihz : 0, .preset = "custom", .always_show_graph = false, .limiter_method = mangohud_limiter_enabled && config::frame_limiter.mangohud_limiter_method == "early" ? "early" : "late", .smooth_motion = smooth_motion_policy.enabled, .smooth_motion_graphics_queue = smooth_motion_policy.enabled && smooth_motion_policy.use_graphics_queue, .hdr = source_hdr, // The machine-side Steam catalog intentionally omits desktop // Proton metadata. The session helper re-discovers the app and // applies this policy only when its launch is actually Proton; // carrying the resolved Wayland/HDR bit here is therefore // required for the brokered direct launch to receive it. .wayland_hdr_compatibility = wayland_hdr_compatibility.enabled, .proton_dualsense_compatibility = config::input.proton_dualsense_compatibility, .playstation_controller_attached = launch_session->playstation_gamepad_mask != 0, }; const bool overlay = policy.provider == "mangohud" || policy.provider == "mangohud-proton"; if (overlay && (config::frame_limiter.mangohud_preset == "1" || config::frame_limiter.mangohud_preset == "2" || config::frame_limiter.mangohud_preset == "3" || config::frame_limiter.mangohud_preset == "4")) { policy.preset = config::frame_limiter.mangohud_preset; } policy.always_show_graph = overlay && config::frame_limiter.mangohud_always_show_graph; const auto command = platf::steam::session_launch_command(steam_app_id, policy); if (!command.empty()) { _app.cmd = command; inherited_steam_environment_ready = true; session_steam_direct_launch = true; BOOST_LOG(info) << "Delegated direct Steam launch for app " << steam_app_id << " to the selected desktop session; no user path or launch option entered the machine host."; } } else { const auto direct_command = platf::steam::launch_command(*game); const auto broker_command = platf::steam::launch_command(steam_app_id); if (!direct_command.empty() && direct_command != broker_command) { _app.cmd = direct_command; if (!game->launch_working_dir.empty()) { _app.working_dir = game->launch_working_dir.generic_string(); } inherited_steam_environment_ready = true; BOOST_LOG(info) << "Resolved direct Steam launch for app " << steam_app_id << "; stream-owned environment and inherited Steam Launch Options will be applied to the game process."; } } if (!inherited_steam_environment_ready) { BOOST_LOG(error) << "Stream-owned launch features cannot activate for Steam app " << steam_app_id << " because a safe direct-launch request could not be constructed; the already-running Steam broker cannot inherit their environment."; } } else { const auto known = std::find_if(games.begin(), games.end(), [steam_app_id](const auto &candidate) { return candidate.app_id == steam_app_id; }); const std::string reason = games.empty() ? "the Steam catalog scan returned no games" : known == games.end() ? "the app is absent from the " + std::to_string(games.size()) + " scanned games" : "the app is present but not marked installed"; BOOST_LOG(error) << "Stream-owned launch features cannot activate for Steam app " << steam_app_id << " because the installed game could not be resolved from Steam metadata: " << reason << '.'; #ifdef __linux__ if (const char *machine_host = std::getenv("ARTLIGHT_MACHINE_HOST"); machine_host && *machine_host) { // The raw apps.json command cannot run inside the machine host: it // has no desktop, and the broker refuses unauthorized commands. // Ask the Steam client in the desktop session to launch it instead. _app.cmd = platf::steam::launch_command(steam_app_id); BOOST_LOG(warning) << "Falling back to a Steam client launch in the desktop session for app " << steam_app_id << '.'; } #endif } } else { BOOST_LOG(error) << "Stream-owned launch features cannot activate because the managed Steam app ID is invalid: [" << _app.steam_id << "]."; } } if (steam_proton_launch) { // Steam metadata is the reliable Proton discriminator. Native Steam and // arbitrary direct executables do not receive these Proton HDR flags. (void) set_stream_environment_default("PROTON_ENABLE_HDR", source_hdr ? "1" : "0"); (void) set_stream_environment_default("DXVK_HDR", source_hdr ? "1" : "0"); if (wayland_hdr_compatibility.enabled) { (void) set_stream_environment_default("PROTON_ENABLE_WAYLAND", "1"); BOOST_LOG(debug) << "Wayland HDR compatibility applied Proton/DXVK launch flags."; } } const bool smooth_motion_launch_ready = smooth_motion_policy.enabled && inherited_steam_environment_ready; const bool mangohud_launch_ready = mangohud_policy.enabled && inherited_steam_environment_ready; _env["NVPRESENT_ENABLE_SMOOTH_MOTION"] = smooth_motion_launch_ready ? "1" : ""; _env["NVPRESENT_QUEUE_FAMILY"] = smooth_motion_launch_ready && smooth_motion_policy.use_graphics_queue ? "1" : ""; if (smooth_motion_launch_ready) { BOOST_LOG(info) << "NVIDIA Smooth Motion enabled for the application launch."; if (!nvidia_present_layer_installed()) { BOOST_LOG(error) << "NVIDIA Smooth Motion was selected, but the VK_LAYER_NV_present implicit Vulkan layer " "was not found. Install a current NVIDIA Linux driver before launching this app."; } } const auto existing_preload = _env["LD_PRELOAD"].to_string(); if (mangohud_policy.enabled && !mangohud_launch_ready) { _env["MANGOHUD"] = ""; _env["MANGOHUD_CONFIG"] = ""; _env["MANGOHUD_FPS_LIMIT"] = ""; _env["LD_PRELOAD"] = platf::mangohud::without_preload(existing_preload); BOOST_LOG(error) << "Linux frame limiter disabled for this launch because the game process cannot inherit its environment."; } else if (mangohud_policy.enabled && proton_limiter) { const bool proton_overlay_enabled = proton_overlay_requested && mangohud_available; if (proton_overlay_enabled) { _env["MANGOHUD"] = "1"; _env["MANGOHUD_CONFIG"] = platf::mangohud::overlay_config_override( {}, config::frame_limiter.mangohud_preset, config::frame_limiter.mangohud_always_show_graph ); _env["MANGOHUD_FPS_LIMIT"] = ""; _env["LD_PRELOAD"] = platf::mangohud::with_preload(existing_preload); } else { _env["MANGOHUD"] = ""; _env["MANGOHUD_CONFIG"] = ""; _env["MANGOHUD_FPS_LIMIT"] = ""; _env["LD_PRELOAD"] = platf::mangohud::without_preload(existing_preload); if (proton_overlay_requested) { BOOST_LOG(warning) << "MangoHUD + Proton was requested, but mangohud was not found in PATH; " "continuing with the Proton renderer limiter only."; } } if (!_app.steam_id.empty()) { if (session_steam_direct_launch) { BOOST_LOG(info) << "Proton DXVK/VKD3D frame limiter delegated at " << mangohud_policy.limit << " FPS for Steam app " << _app.steam_id << (proton_overlay_enabled ? " with the MangoHUD overlay." : "."); } else { const auto state_path = platf::mangohud::write_state( _app.steam_id, proton_overlay_enabled ? "mangohud-proton" : "proton", mangohud_policy.limit, config::frame_limiter.mangohud_preset, proton_overlay_enabled && config::frame_limiter.mangohud_always_show_graph, config::frame_limiter.mangohud_limiter_method ); if (state_path.empty()) { BOOST_LOG(warning) << "Could not write the Steam Proton limiter handoff state for app " << _app.steam_id << "."; } else { BOOST_LOG(info) << "Proton DXVK/VKD3D frame limiter ready at " << mangohud_policy.limit << " FPS for Steam app " << _app.steam_id << (proton_overlay_enabled ? " with the MangoHUD overlay." : "."); } } } else { BOOST_LOG(warning) << "The Proton DXVK/VKD3D frame limiter requires a managed Steam Proton application."; } } else if (mangohud_policy.enabled) { if (!mangohud_available) { _env["MANGOHUD"] = ""; _env["MANGOHUD_CONFIG"] = ""; _env["MANGOHUD_FPS_LIMIT"] = ""; _env["LD_PRELOAD"] = platf::mangohud::without_preload(existing_preload); BOOST_LOG(warning) << "MangoHUD frame limiting requested, but mangohud was not found in PATH."; } else { // MANGOHUD enables the implicit Vulkan layer; the shim preload covers // native OpenGL. MANGOHUD_CONFIG survives Steam's container boundary; // MANGOHUD_FPS_LIMIT is also set for direct and newer-runtime launches. _env["MANGOHUD"] = "1"; _env["MANGOHUD_CONFIG"] = platf::mangohud::config_override( mangohud_policy.limit, config::frame_limiter.mangohud_preset, config::frame_limiter.mangohud_always_show_graph, config::frame_limiter.mangohud_limiter_method ); _env["MANGOHUD_FPS_LIMIT"] = platf::mangohud::fps_limit_environment_override( mangohud_policy.limit_millihz, mangohud_policy.limit ); _env["LD_PRELOAD"] = platf::mangohud::with_preload(existing_preload); BOOST_LOG(info) << "MangoHUD frame limiter enabled at " << mangohud_policy.limit << " FPS."; if (!_app.steam_id.empty()) { if (session_steam_direct_launch) { BOOST_LOG(info) << "Steam MangoHUD handoff delegated for app " << _app.steam_id << "."; } else { const auto state_path = platf::mangohud::write_state( _app.steam_id, "mangohud", mangohud_policy.limit, config::frame_limiter.mangohud_preset, config::frame_limiter.mangohud_always_show_graph, config::frame_limiter.mangohud_limiter_method ); if (state_path.empty()) { BOOST_LOG(warning) << "Could not write the Steam MangoHUD handoff state for app " << _app.steam_id << "."; } else { BOOST_LOG(info) << "Steam MangoHUD handoff ready for app " << _app.steam_id << "; the direct game command will apply the stream limit after inherited Steam Launch Options."; } } } } } else { // proc_t reuses its environment between launches, so remove overrides // owned by a previous ArtLight-managed MangoHUD launch. _env["MANGOHUD"] = ""; _env["MANGOHUD_CONFIG"] = ""; _env["MANGOHUD_FPS_LIMIT"] = ""; _env["LD_PRELOAD"] = platf::mangohud::without_preload(existing_preload); } #endif int channelCount = launch_session->surround_info & 65535; switch (channelCount) { case 2: _env["SUNSHINE_CLIENT_AUDIO_CONFIGURATION"] = "2.0"; _env["APOLLO_CLIENT_AUDIO_CONFIGURATION"] = "2.0"; break; case 6: _env["SUNSHINE_CLIENT_AUDIO_CONFIGURATION"] = "5.1"; _env["APOLLO_CLIENT_AUDIO_CONFIGURATION"] = "5.1"; break; case 8: _env["SUNSHINE_CLIENT_AUDIO_CONFIGURATION"] = "7.1"; _env["APOLLO_CLIENT_AUDIO_CONFIGURATION"] = "7.1"; break; } _env["SUNSHINE_CLIENT_AUDIO_SURROUND_PARAMS"] = launch_session->surround_params; _env["APOLLO_CLIENT_AUDIO_SURROUND_PARAMS"] = launch_session->surround_params; #ifdef _WIN32 resolved_lossless_exe_path = resolve_lossless_executable_path(); if (resolved_lossless_exe_path) { resolved_lossless_exe_utf8 = lossless_path_to_utf8(*resolved_lossless_exe_path); } _env["SUNSHINE_LOSSLESS_SCALING_EXE"] = !resolved_lossless_exe_utf8.empty() ? resolved_lossless_exe_utf8 : config::lossless_scaling.exe_path; #else try { _env["SUNSHINE_LOSSLESS_SCALING_EXE"] = config::lossless_scaling.exe_path; } catch (...) { _env["SUNSHINE_LOSSLESS_SCALING_EXE"] = ""; } #endif auto clear_lossless_runtime_env = [&]() { _env[ENV_LOSSLESS_ENABLED] = ""; _env[ENV_LOSSLESS_PROFILE] = ""; _env[ENV_LOSSLESS_CAPTURE_API] = ""; _env[ENV_LOSSLESS_QUEUE_TARGET] = ""; _env[ENV_LOSSLESS_HDR] = ""; _env[ENV_LOSSLESS_FLOW_SCALE] = ""; _env[ENV_LOSSLESS_PERFORMANCE_MODE] = ""; _env[ENV_LOSSLESS_RESOLUTION] = ""; _env[ENV_LOSSLESS_FRAMEGEN_MODE] = ""; _env[ENV_LOSSLESS_LSFG3_MODE] = ""; _env[ENV_LOSSLESS_SCALING_TYPE] = ""; _env[ENV_LOSSLESS_SHARPNESS] = ""; _env[ENV_LOSSLESS_LS1_SHARPNESS] = ""; _env[ENV_LOSSLESS_ANIME4K_TYPE] = ""; _env[ENV_LOSSLESS_ANIME4K_VRS] = ""; _env[ENV_LOSSLESS_LAUNCH_DELAY] = ""; _env[ENV_LOSSLESS_LEGACY_AUTO_DETECT] = ""; }; #ifdef _WIN32 const bool lossless_scaling_enabled = playnite_launcher::lossless::policy::should_enable_runtime( _app.lossless_scaling_enabled, _app.lossless_scaling_framegen ); #else const bool lossless_scaling_enabled = _app.lossless_scaling_enabled || _app.lossless_scaling_framegen; #endif _env["SUNSHINE_FRAME_GENERATION_PROVIDER"] = _app.frame_generation_enabled ? _app.frame_generation_provider : ""; const bool using_lossless_provider = _app.lossless_scaling_framegen && boost::iequals(_app.frame_generation_provider, "lossless-scaling"); if (lossless_scaling_enabled) { _env[ENV_LOSSLESS_ENABLED] = "1"; _env["SUNSHINE_LOSSLESS_SCALING_FRAMEGEN"] = _app.lossless_scaling_framegen ? "1" : ""; if (using_lossless_provider && effective_lossless_target) { _env["SUNSHINE_LOSSLESS_SCALING_TARGET_FPS"] = std::to_string(*effective_lossless_target); } else { _env["SUNSHINE_LOSSLESS_SCALING_TARGET_FPS"] = ""; } if (using_lossless_provider && effective_lossless_rtss) { _env["SUNSHINE_LOSSLESS_SCALING_RTSS_LIMIT"] = std::to_string(*effective_lossless_rtss); } else { _env["SUNSHINE_LOSSLESS_SCALING_RTSS_LIMIT"] = ""; } const bool wants_lossless_framegen = using_lossless_provider; auto runtime = compute_lossless_runtime(_app, wants_lossless_framegen); if (rtsp_stream::rtx_hdr_conversion_requested(*launch_session, config::video)) { runtime.hdr_enabled = false; BOOST_LOG(info) << "Lossless Scaling: disabling HDR support because RTX HDR conversion is active."; } #ifdef _WIN32 bool has_launch_commands = !_app.cmd.empty() || !_app.detached.empty(); _lossless_should_start_support = has_launch_commands && _app.playnite_id.empty() && !_app.playnite_fullscreen; if (_lossless_should_start_support) { _lossless_metadata.enabled = true; if (using_lossless_provider) { _lossless_metadata.target_fps = effective_lossless_target; _lossless_metadata.rtss_limit = effective_lossless_rtss; } else { _lossless_metadata.target_fps.reset(); _lossless_metadata.rtss_limit.reset(); } if (resolved_lossless_exe_path) { _lossless_metadata.configured_path = *resolved_lossless_exe_path; } else if (auto configured_path = lossless_to_path(config::lossless_scaling.exe_path)) { _lossless_metadata.configured_path = *configured_path; } _lossless_metadata.active_profile = runtime.profile; _lossless_metadata.capture_api = runtime.capture_api; _lossless_metadata.queue_target = runtime.queue_target; _lossless_metadata.hdr_enabled = runtime.hdr_enabled; _lossless_metadata.flow_scale = runtime.flow_scale; _lossless_metadata.performance_mode = runtime.performance_mode; _lossless_metadata.resolution_scale_factor = runtime.resolution_scale_factor; _lossless_metadata.frame_generation_mode = runtime.frame_generation; _lossless_metadata.lsfg3_mode = runtime.lsfg3_mode; _lossless_metadata.scaling_type = runtime.scaling_type; _lossless_metadata.sharpness = runtime.sharpness; _lossless_metadata.ls1_sharpness = runtime.ls1_sharpness; _lossless_metadata.anime4k_type = runtime.anime4k_type; _lossless_metadata.anime4k_vrs = runtime.anime4k_vrs; _lossless_metadata.launch_delay_seconds = _app.lossless_scaling_launch_delay_seconds; _lossless_metadata.legacy_auto_detect = _app.lossless_scaling_legacy_auto_detect; } #endif #ifdef _WIN32 std::optional rtss_warmup_limit; if (using_lossless_provider) { if (effective_lossless_rtss && *effective_lossless_rtss > 0) { rtss_warmup_limit = *effective_lossless_rtss; } const auto warmup_policy = rtsp_stream::make_framegen_stream_start_policy( *launch_session, rtss_warmup_limit, config::video.capture, platf::dxgi::should_use_wgc_default(), config::frame_limiter.virtual_display_limiter_enabled(), config::frame_limiter.fixed_virtual_display_refresh_multiplier(), config::frame_limiter.fixed_virtual_display_refresh_millihz(launch_session->client_vrr_requested) ); platf::frame_limiter_prepare_launch(warmup_policy); } #endif auto set_string = [&](const char *key, const std::optional &value) { if (value && !value->empty()) { _env[key] = *value; } else { _env[key] = ""; } }; auto set_int = [&](const char *key, const std::optional &value) { if (value.has_value()) { _env[key] = std::to_string(*value); } else { _env[key] = ""; } }; auto set_double = [&](const char *key, const std::optional &value) { if (value.has_value()) { std::ostringstream stream; stream.setf(std::ios::fixed); stream << std::setprecision(2) << *value; _env[key] = stream.str(); } else { _env[key] = ""; } }; auto set_bool = [&](const char *key, const std::optional &value) { if (value.has_value()) { _env[key] = *value ? "1" : "0"; } else { _env[key] = ""; } }; _env[ENV_LOSSLESS_PROFILE] = runtime.profile; set_string(ENV_LOSSLESS_CAPTURE_API, runtime.capture_api); set_int(ENV_LOSSLESS_QUEUE_TARGET, runtime.queue_target); set_bool(ENV_LOSSLESS_HDR, runtime.hdr_enabled); set_int(ENV_LOSSLESS_FLOW_SCALE, runtime.flow_scale); set_bool(ENV_LOSSLESS_PERFORMANCE_MODE, runtime.performance_mode); set_double(ENV_LOSSLESS_RESOLUTION, runtime.resolution_scale_factor); set_string(ENV_LOSSLESS_FRAMEGEN_MODE, runtime.frame_generation); set_string(ENV_LOSSLESS_LSFG3_MODE, runtime.lsfg3_mode); set_string(ENV_LOSSLESS_SCALING_TYPE, runtime.scaling_type); set_int(ENV_LOSSLESS_SHARPNESS, runtime.sharpness); set_int(ENV_LOSSLESS_LS1_SHARPNESS, runtime.ls1_sharpness); set_string(ENV_LOSSLESS_ANIME4K_TYPE, runtime.anime4k_type); set_bool(ENV_LOSSLESS_ANIME4K_VRS, runtime.anime4k_vrs); set_int(ENV_LOSSLESS_LAUNCH_DELAY, std::optional(_app.lossless_scaling_launch_delay_seconds)); set_bool(ENV_LOSSLESS_LEGACY_AUTO_DETECT, std::optional(_app.lossless_scaling_legacy_auto_detect)); } else { _env[ENV_LOSSLESS_ENABLED] = ""; _env["SUNSHINE_LOSSLESS_SCALING_FRAMEGEN"] = ""; _env["SUNSHINE_LOSSLESS_SCALING_TARGET_FPS"] = ""; _env["SUNSHINE_LOSSLESS_SCALING_RTSS_LIMIT"] = ""; clear_lossless_runtime_env(); } if (!_app.output.empty() && _app.output != "null"sv) { #ifdef _WIN32 // fopen() interprets the filename as an ANSI string on Windows, so we must convert it // to UTF-16 and use the wchar_t variants for proper Unicode log file path support. auto woutput = utf_utils::from_utf8(_app.output); // Use _SH_DENYNO to allow us to open this log file again for writing even if it is // still open from a previous execution. This is required to handle the case of a // detached process executing again while the previous process is still running. _pipe.reset(_wfsopen(woutput.c_str(), L"a", _SH_DENYNO)); #else _pipe.reset(fopen(_app.output.c_str(), "a")); #endif } #ifdef _WIN32 const auto requires_user_session = [&]() { return !_app.prep_cmds.empty() || !_app.detached.empty() || !_app.cmd.empty() || !_app.playnite_id.empty() || _app.playnite_fullscreen; }; const auto user_session_ready = [&]() { HANDLE user_token = platf::dxgi::retrieve_users_token(false); if (!user_token) { return false; } CloseHandle(user_token); return true; }; if (platf::is_running_as_system() && requires_user_session() && !user_session_ready()) { BOOST_LOG(info) << "No active user session; deferring app launch until sign-in."; _deferred_launch = true; return 0; } #endif #ifdef __linux__ if (!_app.steam_id.empty() && !session_steam_direct_launch && (wayland_hdr_compatibility.enabled || config::input.proton_dualsense_compatibility)) { // A Steam URI is only a handoff to its already-running daemon. Prepare // the session-owned Proton hook before its launch commands run, or the // daemon would spawn the game with its old environment. platf::frame_limiter_streaming_start( platf::frame_limiter_owner::application, mangohud_stream_policy, {.color_mode = stream_hdr ? platf::proton_color_mode::hdr : (launch_session->prefer_sdr_10bit ? platf::proton_color_mode::sdr10 : platf::proton_color_mode::sdr), .wayland_hdr_compatibility = wayland_hdr_compatibility.enabled} ); BOOST_LOG(debug) << "Global Proton compatibility hook prepared before the running Steam handoff."; } #endif return launch_app_commands(true); } int proc_t::launch_app_commands(bool stream_lifecycle_lock_held) { std::error_code ec; _app_prep_begin = std::begin(_app.prep_cmds); _app_prep_it = _app_prep_begin; // Executed when returning from function on failure auto fg = util::fail_guard([&]() { terminate(false, true, false, stream_lifecycle_lock_held); }); #ifdef _WIN32 // Some launchers disable the user's global screen saver setting and may // exit without restoring it. Preserve the pre-launch state independently // of whether the launched process remains trackable. platf::cache_screen_saver_state(); std::unordered_set lossless_baseline_pids; bool lossless_monitor_started = false; std::string lossless_install_dir_hint; #endif for (; _app_prep_it != std::end(_app.prep_cmds); ++_app_prep_it) { auto &cmd = *_app_prep_it; // Skip empty commands if (cmd.do_cmd.empty()) { continue; } boost::filesystem::path working_dir = _app.working_dir.empty() ? find_working_directory(cmd.do_cmd, _env) : boost::filesystem::path(_app.working_dir); BOOST_LOG(info) << "Executing Do Cmd: ["sv << cmd.do_cmd << "] elevated: " << cmd.elevated; auto child = platf::run_command(cmd.elevated, true, cmd.do_cmd, working_dir, _env, _pipe.get(), ec, nullptr); if (ec) { BOOST_LOG(error) << "Couldn't run ["sv << cmd.do_cmd << "]: System: "sv << ec.message(); // We don't want any prep commands failing launch of the desktop. // This is to prevent the issue where users reboot their PC and need to log in with Sunshine. // permission_denied is typically returned when the user impersonation fails, which can happen when user is not signed in yet. if (!(_app.cmd.empty() && ec == std::errc::permission_denied)) { return -1; } } child.wait(ec); if (ec) { BOOST_LOG(error) << '[' << cmd.do_cmd << "] wait failed with error code ["sv << ec << ']'; return -1; } auto ret = child.exit_code(); if (ret != 0) { BOOST_LOG(error) << '[' << cmd.do_cmd << "] exited with code ["sv << ret << ']'; return -1; } } _env["APOLLO_APP_STATUS"] = "RUNNING"; for (auto &cmd : _app.detached) { boost::filesystem::path working_dir = _app.working_dir.empty() ? find_working_directory(cmd, _env) : boost::filesystem::path(_app.working_dir); #ifdef _WIN32 if (_lossless_should_start_support && !lossless_monitor_started && lossless_baseline_pids.empty()) { lossless_baseline_pids = capture_process_baseline_for_lossless(); } if (_lossless_should_start_support && !lossless_monitor_started && lossless_install_dir_hint.empty()) { try { lossless_install_dir_hint = platf::dxgi::wide_to_utf8(working_dir.wstring()); } catch (...) { lossless_install_dir_hint.clear(); } } #endif BOOST_LOG(info) << "Spawning ["sv << cmd << "] in ["sv << working_dir << ']'; auto child = platf::run_command(_app.elevated, true, cmd, working_dir, _env, _pipe.get(), ec, nullptr); #ifdef _WIN32 DWORD detached_pid = 0; if (!ec) { try { detached_pid = static_cast(child.id()); } catch (...) { detached_pid = 0; } } #endif if (ec) { BOOST_LOG(warning) << "Couldn't spawn ["sv << cmd << "]: System: "sv << ec.message(); } else { child.detach(); #ifdef _WIN32 if (_lossless_should_start_support && !lossless_monitor_started) { if (lossless_baseline_pids.empty()) { lossless_baseline_pids = capture_process_baseline_for_lossless(); } start_lossless_scaling_support(std::move(lossless_baseline_pids), _lossless_metadata, std::move(lossless_install_dir_hint), detached_pid); lossless_monitor_started = true; } #endif } } // Playnite-backed apps: invoke via Playnite and treat as placebo (lifetime managed via Playnite status) #ifdef _WIN32 if (!_app.playnite_id.empty() && _app.cmd.empty()) { // Auto-update Playnite plugin if an update is available try { std::string installed_ver, packaged_ver; bool have_installed = platf::playnite::get_installed_plugin_version(installed_ver); bool have_packaged = platf::playnite::get_packaged_plugin_version(packaged_ver); if (have_installed && have_packaged) { // Simple version comparison: compare as strings (works for semantic versioning) auto normalize_ver = [](std::string s) -> std::string { // Strip leading 'v' if present if (!s.empty() && (s[0] == 'v' || s[0] == 'V')) { s = s.substr(1); } // Remove whitespace s.erase(std::remove_if(s.begin(), s.end(), ::isspace), s.end()); return s; }; std::string installed_normalized = normalize_ver(installed_ver); std::string packaged_normalized = normalize_ver(packaged_ver); if (installed_normalized < packaged_normalized) { BOOST_LOG(info) << "Playnite plugin update available (" << installed_ver << " -> " << packaged_ver << "), auto-updating before launch"; std::string install_error; if (platf::playnite::install_plugin(install_error)) { BOOST_LOG(info) << "Playnite plugin auto-update succeeded"; } else { BOOST_LOG(warning) << "Playnite plugin auto-update failed: " << install_error << " (continuing with game launch)"; } } } } catch (const std::exception &e) { BOOST_LOG(warning) << "Exception during Playnite plugin auto-update check: " << e.what() << " (continuing with game launch)"; } catch (...) { BOOST_LOG(warning) << "Unknown exception during Playnite plugin auto-update check (continuing with game launch)"; } BOOST_LOG(info) << "Launching Playnite game via helper, id=" << _app.playnite_id; bool launched = false; // Resolve launcher alongside sunshine.exe: tools\\playnite-launcher.exe try { WCHAR exePathW[MAX_PATH] = {}; GetModuleFileNameW(nullptr, exePathW, ARRAYSIZE(exePathW)); std::filesystem::path exeDir = std::filesystem::path(exePathW).parent_path(); std::filesystem::path launcher = exeDir / L"tools" / L"playnite-launcher.exe"; std::string lpath = launcher.string(); std::string cmd = std::string("\"") + lpath + "\" --game-id " + _app.playnite_id; // Pass graceful-exit timeout to launcher for cleanup behavior try { int exit_to = (int) std::max(0, _app.exit_timeout.count()); if (exit_to > 0) { cmd += std::string(" --exit-timeout ") + std::to_string(exit_to); } } catch (...) {} // Pass focus attempts from config so the helper can try to bring Playnite/game to foreground try { if (config::playnite.focus_attempts > 0) { cmd += std::string(" --focus-attempts ") + std::to_string(config::playnite.focus_attempts); } if (config::playnite.focus_timeout_secs > 0) { cmd += std::string(" --focus-timeout ") + std::to_string(config::playnite.focus_timeout_secs); } if (config::playnite.focus_exit_on_first) { cmd += std::string(" --focus-exit-on-first"); } } catch (...) {} std::error_code fec; boost::filesystem::path wd; // empty wd _process = platf::run_command(false, true, cmd, wd, _env, _pipe.get(), fec, &_process_group); if (fec) { BOOST_LOG(warning) << "Playnite helper launch failed: "sv << fec.message() << "; attempting URI fallback"sv; } else { BOOST_LOG(info) << "Playnite helper launched and is being monitored"; try { auto pid = static_cast(_process.id()); if (!platf::playnite::announce_launcher(pid, _app.playnite_id)) { BOOST_LOG(debug) << "Playnite helper: announce_launcher reported inactive IPC"; } } catch (...) { } launched = true; } } catch (...) { launched = false; } if (!launched) { // Best-effort fallback using Playnite URI protocol std::string uri = std::string("playnite://playnite/start/") + _app.playnite_id; std::error_code fec; boost::filesystem::path wd; // empty working dir as lvalue auto child = platf::run_command(false, true, std::string("cmd /c start \"\" \"") + uri + "\"", wd, _env, _pipe.get(), fec, nullptr); if (fec) { BOOST_LOG(warning) << "Playnite URI launch failed: "sv << fec.message(); } else { BOOST_LOG(info) << "Playnite URI launch started"; child.detach(); launched = true; } } if (!launched) { BOOST_LOG(error) << "Failed to launch Playnite game."sv; return -1; } // Start Playnite IPC client to receive game events (gameStopped, etc.) platf::playnite::start_client_for_session(); // Track the helper process; when it exits, Sunshine will terminate the stream automatically placebo = false; } else #endif #ifdef _WIN32 if (_app.playnite_fullscreen) { BOOST_LOG(info) << "Launching Playnite in fullscreen via helper"; bool launched = false; try { WCHAR exePathW[MAX_PATH] = {}; GetModuleFileNameW(nullptr, exePathW, ARRAYSIZE(exePathW)); std::filesystem::path exeDir = std::filesystem::path(exePathW).parent_path(); std::filesystem::path launcher = exeDir / L"tools" / L"playnite-launcher.exe"; std::string lpath = launcher.string(); std::string cmd = std::string("\"") + lpath + "\" --fullscreen"; try { if (config::playnite.focus_attempts > 0) { cmd += std::string(" --focus-attempts ") + std::to_string(config::playnite.focus_attempts); } if (config::playnite.focus_timeout_secs > 0) { cmd += std::string(" --focus-timeout ") + std::to_string(config::playnite.focus_timeout_secs); } if (config::playnite.focus_exit_on_first) { cmd += std::string(" --focus-exit-on-first"); } } catch (...) {} std::error_code fec; boost::filesystem::path wd; // empty wd _process = platf::run_command(false, true, cmd, wd, _env, _pipe.get(), fec, &_process_group); if (fec) { BOOST_LOG(warning) << "Playnite fullscreen helper launch failed: "sv << fec.message(); } else { BOOST_LOG(info) << "Playnite fullscreen helper launched"; try { auto pid = static_cast(_process.id()); if (!platf::playnite::announce_launcher(pid, std::string())) { BOOST_LOG(debug) << "Playnite helper (fullscreen): announce_launcher reported inactive IPC"; } } catch (...) { } launched = true; } } catch (...) { launched = false; } if (!launched) { BOOST_LOG(error) << "Failed to launch Playnite fullscreen."sv; return -1; } // Start Playnite IPC client to receive game events (gameStopped, etc.) platf::playnite::start_client_for_session(); placebo = false; } else #endif if (_app.cmd.empty()) { BOOST_LOG(info) << "Executing [Desktop]"sv; BOOST_LOG(info) << "Playnite launch path complete; treating app as placebo (status-driven)."; placebo = true; } else { boost::filesystem::path working_dir = _app.working_dir.empty() ? find_working_directory(_app.cmd, _env) : boost::filesystem::path(_app.working_dir); #ifdef _WIN32 if (_lossless_should_start_support && !lossless_monitor_started && lossless_baseline_pids.empty()) { lossless_baseline_pids = capture_process_baseline_for_lossless(); } if (_lossless_should_start_support && !lossless_monitor_started && lossless_install_dir_hint.empty()) { try { lossless_install_dir_hint = platf::dxgi::wide_to_utf8(working_dir.wstring()); } catch (...) { lossless_install_dir_hint.clear(); } } #endif // Steam's xdg-open/cmd URI launcher is intentionally short-lived. Take // the process baseline immediately before handing the URI to Steam so // the game can be owned independently of that launcher process. const auto provider_id = !_app.steam_id.empty() ? _app.steam_id : _app.lutris_id; const auto provider_name = !_app.steam_id.empty() ? "Steam" : "Lutris"; const auto provider_directory = !_app.steam_install_dir.empty() ? _app.steam_install_dir : _app.lutris_directory; if (!provider_id.empty() && !provider_directory.empty()) { _steam_tracker.clear(); _steam_tracking_active = _steam_tracker.begin(provider_directory); _steam_tracking_associated = false; _steam_tracking_deadline = std::chrono::steady_clock::now() + 15s; _steam_last_tracking_poll = {}; if (!_steam_tracking_active) { BOOST_LOG(warning) << provider_name << " app " << provider_id << " could not capture a process baseline; continuing with untrackable detached behavior."; } else { BOOST_LOG(debug) << provider_name << " app " << provider_id << " process baseline captured for install directory [" << provider_directory << "]"; } } BOOST_LOG(info) << "Executing: ["sv << _app.cmd << "] in ["sv << working_dir << ']'; _process = platf::run_command(_app.elevated, true, _app.cmd, working_dir, _env, _pipe.get(), ec, &_process_group); if (ec) { BOOST_LOG(warning) << "Couldn't run ["sv << _app.cmd << "]: System: "sv << ec.message(); return -1; } } #ifdef _WIN32 if (_lossless_should_start_support && !lossless_monitor_started) { if (lossless_baseline_pids.empty()) { lossless_baseline_pids = capture_process_baseline_for_lossless(); } if (lossless_install_dir_hint.empty() && !_app.working_dir.empty()) { lossless_install_dir_hint = _app.working_dir; } if (lossless_baseline_pids.empty()) { // still proceed; detection handles empty baseline } DWORD candidate_pid = 0; if (_process) { try { candidate_pid = static_cast(_process.id()); } catch (...) { candidate_pid = 0; } } start_lossless_scaling_support(std::move(lossless_baseline_pids), _lossless_metadata, std::move(lossless_install_dir_hint), candidate_pid); lossless_monitor_started = true; } #endif _app_launch_time = std::chrono::steady_clock::now(); #if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 system_tray::update_tray_playing(_app.name); #endif fg.disable(); return 0; } int proc_t::running() { #ifndef _WIN32 // On POSIX OSes, we must periodically wait for our children to avoid // them becoming zombies. This must be synchronized carefully with // calls to bp::wait() and platf::process_group_running() which both // invoke waitpid() under the hood. auto reaper = util::fail_guard([]() { while (waitpid(-1, nullptr, WNOHANG) > 0); }); #endif #ifdef _WIN32 if (_deferred_launch) { if (platf::is_running_as_system()) { HANDLE user_token = platf::dxgi::retrieve_users_token(false); if (!user_token) { return _app_id; } CloseHandle(user_token); } std::optional rtss_warmup_limit; if (_lossless_metadata.enabled && _lossless_metadata.rtss_limit && *_lossless_metadata.rtss_limit > 0) { rtss_warmup_limit = *_lossless_metadata.rtss_limit; } const bool wants_frame_limit = config::frame_limiter.enable || _app.frame_generation_enabled || _app.gen1_framegen_fix || _app.gen2_framegen_fix || (rtss_warmup_limit && *rtss_warmup_limit > 0); if (wants_frame_limit) { bool warmup_uses_virtual = _app.virtual_screen || config::video.virtual_display_mode != config::video_t::virtual_display_mode_e::disabled; if (_app.virtual_display_mode_override) { warmup_uses_virtual = *_app.virtual_display_mode_override != config::video_t::virtual_display_mode_e::disabled; } if (_app.output_name_override && !_app.output_name_override->empty() && !VDISPLAY::is_virtual_display_selection(*_app.output_name_override)) { warmup_uses_virtual = false; } const auto warmup_policy = framegen::make_stream_start_policy({ .fps = 0, .frame_generation_enabled = _app.frame_generation_enabled, .gen1_framegen_fix = _app.gen1_framegen_fix, .gen2_framegen_fix = _app.gen2_framegen_fix, .lossless_scaling_framegen = _app.lossless_scaling_framegen, .lossless_rtss_limit = rtss_warmup_limit, .frame_generation_provider = _app.frame_generation_provider, .uses_virtual_display = warmup_uses_virtual, .capture_mode = config::video.capture, .auto_capture_uses_wgc = platf::dxgi::should_use_wgc_default(), .auto_virtual_framegen_limiter = config::frame_limiter.virtual_display_limiter_enabled(), .virtual_display_refresh_multiplier = config::frame_limiter.fixed_virtual_display_refresh_multiplier(), .virtual_display_fixed_refresh_millihz = config::frame_limiter.fixed_virtual_display_refresh_millihz(), }); platf::frame_limiter_prepare_launch(warmup_policy); const bool provider_auto = config::frame_limiter.provider.empty() || boost::iequals(config::frame_limiter.provider, "auto"); const bool provider_rtss = boost::iequals(config::frame_limiter.provider, "rtss"); const bool should_wait_rtss = platf::rtss_is_configured() && (provider_auto || provider_rtss || _app.frame_generation_enabled || _app.gen1_framegen_fix || _app.gen2_framegen_fix); if (should_wait_rtss) { const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3); bool running = false; while (std::chrono::steady_clock::now() < deadline) { if (platf::rtss_get_status().process_running) { running = true; break; } std::this_thread::sleep_for(std::chrono::milliseconds(100)); } BOOST_LOG(info) << "RTSS warmup " << (running ? "complete" : "timeout") << " after deferred login."; } } BOOST_LOG(info) << "User session detected; resuming deferred launch for app '" << _app.name << "'."; _deferred_launch = false; int err = launch_app_commands(false); if (err != 0) { BOOST_LOG(error) << "Deferred launch failed; terminating session."; return 0; } } #endif if (_steam_tracking_active) { const auto now = std::chrono::steady_clock::now(); constexpr auto tracking_poll_interval = 100ms; if (_steam_last_tracking_poll.time_since_epoch().count() == 0 || now - _steam_last_tracking_poll >= tracking_poll_interval) { _steam_last_tracking_poll = now; const auto tracking = _steam_tracker.finish(); if (tracking.associated()) { if (!_steam_tracking_associated) { const auto provider_name = !_app.steam_id.empty() ? "Steam" : "Lutris"; const auto provider_id = !_app.steam_id.empty() ? _app.steam_id : _app.lutris_id; BOOST_LOG(info) << provider_name << " app " << provider_id << " associated with " << tracking.tree.processes.size() << " tracked process(es)."; } _steam_tracking_associated = true; placebo = false; } else if (_steam_tracking_associated && tracking.reason.find("unavailable") == std::string::npos) { // A complete snapshot with no retained PID means the tracked game // tree has exited. Let the normal cleanup path run below. _steam_tracking_associated = false; _steam_tracking_active = false; } } if (_steam_tracking_associated) { return _app_id; } if (now < _steam_tracking_deadline) { // Keep the stream alive while Steam is still starting the game. This // preserves the existing detached/placebo behavior if association // eventually fails, without turning polling into a blocking wait. return _app_id; } BOOST_LOG(warning) << "Steam app " << _app.steam_id << " process tree remained untrackable after the 15 second launch window;" " retaining detached streaming behavior."; _steam_tracking_active = false; placebo = true; return _app_id; } if (placebo) { return _app_id; } else if (_app.wait_all && _process_group && platf::process_group_running((std::uintptr_t) _process_group.native_handle())) { // The app is still running if any process in the group is still running return _app_id; } else if (_process.running()) { // The app is still running only if the initial process launched is still running return _app_id; } else if (_app.auto_detach && std::chrono::steady_clock::now() - _app_launch_time < 5s) { BOOST_LOG(info) << "App exited with code ["sv << _process.native_exit_code() << "] within 5 seconds of launch. Treating the app as a detached command."sv; BOOST_LOG(info) << "Adjust this behavior in the Applications tab or apps.json if this is not what you want."sv; BOOST_LOG(info) << "Playnite launch path complete; treating app as placebo (status-driven)."; placebo = true; #if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 if (_process.native_exit_code() != 0) { system_tray::update_tray_launch_error(proc::proc.get_last_run_app_name(), _process.native_exit_code()); } #endif return _app_id; } // Perform cleanup actions now if needed if (_process) { std::unique_lock stream_lifecycle_lock {nvhttp::stream_lifecycle_mutex(), std::try_to_lock}; if (!stream_lifecycle_lock.owns_lock()) { // Teardown is already in flight on another thread. Blocking here // parks the caller - including the single serverinfo worker, which // made the host undiscoverable for the whole teardown tail // (artlight#326). The gate owner performs the cleanup; report // not-running without waiting on it. BOOST_LOG(debug) << "[running] App exited but stream teardown is already in flight; deferring cleanup to the gate owner."; return 0; } BOOST_LOG(info) << "[running] _process.running() is false; calling terminate(). App exited with code ["sv << _process.native_exit_code() << "] for app '" << _app.name << "' (id=" << _app_id << ")"; // running() owns the lifecycle gate above; prevent terminate() from // acquiring the non-recursive mutex a second time. terminate(false, true, false, true); } return 0; } int proc_t::current_app_id() const { return _app_id.load(std::memory_order_acquire); } void proc_t::resume() { BOOST_LOG(info) << "Session resuming for app [" << _app_name << "]."; #ifdef _WIN32 // pause() consumes the prior snapshot after restoring it. Capture a new // baseline before any resume command can change the setting again. platf::cache_screen_saver_state(); #endif if (!_app.state_cmds.empty()) { auto exec_thread = std::thread([cmd_list = _app.state_cmds, app_working_dir = _app.working_dir, _env = _env]() mutable { _env["APOLLO_APP_STATUS"] = "RESUMING"; std::error_code ec; auto _state_resume_it = std::begin(cmd_list); for (; _state_resume_it != std::end(cmd_list); ++_state_resume_it) { auto &cmd = *_state_resume_it; // Skip empty commands if (cmd.do_cmd.empty()) { continue; } boost::filesystem::path working_dir = app_working_dir.empty() ? find_working_directory(cmd.do_cmd, _env) : boost::filesystem::path(app_working_dir); BOOST_LOG(info) << "Executing Resume Cmd: ["sv << cmd.do_cmd << "] elevated: " << cmd.elevated; auto child = platf::run_command(cmd.elevated, true, cmd.do_cmd, working_dir, _env, nullptr, ec, nullptr); if (ec) { BOOST_LOG(error) << "Couldn't run ["sv << cmd.do_cmd << "]: System: "sv << ec.message(); break; } child.wait(); auto ret = child.exit_code(); if (ret != 0 && ec != std::errc::permission_denied) { BOOST_LOG(error) << '[' << cmd.do_cmd << "] failed with code ["sv << ret << ']'; break; } } }); exec_thread.detach(); } } void proc_t::pause(bool stream_lifecycle_lock_held) { const int app_id = stream_lifecycle_lock_held ? current_app_id() : running(); if (app_id <= 0) { BOOST_LOG(info) << "Session already stopped, do not run pause commands."; return; } if (_app.terminate_on_pause) { BOOST_LOG(info) << "Terminating app [" << _app_name << "] when all clients are disconnected. Pause commands are skipped."; terminate(false, true, false, stream_lifecycle_lock_held); return; } BOOST_LOG(info) << "Session pausing for app [" << _app_name << "]."; std::function finish_screen_saver_restore = [] {}; #ifdef _WIN32 if (!_app.state_cmds.empty()) { finish_screen_saver_restore = platf::deferred_screen_saver_restore(); } #endif // Release the registered worker if copying commands or starting its thread // fails. After successful launch the worker owns this completion instead. auto restore_on_launch_failure = util::fail_guard(finish_screen_saver_restore); #ifdef _WIN32 // Preserve immediate restoration even if a pause command runs indefinitely. // The registered worker retains the baseline for a later reassertion. platf::restore_screen_saver_state(); #endif if (!_app.state_cmds.empty()) { auto exec_thread = std::thread([cmd_list = _app.state_cmds, app_working_dir = _app.working_dir, _env = _env, finish_screen_saver_restore]() mutable { auto restore_guard = util::fail_guard(finish_screen_saver_restore); _env["APOLLO_APP_STATUS"] = "PAUSING"; std::error_code ec; auto _state_pause_it = std::begin(cmd_list); for (; _state_pause_it != std::end(cmd_list); ++_state_pause_it) { auto &cmd = *_state_pause_it; // Skip empty commands if (cmd.undo_cmd.empty()) { continue; } boost::filesystem::path working_dir = app_working_dir.empty() ? find_working_directory(cmd.undo_cmd, _env) : boost::filesystem::path(app_working_dir); BOOST_LOG(info) << "Executing Pause Cmd: ["sv << cmd.undo_cmd << "] elevated: " << cmd.elevated; auto child = platf::run_command(cmd.elevated, true, cmd.undo_cmd, working_dir, _env, nullptr, ec, nullptr); if (ec) { BOOST_LOG(error) << "Couldn't run ["sv << cmd.undo_cmd << "]: System: "sv << ec.message(); break; } child.wait(ec); if (ec) { BOOST_LOG(error) << '[' << cmd.undo_cmd << "] wait failed with error code ["sv << ec << ']'; break; } auto ret = child.exit_code(); if (ret != 0 && ec != std::errc::permission_denied) { BOOST_LOG(error) << '[' << cmd.undo_cmd << "] failed with code ["sv << ret << ']'; break; } } }); exec_thread.detach(); restore_on_launch_failure.disable(); } #if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 system_tray::update_tray_pausing(proc::proc.get_last_run_app_name()); #endif } bool proc_t::foreground_window_matches_running_app() { #ifdef _WIN32 if (!has_trackable_running_app()) { return false; } const auto foreground_pid = foreground_window_process_id(); if (!foreground_pid) { return false; } try { if (_process && static_cast(_process.id()) == *foreground_pid) { return true; } } catch (...) { } try { return process_group_contains_pid(_process_group, *foreground_pid); } catch (...) { return false; } #else return false; #endif } #ifdef _WIN32 bool proc_t::running_app_contains_pid(uint32_t pid) { if (!has_trackable_running_app() || pid == 0) { return false; } const auto win_pid = static_cast(pid); try { if (_process && static_cast(_process.id()) == win_pid) { return true; } } catch (...) { } try { return process_group_contains_pid(_process_group, win_pid); } catch (...) { return false; } } running_app_state_t proc_t::running_app_state() const { running_app_state_t state; state.has_active_app = _app_id > 0; if (!state.has_active_app) { return state; } state.trackable = !placebo && (_process || _process_group); state.uses_playnite = !_app.playnite_id.empty(); state.playnite_id = _app.playnite_id; state.name = _app.name; state.command = _app.cmd; state.working_dir = _app.working_dir; try { if (_process) { state.root_pid = static_cast(_process.id()); } } catch (...) { state.root_pid = 0; } return state; } #endif bool proc_t::has_trackable_running_app() const { #ifdef _WIN32 return _app_id > 0 && !placebo && (_process || _process_group); #else return _app_id > 0 && !placebo && (_steam_tracking_associated || static_cast(_process)); #endif } void proc_t::terminate( bool immediate, bool needs_refresh, bool skip_display_revert, bool stream_lifecycle_lock_held ) { #ifdef __linux__ // This owner represents a running-Steam handoff. Dropping it when the // app ends also prevents a later SDR launch from retaining HDR policy. platf::frame_limiter_streaming_stop(platf::frame_limiter_owner::application); #endif std::unique_lock stream_lifecycle_lock; if (!stream_lifecycle_lock_held) { stream_lifecycle_lock = std::unique_lock {nvhttp::stream_lifecycle_mutex()}; } // Mark termination before process teardown so a concurrent final audio // owner restores directly instead of retaining state for the ended app. audio::app_termination_requested(); // App termination can remove a display directly and can continue through // process, undo-command, helper, watchdog, and deferred-config cleanup. // Keep HTTP encoder probing out of that entire tail. stream::session::cleanup_reservation_t cleanup_reservation; std::error_code ec; const bool had_active_app = _app_id > 0; #ifdef __linux__ if (!_app.steam_id.empty()) { platf::mangohud::remove_state(_app.steam_id); } #endif placebo = false; std::chrono::seconds remaining_timeout = _app.exit_timeout; #ifdef _WIN32 _deferred_launch = false; _lossless_should_start_support = false; stop_lossless_scaling_support(); #endif // For Playnite-managed apps, request a graceful stop via Playnite first #ifdef _WIN32 if (had_active_app && !_app.playnite_id.empty()) { bool should_request_playnite_stop = true; try { if (_process && !_process.running() && _process.native_exit_code() == 0) { // The launcher already exited cleanly (typically after receiving gameStopped). // Avoid sending a redundant stop command that can race into the next launch. should_request_playnite_stop = false; BOOST_LOG(debug) << "Playnite: launcher exited cleanly; skipping redundant stop request"; } } catch (...) {} try { if (should_request_playnite_stop) { // Ask Playnite to stop the game; then wait up to exit-timeout to let it close. platf::playnite::stop_game(_app.playnite_id); while (remaining_timeout.count() > 0 && _process_group && platf::process_group_running((std::uintptr_t) _process_group.native_handle())) { std::this_thread::sleep_for(1s); remaining_timeout -= 1s; } } } catch (...) {} // Stop the IPC client since the Playnite session is ending platf::playnite::stop_client_for_session(); } else if (had_active_app && _app.playnite_fullscreen) { // For fullscreen mode, also stop the IPC client platf::playnite::stop_client_for_session(); } #endif // Provider URI launchers are not the game process and their process group // does not own the Proton/Wine tree. Stop only identities retained by the // tracker; Steam/Lutris client and runtime PIDs are never inserted. if (!_steam_tracker.tree().empty()) { if (!_steam_process_controller) { _steam_process_controller = platf::steam::lifecycle::native_process_controller(); } platf::steam::lifecycle::stop_options steam_stop_options; steam_stop_options.grace_period = std::chrono::duration_cast( std::max(remaining_timeout, 0s) ); const auto stopped = platf::steam::lifecycle::stop_tree( _steam_tracker.tree(), *_steam_process_controller, steam_stop_options ); const auto provider_name = !_app.steam_id.empty() ? "Steam" : "Lutris"; BOOST_LOG(info) << provider_name << " tracked tree termination requested: TERM=" << stopped.terminate_sent << ", KILL=" << stopped.kill_sent << ", skipped=" << stopped.skipped << ", complete=" << (stopped.complete ? "yes" : "no"); // The app timeout has been consumed by the Steam-owned tree. The URI // launcher/group cleanup below remains best effort and must not extend // the configured Steam app timeout a second time. remaining_timeout = 0s; } _steam_tracker.clear(); _steam_tracking_active = false; _steam_tracking_associated = false; _steam_tracking_deadline = {}; _steam_last_tracking_poll = {}; // Regardless, ensure process group is terminated (graceful then forceful with remaining timeout) terminate_process_group(_process, _process_group, remaining_timeout); _process = bp::child(); _process_group = bp::group(); _env["APOLLO_APP_STATUS"] = "TERMINATING"; const bool has_run = had_active_app; const bool should_dispatch_revert = has_run && !proc::proc.get_last_run_app_name().empty(); const auto undo_timeout = std::max(_app.exit_timeout, std::chrono::seconds(15)); for (; _app_prep_it != _app_prep_begin; --_app_prep_it) { auto &cmd = *(_app_prep_it - 1); if (cmd.undo_cmd.empty()) { continue; } boost::filesystem::path working_dir = _app.working_dir.empty() ? find_working_directory(cmd.undo_cmd, _env) : boost::filesystem::path(_app.working_dir); BOOST_LOG(info) << "Executing Undo Cmd: ["sv << cmd.undo_cmd << ']'; auto child = platf::run_command(cmd.elevated, true, cmd.undo_cmd, working_dir, _env, _pipe.get(), ec, nullptr); if (ec) { BOOST_LOG(warning) << "System: "sv << ec.message(); ec.clear(); continue; } const auto deadline = std::chrono::steady_clock::now() + undo_timeout; while (child.running() && std::chrono::steady_clock::now() < deadline) { std::this_thread::sleep_for(100ms); } if (child.running()) { BOOST_LOG(warning) << "Undo command timed out after " << undo_timeout.count() << " seconds; continuing teardown without waiting for completion."; try { child.detach(); } catch (...) { } continue; } child.wait(ec); if (ec) { BOOST_LOG(warning) << '[' << cmd.undo_cmd << "] wait failed with error code ["sv << ec << ']'; ec.clear(); continue; } auto ret = child.exit_code(); if (ret != 0) { BOOST_LOG(warning) << "Return code ["sv << ret << ']'; } } #ifdef _WIN32 // Restore after terminating the app and running its undo commands so a // detached/placebo launcher cannot leave this global setting disabled. platf::restore_screen_saver_state(); #endif _pipe.reset(); #if defined(_WIN32) || defined(__linux__) // Release only this app's normal-display role before deciding whether the // process-wide display restore is now allowed. The coordinator keeps that // display protected while capture references drain or a retained Remote // Monitor still owns the same client identity. if (!_active_client_uuid.empty() && _active_client_vdd_identity_token != 0) { remote_display_topology::instance().release_normal_game_identity( _active_client_uuid, _active_client_vdd_identity_token ); } #endif #if defined(_WIN32) || defined(__linux__) // A Remote Monitor kept for resume outlives its transport, but not the app it was showing: // once the app terminates there is nothing left to resume into. That retained identity is // what makes the check below answer "another streaming session is still active", so cleanup // and the revert both defer and nothing ever retries - the desktop keeps the monitor's // virtual display and the game's after a plain stop, with no stream running (2026-11-04 // 23:26:08: stopped, then three deferrals and two active SDD displays left behind). // App replacement is excluded: there the new session has already applied its topology. if (should_dispatch_revert && !skip_display_revert) { remote_display_topology::instance().release_abandoned_remote_monitors("app terminated"); } #endif // A draining normal capture or retained Remote Monitor keeps cleanup and // REVERT pending until the final capture reference has been released. const bool other_streaming_session_active = stream::session::has_shared_runtime_owner(); #ifdef _WIN32 if (_virtual_display_active) { if (!other_streaming_session_active) { const auto cleanup = platf::virtual_display_cleanup::run("app_termination", false); if (!cleanup.virtual_displays_removed) { BOOST_LOG(warning) << "Failed to remove virtual display after app termination."; } else { BOOST_LOG(info) << "Virtual display cleanup completed after app termination."; } } else { BOOST_LOG(info) << "Deferring virtual display removal after app termination because shared stream runtime is still owned."; } std::memset(&_virtual_display_guid, 0, sizeof(_virtual_display_guid)); _virtual_display_active = false; } if (_runtime_output_override_lease) { (void) config::clear_runtime_output_name_override_if_lease(*_runtime_output_override_lease); _runtime_output_override_lease.reset(); } #endif // Only show the Stopped notification if we actually have an app to stop // Since terminate() is always run when a new app has started if (should_dispatch_revert) { #if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 system_tray::update_tray_stopped(proc::proc.get_last_run_app_name()); #endif } // Load the configured output_name first // to prevent the value being write to empty when the initial terminate happens if (!has_run && initial_display.empty()) { initial_display = config::video.output_name; } else { // Restore output name to its original value config::video.output_name = initial_display; } if (should_dispatch_revert && skip_display_revert) { #ifdef _WIN32 clear_deferred_display_revert(); BOOST_LOG(info) << "Skipping display revert during app replacement because the new session has already applied its display configuration."; #endif } else if (should_dispatch_revert && !other_streaming_session_active) { #ifdef _WIN32 clear_deferred_display_revert(); const bool reverted = display_helper_integration::revert(); if (reverted && rtsp_stream::session_count_no_cleanup() == 0) { BOOST_LOG(debug) << "Display helper: stopping watchdog after app termination."; display_helper_integration::stop_watchdog(); } #elif defined(__linux__) platf::linux_display::backend().cancel_scheduled_revert(); if (config::video.dd.config_revert_delay.count() > 0) { platf::linux_display::backend().schedule_revert( config::video.dd.config_revert_delay, "app-end delay" ); } else { (void) display_helper_integration::revert(); } #endif } else if (should_dispatch_revert && other_streaming_session_active) { #ifdef _WIN32 defer_display_revert(); #endif BOOST_LOG(info) << "Deferring display revert after app termination because another streaming session is still active."; } _active_client_uuid.clear(); _active_client_vdd_identity_token = 0; _app_launch_time = {}; _app_id = -1; _app_name.clear(); _app = {}; display_name.clear(); initial_display.clear(); _launch_session.reset(); virtual_display = false; allow_client_commands = false; if (_saved_input_config) { config::input = *_saved_input_config; _saved_input_config.reset(); } if (needs_refresh) { refresh(config::stream.file_apps, false); } // Clear any per-app runtime config overrides now that the app is terminating. // If we can safely hot-apply immediately, restore global config now; otherwise defer. if (has_run) { config::clear_runtime_config_overrides(); if (!other_streaming_session_active) { config::apply_config_now(); } else { config::mark_deferred_reload(); } } } active_session_guard_t proc_t::active_session_guard() const { std::scoped_lock lk(_apps_mutex); active_session_guard_t guard; guard.has_active_app = _app_id > 0; guard.playnite_id = guard.has_active_app ? _app.playnite_id : std::string(); guard.uses_playnite = guard.has_active_app && !_app.playnite_id.empty(); guard.client_uuid = guard.has_active_app ? _active_client_uuid : std::string(); guard.normal_vdd_identity_token = guard.has_active_app ? _active_client_vdd_identity_token : 0; guard.launch_started_at = _app_launch_time; return guard; } std::vector proc_t::get_apps() const { std::scoped_lock lk(_apps_mutex); return _apps; } // Gets application image from application list. // Returns image from assets directory if found there. // Returns default image if image configuration is not set. // Returns http content-type header compatible image type. std::string proc_t::get_app_image(int app_id) { std::scoped_lock lk(_apps_mutex); auto resolved_app = resolve_app_from_snapshot(_apps, std::to_string(app_id), ""); auto app_image_path = resolved_app ? resolved_app->image_path : std::string(); return validate_app_image_path(app_image_path); } std::optional proc_t::resolve_app(const std::string &appid, const std::string &appuuid) const { std::scoped_lock lk(_apps_mutex); return resolve_app_from_snapshot(_apps, appid, appuuid); } std::optional proc_t::resolve_app(int app_id) const { if (app_id <= 0) { return std::nullopt; } return resolve_app(std::to_string(app_id), ""); } std::string proc_t::get_last_run_app_name() { return _app_name; } std::string proc_t::get_running_app_uuid() { return _app.uuid; } bp::environment proc_t::get_env() { return _env; } bool proc_t::last_run_app_frame_gen_limiter_fix() const { return _app.frame_gen_limiter_fix; } bool proc_t::is_launch_deferred() const { #ifdef _WIN32 return _deferred_launch; #else return false; #endif } proc_t::~proc_t() { // It's not safe to call terminate() here because our proc_t is a static variable // that may be destroyed after the Boost loggers have been destroyed. Instead, // we return a deinit_t to main() to handle termination when we're exiting. // Once we reach this point here, termination must have already happened. assert(!placebo); assert(!_process.running()); } std::string_view::iterator find_match(std::string_view::iterator begin, std::string_view::iterator end) { int stack = 0; --begin; do { ++begin; switch (*begin) { case '(': ++stack; break; case ')': --stack; } } while (begin != end && stack != 0); if (begin == end) { throw std::out_of_range("Missing closing bracket \')\'"); } return begin; } std::string parse_env_val(bp::native_environment &env, const std::string_view &val_raw) { auto pos = std::begin(val_raw); auto dollar = std::find(pos, std::end(val_raw), '$'); std::stringstream ss; while (dollar != std::end(val_raw)) { auto next = dollar + 1; if (next != std::end(val_raw)) { switch (*next) { case '(': { ss.write(pos, (dollar - pos)); auto var_begin = next + 1; auto var_end = find_match(next, std::end(val_raw)); auto var_name = std::string {var_begin, var_end}; #ifdef _WIN32 // Windows treats environment variable names in a case-insensitive manner, // so we look for a case-insensitive match here. This is critical for // correctly appending to PATH on Windows. auto itr = std::find_if(env.cbegin(), env.cend(), [&](const auto &e) { return boost::iequals(e.get_name(), var_name); }); if (itr != env.cend()) { // Use an existing case-insensitive match var_name = itr->get_name(); } #endif ss << env[var_name].to_string(); pos = var_end + 1; next = var_end; break; } case '$': ss.write(pos, (next - pos)); pos = next + 1; ++next; break; } dollar = std::find(next, std::end(val_raw), '$'); } else { BOOST_LOG(warning) << "Trailing '$' at end of environment variable value will be passed through literally"; dollar = next; } } ss.write(pos, (dollar - pos)); return ss.str(); } /** * @brief Validates a path whether it is a valid PNG. * @param path The path to the PNG file. * @return true if the file has a valid PNG signature, false otherwise. */ bool check_valid_png(const std::filesystem::path &path) { std::ifstream file(path, std::ios::binary); if (!file) { return false; } std::array header {}; file.read(reinterpret_cast(header.data()), static_cast(header.size())); return file.gcount() == static_cast(header.size()) && catalog::has_png_signature(header); } namespace { constexpr std::size_t maximum_machine_cover_bytes = 16U * 1024U * 1024U; [[maybe_unused]] bool machine_host_runtime() { const char *value = std::getenv("ARTLIGHT_MACHINE_HOST"); return value && std::string_view {value} == "1"; } #if defined(__linux__) bool same_file_snapshot(const struct stat &before, const struct stat &after) { return before.st_dev == after.st_dev && before.st_ino == after.st_ino && before.st_mode == after.st_mode && before.st_uid == after.st_uid && before.st_gid == after.st_gid && before.st_size == after.st_size && before.st_mtim.tv_sec == after.st_mtim.tv_sec && before.st_mtim.tv_nsec == after.st_mtim.tv_nsec && before.st_ctim.tv_sec == after.st_ctim.tv_sec && before.st_ctim.tv_nsec == after.st_ctim.tv_nsec; } std::optional confined_relative_path( const std::filesystem::path &candidate, const std::filesystem::path &root ) { const auto normalized_candidate = candidate.lexically_normal(); const auto normalized_root = root.lexically_normal(); auto relative = normalized_candidate.lexically_relative(normalized_root); if (relative.empty() || relative.is_absolute()) { return std::nullopt; } for (const auto &component : relative) { if (component == ".." || component == ".") { return std::nullopt; } } return relative; } std::optional read_machine_image( const std::string &path, const bool complete ) { const std::filesystem::path candidate {path}; const std::filesystem::path assets_root {SUNSHINE_ASSETS_DIR}; const std::filesystem::path covers_root = platf::appdata() / "covers"; if (!candidate.is_absolute() || !catalog::machine_image_path_is_confined(path, assets_root.string(), covers_root.string())) { return std::nullopt; } bool immutable_assets = false; auto relative = confined_relative_path(candidate, assets_root); auto root = assets_root; if (relative) { immutable_assets = true; } else { relative = confined_relative_path(candidate, covers_root); root = covers_root; } if (!relative) { return std::nullopt; } const int root_descriptor = open(root.c_str(), O_PATH | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW); if (root_descriptor < 0) { return std::nullopt; } struct stat root_attributes {}; const bool safe_root = fstat(root_descriptor, &root_attributes) == 0 && S_ISDIR(root_attributes.st_mode) && (root_attributes.st_mode & 07777) == (immutable_assets ? 0755 : 0700) && root_attributes.st_uid == (immutable_assets ? uid_t {0} : geteuid()); if (!safe_root) { close(root_descriptor); return std::nullopt; } const auto relative_string = relative->string(); const struct open_how how { .flags = O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOFOLLOW, .resolve = RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS | RESOLVE_NO_SYMLINKS | RESOLVE_NO_XDEV, }; int descriptor = static_cast(syscall( SYS_openat2, root_descriptor, relative_string.c_str(), &how, sizeof(how) )); if (descriptor < 0 && errno == ENOSYS) { descriptor = platf::linux_security::open_readonly_beneath( root_descriptor, *relative, root_attributes.st_dev, immutable_assets ? uid_t {0} : geteuid(), !immutable_assets ); } close(root_descriptor); if (descriptor < 0) { return std::nullopt; } struct stat before {}, after {}; if (fstat(descriptor, &before) != 0 || !S_ISREG(before.st_mode) || before.st_size < 8 || static_cast(before.st_size) > maximum_machine_cover_bytes || (before.st_mode & 07777) != (immutable_assets ? 0644 : 0600) || (immutable_assets ? before.st_uid != 0 : before.st_uid != geteuid())) { close(descriptor); return std::nullopt; } const auto bytes_to_read = complete ? static_cast(before.st_size) : std::size_t {8}; catalog::byte_buffer_t bytes(bytes_to_read); std::size_t offset = 0; while (offset < bytes.size()) { const auto received = read(descriptor, bytes.data() + offset, bytes.size() - offset); if (received < 0 && errno == EINTR) { continue; } if (received <= 0) { close(descriptor); return std::nullopt; } offset += static_cast(received); } const bool stable = fstat(descriptor, &after) == 0 && same_file_snapshot(before, after); close(descriptor); if (!stable || !catalog::has_png_signature(bytes)) { return std::nullopt; } return bytes; } #endif std::optional read_image_for_validation(const std::string &path) { #if defined(__linux__) if (machine_host_runtime()) { return read_machine_image(path, false); } #endif std::ifstream file(path, std::ios::binary); if (!file) { return std::nullopt; } catalog::byte_buffer_t header(8); file.read(reinterpret_cast(header.data()), static_cast(header.size())); if (file.gcount() != static_cast(header.size())) { return std::nullopt; } return header; } } // namespace std::string validate_app_image_path(std::string app_image_path) { return catalog::validate_image_path( std::move(app_image_path), SUNSHINE_ASSETS_DIR, DEFAULT_APP_IMAGE_PATH, read_image_for_validation ); } std::optional read_validated_app_image(const std::string &validated_path) { #if defined(__linux__) if (machine_host_runtime()) { const auto bytes = read_machine_image(validated_path, true); if (!bytes) { return std::nullopt; } return std::string { reinterpret_cast(bytes->data()), bytes->size() }; } #endif std::ifstream file(validated_path, std::ios::binary | std::ios::ate); if (!file) { return std::nullopt; } const auto size = file.tellg(); if (size < 8 || size > static_cast(maximum_machine_cover_bytes)) { return std::nullopt; } std::string bytes(static_cast(size), '\0'); file.seekg(0); const auto stream_size = static_cast(size); file.read(bytes.data(), stream_size); if (file.gcount() != stream_size || !catalog::has_png_signature(std::span {reinterpret_cast(bytes.data()), bytes.size()})) { return std::nullopt; } return bytes; } std::optional calculate_sha256(const std::string &filename) { crypto::md_ctx_t ctx {EVP_MD_CTX_create()}; if (!ctx) { return std::nullopt; } if (!EVP_DigestInit_ex(ctx.get(), EVP_sha256(), nullptr)) { return std::nullopt; } #if defined(__linux__) if (machine_host_runtime()) { const auto bytes = read_machine_image(filename, true); if (!bytes || !EVP_DigestUpdate(ctx.get(), bytes->data(), bytes->size())) { return std::nullopt; } unsigned char result[SHA256_DIGEST_LENGTH]; if (!EVP_DigestFinal_ex(ctx.get(), result, nullptr)) { return std::nullopt; } std::stringstream ss; ss << std::hex << std::setfill('0'); for (const auto &byte : result) { ss << std::setw(2) << static_cast(byte); } return ss.str(); } #endif // Read file and update calculated SHA char buf[1024 * 16]; std::ifstream file(filename, std::ifstream::binary); while (file.good()) { file.read(buf, sizeof(buf)); if (!EVP_DigestUpdate(ctx.get(), buf, file.gcount())) { return std::nullopt; } } file.close(); unsigned char result[SHA256_DIGEST_LENGTH]; if (!EVP_DigestFinal_ex(ctx.get(), result, nullptr)) { return std::nullopt; } // Transform byte-array to string std::stringstream ss; ss << std::hex << std::setfill('0'); for (const auto &byte : result) { ss << std::setw(2) << (int) byte; } return ss.str(); } uint32_t calculate_crc32(const std::string &input) { boost::crc_32_type result; result.process_bytes(input.data(), input.length()); return result.checksum(); } using app_id_alias_state_t = catalog::alias_state_t; std::string calculate_numeric_id_from_parts(const std::vector &parts, int index) { std::stringstream ss; for (const auto &part : parts) { ss << part; } ss << index; return std::to_string(abs((int32_t) calculate_crc32(ss.str()))); } std::string calculate_numeric_id_from_parts(const std::vector &parts) { std::stringstream ss; for (const auto &part : parts) { ss << part; } return std::to_string(abs((int32_t) calculate_crc32(ss.str()))); } std::tuple calculate_cover_versioned_app_id(const std::string &app_uuid, const std::string &cover_fingerprint, int index) { return catalog::calculate_versioned_ids(app_uuid, cover_fingerprint, index); } std::string calculate_app_cover_fingerprint(std::string app_image_path) { const auto file_path = validate_app_image_path(std::move(app_image_path)); if (file_path == DEFAULT_APP_IMAGE_PATH) { return "default"; } auto file_hash = calculate_sha256(file_path); if (file_hash) { return "sha256:" + file_hash.value(); } BOOST_LOG(warning) << "Failed to compute SHA256 for image ["sv << file_path << "], falling back to path for app art version"; return "path:" + file_path; } std::map load_app_id_alias_state() { std::map result; statefile::migrate_recent_state_keys(); const auto &path = statefile::vibeshine_state_path(); if (path.empty()) { return result; } std::lock_guard lock(statefile::state_mutex()); pt::ptree tree; if (!statefile::load_json_for_update(path, tree)) { BOOST_LOG(warning) << "Unable to load state file for app ID aliases; using in-memory app IDs for this refresh."; return result; } const auto aliases_root = tree.get_child_optional("root.app_id_aliases"); if (!aliases_root) { return result; } for (const auto &app_node : *aliases_root) { const auto &app_uuid = app_node.first; if (app_uuid.empty()) { continue; } app_id_alias_state_t state; state.current_id = app_node.second.get("current_id", ""); state.cover_fingerprint = app_node.second.get("cover_fingerprint", ""); if (const auto aliases = app_node.second.get_child_optional("aliases")) { for (const auto &alias_node : *aliases) { auto alias = alias_node.second.get_value(""); boost::algorithm::trim(alias); if (!alias.empty()) { state.aliases.insert(std::move(alias)); } } } if (!state.current_id.empty()) { result.emplace(app_uuid, std::move(state)); } } return result; } bool save_app_id_alias_state(const std::map &state) { statefile::migrate_recent_state_keys(); const auto &path = statefile::vibeshine_state_path(); if (path.empty()) { return false; } std::lock_guard lock(statefile::state_mutex()); pt::ptree tree; if (!statefile::load_json_for_update(path, tree)) { BOOST_LOG(warning) << "Unable to update state file with app ID aliases."; return false; } pt::ptree aliases_root; for (const auto &[app_uuid, entry] : state) { if (app_uuid.empty() || entry.current_id.empty()) { continue; } pt::ptree app_node; app_node.put("current_id", entry.current_id); app_node.put("cover_fingerprint", entry.cover_fingerprint); pt::ptree aliases_node; for (const auto &alias : entry.aliases) { if (alias.empty() || alias == entry.current_id) { continue; } pt::ptree alias_node; alias_node.put_value(alias); aliases_node.push_back(std::make_pair("", std::move(alias_node))); } app_node.put_child("aliases", aliases_node); aliases_root.push_back(std::make_pair(app_uuid, std::move(app_node))); } pt::ptree empty_root; pt::ptree root = tree.get_child("root", empty_root); root.put_child("app_id_aliases", std::move(aliases_root)); tree.put_child("root", std::move(root)); try { statefile::write_json_atomic(path, tree); return true; } catch (const std::exception &e) { BOOST_LOG(warning) << "Failed to persist app ID aliases: " << e.what(); return false; } } void remember_alias(app_id_alias_state_t &state, const std::string &alias) { if (!alias.empty() && alias != state.current_id) { state.aliases.insert(alias); } } void assign_compatible_app_id( ctx_t &ctx, const std::string &app_name, int index, std::set &ids, std::map &alias_state, std::set &active_uuids, bool &alias_state_changed ) { catalog::app_identity_t identity; identity.name = app_name; identity.uuid = ctx.uuid; if (ctx.uuid.empty()) { const auto validated = validate_app_image_path(ctx.image_path); if (validated != DEFAULT_APP_IMAGE_PATH) { identity.legacy_image_identity = calculate_sha256(validated).value_or(validated); } } else { identity.art_version = calculate_app_cover_fingerprint(ctx.image_path); } catalog::assign_compatible_id(identity, index, ids, alias_state, active_uuids, alias_state_changed); ctx.id = std::move(identity.id); ctx.art_version = std::move(identity.art_version); ctx.id_aliases = std::move(identity.aliases); } void prune_and_filter_app_id_alias_state( std::vector &apps, std::map &alias_state, const std::set &active_uuids, bool &alias_state_changed ) { std::vector identities; identities.reserve(apps.size()); for (const auto &app : apps) { identities.push_back({app.name, app.uuid, {}, app.art_version, app.id, app.id_aliases}); } catalog::prune_and_filter_aliases(identities, alias_state, active_uuids, alias_state_changed); for (std::size_t i = 0; i < apps.size(); ++i) { apps[i].id_aliases = std::move(identities[i].aliases); } } std::optional resolve_app_from_snapshot(const std::vector &apps, const std::string &appid, const std::string &appuuid) { std::vector identities; identities.reserve(apps.size()); for (const auto &app : apps) { identities.push_back({app.name, app.uuid, {}, app.art_version, app.id, app.id_aliases}); } const auto resolved = catalog::resolve_app(identities, appid, appuuid); return resolved ? std::optional {apps[*resolved]} : std::nullopt; } std::tuple calculate_app_id( const std::string &app_name, const std::string &app_uuid, std::string app_image_path, int index ) { // Prefer the persistent app UUID for stable client-facing IDs. Artwork can be // refreshed by Playnite sync, so image bytes must not affect launch identity. std::vector to_hash; if (!app_uuid.empty()) { to_hash.push_back(app_uuid); } else { // Legacy fallback for app entries that predate UUID normalization. to_hash.push_back(app_name); auto file_path = validate_app_image_path(app_image_path); if (file_path != DEFAULT_APP_IMAGE_PATH) { auto file_hash = calculate_sha256(file_path); if (file_hash) { to_hash.push_back(file_hash.value()); } else { BOOST_LOG(warning) << "Failed to compute SHA256 for image ["sv << file_path << "], falling back to path for app ID hash"; // Fallback to just hashing image path. to_hash.push_back(file_path); } } } const std::string legacy_image_identity = to_hash.size() > 1 ? to_hash[1] : std::string {}; return catalog::calculate_ids(app_name, app_uuid, legacy_image_identity, index); } /** * @brief Migrate the applications stored in the file tree by merging in a new app. * * This function updates the application entries in *fileTree_p* using the data in *inputTree_p*. * If an app in the file tree does not have a UUID, one is generated and inserted. * If an app with the same UUID as the new app is found, it is replaced. * Additionally, empty keys (such as "prep-cmd" or "detached") and keys no longer needed ("launching", "index") * are removed from the input. * * Legacy versions of Sunshine/Apollo stored boolean and integer values as strings. * The following keys are converted: * - Boolean keys: "exclude-global-prep-cmd", "elevated", "auto-detach", "wait-all", * "use-app-identity", "per-client-app-identity", "virtual-display" * - Integer keys: "exit-timeout" * * A migration version is stored in the file tree (under "version") so that future changes can be applied. * * @param fileTree_p Pointer to the JSON object representing the file tree. * @param inputTree_p Pointer to the JSON object representing the new app. */ void migrate_apps(nlohmann::json *fileTree_p, nlohmann::json *inputTree_p) { std::string new_app_uuid; if (inputTree_p) { // If the input contains a non-empty "uuid", use it; otherwise generate one. if (inputTree_p->contains("uuid") && !(*inputTree_p)["uuid"].get().empty()) { new_app_uuid = (*inputTree_p)["uuid"].get(); } else { new_app_uuid = uuid_util::uuid_t::generate().string(); (*inputTree_p)["uuid"] = new_app_uuid; } // Remove "prep-cmd" if empty. if (inputTree_p->contains("prep-cmd") && (*inputTree_p)["prep-cmd"].empty()) { inputTree_p->erase("prep-cmd"); } // Remove "detached" if empty. if (inputTree_p->contains("detached") && (*inputTree_p)["detached"].empty()) { inputTree_p->erase("detached"); } // Remove keys that are no longer needed. inputTree_p->erase("launching"); inputTree_p->erase("index"); } // Get the current apps array; if it doesn't exist, create one. nlohmann::json newApps = nlohmann::json::array(); if (fileTree_p->contains("apps") && (*fileTree_p)["apps"].is_array()) { for (auto &app : (*fileTree_p)["apps"]) { // For apps without a UUID, generate one and remove "launching". if (!app.contains("uuid") || app["uuid"].get().empty()) { app["uuid"] = uuid_util::uuid_t::generate().string(); app.erase("launching"); newApps.push_back(std::move(app)); } else { // If an app with the same UUID as the new app is found, replace it. if (!new_app_uuid.empty() && app["uuid"].get() == new_app_uuid) { newApps.push_back(*inputTree_p); new_app_uuid.clear(); } else { newApps.push_back(std::move(app)); } } } } // If the new app's UUID has not been merged yet, add it. if (!new_app_uuid.empty() && inputTree_p) { newApps.push_back(*inputTree_p); } (*fileTree_p)["apps"] = newApps; } void migration_v2(nlohmann::json &fileTree) { static const int this_version = 2; // Determine the current migration version (default to 1 if not present). int file_version = 1; if (fileTree.contains("version")) { try { file_version = fileTree["version"].get(); } catch (const std::exception &e) { BOOST_LOG(info) << "Cannot parse apps.json version, treating as v1: " << e.what(); } } // If the version is less than this_version, perform legacy conversion. if (file_version < this_version) { BOOST_LOG(info) << "Migrating app list from v1 to v2..."; migrate_apps(&fileTree, nullptr); // List of keys to convert to booleans. std::vector boolean_keys = { "allow-client-commands", "exclude-global-prep-cmd", "elevated", "auto-detach", "wait-all", "prefer-10bit-sdr", "use-app-identity", "per-client-app-identity", "virtual-display" }; // List of keys to convert to integers. std::vector integer_keys = { "exit-timeout", "scale-factor" }; // Walk through each app and convert legacy string values. for (auto &app : fileTree["apps"]) { for (const auto &key : boolean_keys) { if (app.contains(key)) { auto &_key = app[key]; if (_key.is_string()) { std::string s = _key.get(); std::transform(s.begin(), s.end(), s.begin(), ::tolower); // Normalize to lowercase for comparison _key = (s == "true" || s == "on" || s == "yes"); } else if (_key.is_array()) { // Check if the array contains at least one item and interpret the first element if (!_key.empty() && _key[0].is_string()) { std::string first = _key[0].get(); std::transform(first.begin(), first.end(), first.begin(), ::tolower); // Normalize if (first == "on" || first == "true" || first == "yes") { _key = true; } else if (first == "off" || first == "false" || first == "no") { _key = false; } else { _key = false; // Default for unknown values } } else { _key = false; // Treat empty arrays or non-string first elements as false } } else { // Fallback: Treat truthy/falsey cases if (_key.is_boolean()) { // Leave booleans as they are } else if (_key.is_number()) { _key = (_key.get() != 0); // Non-zero numbers are truthy } else if (_key.is_null()) { _key = false; // Null is false } else { _key = !_key.empty(); // Non-empty objects/arrays are truthy, empty ones are falsey } } } } for (const auto &key : integer_keys) { if (app.contains(key) && app[key].is_string()) { std::string s = app[key].get(); app[key] = std::stoi(s); } } // For each entry in the "prep-cmd" array, convert "elevated" if necessary. if (app.contains("prep-cmd") && app["prep-cmd"].is_array()) { for (auto &prep : app["prep-cmd"]) { if (prep.contains("elevated") && prep["elevated"].is_string()) { std::string s = prep["elevated"].get(); prep["elevated"] = (s == "true"); } } } } // Update migration version to this_version. fileTree["version"] = this_version; BOOST_LOG(info) << "Migrated app list from v1 to v2."; } } void migrate(nlohmann::json &fileTree, const std::string &fileName) { int last_version = 2; int file_version = 0; if (fileTree.contains("version")) { file_version = fileTree["version"].get(); } if (file_version < last_version) { migration_v2(fileTree); file_handler::write_file(fileName.c_str(), fileTree.dump(4)); } } std::optional parse(const std::string &file_name) { // Prepare environment variables. auto this_env = bp::this_process::env(); #if defined(__linux__) // Keep the daemon's own HOME pointed at the machine-owned profile while // giving desktop applications the environment of the session account // that the capability helper will actually enter. if (std::getenv("ARTLIGHT_MACHINE_HOST")) { const auto *session_home = std::getenv("ARTLIGHT_SESSION_HOME"); const auto *session_user = std::getenv("ARTLIGHT_SESSION_USER"); if (session_home && *session_home && session_user && *session_user) { this_env["HOME"] = session_home; this_env["USER"] = session_user; this_env["LOGNAME"] = session_user; this_env["XDG_CONFIG_HOME"] = std::string {session_home} + "/.config"; this_env["XDG_DATA_HOME"] = std::string {session_home} + "/.local/share"; } } #endif std::set ids; std::vector apps; auto app_id_alias_state = load_app_id_alias_state(); std::set active_app_uuids; bool app_id_alias_state_changed = false; int i = 0; bool apps_parsed_ok = false; size_t fail_count = 0; do { // Read the JSON file into a tree. nlohmann::json tree; try { std::string content = file_handler::read_file(file_name.c_str()); tree = nlohmann::json::parse(content); } catch (const std::exception &e) { BOOST_LOG(warning) << "Couldn't read apps.json properly! Apps will not be loaded."sv; break; } try { migrate(tree, file_name); if (tree.contains("env") && tree["env"].is_object()) { for (auto &item : tree["env"].items()) { if (!is_valid_env_key(item.key())) { BOOST_LOG(warning) << "Skipping invalid environment variable name ["sv << item.key() << ']'; continue; } this_env[item.key()] = parse_env_val(this_env, item.value().get()); } } // Ensure the "apps" array exists. if (!tree.contains("apps") || !tree["apps"].is_array()) { BOOST_LOG(warning) << "No apps were defined in apps.json!!!"sv; break; } // Iterate over each application in the "apps" array. for (auto &app_node : tree["apps"]) { proc::ctx_t ctx {}; ctx.idx = std::to_string(i); ctx.uuid = app_node.at("uuid"); // Build the list of preparation commands. std::vector prep_cmds; bool exclude_global_prep = app_node.value("exclude-global-prep-cmd", false); if (!exclude_global_prep) { prep_cmds.reserve(config::sunshine.prep_cmds.size()); for (auto &prep_cmd : config::sunshine.prep_cmds) { auto do_cmd = parse_env_val(this_env, prep_cmd.do_cmd); auto undo_cmd = parse_env_val(this_env, prep_cmd.undo_cmd); prep_cmds.emplace_back( std::move(do_cmd), std::move(undo_cmd), std::move(prep_cmd.elevated) ); } } if (app_node.contains("prep-cmd") && app_node["prep-cmd"].is_array()) { const auto &prep_nodes = app_node["prep-cmd"]; prep_cmds.reserve(prep_cmds.size() + prep_nodes.size()); for (const auto &prep_node : prep_nodes) { std::string do_cmd = parse_env_val(this_env, prep_node.value("do", "")); std::string undo_cmd = parse_env_val(this_env, prep_node.value("undo", "")); bool elevated = prep_node.value("elevated", false); prep_cmds.emplace_back( std::move(do_cmd), std::move(undo_cmd), std::move(elevated) ); } } // Build the list of pause/resume commands. std::vector state_cmds; bool exclude_global_state_cmds = app_node.value("exclude-global-state-cmd", false); if (!exclude_global_state_cmds) { state_cmds.reserve(config::sunshine.state_cmds.size()); for (auto &state_cmd : config::sunshine.state_cmds) { auto do_cmd = parse_env_val(this_env, state_cmd.do_cmd); auto undo_cmd = parse_env_val(this_env, state_cmd.undo_cmd); state_cmds.emplace_back( std::move(do_cmd), std::move(undo_cmd), std::move(state_cmd.elevated) ); } } if (app_node.contains("state-cmd") && app_node["state-cmd"].is_array()) { for (auto &prep_node : app_node["state-cmd"]) { std::string do_cmd = parse_env_val(this_env, prep_node.value("do", "")); std::string undo_cmd = parse_env_val(this_env, prep_node.value("undo", "")); bool elevated = prep_node.value("elevated", false); state_cmds.emplace_back( std::move(do_cmd), std::move(undo_cmd), std::move(elevated) ); } } // Build the list of detached commands. std::vector detached; if (app_node.contains("detached") && app_node["detached"].is_array()) { for (auto &detached_val : app_node["detached"]) { detached.emplace_back(parse_env_val(this_env, detached_val.get())); } } // Process other fields. if (app_node.contains("output")) { ctx.output = parse_env_val(this_env, app_node.value("output", "")); } if (app_node.contains("display-output")) { ctx.output_name_override = parse_env_val(this_env, app_node.value("display-output", "")); } std::string name = parse_env_val(this_env, app_node.value("name", "")); if (app_node.contains("cmd")) { ctx.cmd = parse_env_val(this_env, app_node.value("cmd", "")); } if (app_node.contains("working-dir")) { ctx.working_dir = parse_env_val(this_env, app_node.value("working-dir", "")); #ifdef _WIN32 // The working directory, unlike the command itself, should not be quoted. boost::erase_all(ctx.working_dir, "\""); ctx.working_dir += '\\'; #endif } if (app_node.contains("steam-id") && app_node["steam-id"].is_string()) { ctx.steam_id = parse_env_val(this_env, app_node["steam-id"].get()); } if (app_node.contains("steam-install-dir") && app_node["steam-install-dir"].is_string()) { ctx.steam_install_dir = parse_env_val(this_env, app_node["steam-install-dir"].get()); } if (app_node.contains("lutris-id") && app_node["lutris-id"].is_string()) { ctx.lutris_id = parse_env_val(this_env, app_node["lutris-id"].get()); } if (app_node.contains("lutris-directory") && app_node["lutris-directory"].is_string()) { ctx.lutris_directory = parse_env_val(this_env, app_node["lutris-directory"].get()); } if (app_node.contains("image-path")) { ctx.image_path = parse_env_val(this_env, app_node.value("image-path", "")); } // Parse per-app global config overrides, keeping values in raw config-file format. if (app_node.contains("config-overrides") && app_node["config-overrides"].is_object()) { for (const auto &item : app_node["config-overrides"].items()) { const auto &val = item.value(); if (val.is_null()) { continue; } std::string encoded; if (val.is_string()) { encoded = parse_env_val(this_env, val.get()); } else { encoded = val.dump(); } if (!encoded.empty()) { ctx.config_overrides.emplace(item.key(), std::move(encoded)); } } } { std::unordered_map normalized_overrides; config::merge_config_overrides(normalized_overrides, ctx.config_overrides); ctx.config_overrides = std::move(normalized_overrides); } ctx.frame_gen_limiter_fix = util::get_non_string_json_value(app_node, "frame-gen-limiter-fix", util::get_non_string_json_value(app_node, "dlss-framegen-limiter-fix", false)); ctx.elevated = util::get_non_string_json_value(app_node, "elevated", false); ctx.virtual_screen = util::get_non_string_json_value(app_node, "virtual-screen", false); ctx.auto_detach = util::get_non_string_json_value(app_node, "auto-detach", true); ctx.wait_all = util::get_non_string_json_value(app_node, "wait-all", true); ctx.exit_timeout = std::chrono::seconds {util::get_non_string_json_value(app_node, "exit-timeout", 10)}; ctx.virtual_display = util::get_non_string_json_value(app_node, "virtual-display", false); ctx.virtual_display_primary = util::get_non_string_json_value(app_node, "virtual-display-primary", false); ctx.scale_factor = util::get_non_string_json_value(app_node, "scale-factor", 100); ctx.use_app_identity = util::get_non_string_json_value(app_node, "use-app-identity", false); ctx.per_client_app_identity = util::get_non_string_json_value(app_node, "per-client-app-identity", false); ctx.allow_client_commands = util::get_non_string_json_value(app_node, "allow-client-commands", true); ctx.terminate_on_pause = util::get_non_string_json_value(app_node, "terminate-on-pause", false); ctx.gamepad = app_node.value("gamepad", ""); const bool frame_generation_capture_fix_enabled = util::get_non_string_json_value(app_node, "gen1-framegen-fix", util::get_non_string_json_value(app_node, "dlss-framegen-capture-fix", false)) || util::get_non_string_json_value(app_node, "gen2-framegen-fix", false); ctx.gen1_framegen_fix = frame_generation_capture_fix_enabled; ctx.gen2_framegen_fix = false; auto virtual_display_mode = util::get_non_string_json_value(app_node, "virtual-display-mode", ""); auto virtual_display_layout = util::get_non_string_json_value(app_node, "virtual-display-layout", ""); if (ctx.virtual_screen) { if (!virtual_display_mode.empty()) { auto normalized = boost::algorithm::to_lower_copy(virtual_display_mode); if (normalized == "disabled") { ctx.virtual_display_mode_override = config::video_t::virtual_display_mode_e::disabled; } else if (normalized == "per_client") { ctx.virtual_display_mode_override = config::video_t::virtual_display_mode_e::per_client; } else if (normalized == "shared") { ctx.virtual_display_mode_override = config::video_t::virtual_display_mode_e::shared; } } if (!virtual_display_layout.empty()) { auto normalized = boost::algorithm::to_lower_copy(virtual_display_layout); if (normalized == "exclusive") { ctx.virtual_display_layout_override = config::video_t::virtual_display_layout_e::exclusive; } else if (normalized == "extended") { ctx.virtual_display_layout_override = config::video_t::virtual_display_layout_e::extended; } else if (normalized == "extended_primary") { ctx.virtual_display_layout_override = config::video_t::virtual_display_layout_e::extended_primary; } else if (normalized == "extended_isolated") { ctx.virtual_display_layout_override = config::video_t::virtual_display_layout_e::extended_isolated; } else if (normalized == "extended_primary_isolated") { ctx.virtual_display_layout_override = config::video_t::virtual_display_layout_e::extended_primary_isolated; } } } std::optional lossless_scaling_launch_delay; if (app_node.contains("lossless-scaling-launch-delay")) { lossless_scaling_launch_delay = util::get_non_string_json_value(app_node, "lossless-scaling-launch-delay", 0); } std::optional legacy_override; if (app_node.contains("lossless-scaling-legacy-auto-detect")) { legacy_override = util::get_non_string_json_value(app_node, "lossless-scaling-legacy-auto-detect", false); } if (app_node.contains("prefer-10bit-sdr") && !app_node["prefer-10bit-sdr"].is_null()) { ctx.prefer_10bit_sdr = util::get_non_string_json_value(app_node, "prefer-10bit-sdr", false); } std::optional dd_config_override; if (app_node.contains("dd-configuration-option")) { dd_config_override = util::get_non_string_json_value(app_node, "dd-configuration-option", ""); } ctx.lossless_scaling_launch_delay_seconds = std::max(0, lossless_scaling_launch_delay.value_or(kLosslessScalingDefaultLaunchDelaySeconds)); ctx.lossless_scaling_legacy_auto_detect = legacy_override.value_or(config::lossless_scaling.legacy_auto_detect); if (dd_config_override && !dd_config_override->empty()) { const auto trimmed = boost::algorithm::trim_copy(*dd_config_override); if (boost::iequals(trimmed, "verify_only")) { ctx.dd_config_option_override = config::video_t::dd_t::config_option_e::verify_only; } else if (boost::iequals(trimmed, "ensure_active")) { ctx.dd_config_option_override = config::video_t::dd_t::config_option_e::ensure_active; } else if (boost::iequals(trimmed, "ensure_primary")) { ctx.dd_config_option_override = config::video_t::dd_t::config_option_e::ensure_primary; } else if (boost::iequals(trimmed, "ensure_only_display")) { ctx.dd_config_option_override = config::video_t::dd_t::config_option_e::ensure_only_display; } else if (boost::iequals(trimmed, "disabled")) { ctx.dd_config_option_override = config::video_t::dd_t::config_option_e::disabled; } else { ctx.dd_config_option_override.reset(); } } ctx.playnite_id.clear(); if (app_node.contains("playnite-id") && app_node["playnite-id"].is_string()) { try { ctx.playnite_id = parse_env_val(this_env, app_node["playnite-id"].get()); } catch (...) { ctx.playnite_id.clear(); } } ctx.playnite_fullscreen = false; if (app_node.contains("playnite-fullscreen")) { try { const auto &flag = app_node["playnite-fullscreen"]; if (flag.is_boolean()) { ctx.playnite_fullscreen = flag.get(); } else if (flag.is_number_integer()) { ctx.playnite_fullscreen = flag.get() != 0; } else if (flag.is_string()) { auto text = flag.get(); boost::algorithm::trim(text); boost::algorithm::to_lower(text); ctx.playnite_fullscreen = (text == "true" || text == "1" || text == "yes"); } } catch (...) { ctx.playnite_fullscreen = false; } } const bool has_lossless_scaling_enabled = app_node.contains("lossless-scaling-enabled"); ctx.lossless_scaling_enabled = util::get_non_string_json_value(app_node, "lossless-scaling-enabled", false); ctx.lossless_scaling_framegen = util::get_non_string_json_value(app_node, "lossless-scaling-framegen", false); if (!has_lossless_scaling_enabled) { ctx.lossless_scaling_enabled = ctx.lossless_scaling_framegen; } ctx.frame_generation_provider = "lossless-scaling"; if (auto it = app_node.find("frame-generation-provider"); it != app_node.end() && it->is_string()) { ctx.frame_generation_provider = normalize_frame_generation_provider(it->get()); } if (auto it = app_node.find("frame-generation-mode"); it != app_node.end() && it->is_string()) { const auto trimmed_mode = boost::algorithm::trim_copy(it->get()); if (boost::iequals(trimmed_mode, "off") || boost::iequals(trimmed_mode, "none") || boost::iequals(trimmed_mode, "disabled")) { ctx.frame_generation_enabled = false; ctx.lossless_scaling_framegen = false; ctx.frame_generation_provider = "lossless-scaling"; // Frame generation explicitly off: legacy capture-fix flags must not re-enable it. ctx.gen1_framegen_fix = false; } else { ctx.frame_generation_provider = normalize_frame_generation_provider(trimmed_mode); ctx.frame_generation_enabled = true; ctx.lossless_scaling_framegen = ctx.frame_generation_provider == "lossless-scaling"; } } else { ctx.frame_generation_enabled = ctx.lossless_scaling_framegen || ctx.frame_generation_provider == "game-provided" || ctx.frame_generation_provider == "nvidia-smooth-motion" || ctx.gen1_framegen_fix || ctx.gen2_framegen_fix; } ctx.lossless_scaling_target_fps.reset(); double lossless_target_fps = util::get_non_string_json_value(app_node, "lossless-scaling-target-fps", 0.0); if (lossless_target_fps > 0) { ctx.lossless_scaling_target_fps = lossless_target_fps; } ctx.lossless_scaling_rtss_limit.reset(); int lossless_rtss_limit = util::get_non_string_json_value(app_node, "lossless-scaling-rtss-limit", 0); if (lossless_rtss_limit > 0) { ctx.lossless_scaling_rtss_limit = lossless_rtss_limit; } ctx.lossless_scaling_profile = LOSSLESS_PROFILE_CUSTOM; if (auto it = app_node.find("lossless-scaling-profile"); it != app_node.end() && it->is_string()) { if (boost::iequals(it->get(), LOSSLESS_PROFILE_RECOMMENDED)) { ctx.lossless_scaling_profile = LOSSLESS_PROFILE_RECOMMENDED; } } if (auto it = app_node.find("lossless-scaling-recommended"); it != app_node.end()) { populate_lossless_overrides(*it, ctx.lossless_scaling_recommended); } if (auto it = app_node.find("lossless-scaling-custom"); it != app_node.end()) { populate_lossless_overrides(*it, ctx.lossless_scaling_custom); } if (!ctx.lossless_scaling_framegen) { ctx.lossless_scaling_target_fps.reset(); ctx.lossless_scaling_rtss_limit.reset(); } // Calculate a unique application id. assign_compatible_app_id(ctx, name, i++, ids, app_id_alias_state, active_app_uuids, app_id_alias_state_changed); ctx.name = std::move(name); ctx.prep_cmds = std::move(prep_cmds); ctx.state_cmds = std::move(state_cmds); ctx.detached = std::move(detached); apps.emplace_back(std::move(ctx)); } fail_count = 0; apps_parsed_ok = true; } catch (std::exception &e) { BOOST_LOG(error) << "Error happened during app loading: "sv << e.what(); fail_count += 1; if (fail_count >= 3) { // No hope for recovering BOOST_LOG(warning) << "Couldn't parse/migrate apps.json properly! Apps will not be loaded."sv; break; } BOOST_LOG(warning) << "App format is still invalid! Trying to re-migrate the app list..."sv; // Always try migrating from scratch when error happened tree["version"] = 0; try { migrate(tree, file_name); } catch (std::exception &e) { BOOST_LOG(error) << "Error happened during migration: "sv << e.what(); break; } this_env = bp::this_process::env(); ids.clear(); apps.clear(); i = 0; continue; } break; } while (fail_count < 3); if (fail_count > 0) { BOOST_LOG(warning) << "No applications configured, adding fallback Desktop entry."; proc::ctx_t ctx {}; ctx.idx = std::to_string(i); ctx.uuid = FALLBACK_DESKTOP_UUID; // Placeholder UUID ctx.name = "Desktop (fallback)"; ctx.image_path = parse_env_val(this_env, "desktop-alt.png"); ctx.virtual_display = false; ctx.scale_factor = 100; ctx.use_app_identity = false; ctx.per_client_app_identity = false; ctx.allow_client_commands = false; ctx.terminate_on_pause = false; ctx.elevated = false; ctx.auto_detach = true; ctx.wait_all = false; // Desktop doesn't have a specific command to wait for ctx.exit_timeout = 5s; // Calculate unique ID auto possible_ids = calculate_app_id(ctx.name, ctx.uuid, ctx.image_path, i++); if (ids.count(std::get<0>(possible_ids)) == 0) { // Avoid using index to generate id if possible ctx.id = std::get<0>(possible_ids); } else { // Fallback to include index on collision ctx.id = std::get<1>(possible_ids); } ids.insert(ctx.id); apps.emplace_back(std::move(ctx)); } // Virtual Display entry #ifdef _WIN32 if (vDisplayDriverStatus.load(std::memory_order_acquire) == VDISPLAY::DRIVER_STATUS::OK) { proc::ctx_t ctx {}; ctx.idx = std::to_string(i); ctx.uuid = VIRTUAL_DISPLAY_UUID; ctx.name = "Virtual Display"; ctx.image_path = parse_env_val(this_env, "virtual_desktop.png"); ctx.virtual_display = true; ctx.scale_factor = 100; ctx.use_app_identity = false; ctx.per_client_app_identity = false; ctx.allow_client_commands = false; ctx.terminate_on_pause = false; ctx.elevated = false; ctx.auto_detach = true; ctx.wait_all = false; ctx.exit_timeout = 5s; auto possible_ids = calculate_app_id(ctx.name, ctx.uuid, ctx.image_path, i++); if (ids.count(std::get<0>(possible_ids)) == 0) { // Avoid using index to generate id if possible ctx.id = std::get<0>(possible_ids); } else { // Fallback to include index on collision ctx.id = std::get<1>(possible_ids); } ids.insert(ctx.id); apps.emplace_back(std::move(ctx)); } #endif if (config::input.enable_input_only_mode) { // Input Only entry { proc::ctx_t ctx {}; ctx.idx = std::to_string(i); ctx.uuid = REMOTE_INPUT_UUID; ctx.name = "Remote Input"; ctx.image_path = parse_env_val(this_env, "input_only.png"); ctx.virtual_display = false; ctx.scale_factor = 100; ctx.use_app_identity = false; ctx.per_client_app_identity = false; ctx.allow_client_commands = false; ctx.terminate_on_pause = true; // There's no need to keep an active input only session ongoing ctx.elevated = false; ctx.auto_detach = true; ctx.wait_all = true; ctx.exit_timeout = 5s; auto possible_ids = calculate_app_id(ctx.name, ctx.uuid, ctx.image_path, i++); if (ids.count(std::get<0>(possible_ids)) == 0) { // Avoid using index to generate id if possible ctx.id = std::get<0>(possible_ids); } else { // Fallback to include index on collision ctx.id = std::get<1>(possible_ids); } ids.insert(ctx.id); input_only_app_id_str = ctx.id; input_only_app_id = util::from_view(ctx.id); apps.emplace_back(std::move(ctx)); } } // Terminate entry { proc::ctx_t ctx {}; ctx.idx = std::to_string(i); ctx.uuid = TERMINATE_APP_UUID; ctx.name = "Terminate"; ctx.image_path = parse_env_val(this_env, "terminate.png"); ctx.virtual_display = false; ctx.scale_factor = 100; ctx.use_app_identity = false; ctx.per_client_app_identity = false; ctx.allow_client_commands = false; ctx.terminate_on_pause = false; ctx.elevated = false; ctx.auto_detach = true; ctx.wait_all = true; ctx.exit_timeout = 5s; auto possible_ids = calculate_app_id(ctx.name, ctx.uuid, ctx.image_path, i++); if (ids.count(std::get<0>(possible_ids)) == 0) { // Avoid using index to generate id if possible ctx.id = std::get<0>(possible_ids); } else { // Fallback to include index on collision ctx.id = std::get<1>(possible_ids); } // ids.insert(ctx.id); terminate_app_id_str = ctx.id; terminate_app_id = util::from_view(ctx.id); apps.emplace_back(std::move(ctx)); } // Only reconcile the persisted alias map when apps.json actually parsed. A failed or partial // parse leaves active_app_uuids empty or incomplete, so pruning here would erase the aliases of // apps that still exist and permanently break their cover-versioned IDs on the next good parse. if (apps_parsed_ok) { prune_and_filter_app_id_alias_state(apps, app_id_alias_state, active_app_uuids, app_id_alias_state_changed); if (app_id_alias_state_changed) { save_app_id_alias_state(app_id_alias_state); } } return proc::proc_t { std::move(this_env), std::move(apps) }; } void refresh(const std::string &file_name, bool needs_terminate) { if (needs_terminate) { proc.terminate(false, false); } #ifdef _WIN32 // initVDisplayDriver() already performs one bounded readiness/recovery pass. // Repeating it here can outlive the restart cooldown and launch a fresh PnP // cycle on every parse, which stalls secondary instances for minutes. if (vDisplayDriverStatus.load(std::memory_order_acquire) != VDISPLAY::DRIVER_STATUS::OK) { initVDisplayDriver(); } #endif auto proc_opt = proc::parse(file_name); if (!proc_opt) { return; } // If an app is currently running, do not replace the entire proc_t instance. // Replacing it would drop tracking state and cause the active stream loop // to think no app is running, prematurely terminating the session. // Instead, update only the applications list to reflect the latest config. if (proc.running() > 0) { // Move the parsed apps list and environment into the existing proc instance // Use proc.update_apps(...) which safely replaces the app list and env proc.update_apps(proc_opt->release_apps(), proc_opt->release_env()); } else { // No app running: safe to refresh full state (env + apps) proc = std::move(*proc_opt); } } void proc_t::update_apps(std::vector &&apps, bp::environment &&env) { // Replace app list while keeping current running app intact. // Only replace _env if no app is currently running, because execute() // populates _env with stream-specific variables (APOLLO_APP_UUID, // APOLLO_CLIENT_UUID, SUNSHINE_CLIENT_*, etc.) that must survive // until terminate() runs the undo prep commands. { std::scoped_lock lk(_apps_mutex); const bool app_was_running = _app_id > 0; if (app_was_running && !_app.uuid.empty()) { const auto refreshed_app = std::find_if(apps.begin(), apps.end(), [&](const ctx_t &candidate) { return candidate.uuid == _app.uuid; }); if (refreshed_app != apps.end()) { _app_id = util::from_view(refreshed_app->id); } } _apps = std::move(apps); if (!app_was_running) { _env = std::move(env); _stream_owned_environment_keys.clear(); } } } #ifdef _WIN32 bool proc_t::update_active_app_live_rtx_hdr_overrides(const std::string &app_uuid) { if (app_uuid.empty()) { return false; } std::unordered_map runtime_overrides; bool changed = false; { std::scoped_lock lk(_apps_mutex); if (_app_id <= 0 || _app.uuid != app_uuid) { return false; } const auto updated = std::find_if(_apps.begin(), _apps.end(), [&](const auto &app) { return app.uuid == app_uuid; }); if (updated == _apps.end()) { return false; } runtime_overrides = config::runtime_config_overrides_snapshot(); for (const auto key_view : RTX_HDR_LIVE_KEYS) { const std::string key {key_view}; const auto old_app_value = _app.config_overrides.find(key); const auto new_app_value = updated->config_overrides.find(key); const auto runtime_value = runtime_overrides.find(key); if (old_app_value == _app.config_overrides.end()) { if (runtime_value != runtime_overrides.end()) { continue; } if (new_app_value != updated->config_overrides.end()) { runtime_overrides.emplace(key, new_app_value->second); _app.config_overrides[key] = new_app_value->second; changed = true; } continue; } if (runtime_value != runtime_overrides.end() && runtime_value->second != old_app_value->second) { continue; } if (new_app_value != updated->config_overrides.end()) { if (runtime_value == runtime_overrides.end() || runtime_value->second != new_app_value->second) { runtime_overrides[key] = new_app_value->second; changed = true; } _app.config_overrides[key] = new_app_value->second; } else { if (runtime_value != runtime_overrides.end()) { runtime_overrides.erase(key); changed = true; } _app.config_overrides.erase(key); } } } if (!changed) { return false; } config::set_runtime_config_overrides(std::move(runtime_overrides)); config::apply_config_now(); return true; } bool proc_t::update_active_app_live_rtx_hdr_overrides( const std::string &app_uuid, const std::unordered_map &rtx_hdr_overrides ) { if (app_uuid.empty()) { return false; } std::unordered_map runtime_overrides; bool changed = false; { std::scoped_lock lk(_apps_mutex); if (_app_id <= 0 || _app.uuid != app_uuid) { return false; } runtime_overrides = config::runtime_config_overrides_snapshot(); for (const auto key_view : RTX_HDR_LIVE_KEYS) { const std::string key {key_view}; const auto old_app_value = _app.config_overrides.find(key); const auto new_app_value = rtx_hdr_overrides.find(key); const auto runtime_value = runtime_overrides.find(key); if (old_app_value == _app.config_overrides.end()) { if (runtime_value != runtime_overrides.end()) { continue; } if (new_app_value != rtx_hdr_overrides.end()) { runtime_overrides.emplace(key, new_app_value->second); _app.config_overrides[key] = new_app_value->second; changed = true; } continue; } if (runtime_value != runtime_overrides.end() && runtime_value->second != old_app_value->second) { continue; } if (new_app_value != rtx_hdr_overrides.end()) { if (runtime_value == runtime_overrides.end() || runtime_value->second != new_app_value->second) { runtime_overrides[key] = new_app_value->second; changed = true; } _app.config_overrides[key] = new_app_value->second; } else { if (runtime_value != runtime_overrides.end()) { runtime_overrides.erase(key); changed = true; } _app.config_overrides.erase(key); } } } if (!changed) { return false; } config::set_runtime_config_overrides(std::move(runtime_overrides)); config::apply_config_now(); return true; } #endif std::vector proc_t::release_apps() { return std::move(_apps); } bp::environment proc_t::release_env() { return std::move(_env); } } // namespace proc