server/tools/playnite_launcher/lossless_scaling.cpp
Rama main · réplica de lectura del repositorio original.
#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 <algorithm>
#include <array>
#include <boost/algorithm/string.hpp>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/xml_parser.hpp>
#include <cctype>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <cwchar>
#include <cwctype>
#include <filesystem>
#include <fstream>
#include <limits>
#include <locale>
#include <memory>
#include <optional>
#include <Psapi.h>
#include <shlobj.h>
#include <string>
#include <system_error>
#include <thread>
#include <unordered_map>
#include <unordered_set>
#include <UserEnv.h>
#include <utility>
#include <vector>
#include <windows.h>
#include <winrt/base.h>
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<std::filesystem::path> resolve_explicit_executable(const std::string &exe_path_utf8);
template<typename Fn>
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<char>(std::tolower(c));
});
return v == "1" || v == "true" || v == "yes";
}
std::optional<int> 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<int> 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<bool> parse_env_flag_optional(const char *value) {
if (!value || !*value) {
return std::nullopt;
}
return parse_env_flag(value);
}
std::optional<std::string> 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<WORD> 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<WORD> 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<WORD>(c);
}
if (c >= '0' && c <= '9') {
return static_cast<WORD>(c);
}
}
if (text.size() > 1 && text.front() == 'F') {
try {
int fkey = std::stoi(text.substr(1));
if (fkey >= 1 && fkey <= 24) {
return static_cast<WORD>(VK_F1 + (fkey - 1));
}
} catch (...) {
}
}
struct NamedKey {
const char *name;
WORD vk;
};
static const std::array<NamedKey, 20> 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<WORD>(VK_NUMPAD0 + num);
}
return std::nullopt;
}
std::vector<WORD> parse_hotkey_modifiers(const std::string &value) {
std::unordered_set<WORD> mods;
std::string token;
for (char ch : value) {
if (std::isspace(static_cast<unsigned char>(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<WORD> ordered;
ordered.reserve(mods.size());
const std::array<WORD, 4> 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<INPUT> 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<UINT>(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<lossless_hotkey_t> read_lossless_hotkey() {
auto worker = [&]() -> std::optional<lossless_hotkey_t> {
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<std::string>("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<std::string>("Settings.HotkeyModifierKeys")) {
hotkey.modifiers = parse_hotkey_modifiers(*mods);
}
return hotkey;
};
return run_with_user_context(worker);
}
std::optional<int> clamp_optional_int(std::optional<int> value, int min_value, int max_value) {
if (!value) {
return std::nullopt;
}
return std::clamp(*value, min_value, max_value);
}
std::optional<double> 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<double> clamp_optional_double(std::optional<double> 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<std::filesystem::path> 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<std::filesystem::path> normalize_directory(std::optional<std::filesystem::path> 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<std::filesystem::path> 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<std::filesystem::path> 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<std::wstring> &seen, std::vector<std::wstring> &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<std::wstring> &seen, std::vector<std::wstring> &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<std::filesystem::path> &explicit_exe, const std::filesystem::path &base_dir, std::unordered_set<std::wstring> &seen, std::vector<std::wstring> &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<std::wstring> &executables) {
std::sort(executables.begin(), executables.end(), [](std::wstring a, std::wstring b) {
lowercase_inplace(a);
lowercase_inplace(b);
return a < b;
});
}
std::vector<std::wstring> lossless_collect_executable_names(const std::filesystem::path &base_dir, const std::optional<std::filesystem::path> &explicit_exe) {
std::vector<std::wstring> executables;
std::unordered_set<std::wstring> 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<std::wstring> &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<std::filesystem::path> 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<std::wstring> 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<std::wstring> 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<std::wstring> 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<std::wstring> exe_from_program_files() {
const std::array<const wchar_t *, 2> 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<std::wstring> 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 == "<xmltext>") {
it = node.erase(it);
} else {
strip_xml_whitespace(it->second);
++it;
}
}
}
std::optional<std::wstring> discover_lossless_scaling_exe(const lossless_scaling_runtime_state &state);
std::optional<std::wstring> 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<std::wstring> 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<std::wstring> &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<std::wstring> &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<Ctx *>(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<LPARAM>(&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<Ctx *>(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<LPARAM>(&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<Ctx *>(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<LPARAM>(&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<Ctx *>(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<LPARAM>(&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<wchar_t>(std::towlower(ch));
}
return value;
}
std::optional<std::wstring> 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<std::wstring> 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<std::wstring> &install_dir_norm, const std::optional<std::wstring> &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<DWORD> 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<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;
}
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;
}
void lossless_scaling_post_wm_close(const std::vector<DWORD> &pids) {
if (pids.empty()) {
return;
}
struct EnumCtx {
const std::vector<DWORD> *pids;
} ctx {&pids};
EnumWindows([](HWND hwnd, LPARAM lparam) -> BOOL {
auto *ctx = reinterpret_cast<EnumCtx *>(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<LPARAM>(&ctx));
}
std::optional<std::wstring> 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<DWORD>(buffer.size());
std::optional<std::wstring> 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<std::string>("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<std::string>("AutoScale")) {
backup.had_auto_scale = true;
backup.auto_scale = *auto_scale;
}
if (auto delay = source->get_optional<int>("AutoScaleDelay")) {
backup.had_auto_scale_delay = true;
backup.auto_scale_delay = *delay;
}
if (auto target = source->get_optional<int>("LSFG3Target")) {
backup.had_lsfg_target = true;
backup.lsfg_target = *target;
}
if (auto capture = source->get_optional<std::string>("CaptureApi")) {
backup.had_capture_api = true;
backup.capture_api = *capture;
}
if (auto queue = source->get_optional<int>("QueueTarget")) {
backup.had_queue_target = true;
backup.queue_target = *queue;
}
if (auto hdr = source->get_optional<bool>("HdrSupport")) {
backup.had_hdr_support = true;
backup.hdr_support = *hdr;
}
if (auto flow = source->get_optional<int>("LSFGFlowScale")) {
backup.had_flow_scale = true;
backup.flow_scale = *flow;
}
if (auto size = source->get_optional<std::string>("LSFGSize")) {
backup.had_lsfg_size = true;
backup.lsfg_size = *size;
}
if (auto mode = source->get_optional<std::string>("LSFG3Mode1")) {
backup.had_lsfg3_mode = true;
backup.lsfg3_mode = *mode;
}
if (auto frame = source->get_optional<std::string>("FrameGeneration")) {
backup.had_frame_generation = true;
backup.frame_generation = *frame;
}
if (auto scaling_type = source->get_optional<std::string>("ScalingType")) {
backup.had_scaling_type = true;
backup.scaling_type = *scaling_type;
}
if (auto ls1_type = source->get_optional<std::string>("LS1Type")) {
backup.had_ls1_type = true;
backup.ls1_type = *ls1_type;
}
if (auto scaling_mode = source->get_optional<std::string>("ScalingMode")) {
backup.had_scaling_mode = true;
backup.scaling_mode = *scaling_mode;
}
if (auto resize_before = source->get_optional<bool>("ResizeBeforeScaling")) {
backup.had_resize_before_scaling = true;
backup.resize_before_scaling = *resize_before;
}
if (auto scaling_fit_mode = source->get_optional<std::string>("ScalingFitMode")) {
backup.had_scaling_fit_mode = true;
backup.scaling_fit_mode = *scaling_fit_mode;
}
if (auto scale_factor = source->get_optional<double>("ScaleFactor")) {
backup.had_scale_factor = true;
backup.scale_factor = *scale_factor;
}
if (auto sharpness = source->get_optional<int>("Sharpness")) {
backup.had_sharpness = true;
backup.sharpness = *sharpness;
}
if (auto ls1 = source->get_optional<int>("LS1Sharpness")) {
backup.had_ls1_sharpness = true;
backup.ls1_sharpness = *ls1;
}
if (auto anime_type = source->get_optional<std::string>("Anime4kType")) {
backup.had_anime4k_type = true;
backup.anime4k_type = *anime_type;
}
if (auto vrs = source->get_optional<bool>("VRS")) {
backup.had_vrs = true;
backup.vrs = *vrs;
}
if (auto sync = source->get_optional<std::string>("SyncMode")) {
backup.had_sync_mode = true;
backup.sync_mode = *sync;
}
if (auto latency = source->get_optional<int>("MaxFrameLatency")) {
backup.had_max_frame_latency = true;
backup.max_frame_latency = *latency;
}
}
std::optional<std::filesystem::path> 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<std::filesystem::path> &base_dir, const std::optional<std::filesystem::path> &explicit_exe) {
std::vector<std::wstring> 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<std::string>("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<std::string>(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<int>(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<bool>(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<double>(key);
if (had_value) {
if (!current || std::abs(*current - value) > std::numeric_limits<double>::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<std::string> 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<DWORD> lossless_scaling_select_focus_pid(const std::string &install_dir_utf8, const std::string &exe_path_utf8, std::optional<DWORD> 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<DWORD> initial_candidates;
std::unordered_set<DWORD> 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<DWORD, focus_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) {
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<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()) {
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);
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<const wchar_t *, 2> 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<bool, bool> 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<wchar_t> 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<void, decltype(&DestroyEnvironmentBlock)> 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<int>(hotkey->modifiers[i]);
}
BOOST_LOG(debug) << "Lossless Scaling: hotkey vk=0x" << std::hex << static_cast<int>(hotkey->key)
<< " mods=" << mods.str();
} else {
BOOST_LOG(debug) << "Lossless Scaling: hotkey vk=0x" << std::hex << static_cast<int>(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