#include "tools/playnite_launcher/lossless_scaling.h" #include "tools/playnite_launcher/lossless_scaling_policy.h" #include "src/logging.h" #include "src/platform/windows/ipc/misc_utils.h" #include "src/platform/windows/misc.h" #include "src/utility.h" #include "tools/playnite_launcher/focus_utils.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std::chrono_literals; namespace playnite_launcher::lossless { lossless_scaling_runtime_state capture_lossless_scaling_state(); namespace { constexpr std::string_view k_lossless_profile_title = "Vibeshine"; constexpr size_t k_lossless_max_executables = 256; constexpr auto k_lossless_observation_duration = std::chrono::seconds(10); constexpr auto k_lossless_poll_interval = std::chrono::milliseconds(250); constexpr int k_sharpness_min = 1; constexpr int k_sharpness_max = 10; constexpr int k_flow_scale_min = 0; constexpr int k_flow_scale_max = 100; constexpr double k_resolution_factor_min = 1.0; constexpr double k_resolution_factor_max = 10.0; constexpr int k_max_frame_latency = 1; std::filesystem::path lossless_scaling_settings_path(); std::optional resolve_explicit_executable(const std::string &exe_path_utf8); template auto run_with_user_context(Fn &&fn) -> decltype(fn()) { if (platf::dxgi::is_running_as_system()) { winrt::handle user_token {platf::dxgi::retrieve_users_token(false)}; if (user_token) { if (!ImpersonateLoggedOnUser(user_token.get())) { BOOST_LOG(warning) << "Lossless Scaling: impersonation failed, error=" << GetLastError(); } else { auto revert_guard = util::fail_guard([&]() { constexpr int kMaxRevertAttempts = 3; for (int attempt = 0; attempt < kMaxRevertAttempts; ++attempt) { if (RevertToSelf()) { return; } DWORD err = GetLastError(); BOOST_LOG(error) << "Lossless Scaling: RevertToSelf attempt " << (attempt + 1) << " failed, error=" << err; std::this_thread::sleep_for(std::chrono::milliseconds(50)); } BOOST_LOG(fatal) << "Lossless Scaling: giving up after repeated RevertToSelf failures"; }); auto result = fn(); return result; } } else { BOOST_LOG(debug) << "Lossless Scaling: no active user token, using service context"; } } return fn(); } std::filesystem::path known_folder_path_for_token(HANDLE token) { PWSTR local = nullptr; std::filesystem::path path; HRESULT hr = SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, token, &local); if (FAILED(hr) || !local) { if (local) { CoTaskMemFree(local); } return path; } path = std::filesystem::path(local); CoTaskMemFree(local); path /= L"Lossless Scaling"; path /= L"settings.xml"; return path; } bool parse_env_flag(const char *value) { if (!value) { return false; } std::string v(value); std::transform(v.begin(), v.end(), v.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); return v == "1" || v == "true" || v == "yes"; } std::optional parse_env_int(const char *value) { if (!value || !*value) { return std::nullopt; } try { int v = std::stoi(value); if (v > 0) { return v; } } catch (...) { } return std::nullopt; } std::optional parse_env_int_allow_zero(const char *value) { if (!value || !*value) { return std::nullopt; } try { return std::stoi(value); } catch (...) { return std::nullopt; } } std::optional parse_env_flag_optional(const char *value) { if (!value || !*value) { return std::nullopt; } return parse_env_flag(value); } std::optional parse_env_string(const char *value) { if (!value || !*value) { return std::nullopt; } std::string converted(value); boost::algorithm::trim(converted); if (converted.empty()) { return std::nullopt; } return converted; } struct lossless_hotkey_t { WORD key = 0; std::vector modifiers; }; bool is_extended_key(WORD vk) { switch (vk) { case VK_LWIN: case VK_RWIN: case VK_RMENU: case VK_RCONTROL: case VK_INSERT: case VK_DELETE: case VK_HOME: case VK_END: case VK_PRIOR: case VK_NEXT: case VK_UP: case VK_DOWN: case VK_LEFT: case VK_RIGHT: case VK_DIVIDE: case VK_APPS: return true; default: return false; } } std::optional parse_hotkey_key(const std::string &value) { std::string text = boost::algorithm::trim_copy(value); if (text.empty()) { return std::nullopt; } boost::algorithm::to_upper(text); if (text.size() == 1) { char c = text.front(); if (c >= 'A' && c <= 'Z') { return static_cast(c); } if (c >= '0' && c <= '9') { return static_cast(c); } } if (text.size() > 1 && text.front() == 'F') { try { int fkey = std::stoi(text.substr(1)); if (fkey >= 1 && fkey <= 24) { return static_cast(VK_F1 + (fkey - 1)); } } catch (...) { } } struct NamedKey { const char *name; WORD vk; }; static const std::array k_named_keys {{ {"SPACE", VK_SPACE}, {"TAB", VK_TAB}, {"ESC", VK_ESCAPE}, {"ESCAPE", VK_ESCAPE}, {"ENTER", VK_RETURN}, {"RETURN", VK_RETURN}, {"BACK", VK_BACK}, {"BACKSPACE", VK_BACK}, {"INSERT", VK_INSERT}, {"DELETE", VK_DELETE}, {"HOME", VK_HOME}, {"END", VK_END}, {"PAGEUP", VK_PRIOR}, {"PGUP", VK_PRIOR}, {"PAGEDOWN", VK_NEXT}, {"PGDN", VK_NEXT}, {"UP", VK_UP}, {"DOWN", VK_DOWN}, {"LEFT", VK_LEFT}, {"RIGHT", VK_RIGHT}, }}; for (const auto &entry : k_named_keys) { if (text == entry.name) { return entry.vk; } } if (text.rfind("NUMPAD", 0) == 0 && text.size() == 7 && std::isdigit(text[6])) { int num = text[6] - '0'; return static_cast(VK_NUMPAD0 + num); } return std::nullopt; } std::vector parse_hotkey_modifiers(const std::string &value) { std::unordered_set mods; std::string token; for (char ch : value) { if (std::isspace(static_cast(ch)) || ch == '+' || ch == ',' || ch == ';' || ch == '|') { if (!token.empty()) { std::string lower = boost::algorithm::to_lower_copy(token); if (lower == "alt" || lower == "menu") { mods.insert(VK_MENU); } else if (lower == "control" || lower == "ctrl") { mods.insert(VK_CONTROL); } else if (lower == "shift") { mods.insert(VK_SHIFT); } else if (lower == "win" || lower == "windows" || lower == "logo") { mods.insert(VK_LWIN); } token.clear(); } continue; } token.push_back(ch); } if (!token.empty()) { std::string lower = boost::algorithm::to_lower_copy(token); if (lower == "alt" || lower == "menu") { mods.insert(VK_MENU); } else if (lower == "control" || lower == "ctrl") { mods.insert(VK_CONTROL); } else if (lower == "shift") { mods.insert(VK_SHIFT); } else if (lower == "win" || lower == "windows" || lower == "logo") { mods.insert(VK_LWIN); } } std::vector ordered; ordered.reserve(mods.size()); const std::array order {VK_CONTROL, VK_MENU, VK_SHIFT, VK_LWIN}; for (auto vk : order) { if (mods.find(vk) != mods.end()) { ordered.push_back(vk); } } return ordered; } bool send_hotkey_input(const lossless_hotkey_t &hotkey) { if (!hotkey.key) { return false; } std::vector inputs; inputs.reserve((hotkey.modifiers.size() * 2) + 2); auto append_key = [&](WORD vk, bool up) { INPUT input {}; input.type = INPUT_KEYBOARD; input.ki.wVk = vk; input.ki.dwFlags = up ? KEYEVENTF_KEYUP : 0; if (is_extended_key(vk)) { input.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY; } inputs.push_back(input); }; for (auto vk : hotkey.modifiers) { append_key(vk, false); } append_key(hotkey.key, false); append_key(hotkey.key, true); for (auto it = hotkey.modifiers.rbegin(); it != hotkey.modifiers.rend(); ++it) { append_key(*it, true); } if (inputs.empty()) { return false; } UINT sent = SendInput(static_cast(inputs.size()), inputs.data(), sizeof(INPUT)); if (sent != inputs.size()) { BOOST_LOG(warning) << "Lossless Scaling: SendInput sent " << sent << " of " << inputs.size(); return false; } return true; } HWND focus_game_window(DWORD pid); bool click_window_center(HWND hwnd); bool apply_hotkey_for_pid(const lossless_hotkey_t &hotkey, DWORD pid, bool click_before_send, int attempts) { if (attempts <= 0) { return false; } for (int attempt = 0; attempt < attempts; ++attempt) { HWND hwnd = nullptr; if (pid) { hwnd = focus_game_window(pid); if (hwnd) { std::this_thread::sleep_for(75ms); if (click_before_send) { click_window_center(hwnd); } std::this_thread::sleep_for(50ms); } } bool sent = send_hotkey_input(hotkey); if (pid) { bool focused = focus::confirm_foreground_pid(pid); BOOST_LOG(info) << "Lossless Scaling: hotkey attempt " << (attempt + 1) << "/" << attempts << " pid=" << pid << " focused=" << (focused ? "true" : "false") << " sent=" << (sent ? "true" : "false"); } else { BOOST_LOG(info) << "Lossless Scaling: hotkey attempt " << (attempt + 1) << "/" << attempts << " pid=none sent=" << (sent ? "true" : "false"); } if (sent) { return true; } std::this_thread::sleep_for(150ms); } return false; } std::optional read_lossless_hotkey() { auto worker = [&]() -> std::optional { auto settings_path = lossless_scaling_settings_path(); if (settings_path.empty()) { return std::nullopt; } boost::property_tree::ptree tree; try { boost::property_tree::read_xml(settings_path.string(), tree); } catch (...) { return std::nullopt; } auto hotkey_text = tree.get_optional("Settings.Hotkey"); if (!hotkey_text || hotkey_text->empty()) { return std::nullopt; } auto key = parse_hotkey_key(*hotkey_text); if (!key) { BOOST_LOG(warning) << "Lossless Scaling: unrecognized hotkey '" << *hotkey_text << "'"; return std::nullopt; } lossless_hotkey_t hotkey {}; hotkey.key = *key; if (auto mods = tree.get_optional("Settings.HotkeyModifierKeys")) { hotkey.modifiers = parse_hotkey_modifiers(*mods); } return hotkey; }; return run_with_user_context(worker); } std::optional clamp_optional_int(std::optional value, int min_value, int max_value) { if (!value) { return std::nullopt; } return std::clamp(*value, min_value, max_value); } std::optional parse_env_double(const char *value) { if (!value || !*value) { return std::nullopt; } try { double v = std::stod(value); if (std::isfinite(v) && v > 0.0) { return v; } } catch (...) { } return std::nullopt; } std::optional clamp_optional_double(std::optional value, double min_value, double max_value) { if (!value) { return std::nullopt; } return std::clamp(*value, min_value, max_value); } void finalize_lossless_options(lossless_scaling_options &options) { if (options.enabled && !options.rtss_limit && options.target_fps && *options.target_fps > 0) { int computed = (int) std::floor(*options.target_fps / 2.0); if (computed > 0) { options.rtss_limit = computed; } } if (options.anime4k_type) { boost::algorithm::to_upper(*options.anime4k_type); } } void lowercase_inplace(std::wstring &value) { std::transform(value.begin(), value.end(), value.begin(), [](wchar_t c) { return std::towlower(c); }); } std::optional utf8_to_path(const std::string &input) { if (input.empty()) { return std::nullopt; } try { std::wstring wide = platf::dxgi::utf8_to_wide(input); if (wide.empty()) { return std::nullopt; } return std::filesystem::path(wide); } catch (...) { return std::nullopt; } } std::optional normalize_directory(std::optional path) { if (!path || path->empty()) { return std::nullopt; } std::error_code ec; auto canonical = std::filesystem::weakly_canonical(*path, ec); if (!ec && !canonical.empty()) { path = canonical; } if (!std::filesystem::is_directory(*path, ec)) { return std::nullopt; } return path; } std::optional parent_directory_from_utf8(const std::string &exe_utf8) { auto exe_path = utf8_to_path(exe_utf8); if (!exe_path) { return std::nullopt; } exe_path = exe_path->parent_path(); return normalize_directory(exe_path); } std::optional lossless_resolve_base_dir(const std::string &install_dir_utf8, const std::string &exe_path_utf8) { auto install_dir = normalize_directory(utf8_to_path(install_dir_utf8)); if (install_dir) { return install_dir; } return parent_directory_from_utf8(exe_path_utf8); } bool lossless_path_within(const std::filesystem::path &candidate, const std::filesystem::path &base) { if (candidate.empty() || base.empty()) { return false; } std::error_code ec; auto rel = std::filesystem::relative(candidate, base, ec); if (ec) { return false; } for (const auto &part : rel) { if (part == L"..") { return false; } } return true; } void add_executable(const std::filesystem::path &candidate, bool require_exists, std::unordered_set &seen, std::vector &executables) { if (executables.size() >= k_lossless_max_executables) { return; } if (require_exists) { std::error_code ec; if (!std::filesystem::exists(candidate, ec) || !std::filesystem::is_regular_file(candidate, ec)) { return; } } auto ext = candidate.extension().wstring(); lowercase_inplace(ext); if (ext != L".exe") { return; } auto name = candidate.filename().wstring(); if (name.empty()) { return; } auto key = name; lowercase_inplace(key); if (seen.insert(key).second) { executables.push_back(name); } } void scan_directory_for_executables(const std::filesystem::path &base, std::unordered_set &seen, std::vector &executables) { if (base.empty()) { return; } std::error_code ec; auto options = std::filesystem::directory_options::skip_permission_denied; std::filesystem::recursive_directory_iterator it(base, options, ec); std::filesystem::recursive_directory_iterator end; for (; it != end && executables.size() < k_lossless_max_executables; it.increment(ec)) { if (ec) { ec.clear(); continue; } if (it->is_regular_file(ec)) { add_executable(it->path(), true, seen, executables); } } } void add_explicit_executable(const std::optional &explicit_exe, const std::filesystem::path &base_dir, std::unordered_set &seen, std::vector &executables) { if (!explicit_exe) { return; } if (!base_dir.empty() && !lossless_path_within(*explicit_exe, base_dir)) { return; } add_executable(*explicit_exe, true, seen, executables); } void sort_executable_names(std::vector &executables) { std::sort(executables.begin(), executables.end(), [](std::wstring a, std::wstring b) { lowercase_inplace(a); lowercase_inplace(b); return a < b; }); } std::vector lossless_collect_executable_names(const std::filesystem::path &base_dir, const std::optional &explicit_exe) { std::vector executables; std::unordered_set seen; if (explicit_exe) { add_explicit_executable(explicit_exe, base_dir, seen, executables); } else { scan_directory_for_executables(base_dir, seen, executables); } sort_executable_names(executables); return executables; } std::string lossless_build_filter(const std::vector &exe_names) { if (exe_names.empty()) { return std::string(); } auto filter = policy::build_executable_filter(exe_names); if (filter.empty()) { return std::string(); } try { return platf::dxgi::wide_to_utf8(filter); } catch (...) { return std::string(); } } std::optional get_lossless_scaling_env_path() { const char *env = std::getenv("SUNSHINE_LOSSLESS_SCALING_EXE"); if (!env || !*env) { return std::nullopt; } try { std::wstring wide = platf::dxgi::utf8_to_wide(env); if (wide.empty()) { return std::nullopt; } return std::filesystem::path(wide); } catch (...) { return std::nullopt; } } std::optional exe_from_env_path() { auto path = get_lossless_scaling_env_path(); if (path && std::filesystem::exists(*path)) { return path->wstring(); } return std::nullopt; } std::optional exe_from_runtime(const lossless_scaling_runtime_state &state) { if (state.exe_path && std::filesystem::exists(*state.exe_path)) { return state.exe_path; } return std::nullopt; } std::filesystem::path lossless_scaling_settings_path() { if (platf::dxgi::is_running_as_system()) { winrt::handle token {platf::dxgi::retrieve_users_token(false)}; if (token) { auto user_path = known_folder_path_for_token(token.get()); if (!user_path.empty()) { return user_path; } BOOST_LOG(debug) << "Lossless Scaling: failed to resolve LocalAppData via user token, falling back"; } } return known_folder_path_for_token(nullptr); } std::optional exe_from_settings() { auto settings = lossless_scaling_settings_path(); if (settings.empty()) { return std::nullopt; } auto local_app = settings.parent_path().parent_path(); if (local_app.empty()) { return std::nullopt; } std::filesystem::path candidate = local_app / L"Programs" / L"Lossless Scaling" / L"Lossless Scaling.exe"; if (std::filesystem::exists(candidate)) { return candidate.wstring(); } return std::nullopt; } std::optional exe_from_program_files() { const std::array env_names {L"PROGRAMFILES", L"PROGRAMFILES(X86)"}; for (auto env_name : env_names) { wchar_t buf[MAX_PATH] = {}; DWORD len = GetEnvironmentVariableW(env_name, buf, ARRAYSIZE(buf)); if (len == 0 || len >= ARRAYSIZE(buf)) { continue; } std::filesystem::path candidate = std::filesystem::path(buf) / L"Lossless Scaling" / L"Lossless Scaling.exe"; if (std::filesystem::exists(candidate)) { return candidate.wstring(); } } return std::nullopt; } std::optional exe_from_explicit_path(const std::string &exe_path_utf8) { auto exe_path = resolve_explicit_executable(exe_path_utf8); if (!exe_path) { return std::nullopt; } return exe_path->wstring(); } void strip_xml_whitespace(boost::property_tree::ptree &node) { for (auto it = node.begin(); it != node.end();) { if (it->first == "") { it = node.erase(it); } else { strip_xml_whitespace(it->second); ++it; } } } std::optional discover_lossless_scaling_exe(const lossless_scaling_runtime_state &state); std::optional select_lossless_launch_exe(const lossless_scaling_runtime_state &state, const std::string &exe_path_utf8) { return policy::select_launch_executable({ .lossless_scaling = discover_lossless_scaling_exe(state), .game = exe_from_explicit_path(exe_path_utf8), }); } std::optional discover_lossless_scaling_exe(const lossless_scaling_runtime_state &state) { if (auto path = exe_from_env_path()) { return path; } if (auto path = exe_from_runtime(state)) { return path; } if (auto path = exe_from_settings()) { return path; } return exe_from_program_files(); } bool is_any_executable_running(const std::vector &exe_names) { if (exe_names.empty()) { return false; } for (const auto &exe_name : exe_names) { if (exe_name.empty()) { continue; } try { auto ids = platf::dxgi::find_process_ids_by_name(exe_name); if (!ids.empty()) { return true; } } catch (...) { } } return false; } bool wait_for_any_executable(const std::vector &exe_names, int timeout_seconds) { if (exe_names.empty() || timeout_seconds <= 0) { return false; } auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(timeout_seconds); while (std::chrono::steady_clock::now() < deadline) { if (is_any_executable_running(exe_names)) { BOOST_LOG(debug) << "Lossless Scaling: game executable detected in process list"; return true; } std::this_thread::sleep_for(250ms); } BOOST_LOG(debug) << "Lossless Scaling: timeout waiting for game executable to appear"; return false; } bool focus_main_lossless_window(DWORD pid) { HWND hwnd = focus::find_main_window_for_pid(pid); return hwnd && focus::try_focus_hwnd(hwnd); } bool focus_any_visible_window(DWORD pid) { struct Ctx { DWORD target; bool focused = false; } ctx {pid, false}; auto proc = [](HWND hwnd, LPARAM param) -> BOOL { auto *ctx = reinterpret_cast(param); DWORD owner = 0; GetWindowThreadProcessId(hwnd, &owner); if (owner == ctx->target && IsWindowVisible(hwnd)) { ctx->focused = focus::try_focus_hwnd(hwnd); if (ctx->focused) { return FALSE; } } return TRUE; }; EnumWindows(proc, reinterpret_cast(&ctx)); return ctx.focused; } bool lossless_scaling_focus_window(DWORD pid) { if (!pid) { return false; } return focus_main_lossless_window(pid) || focus_any_visible_window(pid); } bool minimize_hwnd_if_visible(HWND hwnd) { if (!hwnd || !IsWindowVisible(hwnd)) { return false; } if (IsIconic(hwnd)) { return true; } return ShowWindowAsync(hwnd, SW_SHOWMINNOACTIVE) != 0; } bool minimize_main_lossless_window(DWORD pid) { return minimize_hwnd_if_visible(focus::find_main_window_for_pid(pid)); } bool minimize_any_visible_window(DWORD pid) { struct Ctx { DWORD target; bool minimized = false; } ctx {pid, false}; auto proc = [](HWND hwnd, LPARAM param) -> BOOL { auto *ctx = reinterpret_cast(param); DWORD owner = 0; GetWindowThreadProcessId(hwnd, &owner); if (owner == ctx->target) { if (minimize_hwnd_if_visible(hwnd)) { ctx->minimized = true; return FALSE; } } return TRUE; }; EnumWindows(proc, reinterpret_cast(&ctx)); return ctx.minimized; } bool lossless_scaling_minimize_window(DWORD pid) { if (!pid) { return false; } return minimize_main_lossless_window(pid) || minimize_any_visible_window(pid); } [[maybe_unused]] bool minimize_visible_windows_except(DWORD keep_pid) { struct Ctx { DWORD keep; bool minimized = false; } ctx {keep_pid, false}; EnumWindows([](HWND hwnd, LPARAM param) -> BOOL { auto *ctx = reinterpret_cast(param); if (!IsWindowVisible(hwnd) || IsIconic(hwnd)) { return TRUE; } if (GetWindow(hwnd, GW_OWNER) != nullptr) { return TRUE; } DWORD pid = 0; GetWindowThreadProcessId(hwnd, &pid); if (pid == 0 || pid == ctx->keep) { return TRUE; } RECT rect {}; if (!GetWindowRect(hwnd, &rect)) { return TRUE; } if ((rect.right - rect.left) <= 1 || (rect.bottom - rect.top) <= 1) { return TRUE; } if (ShowWindowAsync(hwnd, SW_SHOWMINNOACTIVE)) { ctx->minimized = true; } return TRUE; }, reinterpret_cast(&ctx)); return ctx.minimized; } HWND focus_any_visible_window_for_pid(DWORD pid) { struct Ctx { DWORD target; HWND focused = nullptr; } ctx {pid, nullptr}; EnumWindows([](HWND hwnd, LPARAM param) -> BOOL { auto *ctx = reinterpret_cast(param); DWORD owner = 0; GetWindowThreadProcessId(hwnd, &owner); if (owner == ctx->target && IsWindowVisible(hwnd)) { if (focus::try_focus_hwnd(hwnd)) { ctx->focused = hwnd; return FALSE; } } return TRUE; }, reinterpret_cast(&ctx)); return ctx.focused; } HWND focus_game_window(DWORD pid) { if (!pid) { return nullptr; } HWND hwnd = focus::find_main_window_for_pid(pid); if (hwnd && focus::try_focus_hwnd(hwnd)) { return hwnd; } return focus_any_visible_window_for_pid(pid); } bool click_window_center(HWND hwnd) { if (!hwnd) { return false; } RECT rect {}; if (!GetWindowRect(hwnd, &rect)) { return false; } if ((rect.right - rect.left) <= 1 || (rect.bottom - rect.top) <= 1) { return false; } POINT original {}; GetCursorPos(&original); int x = rect.left + (rect.right - rect.left) / 2; int y = rect.top + (rect.bottom - rect.top) / 2; SetCursorPos(x, y); INPUT inputs[2] {}; inputs[0].type = INPUT_MOUSE; inputs[0].mi.dwFlags = MOUSEEVENTF_LEFTDOWN; inputs[1].type = INPUT_MOUSE; inputs[1].mi.dwFlags = MOUSEEVENTF_LEFTUP; UINT sent = SendInput(2, inputs, sizeof(INPUT)); SetCursorPos(original.x, original.y); return sent == 2; } std::wstring normalize_lowercase_path(std::wstring value) { for (auto &ch : value) { if (ch == L'/') { ch = L'\\'; } ch = static_cast(std::towlower(ch)); } return value; } std::optional query_process_image_path_optional(DWORD pid) { std::wstring path; if (focus::get_process_image_path(pid, path)) { return path; } return std::nullopt; } std::optional normalize_utf8_path(const std::string &path_utf8) { if (path_utf8.empty()) { return std::nullopt; } try { auto wide = platf::dxgi::utf8_to_wide(path_utf8); if (wide.empty()) { return std::nullopt; } return normalize_lowercase_path(wide); } catch (...) { return std::nullopt; } } bool path_matches_prefix(const std::wstring &path, const std::wstring &dir_prefix) { if (dir_prefix.empty()) { return false; } if (path.size() < dir_prefix.size()) { return false; } return path.compare(0, dir_prefix.size(), dir_prefix) == 0; } bool path_matches_filter(const std::wstring &path, const std::optional &install_dir_norm, const std::optional &exe_path_norm) { if (path.empty()) { return false; } auto normalized = normalize_lowercase_path(path); if (exe_path_norm && !exe_path_norm->empty() && normalized == *exe_path_norm) { return true; } if (install_dir_norm && !install_dir_norm->empty()) { return path_matches_prefix(normalized, *install_dir_norm); } return false; } bool is_ignored_process_path(const std::wstring &path); std::optional match_foreground_pid_to_filter(const std::string &install_dir_utf8, const std::string &exe_path_utf8) { HWND foreground = GetForegroundWindow(); if (!foreground) { return std::nullopt; } DWORD pid = 0; GetWindowThreadProcessId(foreground, &pid); if (!pid) { return std::nullopt; } auto path = query_process_image_path_optional(pid); if (!path || is_ignored_process_path(*path)) { return std::nullopt; } auto install_dir_norm = normalize_utf8_path(install_dir_utf8); auto exe_path_norm = normalize_utf8_path(exe_path_utf8); if (install_dir_norm && !install_dir_norm->empty() && install_dir_norm->back() != L'\\') { install_dir_norm->push_back(L'\\'); } const bool has_filter = (install_dir_norm && !install_dir_norm->empty()) || (exe_path_norm && !exe_path_norm->empty()); if (has_filter && !path_matches_filter(*path, install_dir_norm, exe_path_norm)) { return std::nullopt; } BOOST_LOG(info) << "Lossless Scaling: using foreground PID=" << pid << " exe=" << platf::dxgi::wide_to_utf8(*path); return pid; } bool is_ignored_process_path(const std::wstring &path) { if (path.empty()) { return false; } std::filesystem::path fs_path(path); auto filename = fs_path.filename().wstring(); if (filename.empty()) { return false; } auto lower = normalize_lowercase_path(filename); return lower == L"losslessscaling.exe" || lower == L"lossless scaling.exe" || lower == L"playnite.fullscreenapp.exe" || lower == L"playnite.desktopapp.exe"; } HWND wait_for_game_window(DWORD pid, std::chrono::milliseconds timeout) { if (!pid) { return nullptr; } auto deadline = std::chrono::steady_clock::now() + timeout; while (std::chrono::steady_clock::now() < deadline) { if (auto hwnd = focus::find_main_window_for_pid(pid)) { return hwnd; } std::this_thread::sleep_for(100ms); } return nullptr; } bool wait_for_lossless_ready(std::chrono::milliseconds timeout) { auto deadline = std::chrono::steady_clock::now() + timeout; while (std::chrono::steady_clock::now() < deadline) { auto runtime = capture_lossless_scaling_state(); if (!runtime.running_pids.empty()) { return true; } std::this_thread::sleep_for(150ms); } return false; } std::vector enumerate_process_ids_snapshot() { DWORD needed = 0; std::vector pids(1024); while (true) { if (!EnumProcesses(pids.data(), static_cast(pids.size() * sizeof(DWORD)), &needed)) { return {}; } if (needed < pids.size() * sizeof(DWORD)) { pids.resize(needed / sizeof(DWORD)); break; } pids.resize(pids.size() * 2); } return pids; } bool sample_process_usage(DWORD pid, ULONGLONG &cpu_time, SIZE_T &working_set) { HANDLE handle = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid); if (!handle) { handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); } if (!handle) { return false; } FILETIME creation {}, exit_time {}, kernel {}, user {}; BOOL got_times = GetProcessTimes(handle, &creation, &exit_time, &kernel, &user); PROCESS_MEMORY_COUNTERS_EX pmc {}; BOOL got_mem = GetProcessMemoryInfo(handle, reinterpret_cast(&pmc), sizeof(pmc)); CloseHandle(handle); if (!got_times) { return false; } ULARGE_INTEGER kernel_int {}; kernel_int.HighPart = kernel.dwHighDateTime; kernel_int.LowPart = kernel.dwLowDateTime; ULARGE_INTEGER user_int {}; user_int.HighPart = user.dwHighDateTime; user_int.LowPart = user.dwLowDateTime; cpu_time = kernel_int.QuadPart + user_int.QuadPart; working_set = got_mem ? pmc.WorkingSetSize : 0; return true; } void lossless_scaling_post_wm_close(const std::vector &pids) { if (pids.empty()) { return; } struct EnumCtx { const std::vector *pids; } ctx {&pids}; EnumWindows([](HWND hwnd, LPARAM lparam) -> BOOL { auto *ctx = reinterpret_cast(lparam); DWORD pid = 0; GetWindowThreadProcessId(hwnd, &pid); if (!pid) { return TRUE; } if (std::find(ctx->pids->begin(), ctx->pids->end(), pid) != ctx->pids->end()) { PostMessageW(hwnd, WM_CLOSE, 0, 0); } return TRUE; }, reinterpret_cast(&ctx)); } std::optional process_path_from_pid(DWORD pid) { HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); if (!process) { return std::nullopt; } std::wstring buffer; buffer.resize(32768); DWORD size = static_cast(buffer.size()); std::optional result; if (QueryFullProcessImageNameW(process, 0, buffer.data(), &size) && size > 0) { buffer.resize(size); result = buffer; } CloseHandle(process); return result; } void collect_runtime_for_process(const wchar_t *process_name, lossless_scaling_runtime_state &state) { if (!process_name || !*process_name) { return; } try { auto ids = platf::dxgi::find_process_ids_by_name(process_name); for (DWORD pid : ids) { if (std::find(state.running_pids.begin(), state.running_pids.end(), pid) != state.running_pids.end()) { continue; } state.running_pids.push_back(pid); if (!state.exe_path) { if (auto path = process_path_from_pid(pid)) { state.exe_path = std::move(*path); } } } } catch (...) { } } struct ProfileTemplates { boost::property_tree::ptree *defaults = nullptr; boost::property_tree::ptree *first = nullptr; }; ProfileTemplates find_profile_templates(boost::property_tree::ptree &profiles) { ProfileTemplates templates; for (auto &entry : profiles) { if (entry.first != "Profile") { continue; } if (!templates.first) { templates.first = &entry.second; } auto path_opt = entry.second.get_optional("Path"); if (!path_opt || path_opt->empty()) { templates.defaults = &entry.second; break; } } return templates; } void capture_backup_fields(const ProfileTemplates &templates, lossless_scaling_profile_backup &backup) { auto source = templates.defaults ? templates.defaults : templates.first; if (!source) { return; } if (auto auto_scale = source->get_optional("AutoScale")) { backup.had_auto_scale = true; backup.auto_scale = *auto_scale; } if (auto delay = source->get_optional("AutoScaleDelay")) { backup.had_auto_scale_delay = true; backup.auto_scale_delay = *delay; } if (auto target = source->get_optional("LSFG3Target")) { backup.had_lsfg_target = true; backup.lsfg_target = *target; } if (auto capture = source->get_optional("CaptureApi")) { backup.had_capture_api = true; backup.capture_api = *capture; } if (auto queue = source->get_optional("QueueTarget")) { backup.had_queue_target = true; backup.queue_target = *queue; } if (auto hdr = source->get_optional("HdrSupport")) { backup.had_hdr_support = true; backup.hdr_support = *hdr; } if (auto flow = source->get_optional("LSFGFlowScale")) { backup.had_flow_scale = true; backup.flow_scale = *flow; } if (auto size = source->get_optional("LSFGSize")) { backup.had_lsfg_size = true; backup.lsfg_size = *size; } if (auto mode = source->get_optional("LSFG3Mode1")) { backup.had_lsfg3_mode = true; backup.lsfg3_mode = *mode; } if (auto frame = source->get_optional("FrameGeneration")) { backup.had_frame_generation = true; backup.frame_generation = *frame; } if (auto scaling_type = source->get_optional("ScalingType")) { backup.had_scaling_type = true; backup.scaling_type = *scaling_type; } if (auto ls1_type = source->get_optional("LS1Type")) { backup.had_ls1_type = true; backup.ls1_type = *ls1_type; } if (auto scaling_mode = source->get_optional("ScalingMode")) { backup.had_scaling_mode = true; backup.scaling_mode = *scaling_mode; } if (auto resize_before = source->get_optional("ResizeBeforeScaling")) { backup.had_resize_before_scaling = true; backup.resize_before_scaling = *resize_before; } if (auto scaling_fit_mode = source->get_optional("ScalingFitMode")) { backup.had_scaling_fit_mode = true; backup.scaling_fit_mode = *scaling_fit_mode; } if (auto scale_factor = source->get_optional("ScaleFactor")) { backup.had_scale_factor = true; backup.scale_factor = *scale_factor; } if (auto sharpness = source->get_optional("Sharpness")) { backup.had_sharpness = true; backup.sharpness = *sharpness; } if (auto ls1 = source->get_optional("LS1Sharpness")) { backup.had_ls1_sharpness = true; backup.ls1_sharpness = *ls1; } if (auto anime_type = source->get_optional("Anime4kType")) { backup.had_anime4k_type = true; backup.anime4k_type = *anime_type; } if (auto vrs = source->get_optional("VRS")) { backup.had_vrs = true; backup.vrs = *vrs; } if (auto sync = source->get_optional("SyncMode")) { backup.had_sync_mode = true; backup.sync_mode = *sync; } if (auto latency = source->get_optional("MaxFrameLatency")) { backup.had_max_frame_latency = true; backup.max_frame_latency = *latency; } } std::optional resolve_explicit_executable(const std::string &exe_path_utf8) { if (exe_path_utf8.empty()) { return std::nullopt; } auto exe = utf8_to_path(exe_path_utf8); if (!exe) { return std::nullopt; } std::error_code ec; auto canonical = std::filesystem::weakly_canonical(*exe, ec); if (!ec && !canonical.empty()) { exe = canonical; } if (!std::filesystem::exists(*exe, ec) || !std::filesystem::is_regular_file(*exe, ec)) { return std::nullopt; } return exe; } std::string build_executable_filter(const std::optional &base_dir, const std::optional &explicit_exe) { std::vector names; if (base_dir || explicit_exe) { names = lossless_collect_executable_names(base_dir.value_or(std::filesystem::path()), explicit_exe); } return lossless_build_filter(names); } boost::property_tree::ptree clone_template_profile(const ProfileTemplates &templates) { if (templates.defaults) { return *templates.defaults; } if (templates.first) { return *templates.first; } return {}; } bool remove_vibeshine_profiles(boost::property_tree::ptree &profiles) { bool removed = false; for (auto it = profiles.begin(); it != profiles.end();) { if (it->first == "Profile" && it->second.get("Title", "") == k_lossless_profile_title) { it = profiles.erase(it); removed = true; continue; } ++it; } return removed; } boost::property_tree::ptree make_vibeshine_profile(const ProfileTemplates &templates, const lossless_scaling_options &options, const std::string &filter_utf8) { auto profile = clone_template_profile(templates); profile.put("Title", std::string(k_lossless_profile_title)); profile.put("Path", filter_utf8); profile.put("Filter", filter_utf8); profile.put("AutoScale", options.legacy_auto_detect ? "true" : "false"); profile.put("AutoScaleDelay", 0); profile.put("SyncMode", "OFF"); if (options.capture_api) { std::string capture = *options.capture_api; boost::algorithm::to_upper(capture); profile.put("CaptureApi", capture); } if (options.queue_target) { profile.put("QueueTarget", std::max(0, *options.queue_target)); } if (options.hdr_enabled.has_value()) { profile.put("HdrSupport", *options.hdr_enabled); } if (options.frame_generation_mode) { std::string frame_mode = *options.frame_generation_mode; boost::algorithm::to_upper(frame_mode); profile.put("FrameGeneration", frame_mode); } else { profile.put("FrameGeneration", "Off"); } if (options.lsfg3_mode) { std::string lsfg_mode = *options.lsfg3_mode; boost::algorithm::to_upper(lsfg_mode); profile.put("LSFG3Mode1", lsfg_mode); } const std::string *scaling_type_for_opts = options.scaling_type ? &(*options.scaling_type) : nullptr; if (options.performance_mode.has_value()) { const bool perf = *options.performance_mode; profile.put("LSFGSize", perf ? "PERFORMANCE" : "BALANCED"); if (scaling_type_for_opts && boost::iequals(*scaling_type_for_opts, "LS1")) { profile.put("LS1Type", perf ? "PERFORMANCE" : "BALANCED"); } } profile.put("MaxFrameLatency", k_max_frame_latency); if (options.flow_scale) { int flow = std::clamp(*options.flow_scale, k_flow_scale_min, k_flow_scale_max); profile.put("LSFGFlowScale", flow); } if (options.target_fps && *options.target_fps > 0) { double target = std::clamp(*options.target_fps, 1.0, 480.0); profile.put("LSFG3Target", target); } double scale_factor = 1.0; const bool has_resolution_scale = options.resolution_scale_factor.has_value(); if (has_resolution_scale) { scale_factor = std::clamp(*options.resolution_scale_factor, k_resolution_factor_min, k_resolution_factor_max); profile.put("ScaleFactor", scale_factor); } if (options.scaling_type) { profile.put("ScalingType", *options.scaling_type); } else if (has_resolution_scale) { profile.put("ScalingType", std::abs(scale_factor - 1.0) < 0.01 ? "Off" : "Auto"); } if (has_resolution_scale && std::abs(scale_factor - 1.0) > 0.01) { profile.put("ScalingMode", "Custom"); profile.put("ResizeBeforeScaling", true); } if (options.sharpness) { int sharpness = std::clamp(*options.sharpness, k_sharpness_min, k_sharpness_max); profile.put("Sharpness", sharpness); } if (options.ls1_sharpness) { int ls1 = std::clamp(*options.ls1_sharpness, k_sharpness_min, k_sharpness_max); profile.put("LS1Sharpness", ls1); } if (options.anime4k_type) { std::string type = *options.anime4k_type; boost::algorithm::to_upper(type); profile.put("Anime4kType", type); } if (options.anime4k_vrs.has_value()) { profile.put("VRS", *options.anime4k_vrs); } return profile; } bool insert_vibeshine_profile(const ProfileTemplates &templates, const lossless_scaling_options &options, const std::string &filter_utf8, boost::property_tree::ptree &profiles, lossless_scaling_profile_backup &backup) { if (filter_utf8.empty()) { return false; } profiles.push_back(std::make_pair("Profile", make_vibeshine_profile(templates, options, filter_utf8))); backup.valid = true; return true; } bool restore_string_field(boost::property_tree::ptree &profile, const char *key, bool had_value, const std::string &value, bool &changed) { auto current = profile.get_optional(key); if (had_value) { if (!current || *current != value) { profile.put(key, value); changed = true; } } else if (current) { profile.erase(key); changed = true; } return changed; } bool restore_int_field(boost::property_tree::ptree &profile, const char *key, bool had_value, int value, bool &changed) { auto current = profile.get_optional(key); if (had_value) { if (!current || *current != value) { profile.put(key, value); changed = true; } } else if (current) { profile.erase(key); changed = true; } return changed; } bool restore_bool_field(boost::property_tree::ptree &profile, const char *key, bool had_value, bool value, bool &changed) { auto current = profile.get_optional(key); if (had_value) { if (!current || *current != value) { profile.put(key, value); changed = true; } } else if (current) { profile.erase(key); changed = true; } return changed; } bool restore_double_field(boost::property_tree::ptree &profile, const char *key, bool had_value, double value, bool &changed) { auto current = profile.get_optional(key); if (had_value) { if (!current || std::abs(*current - value) > std::numeric_limits::epsilon()) { profile.put(key, value); changed = true; } } else if (current) { profile.erase(key); changed = true; } return changed; } bool apply_backup_to_profile(boost::property_tree::ptree &profile, const lossless_scaling_profile_backup &backup) { bool changed = false; restore_string_field(profile, "AutoScale", backup.had_auto_scale, backup.auto_scale, changed); restore_int_field(profile, "AutoScaleDelay", backup.had_auto_scale_delay, backup.auto_scale_delay, changed); restore_int_field(profile, "LSFG3Target", backup.had_lsfg_target, backup.lsfg_target, changed); restore_string_field(profile, "CaptureApi", backup.had_capture_api, backup.capture_api, changed); restore_int_field(profile, "QueueTarget", backup.had_queue_target, backup.queue_target, changed); restore_bool_field(profile, "HdrSupport", backup.had_hdr_support, backup.hdr_support, changed); restore_int_field(profile, "LSFGFlowScale", backup.had_flow_scale, backup.flow_scale, changed); restore_string_field(profile, "LSFGSize", backup.had_lsfg_size, backup.lsfg_size, changed); restore_string_field(profile, "LSFG3Mode1", backup.had_lsfg3_mode, backup.lsfg3_mode, changed); restore_string_field(profile, "FrameGeneration", backup.had_frame_generation, backup.frame_generation, changed); restore_string_field(profile, "ScalingType", backup.had_scaling_type, backup.scaling_type, changed); restore_string_field(profile, "LS1Type", backup.had_ls1_type, backup.ls1_type, changed); restore_string_field(profile, "ScalingMode", backup.had_scaling_mode, backup.scaling_mode, changed); restore_bool_field(profile, "ResizeBeforeScaling", backup.had_resize_before_scaling, backup.resize_before_scaling, changed); restore_string_field(profile, "ScalingFitMode", backup.had_scaling_fit_mode, backup.scaling_fit_mode, changed); restore_double_field(profile, "ScaleFactor", backup.had_scale_factor, backup.scale_factor, changed); restore_int_field(profile, "Sharpness", backup.had_sharpness, backup.sharpness, changed); restore_int_field(profile, "LS1Sharpness", backup.had_ls1_sharpness, backup.ls1_sharpness, changed); restore_string_field(profile, "Anime4kType", backup.had_anime4k_type, backup.anime4k_type, changed); restore_bool_field(profile, "VRS", backup.had_vrs, backup.vrs, changed); restore_string_field(profile, "SyncMode", backup.had_sync_mode, backup.sync_mode, changed); restore_int_field(profile, "MaxFrameLatency", backup.had_max_frame_latency, backup.max_frame_latency, changed); return changed; } bool write_settings_tree(boost::property_tree::ptree &tree, const std::filesystem::path &path) { strip_xml_whitespace(tree); try { boost::property_tree::xml_writer_settings settings(' ', 2); boost::property_tree::write_xml(path.string(), tree, std::locale(), settings); return true; } catch (...) { BOOST_LOG(warning) << "Lossless Scaling: failed to write settings"; return false; } } } // namespace lossless_scaling_options lossless_scaling_env_loader::load() const { lossless_scaling_options opt; opt.enabled = policy::should_enable_runtime( parse_env_flag(std::getenv("SUNSHINE_LOSSLESS_SCALING_ENABLED")), parse_env_flag(std::getenv("SUNSHINE_LOSSLESS_SCALING_FRAMEGEN")) ); opt.target_fps = parse_env_double(std::getenv("SUNSHINE_LOSSLESS_SCALING_TARGET_FPS")); opt.rtss_limit = parse_env_int(std::getenv("SUNSHINE_LOSSLESS_SCALING_RTSS_LIMIT")); opt.active_profile = parse_env_string(std::getenv("SUNSHINE_LOSSLESS_SCALING_ACTIVE_PROFILE")); opt.capture_api = parse_env_string(std::getenv("SUNSHINE_LOSSLESS_SCALING_CAPTURE_API")); opt.queue_target = parse_env_int_allow_zero(std::getenv("SUNSHINE_LOSSLESS_SCALING_QUEUE_TARGET")); opt.hdr_enabled = parse_env_flag_optional(std::getenv("SUNSHINE_LOSSLESS_SCALING_HDR")); opt.flow_scale = clamp_optional_int(parse_env_int_allow_zero(std::getenv("SUNSHINE_LOSSLESS_SCALING_FLOW_SCALE")), k_flow_scale_min, k_flow_scale_max); opt.performance_mode = parse_env_flag_optional(std::getenv("SUNSHINE_LOSSLESS_SCALING_PERFORMANCE_MODE")); opt.resolution_scale_factor = clamp_optional_double(parse_env_double(std::getenv("SUNSHINE_LOSSLESS_SCALING_RESOLUTION_SCALE")), k_resolution_factor_min, k_resolution_factor_max); opt.frame_generation_mode = parse_env_string(std::getenv("SUNSHINE_LOSSLESS_SCALING_FRAMEGEN_MODE")); opt.lsfg3_mode = parse_env_string(std::getenv("SUNSHINE_LOSSLESS_SCALING_LSFG3_MODE")); opt.scaling_type = parse_env_string(std::getenv("SUNSHINE_LOSSLESS_SCALING_SCALING_TYPE")); opt.sharpness = clamp_optional_int(parse_env_int_allow_zero(std::getenv("SUNSHINE_LOSSLESS_SCALING_SHARPNESS")), k_sharpness_min, k_sharpness_max); opt.ls1_sharpness = clamp_optional_int(parse_env_int_allow_zero(std::getenv("SUNSHINE_LOSSLESS_SCALING_LS1_SHARPNESS")), k_sharpness_min, k_sharpness_max); opt.anime4k_type = parse_env_string(std::getenv("SUNSHINE_LOSSLESS_SCALING_ANIME4K_TYPE")); opt.anime4k_vrs = parse_env_flag_optional(std::getenv("SUNSHINE_LOSSLESS_SCALING_ANIME4K_VRS")); opt.legacy_auto_detect = parse_env_flag(std::getenv("SUNSHINE_LOSSLESS_SCALING_LEGACY_AUTO_DETECT")); if (auto delay = parse_env_int_allow_zero(std::getenv("SUNSHINE_LOSSLESS_SCALING_LAUNCH_DELAY"))) { opt.launch_delay_seconds = std::max(0, *delay); } if (auto configured = get_lossless_scaling_env_path()) { if (!configured->empty()) { opt.configured_path = configured; } } finalize_lossless_options(opt); return opt; } lossless_scaling_metadata_loader::lossless_scaling_metadata_loader(lossless_scaling_app_metadata metadata): _metadata(std::move(metadata)) { } lossless_scaling_options lossless_scaling_metadata_loader::load() const { lossless_scaling_options opt; opt.enabled = _metadata.enabled; opt.target_fps = _metadata.target_fps; opt.rtss_limit = _metadata.rtss_limit; opt.configured_path = _metadata.configured_path; opt.active_profile = _metadata.active_profile; opt.capture_api = _metadata.capture_api; opt.queue_target = _metadata.queue_target; opt.hdr_enabled = _metadata.hdr_enabled; opt.flow_scale = clamp_optional_int(_metadata.flow_scale, k_flow_scale_min, k_flow_scale_max); opt.performance_mode = _metadata.performance_mode; opt.resolution_scale_factor = clamp_optional_double(_metadata.resolution_scale_factor, k_resolution_factor_min, k_resolution_factor_max); opt.frame_generation_mode = _metadata.frame_generation_mode; opt.lsfg3_mode = _metadata.lsfg3_mode; opt.scaling_type = _metadata.scaling_type; opt.sharpness = clamp_optional_int(_metadata.sharpness, k_sharpness_min, k_sharpness_max); opt.ls1_sharpness = clamp_optional_int(_metadata.ls1_sharpness, k_sharpness_min, k_sharpness_max); opt.anime4k_type = _metadata.anime4k_type; opt.anime4k_vrs = _metadata.anime4k_vrs; opt.launch_delay_seconds = std::max(0, _metadata.launch_delay_seconds); opt.legacy_auto_detect = _metadata.legacy_auto_detect; finalize_lossless_options(opt); return opt; } lossless_scaling_options read_lossless_scaling_options() { lossless_scaling_env_loader loader; return loader.load(); } lossless_scaling_options read_lossless_scaling_options(const lossless_scaling_app_metadata &metadata) { lossless_scaling_metadata_loader loader(metadata); return loader.load(); } std::optional lossless_scaling_select_focus_pid(const std::string &install_dir_utf8, const std::string &exe_path_utf8, std::optional preferred_pid) { auto install_dir_norm = normalize_utf8_path(install_dir_utf8); auto exe_path_norm = normalize_utf8_path(exe_path_utf8); if (install_dir_norm && !install_dir_norm->empty() && install_dir_norm->back() != L'\\') { install_dir_norm->push_back(L'\\'); } const bool has_filter = (install_dir_norm && !install_dir_norm->empty()) || (exe_path_norm && !exe_path_norm->empty()); if (!has_filter) { BOOST_LOG(warning) << "Lossless Scaling: PID selection using windowed heuristic (install/exe empty)"; } auto snapshot = enumerate_process_ids_snapshot(); std::vector initial_candidates; std::unordered_set allowed_pids; initial_candidates.reserve(snapshot.size()); for (auto pid : snapshot) { if (pid == 0) { continue; } auto path = query_process_image_path_optional(pid); if (!path) { continue; } if (is_ignored_process_path(*path)) { continue; } bool has_main_window = focus::find_main_window_for_pid(pid) != nullptr; bool path_matches = has_filter && path_matches_filter(*path, install_dir_norm, exe_path_norm); if (policy::should_accept_focus_candidate(has_filter, path_matches, has_main_window)) { initial_candidates.push_back(pid); } } if (initial_candidates.empty()) { return std::nullopt; } if (initial_candidates.size() == 1) { return initial_candidates.front(); } allowed_pids.reserve(initial_candidates.size()); for (auto pid : initial_candidates) { allowed_pids.insert(pid); } struct focus_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; }; std::unordered_map candidates; auto deadline = std::chrono::steady_clock::now() + k_lossless_observation_duration; SYSTEM_INFO sys_info {}; GetSystemInfo(&sys_info); double cpu_count = sys_info.dwNumberOfProcessors > 0 ? static_cast(sys_info.dwNumberOfProcessors) : 1.0; while (std::chrono::steady_clock::now() < deadline) { auto now = std::chrono::steady_clock::now(); snapshot = enumerate_process_ids_snapshot(); for (auto pid : snapshot) { if (pid == 0) { continue; } if (!allowed_pids.empty() && allowed_pids.find(pid) == allowed_pids.end()) { continue; } auto &entry = candidates[pid]; if (entry.pid == 0) { auto path = query_process_image_path_optional(pid); if (!path || is_ignored_process_path(*path)) { candidates.erase(pid); continue; } if (has_filter && !path_matches_filter(*path, install_dir_norm, exe_path_norm)) { candidates.erase(pid); continue; } entry.pid = pid; entry.path = *path; 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; } } std::this_thread::sleep_for(k_lossless_poll_interval); } if (candidates.empty()) { return initial_candidates.front(); } 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; bool exe_match; }; std::vector scores; scores.reserve(candidates.size()); for (auto &[pid, candidate] : candidates) { if (candidate.start_cpu == 0 || candidate.last_cpu < candidate.start_cpu) { continue; } if (candidate.last_seen <= candidate.first_seen) { continue; } if (candidate.path.empty()) { continue; } double elapsed = std::chrono::duration(candidate.last_seen - candidate.first_seen).count(); if (elapsed <= 0.1) { elapsed = 0.1; } double cpu_seconds = static_cast(candidate.last_cpu - candidate.start_cpu) / 10000000.0; if (cpu_seconds < 0.0) { cpu_seconds = 0.0; } double cpu_ratio = cpu_seconds / (elapsed * cpu_count); if (cpu_ratio < 0.0) { cpu_ratio = 0.0; } double mem_mb = static_cast(candidate.peak_working_set) / (1024.0 * 1024.0); auto normalized_path = normalize_lowercase_path(candidate.path); bool matches = install_dir_norm && !install_dir_norm->empty() ? path_matches_prefix(normalized_path, *install_dir_norm) : false; bool exe_match = exe_path_norm && !exe_path_norm->empty() && normalized_path == *exe_path_norm; scores.push_back({pid, candidate.path, cpu_ratio, mem_mb, matches, exe_match}); max_cpu_ratio = std::max(max_cpu_ratio, cpu_ratio); max_mem = std::max(max_mem, mem_mb); } if (scores.empty()) { return initial_candidates.front(); } 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; DWORD root_pid = preferred_pid.value_or(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.exe_match) { combined += 0.25; } if (root_pid && 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 initial_candidates.front(); } BOOST_LOG(debug) << "Lossless Scaling: focus candidate PID=" << best->pid << " cpu=" << best->cpu_ratio << " memMB=" << best->mem_mb; BOOST_LOG(info) << "Lossless Scaling: selected focus PID=" << best->pid << " exe=" << platf::dxgi::wide_to_utf8(best->path); return best->pid; } lossless_scaling_runtime_state capture_lossless_scaling_state() { lossless_scaling_runtime_state state; const std::array names {L"Lossless Scaling.exe", L"LosslessScaling.exe"}; for (auto name : names) { collect_runtime_for_process(name, state); } state.previously_running = !state.running_pids.empty(); return state; } void lossless_scaling_stop_processes(lossless_scaling_runtime_state &state) { if (state.running_pids.empty()) { return; } lossless_scaling_post_wm_close(state.running_pids); for (DWORD pid : state.running_pids) { HANDLE h = OpenProcess(SYNCHRONIZE | PROCESS_TERMINATE, FALSE, pid); if (!h) { continue; } DWORD wait = WaitForSingleObject(h, 4000); if (wait == WAIT_TIMEOUT) { TerminateProcess(h, 0); WaitForSingleObject(h, 2000); } CloseHandle(h); } state.stopped = true; } bool focus_and_minimize_existing_instances(const lossless_scaling_runtime_state &state, bool minimize_window) { if (state.stopped || !state.previously_running) { return false; } bool handled = false; for (DWORD pid : state.running_pids) { bool focused = lossless_scaling_focus_window(pid); bool minimized = minimize_window ? lossless_scaling_minimize_window(pid) : false; handled = focused || minimized || handled; } return handled; } static bool focus_with_retry(DWORD pid, int attempts, std::chrono::milliseconds delay) { if (!pid) { return false; } for (int attempt = 0; attempt < attempts; ++attempt) { if (lossless_scaling_focus_window(pid)) { return true; } std::this_thread::sleep_for(delay); } return false; } std::pair focus_and_minimize_new_process(PROCESS_INFORMATION &pi, DWORD game_pid, bool minimize_window) { bool focused = false; bool minimized = false; if (pi.hProcess) { WaitForInputIdle(pi.hProcess, 5000); constexpr int kFocusRetries = 4; constexpr auto kRetryDelay = std::chrono::milliseconds(120); if (game_pid) { focus_with_retry(game_pid, kFocusRetries, kRetryDelay); std::this_thread::sleep_for(150ms); bool first_lossless = focus_with_retry(pi.dwProcessId, kFocusRetries, kRetryDelay); focused = focused || first_lossless; std::this_thread::sleep_for(150ms); focus_with_retry(game_pid, kFocusRetries, kRetryDelay); std::this_thread::sleep_for(150ms); bool second_lossless = focus_with_retry(pi.dwProcessId, kFocusRetries, kRetryDelay); focused = focused || second_lossless; } else { focused = focus_with_retry(pi.dwProcessId, kFocusRetries, kRetryDelay); } if (minimize_window) { minimized = lossless_scaling_minimize_window(pi.dwProcessId); } if (!focused) { focused = lossless_scaling_focus_window(pi.dwProcessId); } if (game_pid) { std::this_thread::sleep_for(150ms); focus_with_retry(game_pid, kFocusRetries, kRetryDelay); } CloseHandle(pi.hProcess); pi.hProcess = nullptr; } if (pi.hThread) { CloseHandle(pi.hThread); pi.hThread = nullptr; } return {focused, minimized}; } bool launch_lossless_executable(const std::wstring &exe, DWORD game_pid, bool minimize_window) { if (exe.empty()) { return false; } SetLastError(ERROR_SUCCESS); STARTUPINFOW si {sizeof(si)}; si.dwFlags = STARTF_USESHOWWINDOW; si.wShowWindow = SW_SHOWNORMAL; PROCESS_INFORMATION pi {}; std::wstring cmd = L"\"" + exe + L"\""; std::vector cmdline(cmd.begin(), cmd.end()); cmdline.push_back(L'\0'); auto finalize_launch = [&](PROCESS_INFORMATION &proc_info) { auto [focused, minimized] = focus_and_minimize_new_process(proc_info, game_pid, minimize_window); if (!focused) { BOOST_LOG(debug) << "Lossless Scaling: launched but could not focus window"; } if (minimize_window && !minimized) { BOOST_LOG(debug) << "Lossless Scaling: launched but could not minimize window"; } return true; }; auto close_process_handles = [&]() { if (pi.hProcess) { CloseHandle(pi.hProcess); pi.hProcess = nullptr; } if (pi.hThread) { CloseHandle(pi.hThread); pi.hThread = nullptr; } }; bool launched = false; if (platf::dxgi::is_running_as_system()) { winrt::handle user_token {platf::dxgi::retrieve_users_token(false)}; if (user_token) { LPVOID raw_env = nullptr; if (!CreateEnvironmentBlock(&raw_env, user_token.get(), FALSE)) { raw_env = nullptr; } std::unique_ptr env_block(raw_env, DestroyEnvironmentBlock); BOOL ok = FALSE; if (ImpersonateLoggedOnUser(user_token.get())) { auto revert_guard = util::fail_guard([&]() { if (!RevertToSelf()) { DWORD err = GetLastError(); BOOST_LOG(fatal) << "Lossless Scaling: failed to revert impersonation after launch, error=" << err; DebugBreak(); } }); ok = CreateProcessAsUserW( user_token.get(), exe.c_str(), cmdline.data(), nullptr, nullptr, FALSE, CREATE_UNICODE_ENVIRONMENT, env_block.get(), nullptr, &si, &pi ); if (!ok) { DWORD err = GetLastError(); BOOST_LOG(warning) << "Lossless Scaling: CreateProcessAsUser failed, error=" << err; SetLastError(err); } } else { DWORD err = GetLastError(); BOOST_LOG(warning) << "Lossless Scaling: impersonation failed for CreateProcessAsUser, error=" << err; SetLastError(err); } if (ok) { launched = true; } else { close_process_handles(); } } else { BOOST_LOG(debug) << "Lossless Scaling: no user token available for impersonated launch"; } } if (!launched) { if (!CreateProcessW(exe.c_str(), cmdline.data(), nullptr, nullptr, FALSE, CREATE_UNICODE_ENVIRONMENT, nullptr, nullptr, &si, &pi)) { DWORD err = GetLastError(); BOOST_LOG(warning) << "Lossless Scaling: CreateProcess fallback failed, error=" << err; close_process_handles(); SetLastError(err); return false; } launched = true; } bool result = launched && finalize_launch(pi); close_process_handles(); return result; } bool lossless_scaling_apply_global_profile(const lossless_scaling_options &options, const std::string &install_dir_utf8, const std::string &exe_path_utf8, lossless_scaling_profile_backup &backup) { backup = {}; auto worker = [&]() -> bool { auto settings_path = lossless_scaling_settings_path(); if (settings_path.empty()) { return false; } boost::property_tree::ptree tree; try { boost::property_tree::read_xml(settings_path.string(), tree); } catch (...) { BOOST_LOG(warning) << "Lossless Scaling: failed to read settings"; return false; } auto profiles_opt = tree.get_child_optional("Settings.GameProfiles"); if (!profiles_opt) { BOOST_LOG(warning) << "Lossless Scaling: GameProfiles missing"; return false; } auto &profiles = *profiles_opt; bool removed = remove_vibeshine_profiles(profiles); ProfileTemplates templates = find_profile_templates(profiles); capture_backup_fields(templates, backup); auto base_dir = lossless_resolve_base_dir(install_dir_utf8, exe_path_utf8); auto explicit_exe = resolve_explicit_executable(exe_path_utf8); std::string filter_utf8 = build_executable_filter(base_dir, explicit_exe); bool inserted = insert_vibeshine_profile(templates, options, filter_utf8, profiles, backup); if (!removed && !inserted) { return false; } return write_settings_tree(tree, settings_path); }; return run_with_user_context(worker); } bool lossless_scaling_restore_global_profile(const lossless_scaling_profile_backup &backup) { auto worker = [&]() -> bool { auto settings_path = lossless_scaling_settings_path(); if (settings_path.empty()) { return false; } boost::property_tree::ptree tree; try { boost::property_tree::read_xml(settings_path.string(), tree); } catch (...) { return false; } auto profiles_opt = tree.get_child_optional("Settings.GameProfiles"); if (!profiles_opt) { return false; } auto &profiles = *profiles_opt; bool changed = remove_vibeshine_profiles(profiles); ProfileTemplates templates = find_profile_templates(profiles); if (templates.defaults && backup.valid) { changed |= apply_backup_to_profile(*templates.defaults, backup); } if (!changed) { return false; } return write_settings_tree(tree, settings_path); }; return run_with_user_context(worker); } void lossless_scaling_restart_foreground(const lossless_scaling_runtime_state &state, bool force_launch, const std::string &install_dir_utf8, const std::string &exe_path_utf8, DWORD focused_game_pid, bool legacy_auto_detect) { focus_and_minimize_existing_instances(state, !legacy_auto_detect); const bool should_launch = policy::should_launch_new_instance({state.running_pids.size(), state.stopped}, force_launch); DWORD target_pid = focused_game_pid; if (!target_pid) { if (auto selected = lossless_scaling_select_focus_pid(install_dir_utf8, exe_path_utf8, std::nullopt)) { target_pid = *selected; } } if (!target_pid) { if (auto foreground_match = match_foreground_pid_to_filter(install_dir_utf8, exe_path_utf8)) { target_pid = *foreground_match; } } if (!install_dir_utf8.empty() || !exe_path_utf8.empty()) { auto base_dir = lossless_resolve_base_dir(install_dir_utf8, exe_path_utf8); auto explicit_exe = resolve_explicit_executable(exe_path_utf8); auto exe_names = lossless_collect_executable_names(base_dir.value_or(std::filesystem::path()), explicit_exe); if (!exe_names.empty()) { const char *timeout_env = std::getenv("SUNSHINE_LOSSLESS_WAIT_TIMEOUT"); int timeout_secs = 10; if (auto parsed = parse_env_int(timeout_env)) { timeout_secs = std::clamp(*parsed, 1, 60); } BOOST_LOG(debug) << "Lossless Scaling: waiting up to " << timeout_secs << " seconds for game process to appear (checking " << exe_names.size() << " executables)"; bool game_detected = wait_for_any_executable(exe_names, timeout_secs); if (game_detected) { BOOST_LOG(info) << "Lossless Scaling: game detected"; } } } if (!target_pid) { if (auto selected = lossless_scaling_select_focus_pid(install_dir_utf8, exe_path_utf8, std::nullopt)) { target_pid = *selected; } else if (auto foreground_match = match_foreground_pid_to_filter(install_dir_utf8, exe_path_utf8)) { target_pid = *foreground_match; } } if (should_launch) { auto exe = select_lossless_launch_exe(state, exe_path_utf8); if (!exe || exe->empty() || !std::filesystem::exists(*exe)) { BOOST_LOG(debug) << "Lossless Scaling: executable path not resolved for relaunch"; return; } if (launch_lossless_executable(*exe, target_pid, !legacy_auto_detect)) { BOOST_LOG(info) << "Lossless Scaling: relaunched at " << platf::dxgi::wide_to_utf8(*exe); } else { DWORD err = GetLastError(); BOOST_LOG(warning) << "Lossless Scaling: relaunch failed, error=" << err; return; } if (!wait_for_lossless_ready(3s)) { BOOST_LOG(debug) << "Lossless Scaling: hotkey readiness wait timed out"; } else { std::this_thread::sleep_for(150ms); } } auto hotkey = read_lossless_hotkey(); if (!hotkey) { BOOST_LOG(warning) << "Lossless Scaling: no hotkey configured; skipping activation"; return; } if (!hotkey->modifiers.empty()) { std::ostringstream mods; for (size_t i = 0; i < hotkey->modifiers.size(); ++i) { if (i > 0) { mods << "+"; } mods << "0x" << std::hex << static_cast(hotkey->modifiers[i]); } BOOST_LOG(debug) << "Lossless Scaling: hotkey vk=0x" << std::hex << static_cast(hotkey->key) << " mods=" << mods.str(); } else { BOOST_LOG(debug) << "Lossless Scaling: hotkey vk=0x" << std::hex << static_cast(hotkey->key) << " mods=none"; } if (!target_pid) { if (auto foreground_match = match_foreground_pid_to_filter(install_dir_utf8, exe_path_utf8)) { target_pid = *foreground_match; } } if (legacy_auto_detect && !target_pid) { for (int attempt = 0; attempt < 3 && !target_pid; ++attempt) { std::this_thread::sleep_for(1s); if (auto selected = lossless_scaling_select_focus_pid(install_dir_utf8, exe_path_utf8, std::nullopt)) { target_pid = *selected; break; } if (auto foreground_match = match_foreground_pid_to_filter(install_dir_utf8, exe_path_utf8)) { target_pid = *foreground_match; break; } } } if (legacy_auto_detect) { return; } HWND target_hwnd = nullptr; if (target_pid) { target_hwnd = focus_game_window(target_pid); } bool sent = apply_hotkey_for_pid(*hotkey, target_pid, target_hwnd != nullptr, 3); if (!sent) { BOOST_LOG(warning) << "Lossless Scaling: failed to send hotkey after retries"; } if (!target_pid) { return; } constexpr int k_retarget_checks = 12; for (int attempt = 0; attempt < k_retarget_checks; ++attempt) { std::this_thread::sleep_for(1s); auto next_pid_opt = lossless_scaling_select_focus_pid(install_dir_utf8, exe_path_utf8, target_pid); if (!next_pid_opt || *next_pid_opt == target_pid) { continue; } DWORD next_pid = *next_pid_opt; BOOST_LOG(info) << "Lossless Scaling: retargeting from PID=" << target_pid << " to PID=" << next_pid; apply_hotkey_for_pid(*hotkey, target_pid, true, 2); HWND new_hwnd = wait_for_game_window(next_pid, 6s); if (new_hwnd) { focus::try_focus_hwnd(new_hwnd); } else { new_hwnd = focus_game_window(next_pid); } apply_hotkey_for_pid(*hotkey, next_pid, true, 2); target_pid = next_pid; } } } // namespace playnite_launcher::lossless