From b5fa878bc6aa3811e1a7a5a1a419db06fd1ab6b3 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Fri, 24 Jul 2026 00:27:49 +0200 Subject: [PATCH] fix(pf-win-display): follow the input desktop so a UAC prompt can't refuse display writes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Field-reported 2026-07-23: a consent prompt left up on an unattended host made the box unreachable. Every connect black-screened and failed after ~60 s, and the host had to be reached over SSH to clear the prompt. Windows refuses ChangeDisplaySettingsEx/SetDisplayConfig issued from a thread that is not on the desktop currently receiving input. While UAC (or the lock / logon screen) is up that desktop is Winlogon, and the host — which the service launches explicitly onto WinSta0\Default — got DISP_CHANGE_FAILED / ERROR_ACCESS_DENIED for every write. A session starting in that window could never set its virtual display's mode, so the capturer sized its ring to a stale mode, every composed frame was dropped for a size mismatch, and bring-up burned 8 retries before giving up. Measured on glass (RTX box, SYSTEM host in console session 2, real consent prompt up, virtual display active): INPUT desktop = Winlogon UNBOUND CDS_TEST -> -1 (DISP_CHANGE_FAILED) UNBOUND SDC_VALIDATE -> 0x5 (ERROR_ACCESS_DENIED) BOUND CDS_TEST -> 0 (DISP_CHANGE_SUCCESSFUL) BOUND SDC_VALIDATE -> 0x0 (ERROR_SUCCESS) New input_desktop.rs mirrors pf-inject's sendinput.rs retry model: issue the write, and only when it fails the way a wrong-desktop write fails, rebind this thread to the current input desktop and retry once. A working write is never touched, so the normal path is unchanged. The retry predicate stays narrow (ERROR_ACCESS_DENIED only) so the unrelated 0x57 exclusive-mode topology bug is not re-issued and mis-attributed. Unlike sendinput's dedicated injector thread, the binding here is SCOPED — a shared display-write thread left on a Winlogon desktop that is later destroyed would fail every subsequent write, which is the wedge this removes. Applied to all eight SetDisplayConfig sites and both ChangeDisplaySettingsExW calls in set_active_mode. The isolate site now decides its supplied config once, outside the write, so a retry re-issues the identical config rather than logging its escalation twice. A save is logged, because a silent one is indistinguishable in a field log from a write that never needed saving — which made the first on-glass verification of this change inconclusive. Both ACCESS_DENIED diagnostics now ask which cause applies instead of naming only the disconnected-RDP one: that phrasing sent this investigation chasing a phantom RDP session on a host that was already service-launched in the console session, while a consent prompt was the actual cause. Verified on glass: connect with a consent prompt up now reaches first frame in 3.0 s (was a black screen then "Connection failed" after ~60 s), with six "retried bound to it and it applied desktop=Winlogon" lines and rc=0x0 throughout. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/pf-win-display/Cargo.toml | 3 + crates/pf-win-display/src/input_desktop.rs | 200 +++++++++++++++++++++ crates/pf-win-display/src/lib.rs | 3 + crates/pf-win-display/src/win_display.rs | 190 ++++++++++++-------- 4 files changed, 326 insertions(+), 70 deletions(-) create mode 100644 crates/pf-win-display/src/input_desktop.rs diff --git a/crates/pf-win-display/Cargo.toml b/crates/pf-win-display/Cargo.toml index ac201f3b..ebcc6e3b 100644 --- a/crates/pf-win-display/Cargo.toml +++ b/crates/pf-win-display/Cargo.toml @@ -31,5 +31,8 @@ windows = { version = "0.62", features = [ "Win32_System_LibraryLoader", # console_session_mismatch: WTSGetActiveConsoleSessionId + ProcessIdToSessionId + GetCurrentProcessId. "Win32_System_RemoteDesktop", + # input_desktop: OpenInputDesktop/SetThreadDesktop/GetUserObjectInformationW — display writes + # follow the input desktop so a UAC/lock screen can't refuse them. + "Win32_System_StationsAndDesktops", "Win32_System_Threading", ] } diff --git a/crates/pf-win-display/src/input_desktop.rs b/crates/pf-win-display/src/input_desktop.rs new file mode 100644 index 00000000..4eab9ea7 --- /dev/null +++ b/crates/pf-win-display/src/input_desktop.rs @@ -0,0 +1,200 @@ +//! Make display-config writes follow the INPUT desktop. +//! +//! Windows refuses `ChangeDisplaySettingsEx` / `SetDisplayConfig` issued from a thread that is not +//! on the desktop currently receiving input. While a UAC consent prompt (or the lock / logon +//! screen) is up, the input desktop is `Winlogon`; a host thread sitting on `WinSta0\Default` — the +//! desktop the service explicitly launches it onto — then gets `DISP_CHANGE_FAILED` / +//! `ERROR_ACCESS_DENIED` for EVERY write. A session starting in that window can never set its +//! virtual display's mode, so the capturer sizes its ring to a stale mode, every composed frame is +//! dropped for a size mismatch, and the client sits on a black screen until bring-up runs out of +//! retries. Field-reported 2026-07-23: a consent prompt left up on an unattended host made the box +//! unreachable until someone walked over to it. +//! +//! Measured on-glass that same day (RTX 4090 box, SYSTEM host in console session 2, a real +//! interactive consent prompt up, virtual display `\\.\DISPLAY42` active): +//! +//! ```text +//! INPUT desktop = Winlogon +//! UNBOUND CDS_TEST -> -1 (DISP_CHANGE_FAILED) <- the field failure, reproduced +//! UNBOUND SDC_VALIDATE -> 0x5 (ERROR_ACCESS_DENIED) <- the field failure, reproduced +//! bound thread desktop -> Winlogon +//! BOUND CDS_TEST -> 0 (DISP_CHANGE_SUCCESSFUL) +//! BOUND SDC_VALIDATE -> 0x0 (ERROR_SUCCESS) +//! ``` +//! +//! The retry model mirrors `pf-inject`'s `sendinput.rs`: do NOT pay +//! `OpenInputDesktop`/`SetThreadDesktop` on every write — issue the write, and only when it fails +//! the way a wrong-desktop write fails do we rebind and retry once. That keeps the normal path +//! byte-for-byte as it was (a working write is never touched) and makes this strictly additive. +//! +//! Unlike sendinput's injector — a dedicated thread that KEEPS its binding — these helpers run on +//! shared/task threads, so the binding here is SCOPED: [`InputDesktopBinding`] restores the +//! thread's original desktop on drop. A thread left bound to a `Winlogon` desktop that is later +//! destroyed (prompt dismissed) would fail every subsequent display write for the life of the +//! process — the exact wedge this module exists to remove, so it must not be introduced here. + +use windows::Win32::Foundation::HANDLE; +use windows::Win32::System::StationsAndDesktops::{ + CloseDesktop, GetThreadDesktop, GetUserObjectInformationW, OpenInputDesktop, SetThreadDesktop, + DESKTOP_ACCESS_FLAGS, DESKTOP_CONTROL_FLAGS, HDESK, UOI_NAME, +}; +use windows::Win32::System::Threading::GetCurrentThreadId; + +/// `GENERIC_ALL` for the desktop open. The `windows` crate models desktop access as its own flag +/// type and doesn't export the generic rights, so spell it out (same constant `sendinput.rs` and +/// the cursor poller use). +const GENERIC_ALL: u32 = 0x1000_0000; + +/// This thread's binding to the input desktop, restored on drop. +/// +/// `GetThreadDesktop` returns a BORROWED handle (documented: it creates no new handle and must not +/// be closed), so only the handle `OpenInputDesktop` produced is closed here — and only after the +/// thread has been moved back off it. +pub(crate) struct InputDesktopBinding { + previous: HDESK, + input: HDESK, + /// `UOI_NAME` of the desktop bound to, for the "this write only landed because we rebound" + /// log. Read once at bind time (the failure path only), never in steady state. + name: String, +} + +impl InputDesktopBinding { + /// Bind the calling thread to the desktop currently receiving input. `None` when there is no + /// reachable input desktop (not privileged for `Winlogon` — a host that is not SYSTEM) or the + /// rebind is refused, in which case the caller keeps whatever result it already had rather than + /// changing behaviour. + pub(crate) fn bind() -> Option { + // SAFETY: all four are FFI calls taking by-value args only. `GetThreadDesktop` yields a + // borrowed handle for THIS thread (never closed here). `OpenInputDesktop` yields an owned + // `HDESK` only on `Ok`; it is either installed by `SetThreadDesktop` (and then owned by the + // returned guard, which closes it exactly once in `Drop`) or closed right here on failure — + // so it is closed exactly once on every path and never used after close. `SetThreadDesktop` + // rebinds only the calling thread, which owns no windows or hooks (these are display-config + // worker threads), so it cannot fail on that account. + unsafe { + let previous = GetThreadDesktop(GetCurrentThreadId()).ok()?; + let input = OpenInputDesktop( + DESKTOP_CONTROL_FLAGS(0), + false, + DESKTOP_ACCESS_FLAGS(GENERIC_ALL), + ) + .ok()?; + let name = desktop_name(input).unwrap_or_else(|| "".into()); + if SetThreadDesktop(input).is_err() { + let _ = CloseDesktop(input); + return None; + } + Some(Self { + previous, + input, + name, + }) + } + } +} + +impl Drop for InputDesktopBinding { + fn drop(&mut self) { + // SAFETY: `self.previous` is the borrowed desktop this thread started on and `self.input` + // is the handle this guard uniquely owns. The thread is moved back FIRST, so the handle is + // no longer the thread's desktop when it is closed — closed exactly once, never used after. + unsafe { + let _ = SetThreadDesktop(self.previous); + let _ = CloseDesktop(self.input); + } + } +} + +/// `UOI_NAME` of the current input desktop — `Some("Winlogon")` while a UAC consent prompt, the +/// lock screen or the logon screen owns input, `Some("Default")` in normal operation, `None` when +/// it cannot be opened at all. +pub(crate) fn input_desktop_name() -> Option { + // SAFETY: `OpenInputDesktop` yields an owned handle only on `Ok`, which is closed exactly once + // below and not used after. + unsafe { + let h = OpenInputDesktop( + DESKTOP_CONTROL_FLAGS(0), + false, + DESKTOP_ACCESS_FLAGS(GENERIC_ALL), + ) + .ok()?; + let name = desktop_name(h); + let _ = CloseDesktop(h); + name + } +} + +/// `UOI_NAME` of an already-open desktop handle. Borrows `h` — closing it stays the caller's job. +/// +/// # Safety +/// `h` must be a live desktop handle for the duration of the call. +unsafe fn desktop_name(h: HDESK) -> Option { + let mut name = [0u16; 64]; // "Default"/"Winlogon"/"Screen-saver" all fit with room to spare + let mut needed = 0u32; + // SAFETY: `h` is live per this fn's contract; `name`/`needed` are live out-params and the call + // writes at most `nlength` bytes, exactly the size passed. + GetUserObjectInformationW( + HANDLE(h.0), + UOI_NAME, + Some(name.as_mut_ptr().cast()), + (name.len() * 2) as u32, + Some(&mut needed), + ) + .ok()?; + let len = name.iter().position(|&c| c == 0).unwrap_or(name.len()); + Some(String::from_utf16_lossy(&name[..len])) +} + +/// `true` when the input desktop is a SECURE one (`UOI_NAME` != `Default`: `Winlogon` for UAC +/// consent / lock / logon, or a screen-saver desktop) — i.e. when display writes from the host's +/// own desktop are being refused for that reason. `false` when it is normal OR unreadable: this +/// only ever phrases a diagnostic, so an unknown desktop must not claim the secure one is up. +pub(crate) fn input_desktop_is_secure() -> bool { + input_desktop_name().is_some_and(|n| !n.eq_ignore_ascii_case("Default")) +} + +/// `ERROR_ACCESS_DENIED` — exactly how Windows refuses a `SetDisplayConfig` issued from a thread +/// that is not on the input desktop (measured, see the module header). Narrower than "any error" on +/// purpose: the CCD path also returns `ERROR_INVALID_PARAMETER` (0x57) for the unrelated +/// exclusive-mode topology bug, and re-issuing THAT on another desktop would only confuse its +/// diagnosis. +const SDC_ACCESS_DENIED: i32 = 5; + +/// [`retry_on_input_desktop`] specialised for the CCD writes, which all return a Win32 error code. +pub(crate) fn retry_set_display_config(write: impl FnMut() -> i32) -> i32 { + retry_on_input_desktop(|rc| *rc == SDC_ACCESS_DENIED, write) +} + +/// Run a display-config write; if it comes back the way a write from the wrong desktop comes back, +/// bind this thread to the CURRENT input desktop and run it exactly once more. +/// +/// `denied` decides which results are worth a retry — keep it NARROW (the specific +/// wrong-desktop verdict), so a genuinely bad mode or a driver-level refusal is not re-issued and +/// mis-attributed. The binding is dropped before returning, so the calling thread always leaves on +/// the desktop it arrived on. +pub(crate) fn retry_on_input_desktop( + denied: impl Fn(&T) -> bool, + mut write: impl FnMut() -> T, +) -> T { + let first = write(); + if !denied(&first) { + return first; + } + // Only worth the rebind when the input desktop is genuinely elsewhere; on a normal desktop the + // refusal means something else entirely and a retry would just repeat it. + let Some(binding) = InputDesktopBinding::bind() else { + return first; + }; + let second = write(); + if !denied(&second) { + // Say so. A silent save is indistinguishable in a log from a write that never needed + // saving, which makes "did the fix fire?" unanswerable in the field — and made the first + // on-glass verification of this very change inconclusive. + tracing::info!( + desktop = %binding.name, + "display write was refused off the input desktop — retried bound to it and it applied \ + (a UAC prompt / lock screen owns input; the session continues normally)" + ); + } + second +} diff --git a/crates/pf-win-display/src/lib.rs b/crates/pf-win-display/src/lib.rs index 8861f6d8..ddcee95f 100644 --- a/crates/pf-win-display/src/lib.rs +++ b/crates/pf-win-display/src/lib.rs @@ -11,6 +11,9 @@ #[cfg(target_os = "windows")] pub mod display_events; +/// Bind display-config writes to the input desktop so a UAC / lock screen can't refuse them. +#[cfg(target_os = "windows")] +mod input_desktop; #[cfg(target_os = "windows")] pub mod monitor_devnode; #[cfg(target_os = "windows")] diff --git a/crates/pf-win-display/src/win_display.rs b/crates/pf-win-display/src/win_display.rs index 5763d8ac..cc72b309 100644 --- a/crates/pf-win-display/src/win_display.rs +++ b/crates/pf-win-display/src/win_display.rs @@ -44,8 +44,8 @@ use windows::Win32::Devices::Display::{ use windows::Win32::Foundation::POINTL; use windows::Win32::Graphics::Gdi::{ ChangeDisplaySettingsExW, EnumDisplaySettingsW, CDS_TEST, CDS_UPDATEREGISTRY, DEVMODEW, - DISP_CHANGE_SUCCESSFUL, DM_BITSPERPEL, DM_DISPLAYFREQUENCY, DM_PELSHEIGHT, DM_PELSWIDTH, - ENUM_CURRENT_SETTINGS, ENUM_DISPLAY_SETTINGS_MODE, + DISP_CHANGE_FAILED, DISP_CHANGE_SUCCESSFUL, DM_BITSPERPEL, DM_DISPLAYFREQUENCY, DM_PELSHEIGHT, + DM_PELSWIDTH, ENUM_CURRENT_SETTINGS, ENUM_DISPLAY_SETTINGS_MODE, }; use punktfunk_core::Mode; @@ -62,7 +62,9 @@ use punktfunk_core::Mode; pub unsafe fn force_extend_topology() { // A topology flag with no supplied path/mode arrays tells the OS to recompute + apply that preset // for the currently-connected displays (the same code path DisplaySwitch.exe drives). - let rc = SetDisplayConfig(None, None, SDC_APPLY | SDC_TOPOLOGY_EXTEND); + let rc = crate::input_desktop::retry_set_display_config(|| { + SetDisplayConfig(None, None, SDC_APPLY | SDC_TOPOLOGY_EXTEND) + }); if rc == 0 { tracing::info!( "display topology forced to EXTEND (a new IddCx monitor would otherwise be CLONED onto the \ @@ -152,11 +154,13 @@ pub unsafe fn activate_target_path(target_id: u32) -> bool { // SAVE_TO_DATABASE so Windows remembers the arrangement — the next same-identity ADD (the driver // reuses the slot's EDID serial/ConnectorIndex) then auto-activates from the persistence DB and // skips this whole fallback ladder. - let rc = SetDisplayConfig( - Some(supplied.as_slice()), - Some(modes.as_slice()), - SDC_APPLY | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_ALLOW_CHANGES | SDC_SAVE_TO_DATABASE, - ); + let rc = crate::input_desktop::retry_set_display_config(|| { + SetDisplayConfig( + Some(supplied.as_slice()), + Some(modes.as_slice()), + SDC_APPLY | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_ALLOW_CHANGES | SDC_SAVE_TO_DATABASE, + ) + }); if rc == 0 { tracing::info!( target_id, @@ -273,14 +277,16 @@ pub unsafe fn force_mode_reenumeration() -> bool { let Some((paths, modes)) = query_active_config() else { return false; }; - let rc = SetDisplayConfig( - Some(paths.as_slice()), - Some(modes.as_slice()), - SDC_APPLY - | SDC_USE_SUPPLIED_DISPLAY_CONFIG - | SDC_ALLOW_CHANGES - | SDC_FORCE_MODE_ENUMERATION, - ); + let rc = crate::input_desktop::retry_set_display_config(|| { + SetDisplayConfig( + Some(paths.as_slice()), + Some(modes.as_slice()), + SDC_APPLY + | SDC_USE_SUPPLIED_DISPLAY_CONFIG + | SDC_ALLOW_CHANGES + | SDC_FORCE_MODE_ENUMERATION, + ) + }); if rc != 0 { tracing::debug!("force mode re-enumeration: SetDisplayConfig rc={rc:#x}"); } @@ -625,9 +631,15 @@ pub fn set_active_mode(gdi_name: &str, mode: Mode) { // SAFETY: `wname` is a live NUL-terminated UTF-16 device name and `&dm` is a live DEVMODEW describing // the requested mode; both outlive the call. CDS_TEST only validates the mode (no apply), the two // trailing args are null, and the API only reads its inputs. - let test = unsafe { - ChangeDisplaySettingsExW(PCWSTR(wname.as_ptr()), Some(&dm), None, CDS_TEST, None) - }; + // A CDS write from a thread that is not on the input desktop is refused with DISP_CHANGE_FAILED + // (UAC consent / lock screen up) — retry it bound to that desktop rather than declaring the mode + // unsupported, which is what stranded sessions on a black screen for a whole bring-up. + let test = crate::input_desktop::retry_on_input_desktop( + |rc| *rc == DISP_CHANGE_FAILED, + || unsafe { + ChangeDisplaySettingsExW(PCWSTR(wname.as_ptr()), Some(&dm), None, CDS_TEST, None) + }, + ); if test != DISP_CHANGE_SUCCESSFUL { tracing::warn!( result = test.0, @@ -642,15 +654,20 @@ pub fn set_active_mode(gdi_name: &str, mode: Mode) { // SAFETY: same inputs as the CDS_TEST call above — `wname` (live NUL-terminated device name) and // `&dm` (live DEVMODEW) both outlive the call; CDS_UPDATEREGISTRY applies the already-validated mode, // and the API only reads its inputs. - let apply = unsafe { - ChangeDisplaySettingsExW( - PCWSTR(wname.as_ptr()), - Some(&dm), - None, - CDS_UPDATEREGISTRY, - None, - ) - }; + // Same wrong-desktop retry as the validate above: the two calls bind independently, so an apply + // still lands even when the secure desktop came up between them. + let apply = crate::input_desktop::retry_on_input_desktop( + |rc| *rc == DISP_CHANGE_FAILED, + || unsafe { + ChangeDisplaySettingsExW( + PCWSTR(wname.as_ptr()), + Some(&dm), + None, + CDS_UPDATEREGISTRY, + None, + ) + }, + ); if apply == DISP_CHANGE_SUCCESSFUL { tracing::info!( "{gdi_name}: active mode set to {}x{}@{}", @@ -672,12 +689,20 @@ pub fn set_active_mode(gdi_name: &str, mode: Mode) { /// Human decode for a failed `ChangeDisplaySettingsExW` result. The two codes worth telling apart /// in a field log: `BADMODE` (the display's mode list genuinely lacks the mode) vs `FAILED` (the -/// write itself was rejected — on a healthy driver that is the signature of a host process without -/// console-session access, e.g. one trapped in a disconnected RDP session). An earlier revision -/// printed "mode not advertised?" for BOTH, which sent a lid-closed field report chasing the wrong -/// cause. +/// write itself was rejected). An earlier revision printed "mode not advertised?" for BOTH, which +/// sent a lid-closed field report chasing the wrong cause. +/// +/// `FAILED` itself has two causes needing opposite fixes — no console-session access (disconnected +/// RDP) versus the right session but the wrong desktop (UAC / lock / logon screen owns input) — so +/// it asks which before naming one. See [`sdc_access_denied_hint`] for the same split on the CCD +/// side. fn disp_change_reason(rc: i32) -> &'static str { match rc { + -1 if crate::input_desktop::input_desktop_is_secure() => { + "DISP_CHANGE_FAILED: the SECURE desktop owns input — a UAC consent prompt, the lock \ + screen or the logon screen is up, and display writes are refused off it. Dismiss the \ + prompt on the host" + } -1 => { "DISP_CHANGE_FAILED: the display write was rejected — a host without console-session \ access (disconnected RDP session / non-console session) fails ALL display writes \ @@ -691,15 +716,28 @@ fn disp_change_reason(rc: i32) -> &'static str { } } -/// Appended to `SetDisplayConfig` failure logs when rc is `ERROR_ACCESS_DENIED` (0x5) — per MS -/// docs "the caller does not have access to the console session", the field signature of a host -/// running in a disconnected RDP / non-console session. Every other rc gets no hint. +/// Appended to `SetDisplayConfig` failure logs when rc is `ERROR_ACCESS_DENIED` (0x5). Every other +/// rc gets no hint. +/// +/// `ERROR_ACCESS_DENIED` has TWO field causes and they need opposite fixes, so ask which one before +/// naming it. MS docs say only "the caller does not have access to the console session", which is +/// the disconnected-RDP / non-console case — but the SAME rc comes back when the host IS in the +/// console session and merely off the input desktop (UAC consent, lock or logon screen up). Naming +/// only the first sent a 2026-07-23 investigation chasing a phantom RDP session while a consent +/// prompt was the actual cause, on a host the message told to "run via the installed service" — +/// which it already was. [`crate::input_desktop`] now retries these bound to the input desktop, so +/// seeing this at all means even that did not help. fn sdc_access_denied_hint(rc: i32) -> &'static str { - if rc == 5 { + if rc != 5 { + return ""; + } + if crate::input_desktop::input_desktop_is_secure() { + " (ERROR_ACCESS_DENIED: the SECURE desktop owns input — a UAC consent prompt, the lock \ + screen or the logon screen is up, and display writes are refused off it. Dismiss the \ + prompt on the host)" + } else { " (ERROR_ACCESS_DENIED: the host has no console-session access — disconnected RDP \ session? run via the installed service so it tracks the console session)" - } else { - "" } } @@ -928,7 +966,9 @@ pub unsafe fn isolate_displays_ccd(keep_target_ids: &[u32]) -> Option 0 && attempt >= 2 { + // The supplied shape is decided (and its escalation announced) ONCE, outside the write, so a + // wrong-desktop retry re-issues the identical config instead of logging the escalation twice. + let keep_only = (others > 0 && attempt >= 2).then(|| { // ESCALATION (attempt 2+): supply ONLY the keep paths. Field-reported (AMD + // pf-vdisplay): carrying the doomed path in the array — inactive, modes unpinned — // gets the whole config rejected 0x57 on EVERY retry, so the loop alone never @@ -946,17 +986,21 @@ pub unsafe fn isolate_displays_ccd(keep_target_ids: &[u32]) -> Option SetDisplayConfig(Some(kp.as_slice()), Some(km.as_slice()), *esc), + None => { + let mut flags = SDC_APPLY + | SDC_USE_SUPPLIED_DISPLAY_CONFIG + | SDC_ALLOW_CHANGES + | SDC_FORCE_MODE_ENUMERATION; + if others == 0 { + flags |= SDC_SAVE_TO_DATABASE; + } + SetDisplayConfig(Some(paths.as_slice()), Some(modes.as_slice()), flags) } - SetDisplayConfig(Some(paths.as_slice()), Some(modes.as_slice()), flags) - }; + }); // A failed apply must be VISIBLE even when the verification below passes vacuously (nothing // else was active to deactivate — the lid-closed laptop case, where the success INFO used to // swallow rc=0x5): the re-commit above is load-bearing (COMMIT_MODES → ASSIGN_SWAPCHAIN), @@ -1134,14 +1178,16 @@ pub unsafe fn apply_source_positions(positions: &[(u32, i32, i32)]) { if moved == 0 { return; } - let rc = SetDisplayConfig( - Some(paths.as_slice()), - Some(modes.as_slice()), - SDC_APPLY - | SDC_USE_SUPPLIED_DISPLAY_CONFIG - | SDC_ALLOW_CHANGES - | SDC_FORCE_MODE_ENUMERATION, - ); + let rc = crate::input_desktop::retry_set_display_config(|| { + SetDisplayConfig( + Some(paths.as_slice()), + Some(modes.as_slice()), + SDC_APPLY + | SDC_USE_SUPPLIED_DISPLAY_CONFIG + | SDC_ALLOW_CHANGES + | SDC_FORCE_MODE_ENUMERATION, + ) + }); if rc == 0 { tracing::info!( ?positions, @@ -1226,14 +1272,16 @@ pub unsafe fn set_virtual_primary_ccd(keep_target_id: u32) -> Option