server/src/process.cpp
Rama main · réplica de lectura del repositorio original.
/**
* @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 <algorithm>
#include <array>
#include <atomic>
#include <cctype>
#include <charconv>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <cwctype>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <iterator>
#include <limits>
#include <map>
#include <optional>
#include <set>
#include <sstream>
#include <string>
#include <string_view>
#include <thread>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
// lib includes
#include <boost/algorithm/string.hpp>
#include <boost/crc.hpp>
#include <boost/filesystem.hpp>
#include <boost/program_options/parsers.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <boost/property_tree/ptree.hpp>
#include <nlohmann/json.hpp>
#include <openssl/evp.h>
#include <openssl/sha.h>
// 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 <Psapi.h>
#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 <fcntl.h>
#include <linux/openat2.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <unistd.h>
#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 <share.h>
#endif
namespace proc {
using namespace std::literals;
namespace pt = boost::property_tree;
std::optional<ctx_t> resolve_app_from_snapshot(const std::vector<ctx_t> &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<std::string_view, 6> 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<std::filesystem::path> 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<char8_t>(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<std::filesystem::path> 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<std::filesystem::path> 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<unsigned char>(ch))) {
normalized.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(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<std::string_view, 3> 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<char>(manifest)),
std::istreambuf_iterator<char>()
);
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<std::string, 11> &lossless_scaling_modes() {
static const std::array<std::string, 11> modes {
"off",
"ls1",
"fsr",
"nis",
"sgsr",
"bcas",
"anime4k",
"xbr",
"sharp-bilinear",
"integer",
"nearest"
};
return modes;
}
std::optional<std::string> 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<std::string, 4> 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<std::string> 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<bool> performance_mode;
std::optional<int> flow_scale;
std::optional<double> resolution_scale_factor;
std::optional<std::string> capture_api;
std::optional<int> queue_target;
std::optional<bool> hdr_enabled;
std::optional<std::string> frame_generation;
std::optional<std::string> lsfg3_mode;
std::optional<std::string> scaling_type;
std::optional<int> sharpness;
std::optional<int> ls1_sharpness;
std::optional<std::string> anime4k_type;
std::optional<bool> anime4k_vrs;
};
std::optional<bool> 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<bool>();
} catch (...) {
}
try {
auto text = child->get_value<std::string>();
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<int> 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<int>();
} catch (...) {
}
try {
auto text = child->get_value<std::string>();
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<std::string>("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<std::string>("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<bool>(node, "performance-mode", false);
}
if (node.contains("flow-scale")) {
target.flow_scale = util::get_non_string_json_value<int>(node, "flow-scale", 0);
}
if (node.contains("resolution-scale")) {
target.resolution_scale = util::get_non_string_json_value<int>(node, "resolution-scale", 0);
}
if (auto it = node.find("scaling-type"); it != node.end() && it->is_string()) {
target.scaling_type = it->get<std::string>();
}
if (node.contains("sharpening")) {
target.sharpening = util::get_non_string_json_value<int>(node, "sharpening", 0);
}
if (auto it = node.find("anime4k-size"); it != node.end() && it->is_string()) {
target.anime4k_size = it->get<std::string>();
}
if (node.contains("anime4k-vrs")) {
target.anime4k_vrs = util::get_non_string_json_value<bool>(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<double>(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<DWORD> 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<DWORD> process_group_pids(const bp::group &group) {
std::vector<DWORD> 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<DWORD>(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<DWORD> enumerate_process_ids_snapshot() {
DWORD needed = 0;
std::vector<DWORD> pids(1024);
while (true) {
if (!EnumProcesses(pids.data(), static_cast<DWORD>(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<DWORD> capture_process_baseline_for_lossless() {
std::unordered_set<DWORD> 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<PROCESS_MEMORY_COUNTERS *>(&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<std::wstring> 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<wchar_t>(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<lossless_selection> detect_lossless_candidate(const std::unordered_set<DWORD> &baseline, DWORD root_pid, const std::wstring &preferred_normalized, std::atomic_bool &stop_flag) {
std::unordered_map<DWORD, lossless_process_candidate> 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<double>(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<candidate_score> 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<double>(candidate.last_seen - candidate.first_seen).count();
if (elapsed <= 0.1) {
elapsed = 0.1;
}
double cpu_seconds = static_cast<double>(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<double>(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<VDISPLAY::DRIVER_STATUS> vDisplayDriverStatus {VDISPLAY::DRIVER_STATUS::UNKNOWN};
std::atomic<VDISPLAY::DRIVER_SELECTION> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<DWORD> 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<platf::deinit_t> playnite_integration_;
#endif
};
std::unique_ptr<platf::deinit_t> init() {
return std::make_unique<deinit_t>();
}
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<rtsp_stream::launch_session_t> launch_session) {
#ifdef _WIN32
std::optional<std::filesystem::path> 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<double> 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<int> 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<uint32_t>(launch_session->resolution_override->width) :
(launch_session->width ? launch_session->width : 1920);
uint32_t client_height = has_resolution_override ?
static_cast<uint32_t>(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_t>(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<std::string> 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<int>(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<int> 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<std::string> &value) {
if (value && !value->empty()) {
_env[key] = *value;
} else {
_env[key] = "";
}
};
auto set_int = [&](const char *key, const std::optional<int> &value) {
if (value.has_value()) {
_env[key] = std::to_string(*value);
} else {
_env[key] = "";
}
};
auto set_double = [&](const char *key, const std::optional<double> &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<bool> &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<int>(_app.lossless_scaling_launch_delay_seconds));
set_bool(ENV_LOSSLESS_LEGACY_AUTO_DETECT, std::optional<bool>(_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<DWORD> 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<DWORD>(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<std::int64_t>(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<uint32_t>(_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<uint32_t>(_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<DWORD>(_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<int> 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<std::mutex> 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<void()> 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<DWORD>(_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<DWORD>(pid);
try {
if (_process && static_cast<DWORD>(_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<uint32_t>(_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<bool>(_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<std::mutex> stream_lifecycle_lock;
if (!stream_lifecycle_lock_held) {
stream_lifecycle_lock =
std::unique_lock<std::mutex> {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::chrono::milliseconds>(
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<ctx_t> 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<ctx_t> 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<ctx_t> 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<std::uint8_t, 8> header {};
file.read(reinterpret_cast<char *>(header.data()), static_cast<std::streamsize>(header.size()));
return file.gcount() == static_cast<std::streamsize>(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<std::filesystem::path> 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<catalog::byte_buffer_t> 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<int>(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<std::uint64_t>(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<std::size_t>(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<std::size_t>(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<catalog::byte_buffer_t> 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<char *>(header.data()), static_cast<std::streamsize>(header.size()));
if (file.gcount() != static_cast<std::streamsize>(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<std::string> 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<const char *>(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<std::streamoff>(maximum_machine_cover_bytes)) {
return std::nullopt;
}
std::string bytes(static_cast<std::size_t>(size), '\0');
file.seekg(0);
const auto stream_size = static_cast<std::streamsize>(size);
file.read(bytes.data(), stream_size);
if (file.gcount() != stream_size || !catalog::has_png_signature(std::span<const std::uint8_t> {reinterpret_cast<const std::uint8_t *>(bytes.data()), bytes.size()})) {
return std::nullopt;
}
return bytes;
}
std::optional<std::string> 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<int>(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<std::string> &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<std::string> &parts) {
std::stringstream ss;
for (const auto &part : parts) {
ss << part;
}
return std::to_string(abs((int32_t) calculate_crc32(ss.str())));
}
std::tuple<std::string, std::string> 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<std::string, app_id_alias_state_t> load_app_id_alias_state() {
std::map<std::string, app_id_alias_state_t> result;
statefile::migrate_recent_state_keys();
const auto &path = statefile::vibeshine_state_path();
if (path.empty()) {
return result;
}
std::lock_guard<std::mutex> 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<std::string>("current_id", "");
state.cover_fingerprint = app_node.second.get<std::string>("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<std::string>("");
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<std::string, app_id_alias_state_t> &state) {
statefile::migrate_recent_state_keys();
const auto &path = statefile::vibeshine_state_path();
if (path.empty()) {
return false;
}
std::lock_guard<std::mutex> 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<std::string> &ids,
std::map<std::string, app_id_alias_state_t> &alias_state,
std::set<std::string> &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<ctx_t> &apps,
std::map<std::string, app_id_alias_state_t> &alias_state,
const std::set<std::string> &active_uuids,
bool &alias_state_changed
) {
std::vector<catalog::app_identity_t> 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<ctx_t> resolve_app_from_snapshot(const std::vector<ctx_t> &apps, const std::string &appid, const std::string &appuuid) {
std::vector<catalog::app_identity_t> 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<ctx_t> {apps[*resolved]} : std::nullopt;
}
std::tuple<std::string, std::string> 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<std::string> 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<std::string>().empty()) {
new_app_uuid = (*inputTree_p)["uuid"].get<std::string>();
} 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<std::string>().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<std::string>() == 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<int>();
} 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<std::string> 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<std::string> 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::string>();
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::string>();
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<double>() != 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<std::string>();
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<std::string>();
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<int>();
}
if (file_version < last_version) {
migration_v2(fileTree);
file_handler::write_file(fileName.c_str(), fileTree.dump(4));
}
}
std::optional<proc::proc_t> 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<std::string> ids;
std::vector<proc::ctx_t> apps;
auto app_id_alias_state = load_app_id_alias_state();
std::set<std::string> 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<std::string>());
}
}
// 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<proc::cmd_t> 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<proc::cmd_t> 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<std::string> 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<std::string>()));
}
}
// 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<std::string>());
}
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<std::string>());
}
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<std::string>());
}
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<std::string>());
}
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<std::string>());
} else {
encoded = val.dump();
}
if (!encoded.empty()) {
ctx.config_overrides.emplace(item.key(), std::move(encoded));
}
}
}
{
std::unordered_map<std::string, std::string> 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<bool>(app_node, "frame-gen-limiter-fix", util::get_non_string_json_value<bool>(app_node, "dlss-framegen-limiter-fix", false));
ctx.elevated = util::get_non_string_json_value<bool>(app_node, "elevated", false);
ctx.virtual_screen = util::get_non_string_json_value<bool>(app_node, "virtual-screen", false);
ctx.auto_detach = util::get_non_string_json_value<bool>(app_node, "auto-detach", true);
ctx.wait_all = util::get_non_string_json_value<bool>(app_node, "wait-all", true);
ctx.exit_timeout = std::chrono::seconds {util::get_non_string_json_value<int>(app_node, "exit-timeout", 10)};
ctx.virtual_display = util::get_non_string_json_value<bool>(app_node, "virtual-display", false);
ctx.virtual_display_primary = util::get_non_string_json_value<bool>(app_node, "virtual-display-primary", false);
ctx.scale_factor = util::get_non_string_json_value<int>(app_node, "scale-factor", 100);
ctx.use_app_identity = util::get_non_string_json_value<bool>(app_node, "use-app-identity", false);
ctx.per_client_app_identity = util::get_non_string_json_value<bool>(app_node, "per-client-app-identity", false);
ctx.allow_client_commands = util::get_non_string_json_value<bool>(app_node, "allow-client-commands", true);
ctx.terminate_on_pause = util::get_non_string_json_value<bool>(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<bool>(app_node, "gen1-framegen-fix", util::get_non_string_json_value<bool>(app_node, "dlss-framegen-capture-fix", false)) ||
util::get_non_string_json_value<bool>(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<std::string>(app_node, "virtual-display-mode", "");
auto virtual_display_layout = util::get_non_string_json_value<std::string>(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<int> lossless_scaling_launch_delay;
if (app_node.contains("lossless-scaling-launch-delay")) {
lossless_scaling_launch_delay = util::get_non_string_json_value<int>(app_node, "lossless-scaling-launch-delay", 0);
}
std::optional<bool> legacy_override;
if (app_node.contains("lossless-scaling-legacy-auto-detect")) {
legacy_override = util::get_non_string_json_value<bool>(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<bool>(app_node, "prefer-10bit-sdr", false);
}
std::optional<std::string> dd_config_override;
if (app_node.contains("dd-configuration-option")) {
dd_config_override = util::get_non_string_json_value<std::string>(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<std::string>());
} 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<bool>();
} else if (flag.is_number_integer()) {
ctx.playnite_fullscreen = flag.get<int>() != 0;
} else if (flag.is_string()) {
auto text = flag.get<std::string>();
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<bool>(app_node, "lossless-scaling-enabled", false);
ctx.lossless_scaling_framegen = util::get_non_string_json_value<bool>(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<std::string>());
}
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<std::string>());
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<double>(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<int>(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<std::string>(), 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<ctx_t> &&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<std::string, std::string> 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<std::string, std::string> &rtx_hdr_overrides
) {
if (app_uuid.empty()) {
return false;
}
std::unordered_map<std::string, std::string> 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<ctx_t> proc_t::release_apps() {
return std::move(_apps);
}
bp::environment proc_t::release_env() {
return std::move(_env);
}
} // namespace proc