/** * @file tools/display_settings_helper.cpp * @brief Detached helper to apply/revert Windows display settings via IPC. */ #ifdef _WIN32 // standard #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 // third-party (libdisplaydevice) #include "src/logging.h" #include "src/platform/windows/ipc/pipes.h" #include #include #include #include #include #include #include #include #include #include #include // platform #ifndef SECURITY_WIN32 #define SECURITY_WIN32 #endif #include #include #include #include #include #include #include #include #include #include #include namespace { static const GUID kMonitorInterfaceGuid = {0xe6f07b5f, 0xee97, 0x4a90, {0xb0, 0x76, 0x33, 0xf5, 0x7b, 0xf4, 0xea, 0xa7}}; } using namespace std::chrono_literals; namespace bl = boost::log; namespace { constexpr DWORD kInvalidSessionId = static_cast(-1); std::wstring query_session_account(DWORD session_id) { if (session_id == kInvalidSessionId) { return {}; } auto fetch_session_string = [&](WTS_INFO_CLASS info_class) -> std::wstring { LPWSTR buffer = nullptr; DWORD bytes = 0; if (!WTSQuerySessionInformationW(WTS_CURRENT_SERVER_HANDLE, session_id, info_class, &buffer, &bytes)) { return {}; } std::wstring value; if (buffer && *buffer != L'\0') { value.assign(buffer); } if (buffer) { WTSFreeMemory(buffer); } return value; }; std::wstring user = fetch_session_string(WTSUserName); if (user.empty()) { return {}; } std::wstring domain = fetch_session_string(WTSDomainName); if (!domain.empty()) { return domain + L"\\" + user; } return user; } std::wstring build_restore_task_name(const std::wstring &username) { return L"ArtLightDisplayRestore"; } // Trigger a more robust Explorer/shell refresh so that desktop/taskbar icons // and other shell-controlled UI elements pick up DPI/metrics changes that // can occur after monitor topology/primary swaps. Avoids wrong-sized icons // without restarting Explorer. inline void refresh_shell_after_display_change() { // 1) Ask the shell to refresh associations/images and flush notifications. // SHCNF_FLUSHNOWAIT avoids blocking if the shell is busy. SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST | SHCNF_FLUSHNOWAIT, nullptr, nullptr); // 2) Force a reload of system icons. This does not change user settings // but prompts Explorer to re-query default icons and sizes. SystemParametersInfoW(SPI_SETICONS, 0, nullptr, SPIF_SENDCHANGE); // Helper to safely broadcast a message with a short timeout so we don't hang // if any app stops responding. auto broadcast = [](UINT msg, WPARAM wParam, LPARAM lParam) { DWORD_PTR result = 0; SendMessageTimeoutW(HWND_BROADCAST, msg, wParam, lParam, SMTO_ABORTIFHUNG | SMTO_NORMAL, 100, &result); }; // 3) Broadcast targeted setting changes that commonly trigger Explorer to // refresh icon metrics and shell state. static const wchar_t kShellState[] = L"ShellState"; static const wchar_t kIconMetrics[] = L"IconMetrics"; broadcast(WM_SETTINGCHANGE, 0, reinterpret_cast(kShellState)); broadcast(WM_SETTINGCHANGE, 0, reinterpret_cast(kIconMetrics)); // 4) Broadcast a display change with current depth and resolution to nudge // windows that cache DPI-dependent icon resources. HDC hdc = GetDC(nullptr); int bpp = 32; if (hdc) { const int planes = GetDeviceCaps(hdc, PLANES); const int bits = GetDeviceCaps(hdc, BITSPIXEL); if (planes > 0 && bits > 0) { bpp = planes * bits; } ReleaseDC(nullptr, hdc); } const LPARAM res = MAKELPARAM(GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN)); broadcast(WM_DISPLAYCHANGE, static_cast(bpp), res); } // Simple framed protocol: [u32 length][u8 type][payload...] enum class MsgType : uint8_t { Apply = 1, // payload: JSON SingleDisplayConfiguration Revert = 2, // no payload Reset = 3, // clear persistence (best-effort) ExportGolden = 4, // no payload; export current settings snapshot as golden restore ApplyResult = 6, // payload: [u8 success][optional message...] Disarm = 7, // cancel any pending restore requests/watchdogs SnapshotCurrent = 8, // snapshot current session state (rotate current->previous) without applying RefreshRate = 10, // payload: [u32 numerator][u32 denominator][UTF-8 device id] RefreshRateResult = 11, // payload: [u8 success] Ping = 0xFE, // no payload, reply with Pong Stop = 0xFF // no payload, terminate process }; inline void send_framed_content(platf::dxgi::AsyncNamedPipe &pipe, MsgType type, std::span payload = {}) { std::vector out(1 + payload.size()); out.front() = static_cast(type); std::copy(payload.begin(), payload.end(), out.begin() + 1); pipe.send(out); } std::optional read_u32_le(std::span payload, std::size_t offset) { if (offset + 4 > payload.size()) { return std::nullopt; } return static_cast(payload[offset]) | (static_cast(payload[offset + 1]) << 8u) | (static_cast(payload[offset + 2]) << 16u) | (static_cast(payload[offset + 3]) << 24u); } // Wrap SettingsManager for easy use in this helper class DisplayController { public: DisplayController() = default; using layout_rotation_map_t = std::map; struct LoadedSnapshot { display_device::DisplaySettingsSnapshot snapshot; int snapshot_version {1}; bool has_layout_data {false}; layout_rotation_map_t layout_rotations; }; static constexpr int snapshot_layout_version_latest = 2; static std::string ascii_lower(std::string s) { for (char &ch : s) { if (ch >= 'A' && ch <= 'Z') { ch = static_cast(ch - 'A' + 'a'); } } return s; } static std::vector flatten_topology_device_ids(const display_device::ActiveTopology &topology) { std::vector ids; for (const auto &group : topology) { for (const auto &id : group) { if (!id.empty()) { ids.push_back(id); } } } std::sort(ids.begin(), ids.end()); ids.erase(std::unique(ids.begin(), ids.end()), ids.end()); return ids; } std::vector missing_devices_for_topology(const display_device::ActiveTopology &topology) const { const auto topo_ids = flatten_topology_device_ids(topology); if (topo_ids.empty()) { return {}; } const auto current_ids = enum_all_device_ids(); std::set current_norm; for (const auto &id : current_ids) { current_norm.insert(ascii_lower(id)); } std::vector missing; for (const auto &id : topo_ids) { if (current_norm.find(ascii_lower(id)) == current_norm.end()) { missing.push_back(id); } } return missing; } // Enumerate all currently available display device IDs (active or inactive). std::set enum_all_device_ids() const { std::set ids; for (const auto &d : enumerate_devices(display_device::DeviceEnumerationDetail::Minimal)) { const auto id = d.m_device_id.empty() ? d.m_display_name : d.m_device_id; if (!id.empty()) { ids.insert(id); } } return ids; } layout_rotation_map_t snapshot_layout_rotations(const std::set &device_ids = {}) const { layout_rotation_map_t out; if (!ensure_initialized()) { return out; } auto names = active_display_names_by_device_id(device_ids); for (const auto &[device_id, display_name] : names) { if (auto rotation = read_display_rotation_degrees(display_name)) { out.emplace(device_id, *rotation); } } return out; } bool apply_layout_rotations(const layout_rotation_map_t &layout_rotations) const { if (layout_rotations.empty()) { return true; } if (!ensure_initialized()) { return false; } auto names = active_display_names_by_device_id(); // --- Phase 1: Prepare all rotation changes without applying them --- std::vector pending; bool all_ok = true; for (const auto &[device_id, rotation] : layout_rotations) { auto it = names.find(device_id); if (it == names.end()) { BOOST_LOG(warning) << "Layout restore: device missing while applying rotation: " << device_id; all_ok = false; continue; } auto prepared = prepare_display_rotation(it->second, rotation); if (!prepared) { BOOST_LOG(warning) << "Layout restore: failed to prepare rotation for " << device_id << " (" << rotation << " degrees)"; all_ok = false; continue; } if (!prepared->already_correct) { pending.push_back(std::move(*prepared)); } } if (pending.empty()) { return all_ok; // All displays already at correct rotation } // Fast path: single display doesn't need batching if (pending.size() == 1) { auto *request = reinterpret_cast(pending[0].devmode_buffer.data()); LONG result = ChangeDisplaySettingsExW(pending[0].display_name.c_str(), request, nullptr, CDS_UPDATEREGISTRY, nullptr); if (result != DISP_CHANGE_SUCCESSFUL) { result = ChangeDisplaySettingsExW(pending[0].display_name.c_str(), request, nullptr, 0, nullptr); } if (result != DISP_CHANGE_SUCCESSFUL) { BOOST_LOG(warning) << "Layout restore: ChangeDisplaySettingsEx failed for display " << std::string(pending[0].display_name.begin(), pending[0].display_name.end()) << " (error=" << result << ")"; all_ok = false; } return all_ok; } // --- Phase 2: Batch all changes to registry with CDS_NORESET --- // Each call writes to the registry but does NOT trigger a mode change. // This prevents the OS from validating intermediate topological states. for (auto &prep : pending) { auto *request = reinterpret_cast(prep.devmode_buffer.data()); LONG result = ChangeDisplaySettingsExW( prep.display_name.c_str(), request, nullptr, CDS_UPDATEREGISTRY | CDS_NORESET, nullptr ); if (result != DISP_CHANGE_SUCCESSFUL) { BOOST_LOG(warning) << "Layout restore: CDS_NORESET batch failed for display " << std::string(prep.display_name.begin(), prep.display_name.end()) << " (error=" << result << ")"; all_ok = false; } } // --- Phase 3: Atomic commit — apply all batched registry changes at once --- // A single null-call triggers one WM_DISPLAYCHANGE and one topology validation. LONG commit_result = ChangeDisplaySettingsExW(nullptr, nullptr, nullptr, 0, nullptr); if (commit_result != DISP_CHANGE_SUCCESSFUL) { BOOST_LOG(warning) << "Layout restore: atomic commit of batched rotations failed (error=" << commit_result << ")"; all_ok = false; } return all_ok; } bool current_layout_matches(const layout_rotation_map_t &expected_layout_rotations) const { if (expected_layout_rotations.empty()) { return true; } if (!ensure_initialized()) { return false; } auto names = active_display_names_by_device_id(); for (const auto &[device_id, expected_rotation] : expected_layout_rotations) { auto it = names.find(device_id); if (it == names.end()) { return false; } auto current_rotation = read_display_rotation_degrees(it->second); if (!current_rotation || *current_rotation != expected_rotation) { return false; } } return true; } // Validate whether a snapshot's topology is currently applicable. bool is_topology_valid(const display_device::DisplaySettingsSnapshot &snap) const { if (!ensure_initialized()) { return false; } try { return m_dd->isTopologyValid(snap.m_topology); } catch (...) { return false; } } bool apply( const display_device::SingleDisplayConfiguration &cfg, const std::optional &base_topology ) { if (!ensure_initialized()) { return false; } // For user-requested APPLY operations, avoid triggering display stack recovery. // Recovery is reserved for REVERT/restore paths where "best effort" repair is desired. display_device::DisplayRecoveryBehaviorGuard recovery_guard(display_device::DisplayRecoveryBehavior::Skip); try { if (base_topology && m_dd->isTopologyValid(*base_topology)) { (void) m_dd->setTopology(*base_topology); } } catch (...) { } using enum display_device::SettingsManagerInterface::ApplyResult; const auto res = m_sm->applySettings(cfg); BOOST_LOG(info) << "ApplySettings result: " << static_cast(res); return res == Ok; } bool apply(const display_device::SingleDisplayConfiguration &cfg) { return apply(cfg, std::nullopt); } // Revert display configuration; returns whether reverted OK. bool revert() { if (!ensure_initialized()) { return false; } using enum display_device::SettingsManagerInterface::RevertResult; const auto res = m_sm->revertSettings(); BOOST_LOG(info) << "RevertSettings result: " << static_cast(res); return res == Ok; } // Reset persistence file; best-effort noop persistence returns true. bool reset_persistence() { if (!ensure_initialized()) { return false; } return m_sm->resetPersistence(); } bool recover_display_stack() { if (!ensure_initialized()) { return false; } try { m_wapi->recoverDisplayStack(); return true; } catch (...) { return false; } } bool set_display_origin(const std::string &device_id, const display_device::Point &origin) { if (!ensure_initialized()) { return false; } // Treat monitor reposition as part of APPLY semantics (no recovery). display_device::DisplayRecoveryBehaviorGuard recovery_guard(display_device::DisplayRecoveryBehavior::Skip); try { return m_dd->setDisplayOrigin(device_id, origin); } catch (...) { return false; } } bool can_reposition_device(const std::string &device_id) const { if (device_id.empty() || !ensure_initialized()) { return false; } try { const auto normalized = normalize_device_id(device_id); const auto devices = m_dd->enumAvailableDevices(display_device::DeviceEnumerationDetail::Minimal); for (const auto &device : devices) { if (device.m_device_id.empty()) { continue; } if (normalize_device_id(device.m_device_id) != normalized) { continue; } // Only attempt reposition for currently active displays. return static_cast(device.m_info); } } catch (...) { } return false; } std::optional get_display_resolution(const std::string &device_id) const { if (device_id.empty() || !ensure_initialized()) { return std::nullopt; } try { const auto normalized = normalize_device_id(device_id); const auto devices = m_dd->enumAvailableDevices(display_device::DeviceEnumerationDetail::Minimal); for (const auto &device : devices) { if (device.m_device_id.empty() || normalize_device_id(device.m_device_id) != normalized) { continue; } if (device.m_info) { return device.m_info->m_resolution; } return std::nullopt; } } catch (...) { } return std::nullopt; } /** * @brief Restore a device's refresh rate to the given rational value. * @return True if the mode was successfully applied. */ bool set_device_refresh_rate(const std::string &device_id, unsigned int num, unsigned int den) { if (device_id.empty() || !ensure_initialized()) { return false; } display_device::DisplayRecoveryBehaviorGuard recovery_guard(display_device::DisplayRecoveryBehavior::Skip); try { std::set device_set {device_id}; auto current_modes = m_dd->getCurrentDisplayModes(device_set); if (current_modes.count(device_id)) { const auto ¤t = current_modes[device_id].m_refresh_rate; if (current.m_denominator != 0 && static_cast(current.m_numerator) * den == static_cast(num) * current.m_denominator) { return true; } current_modes[device_id].m_refresh_rate = display_device::Rational {num, den}; return m_dd->setDisplayModes(current_modes); } } catch (...) { } return false; } bool configuration_matches_current_state(const display_device::SingleDisplayConfiguration &cfg) const { if (!ensure_initialized()) { return false; } if (cfg.m_device_id.empty()) { return false; } try { // Use targeted APIs instead of enumAvailableDevices() which enumerates all devices. // getCurrentDisplayModes/getCurrentHdrStates use queryDisplayConfig() to get active // config and search for specific devices - much faster than full enumeration. const std::set device_ids {cfg.m_device_id}; // Check resolution and refresh rate if specified in config if (cfg.m_resolution || cfg.m_refresh_rate) { auto modes = m_dd->getCurrentDisplayModes(device_ids); auto it = modes.find(cfg.m_device_id); if (it == modes.end()) { return false; // Device not active or not found } const auto &mode = it->second; if (cfg.m_resolution) { if (mode.m_resolution.m_width != cfg.m_resolution->m_width || mode.m_resolution.m_height != cfg.m_resolution->m_height) { return false; } } if (cfg.m_refresh_rate) { auto desired = floating_to_double(*cfg.m_refresh_rate); display_device::FloatingPoint actual_fp = mode.m_refresh_rate; auto actual = floating_to_double(actual_fp); if (!desired || !actual || !nearly_equal(*desired, *actual)) { return false; } } } // Check HDR state if specified in config if (cfg.m_hdr_state) { auto hdr_states = m_dd->getCurrentHdrStates(device_ids); auto it = hdr_states.find(cfg.m_device_id); if (it == hdr_states.end() || !it->second || *it->second != *cfg.m_hdr_state) { return false; } } return true; } catch (...) { return false; } } // Capture a full snapshot of current settings. display_device::DisplaySettingsSnapshot snapshot() const { display_device::DisplaySettingsSnapshot snap; if (!ensure_initialized()) { return snap; } try { // Topology - snapshot is taken before virtual displays are created, // so no filtering is needed. snap.m_topology = m_dd->getCurrentTopology(); // Flatten device ids present in topology std::set device_ids; for (const auto &grp : snap.m_topology) { device_ids.insert(grp.begin(), grp.end()); } // Fall back to all enumerated devices if needed if (device_ids.empty()) { collect_all_device_ids(device_ids); } // Modes and HDR snap.m_modes = m_dd->getCurrentDisplayModes(device_ids); snap.m_hdr_states = m_dd->getCurrentHdrStates(device_ids); // Primary device const auto primary = find_primary_in_set(device_ids); if (primary) { snap.m_primary_device = *primary; } // Origins (monitor positions) for (const auto &d : enumerate_devices(display_device::DeviceEnumerationDetail::Minimal)) { const auto id = d.m_device_id.empty() ? d.m_display_name : d.m_device_id; if (!id.empty() && d.m_info && device_ids.count(id)) { snap.m_origins[id] = d.m_info->m_origin_point; } } } catch (...) { // best-effort snapshot } return snap; } // Validate whether a proposed topology is acceptable by the OS using SDC_VALIDATE. bool validate_topology_with_os(const display_device::ActiveTopology &topo) const { if (!ensure_initialized()) { return false; } try { if (!m_dd->isTopologyValid(topo)) { return false; } const auto original_data = m_wapi->queryDisplayConfig(display_device::QueryType::All); if (!original_data) { return false; } const auto path_data = display_device::win_utils::collectSourceDataForMatchingPaths(*m_wapi, original_data->m_paths); if (path_data.empty()) { return false; } auto paths = display_device::win_utils::makePathsForNewTopology(topo, path_data, original_data->m_paths); if (paths.empty()) { return false; } UINT32 flags = SDC_VALIDATE | SDC_TOPOLOGY_SUPPLIED | SDC_ALLOW_PATH_ORDER_CHANGES | SDC_VIRTUAL_MODE_AWARE; LONG result = m_wapi->setDisplayConfig(paths, {}, flags); if (result == ERROR_GEN_FAILURE) { flags = SDC_VALIDATE | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_VIRTUAL_MODE_AWARE; result = m_wapi->setDisplayConfig(paths, {}, flags); } if (result != ERROR_SUCCESS) { BOOST_LOG(warning) << "Topology validation failed: " << result; return false; } return true; } catch (...) { return false; } } bool soft_test_display_settings( const display_device::SingleDisplayConfiguration &cfg, const std::optional &base_topology ) const { if (!ensure_initialized()) { return false; } try { auto topo_before = base_topology.value_or(m_dd->getCurrentTopology()); if (!m_dd->isTopologyValid(topo_before)) { return false; } const auto devices = enumerate_devices(display_device::DeviceEnumerationDetail::Minimal); auto initial = display_device::win_utils::computeInitialState(std::nullopt, topo_before, devices); if (!initial) { return false; } const auto [new_topology, device_to_configure, additional_devices] = display_device::win_utils::computeNewTopologyAndMetadata( cfg.m_device_prep, cfg.m_device_id, *initial ); if (m_dd->isTopologyTheSame(topo_before, new_topology)) { return true; } return validate_topology_with_os(new_topology); } catch (...) { return false; } } bool soft_test_display_settings(const display_device::SingleDisplayConfiguration &cfg) const { return soft_test_display_settings(cfg, std::nullopt); } std::optional compute_expected_topology( const display_device::SingleDisplayConfiguration &cfg, const std::optional &base_topology ) const { if (!ensure_initialized()) { return std::nullopt; } try { auto topo_before = base_topology.value_or(m_dd->getCurrentTopology()); if (!m_dd->isTopologyValid(topo_before)) { return std::nullopt; } const auto devices = enumerate_devices(display_device::DeviceEnumerationDetail::Minimal); auto initial = display_device::win_utils::computeInitialState(std::nullopt, topo_before, devices); if (!initial) { return std::nullopt; } const auto [new_topology, device_to_configure, additional_devices] = display_device::win_utils::computeNewTopologyAndMetadata( cfg.m_device_prep, cfg.m_device_id, *initial ); return new_topology; } catch (...) { return std::nullopt; } } std::optional compute_expected_topology(const display_device::SingleDisplayConfiguration &cfg) const { return compute_expected_topology(cfg, std::nullopt); } bool is_topology_the_same(const display_device::ActiveTopology &a, const display_device::ActiveTopology &b) const { if (!ensure_initialized()) { return false; } try { return m_dd->isTopologyTheSame(a, b); } catch (...) { return false; } } // Apply the HDR blank workaround synchronously (call from a background thread) void blank_hdr_states(std::chrono::milliseconds delay) { if (!ensure_initialized()) { return; } try { display_device::win_utils::blankHdrStates(*m_dd, delay); } catch (...) { // ignore errors; best effort } } // Compute a simple signature string from snapshot for change detection/logging. std::string signature(const display_device::DisplaySettingsSnapshot &snap) const { // Build a stable textual representation std::string s; s.reserve(1024); // Topology (canonical order: group enumeration order is OS-dependent and meaningless) s += "T:"; auto topology = snap.m_topology; for (auto &grp : topology) { std::sort(grp.begin(), grp.end()); } std::sort(topology.begin(), topology.end()); for (const auto &grp : topology) { s += "["; for (const auto &id : grp) { s += id; s += ","; } s += "]"; } // Modes s += ";M:"; for (const auto &kv : snap.m_modes) { s += kv.first; s += "="; s += std::to_string(kv.second.m_resolution.m_width); s += "x"; s += std::to_string(kv.second.m_resolution.m_height); s += "@"; s += std::to_string(kv.second.m_refresh_rate.m_numerator); s += "/"; s += std::to_string(kv.second.m_refresh_rate.m_denominator); s += ";"; } // HDR s += ";H:"; for (const auto &kh : snap.m_hdr_states) { s += kh.first; s += "="; // Avoid ambiguous null; use explicit string for readability if (!kh.second.has_value()) { s += "unknown"; } else { s += (*kh.second == display_device::HdrState::Enabled) ? "on" : "off"; } s += ";"; } // Primary s += ";P:"; s += snap.m_primary_device; // Origins s += ";O:"; for (const auto &ko : snap.m_origins) { s += ko.first; s += "="; s += std::to_string(ko.second.m_x); s += ","; s += std::to_string(ko.second.m_y); s += ";"; } return s; } // Convenience: current topology signature for change detection watchers. std::string current_topology_signature() const { return signature(snapshot()); } bool write_snapshot_text_atomically(const std::string &out, const std::filesystem::path &path) const { std::error_code ec; std::filesystem::create_directories(path.parent_path(), ec); auto temp_path = path; temp_path += L".tmp"; { FILE *f = _wfopen(temp_path.wstring().c_str(), L"wb"); if (!f) { return false; } auto guard = std::unique_ptr(f, fclose); const auto written = fwrite(out.data(), 1, out.size(), f); if (written != out.size()) { guard.reset(); std::error_code ec_rm_tmp; std::filesystem::remove(temp_path, ec_rm_tmp); return false; } } std::error_code ec_exist; const bool target_exists = std::filesystem::exists(path, ec_exist) && !ec_exist; if (!target_exists) { std::error_code ec_move; std::filesystem::rename(temp_path, path, ec_move); if (!ec_move) { return true; } } std::error_code ec_copy; std::filesystem::copy_file(temp_path, path, std::filesystem::copy_options::overwrite_existing, ec_copy); if (ec_copy) { return false; } std::error_code ec_rm_tmp; std::filesystem::remove(temp_path, ec_rm_tmp); return true; } // Save snapshot to file as JSON-like format. bool save_display_settings_snapshot_to_file(const std::filesystem::path &path) const { auto snap = snapshot(); const auto snapshot_exclusions = snapshot_exclusions_copy(); auto is_excluded = [&](const std::string &device_id) { if (snapshot_exclusions.empty()) { return false; } const auto norm = normalize_device_id(device_id); return std::find(snapshot_exclusions.begin(), snapshot_exclusions.end(), norm) != snapshot_exclusions.end(); }; if (!is_topology_valid(snap)) { BOOST_LOG(warning) << "Skipping display snapshot save; topology is invalid or empty for path=" << path.string(); return false; } if (snap.m_modes.empty()) { BOOST_LOG(warning) << "Skipping display snapshot save; mode set is empty for path=" << path.string(); return false; } // Filter out devices without display_name. These are not safe restore // targets and are intentionally excluded from persisted snapshots. { auto devices = enumerate_devices(display_device::DeviceEnumerationDetail::Minimal); std::set valid_device_ids; std::vector enumerated_devices; std::vector virtual_devices; for (const auto &d : devices) { const auto id = d.m_device_id.empty() ? d.m_display_name : d.m_device_id; if (!id.empty()) { std::string detail = id; detail += "(display_name="; detail += d.m_display_name.empty() ? "" : d.m_display_name; detail += ")"; enumerated_devices.push_back(std::move(detail)); } if (is_virtual_display_device(d)) { if (is_active_display_device(d) && !id.empty()) { virtual_devices.push_back(id); } continue; } if (!d.m_display_name.empty()) { if (!id.empty()) { valid_device_ids.insert(id); } } } if (!virtual_devices.empty()) { std::string joined; for (size_t i = 0; i < virtual_devices.size(); ++i) { if (i > 0) { joined += ", "; } joined += virtual_devices[i]; } BOOST_LOG(warning) << "Skipping display snapshot save; active virtual display device(s) are present: [" << joined << "]"; return false; } if (!snapshot_exclusions.empty()) { std::set filtered_ids; std::vector excluded_now; for (const auto &id : valid_device_ids) { if (is_excluded(id)) { excluded_now.push_back(id); continue; } filtered_ids.insert(id); } if (!excluded_now.empty()) { std::string joined; for (size_t i = 0; i < excluded_now.size(); ++i) { if (i > 0) { joined += ", "; } joined += excluded_now[i]; } BOOST_LOG(info) << "Display snapshot: excluding devices from snapshot: [" << joined << "]"; } valid_device_ids.swap(filtered_ids); if (valid_device_ids.empty()) { BOOST_LOG(warning) << "Skipping display snapshot save; all devices are excluded for path=" << path.string(); return false; } } // Filter topology groups to devices with a restore-capable display_name. display_device::ActiveTopology filtered_topology; for (const auto &grp : snap.m_topology) { std::vector filtered_grp; for (const auto &device_id : grp) { if (valid_device_ids.count(device_id)) { filtered_grp.push_back(device_id); } } if (!filtered_grp.empty()) { filtered_topology.push_back(std::move(filtered_grp)); } } if (filtered_topology.empty()) { BOOST_LOG(warning) << "Skipping display snapshot save; no devices with valid display_name for path=" << path.string(); if (!enumerated_devices.empty()) { std::string joined; for (size_t i = 0; i < enumerated_devices.size(); ++i) { if (i > 0) { joined += ", "; } joined += enumerated_devices[i]; } BOOST_LOG(debug) << "Display snapshot save rejected details: enumerated_devices=[" << joined << "]"; } return false; } // Update snapshot with filtered data snap.m_topology = std::move(filtered_topology); // Filter modes and hdr_states to only include valid devices for (auto it = snap.m_modes.begin(); it != snap.m_modes.end();) { if (!valid_device_ids.count(it->first)) { it = snap.m_modes.erase(it); } else { ++it; } } for (auto it = snap.m_hdr_states.begin(); it != snap.m_hdr_states.end();) { if (!valid_device_ids.count(it->first)) { it = snap.m_hdr_states.erase(it); } else { ++it; } } for (auto it = snap.m_origins.begin(); it != snap.m_origins.end();) { if (!valid_device_ids.count(it->first)) { it = snap.m_origins.erase(it); } else { ++it; } } // Clear primary if it was filtered out if (!valid_device_ids.count(snap.m_primary_device)) { snap.m_primary_device.clear(); } } const auto layout_ids_vec = flatten_topology_device_ids(snap.m_topology); const std::set layout_ids(layout_ids_vec.begin(), layout_ids_vec.end()); const auto layout_rotations = snapshot_layout_rotations(layout_ids); std::string out; out += "{\n \"snapshot_version\": " + std::to_string(snapshot_layout_version_latest) + ",\n \"topology\": ["; for (size_t i = 0; i < snap.m_topology.size(); ++i) { const auto &grp = snap.m_topology[i]; out += "["; for (size_t j = 0; j < grp.size(); ++j) { out += "\"" + grp[j] + "\""; if (j + 1 < grp.size()) { out += ","; } } out += "]"; if (i + 1 < snap.m_topology.size()) { out += ","; } } out += "],\n \"modes\": {"; size_t k = 0; for (const auto &kv : snap.m_modes) { out += "\n \"" + kv.first + "\": { \"w\": " + std::to_string(kv.second.m_resolution.m_width) + ", \"h\": " + std::to_string(kv.second.m_resolution.m_height) + ", \"num\": " + std::to_string(kv.second.m_refresh_rate.m_numerator) + ", \"den\": " + std::to_string(kv.second.m_refresh_rate.m_denominator) + " }"; if (++k < snap.m_modes.size()) { out += ","; } } out += "\n },\n \"hdr\": {"; k = 0; for (const auto &kh : snap.m_hdr_states) { out += "\n \"" + kh.first + "\": "; if (!kh.second.has_value()) { out += "null"; } else { out += (*kh.second == display_device::HdrState::Enabled) ? "\"on\"" : "\"off\""; } if (++k < snap.m_hdr_states.size()) { out += ","; } } out += "\n },\n \"primary\": \"" + snap.m_primary_device + "\",\n \"origins\": {"; k = 0; for (const auto &ko : snap.m_origins) { out += "\n \"" + ko.first + "\": { \"x\": " + std::to_string(ko.second.m_x) + ", \"y\": " + std::to_string(ko.second.m_y) + " }"; if (++k < snap.m_origins.size()) { out += ","; } } out += "\n },\n \"layouts\": {"; k = 0; for (const auto &layout : layout_rotations) { out += "\n \"" + layout.first + "\": { \"rotation\": " + std::to_string(layout.second) + " }"; if (++k < layout_rotations.size()) { out += ","; } } out += "\n }\n}"; return write_snapshot_text_atomically(out, path); } // Save a provided snapshot to file (without validation/filtering). bool save_snapshot_to_file(const display_device::DisplaySettingsSnapshot &snap, const std::filesystem::path &path) const { const auto layout_ids_vec = flatten_topology_device_ids(snap.m_topology); const std::set layout_ids(layout_ids_vec.begin(), layout_ids_vec.end()); const auto layout_rotations = snapshot_layout_rotations(layout_ids); std::string out; out += "{\n \"snapshot_version\": " + std::to_string(snapshot_layout_version_latest) + ",\n \"topology\": ["; for (size_t i = 0; i < snap.m_topology.size(); ++i) { const auto &grp = snap.m_topology[i]; out += "["; for (size_t j = 0; j < grp.size(); ++j) { out += "\"" + grp[j] + "\""; if (j + 1 < grp.size()) { out += ","; } } out += "]"; if (i + 1 < snap.m_topology.size()) { out += ","; } } out += "],\n \"modes\": {"; size_t k = 0; for (const auto &kv : snap.m_modes) { out += "\n \"" + kv.first + "\": { \"w\": " + std::to_string(kv.second.m_resolution.m_width) + ", \"h\": " + std::to_string(kv.second.m_resolution.m_height) + ", \"num\": " + std::to_string(kv.second.m_refresh_rate.m_numerator) + ", \"den\": " + std::to_string(kv.second.m_refresh_rate.m_denominator) + " }"; if (++k < snap.m_modes.size()) { out += ","; } } out += "\n },\n \"hdr\": {"; k = 0; for (const auto &kh : snap.m_hdr_states) { out += "\n \"" + kh.first + "\": "; if (!kh.second.has_value()) { out += "null"; } else { out += (*kh.second == display_device::HdrState::Enabled) ? "\"on\"" : "\"off\""; } if (++k < snap.m_hdr_states.size()) { out += ","; } } out += "\n },\n \"primary\": \"" + snap.m_primary_device + "\",\n \"origins\": {"; k = 0; for (const auto &ko : snap.m_origins) { out += "\n \"" + ko.first + "\": { \"x\": " + std::to_string(ko.second.m_x) + ", \"y\": " + std::to_string(ko.second.m_y) + " }"; if (++k < snap.m_origins.size()) { out += ","; } } out += "\n },\n \"layouts\": {"; k = 0; for (const auto &layout : layout_rotations) { out += "\n \"" + layout.first + "\": { \"rotation\": " + std::to_string(layout.second) + " }"; if (++k < layout_rotations.size()) { out += ","; } } out += "\n }\n}"; return write_snapshot_text_atomically(out, path); } // Load snapshot from file with compatibility metadata. std::optional load_display_settings_snapshot_with_metadata(const std::filesystem::path &path) const { std::error_code ec; if (!std::filesystem::exists(path, ec)) { return std::nullopt; } FILE *f = _wfopen(path.wstring().c_str(), L"rb"); if (!f) { return std::nullopt; } auto guard = std::unique_ptr(f, fclose); std::string data; char buf[4096]; while (size_t n = fread(buf, 1, sizeof(buf), f)) { data.append(buf, n); } display_device::DisplaySettingsSnapshot snap; int snapshot_version = 1; layout_rotation_map_t layout_rotations; bool has_layout_data = false; try { auto j = nlohmann::json::parse(data, nullptr, false); if (j.is_object()) { if (j.contains("snapshot_version") && j["snapshot_version"].is_number_integer()) { snapshot_version = std::max(1, j["snapshot_version"].get()); } parse_layouts_field(j, layout_rotations, has_layout_data); } } catch (...) { } const auto prim = find_str_section(data, "primary"); const auto topo_s = find_str_section(data, "topology"); const auto modes_s = find_str_section(data, "modes"); const auto hdr_s = find_str_section(data, "hdr"); parse_primary_field(prim, snap); parse_topology_field(topo_s, snap); parse_modes_field(modes_s, snap); parse_hdr_field(hdr_s, snap); const auto origins_s = find_str_section(data, "origins"); parse_origins_field(origins_s, snap); // Filter snapshot using current exclusion list and currently enumerated devices. // Note: `m_display_name` is only populated for active displays in libdisplaydevice, so // using it here would incorrectly treat inactive-but-connected monitors as missing. // For loading/restore, we only require a matching device id (display_name is not required). const auto join = [](const auto &items) { std::string out; bool first = true; for (const auto &item : items) { if (!first) { out += ", "; } first = false; out += item; } return out; }; std::set valid_devices_norm; std::set virtual_devices_norm; std::vector filtered_out_excluded; std::vector enumerated_devices; const auto exclusions = snapshot_exclusions_copy(); std::set exclusions_norm; for (auto id : exclusions) { exclusions_norm.insert(normalize_device_id(std::move(id))); } for (const auto &d : enumerate_devices(display_device::DeviceEnumerationDetail::Minimal)) { auto id = d.m_device_id.empty() ? d.m_display_name : d.m_device_id; if (id.empty()) { continue; } enumerated_devices.push_back(id); auto norm = normalize_device_id(id); if (is_virtual_display_device(d)) { if (is_active_display_device(d)) { virtual_devices_norm.insert(std::move(norm)); } continue; } if (!exclusions_norm.empty() && exclusions_norm.count(norm)) { filtered_out_excluded.push_back(id); continue; } valid_devices_norm.insert(std::move(norm)); } if (!virtual_devices_norm.empty()) { std::vector snapshot_devices; for (const auto &grp : snap.m_topology) { snapshot_devices.insert(snapshot_devices.end(), grp.begin(), grp.end()); } for (const auto &device_id : snapshot_devices) { if (virtual_devices_norm.count(normalize_device_id(device_id))) { BOOST_LOG(warning) << "Snapshot load rejected: snapshot contains active virtual display device " << device_id << " for path=" << path.string(); return std::nullopt; } } } if (valid_devices_norm.empty()) { BOOST_LOG(warning) << "Snapshot load rejected: no valid devices available for path=" << path.string(); BOOST_LOG(debug) << "Snapshot load rejected details: enumerated_devices=[" << join(enumerated_devices) << "], exclusions=[" << join(exclusions_norm) << "]"; return std::nullopt; } auto is_allowed = [&](const std::string &device_id) { const auto norm = normalize_device_id(device_id); if (!valid_devices_norm.count(norm)) { return false; } return exclusions_norm.empty() || !exclusions_norm.count(norm); }; display_device::ActiveTopology filtered_topology; for (const auto &grp : snap.m_topology) { std::vector filtered_grp; for (const auto &device_id : grp) { if (is_allowed(device_id)) { filtered_grp.push_back(device_id); } else if (!exclusions_norm.empty() && exclusions_norm.count(normalize_device_id(device_id))) { filtered_out_excluded.push_back(device_id); } } if (!filtered_grp.empty()) { filtered_topology.push_back(std::move(filtered_grp)); } } if (filtered_topology.empty()) { BOOST_LOG(warning) << "Snapshot load rejected: all devices filtered for path=" << path.string(); std::vector snapshot_devices; for (const auto &grp : snap.m_topology) { snapshot_devices.insert(snapshot_devices.end(), grp.begin(), grp.end()); } std::sort(snapshot_devices.begin(), snapshot_devices.end()); snapshot_devices.erase(std::unique(snapshot_devices.begin(), snapshot_devices.end()), snapshot_devices.end()); BOOST_LOG(debug) << "Snapshot load rejected details: snapshot_devices=[" << join(snapshot_devices) << "], present_devices=[" << join(valid_devices_norm) << "], exclusions=[" << join(exclusions_norm) << "]"; return std::nullopt; } snap.m_topology = std::move(filtered_topology); for (auto it = snap.m_modes.begin(); it != snap.m_modes.end();) { if (!is_allowed(it->first)) { it = snap.m_modes.erase(it); } else { ++it; } } for (auto it = snap.m_hdr_states.begin(); it != snap.m_hdr_states.end();) { if (!is_allowed(it->first)) { it = snap.m_hdr_states.erase(it); } else { ++it; } } for (auto it = snap.m_origins.begin(); it != snap.m_origins.end();) { if (!is_allowed(it->first)) { it = snap.m_origins.erase(it); } else { ++it; } } for (auto it = layout_rotations.begin(); it != layout_rotations.end();) { if (!is_allowed(it->first)) { it = layout_rotations.erase(it); } else { ++it; } } if (layout_rotations.empty()) { has_layout_data = false; } if (!snap.m_primary_device.empty() && !is_allowed(snap.m_primary_device)) { snap.m_primary_device.clear(); } if (!filtered_out_excluded.empty()) { std::sort(filtered_out_excluded.begin(), filtered_out_excluded.end()); filtered_out_excluded.erase(std::unique(filtered_out_excluded.begin(), filtered_out_excluded.end()), filtered_out_excluded.end()); std::string joined; for (size_t i = 0; i < filtered_out_excluded.size(); ++i) { if (i > 0) { joined += ", "; } joined += filtered_out_excluded[i]; } BOOST_LOG(info) << "Snapshot load: excluded devices filtered from " << path.string() << ": [" << joined << "]"; } LoadedSnapshot loaded; loaded.snapshot = std::move(snap); loaded.snapshot_version = snapshot_version; loaded.has_layout_data = has_layout_data; loaded.layout_rotations = std::move(layout_rotations); return loaded; } // Load snapshot from file. std::optional load_display_settings_snapshot(const std::filesystem::path &path) const { auto loaded = load_display_settings_snapshot_with_metadata(path); if (!loaded) { return std::nullopt; } return loaded->snapshot; } bool snapshot_file_has_restore_payload(const std::filesystem::path &path) const { std::error_code ec; if (!std::filesystem::exists(path, ec) || ec) { return false; } FILE *f = _wfopen(path.wstring().c_str(), L"rb"); if (!f) { return false; } std::string data; char buf[4096]; size_t n = 0; while ((n = fread(buf, 1, sizeof(buf), f)) > 0) { data.append(buf, n); } fclose(f); try { display_device::DisplaySettingsSnapshot snap; parse_topology_field(find_str_section(data, "topology"), snap); parse_modes_field(find_str_section(data, "modes"), snap); return !snap.m_topology.empty() && !snap.m_modes.empty(); } catch (...) { return false; } } // Apply snapshot best-effort. bool apply_snapshot( const display_device::DisplaySettingsSnapshot &snap, const layout_rotation_map_t *layout_rotations = nullptr ) { if (!ensure_initialized()) { return false; } try { (void) m_dd->setTopology(snap.m_topology); (void) m_dd->setDisplayModes(snap.m_modes); (void) m_dd->setHdrStates(snap.m_hdr_states); if (!snap.m_primary_device.empty()) { (void) m_dd->setAsPrimary(snap.m_primary_device); } for (const auto &[device_id, point] : snap.m_origins) { (void) m_dd->setDisplayOrigin(device_id, point); } if (layout_rotations && !layout_rotations->empty()) { return apply_layout_rotations(*layout_rotations); } return true; } catch (...) { return false; } } void set_snapshot_exclusions(const std::vector &ids) { std::lock_guard lock(snapshot_exclude_mutex_); snapshot_exclude_devices_.clear(); std::set unique; for (auto id : ids) { id = normalize_device_id(std::move(id)); if (!id.empty()) { unique.insert(std::move(id)); } } snapshot_exclude_devices_.assign(unique.begin(), unique.end()); } std::vector snapshot_exclusions_copy_public() const { return snapshot_exclusions_copy(); } private: enum class InitState : uint8_t { Uninitialized, Ready, Failed }; bool ensure_initialized() const { auto state = m_init_state.load(std::memory_order_acquire); if (state == InitState::Ready) { return true; } if (state == InitState::Failed) { return false; } std::call_once(m_init_once, [this]() noexcept { try { auto wapi = std::make_shared(); auto dd = std::make_shared(wapi); auto sm = std::make_unique( dd, std::make_shared(), std::make_unique(std::make_shared()), display_device::WinWorkarounds {} ); m_wapi = std::move(wapi); m_dd = std::move(dd); m_sm = std::move(sm); m_init_state.store(InitState::Ready, std::memory_order_release); } catch (...) { BOOST_LOG(error) << "Display helper: failed to initialize display controller stack."; m_init_state.store(InitState::Failed, std::memory_order_release); } }); return m_init_state.load(std::memory_order_acquire) == InitState::Ready; } mutable std::once_flag m_init_once; mutable std::atomic m_init_state {InitState::Uninitialized}; mutable std::shared_ptr m_wapi; mutable std::shared_ptr m_dd; mutable std::unique_ptr m_sm; mutable std::mutex snapshot_exclude_mutex_; std::vector snapshot_exclude_devices_; static std::string normalize_device_id(std::string id) { id.erase(id.begin(), std::find_if(id.begin(), id.end(), [](unsigned char ch) { return !std::isspace(ch); })); id.erase(std::find_if(id.rbegin(), id.rend(), [](unsigned char ch) { return !std::isspace(ch); }).base(), id.end()); std::transform(id.begin(), id.end(), id.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); return id; } static bool contains_ci(const std::string &haystack, const std::string &needle) { if (needle.empty()) { return true; } if (haystack.size() < needle.size()) { return false; } for (size_t i = 0; i + needle.size() <= haystack.size(); ++i) { bool match = true; for (size_t j = 0; j < needle.size(); ++j) { if (std::tolower(static_cast(haystack[i + j])) != std::tolower(static_cast(needle[j]))) { match = false; break; } } if (match) { return true; } } return false; } static bool equals_ci(const std::string &lhs, const std::string &rhs) { return lhs.size() == rhs.size() && contains_ci(lhs, rhs); } static bool is_virtual_display_device(const display_device::EnumeratedDevice &device) { if (contains_ci(device.m_device_id, "SunshineVirtualDisplay") || contains_ci(device.m_device_id, "Sunshine Virtual Display") || contains_ci(device.m_display_name, "SunshineVirtualDisplay") || contains_ci(device.m_display_name, "Sunshine Virtual Display") || contains_ci(device.m_friendly_name, "SunshineVirtualDisplay") || contains_ci(device.m_friendly_name, "Sunshine Virtual Display")) { return true; } if (equals_ci(device.m_friendly_name, "Sunshine Virtual Display Driver")) { return true; } // SDD = bundled Sunshine virtual display driver, SMK = SudoVDA (legacy driver). return device.m_edid && (equals_ci(device.m_edid->m_manufacturer_id, "SDD") || equals_ci(device.m_edid->m_manufacturer_id, "SMK")); } static bool is_active_display_device(const display_device::EnumeratedDevice &device) { return device.m_info.has_value() || !device.m_display_name.empty(); } std::vector snapshot_exclusions_copy() const { std::lock_guard lock(snapshot_exclude_mutex_); return snapshot_exclude_devices_; } void collect_all_device_ids(std::set &out) const { for (const auto &d : enumerate_devices(display_device::DeviceEnumerationDetail::Minimal)) { const auto id = d.m_device_id.empty() ? d.m_display_name : d.m_device_id; if (!id.empty()) { out.insert(id); } } } static std::optional normalize_rotation_degrees(int degrees) { int normalized = degrees % 360; if (normalized < 0) { normalized += 360; } switch (normalized) { case 0: case 90: case 180: case 270: return normalized; default: return std::nullopt; } } static std::optional dmdo_to_degrees(DWORD orientation) { switch (orientation) { case DMDO_DEFAULT: return 0; case DMDO_90: return 90; case DMDO_180: return 180; case DMDO_270: return 270; default: return std::nullopt; } } static std::optional degrees_to_dmdo(int degrees) { auto normalized = normalize_rotation_degrees(degrees); if (!normalized) { return std::nullopt; } switch (*normalized) { case 0: return DMDO_DEFAULT; case 90: return DMDO_90; case 180: return DMDO_180; case 270: return DMDO_270; default: return std::nullopt; } } std::unordered_map active_display_names_by_device_id(const std::set &device_ids = {}) const { std::unordered_map out; const bool filter = !device_ids.empty(); for (const auto &d : enumerate_devices(display_device::DeviceEnumerationDetail::Minimal)) { const auto id = d.m_device_id.empty() ? d.m_display_name : d.m_device_id; if (id.empty()) { continue; } if (filter && !device_ids.contains(id)) { continue; } std::string display_name = d.m_display_name; if (display_name.empty()) { try { display_name = m_dd->getDisplayName(id); } catch (...) { } } if (display_name.empty()) { continue; } const std::wstring display_name_w(display_name.begin(), display_name.end()); out.emplace(id, display_name_w); const auto id_lower = ascii_lower(id); if (id_lower != id) { out.emplace(id_lower, display_name_w); } } return out; } /** * @brief Dynamically allocate and populate a full DEVMODEW (including dmDriverExtra) * for the given display. This avoids truncating driver-specific data that some * GPU drivers attach beyond the standard DEVMODEW structure. * @return Heap-allocated buffer containing the fully populated DEVMODEW, or nullptr on failure. */ static std::vector alloc_full_devmode(const std::wstring &display_name) { // Step 1: Probe to discover the required dmDriverExtra size. DEVMODEW probe {}; probe.dmSize = sizeof(DEVMODEW); probe.dmDriverExtra = 0; if (!EnumDisplaySettingsExW(display_name.c_str(), ENUM_CURRENT_SETTINGS, &probe, 0)) { return {}; } // Step 2: Allocate a buffer large enough for the base struct + driver payload. const size_t total = static_cast(probe.dmSize) + probe.dmDriverExtra; std::vector buffer(total, 0); auto *mode = reinterpret_cast(buffer.data()); mode->dmSize = probe.dmSize; mode->dmDriverExtra = probe.dmDriverExtra; // Step 3: Re-enumerate to fully populate the buffer. if (!EnumDisplaySettingsExW(display_name.c_str(), ENUM_CURRENT_SETTINGS, mode, 0)) { return {}; } return buffer; } std::optional read_display_rotation_degrees(const std::wstring &display_name) const { if (display_name.empty()) { return std::nullopt; } auto buf = alloc_full_devmode(display_name); if (buf.empty()) { return std::nullopt; } const auto *mode = reinterpret_cast(buf.data()); return dmdo_to_degrees(mode->dmDisplayOrientation); } /** * @brief A prepared rotation change ready for batched application. */ struct PreparedRotation { std::wstring display_name; std::vector devmode_buffer; ///< Heap buffer holding the full DEVMODEW + dmDriverExtra bool already_correct = false; ///< True if the display is already at the target rotation }; /** * @brief Prepare (but do NOT apply) a rotation change for a single display. * The returned PreparedRotation can be batched with CDS_NORESET. */ std::optional prepare_display_rotation(const std::wstring &display_name, int degrees) const { if (display_name.empty()) { return std::nullopt; } auto target = degrees_to_dmdo(degrees); if (!target) { return std::nullopt; } auto buf = alloc_full_devmode(display_name); if (buf.empty()) { return std::nullopt; } auto *mode = reinterpret_cast(buf.data()); if (mode->dmDisplayOrientation == *target) { return PreparedRotation {display_name, {}, true}; } const bool swap_axes = ((mode->dmDisplayOrientation + *target) % 2) == 1; mode->dmFields = DM_DISPLAYORIENTATION | DM_POSITION; mode->dmDisplayOrientation = *target; if (swap_axes) { std::swap(mode->dmPelsWidth, mode->dmPelsHeight); mode->dmFields |= DM_PELSWIDTH | DM_PELSHEIGHT; } return PreparedRotation {display_name, std::move(buf), false}; } /** * @brief Apply a single rotation immediately (non-batched). Used when only one display * needs rotation, so batching overhead is unnecessary. */ bool apply_display_rotation_degrees(const std::wstring &display_name, int degrees) const { auto prepared = prepare_display_rotation(display_name, degrees); if (!prepared) { return false; } if (prepared->already_correct) { return true; } auto *request = reinterpret_cast(prepared->devmode_buffer.data()); LONG test_result = ChangeDisplaySettingsExW(display_name.c_str(), request, nullptr, CDS_TEST, nullptr); if (test_result != DISP_CHANGE_SUCCESSFUL) { BOOST_LOG(warning) << "Layout restore: CDS_TEST failed for display " << std::string(display_name.begin(), display_name.end()) << " (error=" << test_result << ")"; return false; } LONG apply_result = ChangeDisplaySettingsExW(display_name.c_str(), request, nullptr, CDS_UPDATEREGISTRY, nullptr); if (apply_result == DISP_CHANGE_SUCCESSFUL) { return true; } apply_result = ChangeDisplaySettingsExW(display_name.c_str(), request, nullptr, 0, nullptr); if (apply_result == DISP_CHANGE_SUCCESSFUL) { return true; } BOOST_LOG(warning) << "Layout restore: ChangeDisplaySettingsEx failed for display " << std::string(display_name.begin(), display_name.end()) << " (error=" << apply_result << ")"; return false; } public: display_device::EnumeratedDeviceList enumerate_devices(display_device::DeviceEnumerationDetail detail) const { if (!ensure_initialized()) { return {}; } try { return m_dd->enumAvailableDevices(detail); } catch (...) { return {}; } } private: std::optional find_primary_in_set(const std::set &ids) const { if (!ensure_initialized()) { return std::nullopt; } for (const auto &id : ids) { if (m_dd->isPrimary(id)) { return id; } } return std::nullopt; } // Parsing helpers to keep cyclomatic complexity low. static std::string find_str_section(const std::string &data, const std::string &key) { auto p = data.find("\"" + key + "\""); if (p == std::string::npos) { return {}; } p = data.find(':', p); if (p == std::string::npos) { return {}; } return data.substr(p + 1); } static std::optional floating_to_double(const display_device::FloatingPoint &value) { if (std::holds_alternative(value)) { return std::get(value); } const auto &rat = std::get(value); if (rat.m_denominator == 0) { return std::nullopt; } return static_cast(rat.m_numerator) / static_cast(rat.m_denominator); } static bool nearly_equal(double lhs, double rhs) { const double diff = std::abs(lhs - rhs); const double scale = std::max({1.0, std::abs(lhs), std::abs(rhs)}); return diff <= scale * 1e-4; } std::optional get_device_info_minimal(const std::string &device_id) const { if (!ensure_initialized()) { return std::nullopt; } try { auto devices = m_dd->enumAvailableDevices(display_device::DeviceEnumerationDetail::Minimal); for (const auto &device : devices) { if (device.m_device_id == device_id && device.m_info) { return device.m_info; } } } catch (...) { } return std::nullopt; } bool info_matches_config( const display_device::EnumeratedDevice::Info &info, const display_device::SingleDisplayConfiguration &cfg ) const { if (cfg.m_resolution) { if (info.m_resolution.m_width != cfg.m_resolution->m_width || info.m_resolution.m_height != cfg.m_resolution->m_height) { return false; } } if (cfg.m_refresh_rate) { auto desired = floating_to_double(*cfg.m_refresh_rate); auto actual = floating_to_double(info.m_refresh_rate); if (!desired || !actual || !nearly_equal(*desired, *actual)) { return false; } } if (cfg.m_hdr_state) { if (!info.m_hdr_state || *info.m_hdr_state != *cfg.m_hdr_state) { return false; } } return true; } static void parse_primary_field(const std::string &prim, display_device::DisplaySettingsSnapshot &snap) { auto q1 = prim.find('"'); auto q2 = prim.find('"', q1 == std::string::npos ? 0 : q1 + 1); if (q1 != std::string::npos && q2 != std::string::npos && q2 > q1) { snap.m_primary_device = prim.substr(q1 + 1, q2 - q1 - 1); } } static void parse_topology_field(const std::string &topo_s, display_device::DisplaySettingsSnapshot &snap) { snap.m_topology.clear(); size_t i = topo_s.find('['); if (i == std::string::npos) { return; } ++i; // skip [ while (i < topo_s.size() && topo_s[i] != ']') { while (i < topo_s.size() && topo_s[i] != '[' && topo_s[i] != ']') { ++i; } if (i >= topo_s.size() || topo_s[i] == ']') { break; } ++i; // skip [ std::vector grp; while (i < topo_s.size() && topo_s[i] != ']') { while (i < topo_s.size() && topo_s[i] != '"' && topo_s[i] != ']') { ++i; } if (i >= topo_s.size() || topo_s[i] == ']') { break; } auto q1 = i + 1; auto q2 = topo_s.find('"', q1); if (q2 == std::string::npos) { break; } grp.emplace_back(topo_s.substr(q1, q2 - q1)); i = q2 + 1; } while (i < topo_s.size() && topo_s[i] != ']') { ++i; } if (i < topo_s.size() && topo_s[i] == ']') { ++i; // skip ] } snap.m_topology.emplace_back(std::move(grp)); } } static unsigned int parse_num_field(const std::string &obj, const char *key) { auto p = obj.find(key); if (p == std::string::npos) { return 0; } p = obj.find(':', p); if (p == std::string::npos) { return 0; } return static_cast(std::stoul(obj.substr(p + 1))); } static void parse_modes_field(const std::string &modes_s, display_device::DisplaySettingsSnapshot &snap) { snap.m_modes.clear(); size_t i = modes_s.find('{'); if (i == std::string::npos) { return; } ++i; while (i < modes_s.size() && modes_s[i] != '}') { while (i < modes_s.size() && modes_s[i] != '"' && modes_s[i] != '}') { ++i; } if (i >= modes_s.size() || modes_s[i] == '}') { break; } auto q1 = i + 1; auto q2 = modes_s.find('"', q1); if (q2 == std::string::npos) { break; } std::string id = modes_s.substr(q1, q2 - q1); i = modes_s.find('{', q2); if (i == std::string::npos) { break; } auto end = modes_s.find('}', i); if (end == std::string::npos) { break; } auto obj = modes_s.substr(i, end - i); display_device::DisplayMode dm; dm.m_resolution.m_width = parse_num_field(obj, "\"w\""); dm.m_resolution.m_height = parse_num_field(obj, "\"h\""); dm.m_refresh_rate.m_numerator = parse_num_field(obj, "\"num\""); dm.m_refresh_rate.m_denominator = parse_num_field(obj, "\"den\""); snap.m_modes.emplace(id, dm); i = end + 1; } } static void parse_hdr_field(const std::string &hdr_s, display_device::DisplaySettingsSnapshot &snap) { snap.m_hdr_states.clear(); size_t i = hdr_s.find('{'); if (i == std::string::npos) { return; } ++i; while (i < hdr_s.size() && hdr_s[i] != '}') { while (i < hdr_s.size() && hdr_s[i] != '"' && hdr_s[i] != '}') { ++i; } if (i >= hdr_s.size() || hdr_s[i] == '}') { break; } auto q1 = i + 1; auto q2 = hdr_s.find('"', q1); if (q2 == std::string::npos) { break; } std::string id = hdr_s.substr(q1, q2 - q1); i = hdr_s.find(':', q2); if (i == std::string::npos) { break; } ++i; while (i < hdr_s.size() && (hdr_s[i] == ' ' || hdr_s[i] == '"')) { ++i; } std::optional val; if (hdr_s.compare(i, 2, "on") == 0) { val = display_device::HdrState::Enabled; } else if (hdr_s.compare(i, 3, "off") == 0) { val = display_device::HdrState::Disabled; } else { val = std::nullopt; } snap.m_hdr_states.emplace(id, val); while (i < hdr_s.size() && hdr_s[i] != ',' && hdr_s[i] != '}') { ++i; } if (i < hdr_s.size() && hdr_s[i] == ',') { ++i; } } } static std::optional parse_layout_rotation_value(const nlohmann::json &value) { if (value.is_number_integer()) { return normalize_rotation_degrees(value.get()); } if (value.is_string()) { const auto rotation_name = ascii_lower(value.get()); if (rotation_name == "landscape") { return 0; } if (rotation_name == "portrait") { return 90; } if (rotation_name == "landscape_flipped" || rotation_name == "landscape_inverted") { return 180; } if (rotation_name == "portrait_flipped" || rotation_name == "portrait_inverted") { return 270; } } if (value.is_object()) { if (auto it = value.find("rotation"); it != value.end()) { return parse_layout_rotation_value(*it); } } return std::nullopt; } static void parse_layouts_field( const nlohmann::json &root, layout_rotation_map_t &layout_rotations, bool &has_layout_data ) { layout_rotations.clear(); has_layout_data = false; auto it_layouts = root.find("layouts"); if (it_layouts == root.end() || !it_layouts->is_object()) { return; } has_layout_data = true; for (auto it = it_layouts->begin(); it != it_layouts->end(); ++it) { if (!it.key().empty()) { if (auto rotation = parse_layout_rotation_value(it.value())) { layout_rotations[it.key()] = *rotation; } } } } static int parse_signed_num_field(const std::string &obj, const char *key) { auto p = obj.find(key); if (p == std::string::npos) { return 0; } p = obj.find(':', p); if (p == std::string::npos) { return 0; } return std::stoi(obj.substr(p + 1)); } static void parse_origins_field(const std::string &origins_s, display_device::DisplaySettingsSnapshot &snap) { snap.m_origins.clear(); size_t i = origins_s.find('{'); if (i == std::string::npos) { return; } ++i; while (i < origins_s.size() && origins_s[i] != '}') { while (i < origins_s.size() && origins_s[i] != '"' && origins_s[i] != '}') { ++i; } if (i >= origins_s.size() || origins_s[i] == '}') { break; } auto q1 = i + 1; auto q2 = origins_s.find('"', q1); if (q2 == std::string::npos) { break; } std::string id = origins_s.substr(q1, q2 - q1); i = origins_s.find('{', q2); if (i == std::string::npos) { break; } auto end = origins_s.find('}', i); if (end == std::string::npos) { break; } auto obj = origins_s.substr(i, end - i); display_device::Point pt; pt.m_x = parse_signed_num_field(obj, "\"x\""); pt.m_y = parse_signed_num_field(obj, "\"y\""); snap.m_origins.emplace(id, pt); i = end + 1; } } }; constexpr std::chrono::milliseconds kApplyDisconnectGrace {5000}; class DisplayDeviceLogBridge { public: DisplayDeviceLogBridge() = default; void install() { display_device::Logger::get().setCustomCallback( [this](display_device::Logger::LogLevel level, std::string message) { handle_log(level, std::move(message)); } ); } private: void handle_log(display_device::Logger::LogLevel level, std::string message) { const auto now = std::chrono::steady_clock::now(); const std::string key = std::to_string(static_cast(level)) + "|" + message; { std::lock_guard lk(mutex_); auto it = last_emit_.find(key); if (it != last_emit_.end()) { if ((now - it->second) < throttle_window_) { return; } it->second = now; } else { if (last_emit_.size() >= max_entries_) { prune(now); } last_emit_.emplace(key, now); } } forward(level, message); } void prune(std::chrono::steady_clock::time_point now) { for (auto it = last_emit_.begin(); it != last_emit_.end();) { if ((now - it->second) > prune_window_) { it = last_emit_.erase(it); } else { ++it; } } if (last_emit_.size() >= max_entries_) { last_emit_.clear(); } } void forward(display_device::Logger::LogLevel level, const std::string &message) { const auto prefixed = std::string("display_device: ") + message; switch (level) { case display_device::Logger::LogLevel::verbose: case display_device::Logger::LogLevel::debug: BOOST_LOG(debug) << prefixed; break; case display_device::Logger::LogLevel::info: BOOST_LOG(info) << prefixed; break; case display_device::Logger::LogLevel::warning: BOOST_LOG(warning) << prefixed; break; case display_device::Logger::LogLevel::error: BOOST_LOG(error) << prefixed; break; case display_device::Logger::LogLevel::fatal: BOOST_LOG(fatal) << prefixed; break; } } std::mutex mutex_; std::unordered_map last_emit_; static constexpr std::chrono::seconds throttle_window_ {15}; static constexpr std::chrono::seconds prune_window_ {60}; static constexpr size_t max_entries_ {256}; }; DisplayDeviceLogBridge &dd_log_bridge() { static DisplayDeviceLogBridge bridge; return bridge; } class DisplayEventPump { public: using Callback = std::function; void start(Callback cb) { stop(); callback_ = std::move(cb); worker_ = std::jthread(&DisplayEventPump::thread_proc, this); } void stop() { if (worker_.joinable()) { if (HWND hwnd = hwnd_.load(std::memory_order_acquire)) { PostMessageW(hwnd, WM_CLOSE, 0, 0); } worker_.request_stop(); worker_.join(); } callback_ = nullptr; hwnd_.store(nullptr, std::memory_order_release); } ~DisplayEventPump() { stop(); } private: static LRESULT CALLBACK wnd_proc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { if (msg == WM_NCCREATE) { auto *create = reinterpret_cast(lParam); auto *self = static_cast(create->lpCreateParams); SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast(self)); self->hwnd_.store(hwnd, std::memory_order_release); return TRUE; } auto *self = reinterpret_cast(GetWindowLongPtrW(hwnd, GWLP_USERDATA)); if (!self) { return DefWindowProcW(hwnd, msg, wParam, lParam); } switch (msg) { case WM_DISPLAYCHANGE: self->signal("wm_displaychange"); break; case WM_DEVICECHANGE: if (wParam == DBT_DEVNODES_CHANGED || wParam == DBT_DEVICEARRIVAL || wParam == DBT_DEVICEREMOVECOMPLETE) { self->signal("wm_devicechange"); } break; case WM_POWERBROADCAST: if (wParam == PBT_POWERSETTINGCHANGE) { const auto *ps = reinterpret_cast(lParam); if (ps && ps->PowerSetting == GUID_MONITOR_POWER_ON) { if (ps->DataLength == sizeof(DWORD)) { const DWORD state = *reinterpret_cast(ps->Data); if (state != 0) { self->signal("power_monitor_on"); } } } } break; case WM_DESTROY: self->cleanup_notifications(); PostQuitMessage(0); break; default: break; } return DefWindowProcW(hwnd, msg, wParam, lParam); } void signal(const char *reason) { auto cb = callback_; if (cb) { try { cb(reason); } catch (...) {} } } void cleanup_notifications() { if (power_cookie_) { UnregisterPowerSettingNotification(power_cookie_); power_cookie_ = nullptr; } if (device_cookie_) { UnregisterDeviceNotification(device_cookie_); device_cookie_ = nullptr; } } void thread_proc(std::stop_token st) { const auto hinst = GetModuleHandleW(nullptr); const wchar_t *klass = L"SunshineDisplayEventWindow"; WNDCLASSEXW wc = {}; wc.cbSize = sizeof(wc); wc.lpfnWndProc = &DisplayEventPump::wnd_proc; wc.hInstance = hinst; wc.lpszClassName = klass; RegisterClassExW(&wc); HWND hwnd = CreateWindowExW(0, klass, L"", 0, 0, 0, 0, 0, HWND_MESSAGE, nullptr, hinst, this); if (!hwnd) { return; } power_cookie_ = RegisterPowerSettingNotification(hwnd, &GUID_MONITOR_POWER_ON, DEVICE_NOTIFY_WINDOW_HANDLE); DEV_BROADCAST_DEVICEINTERFACE_W dbi = {}; dbi.dbcc_size = sizeof(dbi); dbi.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE; dbi.dbcc_classguid = kMonitorInterfaceGuid; device_cookie_ = RegisterDeviceNotificationW(hwnd, &dbi, DEVICE_NOTIFY_WINDOW_HANDLE); MSG msg; while (!st.stop_requested()) { const BOOL res = GetMessageW(&msg, nullptr, 0, 0); if (res == -1) { break; } if (res == 0) { break; } TranslateMessage(&msg); DispatchMessageW(&msg); } cleanup_notifications(); if (hwnd) { DestroyWindow(hwnd); } hwnd_.store(nullptr, std::memory_order_release); UnregisterClassW(klass, hinst); } std::jthread worker_; Callback callback_; std::atomic hwnd_ {nullptr}; HPOWERNOTIFY power_cookie_ {nullptr}; HDEVNOTIFY device_cookie_ {nullptr}; }; bool create_restore_scheduled_task(); bool delete_restore_scheduled_task(); struct ServiceState { enum class RestoreWindow { Primary, Event }; DisplayController controller; DisplayEventPump event_pump; std::atomic event_pump_running {false}; std::mutex restore_event_mutex; std::condition_variable restore_event_cv; bool restore_event_flag {false}; std::atomic restore_active_until_ms {0}; std::atomic last_restore_event_ms {0}; std::atomic restore_stage_running {false}; std::atomic restore_active_window {RestoreWindow::Event}; std::atomic retry_apply_on_topology {false}; std::atomic retry_revert_on_topology {false}; // A refresh-only request transfers refresh-rate ownership to Sunshine's // adaptive virtual-display controller. Keep that rate in the configuration // used by delayed verification so the verifier cannot start a base/high-rate // feedback loop. The high and low DWORDs hold numerator and denominator; // zero means no override is active. std::atomic refresh_rate_override {0}; std::optional last_cfg; std::atomic exit_after_revert {false}; std::atomic *running_flag {nullptr}; std::jthread delayed_reapply_thread; // Best-effort re-apply timer std::mutex delayed_reapply_mutex; std::jthread hdr_blank_thread; // Async HDR workaround thread (one-shot) std::jthread post_apply_thread; // Async post-apply tasks (shell refresh, re-apply, HDR blank) std::filesystem::path golden_path; // file to store golden snapshot std::filesystem::path golden_status_path; // restore health marker for golden snapshot std::filesystem::path session_current_path; // file to store current session baseline snapshot (first apply) std::filesystem::path session_previous_path; // file to persist last known-good baseline across runs std::atomic session_saved {false}; // Track last APPLY to suppress revert-on-topology within a grace window std::atomic last_apply_ms {0}; // If a REVERT was requested directly by Sunshine, bypass grace std::atomic direct_revert_bypass_grace {false}; // Track whether a revert/restore is currently pending std::atomic restore_requested {false}; std::atomic restore_cancel_generation {0}; // True after the restore loop has made at least one restore attempt that has // not yet been confirmed. DISARM/SNAPSHOT_CURRENT from a later stream-start // probe must not cancel or overwrite that restore baseline. std::atomic restore_attempted_unconfirmed {false}; // Guard: if a session restore succeeded recently, suppress Golden for a cooldown std::atomic last_session_restore_success_ms {0}; // Track confirmed session fallbacks for diagnostics while the configured // golden baseline remains pending. std::atomic golden_pending_session_fallbacks {0}; // When true, prefer golden snapshot over session snapshots during restore (reduces stuck virtual screens) std::atomic always_restore_from_golden {false}; // When true, prefer golden over previous only when current is unavailable. std::atomic prefer_golden_if_current_missing {true}; // Set by APPLY. When false, a broken Sunshine IPC connection should not // autonomously restore because the stream is intentionally pause-retained. std::atomic restore_on_disconnect {true}; // Polling-based restore loop state (replaces topology-change-triggered retries) std::jthread restore_poll_thread; std::atomic restore_poll_active {false}; std::atomic next_connection_epoch {1}; std::atomic active_connection_epoch {0}; std::atomic restore_origin_epoch {0}; std::atomic heartbeat_monitor_active {false}; std::atomic heartbeat_optional_until_ms {0}; std::atomic last_heartbeat_ms {0}; std::atomic heartbeat_revert_armed {false}; std::atomic heartbeat_revert_deadline_ms {0}; // A dropped control pipe is not proof that the host exited. The host retires and // re-opens the connection routinely (cached-connection resets around capture // reinit, and any reply that outruns its client-side timeout). Exiting on the // first disconnect left a live stream with no helper for the rest of the session, // so a disconnect with nothing to restore now waits for a reconnect instead. std::atomic reconnect_exit_deadline_ms {0}; static constexpr auto kRestoreWindowPrimary = std::chrono::minutes(2); static constexpr auto kRestoreWindowEvent = std::chrono::seconds(30); static constexpr auto kRestoreEventDebounce = std::chrono::milliseconds(500); static constexpr auto kReconnectExitGrace = std::chrono::seconds(60); static constexpr auto kHeartbeatOptionalWindow = std::chrono::seconds(30); static constexpr auto kHeartbeatMissWindow = std::chrono::seconds(30); static constexpr auto kHeartbeatRecoveryWindow = std::chrono::minutes(2); static constexpr auto kVerificationSettleDelay = std::chrono::milliseconds(250); static constexpr size_t kGoldenOutOfDateFailureThreshold = 3; static constexpr auto kGoldenOutOfDateFailureWindow = std::chrono::hours(72); std::mutex golden_restore_issue_mutex; bool golden_restore_had_issue_this_request {false}; std::string golden_restore_last_issue; std::atomic restore_backoff_index {0}; std::atomic restore_next_allowed_ms {0}; static constexpr std::array kRestoreBackoffProfile { std::chrono::seconds(0), std::chrono::seconds(1), std::chrono::seconds(3), std::chrono::seconds(5), std::chrono::seconds(10), std::chrono::seconds(15), std::chrono::seconds(20), std::chrono::seconds(30) }; // IPC command queue to decouple pipe reads from heavy display operations std::mutex command_queue_mutex; std::condition_variable command_queue_cv; std::deque> command_queue; std::atomic command_worker_stop {false}; std::jthread command_worker; std::atomic command_worker_epoch {0}; std::mutex async_join_mutex; // Guards async joiners used to avoid blocking the command loop std::vector async_join_threads; static long long steady_now_ms() { return std::chrono::duration_cast( std::chrono::steady_clock::now().time_since_epoch() ) .count(); } void arm_reconnect_exit_grace(const char *reason) { const auto deadline = steady_now_ms() + std::chrono::duration_cast(kReconnectExitGrace).count(); reconnect_exit_deadline_ms.store(deadline, std::memory_order_release); BOOST_LOG(info) << "Client disconnected (" << reason << "); staying alive for " << std::chrono::duration_cast(kReconnectExitGrace).count() << "s in case the host reconnects."; } void disarm_reconnect_exit_grace() { reconnect_exit_deadline_ms.store(0, std::memory_order_release); } bool reconnect_exit_grace_expired() const { const auto deadline = reconnect_exit_deadline_ms.load(std::memory_order_acquire); return deadline != 0 && steady_now_ms() >= deadline; } void begin_heartbeat_monitoring() { const auto now = steady_now_ms(); heartbeat_monitor_active.store(true, std::memory_order_release); last_heartbeat_ms.store(now, std::memory_order_release); heartbeat_optional_until_ms.store( now + std::chrono::duration_cast(kHeartbeatOptionalWindow).count(), std::memory_order_release ); heartbeat_revert_armed.store(false, std::memory_order_release); heartbeat_revert_deadline_ms.store(0, std::memory_order_release); } void end_heartbeat_monitoring() { heartbeat_monitor_active.store(false, std::memory_order_release); heartbeat_revert_armed.store(false, std::memory_order_release); heartbeat_optional_until_ms.store(0, std::memory_order_release); heartbeat_revert_deadline_ms.store(0, std::memory_order_release); last_heartbeat_ms.store(0, std::memory_order_release); } void record_heartbeat_ping() { if (!heartbeat_monitor_active.load(std::memory_order_acquire)) { return; } const auto now = steady_now_ms(); last_heartbeat_ms.store(now, std::memory_order_release); if (heartbeat_revert_armed.exchange(false, std::memory_order_acq_rel)) { heartbeat_revert_deadline_ms.store(0, std::memory_order_release); BOOST_LOG(info) << "Heartbeat restored; cancelling pending revert countdown."; } } bool check_heartbeat_timeout() { if (!heartbeat_monitor_active.load(std::memory_order_acquire)) { return false; } const auto now = steady_now_ms(); const auto optional_until = heartbeat_optional_until_ms.load(std::memory_order_acquire); if (optional_until > 0 && now < optional_until) { return false; } const auto last_ping = last_heartbeat_ms.load(std::memory_order_acquire); const auto since_last = now - last_ping; const auto miss_threshold = std::chrono::duration_cast(kHeartbeatMissWindow).count(); if (!heartbeat_revert_armed.load(std::memory_order_acquire)) { if (since_last < miss_threshold) { return false; } const auto recovery_ms = std::chrono::duration_cast(kHeartbeatRecoveryWindow).count(); heartbeat_revert_deadline_ms.store(now + recovery_ms, std::memory_order_release); heartbeat_revert_armed.store(true, std::memory_order_release); BOOST_LOG(warning) << "Heartbeat missing for " << (since_last / 1000.0) << "s; allowing up to " << (recovery_ms / 1000.0) << "s for Sunshine to reconnect before restoring display configuration."; return false; } const auto deadline = heartbeat_revert_deadline_ms.load(std::memory_order_acquire); if (deadline != 0 && now >= deadline) { heartbeat_monitor_active.store(false, std::memory_order_release); heartbeat_revert_armed.store(false, std::memory_order_release); heartbeat_revert_deadline_ms.store(0, std::memory_order_release); return true; } return false; } void reset_restore_backoff() { restore_backoff_index.store(0, std::memory_order_release); restore_next_allowed_ms.store(0, std::memory_order_release); } void reset_pending_golden_session_fallbacks() { golden_pending_session_fallbacks.store(0, std::memory_order_release); } size_t note_pending_golden_session_fallback() { return golden_pending_session_fallbacks.fetch_add(1, std::memory_order_acq_rel) + 1; } bool write_text_atomically(const std::filesystem::path &path, const std::string &text) const { if (path.empty()) { return false; } std::error_code ec; std::filesystem::create_directories(path.parent_path(), ec); auto temp_path = path; temp_path += L".tmp"; { FILE *f = _wfopen(temp_path.wstring().c_str(), L"wb"); if (!f) { return false; } auto guard = std::unique_ptr(f, fclose); const auto written = fwrite(text.data(), 1, text.size(), f); if (written != text.size()) { guard.reset(); std::error_code ec_rm_tmp; std::filesystem::remove(temp_path, ec_rm_tmp); return false; } } std::error_code ec_move; std::filesystem::rename(temp_path, path, ec_move); if (!ec_move) { return true; } std::error_code ec_copy; std::filesystem::copy_file(temp_path, path, std::filesystem::copy_options::overwrite_existing, ec_copy); std::error_code ec_rm_tmp; std::filesystem::remove(temp_path, ec_rm_tmp); return !ec_copy; } void clear_golden_restore_status(const char *reason) { reset_golden_restore_request_tracking(); if (golden_status_path.empty()) { return; } std::error_code ec; const bool removed = std::filesystem::remove(golden_status_path, ec); if (removed && !ec) { BOOST_LOG(info) << "Golden restore health reset" << (reason ? std::string(" (") + reason + ")" : "") << "."; } } void reset_golden_restore_request_tracking() { std::lock_guard lock(golden_restore_issue_mutex); golden_restore_had_issue_this_request = false; golden_restore_last_issue.clear(); } void note_golden_restore_issue(const char *reason) { std::lock_guard lock(golden_restore_issue_mutex); golden_restore_had_issue_this_request = true; golden_restore_last_issue = reason && *reason ? reason : "restore_failed"; } void register_unresolved_golden_restore_request(const char *context) { std::string reason; { std::lock_guard lock(golden_restore_issue_mutex); if (!golden_restore_had_issue_this_request) { return; } reason = golden_restore_last_issue.empty() ? "restore_failed" : golden_restore_last_issue; golden_restore_had_issue_this_request = false; golden_restore_last_issue.clear(); } const auto now_ms = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch() ) .count(); long long first_failure_ms = now_ms; size_t failures = 0; if (!golden_status_path.empty()) { try { std::ifstream file(golden_status_path, std::ios::binary); if (file.is_open()) { auto previous = nlohmann::json::parse(file, nullptr, false); if (!previous.is_discarded() && previous.is_object()) { first_failure_ms = previous.value("first_failure_unix_ms", first_failure_ms); failures = static_cast(previous.value("unresolved_restore_attempts", 0ull)); } } } catch (...) { } } failures += 1; const auto failure_window_ms = std::chrono::duration_cast( kGoldenOutOfDateFailureWindow ) .count(); const auto unresolved_ms = std::max(0, now_ms - first_failure_ms); const bool enough_attempts = failures >= kGoldenOutOfDateFailureThreshold; const bool old_enough = unresolved_ms >= failure_window_ms; const bool should_warn = enough_attempts && old_enough; nlohmann::json status; status["snapshot_out_of_date"] = should_warn; status["reason"] = should_warn ? "restore_failed_for_days" : reason; status["last_failure_reason"] = reason; status["unresolved_restore_attempts"] = failures; status["failure_threshold"] = kGoldenOutOfDateFailureThreshold; status["failure_window_hours"] = std::chrono::duration_cast( kGoldenOutOfDateFailureWindow ) .count(); status["first_failure_unix_ms"] = first_failure_ms; status["latest_failure_unix_ms"] = now_ms; status["updated_at_unix_ms"] = now_ms; if (!write_text_atomically(golden_status_path, status.dump(2) + "\n")) { BOOST_LOG(warning) << "Golden restore remained unresolved" << (context ? std::string(" (") + context + ")" : "") << ", but failed to write restore health marker."; return; } if (should_warn) { BOOST_LOG(warning) << "Golden restore has remained unresolved for " << std::chrono::duration_cast( std::chrono::milliseconds(unresolved_ms) ) .count() << "h across " << failures << " restore request(s); marking saved display snapshot as possibly out of date."; } else { BOOST_LOG(info) << "Golden restore remained unresolved" << (context ? std::string(" (") + context + ")" : "") << " (" << reason << "); observing for " << kGoldenOutOfDateFailureThreshold << " attempts over " << std::chrono::duration_cast(kGoldenOutOfDateFailureWindow).count() << "h before warning."; } } void arm_restore_grace(std::chrono::milliseconds delay, const char *reason) { if (delay <= std::chrono::milliseconds::zero()) { return; } const auto now = steady_now_ms(); const auto target = now + delay.count(); const auto existing = restore_next_allowed_ms.load(std::memory_order_acquire); if (existing != 0 && existing >= target) { return; } restore_next_allowed_ms.store(target, std::memory_order_release); BOOST_LOG(debug) << "Restore grace armed for " << delay.count() << "ms" << (reason ? std::string(" (") + reason + ")" : ""); } void request_restore_cancel() { restore_cancel_generation.fetch_add(1, std::memory_order_acq_rel); signal_restore_event(nullptr); } void register_restore_failure() { size_t idx = restore_backoff_index.load(std::memory_order_acquire); if (idx + 1 < kRestoreBackoffProfile.size()) { ++idx; } const auto delay = kRestoreBackoffProfile[idx]; const auto now = steady_now_ms(); restore_backoff_index.store(idx, std::memory_order_release); restore_next_allowed_ms.store( now + std::chrono::duration_cast(delay).count(), std::memory_order_release ); if (delay.count() > 0) { BOOST_LOG(info) << "Restore polling: scheduling next attempt in " << delay.count() << "s."; } } bool await_restore_backoff(std::stop_token st) { constexpr auto kStep = std::chrono::milliseconds(200); while (!st.stop_requested()) { if (!restore_requested.load(std::memory_order_acquire)) { return false; } const auto allowed = restore_next_allowed_ms.load(std::memory_order_acquire); if (allowed == 0) { return true; } const auto now = steady_now_ms(); if (now >= allowed) { return true; } const auto remaining = allowed - now; const auto sleep_ms = std::clamp(remaining, 1, kStep.count()); std::this_thread::sleep_for(std::chrono::milliseconds(sleep_ms)); } return false; } // Move the current session snapshot to the previous slot (overwrite) so we keep // one level of history for the restore chain. bool promote_current_snapshot_to_previous(const char *reason = nullptr) { std::error_code ec_exist; if (!std::filesystem::exists(session_current_path, ec_exist) || ec_exist) { return false; } std::error_code ec_dir; std::filesystem::create_directories(session_previous_path.parent_path(), ec_dir); (void) ec_dir; std::error_code ec_rm_prev; std::filesystem::remove(session_previous_path, ec_rm_prev); (void) ec_rm_prev; std::error_code ec_move; std::filesystem::rename(session_current_path, session_previous_path, ec_move); bool ok = !ec_move; if (ec_move) { std::error_code ec_copy; std::filesystem::copy_file( session_current_path, session_previous_path, std::filesystem::copy_options::overwrite_existing, ec_copy ); ok = !ec_copy; if (ok) { std::error_code ec_rm_cur; std::filesystem::remove(session_current_path, ec_rm_cur); (void) ec_rm_cur; } } const char *why = reason ? reason : "rotation"; BOOST_LOG(ok ? info : warning) << "Session snapshot promotion (" << why << ") current->previous result=" << (ok ? "true" : "false"); return ok; } static bool path_exists(const std::filesystem::path &path) { std::error_code ec; return std::filesystem::exists(path, ec) && !ec; } static bool copy_file_overwrite(const std::filesystem::path &from, const std::filesystem::path &to) { std::error_code ec_dir; std::filesystem::create_directories(to.parent_path(), ec_dir); std::error_code ec_copy; std::filesystem::copy_file(from, to, std::filesystem::copy_options::overwrite_existing, ec_copy); return !ec_copy; } bool save_snapshot_with_retry( const std::filesystem::path &path, const char *reason = nullptr, int max_attempts = 3, std::chrono::milliseconds retry_delay = 50ms ) { const char *why = reason ? reason : "snapshot"; for (int attempt = 1; attempt <= max_attempts; ++attempt) { if (controller.save_display_settings_snapshot_to_file(path)) { if (attempt > 1) { BOOST_LOG(info) << "Display snapshot save succeeded on retry #" << attempt << " (" << why << ")."; } return true; } if (attempt < max_attempts) { BOOST_LOG(info) << "Display snapshot save retry #" << (attempt + 1) << " scheduled (" << why << ")."; std::this_thread::sleep_for(retry_delay); } } return false; } // Capture the current display state to the "current" snapshot slot. bool capture_current_snapshot(const char *reason = nullptr) { const bool saved = save_snapshot_with_retry(session_current_path, reason); session_saved.store(saved || path_exists(session_current_path), std::memory_order_release); const char *why = reason ? reason : "apply"; BOOST_LOG(info) << "Saved current session snapshot (" << why << "): " << (saved ? "true" : "false"); return saved; } bool refresh_current_snapshot_preserving_previous(const char *reason = nullptr) { auto staged_path = session_current_path; staged_path += L".candidate"; std::error_code ec_rm; std::filesystem::remove(staged_path, ec_rm); const bool staged_saved = save_snapshot_with_retry(staged_path, reason); const char *why = reason ? reason : "snapshot-only"; if (!staged_saved) { session_saved.store(path_exists(session_current_path), std::memory_order_release); BOOST_LOG(info) << "Refreshed current session snapshot (" << why << "): false"; return false; } if (path_exists(session_current_path) && !copy_file_overwrite(session_current_path, session_previous_path)) { BOOST_LOG(warning) << "Failed to refresh session snapshot history (" << why << "): current->previous copy failed."; } const bool replaced = copy_file_overwrite(staged_path, session_current_path); std::error_code ec_rm_stage; std::filesystem::remove(staged_path, ec_rm_stage); session_saved.store(replaced || path_exists(session_current_path), std::memory_order_release); BOOST_LOG(info) << "Refreshed current session snapshot (" << why << "): " << (replaced ? "true" : "false"); return replaced; } void prepare_session_topology() { if (session_saved.load(std::memory_order_acquire)) { return; } std::error_code ec_exist; const bool exists = std::filesystem::exists(session_current_path, ec_exist); if (exists && !ec_exist) { session_saved.store(true, std::memory_order_release); BOOST_LOG(info) << "Session baseline already exists; preserving existing snapshot: " << session_current_path.string(); return; } const bool saved = save_snapshot_with_retry(session_current_path, "session-baseline"); session_saved.store(saved, std::memory_order_release); BOOST_LOG(info) << "Saved session baseline snapshot to file: " << (saved ? "true" : "false"); } void ensure_session_state(const display_device::ActiveTopology &expected_topology) { if (session_saved.load(std::memory_order_acquire)) { return; } std::error_code ec_exist; if (std::filesystem::exists(session_current_path, ec_exist) && !ec_exist) { session_saved.store(true, std::memory_order_release); return; } const auto actual = controller.snapshot().m_topology; const bool matches_expected = controller.is_topology_the_same(actual, expected_topology); std::error_code ec_prev; const bool has_prev = std::filesystem::exists(session_previous_path, ec_prev) && !ec_prev; if (has_prev && matches_expected) { auto prev = controller.load_display_settings_snapshot(session_previous_path); if (prev && !controller.is_topology_the_same(prev->m_topology, expected_topology)) { std::error_code ec_copy; std::filesystem::copy_file(session_previous_path, session_current_path, std::filesystem::copy_options::overwrite_existing, ec_copy); if (!ec_copy) { BOOST_LOG(info) << "Promoted previous session snapshot to current."; session_saved.store(true, std::memory_order_release); return; } BOOST_LOG(warning) << "Failed to promote previous ΓåÆ current (copy error); will snapshot current instead."; } } const bool saved = save_snapshot_with_retry(session_current_path, "session-baseline-fresh"); session_saved.store(saved, std::memory_order_release); BOOST_LOG(info) << "Saved session baseline snapshot (fresh) to file: " << (saved ? "true" : "false"); } // Read a stable snapshot: two identical consecutive reads within the deadline bool read_stable_snapshot( display_device::DisplaySettingsSnapshot &out, std::chrono::milliseconds deadline = 2000ms, std::chrono::milliseconds interval = 150ms, std::stop_token st = {} ) { auto t0 = std::chrono::steady_clock::now(); auto have_last = false; display_device::DisplaySettingsSnapshot last; while (std::chrono::steady_clock::now() - t0 < deadline) { if (st.stop_possible() && st.stop_requested()) { return false; } auto cur = controller.snapshot(); // Heuristic: treat completely empty topology+modes as transient const bool emptyish = cur.m_topology.empty() && cur.m_modes.empty(); if (have_last && !emptyish && (cur == last)) { out = std::move(cur); return true; } last = std::move(cur); have_last = true; if (st.stop_possible() && st.stop_requested()) { return false; } std::this_thread::sleep_for(interval); } return false; } void schedule_hdr_blank_if_needed(bool enabled) { cancel_hdr_blank(); if (!enabled) { return; } hdr_blank_thread = std::jthread(&ServiceState::hdr_blank_proc, this); } void cancel_hdr_blank() { if (hdr_blank_thread.joinable()) { hdr_blank_thread.request_stop(); hdr_blank_thread.join(); } } static void hdr_blank_proc(std::stop_token st, ServiceState *self) { using namespace std::chrono_literals; // Fire soon after apply; delay is baked into blank_hdr_states if (st.stop_requested()) { return; } // Use fixed 1 second delay per requirements self->controller.blank_hdr_states(1000ms); } // Windows enumerates topology groups in an arbitrary, session-dependent order; // only the set of groups (and their members) is meaningful, mirroring // WinDisplayDevice::isTopologyTheSame. static display_device::ActiveTopology canonical_topology(display_device::ActiveTopology topology) { for (auto &group : topology) { std::sort(group.begin(), group.end()); } std::sort(topology.begin(), topology.end()); return topology; } // Strict comparator: require full structural equality; allow Unknown==Unknown for HDR. // Topology is compared order-insensitively so a restore isn't treated as failed // (and endlessly re-applied) just because the OS enumerates paths in a new order. static bool equal_snapshots_strict(const display_device::DisplaySettingsSnapshot &a, const display_device::DisplaySettingsSnapshot &b) { if (canonical_topology(a.m_topology) != canonical_topology(b.m_topology)) { return false; } if (!(a.m_modes == b.m_modes && a.m_hdr_states == b.m_hdr_states && a.m_primary_device == b.m_primary_device)) { return false; } // Origins are optional for backward compatibility with older snapshots if (!a.m_origins.empty() && !b.m_origins.empty()) { return a.m_origins == b.m_origins; } return true; } static std::set snapshot_device_set(const display_device::DisplaySettingsSnapshot &s) { std::set out; for (const auto &grp : s.m_topology) { for (const auto &id : grp) { out.insert(id); } } if (out.empty()) { for (const auto &kv : s.m_modes) { out.insert(kv.first); } } return out; } static std::set topology_device_set(const display_device::ActiveTopology &topology) { std::set out; for (const auto &grp : topology) { out.insert(grp.begin(), grp.end()); } return out; } bool should_skip_session_snapshot( const display_device::SingleDisplayConfiguration &cfg, const display_device::DisplaySettingsSnapshot &snap ) { using Prep = display_device::SingleDisplayConfiguration::DevicePreparation; if (cfg.m_device_prep != Prep::EnsureOnlyDisplay) { return false; } auto expected_topology = controller.compute_expected_topology(cfg); if (!expected_topology) { return false; } if (!controller.is_topology_the_same(snap.m_topology, *expected_topology)) { return false; } const auto expected_devices = topology_device_set(*expected_topology); if (expected_devices.empty()) { return false; } const auto snap_devices = snapshot_device_set(snap); if (snap_devices != expected_devices) { return false; } const auto all_devices = controller.enum_all_device_ids(); for (const auto &id : all_devices) { if (!expected_devices.contains(id)) { return true; } } return false; } static bool equal_monitors_only(const display_device::DisplaySettingsSnapshot &a, const display_device::DisplaySettingsSnapshot &b) { return snapshot_device_set(a) == snapshot_device_set(b); } // Quiet period: ensure no changes for the specified duration bool quiet_period( std::chrono::milliseconds duration = 750ms, std::chrono::milliseconds interval = 150ms, std::stop_token st = {} ) { display_device::DisplaySettingsSnapshot base; if (!read_stable_snapshot(base, 2000ms, 150ms, st)) { return false; } auto t0 = std::chrono::steady_clock::now(); while (std::chrono::steady_clock::now() - t0 < duration) { if (st.stop_possible() && st.stop_requested()) { return false; } display_device::DisplaySettingsSnapshot cur; if (!read_stable_snapshot(cur, 2000ms, 150ms, st)) { return false; } if (!(cur == base)) { // topology changed during quiet period return false; } if (st.stop_possible() && st.stop_requested()) { return false; } std::this_thread::sleep_for(interval); } return true; } void signal_restore_event( const char *reason = nullptr, RestoreWindow window = RestoreWindow::Event, bool force_start = false ) { if (!restore_requested.load(std::memory_order_acquire)) { return; } if (force_start || reason) { reset_restore_backoff(); } if (!force_start && reason && restore_stage_running.load(std::memory_order_acquire)) { BOOST_LOG(debug) << "Dropping restore event while stage loop active: " << reason; return; } const auto now = std::chrono::steady_clock::now(); const auto now_ms = std::chrono::duration_cast(now.time_since_epoch()).count(); const auto debounce_window_ms = std::chrono::duration_cast(kRestoreEventDebounce).count(); const auto window_duration = (window == RestoreWindow::Primary) ? kRestoreWindowPrimary : kRestoreWindowEvent; const auto desired_until = now + window_duration; const auto desired_until_ms = std::chrono::duration_cast(desired_until.time_since_epoch()).count(); bool should_signal = true; if (force_start) { restore_active_until_ms.store(desired_until_ms, std::memory_order_release); restore_active_window.store(window, std::memory_order_release); last_restore_event_ms.store(now_ms, std::memory_order_release); if (reason) { BOOST_LOG(info) << "Restore event signalled: " << reason; } } else if (reason) { const auto last_event = last_restore_event_ms.load(std::memory_order_acquire); if (last_event != 0 && (now_ms - last_event) < debounce_window_ms) { should_signal = false; } else { last_restore_event_ms.store(now_ms, std::memory_order_release); BOOST_LOG(info) << "Restore event signalled: " << reason; const auto current_until_ms = restore_active_until_ms.load(std::memory_order_acquire); if (current_until_ms == 0 || now_ms >= current_until_ms || desired_until_ms > current_until_ms) { restore_active_until_ms.store(desired_until_ms, std::memory_order_release); restore_active_window.store(window, std::memory_order_release); } } } if (!should_signal) { return; } { std::lock_guard lk(restore_event_mutex); restore_event_flag = true; } restore_event_cv.notify_all(); } bool wait_for_restore_event(std::stop_token st, std::chrono::milliseconds fallback) { std::unique_lock lk(restore_event_mutex); auto pred = [&]() { return restore_event_flag || st.stop_requested(); }; if (!restore_event_flag) { restore_event_cv.wait_for(lk, fallback, pred); } if (restore_event_flag) { restore_event_flag = false; return true; } return false; } // Helper to access known-present devices: union of active (modes keys) // and all enumerated devices (captures inactive but connected displays). std::set known_present_devices() { std::set result; try { // Active devices (have modes) const auto snap = controller.snapshot(); for (const auto &kv : snap.m_modes) { result.insert(kv.first); } // Enumerated devices (active or inactive) const auto all = controller.enum_all_device_ids(); result.insert(all.begin(), all.end()); // Fallback to topology flatten if the above produced nothing if (result.empty()) { for (const auto &grp : snap.m_topology) { result.insert(grp.begin(), grp.end()); } } } catch (...) {} return result; } // Golden cooldown and device presence pre-checks bool should_skip_golden(const display_device::DisplaySettingsSnapshot &golden) { const auto now_ms = std::chrono::duration_cast( std::chrono::steady_clock::now().time_since_epoch() ) .count(); const auto last_ok = last_session_restore_success_ms.load(std::memory_order_acquire); if (last_ok != 0 && (now_ms - last_ok) < 60'000) { BOOST_LOG(info) << "Skipping golden: recent session restore success guard active."; return true; } // Ensure all devices in golden exist now std::set golden_devices; for (const auto &grp : golden.m_topology) { for (const auto &id : grp) { golden_devices.insert(id); } } if (golden_devices.empty()) { // be conservative if snapshot malformed return true; } const auto present = known_present_devices(); for (const auto &id : golden_devices) { if (!present.contains(id)) { BOOST_LOG(info) << "Skipping golden: device not present: " << id; return true; } } return false; } void clear_session_restore_snapshots_after_golden() { std::error_code ec_cur; const bool removed_current = std::filesystem::remove(session_current_path, ec_cur); std::error_code ec_prev; const bool removed_previous = std::filesystem::remove(session_previous_path, ec_prev); session_saved.store(false, std::memory_order_release); BOOST_LOG(info) << "Golden restore cleanup: removed current=" << (removed_current && !ec_cur ? "true" : "false") << ", previous=" << (removed_previous && !ec_prev ? "true" : "false"); if (ec_cur) { BOOST_LOG(warning) << "Golden restore cleanup: failed to remove current session snapshot '" << session_current_path.string() << "' (ec=" << ec_cur.value() << ")"; } if (ec_prev) { BOOST_LOG(warning) << "Golden restore cleanup: failed to remove previous session snapshot '" << session_previous_path.string() << "' (ec=" << ec_prev.value() << ")"; } } // Apply the golden snapshot (if available) and verify the system now matches it. // Performs up to two attempts (initial + one retry) with short pauses to allow // Windows to settle. Returns true only if the post-apply signature exactly // matches the golden snapshot signature. bool apply_golden_and_confirm(std::stop_token st, uint64_t guard_generation) { auto golden_loaded = controller.load_display_settings_snapshot_with_metadata(golden_path); if (!golden_loaded) { BOOST_LOG(warning) << "Golden restore snapshot not found; cannot perform revert."; return false; } const auto &golden = golden_loaded->snapshot; const auto &golden_layouts = golden_loaded->layout_rotations; const bool require_layout_match = golden_loaded->has_layout_data; if (!require_layout_match && golden_loaded->snapshot_version < DisplayController::snapshot_layout_version_latest) { BOOST_LOG(info) << "Golden restore snapshot uses legacy schema (version " << golden_loaded->snapshot_version << "): no display layout metadata."; } if (should_skip_golden(golden)) { return false; } const auto before_sig = controller.signature(controller.snapshot()); const auto should_cancel = [&]() { if (restore_cancel_generation.load(std::memory_order_acquire) != guard_generation) { return true; } if (!restore_requested.load(std::memory_order_acquire)) { return true; } return st.stop_possible() && st.stop_requested(); }; auto confirm_current_matches_golden = [&]() -> bool { display_device::DisplaySettingsSnapshot cur; const bool got_stable = read_stable_snapshot(cur, 2000ms, 150ms, st); if (should_cancel()) { return false; } const bool layout_ok = !require_layout_match || controller.current_layout_matches(golden_layouts); const bool ok = got_stable && equal_snapshots_strict(cur, golden) && layout_ok && quiet_period(750ms, 150ms, st); if (ok) { BOOST_LOG(info) << "Golden restore: current state already matches golden snapshot; skipping apply."; } return ok; }; if (should_cancel()) { return false; } if (confirm_current_matches_golden()) { BOOST_LOG(info) << "Golden restore confirmed without apply; clearing session restore snapshots."; clear_session_restore_snapshots_after_golden(); clear_golden_restore_status("restore confirmed"); return true; } // Attempt 1 if (should_cancel()) { return false; } (void) controller.apply_snapshot(golden, require_layout_match ? &golden_layouts : nullptr); display_device::DisplaySettingsSnapshot cur; const bool got_stable = read_stable_snapshot(cur, 2000ms, 150ms, st); if (should_cancel()) { return false; } const bool layout_ok_1 = !require_layout_match || controller.current_layout_matches(golden_layouts); bool ok = got_stable && equal_snapshots_strict(cur, golden) && layout_ok_1 && quiet_period(750ms, 150ms, st); BOOST_LOG(info) << "Golden restore attempt #1: before_sig=" << before_sig << ", current_sig=" << controller.signature(cur) << ", golden_sig=" << controller.signature(golden) << ", layout_match=" << (layout_ok_1 ? "true" : "false") << ", match=" << (ok ? "true" : "false"); if (ok) { BOOST_LOG(info) << "Golden restore confirmed; clearing session restore snapshots."; clear_session_restore_snapshots_after_golden(); clear_golden_restore_status("restore confirmed"); return true; } // Attempt 2 (double-check) after a short delay if (should_cancel()) { return false; } if (!wait_with_cancel(st, 700ms, should_cancel)) { return false; } if (should_cancel()) { return false; } if (confirm_current_matches_golden()) { BOOST_LOG(info) << "Golden restore confirmed before retry apply; clearing session restore snapshots."; clear_session_restore_snapshots_after_golden(); clear_golden_restore_status("restore confirmed"); return true; } (void) controller.apply_snapshot(golden, require_layout_match ? &golden_layouts : nullptr); display_device::DisplaySettingsSnapshot cur2; const bool got_stable2 = read_stable_snapshot(cur2, 2000ms, 150ms, st); if (should_cancel()) { return false; } const bool layout_ok_2 = !require_layout_match || controller.current_layout_matches(golden_layouts); ok = got_stable2 && equal_snapshots_strict(cur2, golden) && layout_ok_2 && quiet_period(750ms, 150ms, st); BOOST_LOG(info) << "Golden restore attempt #2: current_sig=" << controller.signature(cur2) << ", golden_sig=" << controller.signature(golden) << ", layout_match=" << (layout_ok_2 ? "true" : "false") << ", match=" << (ok ? "true" : "false"); if (ok) { BOOST_LOG(info) << "Golden restore confirmed (retry); clearing session restore snapshots."; clear_session_restore_snapshots_after_golden(); clear_golden_restore_status("restore confirmed"); } return ok; } // Apply a session snapshot (current/previous) and verify the system now matches it. bool apply_session_snapshot_from_path( const std::filesystem::path &path, const char *label, std::stop_token st, uint64_t guard_generation, bool &attempted ) { attempted = false; auto base_loaded = controller.load_display_settings_snapshot_with_metadata(path); if (!base_loaded) { BOOST_LOG(info) << (label ? label : "session") << " snapshot not available."; return false; } const auto &base = base_loaded->snapshot; const auto &base_layouts = base_loaded->layout_rotations; const bool require_layout_match = base_loaded->has_layout_data; if (!require_layout_match && base_loaded->snapshot_version < DisplayController::snapshot_layout_version_latest) { BOOST_LOG(info) << (label ? label : "session") << " snapshot uses legacy schema (version " << base_loaded->snapshot_version << "): no display layout metadata."; } attempted = true; if (auto missing = controller.missing_devices_for_topology(base.m_topology); !missing.empty()) { std::string joined; for (size_t i = 0; i < missing.size(); ++i) { if (i > 0) { joined += ", "; } joined += missing[i]; } BOOST_LOG(info) << (label ? label : "session") << " snapshot skipped (missing devices): [" << joined << "]"; return false; } if (!controller.is_topology_valid(base)) { BOOST_LOG(info) << (label ? label : "session") << " snapshot rejected due to invalid topology."; return false; } const auto before_sig = controller.signature(controller.snapshot()); const auto should_cancel = [&]() { if (restore_cancel_generation.load(std::memory_order_acquire) != guard_generation) { return true; } if (!restore_requested.load(std::memory_order_acquire)) { return true; } return st.stop_possible() && st.stop_requested(); }; auto confirm_current_matches_session = [&]() -> bool { display_device::DisplaySettingsSnapshot cur; const bool got_stable = read_stable_snapshot(cur, 2000ms, 150ms, st); if (should_cancel()) { return false; } const bool layout_ok = !require_layout_match || controller.current_layout_matches(base_layouts); const bool ok = got_stable && equal_snapshots_strict(cur, base) && layout_ok && quiet_period(750ms, 150ms, st); if (ok) { BOOST_LOG(info) << "Session restore (" << (label ? label : "session") << "): current state already matches baseline; skipping apply."; } return ok; }; if (should_cancel()) { return false; } if (confirm_current_matches_session()) { const auto now_ms = std::chrono::duration_cast( std::chrono::steady_clock::now().time_since_epoch() ) .count(); last_session_restore_success_ms.store(now_ms, std::memory_order_release); return true; } if (should_cancel()) { return false; } (void) controller.apply_snapshot(base, require_layout_match ? &base_layouts : nullptr); display_device::DisplaySettingsSnapshot cur; const bool got_stable = read_stable_snapshot(cur, 2000ms, 150ms, st); if (should_cancel()) { return false; } const bool layout_ok_1 = !require_layout_match || controller.current_layout_matches(base_layouts); bool ok = got_stable && equal_snapshots_strict(cur, base) && layout_ok_1 && quiet_period(750ms, 150ms, st); BOOST_LOG(info) << "Session restore (" << (label ? label : "session") << ") attempt #1: before_sig=" << before_sig << ", current_sig=" << controller.signature(cur) << ", baseline_sig=" << controller.signature(base) << ", layout_match=" << (layout_ok_1 ? "true" : "false") << ", match=" << (ok ? "true" : "false"); if (!ok) { if (should_cancel()) { return false; } if (!wait_with_cancel(st, 700ms, should_cancel)) { return false; } if (should_cancel()) { return false; } if (confirm_current_matches_session()) { const auto now_ms = std::chrono::duration_cast( std::chrono::steady_clock::now().time_since_epoch() ) .count(); last_session_restore_success_ms.store(now_ms, std::memory_order_release); return true; } (void) controller.apply_snapshot(base, require_layout_match ? &base_layouts : nullptr); display_device::DisplaySettingsSnapshot cur2; const bool got_stable2 = read_stable_snapshot(cur2, 2000ms, 150ms, st); if (should_cancel()) { return false; } const bool layout_ok_2 = !require_layout_match || controller.current_layout_matches(base_layouts); ok = got_stable2 && equal_snapshots_strict(cur2, base) && layout_ok_2 && quiet_period(750ms, 150ms, st); BOOST_LOG(info) << "Session restore (" << (label ? label : "session") << ") attempt #2: current_sig=" << controller.signature(cur2) << ", baseline_sig=" << controller.signature(base) << ", layout_match=" << (layout_ok_2 ? "true" : "false") << ", match=" << (ok ? "true" : "false"); } if (ok) { const auto now_ms = std::chrono::duration_cast( std::chrono::steady_clock::now().time_since_epoch() ) .count(); last_session_restore_success_ms.store(now_ms, std::memory_order_release); } return ok; } // Attempt a restore once if a valid topology is present. Returns true only // when restore is fully complete, false otherwise. // When always_restore_from_golden is true, golden snapshot is preferred. A // session snapshot may be applied as a temporary fallback, but the helper // keeps polling until golden restore succeeds or the restore window ends. // Otherwise, prefers session baseline chain, then golden. bool try_restore_once_if_valid(std::stop_token st, uint64_t guard_generation) { const auto cancelled = [&]() { if (restore_cancel_generation.load(std::memory_order_acquire) != guard_generation) { return true; } if (!restore_requested.load(std::memory_order_acquire)) { return true; } return st.stop_possible() && st.stop_requested(); }; if (cancelled()) { return false; } restore_attempted_unconfirmed.store(true, std::memory_order_release); const bool golden_first = always_restore_from_golden.load(std::memory_order_acquire); if (!golden_first) { reset_pending_golden_session_fallbacks(); } // Lambda to try golden restore auto try_golden = [&]() -> bool { if (cancelled()) { return false; } if (auto golden = controller.load_display_settings_snapshot(golden_path)) { if (cancelled()) { return false; } if (auto missing = controller.missing_devices_for_topology(golden->m_topology); !missing.empty()) { std::string joined; for (size_t i = 0; i < missing.size(); ++i) { if (i > 0) { joined += ", "; } joined += missing[i]; } BOOST_LOG(info) << "Golden snapshot skipped (missing devices): [" << joined << "]"; note_golden_restore_issue("missing_devices"); return false; } if (controller.validate_topology_with_os(golden->m_topology)) { if (apply_golden_and_confirm(st, guard_generation)) { return true; } if (!cancelled()) { note_golden_restore_issue("restore_not_confirmed"); } } else { note_golden_restore_issue("invalid_topology"); } } return false; }; // Lambda to try session snapshots (current then previous) bool tried_golden_before_previous = false; auto try_session_snapshots = [&]() -> bool { bool attempted_current = false; const bool restored_current = apply_session_snapshot_from_path( session_current_path, "current", st, guard_generation, attempted_current ); if (restored_current) { (void) promote_current_snapshot_to_previous("restore success"); return true; } const bool current_snapshot_unavailable = !attempted_current; const bool prefer_golden_before_previous = prefer_golden_if_current_missing.load(std::memory_order_acquire) && current_snapshot_unavailable; if (prefer_golden_before_previous) { std::error_code ec_prev, ec_golden; const bool has_previous = std::filesystem::exists(session_previous_path, ec_prev) && !ec_prev; const bool has_golden = std::filesystem::exists(golden_path, ec_golden) && !ec_golden; if (has_previous && has_golden) { tried_golden_before_previous = true; BOOST_LOG(info) << "Restore: current snapshot unavailable; preferring golden snapshot over previous session snapshot."; if (try_golden()) { return true; } } } bool attempted_previous = false; const bool restored_previous = apply_session_snapshot_from_path( session_previous_path, "previous", st, guard_generation, attempted_previous ); if (restored_previous) { if (attempted_current) { std::error_code ec_rm_bad; (void) std::filesystem::remove(session_current_path, ec_rm_bad); } return true; } (void) attempted_previous; return false; }; if (golden_first) { // Prefer golden snapshot, fallback to session snapshots BOOST_LOG(info) << "Restore: using golden-first strategy (always_restore_from_golden=true)"; if (try_golden()) { reset_pending_golden_session_fallbacks(); return true; } // Golden failed. Session snapshots can keep the machine usable, but // they cannot complete a request whose configured authoritative // baseline is still pending. if (!try_session_snapshots()) { reset_pending_golden_session_fallbacks(); return false; } if (controller.load_display_settings_snapshot(golden_path)) { const auto fallback_count = note_pending_golden_session_fallback(); BOOST_LOG(info) << "Restore: session fallback applied while golden snapshot remains pending; continuing polling (attempt " << fallback_count << ")."; return false; } register_unresolved_golden_restore_request("session fallback accepted"); reset_pending_golden_session_fallbacks(); return true; } else { // Default: prefer session snapshots, fallback to golden if (try_session_snapshots()) { if (tried_golden_before_previous) { register_unresolved_golden_restore_request("session fallback accepted"); } reset_pending_golden_session_fallbacks(); return true; } if (tried_golden_before_previous) { reset_pending_golden_session_fallbacks(); return false; } const bool restored_golden = try_golden(); reset_pending_golden_session_fallbacks(); return restored_golden; } } // Start a background polling loop that checks every ~3s whether the // requested restore topology is valid; if so, perform the restore and // confirm success. Logging is throttled (~15 minutes) to avoid noise. void ensure_restore_polling( RestoreWindow window = RestoreWindow::Primary, const char *reason = "initial", bool force_start = true ) { if (!restore_requested.load(std::memory_order_acquire)) { return; } bool pump_expected = false; if (event_pump_running.compare_exchange_strong(pump_expected, true, std::memory_order_acq_rel)) { event_pump.start([this](const char *event_reason) { if (!restore_requested.load(std::memory_order_acquire)) { return; } const char *why = event_reason ? event_reason : "event"; if (!restore_poll_active.load(std::memory_order_acquire)) { ensure_restore_polling(RestoreWindow::Event, why, true); } else { signal_restore_event(why, RestoreWindow::Event); } }); } bool expected = false; if (!restore_poll_active.compare_exchange_strong(expected, true, std::memory_order_acq_rel)) { const char *label = reason ? reason : ((window == RestoreWindow::Primary) ? "initial" : "event"); signal_restore_event(label, window, force_start); BOOST_LOG(debug) << "Restore loop already active; window updated to " << ((window == RestoreWindow::Primary) ? "primary" : "event"); return; } const char *label = reason ? reason : ((window == RestoreWindow::Primary) ? "initial" : "event"); signal_restore_event(label, window, force_start); restore_poll_thread = std::jthread(&ServiceState::restore_poll_proc, this); } void stop_restore_polling() { restore_poll_active.store(false, std::memory_order_release); request_restore_cancel(); event_pump.stop(); event_pump_running.store(false, std::memory_order_release); restore_attempted_unconfirmed.store(false, std::memory_order_release); reset_restore_backoff(); restore_active_until_ms.store(0, std::memory_order_release); last_restore_event_ms.store(0, std::memory_order_release); restore_active_window.store(RestoreWindow::Event, std::memory_order_release); restore_stage_running.store(false, std::memory_order_release); stop_and_join(restore_poll_thread, "restore-poll"); restore_requested.store(false, std::memory_order_release); restore_origin_epoch.store(0, std::memory_order_release); prefer_golden_if_current_missing.store(true, std::memory_order_release); reset_pending_golden_session_fallbacks(); } void disarm_restore_requests(const char *reason = nullptr) { const bool had_pending = restore_requested.load(std::memory_order_acquire); stop_restore_polling(); cancel_delayed_reapply(); cancel_post_apply_tasks(); delete_restore_scheduled_task(); direct_revert_bypass_grace.store(false, std::memory_order_release); exit_after_revert.store(false, std::memory_order_release); retry_apply_on_topology.store(false, std::memory_order_release); retry_revert_on_topology.store(false, std::memory_order_release); if (reason) { BOOST_LOG(info) << reason << " (pending_restore=" << (had_pending ? "true" : "false") << ")"; } else if (had_pending) { BOOST_LOG(info) << "Restore requests disarmed."; } } uint64_t begin_connection_epoch() { const auto epoch = next_connection_epoch.fetch_add(1, std::memory_order_acq_rel); active_connection_epoch.store(epoch, std::memory_order_release); return epoch; } uint64_t current_connection_epoch() const { return active_connection_epoch.load(std::memory_order_acquire); } bool is_connection_epoch_current(uint64_t epoch) const { return current_connection_epoch() == epoch; } void clear_restore_origin() { restore_origin_epoch.store(0, std::memory_order_release); prefer_golden_if_current_missing.store(true, std::memory_order_release); restore_attempted_unconfirmed.store(false, std::memory_order_release); reset_pending_golden_session_fallbacks(); } bool should_exit_after_restore() const { const auto origin = restore_origin_epoch.load(std::memory_order_acquire); if (origin == 0) { return true; } return origin == current_connection_epoch(); } static void restore_poll_proc(std::stop_token st, ServiceState *self) { using namespace std::chrono_literals; const auto kPoll = 3s; const auto kLogThrottle = std::chrono::minutes(15); auto last_log = std::chrono::steady_clock::now() - kLogThrottle; // allow immediate log const auto guard_generation = self->restore_cancel_generation.load(std::memory_order_acquire); auto cancelled = [&]() { if (st.stop_requested()) { return true; } if (self->restore_cancel_generation.load(std::memory_order_acquire) != guard_generation) { return true; } if (!self->restore_requested.load(std::memory_order_acquire)) { return true; } return false; }; auto run_restore_cleanup = [&](const char *context) { bool allow_cleanup = !cancelled(); if (allow_cleanup) { refresh_shell_after_display_change(); allow_cleanup = !cancelled(); } if (allow_cleanup) { delete_restore_scheduled_task(); } else { BOOST_LOG(debug) << "Restore cleanup skipped" << (context ? std::string(" (") + context + ")" : "") << " due to cancellation."; } }; // If there is no session or golden snapshot, there is nothing to restore. try { std::error_code ec1, ec2; const bool has_session = std::filesystem::exists(self->session_current_path, ec1); std::error_code ec_prev; const bool has_previous = std::filesystem::exists(self->session_previous_path, ec_prev); const bool has_golden = std::filesystem::exists(self->golden_path, ec2); if (!has_session && !has_previous && !has_golden) { BOOST_LOG(info) << "Restore polling: no session/previous or golden snapshot present; exiting helper."; if (self->running_flag) { self->running_flag->store(false, std::memory_order_release); } self->event_pump.stop(); self->event_pump_running.store(false, std::memory_order_release); self->restore_poll_active.store(false, std::memory_order_release); self->restore_requested.store(false, std::memory_order_release); self->clear_restore_origin(); return; } } catch (...) { // fall through } if (cancelled()) { self->restore_stage_running.store(false, std::memory_order_release); self->restore_poll_active.store(false, std::memory_order_release); return; } // Initial one-shot attempt before entering the loop bool initial_attempted = false; bool initial_success = false; try { if (!cancelled() && self->await_restore_backoff(st) && !cancelled()) { initial_attempted = true; initial_success = self->try_restore_once_if_valid(st, guard_generation); } } catch (...) {} if (initial_success) { if (cancelled()) { self->event_pump.stop(); self->event_pump_running.store(false, std::memory_order_release); self->restore_poll_active.store(false, std::memory_order_release); self->restore_active_until_ms.store(0, std::memory_order_release); self->restore_active_window.store(RestoreWindow::Event, std::memory_order_release); self->last_restore_event_ms.store(0, std::memory_order_release); self->restore_requested.store(false, std::memory_order_release); self->clear_restore_origin(); return; } self->reset_restore_backoff(); self->retry_revert_on_topology.store(false, std::memory_order_release); self->exit_after_revert.store(false, std::memory_order_release); run_restore_cleanup("initial attempt"); if (cancelled()) { self->event_pump.stop(); self->event_pump_running.store(false, std::memory_order_release); self->restore_poll_active.store(false, std::memory_order_release); self->restore_active_until_ms.store(0, std::memory_order_release); self->restore_active_window.store(RestoreWindow::Event, std::memory_order_release); self->last_restore_event_ms.store(0, std::memory_order_release); self->restore_requested.store(false, std::memory_order_release); self->clear_restore_origin(); return; } const bool exit_helper = self->should_exit_after_restore(); if (exit_helper && self->running_flag) { BOOST_LOG(info) << "Restore confirmed (initial attempt); exiting helper."; self->running_flag->store(false, std::memory_order_release); } else if (!exit_helper) { BOOST_LOG(info) << "Restore confirmed (initial attempt); keeping helper alive for newer connection."; } self->event_pump.stop(); self->event_pump_running.store(false, std::memory_order_release); self->restore_poll_active.store(false, std::memory_order_release); self->restore_active_until_ms.store(0, std::memory_order_release); self->restore_active_window.store(RestoreWindow::Event, std::memory_order_release); self->last_restore_event_ms.store(0, std::memory_order_release); self->restore_requested.store(false, std::memory_order_release); self->clear_restore_origin(); return; } if (initial_attempted && !initial_success) { self->register_restore_failure(); } bool exit_due_to_timeout = false; while (!cancelled()) { const auto now = std::chrono::steady_clock::now(); const auto now_ms = std::chrono::duration_cast(now.time_since_epoch()).count(); auto active_until_ms = self->restore_active_until_ms.load(std::memory_order_acquire); const auto active_window_kind = self->restore_active_window.load(std::memory_order_acquire); bool active_window = (active_until_ms != 0 && now_ms <= active_until_ms); bool window_expired = false; if (!active_window && active_until_ms != 0 && now_ms > active_until_ms) { window_expired = true; self->restore_active_until_ms.store(0, std::memory_order_release); self->restore_active_window.store(RestoreWindow::Event, std::memory_order_release); } const auto wait_timeout = active_window ? 500ms : kPoll; bool triggered = false; try { triggered = self->wait_for_restore_event(st, wait_timeout); } catch (...) {} if (!triggered && active_window && active_window_kind == RestoreWindow::Primary) { triggered = true; } if (cancelled()) { break; } if (!triggered) { if (window_expired) { const char *window_label = (active_window_kind == RestoreWindow::Primary) ? "primary" : "event"; BOOST_LOG(info) << "Restore polling: " << window_label << " window exhausted; pausing attempts until next event."; exit_due_to_timeout = true; break; } const auto now2 = std::chrono::steady_clock::now(); if (now2 - last_log >= kLogThrottle) { last_log = now2; BOOST_LOG(info) << "Restore polling: waiting for event-driven topology changes."; } continue; } if (!self->await_restore_backoff(st)) { break; } if (cancelled()) { break; } const auto window_deadline_ms = self->restore_active_until_ms.load(std::memory_order_acquire); self->restore_stage_running.store(true, std::memory_order_release); bool success = false; try { success = self->try_restore_once_if_valid(st, guard_generation); } catch (...) { self->restore_stage_running.store(false, std::memory_order_release); throw; } self->restore_stage_running.store(false, std::memory_order_release); if (cancelled()) { break; } if (success) { if (cancelled()) { self->event_pump.stop(); self->event_pump_running.store(false, std::memory_order_release); self->restore_poll_active.store(false, std::memory_order_release); self->restore_active_until_ms.store(0, std::memory_order_release); self->restore_active_window.store(RestoreWindow::Event, std::memory_order_release); self->last_restore_event_ms.store(0, std::memory_order_release); self->restore_requested.store(false, std::memory_order_release); self->clear_restore_origin(); return; } self->reset_restore_backoff(); self->retry_revert_on_topology.store(false, std::memory_order_release); self->exit_after_revert.store(false, std::memory_order_release); run_restore_cleanup("polling attempt"); if (cancelled()) { self->event_pump.stop(); self->event_pump_running.store(false, std::memory_order_release); self->restore_poll_active.store(false, std::memory_order_release); self->restore_active_until_ms.store(0, std::memory_order_release); self->restore_active_window.store(RestoreWindow::Event, std::memory_order_release); self->last_restore_event_ms.store(0, std::memory_order_release); self->restore_requested.store(false, std::memory_order_release); self->clear_restore_origin(); return; } const bool exit_helper = self->should_exit_after_restore(); if (exit_helper && self->running_flag) { BOOST_LOG(info) << "Restore confirmed; exiting helper."; self->running_flag->store(false, std::memory_order_release); } else if (!exit_helper) { BOOST_LOG(info) << "Restore confirmed while newer connection active; helper remains running."; } self->restore_poll_active.store(false, std::memory_order_release); self->event_pump.stop(); self->event_pump_running.store(false, std::memory_order_release); self->restore_active_until_ms.store(0, std::memory_order_release); self->restore_active_window.store(RestoreWindow::Event, std::memory_order_release); self->last_restore_event_ms.store(0, std::memory_order_release); self->restore_requested.store(false, std::memory_order_release); self->clear_restore_origin(); return; } self->register_restore_failure(); const auto post_ms = std::chrono::duration_cast( std::chrono::steady_clock::now().time_since_epoch() ) .count(); if (window_deadline_ms != 0 && post_ms > window_deadline_ms) { self->restore_active_until_ms.store(0, std::memory_order_release); self->restore_active_window.store(RestoreWindow::Event, std::memory_order_release); } } self->restore_stage_running.store(false, std::memory_order_release); self->restore_poll_active.store(false, std::memory_order_release); self->restore_active_until_ms.store(0, std::memory_order_release); self->restore_active_window.store(RestoreWindow::Event, std::memory_order_release); self->last_restore_event_ms.store(0, std::memory_order_release); self->reset_restore_backoff(); if (exit_due_to_timeout) { self->register_unresolved_golden_restore_request("restore window exhausted"); return; } if (!cancelled()) { self->register_unresolved_golden_restore_request("restore ended unresolved"); } self->event_pump.stop(); self->event_pump_running.store(false, std::memory_order_release); self->restore_requested.store(false, std::memory_order_release); self->clear_restore_origin(); } void on_topology_changed() { // Re-apply path if (retry_apply_on_topology.load(std::memory_order_acquire)) { BOOST_LOG(info) << "Topology changed: reattempting apply"; const auto cfg = effective_last_cfg(); if (cfg && controller.apply(*cfg)) { retry_apply_on_topology.store(false, std::memory_order_release); refresh_shell_after_display_change(); } return; } // Revert/restore path is handled by restore polling loop now. (void) 0; } // Schedule delayed re-apply attempts to work around Windows sometimes forcing native // resolution immediately after activating a display. The provided delays represent // the windows (relative to now) when verification/re-apply should be attempted. void schedule_delayed_reapply(std::vector delays = {250ms, 750ms}) { std::lock_guard lock(delayed_reapply_mutex); if (delayed_reapply_thread.joinable()) { delayed_reapply_thread.request_stop(); delayed_reapply_thread.join(); } if (!last_cfg || delays.empty()) { return; } delayed_reapply_thread = std::jthread(&ServiceState::delayed_reapply_proc, this, std::move(delays)); } void cancel_delayed_reapply() { std::lock_guard lock(delayed_reapply_mutex); if (delayed_reapply_thread.joinable()) { delayed_reapply_thread.request_stop(); delayed_reapply_thread.join(); } } void stop_and_async_join(std::jthread &thread, const char *label) { if (!thread.joinable()) { return; } thread.request_stop(); std::jthread joiner([label, t = std::move(thread)]() mutable { const auto start = std::chrono::steady_clock::now(); if (t.joinable()) { t.join(); const auto elapsed = std::chrono::duration_cast( std::chrono::steady_clock::now() - start ); BOOST_LOG(debug) << "Async join completed for " << (label ? label : "thread") << " after " << elapsed.count() << "ms"; } }); { std::lock_guard lg(async_join_mutex); async_join_threads.emplace_back(std::move(joiner)); } } void stop_and_join(std::jthread &thread, const char *label) { if (!thread.joinable()) { return; } thread.request_stop(); const auto start = std::chrono::steady_clock::now(); thread.join(); const auto elapsed = std::chrono::duration_cast( std::chrono::steady_clock::now() - start ); BOOST_LOG(debug) << "Join completed for " << (label ? label : "thread") << " after " << elapsed.count() << "ms"; } static bool wait_with_stop(std::stop_token st, std::chrono::milliseconds duration) { using namespace std::chrono_literals; constexpr auto step = 50ms; auto remaining = duration; while (remaining > std::chrono::milliseconds::zero()) { if (st.stop_requested()) { return false; } const auto slice = remaining > step ? step : remaining; std::this_thread::sleep_for(slice); remaining -= slice; } return !st.stop_requested(); } template static bool wait_with_cancel(std::stop_token st, std::chrono::milliseconds duration, CancelPredicate cancelled) { using namespace std::chrono_literals; constexpr auto step = 50ms; auto remaining = duration; while (remaining > std::chrono::milliseconds::zero()) { if (st.stop_requested() || cancelled()) { return false; } const auto slice = remaining > step ? step : remaining; std::this_thread::sleep_for(slice); remaining -= slice; } return !(st.stop_requested() || cancelled()); } static void delayed_reapply_proc(std::stop_token st, ServiceState *self, std::vector delays) { for (auto delay : delays) { if (!wait_with_stop(st, delay)) { return; } if (self->restore_requested.load(std::memory_order_acquire)) { return; } if (self->verify_last_configuration_sticky(kVerificationSettleDelay, st)) { continue; } if (self->restore_requested.load(std::memory_order_acquire)) { return; } BOOST_LOG(info) << "Delayed re-apply attempt after activation 213Q902"; self->best_effort_apply_last_cfg(); } } std::optional effective_last_cfg() const { if (!last_cfg) { return std::nullopt; } auto cfg = *last_cfg; const auto packed_rate = refresh_rate_override.load(std::memory_order_acquire); if (packed_rate != 0) { const auto numerator = static_cast(packed_rate >> 32u); const auto denominator = static_cast(packed_rate & 0xffffffffu); cfg.m_refresh_rate = display_device::Rational {numerator, denominator}; } return cfg; } void best_effort_apply_last_cfg() { try { if (const auto cfg = effective_last_cfg()) { (void) controller.apply(*cfg); refresh_shell_after_display_change(); } } catch (...) {} } bool verify_last_configuration_sticky(std::chrono::milliseconds settle_delay = kVerificationSettleDelay, std::stop_token st = {}) { auto matches = [&]() { const auto cfg = effective_last_cfg(); return !cfg || controller.configuration_matches_current_state(*cfg); }; if (!matches()) { return false; } if (settle_delay > std::chrono::milliseconds::zero()) { if (!wait_with_stop(st, settle_delay)) { return false; } return matches(); } return true; } bool configuration_matches_last() const { const auto cfg = effective_last_cfg(); return !cfg || controller.configuration_matches_current_state(*cfg); } void cancel_post_apply_tasks() { stop_and_join(post_apply_thread, "post-apply"); } void schedule_post_apply_tasks( bool enforce_snapshot, std::optional before_sig, bool wa_hdr_toggle, std::optional requested_virtual_layout, std::vector> monitor_position_overrides, std::vector>> refresh_rate_overrides, std::vector reapply_delays ) { cancel_post_apply_tasks(); post_apply_thread = std::jthread( [this, enforce_snapshot, before_sig = std::move(before_sig), wa_hdr_toggle, requested_virtual_layout = std::move(requested_virtual_layout), monitor_position_overrides = std::move(monitor_position_overrides), refresh_rate_overrides = std::move(refresh_rate_overrides), reapply_delays = std::move(reapply_delays)](std::stop_token st) mutable { const auto apply_epoch = current_connection_epoch(); auto cancelled = [&]() { return st.stop_requested() || !is_connection_epoch_current(apply_epoch); }; if (cancelled()) { return; } if (enforce_snapshot && before_sig) { display_device::DisplaySettingsSnapshot cur; const bool got_stable = read_stable_snapshot( cur, std::chrono::milliseconds(600), std::chrono::milliseconds(75), st ); (void) got_stable; } if (cancelled()) { return; } retry_apply_on_topology.store(false, std::memory_order_release); if (!reapply_delays.empty()) { if (cancelled()) { return; } schedule_delayed_reapply(std::move(reapply_delays)); } if (cancelled()) { return; } refresh_shell_after_display_change(); if (cancelled()) { return; } schedule_hdr_blank_if_needed(wa_hdr_toggle); if (cancelled()) { return; } if (requested_virtual_layout) { BOOST_LOG(info) << "Display helper: requested virtual display layout=" << *requested_virtual_layout; } if (cancelled()) { return; } if (!monitor_position_overrides.empty()) { constexpr int kMinDisplayOrigin = -32768; constexpr int kMaxDisplayOrigin = 32767; constexpr auto kRepositionRetryInterval = 200ms; constexpr auto kRepositionRetryWindow = 3s; auto pending_overrides = monitor_position_overrides; const auto retry_deadline = std::chrono::steady_clock::now() + kRepositionRetryWindow; int retry_attempt = 0; while (!pending_overrides.empty()) { if (cancelled()) { return; } ++retry_attempt; std::vector> next_pending; next_pending.reserve(pending_overrides.size()); for (const auto &[device_id, origin] : pending_overrides) { if (cancelled()) { return; } if (device_id.empty()) { continue; } if (!controller.can_reposition_device(device_id)) { next_pending.emplace_back(device_id, origin); continue; } // The whole monitor rect must fit inside the GDI virtual screen // (±32767), so the maximum origin shrinks by the monitor size. // Clamping only the origin to 32767 always fails SetDisplayConfig // with ERROR_INVALID_PARAMETER for any non-zero-sized display. int max_origin_x = kMaxDisplayOrigin; int max_origin_y = kMaxDisplayOrigin; if (const auto res = controller.get_display_resolution(device_id)) { max_origin_x = std::max(kMinDisplayOrigin, kMaxDisplayOrigin - static_cast(res->m_width) + 1); max_origin_y = std::max(kMinDisplayOrigin, kMaxDisplayOrigin - static_cast(res->m_height) + 1); } const auto clamped_origin = display_device::Point { std::clamp(origin.m_x, kMinDisplayOrigin, max_origin_x), std::clamp(origin.m_y, kMinDisplayOrigin, max_origin_y) }; if (clamped_origin.m_x != origin.m_x || clamped_origin.m_y != origin.m_y) { BOOST_LOG(warning) << "Display helper: clamped monitor position override for device_id=" << device_id << " from (" << origin.m_x << "," << origin.m_y << ") to (" << clamped_origin.m_x << "," << clamped_origin.m_y << ")"; } const bool ok_origin = controller.set_display_origin(device_id, clamped_origin); if (!ok_origin) { next_pending.emplace_back(device_id, origin); } } pending_overrides = std::move(next_pending); if (pending_overrides.empty()) { break; } if (std::chrono::steady_clock::now() >= retry_deadline) { break; } if (!wait_with_stop(st, kRepositionRetryInterval)) { return; } } if (!pending_overrides.empty()) { std::string pending_ids; for (size_t i = 0; i < pending_overrides.size(); ++i) { if (i > 0) { pending_ids += ", "; } pending_ids += pending_overrides[i].first; } BOOST_LOG(warning) << "Display helper: monitor position overrides not fully applied after " << retry_attempt << " attempt(s); pending device_id(s)=" << pending_ids; } BOOST_LOG(info) << "Display helper: monitor position overrides applied result=" << (pending_overrides.empty() ? "true" : "false"); } // Restore physical monitor refresh rates from pre-VD-creation snapshot. // When a virtual display is created at (0,0), Windows may reset other monitors' // refresh rates (e.g. 240Hz → 60Hz). This restores the original rates. if (!refresh_rate_overrides.empty()) { if (cancelled()) { return; } bool rate_result = true; for (const auto &[device_id, rate] : refresh_rate_overrides) { if (cancelled()) { break; } if (device_id.empty() || rate.first == 0 || rate.second == 0) { continue; } // Skip the virtual display device if (last_cfg && device_id == last_cfg->m_device_id) { continue; } const bool ok = controller.set_device_refresh_rate(device_id, rate.first, rate.second); if (ok) { BOOST_LOG(info) << "Display helper: restored refresh rate for device=" << device_id << " to " << rate.first << "/" << rate.second; } else { BOOST_LOG(warning) << "Display helper: failed to restore refresh rate for device=" << device_id; } rate_result = rate_result && ok; } BOOST_LOG(info) << "Display helper: refresh rate overrides applied result=" << (rate_result ? "true" : "false"); } } ); } }; } // namespace // Utilities to reduce main() complexity namespace { HANDLE make_named_mutex(const wchar_t *name) { SECURITY_ATTRIBUTES sa {}; sa.nLength = sizeof(sa); sa.bInheritHandle = FALSE; return CreateMutexW(&sa, FALSE, name); } bool ensure_single_instance(HANDLE &out_handle) { out_handle = make_named_mutex(L"Global\\SunshineDisplayHelper"); if (!out_handle && GetLastError() == ERROR_ACCESS_DENIED) { out_handle = make_named_mutex(L"Local\\SunshineDisplayHelper"); } if (!out_handle) { return true; // continue; best-effort singleton failed } if (GetLastError() == ERROR_ALREADY_EXISTS) { return false; // another instance running } return true; } std::filesystem::path compute_log_dir() { // Try roaming AppData first std::wstring appdataW; appdataW.resize(MAX_PATH); if (SUCCEEDED(SHGetFolderPathW(nullptr, CSIDL_APPDATA, nullptr, SHGFP_TYPE_CURRENT, appdataW.data()))) { appdataW.resize(wcslen(appdataW.c_str())); auto path = std::filesystem::path(appdataW) / L"Sunshine"; std::error_code ec; std::filesystem::create_directories(path, ec); return path; } // Next, %APPDATA% std::wstring envAppData; DWORD needed = GetEnvironmentVariableW(L"APPDATA", nullptr, 0); if (needed > 0) { envAppData.resize(needed); DWORD written = GetEnvironmentVariableW(L"APPDATA", envAppData.data(), needed); if (written > 0) { envAppData.resize(written); auto path = std::filesystem::path(envAppData) / L"Sunshine"; std::error_code ec; std::filesystem::create_directories(path, ec); return path; } } // Fallback: temp directory or current dir std::wstring tempW; tempW.resize(MAX_PATH); DWORD tlen = GetTempPathW(MAX_PATH, tempW.data()); if (tlen > 0 && tlen < MAX_PATH) { tempW.resize(tlen); auto path = std::filesystem::path(tempW) / L"Sunshine"; std::error_code ec; std::filesystem::create_directories(path, ec); return path; } auto path = std::filesystem::path(L".") / L"Sunshine"; std::error_code ec; std::filesystem::create_directories(path, ec); return path; } std::filesystem::path compute_snapshot_dir() { // When running as SYSTEM, prefer a shared ProgramData location for snapshots. if (platf::dxgi::is_running_as_system()) { std::wstring programDataW; programDataW.resize(MAX_PATH); if (SUCCEEDED(SHGetFolderPathW(nullptr, CSIDL_COMMON_APPDATA, nullptr, SHGFP_TYPE_CURRENT, programDataW.data()))) { programDataW.resize(wcslen(programDataW.c_str())); auto path = std::filesystem::path(programDataW) / L"Sunshine"; std::error_code ec; std::filesystem::create_directories(path, ec); return path; } } // Default to per-user roaming AppData (or fallback locations inside compute_log_dir). return compute_log_dir(); } struct SnapshotPaths { std::filesystem::path golden; std::filesystem::path golden_status; std::filesystem::path session_current; std::filesystem::path session_previous; std::filesystem::path vibeshine_state; }; SnapshotPaths make_snapshot_paths(const std::filesystem::path &root) { return SnapshotPaths { .golden = root / L"display_golden_restore.json", .golden_status = root / L"display_golden_restore_status.json", .session_current = root / L"display_session_current.json", .session_previous = root / L"display_session_previous.json", .vibeshine_state = root / L"vibeshine_state.json", }; } std::vector executable_config_search_roots() { std::vector roots; wchar_t exe_path[MAX_PATH] = {}; if (!GetModuleFileNameW(nullptr, exe_path, MAX_PATH)) { return roots; } const auto module_path = std::filesystem::path(exe_path); const auto module_dir = module_path.parent_path(); if (module_dir.empty()) { return roots; } roots.push_back(module_dir / L"config"); roots.push_back(module_dir.parent_path() / L"config"); roots.push_back(module_dir.parent_path()); return roots; } std::vector snapshot_search_roots() { std::vector roots; const auto user_root = compute_log_dir(); if (!user_root.empty()) { roots.push_back(user_root); } { std::wstring programDataW; programDataW.resize(MAX_PATH); if (SUCCEEDED(SHGetFolderPathW(nullptr, CSIDL_COMMON_APPDATA, nullptr, SHGFP_TYPE_CURRENT, programDataW.data()))) { programDataW.resize(wcslen(programDataW.c_str())); roots.push_back(std::filesystem::path(programDataW) / L"Sunshine"); } } for (const auto &root : executable_config_search_roots()) { if (!root.empty()) { roots.push_back(root); } } // De-duplicate while preserving order. std::vector uniq; for (const auto &root : roots) { if (root.empty()) { continue; } if (std::find(uniq.begin(), uniq.end(), root) == uniq.end()) { uniq.push_back(root); } } return uniq; } bool create_restore_scheduled_task() { BOOST_LOG(info) << "Attempting to create scheduled task 'ArtLightDisplayRestore'..."; const DWORD active_session_id = WTSGetActiveConsoleSessionId(); HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); if (FAILED(hr)) { BOOST_LOG(error) << "Failed to initialize COM for Task Scheduler: 0x" << std::hex << hr; return false; } ITaskService *service = nullptr; hr = CoCreateInstance(CLSID_TaskScheduler, nullptr, CLSCTX_INPROC_SERVER, IID_ITaskService, (void **) &service); if (FAILED(hr)) { BOOST_LOG(error) << "Failed to create Task Scheduler service instance: 0x" << std::hex << hr; CoUninitialize(); return false; } hr = service->Connect(_variant_t(), _variant_t(), _variant_t(), _variant_t()); if (FAILED(hr)) { BOOST_LOG(error) << "Failed to connect to Task Scheduler service: 0x" << std::hex << hr; service->Release(); CoUninitialize(); return false; } ITaskFolder *root_folder = nullptr; hr = service->GetFolder(_bstr_t(L"\\"), &root_folder); if (FAILED(hr)) { BOOST_LOG(error) << "Failed to get root task folder: 0x" << std::hex << hr; service->Release(); CoUninitialize(); return false; } ITaskDefinition *task = nullptr; hr = service->NewTask(0, &task); if (FAILED(hr)) { BOOST_LOG(error) << "Failed to create new task definition: 0x" << std::hex << hr; root_folder->Release(); service->Release(); CoUninitialize(); return false; } IRegistrationInfo *reg_info = nullptr; hr = task->get_RegistrationInfo(®_info); if (SUCCEEDED(hr)) { reg_info->put_Author(_bstr_t(L"ArtLight Display Helper")); reg_info->put_Description(_bstr_t(L"Automatically restores display settings after reboot")); reg_info->Release(); } ITaskSettings *settings = nullptr; hr = task->get_Settings(&settings); if (SUCCEEDED(hr)) { settings->put_StartWhenAvailable(VARIANT_TRUE); settings->put_DisallowStartIfOnBatteries(VARIANT_FALSE); settings->put_StopIfGoingOnBatteries(VARIANT_FALSE); settings->put_ExecutionTimeLimit(_bstr_t(L"PT0S")); settings->put_Hidden(VARIANT_TRUE); settings->Release(); } std::wstring username = query_session_account(active_session_id); if (username.empty()) { DWORD sam_required = 0; if (!GetUserNameExW(NameSamCompatible, nullptr, &sam_required) && GetLastError() == ERROR_MORE_DATA && sam_required > 0) { std::wstring sam_name; sam_name.resize(sam_required); DWORD sam_size = sam_required; if (GetUserNameExW(NameSamCompatible, sam_name.data(), &sam_size)) { sam_name.resize(sam_size); username = std::move(sam_name); } } } if (username.empty()) { wchar_t fallback[UNLEN + 1] = {0}; DWORD fallback_len = UNLEN + 1; if (GetUserNameW(fallback, &fallback_len) && fallback_len > 0) { username.assign(fallback); } } bool has_username = !username.empty(); if (has_username) { if (_wcsicmp(username.c_str(), L"SYSTEM") == 0 || _wcsicmp(username.c_str(), L"NT AUTHORITY\\SYSTEM") == 0) { BOOST_LOG(warning) << "Resolved session identity is SYSTEM; skipping per-user task registration"; has_username = false; } } else { BOOST_LOG(warning) << "Failed to get current username, using empty user for task"; } if (!has_username) { BOOST_LOG(info) << "No interactive user available; skipping scheduled task creation (will retry when user logs in)."; task->Release(); root_folder->Release(); service->Release(); CoUninitialize(); return true; } const std::wstring task_name = build_restore_task_name(has_username ? username : std::wstring {}); wchar_t exe_path[MAX_PATH]; if (!GetModuleFileNameW(nullptr, exe_path, MAX_PATH)) { BOOST_LOG(error) << "Failed to get current executable path"; task->Release(); root_folder->Release(); service->Release(); CoUninitialize(); return false; } ITriggerCollection *trigger_collection = nullptr; hr = task->get_Triggers(&trigger_collection); if (FAILED(hr)) { BOOST_LOG(error) << "Failed to get trigger collection: " << std::hex << hr; task->Release(); root_folder->Release(); service->Release(); CoUninitialize(); return false; } ITrigger *trigger = nullptr; hr = trigger_collection->Create(TASK_TRIGGER_LOGON, &trigger); trigger_collection->Release(); if (FAILED(hr)) { BOOST_LOG(error) << "Failed to create logon trigger: " << std::hex << hr; task->Release(); root_folder->Release(); service->Release(); CoUninitialize(); return false; } ILogonTrigger *logon_trigger = nullptr; hr = trigger->QueryInterface(IID_ILogonTrigger, (void **) &logon_trigger); trigger->Release(); if (SUCCEEDED(hr)) { logon_trigger->put_Id(_bstr_t(L"ArtLightDisplayHelperLogonTrigger")); logon_trigger->put_Enabled(VARIANT_TRUE); if (has_username) { logon_trigger->put_UserId(_bstr_t(username.c_str())); } logon_trigger->Release(); } IActionCollection *action_collection = nullptr; hr = task->get_Actions(&action_collection); if (FAILED(hr)) { BOOST_LOG(error) << "Failed to get action collection: " << std::hex << hr; task->Release(); root_folder->Release(); service->Release(); CoUninitialize(); return false; } IAction *action = nullptr; hr = action_collection->Create(TASK_ACTION_EXEC, &action); action_collection->Release(); if (FAILED(hr)) { BOOST_LOG(error) << "Failed to create exec action: " << std::hex << hr; task->Release(); root_folder->Release(); service->Release(); CoUninitialize(); return false; } IExecAction *exec_action = nullptr; hr = action->QueryInterface(IID_IExecAction, (void **) &exec_action); action->Release(); if (FAILED(hr)) { BOOST_LOG(error) << "Failed to query IExecAction interface: " << std::hex << hr; task->Release(); root_folder->Release(); service->Release(); CoUninitialize(); return false; } exec_action->put_Path(_bstr_t(exe_path)); exec_action->put_Arguments(_bstr_t(L"--restore")); exec_action->Release(); // Without an interactive user (helper launched as SYSTEM after sign-out or // from the lock screen), TASK_LOGON_INTERACTIVE_TOKEN cannot be resolved and // registration fails with 0x80070534 (ERROR_NONE_MAPPED). Bind the task to // BUILTIN\Users by SID (locale independent) so it still runs at next logon. static constexpr wchar_t builtin_users_sid[] = L"S-1-5-32-545"; const TASK_LOGON_TYPE logon_type = has_username ? TASK_LOGON_INTERACTIVE_TOKEN : TASK_LOGON_GROUP; IPrincipal *principal = nullptr; hr = task->get_Principal(&principal); if (SUCCEEDED(hr)) { if (!has_username) { principal->put_GroupId(_bstr_t(builtin_users_sid)); } principal->put_LogonType(logon_type); principal->put_RunLevel(TASK_RUNLEVEL_LUA); principal->Release(); } IRegisteredTask *registered_task = nullptr; HRESULT registration_hr = root_folder->RegisterTaskDefinition( _bstr_t(task_name.c_str()), task, TASK_CREATE_OR_UPDATE, has_username ? _variant_t() : _variant_t(builtin_users_sid), _variant_t(), logon_type, _variant_t(L""), ®istered_task ); if (registered_task) { registered_task->Release(); } task->Release(); root_folder->Release(); service->Release(); if (FAILED(registration_hr)) { BOOST_LOG(error) << "Failed to register scheduled task: " << std::hex << registration_hr; CoUninitialize(); return false; } BOOST_LOG(info) << "Successfully created scheduled task '" << std::string(task_name.begin(), task_name.end()) << "'"; CoUninitialize(); return true; } bool delete_restore_scheduled_task() { BOOST_LOG(info) << "Attempting to delete restore helper scheduled tasks"; HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); if (FAILED(hr)) { BOOST_LOG(error) << "Failed to initialize COM for Task Scheduler deletion: 0x" << std::hex << hr; return false; } ITaskService *service = nullptr; hr = CoCreateInstance(CLSID_TaskScheduler, nullptr, CLSCTX_INPROC_SERVER, IID_ITaskService, (void **) &service); if (FAILED(hr)) { BOOST_LOG(error) << "Failed to create Task Scheduler service instance for deletion: 0x" << std::hex << hr; CoUninitialize(); return false; } hr = service->Connect(_variant_t(), _variant_t(), _variant_t(), _variant_t()); if (FAILED(hr)) { BOOST_LOG(error) << "Failed to connect to Task Scheduler service for deletion: 0x" << std::hex << hr; service->Release(); CoUninitialize(); return false; } ITaskFolder *root_folder = nullptr; hr = service->GetFolder(_bstr_t(L"\\"), &root_folder); if (FAILED(hr)) { BOOST_LOG(error) << "Failed to get root task folder for deletion: " << std::hex << hr; service->Release(); CoUninitialize(); return false; } const DWORD active_session_id = WTSGetActiveConsoleSessionId(); std::wstring username = query_session_account(active_session_id); if (username.empty()) { DWORD sam_required = 0; if (!GetUserNameExW(NameSamCompatible, nullptr, &sam_required) && GetLastError() == ERROR_MORE_DATA && sam_required > 0) { std::wstring sam_name; sam_name.resize(sam_required); DWORD sam_size = sam_required; if (GetUserNameExW(NameSamCompatible, sam_name.data(), &sam_size)) { sam_name.resize(sam_size); username = std::move(sam_name); } } } if (username.empty()) { wchar_t fallback[UNLEN + 1] = {0}; DWORD fallback_len = UNLEN + 1; if (GetUserNameW(fallback, &fallback_len) && fallback_len > 0) { username.assign(fallback); } } std::vector task_names; task_names.push_back(build_restore_task_name({})); if (!username.empty()) { if (_wcsicmp(username.c_str(), L"SYSTEM") != 0 && _wcsicmp(username.c_str(), L"NT AUTHORITY\\SYSTEM") != 0) { task_names.push_back(build_restore_task_name(username)); } } bool success = true; for (const auto &name : task_names) { const HRESULT delete_hr = root_folder->DeleteTask(_bstr_t(name.c_str()), 0); if (SUCCEEDED(delete_hr)) { BOOST_LOG(info) << "Removed scheduled task '" << std::string(name.begin(), name.end()) << "'"; continue; } if (delete_hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)) { BOOST_LOG(debug) << "Scheduled task '" << std::string(name.begin(), name.end()) << "' not found"; continue; } BOOST_LOG(error) << "Failed to delete scheduled task '" << std::string(name.begin(), name.end()) << "': 0x" << std::hex << delete_hr; success = false; } root_folder->Release(); service->Release(); CoUninitialize(); return success; } void hide_console_window() { HWND console = GetConsoleWindow(); if (console) { ShowWindow(console, SW_HIDE); } } bool validate_session_snapshot(ServiceState &state, const std::filesystem::path &path) { std::error_code ec; if (!std::filesystem::exists(path, ec) || ec) { return false; } if (state.controller.snapshot_file_has_restore_payload(path)) { return true; } BOOST_LOG(warning) << "Existing session snapshot is missing restore topology/mode data; removing path=" << path.string(); { std::error_code ec_rm; std::filesystem::remove(path, ec_rm); (void) ec_rm; } return false; } std::vector parse_snapshot_exclude_json_node(const nlohmann::json &node) { std::vector ids; const nlohmann::json *arr = &node; nlohmann::json nested; if (node.is_object()) { if (node.contains("exclude_devices")) { nested = node["exclude_devices"]; arr = &nested; } else if (node.contains("devices")) { nested = node["devices"]; arr = &nested; } } if (!arr->is_array()) { return ids; } for (const auto &el : *arr) { if (el.is_string()) { ids.push_back(el.get()); } else if (el.is_object()) { if (el.contains("device_id") && el["device_id"].is_string()) { ids.push_back(el["device_id"].get()); } else if (el.contains("id") && el["id"].is_string()) { ids.push_back(el["id"].get()); } } } return ids; } std::optional> parse_snapshot_exclude_payload(std::span payload) { if (payload.empty()) { return std::nullopt; } try { std::string raw(reinterpret_cast(payload.data()), payload.size()); if (raw.empty()) { return std::vector {}; } auto j = nlohmann::json::parse(raw, nullptr, false); if (j.is_discarded()) { return std::nullopt; } return parse_snapshot_exclude_json_node(j); } catch (...) { return std::nullopt; } } struct RevertOptions { bool prefer_golden_if_current_missing {true}; std::optional always_restore_from_golden; }; RevertOptions parse_revert_payload(std::span payload) { RevertOptions options; if (payload.empty()) { return options; } try { std::string raw(reinterpret_cast(payload.data()), payload.size()); auto j = nlohmann::json::parse(raw, nullptr, false); if (!j.is_object()) { return options; } auto it = j.find("sunshine_prefer_golden_if_current_missing"); if (it != j.end() && it->is_boolean()) { options.prefer_golden_if_current_missing = it->get(); } it = j.find("sunshine_always_restore_from_golden"); if (it != j.end() && it->is_boolean()) { options.always_restore_from_golden = it->get(); } } catch (...) { } return options; } /** * @brief Load snapshot exclusion devices from vibeshine_state.json. * * This reads the exclusion list that Sunshine persists to the state file, * allowing the display helper to know which devices to exclude without * depending on IPC from Sunshine. * * @param path Path to vibeshine_state.json * @param ids_out Output vector for device IDs * @return true if loaded successfully, false otherwise */ bool load_vibeshine_snapshot_exclusions(const std::filesystem::path &path, std::vector &ids_out) { ids_out.clear(); if (path.empty()) { return false; } std::error_code ec; if (!std::filesystem::exists(path, ec) || ec) { return false; } try { FILE *f = _wfopen(path.wstring().c_str(), L"rb"); if (!f) { return false; } auto guard = std::unique_ptr(f, fclose); std::string data; char buf[4096]; while (size_t n = fread(buf, 1, sizeof(buf), f)) { data.append(buf, n); } auto j = nlohmann::json::parse(data, nullptr, false); if (j.is_discarded()) { return false; } // vibeshine_state.json format: // { "root": { "snapshot_exclude_devices": [...], "virtual_display_devices": [...] } } // Sunshine-managed virtual display ids are merged into the exclusions so they are // never captured into (or restored from) display baselines. if (j.is_object() && j.contains("root")) { const auto &root = j["root"]; if (!root.is_object()) { return false; } bool found = false; if (root.contains("snapshot_exclude_devices")) { ids_out = parse_snapshot_exclude_json_node(root["snapshot_exclude_devices"]); found = !ids_out.empty() || root["snapshot_exclude_devices"].is_array(); } if (root.contains("virtual_display_devices")) { auto virtual_ids = parse_snapshot_exclude_json_node(root["virtual_display_devices"]); for (auto &id : virtual_ids) { if (std::find(ids_out.begin(), ids_out.end(), id) == ids_out.end()) { ids_out.push_back(std::move(id)); } } found = found || !ids_out.empty(); } return found; } } catch (const std::exception &e) { BOOST_LOG(warning) << "Failed to parse vibeshine_state.json for snapshot exclusions: " << e.what(); } catch (...) { } return false; } bool handle_apply(ServiceState &state, std::span payload, std::string &error_msg) { // Cancel any ongoing restore activity since a new APPLY supersedes it state.stop_restore_polling(); state.cancel_delayed_reapply(); state.cancel_post_apply_tasks(); state.refresh_rate_override.store(0, std::memory_order_release); state.exit_after_revert.store(false, std::memory_order_release); std::string json(reinterpret_cast(payload.data()), payload.size()); bool wa_hdr_toggle = false; std::optional requested_virtual_layout; std::vector> monitor_position_overrides; std::vector>> refresh_rate_overrides; std::optional sunshine_topology; std::optional> snapshot_exclude_devices; std::string sanitized_json = json; try { auto j = nlohmann::json::parse(json); if (j.is_object()) { // v2 capability detection adds a backward-compatible correlation // token to every APPLY. The legacy engine has no asynchronous // verification phase, so discard it before deserializing the public // display configuration and retain the original untagged response. j.erase("sunshine_apply_id"); // The legacy helper completes APPLY synchronously and does not have the // v2 initial-repair ladder controlled by this private metadata. j.erase("sunshine_omit_final_initial_hdr_reapply"); if (j.contains("wa_hdr_toggle")) { wa_hdr_toggle = j["wa_hdr_toggle"].get(); j.erase("wa_hdr_toggle"); } if (j.contains("sunshine_virtual_layout") && j["sunshine_virtual_layout"].is_string()) { requested_virtual_layout = j["sunshine_virtual_layout"].get(); j.erase("sunshine_virtual_layout"); } if (j.contains("sunshine_monitor_positions") && j["sunshine_monitor_positions"].is_object()) { for (auto it = j["sunshine_monitor_positions"].begin(); it != j["sunshine_monitor_positions"].end(); ++it) { const auto &node = it.value(); if (!node.is_object()) { continue; } auto x_it = node.find("x"); auto y_it = node.find("y"); if (x_it == node.end() || y_it == node.end() || !x_it->is_number_integer() || !y_it->is_number_integer()) { continue; } monitor_position_overrides.emplace_back( it.key(), display_device::Point {x_it->get(), y_it->get()} ); } j.erase("sunshine_monitor_positions"); } if (j.contains("sunshine_snapshot_exclude_devices")) { snapshot_exclude_devices = parse_snapshot_exclude_json_node(j["sunshine_snapshot_exclude_devices"]); j.erase("sunshine_snapshot_exclude_devices"); } if (j.contains("sunshine_topology") && j["sunshine_topology"].is_array()) { display_device::ActiveTopology topo; for (const auto &grp_node : j["sunshine_topology"]) { if (!grp_node.is_array()) { continue; } std::vector grp; for (const auto &id_node : grp_node) { if (!id_node.is_string()) { continue; } grp.push_back(id_node.get()); } if (!grp.empty()) { topo.push_back(std::move(grp)); } } if (!topo.empty()) { sunshine_topology = std::move(topo); } j.erase("sunshine_topology"); } if (j.contains("sunshine_always_restore_from_golden") && j["sunshine_always_restore_from_golden"].is_boolean()) { state.always_restore_from_golden.store(j["sunshine_always_restore_from_golden"].get(), std::memory_order_release); j.erase("sunshine_always_restore_from_golden"); } if (j.contains("sunshine_restore_on_disconnect") && j["sunshine_restore_on_disconnect"].is_boolean()) { state.restore_on_disconnect.store(j["sunshine_restore_on_disconnect"].get(), std::memory_order_release); j.erase("sunshine_restore_on_disconnect"); } else { state.restore_on_disconnect.store(true, std::memory_order_release); } if (j.contains("sunshine_device_refresh_rate_overrides") && j["sunshine_device_refresh_rate_overrides"].is_object()) { for (auto it = j["sunshine_device_refresh_rate_overrides"].begin(); it != j["sunshine_device_refresh_rate_overrides"].end(); ++it) { const auto &node = it.value(); if (!node.is_object()) continue; auto num_it = node.find("num"); auto den_it = node.find("den"); if (num_it == node.end() || den_it == node.end() || !num_it->is_number_unsigned() || !den_it->is_number_unsigned()) continue; refresh_rate_overrides.emplace_back( it.key(), std::make_pair(num_it->get(), den_it->get()) ); } j.erase("sunshine_device_refresh_rate_overrides"); } sanitized_json = j.dump(); } } catch (...) { } if (snapshot_exclude_devices.has_value()) { state.controller.set_snapshot_exclusions(*snapshot_exclude_devices); } display_device::SingleDisplayConfiguration cfg {}; std::string err; if (!display_device::fromJson(sanitized_json, cfg, &err)) { BOOST_LOG(error) << "Failed to parse SingleDisplayConfiguration JSON: " << err; error_msg = "Invalid display configuration payload"; return false; } state.last_apply_ms.store( std::chrono::duration_cast( std::chrono::steady_clock::now().time_since_epoch() ) .count(), std::memory_order_release ); state.last_cfg = cfg; // The session baseline is normally captured earlier via SnapshotCurrent, before any // display enumeration that might activate external dummy plugs. That request is // fire-and-forget and can be lost (helper hard-restart races, helper not yet running), // which used to leave REVERT with nothing to restore and strand the user on the // session-only display layout (vibeshine#223). Capture the pre-apply state here as a // fallback whenever no baseline exists yet; the snapshot exclusions set above keep // virtual displays out of it. if (!ServiceState::path_exists(state.session_current_path)) { BOOST_LOG(warning) << "Display helper: no session baseline present at APPLY; capturing pre-apply baseline now."; if (!state.capture_current_snapshot("pre-apply baseline")) { BOOST_LOG(warning) << "Display helper: pre-apply baseline capture failed; REVERT may have nothing to restore."; } } state.retry_revert_on_topology.store(false, std::memory_order_release); state.exit_after_revert.store(false, std::memory_order_release); bool validated = state.controller.soft_test_display_settings(cfg, sunshine_topology); if (!validated) { BOOST_LOG(warning) << "Display helper: configuration failed SDC_VALIDATE soft-test; attempting display stack recovery and retrying once."; if (state.controller.recover_display_stack()) { std::this_thread::sleep_for(500ms); validated = state.controller.soft_test_display_settings(cfg, sunshine_topology); } } if (validated) { BOOST_LOG(info) << "Display configuration validated, creating scheduled task before applying settings"; const bool task_created = create_restore_scheduled_task(); BOOST_LOG(info) << "Scheduled task creation result: " << (task_created ? "SUCCESS" : "FAILED"); if (!state.controller.apply(cfg, sunshine_topology)) { error_msg = "Helper failed to apply requested display configuration"; return false; } constexpr int kMaxSyncVerifyAttempts = 2; bool verified_sync = false; std::vector reapply_delays {750ms}; if (cfg.m_hdr_state) { // HDR state can be changed asynchronously by Windows shortly after topology/mode changes, // especially for virtual displays. Schedule a few extra best-effort re-apply attempts to // enforce either HDR enablement or the SDR request's HDR disablement. reapply_delays = {750ms, 2500ms, 5500ms}; } for (int attempt = 1; attempt <= kMaxSyncVerifyAttempts; ++attempt) { if (state.verify_last_configuration_sticky(ServiceState::kVerificationSettleDelay)) { verified_sync = true; if (attempt > 1) { BOOST_LOG(info) << "Display helper: verification succeeded on attempt #" << attempt << " after re-apply."; } break; } BOOST_LOG(warning) << "Display helper: verification attempt #" << attempt << " did not stick; " << (attempt < kMaxSyncVerifyAttempts ? "retrying synchronously." : "deferring to async retry."); state.best_effort_apply_last_cfg(); } if (verified_sync) { BOOST_LOG(debug) << "Display helper: synchronous verification succeeded; scheduling follow-up check."; } else { BOOST_LOG(warning) << "Display helper: synchronous verification failed; scheduling async fallback."; } state.retry_apply_on_topology.store(false, std::memory_order_release); state.schedule_post_apply_tasks( false, std::nullopt, wa_hdr_toggle, requested_virtual_layout, std::move(monitor_position_overrides), std::move(refresh_rate_overrides), std::move(reapply_delays) ); } else { BOOST_LOG(error) << "Display helper: configuration failed SDC_VALIDATE soft-test; not applying."; error_msg = "Display configuration failed validation"; return false; } error_msg.clear(); return true; } void handle_revert(ServiceState &state, std::atomic &running, std::span payload) { const auto revert_options = parse_revert_payload(payload); BOOST_LOG(info) << "REVERT command received - initiating display settings restoration" << (revert_options.prefer_golden_if_current_missing ? " (prefer golden if current missing)." : "."); state.retry_apply_on_topology.store(false, std::memory_order_release); state.cancel_delayed_reapply(); state.cancel_post_apply_tasks(); state.direct_revert_bypass_grace.store(true, std::memory_order_release); state.exit_after_revert.store(true, std::memory_order_release); state.reset_golden_restore_request_tracking(); state.restore_requested.store(true, std::memory_order_release); if (revert_options.always_restore_from_golden.has_value()) { state.always_restore_from_golden.store(*revert_options.always_restore_from_golden, std::memory_order_release); } state.prefer_golden_if_current_missing.store(revert_options.prefer_golden_if_current_missing, std::memory_order_release); state.restore_origin_epoch.store(state.current_connection_epoch(), std::memory_order_release); // Give Sunshine a short window to immediately start a new session and DISARM, // avoiding costly restore/apply thrash during fast client switching. state.arm_restore_grace(5000ms, "revert"); state.ensure_restore_polling(ServiceState::RestoreWindow::Primary); } void handle_misc(ServiceState &state, platf::dxgi::AsyncNamedPipe &async_pipe, MsgType type, std::span payload) { if (auto exclusions = parse_snapshot_exclude_payload(payload)) { state.controller.set_snapshot_exclusions(*exclusions); } if (type == MsgType::ExportGolden) { const bool saved = state.save_snapshot_with_retry(state.golden_path, "export-golden"); if (saved) { state.clear_golden_restore_status("snapshot exported"); } BOOST_LOG(info) << "Export golden restore snapshot result=" << (saved ? "true" : "false"); } else if (type == MsgType::Reset) { (void) state.controller.reset_persistence(); state.retry_apply_on_topology.store(false, std::memory_order_release); state.retry_revert_on_topology.store(false, std::memory_order_release); } else if (type == MsgType::Disarm) { const bool force = !payload.empty() && payload.front() != 0; if (!force && state.restore_requested.load(std::memory_order_acquire) && state.restore_attempted_unconfirmed.load(std::memory_order_acquire)) { BOOST_LOG(info) << "DISARM command ignored because an unconfirmed restore attempt is still pending."; return; } state.disarm_restore_requests("DISARM command received"); } else if (type == MsgType::SnapshotCurrent) { if (state.restore_requested.load(std::memory_order_acquire)) { BOOST_LOG(info) << "Skipping current session snapshot refresh while restore is pending."; return; } (void) state.refresh_current_snapshot_preserving_previous("snapshot-only"); } else if (type == MsgType::RefreshRate) { const auto numerator = read_u32_le(payload, 0); const auto denominator = read_u32_le(payload, 4); std::string device_id; if (payload.size() > 8) { device_id.assign( reinterpret_cast(payload.data() + 8), payload.size() - 8 ); } const bool valid = numerator && denominator && *numerator > 0 && *denominator > 0 && !device_id.empty(); const bool overrides_configured_rate = valid && state.last_cfg && _stricmp(state.last_cfg->m_device_id.c_str(), device_id.c_str()) == 0; if (overrides_configured_rate) { // Stop any in-flight verifier before changing ownership, then make its // future comparisons and re-applies use the adaptive refresh rate. const auto packed_rate = (static_cast(*numerator) << 32u) | *denominator; state.refresh_rate_override.store(packed_rate, std::memory_order_release); state.cancel_delayed_reapply(); } const bool success = valid && state.controller.set_device_refresh_rate(device_id, *numerator, *denominator); if (overrides_configured_rate && !success) { state.refresh_rate_override.store(0, std::memory_order_release); } if (overrides_configured_rate) { std::vector reapply_delays {750ms}; if (state.last_cfg && state.last_cfg->m_hdr_state) { reapply_delays = {750ms, 2500ms, 5500ms}; } state.schedule_delayed_reapply(std::move(reapply_delays)); } BOOST_LOG(success ? info : warning) << "Display helper: refresh-only request device=" << (device_id.empty() ? "(missing)" : device_id) << " rate=" << (numerator ? std::to_string(*numerator) : "invalid") << '/' << (denominator ? std::to_string(*denominator) : "invalid") << " result=" << (success ? "true" : "false"); std::array result {static_cast(success ? 1u : 0u)}; send_framed_content(async_pipe, MsgType::RefreshRateResult, result); } else if (type == MsgType::Ping) { state.record_heartbeat_ping(); send_framed_content(async_pipe, MsgType::Ping); } else { BOOST_LOG(warning) << "Unknown message type: " << static_cast(type); } } void handle_frame(ServiceState &state, platf::dxgi::AsyncNamedPipe &async_pipe, MsgType type, std::span payload, std::atomic &running) { if (type == MsgType::Apply) { std::string error_msg; bool success = handle_apply(state, payload, error_msg); std::vector result_payload; result_payload.push_back(success ? 1u : 0u); if (!error_msg.empty()) { const auto *begin = reinterpret_cast(error_msg.data()); result_payload.insert(result_payload.end(), begin, begin + error_msg.size()); } send_framed_content(async_pipe, MsgType::ApplyResult, result_payload); } else if (type == MsgType::Revert) { handle_revert(state, running, payload); } else if (type == MsgType::Stop) { running.store(false, std::memory_order_release); } else { handle_misc(state, async_pipe, type, payload); } } void attempt_revert_after_disconnect(ServiceState &state, std::atomic &running, uint64_t connection_epoch) { if (!state.is_connection_epoch_current(connection_epoch)) { BOOST_LOG(info) << "Ignoring disconnect event from stale connection (epoch=" << connection_epoch << ", current=" << state.current_connection_epoch() << ")"; return; } auto still_current = [&]() { return state.is_connection_epoch_current(connection_epoch); }; // Pipe broken -> Sunshine might have crashed. Begin autonomous restore. state.retry_apply_on_topology.store(false, std::memory_order_release); state.cancel_delayed_reapply(); const bool potentially_modified = state.last_cfg.has_value() || state.exit_after_revert.load(std::memory_order_acquire); if (!potentially_modified) { state.restore_requested.store(false, std::memory_order_release); state.arm_reconnect_exit_grace("nothing to restore"); return; } const bool explicit_restore_pending = state.exit_after_revert.load(std::memory_order_acquire) || state.direct_revert_bypass_grace.load(std::memory_order_acquire) || state.restore_requested.load(std::memory_order_acquire); if (!state.restore_on_disconnect.load(std::memory_order_acquire) && !explicit_restore_pending) { BOOST_LOG(info) << "Client disconnected with restore-on-disconnect disabled; disarming restore state."; state.disarm_restore_requests("Restore-on-disconnect disabled after client disconnect"); state.arm_reconnect_exit_grace("restore-on-disconnect disabled"); return; } if (!state.direct_revert_bypass_grace.load(std::memory_order_acquire)) { const auto now_ms = std::chrono::duration_cast( std::chrono::steady_clock::now().time_since_epoch() ) .count(); const auto last_apply = state.last_apply_ms.load(std::memory_order_acquire); if (last_apply > 0 && now_ms >= last_apply) { const auto delta_ms = now_ms - last_apply; if (delta_ms <= kApplyDisconnectGrace.count()) { BOOST_LOG(info) << "Client disconnected " << delta_ms << "ms after APPLY; deferring restore to avoid thrash."; state.schedule_delayed_reapply(); state.restore_requested.store(false, std::memory_order_release); return; } } } if (!still_current()) { BOOST_LOG(info) << "Skipping restore after disconnect because a newer connection is active (epoch=" << connection_epoch << ", current=" << state.current_connection_epoch() << ")"; return; } BOOST_LOG(info) << "Client disconnected; entering restore polling loop (3s interval) until successful."; state.exit_after_revert.store(true, std::memory_order_release); state.reset_golden_restore_request_tracking(); state.restore_requested.store(true, std::memory_order_release); state.restore_origin_epoch.store(connection_epoch, std::memory_order_release); state.arm_restore_grace(5000ms, "disconnect"); state.ensure_restore_polling(ServiceState::RestoreWindow::Primary); } void process_incoming_frame(ServiceState &state, platf::dxgi::AsyncNamedPipe &async_pipe, std::span frame, std::atomic &running) { if (frame.empty()) { return; } MsgType type {}; std::span payload; if (frame.size() >= 5) { uint32_t len = 0; std::memcpy(&len, frame.data(), 4); if (len > 0 && frame.size() >= 4u + len) { type = static_cast(frame[4]); if (len > 1) { payload = std::span(frame.data() + 5, len - 1); } else { payload = {}; } } else { type = static_cast(frame[0]); payload = frame.subspan(1); } } else { type = static_cast(frame[0]); payload = frame.subspan(1); } handle_frame(state, async_pipe, type, payload, running); } } // namespace int run_legacy_helper(int argc, char *argv[]) { bool restore_mode = false; if (argc > 1) { for (int i = 1; i < argc; ++i) { if (std::strcmp(argv[i], "--restore") == 0) { restore_mode = true; } else if (std::strcmp(argv[i], "--no-startup-restore") == 0) { BOOST_LOG(info) << "--no-startup-restore is deprecated and ignored."; } } } if (restore_mode) { FreeConsole(); hide_console_window(); } HANDLE singleton = nullptr; if (!ensure_single_instance(singleton)) { return 3; } const auto logdir = compute_log_dir(); const auto snapshot_dir = compute_snapshot_dir(); const auto logfile = (logdir / L"sunshine_display_helper.log"); const auto active_snapshots = make_snapshot_paths(snapshot_dir); const auto search_roots = snapshot_search_roots(); auto _log_guard = logging::init(2 /*info*/, logfile); if (restore_mode) { BOOST_LOG(info) << "Display helper started in restore mode (--restore flag)"; dd_log_bridge().install(); ServiceState state; state.golden_path = active_snapshots.golden; state.golden_status_path = active_snapshots.golden_status; state.session_current_path = active_snapshots.session_current; state.session_previous_path = active_snapshots.session_previous; { // Load snapshot exclusions from vibeshine_state.json (source of truth from Sunshine). std::vector persisted; for (const auto &root : search_roots) { const auto vibeshine_state_file = root / L"vibeshine_state.json"; if (load_vibeshine_snapshot_exclusions(vibeshine_state_file, persisted)) { BOOST_LOG(info) << "Loaded snapshot exclusions from vibeshine_state.json (" << persisted.size() << ") at " << vibeshine_state_file.string(); state.controller.set_snapshot_exclusions(persisted); break; } } } { for (const auto &root : search_roots) { auto paths = make_snapshot_paths(root); std::error_code ec_cur; const bool cur_exists = std::filesystem::exists(paths.session_current, ec_cur); if (cur_exists && !ec_cur) { if (validate_session_snapshot(state, paths.session_current)) { state.session_saved.store(true, std::memory_order_release); BOOST_LOG(info) << "Existing current session snapshot detected; will preserve until confirmed restore: " << paths.session_current.string(); if (paths.session_current != state.session_current_path) { std::error_code ec_copy; std::filesystem::create_directories(state.session_current_path.parent_path(), ec_copy); std::filesystem::copy_file(paths.session_current, state.session_current_path, std::filesystem::copy_options::overwrite_existing, ec_copy); } break; } } } } { for (const auto &root : search_roots) { auto paths = make_snapshot_paths(root); std::error_code ec_prev_check; if (std::filesystem::exists(paths.session_previous, ec_prev_check) && !ec_prev_check) { if (validate_session_snapshot(state, paths.session_previous)) { if (paths.session_previous != state.session_previous_path) { std::error_code ec_copy; std::filesystem::create_directories(state.session_previous_path.parent_path(), ec_copy); std::filesystem::copy_file(paths.session_previous, state.session_previous_path, std::filesystem::copy_options::overwrite_existing, ec_copy); } break; } } } } std::atomic running {true}; state.running_flag = &running; state.exit_after_revert.store(true, std::memory_order_release); state.reset_golden_restore_request_tracking(); state.restore_requested.store(true, std::memory_order_release); state.restore_origin_epoch.store(0, std::memory_order_release); state.ensure_restore_polling(ServiceState::RestoreWindow::Primary); while (running.load(std::memory_order_acquire)) { std::this_thread::sleep_for(500ms); } BOOST_LOG(info) << "Display helper restore mode completed; shutting down"; logging::log_flush(); return 0; } platf::dxgi::FramedPipeFactory pipe_factory(std::make_unique()); dd_log_bridge().install(); ServiceState state; // Suppression of startup restore is deprecated; REVERTs are always allowed. state.golden_path = active_snapshots.golden; state.golden_status_path = active_snapshots.golden_status; state.session_current_path = active_snapshots.session_current; state.session_previous_path = active_snapshots.session_previous; { // Load snapshot exclusions from vibeshine_state.json (source of truth from Sunshine). std::vector persisted; for (const auto &root : search_roots) { const auto vibeshine_state_file = root / L"vibeshine_state.json"; if (load_vibeshine_snapshot_exclusions(vibeshine_state_file, persisted)) { BOOST_LOG(info) << "Loaded snapshot exclusions from vibeshine_state.json (" << persisted.size() << ") at " << vibeshine_state_file.string(); state.controller.set_snapshot_exclusions(persisted); break; } } } { for (const auto &root : search_roots) { auto paths = make_snapshot_paths(root); std::error_code ec_cur; const bool cur_exists = std::filesystem::exists(paths.session_current, ec_cur); if (cur_exists && !ec_cur) { if (validate_session_snapshot(state, paths.session_current)) { state.session_saved.store(true, std::memory_order_release); BOOST_LOG(info) << "Existing current session snapshot detected; will preserve until confirmed restore: " << paths.session_current.string(); if (paths.session_current != state.session_current_path) { std::error_code ec_copy; std::filesystem::create_directories(state.session_current_path.parent_path(), ec_copy); std::filesystem::copy_file(paths.session_current, state.session_current_path, std::filesystem::copy_options::overwrite_existing, ec_copy); } break; } } } } { for (const auto &root : search_roots) { auto paths = make_snapshot_paths(root); std::error_code ec_prev_check; if (std::filesystem::exists(paths.session_previous, ec_prev_check) && !ec_prev_check) { if (validate_session_snapshot(state, paths.session_previous)) { if (paths.session_previous != state.session_previous_path) { std::error_code ec_copy; std::filesystem::create_directories(state.session_previous_path.parent_path(), ec_copy); std::filesystem::copy_file(paths.session_previous, state.session_previous_path, std::filesystem::copy_options::overwrite_existing, ec_copy); } break; } } } } // Topology-based retries disabled; no watcher needed anymore. std::atomic running {true}; state.running_flag = &running; auto last_connect_wait_log = std::chrono::steady_clock::time_point::min(); constexpr auto kReconnectLogInterval = std::chrono::hours(1); // Outer service loop: keep accepting new client sessions while running while (running.load(std::memory_order_acquire)) { auto ctrl_pipe = pipe_factory.create_server("sunshine_display_helper"); if (!ctrl_pipe) { platf::dxgi::FramedPipeFactory fallback_factory(std::make_unique()); ctrl_pipe = fallback_factory.create_server("sunshine_display_helper"); if (!ctrl_pipe) { BOOST_LOG(error) << "Failed to create control pipe; retrying in 500ms"; std::this_thread::sleep_for(500ms); continue; } } platf::dxgi::AsyncNamedPipe async_pipe(std::move(ctrl_pipe)); // Wait for a client connection before starting the async worker thread. // // Without this, the code below would immediately reach the cleanup path // (async_pipe.is_connected() is false at startup), call async_pipe.stop(), // and tear down the server pipe before Sunshine has any chance to connect. async_pipe.wait_for_client_connection(15000); if (!async_pipe.is_connected()) { if (state.reconnect_exit_grace_expired()) { BOOST_LOG(info) << "No client reconnected within the post-disconnect grace window; shutting down."; break; } const auto now = std::chrono::steady_clock::now(); if (now - last_connect_wait_log > kReconnectLogInterval) { BOOST_LOG(info) << "Waiting for Sunshine to connect to display helper IPC..."; last_connect_wait_log = now; } continue; } state.disarm_reconnect_exit_grace(); const auto connection_epoch = state.begin_connection_epoch(); // Do not cancel restore polling merely because Sunshine connected. Stream start // often opens the helper first for SNAPSHOT_CURRENT/DISARM probes; cancelling // here can strand a prior, unconfirmed restore when a physical monitor is // present but its input is switched away. APPLY/DISARM handlers decide // explicitly whether a restore should be superseded. state.begin_heartbeat_monitoring(); // Reset and start per-connection command worker so IPC stays responsive even during heavy display work. state.command_worker_stop.store(true, std::memory_order_release); state.command_queue_cv.notify_all(); if (state.command_worker.joinable()) { state.command_worker.join(); } { std::lock_guard lg(state.command_queue_mutex); state.command_queue.clear(); } state.command_worker_stop.store(false, std::memory_order_release); state.command_worker_epoch.store(connection_epoch, std::memory_order_release); auto start_command_worker = [&](platf::dxgi::AsyncNamedPipe &pipe) { state.command_worker = std::jthread([&, connection_epoch](std::stop_token) { while (!state.command_worker_stop.load(std::memory_order_acquire) && running.load(std::memory_order_acquire) && state.is_connection_epoch_current(connection_epoch)) { std::vector next; { std::unique_lock lk(state.command_queue_mutex); state.command_queue_cv.wait(lk, [&]() { return state.command_worker_stop.load(std::memory_order_acquire) || !running.load(std::memory_order_acquire) || !state.command_queue.empty() || !state.is_connection_epoch_current(connection_epoch); }); if (state.command_worker_stop.load(std::memory_order_acquire) || !state.is_connection_epoch_current(connection_epoch) || !running.load(std::memory_order_acquire)) { break; } if (state.command_queue.empty()) { continue; } next = std::move(state.command_queue.front()); state.command_queue.pop_front(); } if (!next.empty()) { try { process_incoming_frame(state, pipe, next, running); } catch (const std::exception &ex) { BOOST_LOG(error) << "IPC framing error in command worker: " << ex.what(); } } } }); }; auto on_message = [&, connection_epoch](std::span bytes) { if (!state.is_connection_epoch_current(connection_epoch)) { return; } { std::lock_guard lg(state.command_queue_mutex); if (state.command_worker_epoch.load(std::memory_order_acquire) == connection_epoch) { state.command_queue.emplace_back(bytes.begin(), bytes.end()); } } state.command_queue_cv.notify_one(); }; // Track broken/disconnect events from the async worker thread without // attempting to stop/join from within the callback (which would deadlock). std::atomic broken {false}; auto on_error = [&, connection_epoch](const std::string &err) { if (!state.is_connection_epoch_current(connection_epoch)) { BOOST_LOG(info) << "Ignoring async pipe error from stale connection (epoch=" << connection_epoch << ", current=" << state.current_connection_epoch() << ")"; return; } BOOST_LOG(error) << "Async pipe error: " << err << "; handling disconnect and revert policy."; broken.store(true, std::memory_order_release); state.command_worker_stop.store(true, std::memory_order_release); state.command_queue_cv.notify_all(); attempt_revert_after_disconnect(state, running, connection_epoch); }; auto on_broken = [&, connection_epoch]() { if (!state.is_connection_epoch_current(connection_epoch)) { BOOST_LOG(info) << "Ignoring disconnect notification from stale connection (epoch=" << connection_epoch << ", current=" << state.current_connection_epoch() << ")"; return; } BOOST_LOG(warning) << "Client disconnected; applying revert policy and staying alive until successful."; broken.store(true, std::memory_order_release); state.command_worker_stop.store(true, std::memory_order_release); state.command_queue_cv.notify_all(); attempt_revert_after_disconnect(state, running, connection_epoch); }; // Start async message loop (establish_connection is a no-op if already connected) async_pipe.start(on_message, on_error, on_broken); start_command_worker(async_pipe); // Stay in this inner loop until the client disconnects or service told to exit while (running.load(std::memory_order_acquire) && async_pipe.is_connected() && !broken.load(std::memory_order_acquire)) { std::this_thread::sleep_for(200ms); if (state.check_heartbeat_timeout() && state.is_connection_epoch_current(connection_epoch)) { BOOST_LOG(warning) << "Heartbeat timeout exceeded; applying revert policy."; broken.store(true, std::memory_order_release); attempt_revert_after_disconnect(state, running, connection_epoch); break; } } // Ensure the worker thread is stopped and the server handle is // disconnected before looping to accept a new session. state.end_heartbeat_monitoring(); state.command_worker_stop.store(true, std::memory_order_release); state.command_queue_cv.notify_all(); if (state.command_worker.joinable()) { state.command_worker.join(); } { std::lock_guard lg(state.command_queue_mutex); state.command_queue.clear(); } async_pipe.stop(); // If a successful restore requested exit, break outer loop if (!running.load(std::memory_order_acquire)) { break; } // Otherwise, loop around to create a fresh pipe for the next session } BOOST_LOG(info) << "Display settings helper shutting down"; logging::log_flush(); return 0; } #endif