From bdec2fd01edbff369fc44147add6b208c7c9a44f Mon Sep 17 00:00:00 2001 From: Ali Younes Date: Mon, 26 Oct 2026 19:18:04 -0400 Subject: [PATCH 01/16] Mirror at once while the second screen starts, and remember a failed start When an iPad connected in Second screen mode, the host restarted the pipeline and sent no video until the virtual display attached or a 10 s wait ran out, on top of the Enable task. The iPad gives up 10 s after the handshake, so on PCs where the display was slow or never attached it never saw a picture, and over USB every redial turned the display off and on again. The running pipeline now keeps mirroring to the iPad while a separate thread turns the display on, and the stream restarts onto it once it attaches. A viewer leaving does not cancel the start. After a failed start later connections mirror without touching the driver, and the PC shows its notice, until the user clicks Try again, restarts the stream or changes the mode. The display stays on 10 s after its viewer leaves, and the host changes it at most once per 10 s on its own. Each decision logs one line; fallbacks carry second_screen_fallback= for the diagnostics upload. --- host/src/capture/mod.rs | 178 +++----- host/src/capture/second_screen.rs | 674 ++++++++++++++++++++++++++++++ host/src/control.rs | 57 +-- host/src/transport/mod.rs | 72 ++-- 4 files changed, 801 insertions(+), 180 deletions(-) create mode 100644 host/src/capture/second_screen.rs diff --git a/host/src/capture/mod.rs b/host/src/capture/mod.rs index e2f61d69..a818b78e 100644 --- a/host/src/capture/mod.rs +++ b/host/src/capture/mod.rs @@ -21,6 +21,7 @@ use crate::control::{CaptureTarget, VddStatus}; #[cfg(windows)] pub mod dxgi; pub mod recovery; +pub(crate) mod second_screen; pub mod synthetic; pub(crate) mod timing; @@ -356,10 +357,11 @@ pub(crate) fn client_connected(shared: &SharedControl) -> bool { } /// Reconcile the virtual display device to the requested target and return the concrete -/// target to capture. `VirtualExtended` enables the bundled driver **only once an iPad is -/// connected**, waits for a genuinely-new output to attach, and returns it as an `Output(name)`; -/// any other target (and the no-client / failure / timeout cases) disables the driver so no -/// phantom monitor lingers, falling back to `PrimaryAuto`. +/// target to capture. `VirtualExtended` captures the virtual display once it is attached and +/// a viewer is connected (or one left less than [`second_screen::RELEASE_GRACE`] ago), and +/// mirrors `PrimaryAuto` otherwise: while [`second_screen`] turns the display on beside the +/// mirror, after it failed to, and with nobody connected, when the display is turned off so +/// no phantom monitor lingers. Any other target turns the display off. #[cfg(windows)] pub(crate) fn reconcile_virtual_display( target: &CaptureTarget, @@ -371,11 +373,21 @@ pub(crate) fn reconcile_virtual_display( startup_heartbeat(shared, crate::clock::host_now_us() / 1000); } }; - // A previous capture thread may still be leaving its attach poll or a - // driver task (up to 30 s) when the supervisor starts the next generation. - // Only one may trigger driver tasks. Waiting here without heartbeats read - // as a wedged capture loop after 3 s, and each forced restart queued - // another generation behind the same lock. + // A start in progress holds the driver lock for as long as Windows takes; + // the mirror must not wait for it. A failed start leaves the driver alone. + if *target == CaptureTarget::VirtualExtended + && matches!( + *shared.vdd_status.lock(), + VddStatus::Attaching | VddStatus::Failed | VddStatus::TaskFailed + ) + { + return capture_is_current(shared, generation).then_some(CaptureTarget::PrimaryAuto); + } + // A previous capture thread may still be leaving a driver task (up to + // 30 s) when the supervisor starts the next generation. Only one may + // trigger driver tasks. Waiting here without heartbeats read as a wedged + // capture loop after 3 s, and each forced restart queued another + // generation behind the same lock. let _lifecycle = loop { if let Some(guard) = shared.vdd_lifecycle.try_lock_for(Duration::from_millis(50)) { break guard; @@ -390,123 +402,42 @@ pub(crate) fn reconcile_virtual_display( } match target { CaptureTarget::VirtualExtended => { - // Defer enabling the VDD until an iPad actually connects. The transport restarts the - // pipeline on the first receiver registration, so this runs again with a connected - // client at that point and the display comes up then — never while the PC sits idle. - let waiting_for_client = { - let session = shared.session.lock(); - let waiting = !session.is_active(); - *shared.vdd_status.lock() = if waiting { - VddStatus::WaitingForClient - } else { - VddStatus::Attaching - }; - waiting - }; - if waiting_for_client { - info!( - "Extended display selected but no iPad has connected yet — leaving the \ - virtual display off and mirroring the primary display until a client registers" - ); - crate::vdd::set_enabled(false, progress); - *shared.vdd_output.lock() = None; - return capture_is_current(shared, generation) - .then_some(CaptureTarget::PrimaryAuto); - } - - let outputs = enumerate_outputs(); - // Settings and codec changes restart capture without removing the - // managed monitor. Reuse that output rather than waiting for it to - // appear a second time or selecting a physical secondary monitor. - if let Some(name) = shared.vdd_output.lock().as_ref() { - if outputs + let connected = shared.client_connected(); + let owned = shared.vdd_output.lock().clone(); + let attached = owned.filter(|name| { + enumerate_outputs() .iter() .any(|output| !output.is_primary && &output.device_name == name) - { - *shared.vdd_status.lock() = VddStatus::Active; - return Some(CaptureTarget::Output(name.clone())); - } - } - *shared.vdd_output.lock() = None; - - // Best effort: match the virtual display's mode list to the - // connected iPad's panel BEFORE enabling the driver (it reads - // vdd_settings.xml at enable time). Failure is non-fatal — the - // driver's built-in modes apply. NEEDS_WINDOWS_VERIFY (M10). - if crate::settings::SettingsFile::load().vdd_match_resolution { - if let Some(info) = shared.session.lock().client_info() { - let modes = crate::vdd_settings::modes_for_client( - info.screen_px.0, - info.screen_px.1, - info.refresh_hz, - ); - let path = std::path::Path::new(r"C:\VirtualDisplayDriver\vdd_settings.xml"); - match crate::vdd_settings::write_to(path, &modes) { - Ok(()) => info!( - ?modes, - "Wrote virtual display mode list for the connected client" - ), - Err(error) => warn!( - %error, - "Could not write vdd_settings.xml — driver default modes apply" - ), - } + }); + // Settings and codec changes restart capture without removing the + // managed monitor, and the stream moves onto it this way once it + // attaches. Reuse that output rather than turning it on again. + if let Some(name) = attached { + let active = *shared.vdd_status.lock() == VddStatus::Active; + if active && (connected || shared.second_screen.lock().keep_without_viewer()) { + return Some(CaptureTarget::Output(name)); } } - - let before: Vec = outputs.into_iter().map(|o| o.device_name).collect(); - if !capture_is_current(shared, generation) { - return None; - } - startup_heartbeat(shared, crate::clock::host_now_us() / 1000); - if !crate::vdd::set_enabled(true, progress) { - if !capture_is_current(shared, generation) { - return None; - } - warn!( - "Virtual display could not be enabled (installer task missing?) — \ - capturing the primary display instead" - ); - *shared.vdd_status.lock() = VddStatus::TaskFailed; + if connected { + // The transport's next tick starts the display beside this mirror. + *shared.vdd_output.lock() = None; + *shared.vdd_status.lock() = VddStatus::WaitingForClient; return Some(CaptureTarget::PrimaryAuto); } - let result = wait_for_virtual_output( - &before, - vdd_attach_timeout(), - || { - if !capture_is_current(shared, generation) { - return None; - } - startup_heartbeat(shared, crate::clock::host_now_us() / 1000); - Some(enumerate_outputs()) - }, - Instant::now, - std::thread::sleep, + info!( + "Extended display selected but no iPad has connected yet — leaving the \ + virtual display off and mirroring the primary display until a client registers" ); - if !capture_is_current(shared, generation) { - return None; - } - match result { - AttachResult::Cancelled => return None, - AttachResult::Attached(chosen) => { - info!( - device = %chosen.device_name, - width = chosen.width, - height = chosen.height, - "Virtual display attached — capturing it" - ); - *shared.vdd_output.lock() = Some(chosen.device_name.clone()); - *shared.vdd_status.lock() = VddStatus::Active; - return Some(CaptureTarget::Output(chosen.device_name)); + let was_on = shared.vdd_output.lock().take().is_some(); + crate::vdd::set_enabled(false, progress); + { + let mut status = shared.vdd_status.lock(); + *status = VddStatus::WaitingForClient; + if was_on { + shared.second_screen.lock().note_toggle(Instant::now()); } - AttachResult::TimedOut => {} } - warn!("Virtual display did not attach in time — capturing the primary display instead"); - // enable() succeeded but nothing attached: turn it back off so we don't strand a - // half-enabled device as a phantom monitor. - crate::vdd::set_enabled(false, progress); - *shared.vdd_status.lock() = VddStatus::Failed; - Some(CaptureTarget::PrimaryAuto) + capture_is_current(shared, generation).then_some(CaptureTarget::PrimaryAuto) } other => { // Any non-virtual target: ensure the virtual display is off. @@ -582,6 +513,13 @@ pub fn frame_slot() -> (FrameSlot, FrameSlot) { (producer, consumer) } +/// `ETERNAL_CAPTURE=synthetic`: the test pattern instead of the desktop, and +/// no virtual display driver. +#[cfg(windows)] +pub(crate) fn synthetic_requested() -> bool { + std::env::var("ETERNAL_CAPTURE").is_ok_and(|v| v.trim().eq_ignore_ascii_case("synthetic")) +} + #[cfg(windows)] fn run_platform_loop( slot: &FrameSlot, @@ -590,9 +528,7 @@ fn run_platform_loop( generation: u64, frame_number: &mut u64, ) -> Result<(), Box> { - let synthetic_requested = - std::env::var("ETERNAL_CAPTURE").is_ok_and(|v| v.trim().eq_ignore_ascii_case("synthetic")); - if synthetic_requested { + if synthetic_requested() { synthetic::run_capture_loop(slot, shared, generation, frame_number) } else { dxgi::run_capture_loop(slot, shared, adapter_index, generation, frame_number) @@ -632,7 +568,7 @@ mod tests { assert!(frame_has_image(0, true)); } - fn connect_viewer(shared: &SharedControl) { + pub(super) fn connect_viewer(shared: &SharedControl) { use crate::transport::link::PeerId; use crate::transport::session::ConfigSource; use eternal_wire::v2::control::{ControlMessage, Hello2, StreamConfig, CAP_DECODE_H264}; diff --git a/host/src/capture/second_screen.rs b/host/src/capture/second_screen.rs new file mode 100644 index 00000000..2e83edc0 --- /dev/null +++ b/host/src/capture/second_screen.rs @@ -0,0 +1,674 @@ +//! Starting the second screen beside a live mirror. +//! +//! 0.3.2 held video back while it turned the virtual display on: about 3 s +//! for the Enable task (6 s behind a Disable still running), then up to 10 s +//! for the display to attach. The iPad gives up 10 s after the handshake when +//! no frame has decoded, so on a PC whose display attached slowly or never, +//! the iPad left first and the mirror fallback never reached it. Over USB the +//! host dialed again a second later and turned the display off and on again +//! for every attempt. +//! +//! Now a connecting iPad gets the mirror at once, from the capture generation +//! already running. The display is turned on by its own thread here, and the +//! transport restarts the pipeline onto it once it exists. A start that +//! failed is remembered: later connections stay on the mirror until the user +//! clicks Try again, restarts the stream, or changes the mode. + +use std::time::{Duration, Instant}; + +#[cfg(any(windows, test))] +use tracing::{info, warn}; + +use crate::control::{CaptureTarget, SharedControl, VddStatus}; + +#[cfg(any(windows, test))] +use super::{wait_for_virtual_output, AttachResult, OutputInfo}; + +/// How long the second screen stays on after its iPad leaves. A USB redial +/// comes about 1 s after the tunnel closes, and an iPad returning from the +/// background or a short Wi-Fi drop is back within seconds; both find the +/// second screen still there. +pub(crate) const RELEASE_GRACE: Duration = Duration::from_secs(10); + +/// The host never turns the virtual display on or off again sooner than this +/// after its previous change. Each change adds or removes a display adapter: +/// the PC's screens blink and Windows plays its device sound. +#[cfg(any(windows, test))] +pub(crate) const MIN_TOGGLE_GAP: Duration = Duration::from_secs(10); + +#[cfg(any(windows, test))] +const WAIT_STEP: Duration = Duration::from_millis(100); + +/// How long a stopped pipeline may take to come back before the display is +/// left off: a restart joins the old generation for up to 5 s, while a quit +/// or Stop leaves it stopped. +#[cfg(any(windows, test))] +const STOPPED_WAIT: Duration = Duration::from_secs(6); + +/// What the transport does next for the second screen. +#[derive(Debug, PartialEq, Eq)] +pub(crate) enum Step { + Nothing, + /// A viewer arrived: start the display beside the mirror. + Attach, + /// The display attached while the mirror streamed: restart onto it. + Switch, + /// Nobody came back within the grace period: restart to turn it off. + Release, +} + +#[derive(Debug, Default)] +pub(crate) struct State { + /// The display attached; the stream has not moved onto it yet. + switch_pending: bool, + /// When the attached display was last seen without a viewer. + viewer_left_at: Option, + /// The grace period ended; the next capture generation turns it off. + release_requested: bool, + /// When the host last turned the display on or off. + last_toggle: Option, +} + +impl State { + pub(crate) fn step( + &mut self, + selected: bool, + status: &mut VddStatus, + connected: bool, + now: Instant, + ) -> Step { + if !selected { + *self = State { + last_toggle: self.last_toggle, + ..State::default() + }; + return Step::Nothing; + } + match *status { + VddStatus::WaitingForClient if connected => { + *status = VddStatus::Attaching; + Step::Attach + } + VddStatus::Active if connected => { + self.viewer_left_at = None; + self.release_requested = false; + if std::mem::take(&mut self.switch_pending) { + Step::Switch + } else { + Step::Nothing + } + } + VddStatus::Active if !self.release_requested => { + let left = *self.viewer_left_at.get_or_insert(now); + if now.duration_since(left) < RELEASE_GRACE { + return Step::Nothing; + } + self.viewer_left_at = None; + self.switch_pending = false; + self.release_requested = true; + Step::Release + } + _ => Step::Nothing, + } + } + + /// Whether a capture generation with no viewer should leave an attached + /// display on: only the grace period's end turns it off. + #[cfg(any(windows, test))] + pub(crate) fn keep_without_viewer(&self) -> bool { + !self.release_requested + } + + #[cfg(any(windows, test))] + pub(crate) fn note_toggle(&mut self, now: Instant) { + self.last_toggle = Some(now); + self.release_requested = false; + } + + #[cfg(any(windows, test))] + fn toggle_delay(&self, now: Instant) -> Duration { + self.last_toggle.map_or(Duration::ZERO, |at| { + (at + MIN_TOGGLE_GAP).saturating_duration_since(now) + }) + } +} + +/// The transport's decision for this tick. Status before state, the order +/// every caller locks them in. +pub(crate) fn next_step(shared: &SharedControl, now: Instant) -> Step { + let selected = *shared.capture_target.lock() == CaptureTarget::VirtualExtended; + let connected = shared.client_connected(); + let mut status = shared.vdd_status.lock(); + shared + .second_screen + .lock() + .step(selected, &mut status, connected, now) +} + +/// Why connections mirror instead of extending, after a failed start. The +/// value appears as `second_screen_fallback=` in the log, which the +/// diagnostics upload sends as is. +pub(crate) fn remembered_failure(status: VddStatus) -> Option<&'static str> { + match status { + VddStatus::Failed => Some("attach_timeout"), + VddStatus::TaskFailed => Some("enable_failed"), + _ => None, + } +} + +/// Turn the display on beside the mirror. Returns at once. +pub(crate) fn start(shared: SharedControl) { + #[cfg(windows)] + if !super::synthetic_requested() { + let worker = shared.clone(); + let spawned = std::thread::Builder::new() + .name("second-screen".into()) + .spawn(move || { + attach( + &worker, + &WindowsDriver { shared: &worker }, + super::vdd_attach_timeout(), + ) + }); + if let Err(error) = spawned { + warn!(%error, second_screen_fallback = "enable_failed", + "Could not start the second screen — mirroring the primary display instead"); + *shared.vdd_status.lock() = VddStatus::TaskFailed; + } + return; + } + tracing::info!( + second_screen_fallback = "no_driver", + "No virtual display driver with this capture source — mirroring the primary display" + ); + *shared.vdd_status.lock() = VddStatus::TaskFailed; +} + +/// What turning the display on needs from Windows, so tests can supply a +/// clock and a driver that attaches late or never. +#[cfg(any(windows, test))] +pub(crate) trait Driver { + fn outputs(&self) -> Vec; + /// Prepare the driver's settings for the connected iPad. + fn prepare(&self); + fn set_enabled(&self, enabled: bool) -> bool; + fn now(&self) -> Instant; + fn sleep(&self, duration: Duration); +} + +/// Turn the display on, wait for it to attach, and record the result. A +/// viewer leaving does not cancel it; only a change of mode does. Capture +/// keeps mirroring throughout. +#[cfg(any(windows, test))] +pub(crate) fn attach(shared: &SharedControl, driver: &impl Driver, timeout: Duration) { + let selected = || *shared.capture_target.lock() == CaptureTarget::VirtualExtended; + // Capture generations turn the display off under the same lock. + let _lifecycle = loop { + if let Some(guard) = shared.vdd_lifecycle.try_lock_for(Duration::from_millis(50)) { + break guard; + } + if !selected() { + return; + } + }; + if !wait_for_toggle_gap(shared, driver, &selected) { + return; + } + if !shared.client_connected() { + info!("The iPad left before the virtual display was turned on — leaving it off"); + *shared.vdd_status.lock() = VddStatus::WaitingForClient; + return; + } + // Quitting turns the display off once; an Enable after that would + // outlive the app. + let stopped_at = driver.now(); + while !shared.running.load(std::sync::atomic::Ordering::SeqCst) { + if driver.now().duration_since(stopped_at) >= STOPPED_WAIT { + info!("The stream stopped before the virtual display was turned on — leaving it off"); + *shared.vdd_status.lock() = VddStatus::WaitingForClient; + return; + } + driver.sleep(WAIT_STEP); + } + driver.prepare(); + let before: Vec = driver + .outputs() + .into_iter() + .map(|output| output.device_name) + .collect(); + let enabled = driver.set_enabled(true); + shared.second_screen.lock().note_toggle(driver.now()); + if !enabled { + warn!( + second_screen_fallback = "enable_failed", + "Virtual display could not be enabled (installer task missing?) — \ + capturing the primary display instead" + ); + *shared.vdd_status.lock() = VddStatus::TaskFailed; + return; + } + let result = wait_for_virtual_output( + &before, + timeout, + || selected().then(|| driver.outputs()), + || driver.now(), + |duration| driver.sleep(duration), + ); + match result { + // The user chose another mode; that generation turns the display off. + AttachResult::Cancelled => {} + AttachResult::Attached(chosen) => { + info!( + device = %chosen.device_name, + width = chosen.width, + height = chosen.height, + "Virtual display attached — capturing it" + ); + *shared.vdd_output.lock() = Some(chosen.device_name); + let mut status = shared.vdd_status.lock(); + *status = VddStatus::Active; + shared.second_screen.lock().switch_pending = true; + } + AttachResult::TimedOut => { + warn!( + second_screen_fallback = "attach_timeout", + timeout_ms = timeout.as_millis() as u64, + "Virtual display did not attach in time — capturing the primary display instead" + ); + // Enable succeeded but nothing attached: turn it back off so no + // half-enabled device lingers as a phantom monitor. + wait_for_toggle_gap(shared, driver, &|| true); + driver.set_enabled(false); + let mut status = shared.vdd_status.lock(); + *status = VddStatus::Failed; + shared.second_screen.lock().note_toggle(driver.now()); + } + } +} + +/// Wait out [`MIN_TOGGLE_GAP`]. False when the mode changed meanwhile. +#[cfg(any(windows, test))] +fn wait_for_toggle_gap( + shared: &SharedControl, + driver: &impl Driver, + selected: &dyn Fn() -> bool, +) -> bool { + let delay = shared.second_screen.lock().toggle_delay(driver.now()); + if delay.is_zero() { + return selected(); + } + info!( + wait_ms = delay.as_millis() as u64, + "Waiting before changing the virtual display again, so Windows doesn't add and \ + remove it in quick succession" + ); + let until = driver.now() + delay; + while driver.now() < until { + if !selected() { + return false; + } + driver.sleep(until.saturating_duration_since(driver.now()).min(WAIT_STEP)); + } + selected() +} + +#[cfg(windows)] +struct WindowsDriver<'a> { + shared: &'a SharedControl, +} + +#[cfg(windows)] +impl Driver for WindowsDriver<'_> { + fn outputs(&self) -> Vec { + super::enumerate_outputs() + } + + // Best effort: match the virtual display's mode list to the connected + // iPad's panel BEFORE enabling the driver (it reads vdd_settings.xml at + // enable time). Failure is non-fatal — the driver's built-in modes apply. + fn prepare(&self) { + if !crate::settings::SettingsFile::load().vdd_match_resolution { + return; + } + let Some(info) = self.shared.session.lock().client_info() else { + return; + }; + let modes = crate::vdd_settings::modes_for_client( + info.screen_px.0, + info.screen_px.1, + info.refresh_hz, + ); + let path = std::path::Path::new(r"C:\VirtualDisplayDriver\vdd_settings.xml"); + match crate::vdd_settings::write_to(path, &modes) { + Ok(()) => info!( + ?modes, + "Wrote virtual display mode list for the connected client" + ), + Err(error) => warn!( + %error, + "Could not write vdd_settings.xml — driver default modes apply" + ), + } + } + + fn set_enabled(&self, enabled: bool) -> bool { + crate::vdd::set_enabled(enabled, || {}) + } + + fn now(&self) -> Instant { + Instant::now() + } + + fn sleep(&self, duration: Duration) { + std::thread::sleep(duration); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::capture::tests::connect_viewer; + use crate::transport::link::LinkId; + use std::cell::{Cell, RefCell}; + + /// The iPad's connect timeout in 0.3.1 and 0.3.2: it disconnects when no + /// frame has decoded this long after the handshake. + const IPAD_TIMEOUT: Duration = Duration::from_secs(10); + /// The 0.3.2 default attach wait after Enable. + const ATTACH_TIMEOUT: Duration = Duration::from_secs(10); + + fn secs(seconds: f64) -> Duration { + Duration::from_secs_f64(seconds) + } + + /// Runs `action` once, the first time the paused clock reaches `at`. + struct Once { + at: Duration, + done: Cell, + } + + impl Once { + fn at(at: Duration) -> Self { + Self { + at, + done: Cell::new(false), + } + } + + fn run(&self, elapsed: Duration, action: impl FnOnce()) { + if elapsed >= self.at && !self.done.replace(true) { + action(); + } + } + } + + fn second_screen_selected() -> SharedControl { + let shared = SharedControl::new(9876, 15_000_000); + shared + .running + .store(true, std::sync::atomic::Ordering::SeqCst); + *shared.capture_target.lock() = CaptureTarget::VirtualExtended; + *shared.vdd_status.lock() = VddStatus::WaitingForClient; + shared + } + + fn disconnect(shared: &SharedControl) { + assert!(shared.session.lock().link_lost(LinkId::Udp).client_lost); + } + + fn output(name: &str, primary: bool) -> OutputInfo { + OutputInfo { + adapter_index: 0, + output_index: 0, + device_name: name.to_string(), + width: 1920, + height: 1080, + left: if primary { 0 } else { -1920 }, + top: 0, + is_primary: primary, + adapter_name: "Test GPU".to_string(), + } + } + + /// A driver on a paused clock. Enable takes `enable_takes`, and the + /// display attaches `attach_after` later, or never. `on_sleep` sees the + /// clock each time the host waits, as the transport's ticks would. + struct FakeDriver<'a> { + start: Instant, + elapsed: Cell, + enable_takes: Duration, + attach_after: Option, + on_since: Cell>, + toggles: RefCell>, + on_sleep: &'a dyn Fn(Duration), + } + + impl<'a> FakeDriver<'a> { + fn new( + enable_takes: Duration, + attach_after: Option, + on_sleep: &'a dyn Fn(Duration), + ) -> Self { + Self { + start: Instant::now(), + elapsed: Cell::new(Duration::ZERO), + enable_takes, + attach_after, + on_since: Cell::new(None), + toggles: RefCell::new(Vec::new()), + on_sleep, + } + } + } + + impl Driver for FakeDriver<'_> { + fn outputs(&self) -> Vec { + let mut outputs = vec![output("primary", true)]; + let attached = self + .on_since + .get() + .zip(self.attach_after) + .is_some_and(|(on, after)| self.elapsed.get() >= on + after); + if attached { + outputs.push(output("virtual", false)); + } + outputs + } + + fn prepare(&self) {} + + fn set_enabled(&self, enabled: bool) -> bool { + if enabled { + self.sleep(self.enable_takes); + } + self.toggles + .borrow_mut() + .push((self.elapsed.get(), enabled)); + self.on_since.set(enabled.then(|| self.elapsed.get())); + true + } + + fn now(&self) -> Instant { + self.start + self.elapsed.get() + } + + fn sleep(&self, duration: Duration) { + // Report every 50 ms, as often as the transport ticks. + let end = self.elapsed.get() + duration; + while self.elapsed.get() < end { + let step = (end - self.elapsed.get()).min(Duration::from_millis(50)); + self.elapsed.set(self.elapsed.get() + step); + (self.on_sleep)(self.elapsed.get()); + } + } + } + + /// Joy's 0.3.2 log and the 0.3.1 field data: the display never attaches, + /// the iPad sends its goodbye at 10 s, and USB dials again 1 s later. + /// Here no transport step restarts the pipeline while the display + /// starts, so the running generation keeps mirroring to the iPad, with + /// an Enable task far slower than the iPad's whole timeout. The start is + /// not cancelled by the goodbye, ends in one Enable and one Disable, and + /// every later connection mirrors without touching the driver. + #[test] + fn a_display_that_never_attaches_mirrors_at_once_and_never_loops() { + let shared = second_screen_selected(); + connect_viewer(&shared); + let start = Instant::now(); + assert_eq!(next_step(&shared, start), Step::Attach); + assert_eq!(*shared.vdd_status.lock(), VddStatus::Attaching); + + let steps = RefCell::new(Vec::new()); + let (goodbye, redial) = (Once::at(IPAD_TIMEOUT), Once::at(IPAD_TIMEOUT + secs(1.0))); + let on_sleep = |elapsed: Duration| { + goodbye.run(elapsed, || disconnect(&shared)); + redial.run(elapsed, || connect_viewer(&shared)); + steps.borrow_mut().push(next_step(&shared, start + elapsed)); + }; + let driver = FakeDriver::new(secs(8.0), None, &on_sleep); + attach(&shared, &driver, ATTACH_TIMEOUT); + + assert!(goodbye.done.get() && redial.done.get()); + assert!( + steps.borrow().iter().all(|step| *step == Step::Nothing), + "no restart while the display starts" + ); + let toggles = driver.toggles.borrow().clone(); + assert_eq!(toggles.len(), 2, "{toggles:?}"); + assert_eq!(toggles[0], (secs(8.0), true)); + assert!(!toggles[1].1 && toggles[1].0 - toggles[0].0 >= MIN_TOGGLE_GAP); + assert_eq!(*shared.vdd_status.lock(), VddStatus::Failed); + assert_eq!( + remembered_failure(*shared.vdd_status.lock()), + Some("attach_timeout") + ); + + // Reconnects for the next minute stay on the mirror. + for second in 0..60 { + disconnect(&shared); + connect_viewer(&shared); + let now = driver.now() + Duration::from_secs(second); + assert_eq!(next_step(&shared, now), Step::Nothing); + } + // Try again starts it once more at the next tick. + shared.retry_second_screen(); + assert_eq!(next_step(&shared, driver.now()), Step::Attach); + } + + #[test] + fn the_stream_moves_to_the_second_screen_once_it_attaches() { + let shared = second_screen_selected(); + connect_viewer(&shared); + assert_eq!(next_step(&shared, Instant::now()), Step::Attach); + let steps = RefCell::new(Vec::new()); + let on_sleep = |_| steps.borrow_mut().push(next_step(&shared, Instant::now())); + let driver = FakeDriver::new(secs(3.0), Some(secs(0.5)), &on_sleep); + attach(&shared, &driver, ATTACH_TIMEOUT); + assert!(steps.borrow().iter().all(|step| *step == Step::Nothing)); + assert_eq!(*shared.vdd_status.lock(), VddStatus::Active); + assert_eq!(shared.vdd_output.lock().as_deref(), Some("virtual")); + assert_eq!(driver.toggles.borrow().len(), 1); + let now = driver.now(); + assert_eq!(next_step(&shared, now), Step::Switch); + assert_eq!(next_step(&shared, now), Step::Nothing, "one switch"); + } + + #[test] + fn a_quick_reconnect_keeps_the_second_screen_and_a_long_absence_releases_it() { + let shared = second_screen_selected(); + *shared.vdd_status.lock() = VddStatus::Active; + connect_viewer(&shared); + let start = Instant::now(); + assert_eq!(next_step(&shared, start), Step::Nothing); + + disconnect(&shared); + let mut elapsed = Duration::ZERO; + while elapsed < secs(9.9) { + assert_eq!(next_step(&shared, start + elapsed), Step::Nothing); + assert!(shared.second_screen.lock().keep_without_viewer()); + elapsed += secs(0.25); + } + // The iPad comes back within the grace period, then leaves again. + connect_viewer(&shared); + assert_eq!(next_step(&shared, start + elapsed), Step::Nothing); + disconnect(&shared); + let left = start + elapsed + secs(0.25); + assert_eq!(next_step(&shared, left), Step::Nothing); + assert_eq!( + next_step(&shared, left + RELEASE_GRACE - secs(0.25)), + Step::Nothing + ); + assert_eq!(next_step(&shared, left + RELEASE_GRACE), Step::Release); + assert!(!shared.second_screen.lock().keep_without_viewer()); + assert_eq!( + next_step(&shared, left + RELEASE_GRACE + secs(1.0)), + Step::Nothing, + "one release" + ); + } + + #[test] + fn the_display_is_never_turned_on_within_the_gap_after_it_was_turned_off() { + let shared = second_screen_selected(); + let on_sleep = |_| {}; + let driver = FakeDriver::new(secs(3.0), Some(Duration::ZERO), &on_sleep); + // The grace period ended and the next generation turned it off. + shared.second_screen.lock().note_toggle(driver.now()); + connect_viewer(&shared); + assert_eq!(next_step(&shared, driver.now()), Step::Attach); + attach(&shared, &driver, ATTACH_TIMEOUT); + let toggles = driver.toggles.borrow().clone(); + assert_eq!(toggles, vec![(MIN_TOGGLE_GAP + secs(3.0), true)]); + assert_eq!(*shared.vdd_status.lock(), VddStatus::Active); + } + + #[test] + fn a_viewer_leaving_during_the_gap_leaves_the_display_off() { + let shared = second_screen_selected(); + connect_viewer(&shared); + assert_eq!(next_step(&shared, Instant::now()), Step::Attach); + let goodbye = Once::at(secs(2.0)); + let on_sleep = |elapsed: Duration| goodbye.run(elapsed, || disconnect(&shared)); + let driver = FakeDriver::new(secs(3.0), Some(Duration::ZERO), &on_sleep); + shared.second_screen.lock().note_toggle(driver.now()); + attach(&shared, &driver, ATTACH_TIMEOUT); + assert!(goodbye.done.get()); + assert!(driver.toggles.borrow().is_empty()); + assert_eq!(*shared.vdd_status.lock(), VddStatus::WaitingForClient); + } + + #[test] + fn a_quit_before_the_enable_leaves_the_display_off() { + let shared = second_screen_selected(); + connect_viewer(&shared); + assert_eq!(next_step(&shared, Instant::now()), Step::Attach); + shared.stop(); + let on_sleep = |_| {}; + let driver = FakeDriver::new(secs(3.0), Some(Duration::ZERO), &on_sleep); + attach(&shared, &driver, ATTACH_TIMEOUT); + assert!(driver.toggles.borrow().is_empty()); + assert_eq!(*shared.vdd_status.lock(), VddStatus::WaitingForClient); + } + + #[test] + fn changing_the_mode_ends_the_start_and_clears_the_timers() { + let shared = second_screen_selected(); + connect_viewer(&shared); + assert_eq!(next_step(&shared, Instant::now()), Step::Attach); + let switch = Once::at(secs(5.0)); + let on_sleep = |elapsed: Duration| { + switch.run(elapsed, || { + *shared.capture_target.lock() = CaptureTarget::PrimaryAuto + }) + }; + let driver = FakeDriver::new(secs(3.0), None, &on_sleep); + attach(&shared, &driver, ATTACH_TIMEOUT); + assert!( + driver.elapsed.get() < secs(5.5), + "{:?}", + driver.elapsed.get() + ); + assert_eq!(driver.toggles.borrow().len(), 1, "only the Enable"); + assert_eq!(next_step(&shared, driver.now()), Step::Nothing); + } +} diff --git a/host/src/control.rs b/host/src/control.rs index f99e3fa3..3f57a2f6 100644 --- a/host/src/control.rs +++ b/host/src/control.rs @@ -39,12 +39,16 @@ pub struct SharedControl { /// Set while Windows blocks screen capture (lock screen, UAC, a display /// it will not duplicate); the GUI explains it instead of a frozen picture. pub capture_block: Arc>>, - /// Serialize driver reconciliation across retiring capture generations. - #[cfg(windows)] + /// Serialize driver changes across retiring capture generations and the + /// thread that turns the second screen on. + #[cfg(any(windows, test))] pub(crate) vdd_lifecycle: Arc>, /// Output owned by the enabled driver, retained across pipeline restarts. - #[cfg(windows)] + #[cfg(any(windows, test))] pub(crate) vdd_output: Arc>>, + /// Second screen timing across pipeline restarts: the pending switch onto + /// it, the grace period after its viewer left, the last driver change. + pub(crate) second_screen: Arc>, /// Watchdog heartbeats (ms on the process clock, relaxed): the capture /// loop's aliveness, the last produced frame, and the last encoded frame. /// The supervisor detects wedged/starved stages from these. @@ -83,14 +87,15 @@ pub enum VddStatus { /// Virtual display requested but not yet active because no iPad has connected. The driver /// is deliberately left off until a receiver registers, so an idle PC shows no phantom monitor. WaitingForClient, - /// The first registration has claimed startup; reconnects must not restart it. + /// The display is being turned on while the iPad gets the mirror. + /// Reconnects and disconnects leave the start alone. Attaching, /// Virtual display enabled and being captured. Active, - /// Virtual display was requested but could not be enabled/attached — capture fell back to - /// the primary display. + /// Virtual display was requested but did not attach — capture fell back to the primary + /// display. Later connections mirror too, until the user tries again or changes the mode. Failed, - /// The installer-owned task itself failed to execute. + /// The installer-owned task itself failed to execute. Remembered like `Failed`. TaskFailed, } @@ -149,10 +154,11 @@ impl SharedControl { capture_target: Arc::new(Mutex::new(CaptureTarget::PrimaryAuto)), vdd_status: Arc::new(Mutex::new(VddStatus::Inactive)), capture_block: Arc::new(Mutex::new(None)), - #[cfg(windows)] + #[cfg(any(windows, test))] vdd_lifecycle: Arc::new(Mutex::new(())), - #[cfg(windows)] + #[cfg(any(windows, test))] vdd_output: Arc::new(Mutex::new(None)), + second_screen: Arc::new(Mutex::new(Default::default())), hb_capture_loop_ms: Arc::new(AtomicU64::new(0)), hb_capture_frame_ms: Arc::new(AtomicU64::new(0)), hb_encode_frame_ms: Arc::new(AtomicU64::new(0)), @@ -190,17 +196,13 @@ impl SharedControl { self.session.lock().is_active() } - /// Claim the single restart needed when a viewer arrives after idle capture. - pub(crate) fn begin_virtual_display_attach(&self) -> bool { - if *self.capture_target.lock() != CaptureTarget::VirtualExtended { - return false; - } + /// The user asked to try again: a second screen that failed to start is + /// tried once more at the next connection. + pub(crate) fn retry_second_screen(&self) { let mut status = self.vdd_status.lock(); - if *status != VddStatus::WaitingForClient { - return false; + if matches!(*status, VddStatus::Failed | VddStatus::TaskFailed) { + *status = VddStatus::WaitingForClient; } - *status = VddStatus::Attaching; - true } } @@ -232,6 +234,7 @@ pub struct GuiControl { impl GuiControl { pub fn request_restart(&self) { crate::vdd::recheck_tasks(); + self.shared.retry_second_screen(); if let Err(error) = self.supervisor_tx.send(SupervisorCommand::Restart) { warn!(error = %error, "Failed to send restart command"); } @@ -245,6 +248,7 @@ impl GuiControl { pub fn request_start(&self) { crate::vdd::recheck_tasks(); + self.shared.retry_second_screen(); if let Err(error) = self.supervisor_tx.send(SupervisorCommand::Start) { warn!(error = %error, "Failed to send start command"); } @@ -261,23 +265,6 @@ impl GuiControl { mod tests { use super::*; - #[test] - fn usb_reconnects_claim_only_one_virtual_display_start() { - let shared = SharedControl::new(9876, 15_000_000); - *shared.capture_target.lock() = CaptureTarget::VirtualExtended; - *shared.vdd_status.lock() = VddStatus::WaitingForClient; - assert!(shared.begin_virtual_display_attach()); - assert_eq!(*shared.vdd_status.lock(), VddStatus::Attaching); - for _ in 0..50 { - assert!(!shared.begin_virtual_display_attach()); - } - *shared.vdd_status.lock() = VddStatus::Active; - assert!(!shared.begin_virtual_display_attach()); - *shared.vdd_status.lock() = VddStatus::WaitingForClient; - *shared.capture_target.lock() = CaptureTarget::PrimaryAuto; - assert!(!shared.begin_virtual_display_attach()); - } - #[test] fn client_fps_caps_the_host_without_overwriting_its_preference() { let shared = SharedControl::new(9876, 15_000_000); diff --git a/host/src/transport/mod.rs b/host/src/transport/mod.rs index a1d52e22..10e81ec0 100644 --- a/host/src/transport/mod.rs +++ b/host/src/transport/mod.rs @@ -23,6 +23,7 @@ use tokio::net::UdpSocket; use tokio::sync::mpsc; use tracing::{debug, info, warn}; +use crate::capture::second_screen; use crate::control::{CaptureTarget, SharedControl, SupervisorCommand, VddStatus}; use crate::encoder::NALUnit; use crate::stats::PIPELINE_STATS; @@ -661,16 +662,14 @@ async fn execute_actions( stats.set_target_addr(target.to_string()); stats.reset_connection_stats(); } - // The virtual extended display is enabled lazily by the capture loop's - // reconcile step, which only acts while a client is connected. If the - // user selected it before any client existed, this first registration - // is the moment to bring it up — which needs a pipeline restart so the - // capture loop re-runs reconciliation. - if shared.begin_virtual_display_attach() { - info!("Client connected with extended display selected — restarting pipeline to enable it"); - shared.stop(); - if let Err(error) = supervisor_tx.send(SupervisorCommand::Restart) { - warn!(error = %error, "Failed to request pipeline restart for the virtual display"); + if *shared.capture_target.lock() == CaptureTarget::VirtualExtended { + let status = *shared.vdd_status.lock(); + if let Some(reason) = second_screen::remembered_failure(status) { + info!( + second_screen_fallback = reason, + "Client connected, but the second screen failed to start earlier — \ + mirroring the primary display until the user clicks Try again" + ); } } } @@ -694,23 +693,48 @@ async fn execute_actions( .lock() .set_target_addr("waiting for client".to_string()); - // Tear the managed virtual display down when its viewer disappears — - // the capture reconcile disables it on restart once no client is - // connected. (This closes the docs/decisions.md "idle-disconnect teardown" - // item, which was blocked on exactly this liveness signal.) - let vdd_in_use = *shared.capture_target.lock() == CaptureTarget::VirtualExtended - && matches!( - *shared.vdd_status.lock(), - VddStatus::Active | VddStatus::Attaching - ); - if vdd_in_use { - info!("Client gone while streaming the virtual display — restarting pipeline to tear it down"); - shared.stop(); - if let Err(error) = supervisor_tx.send(SupervisorCommand::Restart) { - warn!(error = %error, "Failed to request pipeline restart after client loss"); + // The managed virtual display outlives its viewer by a grace period, + // so a quick reconnect finds it, and a start in progress finishes. + // `second_screen::next_step` turns it off once the grace period ends. + if *shared.capture_target.lock() == CaptureTarget::VirtualExtended { + match *shared.vdd_status.lock() { + VddStatus::Active => info!( + grace_s = second_screen::RELEASE_GRACE.as_secs(), + "Client gone while streaming the virtual display — keeping it on in case \ + the iPad reconnects" + ), + VddStatus::Attaching => { + info!("Client gone while the virtual display starts — letting the start finish") + } + _ => {} } } } + + let restart = match second_screen::next_step(shared, Instant::now()) { + second_screen::Step::Nothing => None, + second_screen::Step::Attach => { + info!( + "Client connected with extended display selected — mirroring the primary \ + display while the virtual display starts" + ); + second_screen::start(shared.clone()); + None + } + second_screen::Step::Switch => { + Some("Virtual display ready — restarting pipeline to stream it") + } + second_screen::Step::Release => { + Some("No iPad came back to the virtual display — restarting pipeline to turn it off") + } + }; + if let Some(reason) = restart { + info!("{reason}"); + shared.stop(); + if let Err(error) = supervisor_tx.send(SupervisorCommand::Restart) { + warn!(error = %error, "Failed to request pipeline restart for the virtual display"); + } + } } #[cfg(test)] From 001b0bd1d5ea32302a57b888989590a6cf32569a Mon Sep 17 00:00:00 2001 From: Ali Younes Date: Mon, 26 Oct 2026 19:18:04 -0400 Subject: [PATCH 02/16] Check that a USB iPad gets the mirror at once with the second screen selected --- host/tests/e2e_synthetic.rs | 60 ++++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/host/tests/e2e_synthetic.rs b/host/tests/e2e_synthetic.rs index 16317894..59d6123b 100644 --- a/host/tests/e2e_synthetic.rs +++ b/host/tests/e2e_synthetic.rs @@ -257,45 +257,49 @@ fn usb_framed_stream_end_to_end() { assert_eq!(PIPELINE_STATS.lock().transport_retransmits, 0); eprintln!("USB_E2E decoded={decoded} after_recovery={after_recovery} heartbeats={heartbeats} dropped={dropped} recovery_ms={recovery_ms}"); - // A virtual-display startup restarts the pipeline once. The tunnel - // must survive that restart: 0.3.1 closed it, the iPad reconnected, - // and with Extended display selected the reconnect could trigger the - // next restart indefinitely. + // With the second screen selected, a connecting iPad gets the mirror + // from the running pipeline at once, while the virtual display starts + // beside it. 0.3.2 restarted the pipeline first and sent no video + // until the display had attached or a 10 s wait ran out; the iPad + // gives up 10 s after the handshake, and over USB it was dialed again + // into the same restart. Here the start fails at once (no driver + // with synthetic capture), and the reconnect after it mirrors too. use eternal_host::control::{CaptureTarget, VddStatus}; use eternal_host::supervisor::CURRENT_GENERATION; *shared.capture_target.lock() = CaptureTarget::VirtualExtended; *shared.vdd_status.lock() = VddStatus::WaitingForClient; let original_generation = CURRENT_GENERATION.load(Ordering::SeqCst); let ControlMessage::Hello2(mut next_hello) = hello else { unreachable!() }; - next_hello.client_nonce += 1; - write_datagram(&mut stream, &encode_control(0, 1, &ControlMessage::Hello2(next_hello.clone()))).await.unwrap(); - let mut received_ack = false; - let mut frames = 0; - let mut new_epoch = None; - let deadline = Instant::now() + Duration::from_secs(5); - while frames < 30 || !received_ack { - assert!(Instant::now() < deadline, "media must resume on the same tunnel after the restart"); - let bytes = tokio::time::timeout(Duration::from_secs(2), read_datagram(&mut stream)) - .await - .expect("tunnel stays open and active across the restart") - .expect("tunnel must not close during the restart"); - if let Ok((_, ControlMessage::HelloAck(ack))) = parse_control(&bytes) { - assert_eq!(ack.status, HelloStatus::Ok); - received_ack = true; - } else if let Ok((header, _)) = MediaHeader::decode(&bytes) { - if header.stream_epoch != epoch && header.frag_index == 0 { - new_epoch = Some(header.stream_epoch); - frames += 1; + for attempt in 1..=2 { + next_hello.client_nonce += 1; + let sent = Instant::now(); + write_datagram(&mut stream, &encode_control(0, 1, &ControlMessage::Hello2(next_hello.clone()))).await.unwrap(); + let mut session = None; + let mut first_keyframe = None; + while first_keyframe.is_none() { + assert!(sent.elapsed() < Duration::from_secs(5), "attempt {attempt}: no mirror within 5 s"); + let bytes = tokio::time::timeout(Duration::from_secs(2), read_datagram(&mut stream)) + .await + .expect("the tunnel stays active") + .expect("the tunnel stays open"); + if let Ok((_, ControlMessage::HelloAck(ack))) = parse_control(&bytes) { + assert_eq!(ack.status, HelloStatus::Ok); + session = Some(ack.session_id); + } else if let Ok((header, _)) = MediaHeader::decode(&bytes) { + if Some(header.session_id) == session && header.is_keyframe && header.frag_index == 0 { + assert_eq!(header.stream_epoch, epoch, "the mirror comes from the running pipeline"); + first_keyframe = Some(sent.elapsed()); + } } } + assert_eq!(CURRENT_GENERATION.load(Ordering::SeqCst), original_generation, + "attempt {attempt}: connecting must not restart the pipeline"); + assert_eq!(*shared.vdd_status.lock(), VddStatus::TaskFailed); + eprintln!("USB_SECOND_SCREEN_MIRROR attempt={attempt} first_keyframe_ms={}", + first_keyframe.unwrap().as_millis()); } - assert!(new_epoch.is_some(), "the restarted pipeline streams with a new epoch"); - assert_eq!(CURRENT_GENERATION.load(Ordering::SeqCst), original_generation + 1, - "USB HELLO during attach must not restart capture again"); - assert_eq!(*shared.vdd_status.lock(), VddStatus::Attaching); *shared.capture_target.lock() = CaptureTarget::PrimaryAuto; *shared.vdd_status.lock() = VddStatus::Inactive; - eprintln!("USB_VDD_RESTART_KEEPS_TUNNEL frames={frames} restarts=1"); drop(stream); let disconnected = Instant::now(); while shared.client_connected() && disconnected.elapsed() < Duration::from_secs(1) { From 0db545c4cc2b3fe740d2e2a35ca23c0bd7a0823a Mon Sep 17 00:00:00 2001 From: Ali Younes Date: Mon, 26 Oct 2026 19:18:04 -0400 Subject: [PATCH 03/16] Describe the mirror-first second screen start and its grace period --- ARCHITECTURE.md | 3 ++- docs/beta-testing.md | 3 ++- docs/decisions.md | 11 +++++++++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index d99a0466..0e53e421 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -69,7 +69,8 @@ Rust and Swift share golden vectors and stable packet-type registry tests. One session owns the active link. Another device receives Busy; a reconnect from the same device supersedes its prior session. Duplicate accepted nonces replay the same ACK. All established-session traffic is session-id gated. Reports/input -maintain liveness; expiry removes the client and its managed virtual display. +maintain liveness; expiry removes the client, and 10 s later its managed +virtual display unless the iPad has come back. UDP carries video, audio and control on the same socket. The iPad's established Network.framework backend remains the default. A selectable BSD backend uses a diff --git a/docs/beta-testing.md b/docs/beta-testing.md index 1c0cf0bf..52682a96 100644 --- a/docs/beta-testing.md +++ b/docs/beta-testing.md @@ -175,7 +175,8 @@ at the top of the host's Stream page, next to **Use the iPad as**, click **Secon Settings → How the iPad shows your PC). A running stream restarts by itself. Dragging a window past the edge of the main screen should land it on the iPad. The virtual monitor is created **on demand and only while the iPad is connected** — there is intentionally no second display when idle, so don't expect to see it in -Windows Display settings before connecting. If the extended display can't start, the host shows an +Windows Display settings before connecting. The iPad shows the mirrored main screen first and +switches to the second screen once Windows has added it. If the extended display can't start, the host shows an amber "The second screen couldn't start" banner and mirrors the primary screen — re-run the installer so its display task is registered. diff --git a/docs/decisions.md b/docs/decisions.md index c0c6301c..3f3038d9 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -119,8 +119,15 @@ registers two SYSTEM scheduled tasks (enable/disable) that the host triggers via `schtasks`, avoiding a per-toggle UAC prompt. - Enabled only while an iPad is connected. Disabled on exit, target change, - startup, panic, and on client loss (the v2 liveness signal finally made - idle teardown possible). + startup, panic, and 10 s after client loss (the v2 liveness signal finally + made idle teardown possible; the grace period lets a quick reconnect keep it). +- The iPad mirrors the primary display while the virtual display starts, and + the stream restarts onto it once it attaches. The iPad gives up 10 s after + the handshake without a frame, which a slow Enable task plus the attach wait + could exceed. A start that fails is remembered until the user clicks Try + again, restarts the stream or changes the mode, so reconnects mirror + instead of toggling the driver again. The host changes the driver at most + once per 10 s on its own. - Before enabling, the host writes `vdd_settings.xml` so the virtual display offers the iPad's native landscape resolution and refresh. There is an opt-out in Settings. From b1dae0012cc1011c655646bc2caa8b18ca0bd8a6 Mon Sep 17 00:00:00 2001 From: Ali Younes Date: Mon, 26 Oct 2026 19:20:22 -0400 Subject: [PATCH 04/16] Keep a second screen start claimed when a new capture generation finds a viewer --- host/src/capture/mod.rs | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/host/src/capture/mod.rs b/host/src/capture/mod.rs index a818b78e..73e7a793 100644 --- a/host/src/capture/mod.rs +++ b/host/src/capture/mod.rs @@ -421,7 +421,7 @@ pub(crate) fn reconcile_virtual_display( if connected { // The transport's next tick starts the display beside this mirror. *shared.vdd_output.lock() = None; - *shared.vdd_status.lock() = VddStatus::WaitingForClient; + wait_for_client(shared); return Some(CaptureTarget::PrimaryAuto); } info!( @@ -430,13 +430,10 @@ pub(crate) fn reconcile_virtual_display( ); let was_on = shared.vdd_output.lock().take().is_some(); crate::vdd::set_enabled(false, progress); - { - let mut status = shared.vdd_status.lock(); - *status = VddStatus::WaitingForClient; - if was_on { - shared.second_screen.lock().note_toggle(Instant::now()); - } + if was_on { + shared.second_screen.lock().note_toggle(Instant::now()); } + wait_for_client(shared); capture_is_current(shared, generation).then_some(CaptureTarget::PrimaryAuto) } other => { @@ -449,6 +446,16 @@ pub(crate) fn reconcile_virtual_display( } } +/// Leave the next start to the transport. A viewer that arrived during this +/// reconcile may already have claimed it; one start runs at a time. +#[cfg(windows)] +fn wait_for_client(shared: &SharedControl) { + let mut status = shared.vdd_status.lock(); + if *status != VddStatus::Attaching { + *status = VddStatus::WaitingForClient; + } +} + /// Runs the platform capture loop on the CURRENT thread until stop/close, /// publishing frames into `slot`. The supervisor spawns and monitors this. pub fn run_capture_stage( From b85ac4507aae341381e494ae1e75d3fe80ab445b Mon Sep 17 00:00:00 2001 From: Ali Younes Date: Mon, 26 Oct 2026 19:26:32 -0400 Subject: [PATCH 05/16] Drop the capture module's unused warn import on Windows --- host/src/capture/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/host/src/capture/mod.rs b/host/src/capture/mod.rs index 73e7a793..d44a6db7 100644 --- a/host/src/capture/mod.rs +++ b/host/src/capture/mod.rs @@ -12,7 +12,7 @@ use std::time::{Duration, Instant}; use parking_lot::{Condvar, Mutex}; use tracing::error; #[cfg(windows)] -use tracing::{info, warn}; +use tracing::info; use crate::control::SharedControl; #[cfg(windows)] From 48aabff63718ff9812262c2ca8907aa2fdec5ab9 Mon Sep 17 00:00:00 2001 From: Ali Younes Date: Mon, 26 Oct 2026 20:02:53 -0400 Subject: [PATCH 06/16] Prove the second screen with a real image, and never let capture wait for the driver After the switch, the capture of the virtual display must deliver a real image within 5 s. A display that fails to duplicate, or whose acquisition only times out, goes back to the mirror, counts as a failed start and shows the PC's notice. No black placeholder is sent from it meanwhile, so the iPad keeps the last mirror frame. Capture generations no longer take the driver lock or run driver tasks: a thread turns the display off as well as on, so the mirror never waits behind a slow Enable, also when Try again races a new generation. Every exit of the start thread clears its claim, and a mode change after Enable turns the display back off, so Second screen can no longer stay stuck in Attaching. Try again also recovers a claim with no thread behind it. --- host/src/capture/dxgi.rs | 20 +- host/src/capture/mod.rs | 124 ++++---- host/src/capture/second_screen.rs | 494 ++++++++++++++++++++++++++++-- host/src/control.rs | 7 +- host/src/transport/mod.rs | 26 +- 5 files changed, 554 insertions(+), 117 deletions(-) diff --git a/host/src/capture/dxgi.rs b/host/src/capture/dxgi.rs index d4f45196..41840a29 100644 --- a/host/src/capture/dxgi.rs +++ b/host/src/capture/dxgi.rs @@ -16,10 +16,10 @@ use super::recovery::{ }; use super::{ client_connected, frame_budget_for, frame_has_image, heartbeat, idle_heartbeat, - reconcile_virtual_display, recovery_poll, resolve_target, FrameSlot, FrameUse, IdleGate, - OutputInfo, RawFrame, ACQUIRE_TIMEOUT_MS, FPS_WINDOW, IDLE_KEEPALIVE, + keepalive_allowed, reconcile_virtual_display, recovery_poll, resolve_target, FrameSlot, + FrameUse, IdleGate, OutputInfo, RawFrame, ACQUIRE_TIMEOUT_MS, FPS_WINDOW, IDLE_KEEPALIVE, }; -use crate::control::SharedControl; +use crate::control::{CaptureTarget, SharedControl}; use crate::stats::PIPELINE_STATS; use windows::Win32::Foundation::RECT; use windows::Win32::Graphics::Direct3D::D3D_DRIVER_TYPE_UNKNOWN; @@ -121,6 +121,8 @@ pub fn run_capture_loop( let Some(target) = reconcile_virtual_display(&requested_target, &shared, generation) else { return Ok(()); }; + let second_screen = matches!(&target, CaptureTarget::Output(name) + if shared.vdd_output.lock().as_deref() == Some(name.as_str())); // DXGI factories snapshot adapters at creation. Enable the driver first, // then enumerate and open the chosen output from the same fresh factory. let factory: IDXGIFactory1 = unsafe { CreateDXGIFactory1()? }; @@ -414,6 +416,9 @@ pub fn run_capture_loop( } continue; } + if !seen_image { + super::second_screen::note_image(&shared, generation); + } seen_image = true; let frame_data: Option>> = if let Some(ref resource) = desktop_resource @@ -532,7 +537,10 @@ pub fn run_capture_loop( // connected, resend the last captured image so the encoder emits the pending IDR // and the stream stays alive. pixel_buf holds the last composited frame (zeroed // black before the first real frame, which is still a valid keyframe). - if client_connected(&shared) && last_send.elapsed() >= IDLE_KEEPALIVE { + if client_connected(&shared) + && last_send.elapsed() >= IDLE_KEEPALIVE + && keepalive_allowed(seen_image, second_screen) + { resend_last_frame( slot, &shared, @@ -590,7 +598,9 @@ pub fn run_capture_loop( return Ok(()); } let now_ms = crate::clock::host_now_us() / 1000; - if recovery_poll(&shared, now_ms, last_send.elapsed()) { + if recovery_poll(&shared, now_ms, last_send.elapsed()) + && keepalive_allowed(seen_image, second_screen) + { resend_last_frame( slot, &shared, diff --git a/host/src/capture/mod.rs b/host/src/capture/mod.rs index d44a6db7..f071779f 100644 --- a/host/src/capture/mod.rs +++ b/host/src/capture/mod.rs @@ -11,17 +11,17 @@ use std::time::{Duration, Instant}; use parking_lot::{Condvar, Mutex}; use tracing::error; -#[cfg(windows)] +#[cfg(any(windows, test))] use tracing::info; use crate::control::SharedControl; -#[cfg(windows)] +#[cfg(any(windows, test))] use crate::control::{CaptureTarget, VddStatus}; #[cfg(windows)] pub mod dxgi; pub mod recovery; -pub(crate) mod second_screen; +pub mod second_screen; pub mod synthetic; pub(crate) mod timing; @@ -333,7 +333,7 @@ pub(crate) fn recovery_poll(shared: &SharedControl, now_ms: u64, since_send: Dur since_send >= IDLE_KEEPALIVE } -#[cfg(windows)] +#[cfg(any(windows, test))] fn capture_is_current(shared: &SharedControl, generation: u64) -> bool { shared.running.load(std::sync::atomic::Ordering::SeqCst) && crate::supervisor::generation_is_current(generation) @@ -360,100 +360,80 @@ pub(crate) fn client_connected(shared: &SharedControl) -> bool { /// target to capture. `VirtualExtended` captures the virtual display once it is attached and /// a viewer is connected (or one left less than [`second_screen::RELEASE_GRACE`] ago), and /// mirrors `PrimaryAuto` otherwise: while [`second_screen`] turns the display on beside the -/// mirror, after it failed to, and with nobody connected, when the display is turned off so -/// no phantom monitor lingers. Any other target turns the display off. -#[cfg(windows)] +/// mirror, after it failed to, and with nobody connected. Any other target turns the display +/// off. Never waits for the driver: the [`second_screen`] threads change it, and the mirror +/// must not wait behind a slow Enable or attach. +#[cfg(any(windows, test))] pub(crate) fn reconcile_virtual_display( target: &CaptureTarget, shared: &SharedControl, generation: u64, ) -> Option { - let progress = || { - if capture_is_current(shared, generation) { - startup_heartbeat(shared, crate::clock::host_now_us() / 1000); - } - }; - // A start in progress holds the driver lock for as long as Windows takes; - // the mirror must not wait for it. A failed start leaves the driver alone. - if *target == CaptureTarget::VirtualExtended - && matches!( - *shared.vdd_status.lock(), - VddStatus::Attaching | VddStatus::Failed | VddStatus::TaskFailed - ) - { - return capture_is_current(shared, generation).then_some(CaptureTarget::PrimaryAuto); - } - // A previous capture thread may still be leaving a driver task (up to - // 30 s) when the supervisor starts the next generation. Only one may - // trigger driver tasks. Waiting here without heartbeats read as a wedged - // capture loop after 3 s, and each forced restart queued another - // generation behind the same lock. - let _lifecycle = loop { - if let Some(guard) = shared.vdd_lifecycle.try_lock_for(Duration::from_millis(50)) { - break guard; - } - if !capture_is_current(shared, generation) { - return None; - } - progress(); - }; if !capture_is_current(shared, generation) { return None; } + let owned = shared.vdd_output.lock().clone(); match target { CaptureTarget::VirtualExtended => { let connected = shared.client_connected(); - let owned = shared.vdd_output.lock().clone(); - let attached = owned.filter(|name| { + let status = *shared.vdd_status.lock(); + if matches!( + status, + VddStatus::Attaching | VddStatus::Failed | VddStatus::TaskFailed + ) { + return Some(CaptureTarget::PrimaryAuto); + } + // Settings and codec changes restart capture without removing the + // managed monitor, and the stream moves onto it this way once it + // attaches. Reuse that output rather than turning it on again. + let attached = owned.clone().filter(|name| { enumerate_outputs() .iter() .any(|output| !output.is_primary && &output.device_name == name) }); - // Settings and codec changes restart capture without removing the - // managed monitor, and the stream moves onto it this way once it - // attaches. Reuse that output rather than turning it on again. if let Some(name) = attached { - let active = *shared.vdd_status.lock() == VddStatus::Active; - if active && (connected || shared.second_screen.lock().keep_without_viewer()) { + if status == VddStatus::Active + && (connected || shared.second_screen.lock().keep_without_viewer()) + { + second_screen::start_proving(shared, generation, Instant::now()); return Some(CaptureTarget::Output(name)); } } - if connected { - // The transport's next tick starts the display beside this mirror. - *shared.vdd_output.lock() = None; - wait_for_client(shared); - return Some(CaptureTarget::PrimaryAuto); + if !connected { + info!( + "Extended display selected but no iPad has connected yet — leaving the \ + virtual display off and mirroring the primary display until a client registers" + ); + if owned.is_some() { + second_screen::turn_off(shared.clone()); + } } - info!( - "Extended display selected but no iPad has connected yet — leaving the \ - virtual display off and mirroring the primary display until a client registers" - ); - let was_on = shared.vdd_output.lock().take().is_some(); - crate::vdd::set_enabled(false, progress); - if was_on { - shared.second_screen.lock().note_toggle(Instant::now()); + // The transport's next tick starts the display beside this mirror. + // A viewer that arrived meanwhile may already have claimed it. + let mut status = shared.vdd_status.lock(); + if *status != VddStatus::Attaching { + *status = VddStatus::WaitingForClient; } - wait_for_client(shared); - capture_is_current(shared, generation).then_some(CaptureTarget::PrimaryAuto) + Some(CaptureTarget::PrimaryAuto) } other => { // Any non-virtual target: ensure the virtual display is off. - crate::vdd::set_enabled(false, progress); - *shared.vdd_output.lock() = None; + if owned.is_some() { + second_screen::turn_off(shared.clone()); + } *shared.vdd_status.lock() = VddStatus::Inactive; Some(other.clone()) } } } -/// Leave the next start to the transport. A viewer that arrived during this -/// reconcile may already have claimed it; one start runs at a time. -#[cfg(windows)] -fn wait_for_client(shared: &SharedControl) { - let mut status = shared.vdd_status.lock(); - if *status != VddStatus::Attaching { - *status = VddStatus::WaitingForClient; - } +/// Whether an idle keepalive may resend the last frame. Before the second +/// screen has delivered an image, that frame is the black placeholder: +/// sending it would replace the iPad's mirror with black, so the switch +/// waits for a real picture, or falls back to the mirror. +#[cfg(any(windows, test))] +pub(crate) fn keepalive_allowed(seen_image: bool, second_screen: bool) -> bool { + seen_image || !second_screen } /// Runs the platform capture loop on the CURRENT thread until stop/close, @@ -474,13 +454,17 @@ pub fn run_capture_stage( let result = recovery::run_until_unblocked( &shared, |frame_number| { - run_platform_loop( + let result = run_platform_loop( &guard.0, shared.clone(), adapter_index, generation, frame_number, - ) + ); + if result.is_err() { + second_screen::note_capture_error(&shared, generation); + } + result }, |delay| wait_while_blocked(&shared, generation, delay), ); diff --git a/host/src/capture/second_screen.rs b/host/src/capture/second_screen.rs index 2e83edc0..65ceaa4b 100644 --- a/host/src/capture/second_screen.rs +++ b/host/src/capture/second_screen.rs @@ -9,10 +9,13 @@ //! for every attempt. //! //! Now a connecting iPad gets the mirror at once, from the capture generation -//! already running. The display is turned on by its own thread here, and the -//! transport restarts the pipeline onto it once it exists. A start that -//! failed is remembered: later connections stay on the mirror until the user -//! clicks Try again, restarts the stream, or changes the mode. +//! already running. Threads here turn the display on and off, so capture +//! never waits for the driver. Once the display attaches the transport +//! restarts the pipeline onto it, and the capture generation that streams it +//! must deliver a real image within [`PROVE_TIMEOUT`]; otherwise the stream +//! goes back to the mirror. A start that failed is remembered: later +//! connections stay on the mirror until the user clicks Try again, restarts +//! the stream, or changes the mode. use std::time::{Duration, Instant}; @@ -36,6 +39,12 @@ pub(crate) const RELEASE_GRACE: Duration = Duration::from_secs(10); #[cfg(any(windows, test))] pub(crate) const MIN_TOGGLE_GAP: Duration = Duration::from_secs(10); +/// How long a capture generation streaming the second screen has to capture +/// a real image of it. Until then the iPad keeps showing the last mirror +/// frame; a display that fails to duplicate, or only ever times out, goes +/// back to the mirror after this. +pub const PROVE_TIMEOUT: Duration = Duration::from_secs(5); + #[cfg(any(windows, test))] const WAIT_STEP: Duration = Duration::from_millis(100); @@ -55,6 +64,16 @@ pub(crate) enum Step { Switch, /// Nobody came back within the grace period: restart to turn it off. Release, + /// The display showed no picture: restart onto the mirror, turn it off. + Fallback(&'static str), +} + +/// A capture generation streaming the second screen, before its first image. +#[derive(Debug, Clone, Copy)] +struct Proving { + generation: u64, + since: Instant, + capture_failed: bool, } #[derive(Debug, Default)] @@ -67,6 +86,11 @@ pub(crate) struct State { release_requested: bool, /// When the host last turned the display on or off. last_toggle: Option, + /// A thread is turning the display on. + attaching: bool, + proving: Option, + /// Why the second screen last failed, for the log and the backend. + failure: Option<&'static str>, } impl State { @@ -80,13 +104,34 @@ impl State { if !selected { *self = State { last_toggle: self.last_toggle, + attaching: self.attaching, ..State::default() }; return Step::Nothing; } + if let Some(proving) = self.proving { + let reason = if proving.capture_failed { + Some("duplication_failed") + } else if now.duration_since(proving.since) >= PROVE_TIMEOUT { + Some("no_frames") + } else { + None + }; + if let Some(reason) = reason { + *self = State { + last_toggle: self.last_toggle, + attaching: self.attaching, + failure: Some(reason), + ..State::default() + }; + *status = VddStatus::Failed; + return Step::Fallback(reason); + } + } match *status { VddStatus::WaitingForClient if connected => { *status = VddStatus::Attaching; + self.attaching = true; Step::Attach } VddStatus::Active if connected => { @@ -148,14 +193,67 @@ pub(crate) fn next_step(shared: &SharedControl, now: Instant) -> Step { /// Why connections mirror instead of extending, after a failed start. The /// value appears as `second_screen_fallback=` in the log, which the /// diagnostics upload sends as is. -pub(crate) fn remembered_failure(status: VddStatus) -> Option<&'static str> { +pub(crate) fn remembered_failure(shared: &SharedControl) -> Option<&'static str> { + let status = *shared.vdd_status.lock(); + let failure = shared.second_screen.lock().failure; match status { - VddStatus::Failed => Some("attach_timeout"), - VddStatus::TaskFailed => Some("enable_failed"), + VddStatus::Failed => Some(failure.unwrap_or("attach_timeout")), + VddStatus::TaskFailed => Some(failure.unwrap_or("enable_failed")), _ => None, } } +/// The user asked to try again: a failed start, or one whose thread is gone, +/// is tried once more at the next connection. +pub(crate) fn retry(shared: &SharedControl) { + let mut status = shared.vdd_status.lock(); + let mut state = shared.second_screen.lock(); + let stale = *status == VddStatus::Attaching && !state.attaching; + if stale || matches!(*status, VddStatus::Failed | VddStatus::TaskFailed) { + *status = VddStatus::WaitingForClient; + state.failure = None; + } +} + +/// The display attached and is the stream's next source. +pub fn note_attached(shared: &SharedControl, device_name: String) { + *shared.vdd_output.lock() = Some(device_name); + let mut status = shared.vdd_status.lock(); + *status = VddStatus::Active; + shared.second_screen.lock().switch_pending = true; +} + +/// Capture `generation` streams the second screen from `now`, and must +/// capture an image of it within [`PROVE_TIMEOUT`]. +pub fn start_proving(shared: &SharedControl, generation: u64, now: Instant) { + shared.second_screen.lock().proving = Some(Proving { + generation, + since: now, + capture_failed: false, + }); +} + +/// Capture `generation` captured a real image. +pub fn note_image(shared: &SharedControl, generation: u64) { + let mut state = shared.second_screen.lock(); + if state.proving.is_some_and(|p| p.generation == generation) { + state.proving = None; + tracing::info!("Second screen captured its first image — streaming it"); + } +} + +/// Capture `generation` failed to open or read its display, for example +/// DuplicateOutput returned DXGI_ERROR_UNSUPPORTED. On the second screen, +/// before its first image, that ends the attempt at the next tick. +pub fn note_capture_error(shared: &SharedControl, generation: u64) { + let mut state = shared.second_screen.lock(); + if let Some(proving) = state.proving.as_mut() { + if proving.generation == generation { + proving.capture_failed = true; + } + } +} + /// Turn the display on beside the mirror. Returns at once. pub(crate) fn start(shared: SharedControl) { #[cfg(windows)] @@ -173,7 +271,11 @@ pub(crate) fn start(shared: SharedControl) { if let Err(error) = spawned { warn!(%error, second_screen_fallback = "enable_failed", "Could not start the second screen — mirroring the primary display instead"); - *shared.vdd_status.lock() = VddStatus::TaskFailed; + let mut status = shared.vdd_status.lock(); + *status = VddStatus::TaskFailed; + let mut state = shared.second_screen.lock(); + state.attaching = false; + state.failure = Some("enable_failed"); } return; } @@ -181,7 +283,28 @@ pub(crate) fn start(shared: SharedControl) { second_screen_fallback = "no_driver", "No virtual display driver with this capture source — mirroring the primary display" ); - *shared.vdd_status.lock() = VddStatus::TaskFailed; + let mut status = shared.vdd_status.lock(); + *status = VddStatus::TaskFailed; + let mut state = shared.second_screen.lock(); + state.attaching = false; + state.failure = Some("no_driver"); +} + +/// Turn the display off on its own thread, so capture never waits for the +/// driver. Returns at once. +pub(crate) fn turn_off(shared: SharedControl) { + #[cfg(windows)] + if !super::synthetic_requested() { + let worker = shared.clone(); + let spawned = std::thread::Builder::new() + .name("second-screen-off".into()) + .spawn(move || release(&worker, &WindowsDriver { shared: &worker })); + if let Err(error) = spawned { + warn!(%error, "Could not turn the virtual display off"); + } + return; + } + *shared.vdd_output.lock() = None; } /// What turning the display on needs from Windows, so tests can supply a @@ -196,13 +319,30 @@ pub(crate) trait Driver { fn sleep(&self, duration: Duration); } +/// However [`attach`] ends, its claim ends with it: a start that did not +/// finish leaves the status for the next tick to claim again. +#[cfg(any(windows, test))] +struct AttachExit<'a>(&'a SharedControl); + +#[cfg(any(windows, test))] +impl Drop for AttachExit<'_> { + fn drop(&mut self) { + let mut status = self.0.vdd_status.lock(); + if *status == VddStatus::Attaching { + *status = VddStatus::WaitingForClient; + } + self.0.second_screen.lock().attaching = false; + } +} + /// Turn the display on, wait for it to attach, and record the result. A /// viewer leaving does not cancel it; only a change of mode does. Capture /// keeps mirroring throughout. #[cfg(any(windows, test))] pub(crate) fn attach(shared: &SharedControl, driver: &impl Driver, timeout: Duration) { + let _exit = AttachExit(shared); let selected = || *shared.capture_target.lock() == CaptureTarget::VirtualExtended; - // Capture generations turn the display off under the same lock. + // One driver change at a time, across the threads here. let _lifecycle = loop { if let Some(guard) = shared.vdd_lifecycle.try_lock_for(Duration::from_millis(50)) { break guard; @@ -216,7 +356,6 @@ pub(crate) fn attach(shared: &SharedControl, driver: &impl Driver, timeout: Dura } if !shared.client_connected() { info!("The iPad left before the virtual display was turned on — leaving it off"); - *shared.vdd_status.lock() = VddStatus::WaitingForClient; return; } // Quitting turns the display off once; an Enable after that would @@ -225,14 +364,24 @@ pub(crate) fn attach(shared: &SharedControl, driver: &impl Driver, timeout: Dura while !shared.running.load(std::sync::atomic::Ordering::SeqCst) { if driver.now().duration_since(stopped_at) >= STOPPED_WAIT { info!("The stream stopped before the virtual display was turned on — leaving it off"); - *shared.vdd_status.lock() = VddStatus::WaitingForClient; return; } driver.sleep(WAIT_STEP); } + // Still on from before (its turn-off saw this start coming): reuse it. + let outputs = driver.outputs(); + let owned = shared.vdd_output.lock().clone(); + if let Some(name) = owned.filter(|name| { + outputs + .iter() + .any(|output| !output.is_primary && &output.device_name == name) + }) { + info!(device = %name, "Virtual display still attached — capturing it"); + note_attached(shared, name); + return; + } driver.prepare(); - let before: Vec = driver - .outputs() + let before: Vec = outputs .into_iter() .map(|output| output.device_name) .collect(); @@ -244,7 +393,9 @@ pub(crate) fn attach(shared: &SharedControl, driver: &impl Driver, timeout: Dura "Virtual display could not be enabled (installer task missing?) — \ capturing the primary display instead" ); - *shared.vdd_status.lock() = VddStatus::TaskFailed; + let mut status = shared.vdd_status.lock(); + *status = VddStatus::TaskFailed; + shared.second_screen.lock().failure = Some("enable_failed"); return; } let result = wait_for_virtual_output( @@ -255,8 +406,14 @@ pub(crate) fn attach(shared: &SharedControl, driver: &impl Driver, timeout: Dura |duration| driver.sleep(duration), ); match result { - // The user chose another mode; that generation turns the display off. - AttachResult::Cancelled => {} + AttachResult::Cancelled => { + // The user chose another mode while the display came up. Nothing + // records it as ours, so turn it off now. + info!("Display mode changed while the virtual display started — turning it off"); + driver.set_enabled(false); + *shared.vdd_output.lock() = None; + shared.second_screen.lock().note_toggle(driver.now()); + } AttachResult::Attached(chosen) => { info!( device = %chosen.device_name, @@ -264,10 +421,7 @@ pub(crate) fn attach(shared: &SharedControl, driver: &impl Driver, timeout: Dura height = chosen.height, "Virtual display attached — capturing it" ); - *shared.vdd_output.lock() = Some(chosen.device_name); - let mut status = shared.vdd_status.lock(); - *status = VddStatus::Active; - shared.second_screen.lock().switch_pending = true; + note_attached(shared, chosen.device_name); } AttachResult::TimedOut => { warn!( @@ -281,11 +435,37 @@ pub(crate) fn attach(shared: &SharedControl, driver: &impl Driver, timeout: Dura driver.set_enabled(false); let mut status = shared.vdd_status.lock(); *status = VddStatus::Failed; - shared.second_screen.lock().note_toggle(driver.now()); + let mut state = shared.second_screen.lock(); + state.note_toggle(driver.now()); + state.failure = Some("attach_timeout"); } } } +/// Turn off a display this host turned on, unless a viewer wants it again. +#[cfg(any(windows, test))] +pub(crate) fn release(shared: &SharedControl, driver: &impl Driver) { + let wanted = || { + *shared.capture_target.lock() == CaptureTarget::VirtualExtended + && matches!( + *shared.vdd_status.lock(), + VddStatus::Active | VddStatus::Attaching + ) + }; + let _lifecycle = shared.vdd_lifecycle.lock(); + if shared.vdd_output.lock().is_none() || wanted() { + return; + } + wait_for_toggle_gap(shared, driver, &|| true); + if wanted() { + return; + } + driver.set_enabled(false); + *shared.vdd_output.lock() = None; + shared.second_screen.lock().note_toggle(driver.now()); + info!("Virtual display turned off"); +} + /// Wait out [`MIN_TOGGLE_GAP`]. False when the mode changed meanwhile. #[cfg(any(windows, test))] fn wait_for_toggle_gap( @@ -367,9 +547,11 @@ impl Driver for WindowsDriver<'_> { #[cfg(test)] mod tests { use super::*; + use crate::capture::reconcile_virtual_display; use crate::capture::tests::connect_viewer; use crate::transport::link::LinkId; use std::cell::{Cell, RefCell}; + use std::sync::atomic::Ordering; /// The iPad's connect timeout in 0.3.1 and 0.3.2: it disconnects when no /// frame has decoded this long after the handshake. @@ -538,10 +720,7 @@ mod tests { assert_eq!(toggles[0], (secs(8.0), true)); assert!(!toggles[1].1 && toggles[1].0 - toggles[0].0 >= MIN_TOGGLE_GAP); assert_eq!(*shared.vdd_status.lock(), VddStatus::Failed); - assert_eq!( - remembered_failure(*shared.vdd_status.lock()), - Some("attach_timeout") - ); + assert_eq!(remembered_failure(&shared), Some("attach_timeout")); // Reconnects for the next minute stay on the mirror. for second in 0..60 { @@ -651,7 +830,7 @@ mod tests { } #[test] - fn changing_the_mode_ends_the_start_and_clears_the_timers() { + fn a_mode_change_after_the_enable_turns_the_display_back_off() { let shared = second_screen_selected(); connect_viewer(&shared); assert_eq!(next_step(&shared, Instant::now()), Step::Attach); @@ -668,7 +847,266 @@ mod tests { "{:?}", driver.elapsed.get() ); - assert_eq!(driver.toggles.borrow().len(), 1, "only the Enable"); + let toggles = driver.toggles.borrow().clone(); + assert_eq!( + toggles.iter().map(|(_, on)| *on).collect::>(), + [true, false] + ); + assert_eq!(*shared.vdd_output.lock(), None); + assert_eq!(*shared.vdd_status.lock(), VddStatus::WaitingForClient); assert_eq!(next_step(&shared, driver.now()), Step::Nothing); } + + /// Review round 1, defect 3: Mirror then Second screen again during the + /// start, before any capture generation reconciled Mirror. The start must + /// not stay claimed with nobody running it. + #[test] + fn a_mode_change_during_the_start_never_leaves_it_claimed() { + let shared = second_screen_selected(); + connect_viewer(&shared); + let on_sleep = |_| {}; + let driver = FakeDriver::new(secs(3.0), None, &on_sleep); + // A change a moment ago: the start waits out the gap first. + shared.second_screen.lock().note_toggle(driver.now()); + assert_eq!(next_step(&shared, driver.now()), Step::Attach); + let mirror = Once::at(secs(2.0)); + let on_sleep = |elapsed: Duration| { + mirror.run(elapsed, || { + *shared.capture_target.lock() = CaptureTarget::PrimaryAuto + }) + }; + let driver = FakeDriver { + on_sleep: &on_sleep, + ..driver + }; + attach(&shared, &driver, ATTACH_TIMEOUT); + assert!(mirror.done.get()); + assert!(driver.toggles.borrow().is_empty()); + *shared.capture_target.lock() = CaptureTarget::VirtualExtended; + assert_eq!(*shared.vdd_status.lock(), VddStatus::WaitingForClient); + assert_eq!(next_step(&shared, driver.now()), Step::Attach); + } + + #[test] + fn try_again_recovers_a_start_whose_thread_is_gone() { + let shared = second_screen_selected(); + connect_viewer(&shared); + assert_eq!(next_step(&shared, Instant::now()), Step::Attach); + // Its thread is running: Try again leaves it alone. + shared.retry_second_screen(); + assert_eq!(*shared.vdd_status.lock(), VddStatus::Attaching); + // A claim with no thread behind it. + shared.second_screen.lock().attaching = false; + shared.retry_second_screen(); + assert_eq!(*shared.vdd_status.lock(), VddStatus::WaitingForClient); + assert_eq!(next_step(&shared, Instant::now()), Step::Attach); + } + + /// Review round 1, defect 1: the switch happened, then the capture + /// generation streaming the display only ever timed out (or read the + /// black placeholder). It goes back to the mirror, and stays there. + #[test] + fn a_second_screen_that_never_shows_an_image_goes_back_to_the_mirror() { + let shared = second_screen_selected(); + connect_viewer(&shared); + note_attached(&shared, "virtual".into()); + let start = Instant::now(); + assert_eq!(next_step(&shared, start), Step::Switch); + start_proving(&shared, 7, start); + let mut elapsed = Duration::ZERO; + while elapsed < PROVE_TIMEOUT { + assert_eq!(next_step(&shared, start + elapsed), Step::Nothing); + elapsed += secs(0.25); + } + assert_eq!( + next_step(&shared, start + PROVE_TIMEOUT), + Step::Fallback("no_frames") + ); + assert_eq!(*shared.vdd_status.lock(), VddStatus::Failed); + assert_eq!(remembered_failure(&shared), Some("no_frames")); + // Acquisition only timing out never sends the black placeholder. + assert!(!super::super::keepalive_allowed(false, true)); + assert!(super::super::keepalive_allowed(false, false)); + assert!(super::super::keepalive_allowed(true, true)); + // Later connections mirror without touching the driver. + for second in 1..30 { + disconnect(&shared); + connect_viewer(&shared); + let now = start + PROVE_TIMEOUT + Duration::from_secs(second); + assert_eq!(next_step(&shared, now), Step::Nothing); + } + let generation = crate::supervisor::CURRENT_GENERATION.load(Ordering::SeqCst); + assert_eq!( + reconcile_virtual_display(&CaptureTarget::VirtualExtended, &shared, generation), + Some(CaptureTarget::PrimaryAuto) + ); + } + + #[test] + fn a_duplication_error_on_the_second_screen_goes_back_to_the_mirror_at_once() { + let shared = second_screen_selected(); + connect_viewer(&shared); + note_attached(&shared, "virtual".into()); + let start = Instant::now(); + assert_eq!(next_step(&shared, start), Step::Switch); + start_proving(&shared, 7, start); + // An older generation's error is not this display's. + note_capture_error(&shared, 6); + assert_eq!(next_step(&shared, start), Step::Nothing); + // DuplicateOutput returned DXGI_ERROR_UNSUPPORTED. + note_capture_error(&shared, 7); + assert_eq!( + next_step(&shared, start), + Step::Fallback("duplication_failed") + ); + assert_eq!(remembered_failure(&shared), Some("duplication_failed")); + } + + #[test] + fn a_second_screen_that_shows_an_image_stays() { + let shared = second_screen_selected(); + connect_viewer(&shared); + note_attached(&shared, "virtual".into()); + let start = Instant::now(); + assert_eq!(next_step(&shared, start), Step::Switch); + start_proving(&shared, 7, start); + note_image(&shared, 6); + note_image(&shared, 7); + note_capture_error(&shared, 7); + for second in 0..60 { + let now = start + Duration::from_secs(second); + assert_eq!(next_step(&shared, now), Step::Nothing); + } + assert_eq!(*shared.vdd_status.lock(), VddStatus::Active); + } + + #[test] + fn a_fallback_turns_the_display_off_after_the_gap() { + let shared = second_screen_selected(); + let on_sleep = |_| {}; + let driver = FakeDriver::new(secs(3.0), Some(Duration::ZERO), &on_sleep); + note_attached(&shared, "virtual".into()); + shared.second_screen.lock().note_toggle(driver.now()); + *shared.vdd_status.lock() = VddStatus::Failed; + release(&shared, &driver); + assert_eq!(*driver.toggles.borrow(), vec![(MIN_TOGGLE_GAP, false)]); + assert_eq!(*shared.vdd_output.lock(), None); + // A second request finds nothing left to turn off. + release(&shared, &driver); + assert_eq!(driver.toggles.borrow().len(), 1); + } + + /// Review round 1, defect 2: no capture generation may wait for the + /// driver lock, whatever the status, the mode or the viewer. Before, a + /// generation that read `WaitingForClient` (Try again had just reset it) + /// waited for the lock behind the attach thread's whole Enable. + #[test] + fn mirror_startup_never_waits_for_the_driver_lock() { + let shared = second_screen_selected(); + let generation = crate::supervisor::CURRENT_GENERATION.load(Ordering::SeqCst); + let _held = shared.vdd_lifecycle.lock(); + for target in [ + CaptureTarget::VirtualExtended, + CaptureTarget::PrimaryAuto, + CaptureTarget::Output("physical".into()), + ] { + for status in [ + VddStatus::Inactive, + VddStatus::WaitingForClient, + VddStatus::Attaching, + VddStatus::Active, + VddStatus::Failed, + VddStatus::TaskFailed, + ] { + for connected in [false, true] { + if connected != shared.client_connected() { + if connected { + connect_viewer(&shared); + } else { + disconnect(&shared); + } + } + *shared.vdd_status.lock() = status; + let (done, finished) = std::sync::mpsc::channel(); + let worker = shared.clone(); + let target = target.clone(); + std::thread::spawn(move || { + let _ = done.send(reconcile_virtual_display(&target, &worker, generation)); + }); + let chosen = finished + .recv_timeout(Duration::from_secs(2)) + .unwrap_or_else(|_| panic!("{status:?} connected={connected} waited")); + assert!(chosen.is_some(), "{status:?}"); + } + } + } + } + + /// The same, with the real attach thread: Try again after a failure, its + /// thread blocked in a slow Enable, and a new capture generation. + #[test] + fn try_again_during_a_slow_enable_still_mirrors_at_once() { + /// Blocks in Enable until released, on a paused clock otherwise. + struct SlowEnable { + entered: std::sync::mpsc::SyncSender<()>, + release: std::sync::mpsc::Receiver<()>, + start: Instant, + elapsed: Cell, + } + impl Driver for SlowEnable { + fn outputs(&self) -> Vec { + vec![output("primary", true)] + } + fn prepare(&self) {} + fn set_enabled(&self, enabled: bool) -> bool { + if enabled { + let _ = self.entered.send(()); + let _ = self.release.recv(); + } + true + } + fn now(&self) -> Instant { + self.start + self.elapsed.get() + } + fn sleep(&self, duration: Duration) { + self.elapsed.set(self.elapsed.get() + duration); + } + } + let shared = second_screen_selected(); + connect_viewer(&shared); + *shared.vdd_status.lock() = VddStatus::Failed; + shared.retry_second_screen(); + assert_eq!(next_step(&shared, Instant::now()), Step::Attach); + let (entered_tx, entered) = std::sync::mpsc::sync_channel(1); + let (release, release_rx) = std::sync::mpsc::channel(); + let worker = shared.clone(); + let attach_thread = std::thread::spawn(move || { + let driver = SlowEnable { + entered: entered_tx, + release: release_rx, + start: Instant::now(), + elapsed: Cell::new(Duration::ZERO), + }; + attach(&worker, &driver, Duration::ZERO); + }); + entered.recv_timeout(Duration::from_secs(5)).unwrap(); + let generation = crate::supervisor::CURRENT_GENERATION.load(Ordering::SeqCst); + let (done, finished) = std::sync::mpsc::channel(); + let capture = shared.clone(); + std::thread::spawn(move || { + let _ = done.send(reconcile_virtual_display( + &CaptureTarget::VirtualExtended, + &capture, + generation, + )); + }); + assert_eq!( + finished.recv_timeout(Duration::from_secs(2)).unwrap(), + Some(CaptureTarget::PrimaryAuto) + ); + release.send(()).unwrap(); + attach_thread.join().unwrap(); + // No output ever appeared: the start failed and is remembered. + assert_eq!(*shared.vdd_status.lock(), VddStatus::Failed); + } } diff --git a/host/src/control.rs b/host/src/control.rs index 3f57a2f6..90d85d26 100644 --- a/host/src/control.rs +++ b/host/src/control.rs @@ -44,7 +44,6 @@ pub struct SharedControl { #[cfg(any(windows, test))] pub(crate) vdd_lifecycle: Arc>, /// Output owned by the enabled driver, retained across pipeline restarts. - #[cfg(any(windows, test))] pub(crate) vdd_output: Arc>>, /// Second screen timing across pipeline restarts: the pending switch onto /// it, the grace period after its viewer left, the last driver change. @@ -156,7 +155,6 @@ impl SharedControl { capture_block: Arc::new(Mutex::new(None)), #[cfg(any(windows, test))] vdd_lifecycle: Arc::new(Mutex::new(())), - #[cfg(any(windows, test))] vdd_output: Arc::new(Mutex::new(None)), second_screen: Arc::new(Mutex::new(Default::default())), hb_capture_loop_ms: Arc::new(AtomicU64::new(0)), @@ -199,10 +197,7 @@ impl SharedControl { /// The user asked to try again: a second screen that failed to start is /// tried once more at the next connection. pub(crate) fn retry_second_screen(&self) { - let mut status = self.vdd_status.lock(); - if matches!(*status, VddStatus::Failed | VddStatus::TaskFailed) { - *status = VddStatus::WaitingForClient; - } + crate::capture::second_screen::retry(self); } } diff --git a/host/src/transport/mod.rs b/host/src/transport/mod.rs index 10e81ec0..e823f8ff 100644 --- a/host/src/transport/mod.rs +++ b/host/src/transport/mod.rs @@ -663,8 +663,7 @@ async fn execute_actions( stats.reset_connection_stats(); } if *shared.capture_target.lock() == CaptureTarget::VirtualExtended { - let status = *shared.vdd_status.lock(); - if let Some(reason) = second_screen::remembered_failure(status) { + if let Some(reason) = second_screen::remembered_failure(shared) { info!( second_screen_fallback = reason, "Client connected, but the second screen failed to start earlier — \ @@ -712,24 +711,35 @@ async fn execute_actions( } let restart = match second_screen::next_step(shared, Instant::now()) { - second_screen::Step::Nothing => None, + second_screen::Step::Nothing => false, second_screen::Step::Attach => { info!( "Client connected with extended display selected — mirroring the primary \ display while the virtual display starts" ); second_screen::start(shared.clone()); - None + false } second_screen::Step::Switch => { - Some("Virtual display ready — restarting pipeline to stream it") + info!("Virtual display ready — restarting pipeline to stream it"); + true } second_screen::Step::Release => { - Some("No iPad came back to the virtual display — restarting pipeline to turn it off") + info!("No iPad came back to the virtual display — restarting pipeline to turn it off"); + true + } + second_screen::Step::Fallback(reason) => { + warn!( + second_screen_fallback = reason, + prove_timeout_ms = second_screen::PROVE_TIMEOUT.as_millis() as u64, + "The virtual display showed no picture — restarting pipeline to mirror the \ + primary display instead" + ); + second_screen::turn_off(shared.clone()); + true } }; - if let Some(reason) = restart { - info!("{reason}"); + if restart { shared.stop(); if let Err(error) = supervisor_tx.send(SupervisorCommand::Restart) { warn!(error = %error, "Failed to request pipeline restart for the virtual display"); From 7b5d9c5fa50fa1f021c16e1d6b91a5db3f4d87d0 Mon Sep 17 00:00:00 2001 From: Ali Younes Date: Mon, 26 Oct 2026 20:02:53 -0400 Subject: [PATCH 07/16] Decode whole frames over USB across the second screen switch and its fallback --- host/tests/e2e_synthetic.rs | 116 +++++++++++++++++++++++++++++------- 1 file changed, 94 insertions(+), 22 deletions(-) diff --git a/host/tests/e2e_synthetic.rs b/host/tests/e2e_synthetic.rs index 59d6123b..a18cbac7 100644 --- a/host/tests/e2e_synthetic.rs +++ b/host/tests/e2e_synthetic.rs @@ -264,40 +264,48 @@ fn usb_framed_stream_end_to_end() { // gives up 10 s after the handshake, and over USB it was dialed again // into the same restart. Here the start fails at once (no driver // with synthetic capture), and the reconnect after it mirrors too. + use eternal_host::capture::second_screen; use eternal_host::control::{CaptureTarget, VddStatus}; use eternal_host::supervisor::CURRENT_GENERATION; *shared.capture_target.lock() = CaptureTarget::VirtualExtended; *shared.vdd_status.lock() = VddStatus::WaitingForClient; - let original_generation = CURRENT_GENERATION.load(Ordering::SeqCst); + let generation = CURRENT_GENERATION.load(Ordering::SeqCst); let ControlMessage::Hello2(mut next_hello) = hello else { unreachable!() }; + let mut session = 0; for attempt in 1..=2 { next_hello.client_nonce += 1; let sent = Instant::now(); write_datagram(&mut stream, &encode_control(0, 1, &ControlMessage::Hello2(next_hello.clone()))).await.unwrap(); - let mut session = None; - let mut first_keyframe = None; - while first_keyframe.is_none() { - assert!(sent.elapsed() < Duration::from_secs(5), "attempt {attempt}: no mirror within 5 s"); - let bytes = tokio::time::timeout(Duration::from_secs(2), read_datagram(&mut stream)) - .await - .expect("the tunnel stays active") - .expect("the tunnel stays open"); - if let Ok((_, ControlMessage::HelloAck(ack))) = parse_control(&bytes) { - assert_eq!(ack.status, HelloStatus::Ok); - session = Some(ack.session_id); - } else if let Ok((header, _)) = MediaHeader::decode(&bytes) { - if Some(header.session_id) == session && header.is_keyframe && header.frag_index == 0 { - assert_eq!(header.stream_epoch, epoch, "the mirror comes from the running pipeline"); - first_keyframe = Some(sent.elapsed()); - } - } - } - assert_eq!(CURRENT_GENERATION.load(Ordering::SeqCst), original_generation, + let (frame_epoch, decoded) = + first_decoded_frame(&mut stream, &mut session, &mut msg_seq, None, sent + Duration::from_secs(5)).await; + assert_eq!(frame_epoch, epoch, "the mirror comes from the running pipeline"); + assert_eq!(CURRENT_GENERATION.load(Ordering::SeqCst), generation, "attempt {attempt}: connecting must not restart the pipeline"); assert_eq!(*shared.vdd_status.lock(), VddStatus::TaskFailed); - eprintln!("USB_SECOND_SCREEN_MIRROR attempt={attempt} first_keyframe_ms={}", - first_keyframe.unwrap().as_millis()); + eprintln!("USB_SECOND_SCREEN_MIRROR attempt={attempt} first_decoded_ms={}", + decoded.duration_since(sent).as_millis()); } + + // The display attached: the pipeline restarts onto it, and the USB + // tunnel carries the new stream epoch without reconnecting. + second_screen::note_attached(&shared, "virtual".into()); + let switched = Instant::now(); + let (second_epoch, decoded) = first_decoded_frame( + &mut stream, &mut session, &mut msg_seq, Some(epoch), switched + Duration::from_secs(5)).await; + assert_eq!(CURRENT_GENERATION.load(Ordering::SeqCst), generation + 1, "one restart onto it"); + assert_eq!(*shared.vdd_status.lock(), VddStatus::Active); + eprintln!("USB_SECOND_SCREEN_SWITCH first_decoded_ms={}", decoded.duration_since(switched).as_millis()); + + // Its capture failed before it showed an image (DuplicateOutput + // returned an error): back to the mirror on the same tunnel. + second_screen::start_proving(&shared, generation + 1, Instant::now()); + second_screen::note_capture_error(&shared, generation + 1); + let failed = Instant::now(); + let (_, decoded) = first_decoded_frame( + &mut stream, &mut session, &mut msg_seq, Some(second_epoch), failed + Duration::from_secs(5)).await; + assert_eq!(CURRENT_GENERATION.load(Ordering::SeqCst), generation + 2, "one restart back"); + assert_eq!(*shared.vdd_status.lock(), VddStatus::Failed); + eprintln!("USB_SECOND_SCREEN_FALLBACK first_decoded_ms={}", decoded.duration_since(failed).as_millis()); *shared.capture_target.lock() = CaptureTarget::PrimaryAuto; *shared.vdd_status.lock() = VddStatus::Inactive; drop(stream); @@ -325,6 +333,70 @@ fn usb_framed_stream_end_to_end() { } } +/// Reads the USB tunnel until a whole frame of the current session decodes, +/// sending receiver reports so the host keeps the session. Frames of +/// `skip_epoch` (the stream before a restart) are ignored. Returns the +/// decoded frame's stream epoch and when it decoded. +async fn first_decoded_frame( + stream: &mut tokio::net::TcpStream, + session: &mut u32, + msg_seq: &mut u32, + skip_epoch: Option, + deadline: Instant, +) -> (u32, Instant) { + use eternal_host::transport::link::{read_datagram, write_datagram}; + let mut reassembler = Reassembler::new(); + let mut decoder = H264TestDecoder::new(); + let mut last_report = Instant::now(); + let mut highest_seq = 0; + let mut current_epoch = 0; + loop { + assert!(Instant::now() < deadline, "no decodable frame in time"); + if *session != 0 && last_report.elapsed() >= Duration::from_millis(300) { + last_report = Instant::now(); + *msg_seq += 1; + let report = ControlMessage::ReceiverReport(ReceiverReport { + stream_epoch: current_epoch, + highest_seq, + decode_fps_x10: 600, + ..ReceiverReport::default() + }); + write_datagram(stream, &encode_control(*session, *msg_seq, &report)) + .await + .unwrap(); + } + let bytes = tokio::time::timeout(Duration::from_secs(2), read_datagram(stream)) + .await + .expect("the tunnel stays active") + .expect("the tunnel stays open"); + if let Ok((_, ControlMessage::HelloAck(ack))) = parse_control(&bytes) { + assert_eq!(ack.status, HelloStatus::Ok); + *session = ack.session_id; + continue; + } + let Ok((header, payload)) = MediaHeader::decode(&bytes) else { + continue; + }; + if header.session_id != *session || Some(header.stream_epoch) == skip_epoch { + continue; + } + current_epoch = header.stream_epoch; + highest_seq = header.frame_seq; + if let AddOutcome::Completed(frame) = reassembler.add_fragment( + header.frame_seq, + header.frag_index, + header.frag_count, + header.stream_epoch, + payload, + Instant::now(), + ) { + if !decoder.decode(&frame).is_empty() { + return (header.stream_epoch, Instant::now()); + } + } + } +} + fn free_udp_port() -> u16 { UdpSocket::bind("127.0.0.1:0") .and_then(|s| s.local_addr()) From 48143ff7d483b9108b40f1dd9b2d4e6d337b19cd Mon Sep 17 00:00:00 2001 From: Ali Younes Date: Mon, 26 Oct 2026 20:02:53 -0400 Subject: [PATCH 08/16] Describe the second screen's image check --- docs/decisions.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/decisions.md b/docs/decisions.md index 3f3038d9..b179ee62 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -122,7 +122,10 @@ triggers via `schtasks`, avoiding a per-toggle UAC prompt. startup, panic, and 10 s after client loss (the v2 liveness signal finally made idle teardown possible; the grace period lets a quick reconnect keep it). - The iPad mirrors the primary display while the virtual display starts, and - the stream restarts onto it once it attaches. The iPad gives up 10 s after + the stream restarts onto it once it attaches. That capture must deliver a + real image within 5 s; a display that fails to duplicate or only times out + goes back to the mirror and counts as a failed start. Capture never waits + for the driver: separate threads turn the display on and off. The iPad gives up 10 s after the handshake without a frame, which a slow Enable task plus the attach wait could exceed. A start that fails is remembered until the user clicks Try again, restarts the stream or changes the mode, so reconnects mirror From 44de2f2a26985527b30712c22c21c715f23c09d0 Mon Sep 17 00:00:00 2001 From: Ali Younes Date: Mon, 26 Oct 2026 20:05:20 -0400 Subject: [PATCH 09/16] Keep unit tests away from the installed display driver tasks --- host/src/capture/second_screen.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/host/src/capture/second_screen.rs b/host/src/capture/second_screen.rs index 65ceaa4b..dcc11fc2 100644 --- a/host/src/capture/second_screen.rs +++ b/host/src/capture/second_screen.rs @@ -256,8 +256,9 @@ pub fn note_capture_error(shared: &SharedControl, generation: u64) { /// Turn the display on beside the mirror. Returns at once. pub(crate) fn start(shared: SharedControl) { + // Unit tests never reach the installed driver tasks. #[cfg(windows)] - if !super::synthetic_requested() { + if !cfg!(test) && !super::synthetic_requested() { let worker = shared.clone(); let spawned = std::thread::Builder::new() .name("second-screen".into()) @@ -293,8 +294,9 @@ pub(crate) fn start(shared: SharedControl) { /// Turn the display off on its own thread, so capture never waits for the /// driver. Returns at once. pub(crate) fn turn_off(shared: SharedControl) { + // Unit tests never reach the installed driver tasks. #[cfg(windows)] - if !super::synthetic_requested() { + if !cfg!(test) && !super::synthetic_requested() { let worker = shared.clone(); let spawned = std::thread::Builder::new() .name("second-screen-off".into()) From c504b628b11a1a20f7484f62be57d8da8e9d746b Mon Sep 17 00:00:00 2001 From: Ali Younes Date: Mon, 26 Oct 2026 20:58:08 -0400 Subject: [PATCH 10/16] Change the virtual display on one worker thread, and keep a failed second screen failed across restarts --- host/src/capture/dxgi.rs | 146 +++--- host/src/capture/mod.rs | 52 +- host/src/capture/second_screen.rs | 806 ++++++++++++++++++++++++------ host/src/control.rs | 10 +- host/src/main.rs | 6 +- host/src/supervisor.rs | 10 +- host/src/transport/mod.rs | 4 +- 7 files changed, 795 insertions(+), 239 deletions(-) diff --git a/host/src/capture/dxgi.rs b/host/src/capture/dxgi.rs index 41840a29..b44210df 100644 --- a/host/src/capture/dxgi.rs +++ b/host/src/capture/dxgi.rs @@ -16,8 +16,9 @@ use super::recovery::{ }; use super::{ client_connected, frame_budget_for, frame_has_image, heartbeat, idle_heartbeat, - keepalive_allowed, reconcile_virtual_display, recovery_poll, resolve_target, FrameSlot, - FrameUse, IdleGate, OutputInfo, RawFrame, ACQUIRE_TIMEOUT_MS, FPS_WINDOW, IDLE_KEEPALIVE, + keepalive_allowed, read_back_image, reconcile_virtual_display, recovery_poll, resolve_target, + FrameSlot, FrameUse, IdleGate, OutputInfo, RawFrame, ACQUIRE_TIMEOUT_MS, FPS_WINDOW, + IDLE_KEEPALIVE, }; use crate::control::{CaptureTarget, SharedControl}; use crate::stats::PIPELINE_STATS; @@ -363,6 +364,9 @@ pub fn run_capture_loop( match hr { Ok(()) => { *frame_number += 1; + if !super::second_screen::PROOF_NEEDS_IMAGE { + super::second_screen::note_image(&shared, generation); + } // --- Track cursor position/shape (must read shape before ReleaseFrame) --- if frame_info.LastMouseUpdateTime != 0 { @@ -416,90 +420,86 @@ pub fn run_capture_loop( } continue; } - if !seen_image { - super::second_screen::note_image(&shared, generation); - } - seen_image = true; + let frame_data = read_back_image(&shared, generation, &mut seen_image, || { + windows::core::Result::Ok(if let Some(ref resource) = desktop_resource { + match resource.cast::() { + Ok(desktop_texture) => { + unsafe { + device_context.CopyResource(&staging_texture, &desktop_texture); + } - let frame_data: Option>> = if let Some(ref resource) = desktop_resource - { - match resource.cast::() { - Ok(desktop_texture) => { - unsafe { - device_context.CopyResource(&staging_texture, &desktop_texture); - } + // Release acquired frame — staging texture has its own copy + unsafe { + dupl.ReleaseFrame()?; + } - // Release acquired frame — staging texture has its own copy - unsafe { - dupl.ReleaseFrame()?; - } + let mut pixel_buf = + match spare.take().and_then(|arc| Arc::try_unwrap(arc).ok()) { + Some(buf) if buf.len() == frame_bytes => buf, + _ => vec![0u8; frame_bytes], + }; - let mut pixel_buf = - match spare.take().and_then(|arc| Arc::try_unwrap(arc).ok()) { - Some(buf) if buf.len() == frame_bytes => buf, - _ => vec![0u8; frame_bytes], - }; - - // Map staging texture for CPU read - let mut mapped = D3D11_MAPPED_SUBRESOURCE::default(); - unsafe { - device_context.Map( - &staging_texture, - 0, - D3D11_MAP_READ, - 0, - Some(&mut mapped), - )?; - } + // Map staging texture for CPU read + let mut mapped = D3D11_MAPPED_SUBRESOURCE::default(); + unsafe { + device_context.Map( + &staging_texture, + 0, + D3D11_MAP_READ, + 0, + Some(&mut mapped), + )?; + } + + // Copy row-by-row (pitch may differ from width*4) + let src_pitch = mapped.RowPitch as usize; + let src_ptr = mapped.pData as *const u8; + for row in 0..tex_height as usize { + let src_offset = row * src_pitch; + let dst_offset = row * row_bytes; + unsafe { + std::ptr::copy_nonoverlapping( + src_ptr.add(src_offset), + pixel_buf.as_mut_ptr().add(dst_offset), + row_bytes, + ); + } + } - // Copy row-by-row (pitch may differ from width*4) - let src_pitch = mapped.RowPitch as usize; - let src_ptr = mapped.pData as *const u8; - for row in 0..tex_height as usize { - let src_offset = row * src_pitch; - let dst_offset = row * row_bytes; unsafe { - std::ptr::copy_nonoverlapping( - src_ptr.add(src_offset), - pixel_buf.as_mut_ptr().add(dst_offset), + device_context.Unmap(&staging_texture, 0); + } + + // Composite the mouse cursor into the BGRA frame. + if cursor_visible && cursor.has_shape { + draw_cursor( + &mut pixel_buf, + tex_width, + tex_height, row_bytes, + &cursor, + cursor_x, + cursor_y, ); } - } - unsafe { - device_context.Unmap(&staging_texture, 0); + Some(Arc::new(pixel_buf)) } - - // Composite the mouse cursor into the BGRA frame. - if cursor_visible && cursor.has_shape { - draw_cursor( - &mut pixel_buf, - tex_width, - tex_height, - row_bytes, - &cursor, - cursor_x, - cursor_y, - ); + Err(e) => { + warn!(error = %e, "Failed to cast IDXGIResource to ID3D11Texture2D"); + unsafe { + dupl.ReleaseFrame()?; + } + None } - - Some(Arc::new(pixel_buf)) } - Err(e) => { - warn!(error = %e, "Failed to cast IDXGIResource to ID3D11Texture2D"); - unsafe { - dupl.ReleaseFrame()?; - } - None + } else { + unsafe { + dupl.ReleaseFrame()?; } - } - } else { - unsafe { - dupl.ReleaseFrame()?; - } - None - }; + None + }) + })?; if let Some(data) = frame_data { if idle.on_frame(client_connected(&shared), now) == FrameUse::Publish { diff --git a/host/src/capture/mod.rs b/host/src/capture/mod.rs index f071779f..c7f3d7fb 100644 --- a/host/src/capture/mod.rs +++ b/host/src/capture/mod.rs @@ -12,7 +12,7 @@ use std::time::{Duration, Instant}; use parking_lot::{Condvar, Mutex}; use tracing::error; #[cfg(any(windows, test))] -use tracing::info; +use tracing::{info, warn}; use crate::control::SharedControl; #[cfg(any(windows, test))] @@ -361,7 +361,7 @@ pub(crate) fn client_connected(shared: &SharedControl) -> bool { /// a viewer is connected (or one left less than [`second_screen::RELEASE_GRACE`] ago), and /// mirrors `PrimaryAuto` otherwise: while [`second_screen`] turns the display on beside the /// mirror, after it failed to, and with nobody connected. Any other target turns the display -/// off. Never waits for the driver: the [`second_screen`] threads change it, and the mirror +/// off. Never waits for the driver: the [`second_screen`] worker changes it, and the mirror /// must not wait behind a slow Enable or attach. #[cfg(any(windows, test))] pub(crate) fn reconcile_virtual_display( @@ -375,6 +375,17 @@ pub(crate) fn reconcile_virtual_display( let owned = shared.vdd_output.lock().clone(); match target { CaptureTarget::VirtualExtended => { + // A restart must not erase a second screen that already failed + // to show an image: it goes back to the mirror now. + if let Some(reason) = second_screen::take_unproven(shared, Instant::now()) { + warn!( + second_screen_fallback = reason, + prove_timeout_ms = second_screen::PROVE_TIMEOUT.as_millis() as u64, + "The virtual display showed no picture — mirroring the primary display instead" + ); + second_screen::send(shared, second_screen::Command::Release); + return Some(CaptureTarget::PrimaryAuto); + } let connected = shared.client_connected(); let status = *shared.vdd_status.lock(); if matches!( @@ -399,29 +410,31 @@ pub(crate) fn reconcile_virtual_display( return Some(CaptureTarget::Output(name)); } } + // The transport's next tick starts the display beside this mirror. + // A viewer that arrived meanwhile may already have claimed it. + { + let mut status = shared.vdd_status.lock(); + if *status != VddStatus::Attaching { + *status = VddStatus::WaitingForClient; + } + } if !connected { info!( "Extended display selected but no iPad has connected yet — leaving the \ virtual display off and mirroring the primary display until a client registers" ); if owned.is_some() { - second_screen::turn_off(shared.clone()); + second_screen::send(shared, second_screen::Command::Release); } } - // The transport's next tick starts the display beside this mirror. - // A viewer that arrived meanwhile may already have claimed it. - let mut status = shared.vdd_status.lock(); - if *status != VddStatus::Attaching { - *status = VddStatus::WaitingForClient; - } Some(CaptureTarget::PrimaryAuto) } other => { // Any non-virtual target: ensure the virtual display is off. + *shared.vdd_status.lock() = VddStatus::Inactive; if owned.is_some() { - second_screen::turn_off(shared.clone()); + second_screen::send(shared, second_screen::Command::Release); } - *shared.vdd_status.lock() = VddStatus::Inactive; Some(other.clone()) } } @@ -436,6 +449,23 @@ pub(crate) fn keepalive_allowed(seen_image: bool, second_screen: bool) -> bool { seen_image || !second_screen } +/// Reads back an acquired frame for the encoder. Only an image that was read +/// back counts as the second screen's proof: when the readback fails, the +/// proof stays pending for the capture error that follows. +#[cfg(any(windows, test))] +pub(crate) fn read_back_image( + shared: &SharedControl, + generation: u64, + seen_image: &mut bool, + read: impl FnOnce() -> Result, E>, +) -> Result, E> { + let image = read()?; + if image.is_some() && !std::mem::replace(seen_image, true) { + second_screen::note_image(shared, generation); + } + Ok(image) +} + /// Runs the platform capture loop on the CURRENT thread until stop/close, /// publishing frames into `slot`. The supervisor spawns and monitors this. pub fn run_capture_stage( diff --git a/host/src/capture/second_screen.rs b/host/src/capture/second_screen.rs index dcc11fc2..78b04913 100644 --- a/host/src/capture/second_screen.rs +++ b/host/src/capture/second_screen.rs @@ -9,18 +9,23 @@ //! for every attempt. //! //! Now a connecting iPad gets the mirror at once, from the capture generation -//! already running. Threads here turn the display on and off, so capture -//! never waits for the driver. Once the display attaches the transport -//! restarts the pipeline onto it, and the capture generation that streams it -//! must deliver a real image within [`PROVE_TIMEOUT`]; otherwise the stream -//! goes back to the mirror. A start that failed is remembered: later -//! connections stay on the mirror until the user clicks Try again, restarts -//! the stream, or changes the mode. - +//! already running. One worker thread here turns the display on and off, fed +//! by a queue, so capture never waits for the driver, and the gap between +//! changes, a start cancelled by a mode change and quitting are decided in +//! one place. Once the display attaches the transport restarts the pipeline +//! onto it, and the second screen must deliver a real image within +//! [`PROVE_TIMEOUT`] of that switch, across any restarts meanwhile; +//! otherwise the stream goes back to the mirror. A start that failed is +//! remembered: later connections stay on the mirror until the user clicks +//! Try again, restarts the stream, or changes the mode. + +use std::collections::VecDeque; use std::time::{Duration, Instant}; +use parking_lot::{Condvar, Mutex}; #[cfg(any(windows, test))] -use tracing::{info, warn}; +use tracing::info; +use tracing::warn; use crate::control::{CaptureTarget, SharedControl, VddStatus}; @@ -39,12 +44,18 @@ pub(crate) const RELEASE_GRACE: Duration = Duration::from_secs(10); #[cfg(any(windows, test))] pub(crate) const MIN_TOGGLE_GAP: Duration = Duration::from_secs(10); -/// How long a capture generation streaming the second screen has to capture -/// a real image of it. Until then the iPad keeps showing the last mirror -/// frame; a display that fails to duplicate, or only ever times out, goes -/// back to the mirror after this. +/// How long the second screen has, from the switch onto it, to capture a +/// real image. Until then the iPad keeps showing the last mirror frame; a +/// display that fails to duplicate, or only ever times out, goes back to the +/// mirror after this. pub const PROVE_TIMEOUT: Duration = Duration::from_secs(5); +/// What proves the second screen. True: an image read back for the encoder +/// from a frame Windows presented (`LastPresentTime != 0`). False: any frame +/// the duplication delivers, even one that only moves the pointer. +#[cfg(windows)] +pub(crate) const PROOF_NEEDS_IMAGE: bool = true; + #[cfg(any(windows, test))] const WAIT_STEP: Duration = Duration::from_millis(100); @@ -54,6 +65,13 @@ const WAIT_STEP: Duration = Duration::from_millis(100); #[cfg(any(windows, test))] const STOPPED_WAIT: Duration = Duration::from_secs(6); +/// How long quitting waits for the worker to exit after the last Enable. +const QUIT_JOIN: Duration = Duration::from_secs(5); + +/// How long quitting waits for an Enable in flight: the driver task's own +/// limit is 30 s. +const QUIT_ENABLE_WAIT: Duration = Duration::from_secs(35); + /// What the transport does next for the second screen. #[derive(Debug, PartialEq, Eq)] pub(crate) enum Step { @@ -68,7 +86,9 @@ pub(crate) enum Step { Fallback(&'static str), } -/// A capture generation streaming the second screen, before its first image. +/// The second screen being streamed, before its first image. It outlives the +/// capture generation that started it: a restart meanwhile keeps the +/// deadline and any capture failure. #[derive(Debug, Clone, Copy)] struct Proving { generation: u64, @@ -86,7 +106,7 @@ pub(crate) struct State { release_requested: bool, /// When the host last turned the display on or off. last_toggle: Option, - /// A thread is turning the display on. + /// A start is claimed: its Attach is queued or running. attaching: bool, proving: Option, /// Why the second screen last failed, for the log and the backend. @@ -109,24 +129,8 @@ impl State { }; return Step::Nothing; } - if let Some(proving) = self.proving { - let reason = if proving.capture_failed { - Some("duplication_failed") - } else if now.duration_since(proving.since) >= PROVE_TIMEOUT { - Some("no_frames") - } else { - None - }; - if let Some(reason) = reason { - *self = State { - last_toggle: self.last_toggle, - attaching: self.attaching, - failure: Some(reason), - ..State::default() - }; - *status = VddStatus::Failed; - return Step::Fallback(reason); - } + if let Some(reason) = self.fail_unproven(status, now) { + return Step::Fallback(reason); } match *status { VddStatus::WaitingForClient if connected => { @@ -157,6 +161,27 @@ impl State { } } + /// End a second screen that failed to prove itself: a capture error, or + /// no image within [`PROVE_TIMEOUT`] of the switch. Returns why. + fn fail_unproven(&mut self, status: &mut VddStatus, now: Instant) -> Option<&'static str> { + let proving = self.proving?; + let reason = if proving.capture_failed { + "duplication_failed" + } else if now.duration_since(proving.since) >= PROVE_TIMEOUT { + "no_frames" + } else { + return None; + }; + *self = State { + last_toggle: self.last_toggle, + attaching: self.attaching, + failure: Some(reason), + ..State::default() + }; + *status = VddStatus::Failed; + Some(reason) + } + /// Whether a capture generation with no viewer should leave an attached /// display on: only the grace period's end turns it off. #[cfg(any(windows, test))] @@ -190,6 +215,23 @@ pub(crate) fn next_step(shared: &SharedControl, now: Instant) -> Step { .step(selected, &mut status, connected, now) } +/// A new capture generation ends a second screen that already failed to +/// prove itself, before anything else: a supervisor restart (a capture +/// error that ended the stage, or the watchdog) must not erase the failure. +/// Returns why it failed. +#[cfg(any(windows, test))] +pub(crate) fn take_unproven(shared: &SharedControl, now: Instant) -> Option<&'static str> { + let mut status = shared.vdd_status.lock(); + shared.second_screen.lock().fail_unproven(&mut status, now) +} + +/// Whether the second screen is still proving itself. Nothing is sent from +/// it meanwhile, so the watchdog leaves the verdict to [`next_step`] and +/// [`take_unproven`]. +pub(crate) fn proving(shared: &SharedControl) -> bool { + shared.second_screen.lock().proving.is_some() +} + /// Why connections mirror instead of extending, after a failed start. The /// value appears as `second_screen_fallback=` in the log, which the /// diagnostics upload sends as is. @@ -220,31 +262,44 @@ pub fn note_attached(shared: &SharedControl, device_name: String) { *shared.vdd_output.lock() = Some(device_name); let mut status = shared.vdd_status.lock(); *status = VddStatus::Active; - shared.second_screen.lock().switch_pending = true; + let mut state = shared.second_screen.lock(); + state.switch_pending = true; + state.proving = None; } -/// Capture `generation` streams the second screen from `now`, and must -/// capture an image of it within [`PROVE_TIMEOUT`]. +/// Capture `generation` streams the second screen from `now`. The first +/// generation after the switch starts the [`PROVE_TIMEOUT`] deadline; a +/// later one inherits it, and any capture failure, until an image arrives. pub fn start_proving(shared: &SharedControl, generation: u64, now: Instant) { - shared.second_screen.lock().proving = Some(Proving { - generation, - since: now, - capture_failed: false, - }); + let mut state = shared.second_screen.lock(); + match state.proving.as_mut() { + Some(proving) => proving.generation = generation, + None => { + state.proving = Some(Proving { + generation, + since: now, + capture_failed: false, + }) + } + } } -/// Capture `generation` captured a real image. +/// Capture `generation` captured a real image, ready for the encoder. pub fn note_image(shared: &SharedControl, generation: u64) { let mut state = shared.second_screen.lock(); - if state.proving.is_some_and(|p| p.generation == generation) { + if let Some(proving) = state.proving.filter(|p| p.generation == generation) { state.proving = None; - tracing::info!("Second screen captured its first image — streaming it"); + tracing::info!( + after_ms = proving.since.elapsed().as_millis() as u64, + "Second screen captured its first image — streaming it" + ); } } /// Capture `generation` failed to open or read its display, for example /// DuplicateOutput returned DXGI_ERROR_UNSUPPORTED. On the second screen, -/// before its first image, that ends the attempt at the next tick. +/// before its first image, that ends the attempt at the next tick, or when +/// the next capture generation starts. pub fn note_capture_error(shared: &SharedControl, generation: u64) { let mut state = shared.second_screen.lock(); if let Some(proving) = state.proving.as_mut() { @@ -254,59 +309,176 @@ pub fn note_capture_error(shared: &SharedControl, generation: u64) { } } -/// Turn the display on beside the mirror. Returns at once. -pub(crate) fn start(shared: SharedControl) { - // Unit tests never reach the installed driver tasks. +/// What the virtual display worker is asked to do. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum Command { + /// A viewer arrived: turn the display on beside the mirror. + Attach, + /// Turn off the display this host turned on, unless a start claimed it + /// again since. + Release, +} + +/// The one thread that turns the virtual display on and off, and its queue. +#[derive(Default)] +pub(crate) struct Worker { + queue: Mutex, + wake: Condvar, + /// Held across each Enable. Quitting takes it after setting + /// [`Queue::quitting`], so no Enable runs or starts once it has. + enable: Mutex<()>, + thread: Mutex>>, +} + +#[derive(Default)] +struct Queue { + commands: VecDeque, + quitting: bool, +} + +/// Ask the worker to change the display. Returns at once. +pub(crate) fn send(shared: &SharedControl, command: Command) { + if !driver_available() { + without_driver(shared, command); + return; + } + { + let mut queue = shared.vdd_worker.queue.lock(); + if queue.quitting { + return; + } + queue.commands.push_back(command); + } + shared.vdd_worker.wake.notify_one(); + // Unit tests run the queue themselves, with a fake driver. + #[cfg(all(windows, not(test)))] + if let Err(error) = spawn_worker(shared) { + warn!(%error, "Could not start the virtual display worker"); + shared.vdd_worker.queue.lock().commands.clear(); + if command == Command::Attach { + warn!( + second_screen_fallback = "enable_failed", + "Could not start the second screen — mirroring the primary display instead" + ); + fail_start(shared, "enable_failed"); + } + } +} + +/// Unit tests queue commands too; they never reach the installed driver +/// tasks. +fn driver_available() -> bool { #[cfg(windows)] - if !cfg!(test) && !super::synthetic_requested() { - let worker = shared.clone(); - let spawned = std::thread::Builder::new() - .name("second-screen".into()) - .spawn(move || { - attach( - &worker, - &WindowsDriver { shared: &worker }, - super::vdd_attach_timeout(), - ) - }); - if let Err(error) = spawned { - warn!(%error, second_screen_fallback = "enable_failed", - "Could not start the second screen — mirroring the primary display instead"); - let mut status = shared.vdd_status.lock(); - *status = VddStatus::TaskFailed; - let mut state = shared.second_screen.lock(); - state.attaching = false; - state.failure = Some("enable_failed"); + { + cfg!(test) || !super::synthetic_requested() + } + #[cfg(not(windows))] + { + cfg!(test) + } +} + +fn without_driver(shared: &SharedControl, command: Command) { + match command { + Command::Attach => { + tracing::info!( + second_screen_fallback = "no_driver", + "No virtual display driver with this capture source — mirroring the primary display" + ); + fail_start(shared, "no_driver"); } - return; + Command::Release => *shared.vdd_output.lock() = None, } - tracing::info!( - second_screen_fallback = "no_driver", - "No virtual display driver with this capture source — mirroring the primary display" - ); +} + +fn fail_start(shared: &SharedControl, reason: &'static str) { let mut status = shared.vdd_status.lock(); *status = VddStatus::TaskFailed; let mut state = shared.second_screen.lock(); state.attaching = false; - state.failure = Some("no_driver"); + state.failure = Some(reason); } -/// Turn the display off on its own thread, so capture never waits for the -/// driver. Returns at once. -pub(crate) fn turn_off(shared: SharedControl) { - // Unit tests never reach the installed driver tasks. - #[cfg(windows)] - if !cfg!(test) && !super::synthetic_requested() { +#[cfg(all(windows, not(test)))] +fn spawn_worker(shared: &SharedControl) -> std::io::Result<()> { + let mut thread = shared.vdd_worker.thread.lock(); + if thread.is_none() { let worker = shared.clone(); - let spawned = std::thread::Builder::new() - .name("second-screen-off".into()) - .spawn(move || release(&worker, &WindowsDriver { shared: &worker })); - if let Err(error) = spawned { - warn!(%error, "Could not turn the virtual display off"); - } + *thread = Some( + std::thread::Builder::new() + .name("second-screen".into()) + .spawn(move || { + run( + &worker, + &WindowsDriver { shared: &worker }, + super::vdd_attach_timeout(), + true, + ) + })?, + ); + } + Ok(()) +} + +/// Quitting: once this returns no Enable runs or will start, so the caller's +/// Disable is the last change. Waits for an Enable in flight, then for the +/// worker to exit, both bounded. +pub fn shut_down(shared: &SharedControl) { + let worker = &shared.vdd_worker; + worker.queue.lock().quitting = true; + worker.wake.notify_all(); + if worker.enable.try_lock_for(QUIT_ENABLE_WAIT).is_none() { + warn!("An Enable of the virtual display is still running — turning it off after it"); + } + let Some(thread) = worker.thread.lock().take() else { return; + }; + let deadline = Instant::now() + QUIT_JOIN; + while !thread.is_finished() && Instant::now() < deadline { + std::thread::sleep(Duration::from_millis(10)); + } + if thread.is_finished() { + let _ = thread.join(); + } else { + warn!("The virtual display worker did not stop in time"); + } +} + +#[cfg(any(windows, test))] +fn quitting(shared: &SharedControl) -> bool { + shared.vdd_worker.queue.lock().quitting +} + +/// The worker: one command at a time, until the app quits. With `wait` +/// false it returns once the queue is empty, for tests on a paused clock. +#[cfg(any(windows, test))] +pub(crate) fn run( + shared: &SharedControl, + driver: &impl Driver, + attach_timeout: Duration, + wait: bool, +) { + loop { + let command = { + let mut queue = shared.vdd_worker.queue.lock(); + loop { + if queue.quitting { + return; + } + if let Some(command) = queue.commands.pop_front() { + break command; + } + if !wait { + return; + } + shared.vdd_worker.wake.wait(&mut queue); + } + }; + match command { + Command::Attach => attach(shared, driver, attach_timeout), + Command::Release => release(shared, driver), + } } - *shared.vdd_output.lock() = None; } /// What turning the display on needs from Windows, so tests can supply a @@ -338,21 +510,29 @@ impl Drop for AttachExit<'_> { } /// Turn the display on, wait for it to attach, and record the result. A -/// viewer leaving does not cancel it; only a change of mode does. Capture -/// keeps mirroring throughout. +/// viewer leaving does not cancel it; a change of mode or quitting does. +/// Capture keeps mirroring throughout. #[cfg(any(windows, test))] pub(crate) fn attach(shared: &SharedControl, driver: &impl Driver, timeout: Duration) { let _exit = AttachExit(shared); - let selected = || *shared.capture_target.lock() == CaptureTarget::VirtualExtended; - // One driver change at a time, across the threads here. - let _lifecycle = loop { - if let Some(guard) = shared.vdd_lifecycle.try_lock_for(Duration::from_millis(50)) { - break guard; - } - if !selected() { - return; - } - }; + let selected = + || *shared.capture_target.lock() == CaptureTarget::VirtualExtended && !quitting(shared); + if !selected() { + return; + } + // Still on from before (its release saw this start coming): reuse it, + // with no change to wait for. + let outputs = driver.outputs(); + let owned = shared.vdd_output.lock().clone(); + if let Some(name) = owned.filter(|name| { + outputs + .iter() + .any(|output| !output.is_primary && &output.device_name == name) + }) { + info!(device = %name, "Virtual display still attached — capturing it"); + note_attached(shared, name); + return; + } if !wait_for_toggle_gap(shared, driver, &selected) { return; } @@ -360,35 +540,24 @@ pub(crate) fn attach(shared: &SharedControl, driver: &impl Driver, timeout: Dura info!("The iPad left before the virtual display was turned on — leaving it off"); return; } - // Quitting turns the display off once; an Enable after that would - // outlive the app. let stopped_at = driver.now(); while !shared.running.load(std::sync::atomic::Ordering::SeqCst) { - if driver.now().duration_since(stopped_at) >= STOPPED_WAIT { + if !selected() || driver.now().duration_since(stopped_at) >= STOPPED_WAIT { info!("The stream stopped before the virtual display was turned on — leaving it off"); return; } driver.sleep(WAIT_STEP); } - // Still on from before (its turn-off saw this start coming): reuse it. let outputs = driver.outputs(); - let owned = shared.vdd_output.lock().clone(); - if let Some(name) = owned.filter(|name| { - outputs - .iter() - .any(|output| !output.is_primary && &output.device_name == name) - }) { - info!(device = %name, "Virtual display still attached — capturing it"); - note_attached(shared, name); - return; - } driver.prepare(); let before: Vec = outputs .into_iter() .map(|output| output.device_name) .collect(); - let enabled = driver.set_enabled(true); - shared.second_screen.lock().note_toggle(driver.now()); + let Some(enabled) = enable(shared, driver) else { + info!("Quitting before the virtual display was turned on — leaving it off"); + return; + }; if !enabled { warn!( second_screen_fallback = "enable_failed", @@ -408,13 +577,15 @@ pub(crate) fn attach(shared: &SharedControl, driver: &impl Driver, timeout: Dura |duration| driver.sleep(duration), ); match result { + AttachResult::Cancelled if quitting(shared) => { + info!("Quitting while the virtual display started — the quit turns it off"); + } AttachResult::Cancelled => { // The user chose another mode while the display came up. Nothing - // records it as ours, so turn it off now. + // records it as ours, so turn it off, after the gap like every + // change: the mirror is already streaming. info!("Display mode changed while the virtual display started — turning it off"); - driver.set_enabled(false); - *shared.vdd_output.lock() = None; - shared.second_screen.lock().note_toggle(driver.now()); + disable(shared, driver); } AttachResult::Attached(chosen) => { info!( @@ -433,33 +604,51 @@ pub(crate) fn attach(shared: &SharedControl, driver: &impl Driver, timeout: Dura ); // Enable succeeded but nothing attached: turn it back off so no // half-enabled device lingers as a phantom monitor. - wait_for_toggle_gap(shared, driver, &|| true); - driver.set_enabled(false); + disable(shared, driver); let mut status = shared.vdd_status.lock(); *status = VddStatus::Failed; - let mut state = shared.second_screen.lock(); - state.note_toggle(driver.now()); - state.failure = Some("attach_timeout"); + shared.second_screen.lock().failure = Some("attach_timeout"); } } } -/// Turn off a display this host turned on, unless a viewer wants it again. +/// The only Enable: never once quitting has begun, and quitting waits for +/// one in flight. None when quitting. +#[cfg(any(windows, test))] +fn enable(shared: &SharedControl, driver: &impl Driver) -> Option { + let _enable = shared.vdd_worker.enable.lock(); + if quitting(shared) { + return None; + } + let enabled = driver.set_enabled(true); + shared.second_screen.lock().note_toggle(driver.now()); + Some(enabled) +} + +/// Turn the display off after the gap. Quitting skips it: the quit turns it +/// off at once. +#[cfg(any(windows, test))] +fn disable(shared: &SharedControl, driver: &impl Driver) { + if wait_for_toggle_gap(shared, driver, &|| !quitting(shared)) { + driver.set_enabled(false); + shared.second_screen.lock().note_toggle(driver.now()); + } + *shared.vdd_output.lock() = None; +} + +/// Turn off a display this host turned on, unless a start claimed it since. +/// Only the claim counts, not the status: a capture generation may publish +/// the status after asking for this. #[cfg(any(windows, test))] pub(crate) fn release(shared: &SharedControl, driver: &impl Driver) { let wanted = || { *shared.capture_target.lock() == CaptureTarget::VirtualExtended - && matches!( - *shared.vdd_status.lock(), - VddStatus::Active | VddStatus::Attaching - ) + && shared.second_screen.lock().attaching }; - let _lifecycle = shared.vdd_lifecycle.lock(); if shared.vdd_output.lock().is_none() || wanted() { return; } - wait_for_toggle_gap(shared, driver, &|| true); - if wanted() { + if !wait_for_toggle_gap(shared, driver, &|| !wanted() && !quitting(shared)) { return; } driver.set_enabled(false); @@ -468,16 +657,16 @@ pub(crate) fn release(shared: &SharedControl, driver: &impl Driver) { info!("Virtual display turned off"); } -/// Wait out [`MIN_TOGGLE_GAP`]. False when the mode changed meanwhile. +/// Wait out [`MIN_TOGGLE_GAP`]. False when `go_on` turned false meanwhile. #[cfg(any(windows, test))] fn wait_for_toggle_gap( shared: &SharedControl, driver: &impl Driver, - selected: &dyn Fn() -> bool, + go_on: &dyn Fn() -> bool, ) -> bool { let delay = shared.second_screen.lock().toggle_delay(driver.now()); if delay.is_zero() { - return selected(); + return go_on(); } info!( wait_ms = delay.as_millis() as u64, @@ -486,12 +675,12 @@ fn wait_for_toggle_gap( ); let until = driver.now() + delay; while driver.now() < until { - if !selected() { + if !go_on() { return false; } driver.sleep(until.saturating_duration_since(driver.now()).min(WAIT_STEP)); } - selected() + go_on() } #[cfg(windows)] @@ -831,6 +1020,9 @@ mod tests { assert_eq!(*shared.vdd_status.lock(), VddStatus::WaitingForClient); } + /// Review round 2, defect 5: Enable done at 3 s, Mirror chosen at 5 s. + /// The Disable waited for nothing and came 2 s after the Enable; now it + /// waits out the gap, like every change, while the mirror streams. #[test] fn a_mode_change_after_the_enable_turns_the_display_back_off() { let shared = second_screen_selected(); @@ -844,16 +1036,13 @@ mod tests { }; let driver = FakeDriver::new(secs(3.0), None, &on_sleep); attach(&shared, &driver, ATTACH_TIMEOUT); - assert!( - driver.elapsed.get() < secs(5.5), - "{:?}", - driver.elapsed.get() - ); let toggles = driver.toggles.borrow().clone(); assert_eq!( toggles.iter().map(|(_, on)| *on).collect::>(), [true, false] ); + assert_eq!(toggles[0].0, secs(3.0)); + assert!(toggles[1].0 - toggles[0].0 >= MIN_TOGGLE_GAP, "{toggles:?}"); assert_eq!(*shared.vdd_output.lock(), None); assert_eq!(*shared.vdd_status.lock(), VddStatus::WaitingForClient); assert_eq!(next_step(&shared, driver.now()), Step::Nothing); @@ -999,14 +1188,15 @@ mod tests { } /// Review round 1, defect 2: no capture generation may wait for the - /// driver lock, whatever the status, the mode or the viewer. Before, a + /// driver, whatever the status, the mode or the viewer. Before, a /// generation that read `WaitingForClient` (Try again had just reset it) /// waited for the lock behind the attach thread's whole Enable. #[test] fn mirror_startup_never_waits_for_the_driver_lock() { let shared = second_screen_selected(); let generation = crate::supervisor::CURRENT_GENERATION.load(Ordering::SeqCst); - let _held = shared.vdd_lifecycle.lock(); + // What a slow Enable on the worker holds. + let _held = shared.vdd_worker.enable.lock(); for target in [ CaptureTarget::VirtualExtended, CaptureTarget::PrimaryAuto, @@ -1111,4 +1301,334 @@ mod tests { // No output ever appeared: the start failed and is remembered. assert_eq!(*shared.vdd_status.lock(), VddStatus::Failed); } + + /// Review round 2, defect 1: the generation streaming the second screen + /// was replaced before the transport's tick ended its proof (by the + /// watchdog, or a capture error that ended the stage). The new generation + /// started the 5 s over and forgot the capture error. + #[test] + fn a_restart_keeps_the_proof_deadline_and_its_capture_error() { + let start = Instant::now(); + let shared = second_screen_selected(); + connect_viewer(&shared); + note_attached(&shared, "virtual".into()); + assert_eq!(next_step(&shared, start), Step::Switch); + start_proving(&shared, 7, start); + start_proving(&shared, 8, start + secs(4.0)); + assert_eq!(next_step(&shared, start + secs(4.9)), Step::Nothing); + assert_eq!( + next_step(&shared, start + PROVE_TIMEOUT), + Step::Fallback("no_frames") + ); + + let shared = second_screen_selected(); + connect_viewer(&shared); + note_attached(&shared, "virtual".into()); + assert_eq!(next_step(&shared, start), Step::Switch); + start_proving(&shared, 7, start); + note_capture_error(&shared, 7); + start_proving(&shared, 8, start + secs(1.0)); + assert_eq!( + next_step(&shared, start + secs(1.0)), + Step::Fallback("duplication_failed") + ); + } + + /// Review round 2, defect 1: nothing is sent from the second screen + /// before its first image, so the watchdog's 5 s without a frame came + /// before the proof's own deadline and restarted the pipeline. + #[test] + fn the_watchdog_leaves_a_second_screen_proving_itself_alone() { + let shared = second_screen_selected(); + connect_viewer(&shared); + let machine = crate::supervisor::Machine::new(1); + let now_ms = 100_000; + shared.hb_capture_loop_ms.store(now_ms, Ordering::Relaxed); + shared + .hb_capture_frame_ms + .store(now_ms - 5_500, Ordering::Relaxed); + shared + .hb_encode_frame_ms + .store(now_ms - 5_500, Ordering::Relaxed); + note_attached(&shared, "virtual".into()); + start_proving(&shared, 7, Instant::now()); + assert_eq!( + crate::supervisor::wedge_reason_at(&shared, &machine, now_ms), + None + ); + // After its first image a stalled second screen is a wedge again. + note_image(&shared, 7); + assert!(crate::supervisor::wedge_reason_at(&shared, &machine, now_ms).is_some()); + } + + /// Review round 2, defect 1: when no tick ended the proof before the + /// restart, the next capture generation does, before anything else. + #[test] + fn the_next_capture_generation_ends_a_second_screen_that_failed() { + let generation = crate::supervisor::CURRENT_GENERATION.load(Ordering::SeqCst); + let previous = generation.wrapping_sub(1); + let long_ago = Instant::now().checked_sub(secs(6.0)).unwrap(); + for (failed_at, capture_error, reason) in [ + (Instant::now(), true, "duplication_failed"), + (long_ago, false, "no_frames"), + ] { + let shared = second_screen_selected(); + connect_viewer(&shared); + note_attached(&shared, "virtual".into()); + assert_eq!(next_step(&shared, failed_at), Step::Switch); + start_proving(&shared, previous, failed_at); + if capture_error { + note_capture_error(&shared, previous); + } + assert_eq!( + reconcile_virtual_display(&CaptureTarget::VirtualExtended, &shared, generation), + Some(CaptureTarget::PrimaryAuto) + ); + assert_eq!(*shared.vdd_status.lock(), VddStatus::Failed); + assert_eq!(remembered_failure(&shared), Some(reason)); + assert!(!proving(&shared)); + // The display is turned off, and the tick finds nothing to do. + let on_sleep = |_| {}; + let driver = FakeDriver::new(secs(3.0), Some(Duration::ZERO), &on_sleep); + run(&shared, &driver, ATTACH_TIMEOUT, false); + assert_eq!(*driver.toggles.borrow(), vec![(Duration::ZERO, false)]); + assert_eq!(next_step(&shared, Instant::now()), Step::Nothing); + } + } + + /// Review round 2, defect 2: the proof was noted before the readback, so + /// a frame that never reached the encoder counted, and the capture error + /// that followed found no proof left to fail. + #[test] + fn only_an_image_read_back_proves_the_second_screen() { + use crate::capture::read_back_image; + let start = Instant::now(); + let shared = second_screen_selected(); + connect_viewer(&shared); + note_attached(&shared, "virtual".into()); + assert_eq!(next_step(&shared, start), Step::Switch); + start_proving(&shared, 7, start); + let mut seen_image = false; + // The texture could not be cast: nothing for the encoder. + assert_eq!( + read_back_image(&shared, 7, &mut seen_image, || Ok::, &str>(None)), + Ok(None) + ); + // Map failed. + assert_eq!( + read_back_image(&shared, 7, &mut seen_image, || Err::, _>("map")), + Err("map") + ); + assert!(proving(&shared) && !seen_image); + note_capture_error(&shared, 7); + assert_eq!( + next_step(&shared, start), + Step::Fallback("duplication_failed") + ); + + let shared = second_screen_selected(); + connect_viewer(&shared); + note_attached(&shared, "virtual".into()); + assert_eq!(next_step(&shared, start), Step::Switch); + start_proving(&shared, 7, start); + let mut seen_image = false; + assert_eq!( + read_back_image(&shared, 7, &mut seen_image, || Ok::<_, ()>(Some(1u8))), + Ok(Some(1)) + ); + assert!(!proving(&shared) && seen_image); + } + + /// A driver for a worker thread. Prepare or Enable can be held until + /// the test lets it go. + struct Gated { + hold_prepare: bool, + hold_enable: bool, + entered: std::sync::mpsc::SyncSender<&'static str>, + go: std::sync::mpsc::Receiver<()>, + toggles: std::sync::Arc>>, + start: Instant, + elapsed: Cell, + } + + impl Driver for Gated { + fn outputs(&self) -> Vec { + vec![output("primary", true)] + } + fn prepare(&self) { + if self.hold_prepare { + let _ = self.entered.send("prepare"); + let _ = self.go.recv(); + } + } + fn set_enabled(&self, enabled: bool) -> bool { + if enabled && self.hold_enable { + let _ = self.entered.send("enable"); + let _ = self.go.recv(); + } + self.toggles.lock().push(enabled); + true + } + fn now(&self) -> Instant { + self.start + self.elapsed.get() + } + fn sleep(&self, duration: Duration) { + self.elapsed.set(self.elapsed.get() + duration); + } + } + + type GatedWorker = ( + std::sync::mpsc::Receiver<&'static str>, + std::sync::mpsc::Sender<()>, + std::sync::Arc>>, + ); + + /// The worker thread as the app runs it, with a [`Gated`] driver. + fn spawn_gated_worker( + shared: &SharedControl, + hold_prepare: bool, + hold_enable: bool, + ) -> GatedWorker { + let (entered_tx, entered) = std::sync::mpsc::sync_channel(4); + let (go, go_rx) = std::sync::mpsc::channel(); + let toggles = std::sync::Arc::new(Mutex::new(Vec::new())); + let (worker, record) = (shared.clone(), toggles.clone()); + let handle = std::thread::spawn(move || { + let driver = Gated { + hold_prepare, + hold_enable, + entered: entered_tx, + go: go_rx, + toggles: record, + start: Instant::now(), + elapsed: Cell::new(Duration::ZERO), + }; + run(&worker, &driver, ATTACH_TIMEOUT, true); + }); + *shared.vdd_worker.thread.lock() = Some(handle); + (entered, go, toggles) + } + + /// Review round 2, defect 3: quitting turned the display off while the + /// start prepared the driver's settings, and its Enable came after: a + /// phantom monitor outlived the app. + #[test] + fn quitting_while_a_start_prepares_never_enables() { + let shared = second_screen_selected(); + connect_viewer(&shared); + assert_eq!(next_step(&shared, Instant::now()), Step::Attach); + let (entered, go, toggles) = spawn_gated_worker(&shared, true, false); + send(&shared, Command::Attach); + assert_eq!(entered.recv_timeout(Duration::from_secs(5)), Ok("prepare")); + let quitter = shared.clone(); + let quit = std::thread::spawn(move || shut_down(&quitter)); + while !quitting(&shared) { + std::thread::sleep(Duration::from_millis(1)); + } + go.send(()).unwrap(); + quit.join().unwrap(); + assert!(shared.vdd_worker.thread.lock().is_none(), "joined"); + assert!(toggles.lock().is_empty(), "{:?}", toggles.lock()); + // Nothing asked for after the quit reaches the driver. + send(&shared, Command::Attach); + assert!(shared.vdd_worker.queue.lock().commands.is_empty()); + } + + /// The other half of defect 3: an Enable already running when the app + /// quits finishes before quitting goes on to its Disable. + #[test] + fn quitting_waits_for_an_enable_in_flight() { + let shared = second_screen_selected(); + connect_viewer(&shared); + assert_eq!(next_step(&shared, Instant::now()), Step::Attach); + let (entered, go, toggles) = spawn_gated_worker(&shared, false, true); + send(&shared, Command::Attach); + assert_eq!(entered.recv_timeout(Duration::from_secs(5)), Ok("enable")); + // Without joining it: an Enable can outlast the join's bound. + let worker = shared.vdd_worker.thread.lock().take().unwrap(); + let (done_tx, done) = std::sync::mpsc::channel(); + let quitter = shared.clone(); + let quit = std::thread::spawn(move || { + shut_down(&quitter); + let _ = done_tx.send(()); + }); + assert!( + done.recv_timeout(Duration::from_millis(300)).is_err(), + "quitting went on while an Enable ran" + ); + go.send(()).unwrap(); + done.recv_timeout(Duration::from_secs(5)).unwrap(); + quit.join().unwrap(); + worker.join().unwrap(); + // The worker leaves the Disable to the quit, which runs it next. + assert_eq!(*toggles.lock(), vec![true]); + } + + /// Review round 2, defect 4: the grace period ended and a capture + /// generation asked for the display to be turned off, but the worker + /// acted before the generation published the status. It read `Active`, + /// took the display as wanted, and nothing asked again. Only a start + /// claimed since keeps it now. + #[test] + fn a_release_turns_the_display_off_whatever_status_it_reads() { + let shared = second_screen_selected(); + note_attached(&shared, "virtual".into()); + shared.second_screen.lock().release_requested = true; + let on_sleep = |_| {}; + let driver = FakeDriver::new(secs(3.0), Some(Duration::ZERO), &on_sleep); + release(&shared, &driver); + assert_eq!(*driver.toggles.borrow(), vec![(Duration::ZERO, false)]); + assert_eq!(*shared.vdd_output.lock(), None); + } + + /// The grace period's end, through a capture generation and the worker: + /// one Disable. + #[test] + fn the_grace_period_ends_with_one_disable() { + let shared = second_screen_selected(); + connect_viewer(&shared); + note_attached(&shared, "virtual".into()); + let start = Instant::now(); + assert_eq!(next_step(&shared, start), Step::Switch); + disconnect(&shared); + assert_eq!(next_step(&shared, start), Step::Nothing); + assert_eq!(next_step(&shared, start + RELEASE_GRACE), Step::Release); + let generation = crate::supervisor::CURRENT_GENERATION.load(Ordering::SeqCst); + assert_eq!( + reconcile_virtual_display(&CaptureTarget::VirtualExtended, &shared, generation), + Some(CaptureTarget::PrimaryAuto) + ); + assert_eq!(*shared.vdd_status.lock(), VddStatus::WaitingForClient); + let on_sleep = |_| {}; + let driver = FakeDriver::new(secs(3.0), Some(Duration::ZERO), &on_sleep); + run(&shared, &driver, ATTACH_TIMEOUT, false); + assert_eq!(*driver.toggles.borrow(), vec![(Duration::ZERO, false)]); + assert_eq!(*shared.vdd_output.lock(), None); + run(&shared, &driver, ATTACH_TIMEOUT, false); + assert_eq!(driver.toggles.borrow().len(), 1); + } + + /// The iPad came back after the release was asked for, before the + /// worker ran it: no change, and the start reuses the display. + #[test] + fn a_viewer_back_before_the_release_runs_keeps_the_display() { + let shared = second_screen_selected(); + note_attached(&shared, "virtual".into()); + shared.second_screen.lock().release_requested = true; + let generation = crate::supervisor::CURRENT_GENERATION.load(Ordering::SeqCst); + assert_eq!( + reconcile_virtual_display(&CaptureTarget::VirtualExtended, &shared, generation), + Some(CaptureTarget::PrimaryAuto) + ); + connect_viewer(&shared); + assert_eq!(next_step(&shared, Instant::now()), Step::Attach); + send(&shared, Command::Attach); + let on_sleep = |_| {}; + let driver = FakeDriver::new(secs(3.0), Some(Duration::ZERO), &on_sleep); + driver.on_since.set(Some(Duration::ZERO)); + run(&shared, &driver, ATTACH_TIMEOUT, false); + assert!(driver.toggles.borrow().is_empty()); + assert_eq!(*shared.vdd_status.lock(), VddStatus::Active); + assert_eq!(next_step(&shared, driver.now()), Step::Switch); + } } diff --git a/host/src/control.rs b/host/src/control.rs index 90d85d26..b385c19e 100644 --- a/host/src/control.rs +++ b/host/src/control.rs @@ -39,10 +39,9 @@ pub struct SharedControl { /// Set while Windows blocks screen capture (lock screen, UAC, a display /// it will not duplicate); the GUI explains it instead of a frozen picture. pub capture_block: Arc>>, - /// Serialize driver changes across retiring capture generations and the - /// thread that turns the second screen on. - #[cfg(any(windows, test))] - pub(crate) vdd_lifecycle: Arc>, + /// The one thread that turns the virtual display on and off, and its + /// queue. + pub(crate) vdd_worker: Arc, /// Output owned by the enabled driver, retained across pipeline restarts. pub(crate) vdd_output: Arc>>, /// Second screen timing across pipeline restarts: the pending switch onto @@ -153,8 +152,7 @@ impl SharedControl { capture_target: Arc::new(Mutex::new(CaptureTarget::PrimaryAuto)), vdd_status: Arc::new(Mutex::new(VddStatus::Inactive)), capture_block: Arc::new(Mutex::new(None)), - #[cfg(any(windows, test))] - vdd_lifecycle: Arc::new(Mutex::new(())), + vdd_worker: Arc::new(Default::default()), vdd_output: Arc::new(Mutex::new(None)), second_screen: Arc::new(Mutex::new(Default::default())), hb_capture_loop_ms: Arc::new(AtomicU64::new(0)), diff --git a/host/src/main.rs b/host/src/main.rs index c5d884c5..cd7826b0 100644 --- a/host/src/main.rs +++ b/host/src/main.rs @@ -302,8 +302,10 @@ fn main() -> Result<(), Box> { gui_control.request_shutdown(); // Never leave the virtual display attached after we exit — it should only exist while - // EternalMonitor is actively using it. If it may still be on, the failure reporter - // turns it off after this process exits. + // EternalMonitor is actively using it. No Enable starts once the second screen worker + // is shut down, so this Disable is the last change. If it may still be on, the failure + // reporter turns it off after this process exits. + eternal_host::capture::second_screen::shut_down(&gui_control.shared); eternal_host::startup::display_off_at_quit(vdd::disable); }; // A window error is saved first and shown after the cleanup, as a panic is. diff --git a/host/src/supervisor.rs b/host/src/supervisor.rs index 581044fa..b7ebfed5 100644 --- a/host/src/supervisor.rs +++ b/host/src/supervisor.rs @@ -392,9 +392,15 @@ pub(crate) fn wedge_reason_at( // A client is connected but no frame has been produced for 5s. Sound // because the idle keepalive guarantees a frame at least every ~750ms - // while a client is registered. + // while a client is registered. Not while the second screen proves + // itself: nothing is sent from it until its first image, and a restart + // here came before its own fallback. let client_connected = shared.client_connected(); - if client_connected && frame_ms > 0 && now_ms.saturating_sub(frame_ms) > 5_000 { + if client_connected + && frame_ms > 0 + && now_ms.saturating_sub(frame_ms) > 5_000 + && !crate::capture::second_screen::proving(shared) + { return Some(format!( "no captured frame for {}ms with a client connected", now_ms.saturating_sub(frame_ms) diff --git a/host/src/transport/mod.rs b/host/src/transport/mod.rs index e823f8ff..623f03e4 100644 --- a/host/src/transport/mod.rs +++ b/host/src/transport/mod.rs @@ -717,7 +717,7 @@ async fn execute_actions( "Client connected with extended display selected — mirroring the primary \ display while the virtual display starts" ); - second_screen::start(shared.clone()); + second_screen::send(shared, second_screen::Command::Attach); false } second_screen::Step::Switch => { @@ -735,7 +735,7 @@ async fn execute_actions( "The virtual display showed no picture — restarting pipeline to mirror the \ primary display instead" ); - second_screen::turn_off(shared.clone()); + second_screen::send(shared, second_screen::Command::Release); true } }; From d350b9b02c781fef69dd3877a7bfe93689e815b8 Mon Sep 17 00:00:00 2001 From: Ali Younes Date: Mon, 26 Oct 2026 20:58:08 -0400 Subject: [PATCH 11/16] Describe the one worker thread and the proof deadline across restarts --- docs/decisions.md | 6 ++++-- host/src/capture/second_screen.rs | 8 ++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/decisions.md b/docs/decisions.md index b179ee62..f83f533c 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -124,8 +124,10 @@ triggers via `schtasks`, avoiding a per-toggle UAC prompt. - The iPad mirrors the primary display while the virtual display starts, and the stream restarts onto it once it attaches. That capture must deliver a real image within 5 s; a display that fails to duplicate or only times out - goes back to the mirror and counts as a failed start. Capture never waits - for the driver: separate threads turn the display on and off. The iPad gives up 10 s after + goes back to the mirror and counts as a failed start. The 5 s run from the + switch, across any pipeline restart meanwhile. Capture never waits for the + driver: one worker thread makes every change, in order, and quitting stops + it before its final Disable. The iPad gives up 10 s after the handshake without a frame, which a slow Enable task plus the attach wait could exceed. A start that fails is remembered until the user clicks Try again, restarts the stream or changes the mode, so reconnects mirror diff --git a/host/src/capture/second_screen.rs b/host/src/capture/second_screen.rs index 78b04913..42f44bed 100644 --- a/host/src/capture/second_screen.rs +++ b/host/src/capture/second_screen.rs @@ -351,7 +351,11 @@ pub(crate) fn send(shared: &SharedControl, command: Command) { } shared.vdd_worker.wake.notify_one(); // Unit tests run the queue themselves, with a fake driver. - #[cfg(all(windows, not(test)))] + #[cfg(windows)] + if cfg!(test) { + return; + } + #[cfg(windows)] if let Err(error) = spawn_worker(shared) { warn!(%error, "Could not start the virtual display worker"); shared.vdd_worker.queue.lock().commands.clear(); @@ -399,7 +403,7 @@ fn fail_start(shared: &SharedControl, reason: &'static str) { state.failure = Some(reason); } -#[cfg(all(windows, not(test)))] +#[cfg(windows)] fn spawn_worker(shared: &SharedControl) -> std::io::Result<()> { let mut thread = shared.vdd_worker.thread.lock(); if thread.is_none() { From 487acb833d73bb7f4759f4adcc3cd224aef3e006 Mon Sep 17 00:00:00 2001 From: Ali Younes Date: Mon, 26 Oct 2026 21:07:47 -0400 Subject: [PATCH 12/16] Describe the never-attaching display test without a name --- host/src/capture/second_screen.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/host/src/capture/second_screen.rs b/host/src/capture/second_screen.rs index 42f44bed..3ee99a94 100644 --- a/host/src/capture/second_screen.rs +++ b/host/src/capture/second_screen.rs @@ -880,7 +880,7 @@ mod tests { } } - /// Joy's 0.3.2 log and the 0.3.1 field data: the display never attaches, + /// A 0.3.2 log and the 0.3.1 field data: the display never attaches, /// the iPad sends its goodbye at 10 s, and USB dials again 1 s later. /// Here no transport step restarts the pipeline while the display /// starts, so the running generation keeps mirroring to the iPad, with From 32c6d5ac86fc2c8339486967bed6f1d045ee5df2 Mon Sep 17 00:00:00 2001 From: Ali Younes Date: Mon, 26 Oct 2026 22:42:48 -0400 Subject: [PATCH 13/16] Answer a keyframe request with an IDR on NVENC and QSV, and stop NVENC holding frames back --- host/src/encoder/mod.rs | 140 +++++++++++++++++++++++++++++++++++++- host/src/transport/mod.rs | 4 +- 2 files changed, 140 insertions(+), 4 deletions(-) diff --git a/host/src/encoder/mod.rs b/host/src/encoder/mod.rs index 95c35970..c6f3f498 100644 --- a/host/src/encoder/mod.rs +++ b/host/src/encoder/mod.rs @@ -732,8 +732,8 @@ impl EncoderState { } /// Capture time and number of the frame in `self.packet`. An encoder - /// with output delay (NVENC holds one frame) returns an earlier frame - /// than the one just submitted. + /// with output delay (a hardware encoder can hold frames) returns an + /// earlier frame than the one just submitted. fn packet_origin(&mut self, just_submitted: (Instant, u64)) -> (Instant, u64) { self.in_flight .take(self.packet.pts()) @@ -906,6 +906,7 @@ fn encoder_options(encoder_name: &str) -> ffmpeg_next::Dictionary<'_> { opts.set("profile", "baseline"); opts.set("zerolatency", "1"); opts.set("rc", "cbr"); + set_nvenc_keyframe_options(&mut opts); } "h264_amf" => { opts.set("usage", "lowlatency"); @@ -936,6 +937,8 @@ fn encoder_options(encoder_name: &str) -> ffmpeg_next::Dictionary<'_> { opts.set("preset", "veryfast"); opts.set("profile", "baseline"); opts.set("look_ahead", "0"); + // Like NVENC, QSV makes a requested keyframe a plain I-frame. + opts.set("forced_idr", "1"); } "libx264" => { opts.set("preset", "ultrafast"); @@ -950,6 +953,7 @@ fn encoder_options(encoder_name: &str) -> ffmpeg_next::Dictionary<'_> { opts.set("tune", "ll"); opts.set("zerolatency", "1"); opts.set("rc", "cbr"); + set_nvenc_keyframe_options(&mut opts); } "hevc_amf" => { opts.set("usage", "lowlatency"); @@ -962,6 +966,7 @@ fn encoder_options(encoder_name: &str) -> ffmpeg_next::Dictionary<'_> { } "hevc_qsv" => { opts.set("preset", "veryfast"); + opts.set("forced_idr", "1"); } "hevc_videotoolbox" => { opts.set("realtime", "1"); @@ -978,6 +983,18 @@ fn encoder_options(encoder_name: &str) -> ffmpeg_next::Dictionary<'_> { opts } +/// A new viewer, or one back within the second screen's grace period, can +/// start only from an IDR. NVENC made a requested keyframe a plain I-frame, +/// which the transport never starts a session from, so the viewer waited +/// for the GOP's next IDR: up to 0.5 s at 60 fps, but up to 22 s on a static +/// second screen resent at 1.3 fps, past the iPad's 10 s. NVENC also held +/// each frame back two frames, another 1.5 s at that rate; without the delay +/// it returns each frame as it is encoded. +fn set_nvenc_keyframe_options(opts: &mut ffmpeg_next::Dictionary<'_>) { + opts.set("forced-idr", "1"); + opts.set("delay", "0"); +} + fn prepare_frame_for_encode( frame: &mut ffmpeg_next::frame::Video, frame_number: u64, @@ -1507,6 +1524,125 @@ mod tests { assert_eq!(labels, (480..490).map(|n| (at(n), n)).collect::>()); } + #[test] + fn nvenc_and_qsv_make_a_requested_keyframe_an_idr() { + for (name, option) in [ + ("h264_nvenc", "forced-idr"), + ("hevc_nvenc", "forced-idr"), + ("h264_qsv", "forced_idr"), + ("hevc_qsv", "forced_idr"), + ] { + assert_eq!(encoder_options(name).get(option), Some("1"), "{name}"); + } + for name in ["h264_nvenc", "hevc_nvenc"] { + assert_eq!(encoder_options(name).get("delay"), Some("0"), "{name}"); + } + } + + /// Encodes frame `n` as the encode loop does, and returns each packet + /// that came out: whether the transport takes it as a keyframe, and the + /// capture time and number of its frame. + fn encode( + state: &mut EncoderState, + n: u64, + captured: Instant, + force_idr: bool, + ) -> Vec<(bool, Instant, u64)> { + let pts = state.next_pts(captured, n); + state.frame.set_pts(Some(pts)); + prepare_frame_for_encode( + &mut state.frame, + n, + &state.opened_encoder, + &mut 0, + force_idr, + ); + state.send_frame(pts, captured, n).unwrap(); + let mut packets = Vec::new(); + while state.encoder.receive_packet(&mut state.packet).is_ok() { + if state.h264.needs_parameter_sets() { + state + .h264 + .refresh_parameter_sets_from_extradata(encoder_extradata(&state.encoder)); + } + let packet_is_key = state.packet.is_key(); + let (_, info) = normalize_h264_payload_with_info( + state.packet.data().unwrap_or_default(), + packet_is_key, + &mut state.h264, + &state.opened_encoder, + ); + let is_keyframe = info.is_keyframe(packet_is_key, &state.opened_encoder); + let (origin, sequence) = state.packet_origin((captured, n)); + packets.push((is_keyframe, origin, sequence)); + } + packets + } + + /// A viewer back within the second screen's grace period finds the + /// encoder in the middle of a GOP, and a static second screen resends + /// its picture every 750 ms. NVENC answered the session's keyframe + /// request with an I-frame the transport cannot start a session from, + /// so the first picture waited for the GOP's next IDR, 30 frames on: + /// 22 s, and the iPad gave up after 10. Runs each encoder that opens + /// here: NVENC on an NVIDIA PC, libx264 everywhere. + #[test] + fn a_viewer_back_mid_gop_starts_from_the_keyframe_it_asked_for() { + use crate::transport::{first_frame_gate, FirstFrame}; + use std::time::Duration; + ffmpeg_next::init().unwrap(); + let start = Instant::now(); + let mut tested = Vec::new(); + for name in ["h264_nvenc", "h264_qsv", "libx264"] { + let Ok(mut state) = + EncoderState::new(name, 1280, 720, 8_000_000, 60, EncoderInput::Yuv420) + else { + continue; + }; + tested.push(name); + // The last viewer watched 40 frames at 60 fps and left. + for n in 0..40 { + encode( + &mut state, + n, + start + Duration::from_micros(n * 16_667), + n == 0, + ); + } + // It comes back 5 s later; its session asks for a keyframe. + let back = start + Duration::from_secs(5); + let mut force_idr = true; + let mut first = None; + 'frames: for n in 40..60 { + let captured = back + Duration::from_millis(10 + (n - 40) * 750); + for (is_keyframe, origin, sequence) in + encode(&mut state, n, captured, std::mem::take(&mut force_idr)) + { + match first_frame_gate(is_keyframe, origin, Some(back)) { + FirstFrame::Send => { + first = Some((n, sequence)); + break 'frames; + } + FirstFrame::Skip => {} + FirstFrame::SkipAndRequestKeyframe => force_idr = true, + } + } + } + // The requested frame itself opens the session; NVENC returns + // it at once, with no frames held back. + let (submitted, sequence) = + first.unwrap_or_else(|| panic!("{name}: no frame could open the session")); + assert_eq!(sequence, 40, "{name}: the session opened on a later frame"); + let held = if name == "h264_nvenc" { 0 } else { 2 }; + assert!( + submitted - 40 <= held, + "{name}: frame 40 came out after frame {submitted}" + ); + } + eprintln!("encoders tested: {tested:?}"); + assert!(tested.contains(&"libx264")); + } + #[test] fn startup_tries_other_hardware_before_software() { let mut attempts = Vec::new(); diff --git a/host/src/transport/mod.rs b/host/src/transport/mod.rs index 623f03e4..32003a8d 100644 --- a/host/src/transport/mod.rs +++ b/host/src/transport/mod.rs @@ -497,7 +497,7 @@ pub async fn start_sender( } #[derive(Debug, PartialEq, Eq)] -enum FirstFrame { +pub(crate) enum FirstFrame { Send, Skip, SkipAndRequestKeyframe, @@ -508,7 +508,7 @@ enum FirstFrame { /// NVENC kept the host's startup frame while the PC sat idle, and the next /// viewer received that old, blank keyframe as its first picture. A stale /// keyframe is skipped and a fresh one requested. -fn first_frame_gate( +pub(crate) fn first_frame_gate( is_keyframe: bool, captured: Instant, session_start: Option, From bd599116dbb66cbd31fa4c6b46f854e2dc87c655 Mon Sep 17 00:00:00 2001 From: Ali Younes Date: Tue, 27 Oct 2026 03:02:35 -0400 Subject: [PATCH 14/16] Resend a static display's picture as soon as a keyframe is requested, and log when the virtual display starts turning off --- host/src/capture/dxgi.rs | 15 ++++++++---- host/src/capture/mod.rs | 39 +++++++++++++++++++++++++++++++ host/src/capture/second_screen.rs | 2 ++ 3 files changed, 51 insertions(+), 5 deletions(-) diff --git a/host/src/capture/dxgi.rs b/host/src/capture/dxgi.rs index b44210df..cb6a8949 100644 --- a/host/src/capture/dxgi.rs +++ b/host/src/capture/dxgi.rs @@ -16,9 +16,9 @@ use super::recovery::{ }; use super::{ client_connected, frame_budget_for, frame_has_image, heartbeat, idle_heartbeat, - keepalive_allowed, read_back_image, reconcile_virtual_display, recovery_poll, resolve_target, - FrameSlot, FrameUse, IdleGate, OutputInfo, RawFrame, ACQUIRE_TIMEOUT_MS, FPS_WINDOW, - IDLE_KEEPALIVE, + keepalive_allowed, keepalive_due, read_back_image, reconcile_virtual_display, recovery_poll, + resolve_target, FrameSlot, FrameUse, IdleGate, OutputInfo, RawFrame, ACQUIRE_TIMEOUT_MS, + FPS_WINDOW, }; use crate::control::{CaptureTarget, SharedControl}; use crate::stats::PIPELINE_STATS; @@ -536,9 +536,14 @@ pub fn run_capture_loop( // the iPad would sit on a black screen until it times out. While a client is // connected, resend the last captured image so the encoder emits the pending IDR // and the stream stays alive. pixel_buf holds the last composited frame (zeroed - // black before the first real frame, which is still a valid keyframe). + // black before the first real frame, which is still a valid keyframe). A + // pending keyframe request is answered without waiting for the keepalive. if client_connected(&shared) - && last_send.elapsed() >= IDLE_KEEPALIVE + && keepalive_due( + last_send.elapsed(), + shared.force_next_idr.load(Ordering::SeqCst), + seen_image, + ) && keepalive_allowed(seen_image, second_screen) { resend_last_frame( diff --git a/host/src/capture/mod.rs b/host/src/capture/mod.rs index c7f3d7fb..a9fb2fa4 100644 --- a/host/src/capture/mod.rs +++ b/host/src/capture/mod.rs @@ -449,6 +449,26 @@ pub(crate) fn keepalive_allowed(seen_image: bool, second_screen: bool) -> bool { seen_image || !second_screen } +/// How long a keyframe request waits for the encoder to take the resent frame +/// before the picture is resent again. +#[cfg(any(windows, test))] +const KEYFRAME_RESEND: Duration = Duration::from_millis(100); + +/// Whether an idle display's last picture is due to be resent to the viewer. +/// A keyframe request (a viewer that joined or lost its picture) is answered +/// at once when there is a real picture to send: a viewer back within the +/// second screen's grace period otherwise waited for the next keepalive, up +/// to 750 ms, before the encoder could start its session. +#[cfg(any(windows, test))] +pub(crate) fn keepalive_due( + since_send: Duration, + keyframe_requested: bool, + seen_image: bool, +) -> bool { + since_send >= IDLE_KEEPALIVE + || (keyframe_requested && seen_image && since_send >= KEYFRAME_RESEND) +} + /// Reads back an acquired frame for the encoder. Only an image that was read /// back counts as the second screen's proof: when the readback fails, the /// proof stays pending for the capture error that follows. @@ -697,6 +717,25 @@ mod tests { assert!(recovery_poll(&shared, now + 50, Duration::from_secs(6))); } + /// A viewer back within the second screen's grace period finds a static + /// picture that DXGI reports no change to. Its session's keyframe request + /// is answered at the next acquire timeout, not at the next keepalive. + #[test] + fn a_keyframe_request_on_a_static_display_is_answered_at_once() { + let ms = Duration::from_millis; + let first_resend = |requested: bool, seen_image: bool| { + (0..) + .map(|poll| ms(10 + 16 * poll)) + .find(|&since_send| keepalive_due(since_send, requested, seen_image)) + .unwrap() + }; + assert_eq!(first_resend(false, true), ms(762)); + assert!(first_resend(true, true) <= KEYFRAME_RESEND + ms(16)); + // Before the display's first image only the black placeholder exists; + // the keepalive sends it on the mirror, and never on the second screen. + assert_eq!(first_resend(true, false), ms(762)); + } + #[test] fn an_unwatched_desktop_is_acquired_once_a_second_and_never_encoded() { let start = Instant::now(); diff --git a/host/src/capture/second_screen.rs b/host/src/capture/second_screen.rs index 3ee99a94..4a49b463 100644 --- a/host/src/capture/second_screen.rs +++ b/host/src/capture/second_screen.rs @@ -652,6 +652,8 @@ pub(crate) fn release(shared: &SharedControl, driver: &impl Driver) { if shared.vdd_output.lock().is_none() || wanted() { return; } + // Marks when the worker took the request, apart from when the task ran. + info!("Turning the virtual display off"); if !wait_for_toggle_gap(shared, driver, &|| !wanted() && !quitting(shared)) { return; } From 41a423dc72bf3ffcbd6af1cce2494e4b82b1cf1d Mon Sep 17 00:00:00 2001 From: Ali Younes Date: Tue, 27 Oct 2026 03:41:43 -0400 Subject: [PATCH 15/16] Allow each encoder its configured output delay in the mid-GOP keyframe test --- host/src/encoder/mod.rs | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/host/src/encoder/mod.rs b/host/src/encoder/mod.rs index c6f3f498..d7dd000e 100644 --- a/host/src/encoder/mod.rs +++ b/host/src/encoder/mod.rs @@ -1579,6 +1579,22 @@ mod tests { packets } + /// The value of an integer option on the opened encoder. + fn encoder_option(state: &EncoderState, option: &str) -> u64 { + let option = std::ffi::CString::new(option).unwrap(); + let mut value = 0; + let result = unsafe { + ffmpeg_next::ffi::av_opt_get_int( + state.encoder.as_ptr().cast_mut().cast(), + option.as_ptr(), + ffmpeg_next::ffi::AV_OPT_SEARCH_CHILDREN, + &mut value, + ) + }; + assert_eq!(result, 0, "no option {option:?}"); + value.try_into().unwrap() + } + /// A viewer back within the second screen's grace period finds the /// encoder in the middle of a GOP, and a static second screen resends /// its picture every 750 ms. NVENC answered the session's keyframe @@ -1628,15 +1644,21 @@ mod tests { } } } - // The requested frame itself opens the session; NVENC returns - // it at once, with no frames held back. + // The requested frame itself opens the session, as soon as the + // encoder's configured delay lets it out: at once on NVENC with + // delay=0 and libx264 with zerolatency, and once QSV has its + // async_depth frames queued (FFmpeg's default 4). let (submitted, sequence) = first.unwrap_or_else(|| panic!("{name}: no frame could open the session")); assert_eq!(sequence, 40, "{name}: the session opened on a later frame"); - let held = if name == "h264_nvenc" { 0 } else { 2 }; + let held = match name { + "h264_nvenc" => encoder_option(&state, "delay"), + "h264_qsv" => encoder_option(&state, "async_depth") - 1, + _ => 0, + }; assert!( submitted - 40 <= held, - "{name}: frame 40 came out after frame {submitted}" + "{name}: frame 40 came out after frame {submitted}, more than {held} frames late" ); } eprintln!("encoders tested: {tested:?}"); From 33d14a754eefcba5c3287b26650b5628b30548b3 Mon Sep 17 00:00:00 2001 From: Ali Younes <159104116+whoisaldo@users.noreply.github.com> Date: Tue, 27 Oct 2026 09:14:23 -0400 Subject: [PATCH 16/16] Show the second screen on hybrid laptops and say why it didn't appear (#98) Pins the virtual display driver to the GPU that drives the screens, only when this host may change the driver's settings, settles the iPad's mode, and logs a display and adapter snapshot when the second screen fails. Carries #72 onto current main. --- .github/workflows/ci.yml | 3 + Cargo.lock | 1 + RELEASE_NOTES.md | 47 + docs/decisions.md | 7 +- host/Cargo.toml | 2 + host/src/capture/mod.rs | 251 ++- host/src/capture/second_screen.rs | 453 +++++- host/src/control.rs | 4 + host/src/diagnostics.rs | 78 +- host/src/gui/page_settings.rs | 17 +- host/src/gui/page_stream.rs | 96 +- host/src/gui/tests.rs | 129 +- host/src/lib.rs | 2 + host/src/main.rs | 5 +- host/src/transport/mod.rs | 2 +- host/src/vdd.rs | 280 +++- host/src/vdd_device.rs | 445 ++++++ host/src/vdd_settings.rs | 1383 ++++++++++++++++- host/src/vdd_snapshot.rs | 596 +++++++ host/tests/fixtures/README.md | 7 + host/tests/fixtures/vdd_settings-25.5.2.xml | 97 ++ .../snapshots/feedback-panel-long-minimum.png | Bin 141076 -> 141336 bytes host/tests/snapshots/feedback-panel-long.png | Bin 182433 -> 183043 bytes host/tests/snapshots/settings.png | Bin 212497 -> 214447 bytes host/tests/startup_order.rs | 8 +- installer/EternalMonitor.iss | 28 +- installer/scripts/vdd-driver-repair.ps1 | 106 ++ installer/scripts/vdd-toggle.ps1 | 47 +- installer/tests/Test-VddDriverRepair.ps1 | 189 +++ scripts/win/Test-VddToggle.ps1 | 135 +- 30 files changed, 4282 insertions(+), 136 deletions(-) create mode 100644 host/src/vdd_device.rs create mode 100644 host/src/vdd_snapshot.rs create mode 100644 host/tests/fixtures/README.md create mode 100644 host/tests/fixtures/vdd_settings-25.5.2.xml create mode 100644 installer/scripts/vdd-driver-repair.ps1 create mode 100644 installer/tests/Test-VddDriverRepair.ps1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3bd1b84b..aefc276e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,6 +88,9 @@ jobs: - name: Verify virtual display driver binding checks shell: powershell run: ./installer/tests/Test-VddDriverVerification.ps1 + - name: Verify Setup repairs a preserved virtual display driver + shell: powershell + run: ./installer/tests/Test-VddDriverRepair.ps1 rust-macos: name: Rust (macOS, full workspace + E2E) diff --git a/Cargo.lock b/Cargo.lock index a6beaf61..cd4e20bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1185,6 +1185,7 @@ dependencies = [ "pkg-config", "plist", "qrcode", + "quick-xml 0.41.0", "regex", "ring", "semver", diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index cba092d3..ded64510 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,50 @@ +## Next Windows release + +Not released yet. + +- When the second screen doesn't appear, the Windows Stream page says why and + what to do: the display driver isn't installed (run Setup again), Windows + couldn't verify the driver's signature, the driver stopped with an error + (with the Windows code: restart, then run Setup again), or the driver ran but + Windows added no screen. When Windows adds the screen but keeps the desktop + off it, the page says to choose Extend with Windows key + P or in Display + settings, and the next try waits 30 seconds for it. EternalMonitor does not + change the display layout itself. The display task no longer reports success + when the driver is missing or stopped with an error. After any failed attempt the support + diagnostics and **Copy logs** hold a display snapshot: the device state, the + graphics cards and the display paths Windows reports. The snapshot leaves out + the PC's name and the user's name; the rest of the log still contains them. + Before, every case read "no display appeared in time". +- The second screen is drawn by the graphics card that drives the main screen, + whether or not **Match the iPad's resolution** is on. Before, the driver + chose the card with the most memory, which on laptops with Intel and NVIDIA + graphics drives no screen; in 0.3.1 diagnostics none of those laptops got the + second screen. A card Windows lists twice under one name, as on some + laptops, is still named when only its first listing drives a screen; when + two cards with one name could, the driver still chooses. When another app + installed the driver and the user or that app named a card, EternalMonitor + leaves that name alone. The log records which card the driver used. +- The second screen comes up at the iPad's resolution. Windows restores the + size it last saved for the second screen, and in 0.3.1 diagnostics 4 of the 5 + PCs whose second screen appeared got a tiny, blurry 800×600 one. EternalMonitor + now sets the second screen's size itself, and only that display's: the + iPad's size, the sharpest size that fits when the driver doesn't offer it, or + 1920×1080 while the iPad's size isn't known. The log records the size once. +- A PC set to **Mirror my screen** shows the first picture without waiting for + the virtual display to be turned off at startup. A slow or stuck display task + held it for up to 19 s in 0.3.1, and about 2 s on a healthy PC. +- Settings says that **Match the iPad's resolution** applies the next time the + second screen starts, when the iPad connects again. Before, changing it + seemed to do nothing. +- Running EternalMonitor-Setup.exe again repairs a virtual display driver that + Setup installed when none of its devices works, for example after the device + was removed in Device Manager. Working devices are never removed, and a + driver another app installed is left alone with a message saying so. Before, + Setup kept the existing driver and stopped with "Windows did not finish + installing the virtual display driver". +- Support diagnostics name Windows 11 from the build number. Windows 11 keeps + "Windows 10" in the registry, which earlier versions reported as the system. + ## EternalMonitor v0.3.2 Released October 22, 2026. Install the Windows and iPad 0.3.2 updates together; diff --git a/docs/decisions.md b/docs/decisions.md index f83f533c..4b8bd45a 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -135,7 +135,12 @@ triggers via `schtasks`, avoiding a per-toggle UAC prompt. once per 10 s on its own. - Before enabling, the host writes `vdd_settings.xml` so the virtual display offers the iPad's native landscape resolution and refresh. There - is an opt-out in Settings. + is an opt-out in Settings. The worker also names the main screen's + graphics card there just before each Enable, the driver's only way to pick + a card, and replaces only a name it may: on a driver Setup installed, an + empty or `default` one, or its own last one. A start that fails, and a + second screen that shows no picture, log a display snapshot while the + display is still on, with the card the driver rendered on. - A first-party signed display driver remains a long-term goal. ## Input relay diff --git a/host/Cargo.toml b/host/Cargo.toml index f0dff145..d5d40a8f 100644 --- a/host/Cargo.toml +++ b/host/Cargo.toml @@ -25,6 +25,7 @@ qrcode = { version = "0.14", default-features = false } serde = { version = "1", features = ["derive"] } serde_json = "1" plist = "1" +quick-xml = "0.41" semver = "1" ureq = { version = "3.4.2", default-features = false, features = ["rustls", "json"] } # SHA-256 of downloaded updates (already used by rustls) @@ -59,6 +60,7 @@ version = "0.58" features = [ "Win32_Foundation", "Win32_Devices_DeviceAndDriverInstallation", + "Win32_Devices_Display", "Win32_Security", "Win32_Media_Audio", "Win32_System_Com", diff --git a/host/src/capture/mod.rs b/host/src/capture/mod.rs index a9fb2fa4..9e80ca98 100644 --- a/host/src/capture/mod.rs +++ b/host/src/capture/mod.rs @@ -17,6 +17,8 @@ use tracing::{info, warn}; use crate::control::SharedControl; #[cfg(any(windows, test))] use crate::control::{CaptureTarget, VddStatus}; +#[cfg(windows)] +use crate::vdd_device::DeviceState; #[cfg(windows)] pub mod dxgi; @@ -339,6 +341,239 @@ fn capture_is_current(shared: &SharedControl, generation: u64) -> bool { && crate::supervisor::generation_is_current(generation) } +#[cfg(windows)] +const VDD_SETTINGS_PATH: &str = r"C:\VirtualDisplayDriver\vdd_settings.xml"; + +/// In the app data folder: the graphics card this host last named in the +/// driver's settings file. +#[cfg(windows)] +const VDD_PIN_RECORD: &str = "vdd-pin.txt"; + +/// What the host asked the driver for before an enable: the graphics card +/// (or why none), and the mode the second screen should come up in (none +/// when the user keeps the driver's modes). +#[cfg(windows)] +struct VddRequest { + pin: Result, + mode: Option, + /// The adapters' LUIDs before the Enable, to find the driver's own. + before: Vec, +} + +/// Before each enable: name the main screen's graphics card for the driver +/// to render on, and, when the user wants it, the iPad's modes, or a +/// 1920x1080 default while the iPad's size is not known. Without a +/// name the driver renders on the card with the most memory. In 0.3.1 +/// diagnostics no Intel + NVIDIA laptop got the second screen; that card +/// choice is a suspect, not a confirmed cause. Failure is non-fatal — the +/// driver's built-in choices apply. Returns the card asked for, or why none. +#[cfg(windows)] +fn prepare_vdd_settings(shared: &SharedControl, outputs: &[OutputInfo]) -> VddRequest { + let main_screen = outputs + .iter() + .find(|output| output.is_primary) + .map(|output| output.adapter_name.as_str()); + let adapters = crate::vdd_snapshot::adapters(); + let before = adapters.iter().map(|adapter| adapter.luid).collect(); + // The virtual display's own adapter, listed while it is on, is not a card. + let cards = crate::vdd_snapshot::render_adapters(&adapters, &crate::vdd_snapshot::targets()); + let wanted = crate::vdd_settings::gpu_to_pin(main_screen, &cards); + if let Err(reason) = wanted { + info!( + reason, + main_screen, "Leaving the virtual display's graphics card to the driver" + ); + } + let gpu = wanted.ok(); + let record = crate::settings::app_data_dir().map(|dir| dir.join(VDD_PIN_RECORD)); + let path = std::path::Path::new(VDD_SETTINGS_PATH); + // Change the card only where this host may replace the name. + let owned = std::env::current_exe() + .ok() + .and_then(|exe| exe.parent().map(crate::vdd_settings::driver_owned)) + .unwrap_or(false); + let mut mode = None; + let applied = match &record { + None => Err(std::io::Error::other("no app data folder")), + Some(record) if crate::settings::SettingsFile::load().vdd_match_resolution => { + let client = shared.session.lock().client_info(); + let modes = client.map_or_else(crate::vdd_settings::default_modes, |info| { + crate::vdd_settings::modes_for_client( + info.screen_px.0, + info.screen_px.1, + info.refresh_hz, + ) + }); + let written = crate::vdd_settings::write_with_pin(path, record, owned, &modes, gpu); + match &written { + Ok(_) => { + info!( + ?modes, + "Wrote virtual display mode list for the connected client" + ); + mode = modes.first().copied(); + } + Err(error) => warn!( + %error, + "Could not write vdd_settings.xml — the modes the driver has apply" + ), + } + written + } + // The modes in the file are the user's: change only the card. + Some(record) => crate::vdd_settings::apply_pin(path, record, owned, gpu), + }; + match &applied { + Ok(crate::vdd_settings::PinOutcome::Named) => info!( + gpu, + "Pointed the virtual display at the main screen's graphics card" + ), + Ok(crate::vdd_settings::PinOutcome::Reset) => { + info!("Took back the graphics card this host named for the virtual display") + } + Ok(crate::vdd_settings::PinOutcome::Kept(reason)) => { + info!( + reason, + gpu, "Left the graphics card named in vdd_settings.xml" + ) + } + Ok(crate::vdd_settings::PinOutcome::Unchanged) => {} + Err(error) if gpu.is_some() => { + warn!(%error, gpu, "Left vdd_settings.xml as it is; the driver chooses the graphics card") + } + Err(_) => {} + } + VddRequest { + pin: crate::vdd_settings::pin_result(wanted, &applied), + mode, + before, + } +} + +/// Bring a freshly attached second screen to the mode asked for, when +/// Windows restored another one for this set of displays, and log the mode +/// it ends up in, once per attach. Only the virtual display changes. +#[cfg(windows)] +fn settle_vdd_mode( + chosen: OutputInfo, + requested: Option, + mut heartbeat: impl FnMut(), +) -> OutputInfo { + let attached = format!("{}x{}", chosen.width, chosen.height); + let Some(mode) = crate::vdd_settings::mode_to_set(requested, (chosen.width, chosen.height)) + else { + info!( + mode = %attached, + requested = %requested.map_or("the driver's modes".to_string(), |m| m.to_string()), + "Second screen mode" + ); + return chosen; + }; + let mut set = crate::vdd_device::set_display_mode(&chosen.device_name, mode); + let mut fallback = None; + if !set { + // The driver does not offer the iPad's mode: take the sharpest that fits. + let offered = crate::vdd_device::offered_modes(&chosen.device_name); + if let Some((width, height)) = crate::vdd_settings::best_offered(mode, &offered) + .filter(|&size| size != (chosen.width, chosen.height)) + { + let size = crate::vdd_settings::VddMode { + width, + height, + ..mode + }; + set = crate::vdd_device::set_display_mode(&chosen.device_name, size); + fallback = Some(size); + } + } + let target = fallback.unwrap_or(mode); + let mut now = chosen.clone(); + // Windows applies the change asynchronously to DXGI's view of the output. + for _ in 0..30 { + heartbeat(); + if let Some(output) = enumerate_outputs() + .into_iter() + .find(|output| output.device_name == chosen.device_name) + { + now = output; + } + if !set || (now.width, now.height) == (target.width, target.height) { + break; + } + std::thread::sleep(Duration::from_millis(100)); + } + info!( + mode = %format!("{}x{}", now.width, now.height), + requested = %mode, + attached = %attached, + fallback = %fallback.map_or("none".to_string(), |m| m.to_string()), + set, + "Second screen mode" + ); + now +} + +/// Log how Windows sees the displays, before the Disable task removes the +/// virtual one. One record per failure; see `vdd_snapshot`. `request` is +/// what the worker asked of the driver before its Enable. +#[cfg(windows)] +fn log_failure_snapshot(device: crate::vdd::EnableReport, request: Option<&VddRequest>) { + let (pin, before) = request.map_or((Err("no settings prepared"), &[][..]), |request| { + (request.pin.clone(), &request.before[..]) + }); + let snapshot = crate::vdd_snapshot::collect(device, pin, before); + warn!( + "Virtual display failure snapshot {}", + crate::vdd_snapshot::line(&snapshot) + ); +} + +/// A start that a mode change ended after this long without a display is +/// worth a snapshot: on PCs where it works, the display attaches within +/// milliseconds of the Enable task. +#[cfg(any(windows, test))] +const CANCELLED_SNAPSHOT_AFTER: Duration = Duration::from_secs(2); + +/// Whether a cancelled attach that waited `waited` logs a snapshot: once per +/// launch (`logged`). In 0.3.2 the iPad's own 10 s limit ended every attempt +/// on PCs whose display never attached, so no failure snapshot was ever +/// written. The iPad no longer ends a start, which times out and logs its +/// own snapshot; the user choosing another mode while it waits still does. +#[cfg(any(windows, test))] +fn snapshot_cancelled_attach(waited: Duration, logged: &std::sync::atomic::AtomicBool) -> bool { + waited >= CANCELLED_SNAPSHOT_AFTER && !logged.swap(true, std::sync::atomic::Ordering::Relaxed) +} + +/// Debug builds only, to check the failure path on a PC whose virtual +/// display works: `ETERNAL_SIMULATE_ATTACH_TIMEOUT=1` treats it as not +/// attached after the normal wait. +#[cfg(all(windows, debug_assertions))] +fn simulated_attach_timeout() -> bool { + std::env::var_os("ETERNAL_SIMULATE_ATTACH_TIMEOUT").is_some() +} + +#[cfg(all(windows, not(debug_assertions)))] +const fn simulated_attach_timeout() -> bool { + false +} + +/// Say why the second screen did not appear: on the Stream page each time, +/// and in the log once per iPad session for each state, so diagnostics show +/// it without repeating every retry. +#[cfg(windows)] +fn report_device(shared: &SharedControl, device: DeviceState) { + static LOGGED: Mutex, DeviceState)>> = Mutex::new(Vec::new()); + let problem = device.problem(); + *shared.vdd_problem.lock() = Some(problem); + let key = (shared.session.lock().session_id(), device); + let mut logged = LOGGED.lock(); + logged.retain(|(session, _)| *session == key.0); + if !logged.contains(&key) { + logged.push(key); + warn!(vdd_device = %device, ?problem, "Virtual display device state"); + } +} + #[cfg(windows)] fn vdd_attach_timeout() -> Duration { std::env::var("ETERNAL_VDD_TIMEOUT_SECS") @@ -383,7 +618,7 @@ pub(crate) fn reconcile_virtual_display( prove_timeout_ms = second_screen::PROVE_TIMEOUT.as_millis() as u64, "The virtual display showed no picture — mirroring the primary display instead" ); - second_screen::send(shared, second_screen::Command::Release); + second_screen::send(shared, second_screen::Command::Fallback(reason)); return Some(CaptureTarget::PrimaryAuto); } let connected = shared.client_connected(); @@ -889,6 +1124,20 @@ mod tests { assert!(matches!(result, AttachResult::Cancelled)); } + /// The iPad left 5 s into the wait on a PC whose display never came, as + /// it did every 11 s on a 0.3.2 hybrid laptop: one snapshot, not one per + /// retry, and none for a restart right after the Enable. + #[test] + fn a_cancelled_attach_logs_one_snapshot_per_launch() { + let logged = std::sync::atomic::AtomicBool::new(false); + assert!(!snapshot_cancelled_attach( + Duration::from_millis(300), + &logged + )); + assert!(snapshot_cancelled_attach(Duration::from_secs(5), &logged)); + assert!(!snapshot_cancelled_attach(Duration::from_secs(5), &logged)); + } + fn out(name: &str, left: i32, top: i32) -> OutputInfo { OutputInfo { adapter_index: 0, diff --git a/host/src/capture/second_screen.rs b/host/src/capture/second_screen.rs index 4a49b463..2807cf2b 100644 --- a/host/src/capture/second_screen.rs +++ b/host/src/capture/second_screen.rs @@ -31,6 +31,10 @@ use crate::control::{CaptureTarget, SharedControl, VddStatus}; #[cfg(any(windows, test))] use super::{wait_for_virtual_output, AttachResult, OutputInfo}; +#[cfg(any(windows, test))] +use crate::vdd::EnableReport; +#[cfg(any(windows, test))] +use crate::vdd_device::{DeviceState, VddProblem}; /// How long the second screen stays on after its iPad leaves. A USB redial /// comes about 1 s after the tunnel closes, and an iPad returning from the @@ -65,6 +69,12 @@ const WAIT_STEP: Duration = Duration::from_millis(100); #[cfg(any(windows, test))] const STOPPED_WAIT: Duration = Duration::from_secs(6); +/// How long the attempt after "kept your desktop off it" waits, while the +/// user picks Extend with Windows key + P or in Settings. The iPad has the +/// mirror meanwhile, so it stays connected. +#[cfg(any(windows, test))] +pub(crate) const EXTEND_TIMEOUT: Duration = Duration::from_secs(30); + /// How long quitting waits for the worker to exit after the last Enable. const QUIT_JOIN: Duration = Duration::from_secs(5); @@ -267,6 +277,20 @@ pub fn note_attached(shared: &SharedControl, device_name: String) { state.proving = None; } +/// [`note_attached`], unless the mode changed or quitting began. The mode +/// is held while the display is recorded, so a mode change either comes +/// first and is seen here, or comes after, and its capture generation finds +/// the display to turn off. +#[cfg(any(windows, test))] +fn note_attached_if_selected(shared: &SharedControl, device_name: String) -> bool { + let mode = shared.capture_target.lock(); + if *mode != CaptureTarget::VirtualExtended || quitting(shared) { + return false; + } + note_attached(shared, device_name); + true +} + /// Capture `generation` streams the second screen from `now`. The first /// generation after the switch starts the [`PROVE_TIMEOUT`] deadline; a /// later one inherits it, and any capture failure, until an image arrives. @@ -316,7 +340,11 @@ pub(crate) enum Command { Attach, /// Turn off the display this host turned on, unless a start claimed it /// again since. + #[cfg_attr(not(any(windows, test)), allow(dead_code))] // Sent by capture's reconcile. Release, + /// The display showed no picture, for this reason: log how Windows sees + /// it while it is still on, then release it. + Fallback(&'static str), } /// The one thread that turns the virtual display on and off, and its queue. @@ -328,6 +356,9 @@ pub(crate) struct Worker { /// [`Queue::quitting`], so no Enable runs or starts once it has. enable: Mutex<()>, thread: Mutex>>, + /// A cancelled start logged its snapshot; see `snapshot_cancelled_attach`. + #[cfg(any(windows, test))] + cancelled_snapshot: std::sync::atomic::AtomicBool, } #[derive(Default)] @@ -391,7 +422,7 @@ fn without_driver(shared: &SharedControl, command: Command) { ); fail_start(shared, "no_driver"); } - Command::Release => *shared.vdd_output.lock() = None, + Command::Release | Command::Fallback(_) => *shared.vdd_output.lock() = None, } } @@ -414,7 +445,10 @@ fn spawn_worker(shared: &SharedControl) -> std::io::Result<()> { .spawn(move || { run( &worker, - &WindowsDriver { shared: &worker }, + &WindowsDriver { + shared: &worker, + request: Default::default(), + }, super::vdd_attach_timeout(), true, ) @@ -481,6 +515,10 @@ pub(crate) fn run( match command { Command::Attach => attach(shared, driver, attach_timeout), Command::Release => release(shared, driver), + Command::Fallback(reason) => { + driver.report(Failure::Unproven(reason)); + release(shared, driver); + } } } } @@ -490,13 +528,47 @@ pub(crate) fn run( #[cfg(any(windows, test))] pub(crate) trait Driver { fn outputs(&self) -> Vec; - /// Prepare the driver's settings for the connected iPad. - fn prepare(&self); + /// Prepare the driver's settings for the connected iPad, just before the + /// Enable: the graphics card to render on and the modes. `outputs` are + /// the displays before it. + fn prepare(&self, outputs: &[OutputInfo]); fn set_enabled(&self, enabled: bool) -> bool; + /// Turn the display on. On failure, what the Enable task found wrong + /// with the device, or why it could not say. + fn enable(&self) -> Result<(), EnableReport> { + if self.set_enabled(true) { + Ok(()) + } else { + Err(Err("display task failed")) + } + } + /// Bring a display that just attached to the mode asked for, and return + /// it as it ends up. + fn settle(&self, chosen: OutputInfo) -> OutputInfo { + chosen + } + /// Say why the display did not appear or showed no picture, while it is + /// still on. + fn report(&self, _failure: Failure) {} fn now(&self) -> Instant; fn sleep(&self, duration: Duration); } +/// Why a second screen failed, for [`Driver::report`]. +#[cfg(any(windows, test))] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum Failure { + /// The Enable task failed. + Enable(EnableReport), + /// Enabled, but no display attached in time. + AttachTimeout, + /// A mode change ended a start that had waited this long for the + /// display. + Cancelled(Duration), + /// The display attached but showed no picture. + Unproven(&'static str), +} + /// However [`attach`] ends, its claim ends with it: a start that did not /// finish leaves the status for the next tick to claim again. #[cfg(any(windows, test))] @@ -553,26 +625,38 @@ pub(crate) fn attach(shared: &SharedControl, driver: &impl Driver, timeout: Dura driver.sleep(WAIT_STEP); } let outputs = driver.outputs(); - driver.prepare(); + driver.prepare(&outputs); let before: Vec = outputs .into_iter() .map(|output| output.device_name) .collect(); + // After "kept your desktop off it" the user switches Windows to Extend + // while this attempt waits, so give them time to. + let timeout = match shared.vdd_problem.lock().take() { + Some(VddProblem::NotExtended) => timeout.max(EXTEND_TIMEOUT), + _ => timeout, + }; let Some(enabled) = enable(shared, driver) else { info!("Quitting before the virtual display was turned on — leaving it off"); return; }; - if !enabled { + if let Err(report) = enabled { warn!( second_screen_fallback = "enable_failed", "Virtual display could not be enabled (installer task missing?) — \ capturing the primary display instead" ); + driver.report(Failure::Enable(report)); + // The task enabled a device that cannot show a display. + if report.is_ok_and(|device| device != DeviceState::Missing) { + disable(shared, driver); + } let mut status = shared.vdd_status.lock(); *status = VddStatus::TaskFailed; shared.second_screen.lock().failure = Some("enable_failed"); return; } + let waiting_since = driver.now(); let result = wait_for_virtual_output( &before, timeout, @@ -580,6 +664,11 @@ pub(crate) fn attach(shared: &SharedControl, driver: &impl Driver, timeout: Dura || driver.now(), |duration| driver.sleep(duration), ); + #[cfg(windows)] + let result = match result { + AttachResult::Attached(_) if super::simulated_attach_timeout() => AttachResult::TimedOut, + result => result, + }; match result { AttachResult::Cancelled if quitting(shared) => { info!("Quitting while the virtual display started — the quit turns it off"); @@ -589,16 +678,30 @@ pub(crate) fn attach(shared: &SharedControl, driver: &impl Driver, timeout: Dura // records it as ours, so turn it off, after the gap like every // change: the mirror is already streaming. info!("Display mode changed while the virtual display started — turning it off"); + let waited = driver.now().duration_since(waiting_since); + if super::snapshot_cancelled_attach(waited, &shared.vdd_worker.cancelled_snapshot) { + driver.report(Failure::Cancelled(waited)); + } disable(shared, driver); } AttachResult::Attached(chosen) => { - info!( - device = %chosen.device_name, - width = chosen.width, - height = chosen.height, - "Virtual display attached — capturing it" - ); - note_attached(shared, chosen.device_name); + // Setting the mode can take seconds, and the user may choose + // another mode meanwhile. + let chosen = driver.settle(chosen); + let (device, width, height) = (chosen.device_name.clone(), chosen.width, chosen.height); + if note_attached_if_selected(shared, chosen.device_name) { + info!( + %device, + width, height, "Virtual display attached — capturing it" + ); + } else if quitting(shared) { + info!("Quitting while the virtual display started — the quit turns it off"); + } else { + // Nothing records it as ours, so no capture generation turns + // it off: this does, after the gap, as for a cancelled start. + info!("Display mode changed while the virtual display's mode was set — turning it off"); + disable(shared, driver); + } } AttachResult::TimedOut => { warn!( @@ -606,6 +709,7 @@ pub(crate) fn attach(shared: &SharedControl, driver: &impl Driver, timeout: Dura timeout_ms = timeout.as_millis() as u64, "Virtual display did not attach in time — capturing the primary display instead" ); + driver.report(Failure::AttachTimeout); // Enable succeeded but nothing attached: turn it back off so no // half-enabled device lingers as a phantom monitor. disable(shared, driver); @@ -619,12 +723,12 @@ pub(crate) fn attach(shared: &SharedControl, driver: &impl Driver, timeout: Dura /// The only Enable: never once quitting has begun, and quitting waits for /// one in flight. None when quitting. #[cfg(any(windows, test))] -fn enable(shared: &SharedControl, driver: &impl Driver) -> Option { +fn enable(shared: &SharedControl, driver: &impl Driver) -> Option> { let _enable = shared.vdd_worker.enable.lock(); if quitting(shared) { return None; } - let enabled = driver.set_enabled(true); + let enabled = driver.enable(); shared.second_screen.lock().note_toggle(driver.now()); Some(enabled) } @@ -692,6 +796,8 @@ fn wait_for_toggle_gap( #[cfg(windows)] struct WindowsDriver<'a> { shared: &'a SharedControl, + /// What the last `prepare` asked of the driver, for the snapshots. + request: std::cell::RefCell>, } #[cfg(windows)] @@ -700,38 +806,61 @@ impl Driver for WindowsDriver<'_> { super::enumerate_outputs() } - // Best effort: match the virtual display's mode list to the connected - // iPad's panel BEFORE enabling the driver (it reads vdd_settings.xml at - // enable time). Failure is non-fatal — the driver's built-in modes apply. - fn prepare(&self) { - if !crate::settings::SettingsFile::load().vdd_match_resolution { - return; - } - let Some(info) = self.shared.session.lock().client_info() else { - return; - }; - let modes = crate::vdd_settings::modes_for_client( - info.screen_px.0, - info.screen_px.1, - info.refresh_hz, - ); - let path = std::path::Path::new(r"C:\VirtualDisplayDriver\vdd_settings.xml"); - match crate::vdd_settings::write_to(path, &modes) { - Ok(()) => info!( - ?modes, - "Wrote virtual display mode list for the connected client" - ), - Err(error) => warn!( - %error, - "Could not write vdd_settings.xml — driver default modes apply" - ), - } + fn prepare(&self, outputs: &[OutputInfo]) { + *self.request.borrow_mut() = Some(super::prepare_vdd_settings(self.shared, outputs)); } fn set_enabled(&self, enabled: bool) -> bool { crate::vdd::set_enabled(enabled, || {}) } + fn enable(&self) -> Result<(), EnableReport> { + crate::vdd::enable(|| {}) + } + + fn settle(&self, chosen: OutputInfo) -> OutputInfo { + let request = self.request.borrow(); + let chosen = super::settle_vdd_mode(chosen, request.as_ref().and_then(|r| r.mode), || {}); + let before = request.as_ref().map_or(&[][..], |r| &r.before[..]); + let render = crate::vdd_snapshot::render_card_now(before); + info!( + device = %chosen.device_name, + render = render.as_deref().unwrap_or("unknown"), + "Virtual display renders on this graphics card" + ); + chosen + } + + fn report(&self, failure: Failure) { + let request = self.request.borrow(); + let device = match failure { + Failure::Enable(report) => { + if let Ok(device) = report { + super::report_device(self.shared, device); + } + report + } + Failure::AttachTimeout => { + // A device that is ready with its monitor means Windows left + // the desktop off it. The display layout stays the user's. + let device = crate::vdd_device::query(); + super::report_device(self.shared, device); + Ok(device) + } + Failure::Cancelled(waited) => { + info!( + waited_ms = waited.as_millis() as u64, + "Virtual display had not attached when the attempt ended" + ); + Ok(crate::vdd_device::query()) + } + // It attached, so the device is fine; the snapshot shows the + // card and the paths it got. + Failure::Unproven(_) => Ok(crate::vdd_device::query()), + }; + super::log_failure_snapshot(device, request.as_ref()); + } + fn now(&self) -> Instant { Instant::now() } @@ -809,17 +938,32 @@ mod tests { } } + /// What the worker asked of a [`FakeDriver`], in order. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + enum Call { + Prepare, + Enable, + Disable, + Settle, + Report(Failure), + } + /// A driver on a paused clock. Enable takes `enable_takes`, and the /// display attaches `attach_after` later, or never. `on_sleep` sees the /// clock each time the host waits, as the transport's ticks would. + /// With `enable_fails`, the Enable task reports that instead. Setting + /// the mode after the attach takes `settle_takes`. struct FakeDriver<'a> { start: Instant, elapsed: Cell, enable_takes: Duration, + settle_takes: Duration, attach_after: Option, on_since: Cell>, toggles: RefCell>, on_sleep: &'a dyn Fn(Duration), + enable_fails: Option, + calls: RefCell>, } impl<'a> FakeDriver<'a> { @@ -832,12 +976,19 @@ mod tests { start: Instant::now(), elapsed: Cell::new(Duration::ZERO), enable_takes, + settle_takes: Duration::ZERO, attach_after, on_since: Cell::new(None), toggles: RefCell::new(Vec::new()), on_sleep, + enable_fails: None, + calls: RefCell::new(Vec::new()), } } + + fn calls(&self) -> Vec { + self.calls.borrow().clone() + } } impl Driver for FakeDriver<'_> { @@ -854,12 +1005,41 @@ mod tests { outputs } - fn prepare(&self) {} + fn prepare(&self, outputs: &[OutputInfo]) { + assert!(outputs.iter().any(|output| output.is_primary)); + self.calls.borrow_mut().push(Call::Prepare); + } + + fn enable(&self) -> Result<(), EnableReport> { + match self.enable_fails { + Some(report) => { + self.sleep(self.enable_takes); + self.calls.borrow_mut().push(Call::Enable); + self.on_since.set(None); + Err(report) + } + None if self.set_enabled(true) => Ok(()), + None => Err(Err("display task failed")), + } + } + + fn settle(&self, chosen: OutputInfo) -> OutputInfo { + self.sleep(self.settle_takes); + self.calls.borrow_mut().push(Call::Settle); + chosen + } + + fn report(&self, failure: Failure) { + self.calls.borrow_mut().push(Call::Report(failure)); + } fn set_enabled(&self, enabled: bool) -> bool { if enabled { self.sleep(self.enable_takes); } + self.calls + .borrow_mut() + .push(if enabled { Call::Enable } else { Call::Disable }); self.toggles .borrow_mut() .push((self.elapsed.get(), enabled)); @@ -1054,6 +1234,46 @@ mod tests { assert_eq!(next_step(&shared, driver.now()), Step::Nothing); } + /// Review round 3, defect 1: Mirror chosen while the attached display's + /// mode was being set. Mirror's capture generation found no display to + /// turn off, and the start then recorded it as the active second screen. + #[test] + fn a_mode_change_while_the_mode_is_set_turns_the_display_back_off() { + let shared = second_screen_selected(); + connect_viewer(&shared); + assert_eq!(next_step(&shared, Instant::now()), Step::Attach); + let mirror = Once::at(secs(4.0)); + let on_sleep = |elapsed: Duration| { + mirror.run(elapsed, || { + *shared.capture_target.lock() = CaptureTarget::PrimaryAuto; + let generation = crate::supervisor::CURRENT_GENERATION.load(Ordering::SeqCst); + assert_eq!( + reconcile_virtual_display(&CaptureTarget::PrimaryAuto, &shared, generation), + Some(CaptureTarget::PrimaryAuto) + ); + }) + }; + // Enable done at 3 s, attached at 3.5 s, the mode set by 6.5 s. + let driver = FakeDriver { + settle_takes: secs(3.0), + ..FakeDriver::new(secs(3.0), Some(secs(0.5)), &on_sleep) + }; + attach(&shared, &driver, ATTACH_TIMEOUT); + run(&shared, &driver, ATTACH_TIMEOUT, false); + assert!(mirror.done.get()); + assert!(driver.calls().contains(&Call::Settle)); + let toggles = driver.toggles.borrow().clone(); + assert_eq!( + toggles.iter().map(|(_, on)| *on).collect::>(), + [true, false], + "{toggles:?}" + ); + assert!(toggles[1].0 - toggles[0].0 >= MIN_TOGGLE_GAP, "{toggles:?}"); + assert_eq!(*shared.vdd_output.lock(), None); + assert_ne!(*shared.vdd_status.lock(), VddStatus::Active); + assert_eq!(next_step(&shared, driver.now()), Step::Nothing); + } + /// Review round 1, defect 3: Mirror then Second screen again during the /// start, before any capture generation reconciled Mirror. The start must /// not stay claimed with nobody running it. @@ -1255,7 +1475,7 @@ mod tests { fn outputs(&self) -> Vec { vec![output("primary", true)] } - fn prepare(&self) {} + fn prepare(&self, _: &[OutputInfo]) {} fn set_enabled(&self, enabled: bool) -> bool { if enabled { let _ = self.entered.send(()); @@ -1398,6 +1618,11 @@ mod tests { let driver = FakeDriver::new(secs(3.0), Some(Duration::ZERO), &on_sleep); run(&shared, &driver, ATTACH_TIMEOUT, false); assert_eq!(*driver.toggles.borrow(), vec![(Duration::ZERO, false)]); + // The snapshot is taken while the display is still on. + assert_eq!( + driver.calls(), + [Call::Report(Failure::Unproven(reason)), Call::Disable] + ); assert_eq!(next_step(&shared, Instant::now()), Step::Nothing); } } @@ -1461,7 +1686,7 @@ mod tests { fn outputs(&self) -> Vec { vec![output("primary", true)] } - fn prepare(&self) { + fn prepare(&self, _: &[OutputInfo]) { if self.hold_prepare { let _ = self.entered.send("prepare"); let _ = self.go.recv(); @@ -1637,4 +1862,144 @@ mod tests { assert_eq!(*shared.vdd_status.lock(), VddStatus::Active); assert_eq!(next_step(&shared, driver.now()), Step::Switch); } + + /// PR #72's attach work, in the worker: the card is named just before + /// the Enable, a display that attaches is brought to the iPad's mode, + /// and one that never does is reported while it is still on. + #[test] + fn the_worker_names_the_card_before_the_enable_and_reports_a_timeout_while_on() { + let shared = second_screen_selected(); + connect_viewer(&shared); + assert_eq!(next_step(&shared, Instant::now()), Step::Attach); + let on_sleep = |_| {}; + let driver = FakeDriver::new(secs(3.0), Some(secs(0.5)), &on_sleep); + attach(&shared, &driver, ATTACH_TIMEOUT); + assert_eq!(driver.calls(), [Call::Prepare, Call::Enable, Call::Settle]); + assert_eq!(*shared.vdd_status.lock(), VddStatus::Active); + + let shared = second_screen_selected(); + connect_viewer(&shared); + assert_eq!(next_step(&shared, Instant::now()), Step::Attach); + let driver = FakeDriver::new(secs(3.0), None, &on_sleep); + attach(&shared, &driver, ATTACH_TIMEOUT); + assert_eq!( + driver.calls(), + [ + Call::Prepare, + Call::Enable, + Call::Report(Failure::AttachTimeout), + Call::Disable + ] + ); + assert_eq!(remembered_failure(&shared), Some("attach_timeout")); + } + + /// The Enable task's report reaches the snapshot. A device it enabled + /// that cannot show a display is turned back off, after the gap; a + /// missing device or a task that could not say is left alone. + #[test] + fn a_failed_enable_is_reported_and_an_enabled_broken_device_turned_off() { + let on_sleep = |_| {}; + for (report, turned_off) in [ + (Ok(DeviceState::Problem(43)), true), + (Ok(DeviceState::NoDriver), true), + (Ok(DeviceState::Missing), false), + (Err("display tasks missing"), false), + ] { + let shared = second_screen_selected(); + connect_viewer(&shared); + assert_eq!(next_step(&shared, Instant::now()), Step::Attach); + let driver = FakeDriver { + enable_fails: Some(report), + ..FakeDriver::new(secs(3.0), None, &on_sleep) + }; + attach(&shared, &driver, ATTACH_TIMEOUT); + let mut expected = vec![ + Call::Prepare, + Call::Enable, + Call::Report(Failure::Enable(report)), + ]; + if turned_off { + expected.push(Call::Disable); + let toggles = driver.toggles.borrow().clone(); + assert!(toggles[0].0 - secs(3.0) >= MIN_TOGGLE_GAP, "{toggles:?}"); + } + assert_eq!(driver.calls(), expected, "{report:?}"); + assert_eq!(*shared.vdd_status.lock(), VddStatus::TaskFailed); + assert_eq!(remembered_failure(&shared), Some("enable_failed")); + } + } + + /// "Windows added the second screen but kept your desktop off it": the + /// iPad keeps the mirror, so after Try again the start waits 30 s for + /// the user to choose Extend. Any other failure waits the usual time. + #[test] + fn after_kept_off_the_desktop_the_next_start_waits_for_extend() { + let on_sleep = |_| {}; + for (problem, waits) in [ + (Some(VddProblem::NotExtended), EXTEND_TIMEOUT), + (Some(VddProblem::NoDisplay), ATTACH_TIMEOUT), + (None, ATTACH_TIMEOUT), + ] { + let shared = second_screen_selected(); + *shared.vdd_problem.lock() = problem; + connect_viewer(&shared); + assert_eq!(next_step(&shared, Instant::now()), Step::Attach); + let driver = FakeDriver::new(secs(3.0), None, &on_sleep); + attach(&shared, &driver, ATTACH_TIMEOUT); + let toggles = driver.toggles.borrow().clone(); + let waited = toggles[1].0 - toggles[0].0; + assert!( + waited >= waits && waited < waits + secs(1.0), + "{problem:?}: {toggles:?}" + ); + assert_eq!(*shared.vdd_problem.lock(), None, "cleared at each attempt"); + } + // Extend chosen 20 s into the wait: the second screen appears. + let shared = second_screen_selected(); + *shared.vdd_problem.lock() = Some(VddProblem::NotExtended); + connect_viewer(&shared); + assert_eq!(next_step(&shared, Instant::now()), Step::Attach); + let driver = FakeDriver::new(secs(3.0), Some(secs(20.0)), &on_sleep); + attach(&shared, &driver, ATTACH_TIMEOUT); + assert_eq!(*shared.vdd_status.lock(), VddStatus::Active); + } + + /// A mode change that ends a start the display has kept waiting logs a + /// snapshot, once per launch; a quick one does not. + #[test] + fn a_long_cancelled_start_is_reported_once() { + let shared = second_screen_selected(); + let cancel_after = |at: f64| { + // Each run has its own clock: forget the last run's toggle. + *shared.second_screen.lock() = State::default(); + *shared.capture_target.lock() = CaptureTarget::VirtualExtended; + *shared.vdd_status.lock() = VddStatus::WaitingForClient; + connect_viewer(&shared); + assert_eq!(next_step(&shared, Instant::now()), Step::Attach); + let switch = Once::at(secs(at)); + let on_sleep = |elapsed: Duration| { + switch.run(elapsed, || { + *shared.capture_target.lock() = CaptureTarget::PrimaryAuto + }) + }; + let driver = FakeDriver::new(secs(3.0), None, &on_sleep); + attach(&shared, &driver, ATTACH_TIMEOUT); + driver.calls() + }; + assert_eq!( + cancel_after(3.5), + [Call::Prepare, Call::Enable, Call::Disable] + ); + let calls = cancel_after(6.0); + assert!( + matches!(calls[2], Call::Report(Failure::Cancelled(waited)) if waited >= secs(3.0)), + "{calls:?}" + ); + assert_eq!(calls[3], Call::Disable); + assert_eq!( + cancel_after(6.0), + [Call::Prepare, Call::Enable, Call::Disable] + ); + } } diff --git a/host/src/control.rs b/host/src/control.rs index b385c19e..a84e81fa 100644 --- a/host/src/control.rs +++ b/host/src/control.rs @@ -36,6 +36,9 @@ pub struct SharedControl { /// Live status of the managed virtual extended display, surfaced in the GUI so a tester /// can see when an Extended-display request silently fell back to the primary monitor. pub vdd_status: Arc>, + /// Why the second screen did not appear, when Windows said, so the GUI + /// can say what to do. Cleared at each attempt. + pub vdd_problem: Arc>>, /// Set while Windows blocks screen capture (lock screen, UAC, a display /// it will not duplicate); the GUI explains it instead of a frozen picture. pub capture_block: Arc>>, @@ -151,6 +154,7 @@ impl SharedControl { encoder_override: Arc::new(Mutex::new(None)), capture_target: Arc::new(Mutex::new(CaptureTarget::PrimaryAuto)), vdd_status: Arc::new(Mutex::new(VddStatus::Inactive)), + vdd_problem: Arc::new(Mutex::new(None)), capture_block: Arc::new(Mutex::new(None)), vdd_worker: Arc::new(Default::default()), vdd_output: Arc::new(Mutex::new(None)), diff --git a/host/src/diagnostics.rs b/host/src/diagnostics.rs index 80d5115c..35c4e13c 100644 --- a/host/src/diagnostics.rs +++ b/host/src/diagnostics.rs @@ -128,14 +128,17 @@ fn windows_system_report() -> Vec { use windows::Win32::System::SystemInformation::{GlobalMemoryStatusEx, MEMORYSTATUSEX}; let os_key = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion"; let cpu_key = "HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0"; + let product = registry_string(os_key, "ProductName"); + let build = registry_string(os_key, "CurrentBuildNumber"); let mut lines = vec![format!( - // ProductName can retain "Windows 10" on Windows 11. Preserve its source - // instead of presenting that legacy registry label as the detected OS. - "Windows registry_product={} installation_type={} display_version={} build={}.{}", - registry_string(os_key, "ProductName"), + // ProductName can retain "Windows 10" on Windows 11. Name the OS from + // the build, and keep the registry label beside it as the source. + "Windows os={} registry_product={} installation_type={} display_version={} build={}.{}", + windows_name(&product, &build), + product, registry_string(os_key, "InstallationType"), registry_string(os_key, "DisplayVersion"), - registry_string(os_key, "CurrentBuildNumber"), + build, registry_dword(os_key, "UBR") )]; lines.push(format!( @@ -159,8 +162,35 @@ fn windows_system_report() -> Vec { lines } +/// The Windows name for a registry ProductName and build number. Windows 11 +/// starts at build 22000 but kept "Windows 10" in ProductName. +#[cfg_attr(not(windows), allow(dead_code))] // Read from Windows only. +fn windows_name(product: &str, build: &str) -> String { + let windows_11 = build + .trim() + .parse::() + .is_ok_and(|build| build >= 22000); + match product.strip_prefix("Windows 10") { + Some(edition) if windows_11 => format!("Windows 11{edition}"), + _ => product.to_string(), + } +} + #[cfg(windows)] -fn registry_string(key: &str, name: &str) -> String { +pub(crate) fn registry_string(key: &str, name: &str) -> String { + registry_sz(key, name).unwrap_or_else(|code| format!("unavailable({code})")) +} + +/// A string value under HKLM, `None` when it is missing or empty. +#[cfg(windows)] +pub(crate) fn registry_value(key: &str, name: &str) -> Option { + registry_sz(key, name) + .ok() + .filter(|value| !value.is_empty()) +} + +#[cfg(windows)] +fn registry_sz(key: &str, name: &str) -> Result { use windows::{ core::PCWSTR, Win32::System::Registry::{RegGetValueW, HKEY_LOCAL_MACHINE, RRF_RT_REG_SZ}, @@ -181,15 +211,17 @@ fn registry_string(key: &str, name: &str) -> String { ) }; if result.is_err() { - return format!("unavailable({})", result.0); + return Err(result.0.to_string()); } - String::from_utf16_lossy(&value[..value.iter().position(|c| *c == 0).unwrap_or(value.len())]) - .trim() - .to_string() + Ok(String::from_utf16_lossy( + &value[..value.iter().position(|c| *c == 0).unwrap_or(value.len())], + ) + .trim() + .to_string()) } #[cfg(windows)] -fn registry_dword(key: &str, name: &str) -> String { +pub(crate) fn registry_dword(key: &str, name: &str) -> String { use windows::{ core::PCWSTR, Win32::System::Registry::{RegGetValueW, HKEY_LOCAL_MACHINE, RRF_RT_REG_DWORD}, @@ -215,3 +247,27 @@ fn registry_dword(key: &str, name: &str) -> String { value.to_string() } } + +#[cfg(test)] +mod tests { + use super::windows_name; + + #[test] + fn windows_11_is_named_from_its_build_not_its_registry_label() { + // The customer's PC: Windows 11 26H2 with the legacy label. + assert_eq!(windows_name("Windows 10 Home", "26300"), "Windows 11 Home"); + assert_eq!(windows_name("Windows 10 Pro", "22000"), "Windows 11 Pro"); + assert_eq!( + windows_name("Windows 10 Home China", "26200"), + "Windows 11 Home China" + ); + assert_eq!(windows_name("Windows 10 Pro", "19045"), "Windows 10 Pro"); + assert_eq!(windows_name("Windows 10 Pro", "21996"), "Windows 10 Pro"); + // Already right, or unreadable: left as Windows reports it. + assert_eq!(windows_name("Windows 11 Pro", "26100"), "Windows 11 Pro"); + assert_eq!( + windows_name("Windows 10 Home", "unavailable(2)"), + "Windows 10 Home" + ); + } +} diff --git a/host/src/gui/page_settings.rs b/host/src/gui/page_settings.rs index 49c5f115..e475a7a1 100644 --- a/host/src/gui/page_settings.rs +++ b/host/src/gui/page_settings.rs @@ -37,11 +37,7 @@ impl AnalyzerApp { widgets::setting_row( ui, "Match the iPad's resolution", - if extending { - "Makes the second screen exactly as sharp and smooth as the iPad's display." - } else { - "Used by Second screen. Makes it exactly as sharp and smooth as the iPad's display." - }, + vdd_match_description(extending), 64.0, |ui| { widgets::toggle(ui, &mut vdd_match, "Match the second screen to the iPad's resolution"); @@ -328,3 +324,14 @@ fn choice_label(label: &str) -> String { other => other.into(), } } + +/// What "Match the iPad's resolution" does, and when a change applies: the +/// driver reads the modes when the second screen starts, and restarting +/// the stream keeps the second screen that is already there. +pub(super) fn vdd_match_description(extending: bool) -> &'static str { + if extending { + "Makes the second screen exactly as sharp and smooth as the iPad's display. A change applies the next time the second screen starts, when the iPad connects again." + } else { + "Used by Second screen. Makes it exactly as sharp and smooth as the iPad's display. A change applies the next time the second screen starts." + } +} diff --git a/host/src/gui/page_stream.rs b/host/src/gui/page_stream.rs index 6f2b085c..42b2b59d 100644 --- a/host/src/gui/page_stream.rs +++ b/host/src/gui/page_stream.rs @@ -12,6 +12,7 @@ use super::{ use crate::capture::recovery::CaptureBlock; use crate::control::VddStatus; use crate::vdd::VddTaskHealth; +use crate::vdd_device::VddProblem; impl AnalyzerApp { pub(super) fn stream_page( @@ -102,13 +103,19 @@ impl AnalyzerApp { ui.add_space(GAP - 10.0); } #[allow(unused_mut)] // Debug builds may simulate a problem below. - let (mut capture_block, mut vdd_status, mut vdd_health) = ( + let (mut capture_block, mut vdd_status, mut vdd_health, mut vdd_problem) = ( *self.control.shared.capture_block.lock(), *self.control.shared.vdd_status.lock(), crate::vdd::task_health(), + *self.control.shared.vdd_problem.lock(), ); #[cfg(debug_assertions)] - simulate_problem(&mut capture_block, &mut vdd_status, &mut vdd_health); + simulate_problem( + &mut capture_block, + &mut vdd_status, + &mut vdd_health, + &mut vdd_problem, + ); if let Some((icon, title, body)) = capture_block_notice(capture_block, snap.pipeline_running) { @@ -117,7 +124,9 @@ impl AnalyzerApp { } // The user asked for the extended (virtual) display but it could not be brought up, // so capture silently fell back to the primary monitor. Say so. - if let Some(notice) = second_screen_notice(vdd_status, vdd_health) { + let vdd_failure = crate::capture::second_screen::remembered_failure(&self.control.shared); + if let Some(notice) = second_screen_notice(vdd_status, vdd_health, vdd_problem, vdd_failure) + { let mut copy_logs = false; let mut retry = false; widgets::banner( @@ -125,7 +134,7 @@ impl AnalyzerApp { WARNING, Icon::Warning, SECOND_SCREEN_FAILED_TITLE, - notice.body, + ¬ice.body, 110.0 * (1 + notice.retry as u8 + notice.installer as u8) as f32, |ui| { copy_logs = @@ -599,6 +608,7 @@ fn simulate_problem( block: &mut Option, vdd: &mut VddStatus, health: &mut VddTaskHealth, + problem: &mut Option, ) { let Ok(name) = std::env::var("ETERNAL_SIMULATE_PROBLEM") else { return; @@ -615,6 +625,24 @@ fn simulate_problem( "driver-tasks-missing" => { (*vdd, *health) = (VddStatus::TaskFailed, VddTaskHealth::TasksMissing) } + "driver-not-installed" => { + (*vdd, *problem) = (VddStatus::TaskFailed, Some(VddProblem::NotInstalled)) + } + "driver-signature" => { + (*vdd, *problem) = (VddStatus::TaskFailed, Some(VddProblem::SignatureBlocked)) + } + "driver-error" => { + (*vdd, *problem) = ( + VddStatus::TaskFailed, + Some(VddProblem::DriverError(Some(43))), + ) + } + "display-did-not-appear" => { + (*vdd, *problem) = (VddStatus::TaskFailed, Some(VddProblem::NoDisplay)) + } + "display-not-extended" => { + (*vdd, *problem) = (VddStatus::Failed, Some(VddProblem::NotExtended)) + } _ => {} } } @@ -622,41 +650,83 @@ fn simulate_problem( /// What the second screen notice says and offers. #[derive(Debug, PartialEq, Eq)] pub(super) struct SecondScreenNotice { - pub body: &'static str, + pub body: String, pub retry: bool, pub installer: bool, } /// The notice for a second screen that could not start, by what the display -/// driver tasks report. `None` unless the second screen failed. +/// driver tasks and its device report, and why the start failed (the +/// `second_screen_fallback` reason). `None` unless the second screen failed. pub(super) fn second_screen_notice( status: VddStatus, health: VddTaskHealth, + problem: Option, + failure: Option<&str>, ) -> Option { - let notice = |body, retry, installer| { + use VddStatus::{Failed, TaskFailed}; + let notice = |body: &str, retry, installer| { Some(SecondScreenNotice { - body, + body: body.to_string(), retry, installer, }) }; - match (status, health) { - (VddStatus::Failed | VddStatus::TaskFailed, VddTaskHealth::NotResponding) => notice( + match (status, health, problem) { + (Failed | TaskFailed, VddTaskHealth::NotResponding, _) => notice( "Windows' display driver stopped responding. Restart the PC to use the second screen.", false, false, ), - (VddStatus::Failed | VddStatus::TaskFailed, VddTaskHealth::TasksMissing) => notice( + (Failed | TaskFailed, VddTaskHealth::TasksMissing, _) => notice( "The second screen isn't installed. Download EternalMonitor-Setup.exe and run it again to add it, then click Try again.", true, true, ), - (VddStatus::TaskFailed, VddTaskHealth::Ok) => notice( + (Failed | TaskFailed, VddTaskHealth::Ok, Some(VddProblem::NotInstalled)) => notice( + "The second screen's display driver isn't installed. Download EternalMonitor-Setup.exe and run it again to reinstall it, then click Try again.", + true, + true, + ), + (Failed | TaskFailed, VddTaskHealth::Ok, Some(VddProblem::SignatureBlocked)) => notice( + "Windows couldn't verify the display driver's signature, so it won't start it (Windows code 52). Run EternalMonitor-Setup.exe again while connected to the internet to reinstall the driver, then click Try again. You don't need to turn off Secure Boot or Memory Integrity.", + true, + true, + ), + (Failed | TaskFailed, VddTaskHealth::Ok, Some(VddProblem::DriverError(code))) => { + let code = code.map_or(String::new(), |code| format!(" (Windows code {code})")); + notice( + &format!("The second screen's display driver stopped with an error{code}. Restart the PC, then run EternalMonitor-Setup.exe again to repair it."), + true, + true, + ) + } + (Failed | TaskFailed, VddTaskHealth::Ok, Some(VddProblem::NoDisplay)) => notice( + "The display driver is running, but Windows didn't add the second screen. Click Try again. If it keeps happening, restart the PC.", + true, + false, + ), + (Failed | TaskFailed, VddTaskHealth::Ok, Some(VddProblem::NotExtended)) => notice( + "Windows added the second screen but kept your desktop off it. Click Try again, and while the second screen turns on, press Windows key + P and choose Extend, or choose Extend these displays in Settings > System > Display. EternalMonitor waits 30 seconds, and Windows remembers the choice.", + true, + false, + ), + (TaskFailed, VddTaskHealth::Ok, None) => notice( "Part of the EternalMonitor install is missing or blocked. Download EternalMonitor-Setup.exe and run it again to repair it, then click Try again.", true, true, ), - (VddStatus::Failed, VddTaskHealth::Ok) => notice( + // It attached, so it was not late: it showed no picture. + (Failed, VddTaskHealth::Ok, None) + if matches!(failure, Some("no_frames" | "duplication_failed")) => + { + notice( + "Windows added the second screen, but EternalMonitor couldn't get a picture from it, so the iPad shows your main screen instead. Click Try again. If it keeps happening, restart the PC.", + true, + false, + ) + } + (Failed, VddTaskHealth::Ok, None) => notice( "Windows didn't add the second screen in time. Click Try again. If it keeps happening, run EternalMonitor-Setup.exe again to repair the display driver.", true, false, diff --git a/host/src/gui/tests.rs b/host/src/gui/tests.rs index 636e5101..e7b756d7 100644 --- a/host/src/gui/tests.rs +++ b/host/src/gui/tests.rs @@ -362,28 +362,139 @@ fn the_second_screen_notice_says_which_driver_problem_applies() { VddTaskHealth::NotResponding, VddTaskHealth::TasksMissing, ] { - assert_eq!(second_screen_notice(status, health), None); + assert_eq!(second_screen_notice(status, health, None, None), None); } } for status in [VddStatus::Failed, VddStatus::TaskFailed] { // A hung driver needs a Windows restart; trying again cannot help. - let hung = second_screen_notice(status, VddTaskHealth::NotResponding).unwrap(); + let hung = second_screen_notice(status, VddTaskHealth::NotResponding, None, None).unwrap(); assert!(hung.body.contains("Restart the PC"), "{}", hung.body); assert!(!hung.retry && !hung.installer); // Missing driver tasks need the installer. - let missing = second_screen_notice(status, VddTaskHealth::TasksMissing).unwrap(); + let missing = + second_screen_notice(status, VddTaskHealth::TasksMissing, None, None).unwrap(); assert!(missing.body.contains("isn't installed"), "{}", missing.body); assert!(missing.retry && missing.installer); } // Without a known driver problem the earlier wording stays. - let task = second_screen_notice(VddStatus::TaskFailed, VddTaskHealth::Ok).unwrap(); + let task = second_screen_notice(VddStatus::TaskFailed, VddTaskHealth::Ok, None, None).unwrap(); assert!(task.body.starts_with("Part of the EternalMonitor install")); assert!(task.installer); - let slow = second_screen_notice(VddStatus::Failed, VddTaskHealth::Ok).unwrap(); - assert!(slow - .body - .starts_with("Windows didn't add the second screen in time")); - assert!(slow.retry && !slow.installer); + for failure in [None, Some("attach_timeout")] { + let slow = + second_screen_notice(VddStatus::Failed, VddTaskHealth::Ok, None, failure).unwrap(); + assert!(slow + .body + .starts_with("Windows didn't add the second screen in time")); + assert!(slow.retry && !slow.installer); + } +} + +/// Review round 3, defect 5: a second screen that attached but showed no +/// picture was said not to have been added in time. +#[test] +fn a_second_screen_without_a_picture_is_not_called_late() { + use crate::control::VddStatus; + use crate::vdd::VddTaskHealth; + for failure in ["no_frames", "duplication_failed"] { + let blank = second_screen_notice(VddStatus::Failed, VddTaskHealth::Ok, None, Some(failure)) + .unwrap(); + assert!( + blank + .body + .starts_with("Windows added the second screen, but EternalMonitor couldn't get a picture from it"), + "{}", + blank.body + ); + assert!(!blank.body.contains("in time"), "{}", blank.body); + assert!(blank.retry && !blank.installer); + } +} + +#[test] +fn the_second_screen_notice_says_what_to_do_about_the_driver_device() { + use crate::control::VddStatus; + use crate::vdd::VddTaskHealth; + use crate::vdd_device::VddProblem; + let notice = |status, problem| { + second_screen_notice(status, VddTaskHealth::Ok, Some(problem), None) + .unwrap_or_else(|| panic!("no notice for {status:?} {problem:?}")) + }; + for status in [VddStatus::Failed, VddStatus::TaskFailed] { + let missing = notice(status, VddProblem::NotInstalled); + assert!(missing.body.contains("isn't installed"), "{}", missing.body); + assert!(missing.body.contains("EternalMonitor-Setup.exe")); + assert!(missing.retry && missing.installer); + + // Code 52 says what Windows blocked, and that Secure Boot can stay on. + let signature = notice(status, VddProblem::SignatureBlocked); + assert!(signature.body.contains("signature"), "{}", signature.body); + assert!(signature.body.contains("code 52")); + assert!(signature + .body + .contains("don't need to turn off Secure Boot")); + assert!(signature.installer); + + let error = notice(status, VddProblem::DriverError(Some(43))); + assert!(error.body.contains("(Windows code 43)"), "{}", error.body); + assert!(error + .body + .contains("Restart the PC, then run EternalMonitor-Setup.exe again")); + assert!(error.installer); + let unstarted = notice(status, VddProblem::DriverError(None)); + assert!(!unstarted.body.contains("code"), "{}", unstarted.body); + + let none = notice(status, VddProblem::NoDisplay); + assert!( + none.body.contains("didn't add the second screen"), + "{}", + none.body + ); + assert!(none.retry && !none.installer); + // The user changes the layout; EternalMonitor never does. + let off_desktop = notice(status, VddProblem::NotExtended); + assert!( + off_desktop.body.contains("kept your desktop off it"), + "{}", + off_desktop.body + ); + assert!(off_desktop + .body + .contains("Windows key + P and choose Extend")); + assert!(off_desktop + .body + .contains("Extend these displays in Settings > System > Display")); + assert!(off_desktop.retry && !off_desktop.installer); + } + // A hung or missing task still says so first: its fix comes before the device's. + let hung = second_screen_notice( + VddStatus::TaskFailed, + VddTaskHealth::NotResponding, + Some(VddProblem::NoDisplay), + None, + ) + .unwrap(); + assert!(hung.body.contains("Restart the PC"), "{}", hung.body); + // Nothing failed: no notice, whatever was last reported. + for status in [VddStatus::Inactive, VddStatus::Active, VddStatus::Attaching] { + assert_eq!( + second_screen_notice(status, VddTaskHealth::Ok, Some(VddProblem::NoDisplay), None), + None + ); + } +} + +#[test] +fn the_resolution_match_says_when_a_change_applies() { + use super::page_settings::vdd_match_description; + for extending in [true, false] { + let text = vdd_match_description(extending); + assert!( + text.contains("applies the next time the second screen starts"), + "{text}" + ); + } + assert!(vdd_match_description(true).contains("when the iPad connects again")); } #[test] diff --git a/host/src/lib.rs b/host/src/lib.rs index b7242d0f..8d4ccafe 100644 --- a/host/src/lib.rs +++ b/host/src/lib.rs @@ -25,7 +25,9 @@ pub mod supervisor; pub mod transport; pub mod updates; pub mod vdd; +pub mod vdd_device; pub mod vdd_settings; +pub mod vdd_snapshot; #[cfg(windows)] pub mod gpu; diff --git a/host/src/main.rs b/host/src/main.rs index cd7826b0..3aa2b462 100644 --- a/host/src/main.rs +++ b/host/src/main.rs @@ -74,8 +74,9 @@ fn main() -> Result<(), Box> { // Never leave a virtual display attached across runs. If a previous run crashed or was // force-killed while streaming the virtual extended display, the VDD device is still enabled // and shows up as a phantom monitor. Disabling it unconditionally at startup guarantees the - // managed display only ever exists while EternalMonitor is actively using it. - vdd::disable(); + // managed display only ever exists while EternalMonitor is actively using it. It runs beside + // startup: a slow or hung task used to hold a Mirror stream's first frame for up to 30 s. + vdd::disable_in_background(); // Belt-and-suspenders: a panic anywhere (GUI, supervisor, pipeline threads) must still tear // the virtual display down before the process unwinds, so a crash can't strand a phantom diff --git a/host/src/transport/mod.rs b/host/src/transport/mod.rs index 32003a8d..c1a24a01 100644 --- a/host/src/transport/mod.rs +++ b/host/src/transport/mod.rs @@ -735,7 +735,7 @@ async fn execute_actions( "The virtual display showed no picture — restarting pipeline to mirror the \ primary display instead" ); - second_screen::send(shared, second_screen::Command::Release); + second_screen::send(shared, second_screen::Command::Fallback(reason)); true } }; diff --git a/host/src/vdd.rs b/host/src/vdd.rs index 0974aa84..838c30b1 100644 --- a/host/src/vdd.rs +++ b/host/src/vdd.rs @@ -59,18 +59,34 @@ enum TaskOutcome { Done, TimedOut, Missing, - Failed, + /// With the device state the Enable task found, when it said. + Failed(Option), } +/// What a failed Enable can tell about the device: the state the task +/// found, or why there is none. +pub(crate) type EnableReport = Result; + #[cfg_attr(not(windows), allow(dead_code))] // Driver tasks exist only on Windows. impl TaskOutcome { + /// For an Enable task that ran and did not succeed. + fn report(self) -> EnableReport { + match self { + Self::Failed(Some(state)) => Ok(state), + Self::Failed(None) => Err("display task failed"), + Self::TimedOut => Err("display task timed out"), + Self::Missing => Err("display tasks missing"), + Self::Done => Err("display task finished"), + } + } + fn from_stderr(stderr: &str) -> Self { if stderr.contains("timed out") { Self::TimedOut } else if stderr.contains("0x80070002") { Self::Missing } else { - Self::Failed + Self::Failed(crate::vdd_device::from_task_stderr(stderr)) } } } @@ -155,7 +171,7 @@ impl TaskGate { self.stuck = Some((now, enabled)); } TaskOutcome::Missing => self.health = VddTaskHealth::TasksMissing, - TaskOutcome::Failed => {} + TaskOutcome::Failed(_) => {} } } } @@ -178,6 +194,10 @@ pub fn recheck_tasks() { struct DriverTasks { serial: parking_lot::Mutex<()>, gate: parking_lot::Mutex, + /// Disables running or waiting for their turn. A pipeline's own Disable + /// does not queue behind one: the display ends up off either way, and + /// a Mirror stream need not wait for it to show its first frame. + disabling: std::sync::atomic::AtomicUsize, } #[cfg_attr(not(windows), allow(dead_code))] // Driver tasks exist only on Windows. @@ -186,6 +206,7 @@ impl DriverTasks { Self { serial: parking_lot::Mutex::new(()), gate: parking_lot::Mutex::new(TaskGate::new()), + disabling: std::sync::atomic::AtomicUsize::new(0), } } @@ -193,19 +214,51 @@ impl DriverTasks { self.gate.lock().health } + #[cfg(test)] fn set_enabled( + &self, + enabled: bool, + purpose: Purpose, + progress: impl FnMut(), + run: impl FnOnce(bool, &mut dyn FnMut()) -> TaskOutcome, + ) -> bool { + self.set_enabled_on_turn(enabled, purpose, progress, || {}, run) + } + + /// As `set_enabled`, calling `on_turn` once this request holds the turn: + /// every request made after that queues behind it. + fn set_enabled_on_turn( &self, enabled: bool, purpose: Purpose, mut progress: impl FnMut(), + on_turn: impl FnOnce(), run: impl FnOnce(bool, &mut dyn FnMut()) -> TaskOutcome, ) -> bool { + use std::sync::atomic::Ordering::SeqCst; + struct Pending<'a>(Option<&'a std::sync::atomic::AtomicUsize>); + impl Drop for Pending<'_> { + fn drop(&mut self) { + if let Some(count) = self.0 { + count.fetch_sub(1, SeqCst); + } + } + } + if !enabled { + if purpose == Purpose::Attempt && self.disabling.load(SeqCst) > 0 { + tracing::debug!("A Disable is already pending; not waiting for it"); + return true; + } + self.disabling.fetch_add(1, SeqCst); + } + let _pending = Pending((!enabled).then_some(&self.disabling)); let _serial = loop { if let Some(guard) = self.serial.try_lock_for(Duration::from_millis(50)) { break guard; } progress(); }; + on_turn(); // The gate guard ends with this statement. Logging below reads the // health through the same mutex, which deadlocked when the guard // lived through the whole match. @@ -234,6 +287,11 @@ pub const TASK_DISABLE: &str = "EternalMonitor VDD Disable"; #[cfg(windows)] fn run_task(task: &str, progress: &mut dyn FnMut()) -> TaskOutcome { use std::os::windows::process::CommandExt; + #[cfg(debug_assertions)] + if let Some(outcome) = simulated_enable_failure(task) { + warn!(task, ?outcome, "Simulating a failed VDD task"); + return outcome; + } let started = Instant::now(); // An Enable that timed out here can still be running in Task Scheduler; // each direction waits for the other first, so the last request wins. @@ -265,7 +323,7 @@ fn run_task(task: &str, progress: &mut dyn FnMut()) -> TaskOutcome { Ok(child) => child, Err(error) => { warn!(task, %error, "Failed to invoke the VDD task"); - return TaskOutcome::Failed; + return TaskOutcome::Failed(None); } }; // vdd_task.ps1 waits up to 10 s for an earlier instance and 15 s for its own. @@ -309,32 +367,90 @@ fn run_task(task: &str, progress: &mut dyn FnMut()) -> TaskOutcome { } Err(error) => { warn!(task, error = %error, "Failed to read the VDD task result"); - TaskOutcome::Failed + TaskOutcome::Failed(None) } } } +/// Debug builds only, to check the failure paths on a PC whose tasks work: +/// `ETERNAL_SIMULATE_ENABLE_FAILURE=missing|timeout|failed` makes the Enable +/// task report that without running. +#[cfg(all(windows, debug_assertions))] +fn simulated_enable_failure(task: &str) -> Option { + if task != TASK_ENABLE { + return None; + } + match std::env::var("ETERNAL_SIMULATE_ENABLE_FAILURE") + .ok()? + .as_str() + { + "missing" => Some(TaskOutcome::Missing), + "timeout" => Some(TaskOutcome::TimedOut), + "failed" => Some(TaskOutcome::Failed(None)), + _ => None, + } +} + /// Complete one installer-owned driver action while capture reports startup progress. #[cfg_attr(not(windows), allow(dead_code))] // Only Windows capture asks. pub(crate) fn set_enabled(enabled: bool, progress: impl FnMut()) -> bool { - request(enabled, Purpose::Attempt, progress) + request(enabled, Purpose::Attempt, progress, &mut None) +} + +/// Why an Enable the gate did not run failed. +#[cfg_attr(not(windows), allow(dead_code))] // Only Windows capture asks. +fn refused(health: VddTaskHealth) -> EnableReport { + Err(match health { + VddTaskHealth::TasksMissing => "display tasks missing", + VddTaskHealth::NotResponding => "display task not responding", + VddTaskHealth::Ok => "display task not run", + }) +} + +/// Turn the virtual display on. On failure, what the Enable task found +/// wrong with the device, or why it could not say. +#[cfg_attr(not(windows), allow(dead_code))] // Only Windows capture asks. +pub(crate) fn enable(progress: impl FnMut()) -> Result<(), EnableReport> { + let mut ran = None; + if request(true, Purpose::Attempt, progress, &mut ran) { + Ok(()) + } else { + Err(ran.map_or_else(|| refused(task_health()), TaskOutcome::report)) + } } -fn request(enabled: bool, purpose: Purpose, progress: impl FnMut()) -> bool { +fn request( + enabled: bool, + purpose: Purpose, + progress: impl FnMut(), + ran: &mut Option, +) -> bool { + request_on_turn(enabled, purpose, progress, || {}, ran) +} + +fn request_on_turn( + enabled: bool, + purpose: Purpose, + progress: impl FnMut(), + on_turn: impl FnOnce(), + ran: &mut Option, +) -> bool { #[cfg(windows)] { - TASKS.set_enabled(enabled, purpose, progress, |enabled, progress| { - let mut outcome = TaskOutcome::Failed; + TASKS.set_enabled_on_turn(enabled, purpose, progress, on_turn, |enabled, progress| { + let mut outcome = TaskOutcome::Failed(None); crate::startup::display::change(enabled, progress, |progress| { outcome = run_task(if enabled { TASK_ENABLE } else { TASK_DISABLE }, progress); outcome == TaskOutcome::Done }); + *ran = Some(outcome); outcome }) } #[cfg(not(windows))] { - let _ = (enabled, purpose, progress); + let _ = (enabled, purpose, progress, ran); + on_turn(); false } } @@ -344,7 +460,31 @@ fn request(enabled: bool, purpose: Purpose, progress: impl FnMut()) -> bool { /// it, and it waits for an Enable still running, within the task's bounds. /// True when the display is known to be off. pub fn disable() -> bool { - request(false, Purpose::Cleanup, || {}) + request(false, Purpose::Cleanup, || {}, &mut None) +} + +/// `disable` at startup, without holding up the first frame: the Disable +/// runs on its own thread, and this returns once it holds the turn, so +/// every later request (an Enable, the quit's Disable) still runs after it. +/// A hung task no longer keeps a Mirror stream black for up to 30 s. +pub fn disable_in_background() { + let (held, turn) = std::sync::mpsc::channel(); + let spawned = std::thread::Builder::new() + .name("vdd-startup-off".into()) + .spawn(move || { + let on_turn = move || { + let _ = held.send(()); + }; + request_on_turn(false, Purpose::Cleanup, || {}, on_turn, &mut None) + }); + match spawned { + Ok(_) => { + let _ = turn.recv(); + } + Err(_) => { + disable(); + } + } } /// Disable the virtual display after `host` ended abnormally, if it owned the @@ -493,7 +633,7 @@ mod tests { let mut gate = TaskGate::new(); let now = Instant::now(); gate.after(false, TaskOutcome::Done, now); - gate.after(true, TaskOutcome::Failed, now); + gate.after(true, TaskOutcome::Failed(None), now); // A failed task leaves the device state unknown, so disable runs again. assert_eq!(gate.before(false, Purpose::Attempt, now), Gate::Run); gate.after(false, TaskOutcome::Missing, now); @@ -554,7 +694,7 @@ mod tests { // A failed Disable leaves the state unknown, so quit tries again. let mut gate = TaskGate::new(); gate.after(true, TaskOutcome::Done, now); - gate.after(false, TaskOutcome::Failed, now); + gate.after(false, TaskOutcome::Failed(None), now); assert_eq!(gate.before(false, Purpose::Cleanup, now), Gate::Run); } @@ -709,6 +849,113 @@ mod tests { assert_eq!(tasks.gate.lock().known_enabled, Some(false)); } + #[test] + fn enable_failures_without_a_device_state_say_why() { + use crate::vdd_device::DeviceState; + assert_eq!( + TaskOutcome::Failed(Some(DeviceState::Problem(43))).report(), + Ok(DeviceState::Problem(43)) + ); + assert_eq!( + TaskOutcome::Failed(None).report(), + Err("display task failed") + ); + assert_eq!( + TaskOutcome::TimedOut.report(), + Err("display task timed out") + ); + assert_eq!(TaskOutcome::Missing.report(), Err("display tasks missing")); + // The gate refused to run the task at all. + assert_eq!( + refused(VddTaskHealth::TasksMissing), + Err("display tasks missing") + ); + assert_eq!( + refused(VddTaskHealth::NotResponding), + Err("display task not responding") + ); + assert_eq!(refused(VddTaskHealth::Ok), Err("display task not run")); + } + + /// Runs `set_enabled` on a thread; `None` if it has not returned in 2 s. + fn returns_within( + tasks: &std::sync::Arc, + enabled: bool, + purpose: Purpose, + runs: std::sync::Arc>>, + ) -> Option { + let (done_tx, done_rx) = std::sync::mpsc::channel(); + let tasks = std::sync::Arc::clone(tasks); + std::thread::spawn(move || { + let ran = tasks.set_enabled( + enabled, + purpose, + || {}, + |e, _| { + runs.lock().unwrap().push(e); + TaskOutcome::Done + }, + ); + let _ = done_tx.send(ran); + }); + done_rx.recv_timeout(Duration::from_secs(2)).ok() + } + + #[test] + fn a_mirror_start_does_not_wait_behind_the_startup_disable() { + use std::sync::mpsc::channel; + use std::sync::{Arc, Mutex}; + let tasks = Arc::new(DriverTasks::new()); + let runs = Arc::new(Mutex::new(Vec::new())); + // The startup Disable holds the turn: its task is hung. + let (started_tx, started_rx) = channel(); + let (release_tx, release_rx) = channel::<()>(); + let startup = { + let tasks = Arc::clone(&tasks); + std::thread::spawn(move || { + tasks.set_enabled( + false, + Purpose::Cleanup, + || {}, + |_, _| { + started_tx.send(()).unwrap(); + release_rx.recv().unwrap(); + TaskOutcome::Done + }, + ) + }) + }; + started_rx.recv().unwrap(); + // The Mirror pipeline's Disable returns at once and runs nothing. + assert_eq!( + returns_within(&tasks, false, Purpose::Attempt, Arc::clone(&runs)), + Some(true), + "the pipeline waited behind the startup Disable" + ); + // An Enable still waits its turn, then runs after the Disable. + let (enable_tx, enable_rx) = channel(); + { + let (tasks, runs) = (Arc::clone(&tasks), Arc::clone(&runs)); + std::thread::spawn(move || { + let _ = enable_tx.send(returns_within(&tasks, true, Purpose::Attempt, runs)); + }); + } + assert!( + enable_rx.recv_timeout(Duration::from_millis(300)).is_err(), + "the Enable did not wait" + ); + release_tx.send(()).unwrap(); + assert!(startup.join().unwrap()); + assert_eq!(enable_rx.recv().unwrap(), Some(true)); + assert_eq!(*runs.lock().unwrap(), vec![true]); + // With nothing pending, a pipeline Disable runs again. + assert_eq!( + returns_within(&tasks, false, Purpose::Attempt, Arc::clone(&runs)), + Some(true) + ); + assert_eq!(*runs.lock().unwrap(), vec![true, false]); + } + #[test] fn task_errors_are_classified_from_the_script_output() { assert_eq!( @@ -723,7 +970,12 @@ mod tests { ); assert_eq!( TaskOutcome::from_stderr("VDD task failed: x, result=1"), - TaskOutcome::Failed + TaskOutcome::Failed(None) + ); + // The Enable task says what it found wrong with the device. + assert_eq!( + TaskOutcome::from_stderr("VDD task failed: EternalMonitor VDD Enable, result=5"), + TaskOutcome::Failed(Some(crate::vdd_device::DeviceState::NoMonitor)) ); } } diff --git a/host/src/vdd_device.rs b/host/src/vdd_device.rs new file mode 100644 index 00000000..75c207da --- /dev/null +++ b/host/src/vdd_device.rs @@ -0,0 +1,445 @@ +//! What Windows reports for the bundled virtual display driver's device, and +//! what the Stream page tells the user to do about it. +//! +//! The Enable task used to succeed whenever the device reported no problem +//! code right after it was enabled. The display could still never appear, and +//! all the host could say was that it did not attach in time. The task now +//! says why it failed (installer/scripts/vdd-toggle.ps1), and the host reads +//! the device itself when the display does not appear. + +use std::fmt; + +/// cfgmgr32.h devnode status flags. +#[cfg_attr(not(windows), allow(dead_code))] // Read from Windows only. +const DN_STARTED: u32 = 0x8; +#[cfg_attr(not(windows), allow(dead_code))] // Read from Windows only. +const DN_HAS_PROBLEM: u32 = 0x400; + +/// The driver's hardware ID, as its INF and vdd-toggle.ps1 match it. +#[cfg_attr(not(windows), allow(dead_code))] // Read from Windows only. +const HARDWARE_ID: &str = r"Root\MttVDD"; + +/// The driver's device, as the Enable task or Configuration Manager reports it. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DeviceState { + /// No present device has the driver's hardware ID. + Missing, + /// The device exists without a driver bound to it. + NoDriver, + /// Windows reports this problem code. 22 is "disabled". + Problem(u32), + /// No problem code, but Windows has not started the device. + NotStarted, + /// The device started, but its monitor did not arrive. + NoMonitor, + /// The device and its monitor are running. + Ready, +} + +/// What the user can do about a second screen that did not appear. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum VddProblem { + /// The driver or its device is gone: run Setup again. + NotInstalled, + /// Windows could not verify the driver's signature (code 52). + SignatureBlocked, + /// The driver failed, with Windows' problem code when there is one. + DriverError(Option), + /// The driver runs, but Windows added no screen for it. + NoDisplay, + /// Windows added the screen but kept the desktop off it. + NotExtended, +} + +impl DeviceState { + /// The Enable task's result, as vdd-toggle.ps1 exits. + pub fn from_task_result(code: i64) -> Option { + Some(match code { + 2 => Self::Missing, + 3 => Self::NoDriver, + 4 => Self::NotStarted, + 5 => Self::NoMonitor, + 101..=199 => Self::Problem((code - 100) as u32), + _ => return None, + }) + } + + /// The device as Configuration Manager reports it: its status flags and + /// problem number, whether a driver is bound, and whether its monitor + /// child is started (`None` without a child). + #[cfg_attr(not(windows), allow(dead_code))] // Read from Windows only. + fn from_devnode(status: u32, problem: u32, has_driver: bool, monitor: Option) -> Self { + if status & DN_HAS_PROBLEM != 0 { + Self::Problem(problem) + } else if !has_driver { + Self::NoDriver + } else if status & DN_STARTED == 0 { + Self::NotStarted + } else if monitor == Some(true) { + Self::Ready + } else { + Self::NoMonitor + } + } + + /// With more than one device, the one closest to showing a display. + #[cfg_attr(not(windows), allow(dead_code))] // Read from Windows only. + fn best(states: impl IntoIterator) -> Self { + let rank = |state: &Self| match state { + Self::Missing => 0, + Self::NoDriver => 1, + Self::Problem(_) => 2, + Self::NotStarted => 3, + Self::NoMonitor => 4, + Self::Ready => 5, + }; + states.into_iter().max_by_key(rank).unwrap_or(Self::Missing) + } + + /// What to tell the user once the display did not appear. A device that + /// is ready with its monitor means Windows kept the desktop off it. + pub fn problem(self) -> VddProblem { + match self { + // Not configured, reinstall needed, or the driver install failed. + Self::Missing | Self::NoDriver | Self::Problem(1 | 18 | 28) => VddProblem::NotInstalled, + Self::Problem(52) => VddProblem::SignatureBlocked, + Self::Problem(code) => VddProblem::DriverError(Some(code)), + Self::NotStarted => VddProblem::DriverError(None), + Self::NoMonitor => VddProblem::NoDisplay, + Self::Ready => VddProblem::NotExtended, + } + } +} + +/// Stable names for logs and diagnostics. No instance IDs or other detail. +impl fmt::Display for DeviceState { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Missing => f.write_str("missing"), + Self::NoDriver => f.write_str("no_driver"), + Self::Problem(code) => write!(f, "problem_{code}"), + Self::NotStarted => f.write_str("not_started"), + Self::NoMonitor => f.write_str("no_monitor"), + Self::Ready => f.write_str("ready"), + } + } +} + +/// The Enable task's result from vdd_task.ps1's failure message, +/// "VDD task failed: , result=". +pub fn from_task_stderr(stderr: &str) -> Option { + let digits: String = stderr + .split("result=") + .nth(1)? + .chars() + .take_while(char::is_ascii_digit) + .collect(); + DeviceState::from_task_result(digits.parse().ok()?) +} + +/// Read the driver's device from Configuration Manager. +#[cfg(windows)] +pub fn query() -> DeviceState { + use windows::core::{w, PCWSTR}; + use windows::Win32::Devices::DeviceAndDriverInstallation::{ + CM_Get_Child, CM_Get_DevNode_Registry_PropertyW, CM_Get_DevNode_Status, + CM_Get_Device_ID_ListW, CM_Get_Device_ID_List_SizeW, CM_Locate_DevNodeW, + CM_DEVNODE_STATUS_FLAGS, CM_DRP_DRIVER, CM_DRP_HARDWAREID, CM_GETIDLIST_FILTER_ENUMERATOR, + CM_GETIDLIST_FILTER_PRESENT, CM_LOCATE_DEVNODE_NORMAL, CM_PROB, CR_BUFFER_SMALL, + CR_SUCCESS, + }; + + let status = |devinst: u32| { + let (mut status, mut problem) = (CM_DEVNODE_STATUS_FLAGS(0), CM_PROB(0)); + (unsafe { CM_Get_DevNode_Status(&mut status, &mut problem, devinst, 0) } == CR_SUCCESS) + .then_some((status.0, problem.0)) + }; + let property = |devinst: u32, which: u32| -> Option> { + let mut bytes = 0u32; + let result = + unsafe { CM_Get_DevNode_Registry_PropertyW(devinst, which, None, None, &mut bytes, 0) }; + if result != CR_BUFFER_SMALL || bytes == 0 { + return None; + } + let mut buffer = vec![0u16; bytes.div_ceil(2) as usize]; + (unsafe { + CM_Get_DevNode_Registry_PropertyW( + devinst, + which, + None, + Some(buffer.as_mut_ptr().cast()), + &mut bytes, + 0, + ) + } == CR_SUCCESS) + .then_some(buffer) + }; + + let flags = CM_GETIDLIST_FILTER_ENUMERATOR | CM_GETIDLIST_FILTER_PRESENT; + let mut len = 0u32; + if unsafe { CM_Get_Device_ID_List_SizeW(&mut len, w!("ROOT"), flags) } != CR_SUCCESS { + return DeviceState::Missing; + } + let mut ids = vec![0u16; len as usize]; + if unsafe { CM_Get_Device_ID_ListW(w!("ROOT"), &mut ids, flags) } != CR_SUCCESS { + return DeviceState::Missing; + } + let states = ids + .split(|&unit| unit == 0) + .filter(|id| !id.is_empty()) + .filter_map(|id| { + let id: Vec = id.iter().copied().chain(Some(0)).collect(); + let mut devinst = 0u32; + if unsafe { + CM_Locate_DevNodeW(&mut devinst, PCWSTR(id.as_ptr()), CM_LOCATE_DEVNODE_NORMAL) + } != CR_SUCCESS + { + return None; + } + let hardware_ids = property(devinst, CM_DRP_HARDWAREID)?; + hardware_ids + .split(|&unit| unit == 0) + .any(|hwid| String::from_utf16_lossy(hwid).eq_ignore_ascii_case(HARDWARE_ID)) + .then_some(devinst) + }) + .filter_map(|devinst| { + let (flags, problem) = status(devinst)?; + let mut child = 0u32; + let monitor = + (unsafe { CM_Get_Child(&mut child, devinst, 0) } == CR_SUCCESS).then(|| { + status(child).is_some_and(|(flags, _)| { + flags & DN_STARTED != 0 && flags & DN_HAS_PROBLEM == 0 + }) + }); + let has_driver = property(devinst, CM_DRP_DRIVER).is_some(); + Some(DeviceState::from_devnode( + flags, problem, has_driver, monitor, + )) + }); + DeviceState::best(states) +} + +/// Set the mode of the virtual display `device` (a GDI name like +/// `\\.\DISPLAY5`), and save it for this set of displays. Only that +/// display changes. Tried with the refresh rate first, then without it. +#[cfg(windows)] +pub fn set_display_mode(device: &str, mode: crate::vdd_settings::VddMode) -> bool { + use windows::core::PCWSTR; + use windows::Win32::Graphics::Gdi::{ + ChangeDisplaySettingsExW, CDS_UPDATEREGISTRY, DEVMODEW, DISP_CHANGE_SUCCESSFUL, + DM_DISPLAYFREQUENCY, DM_PELSHEIGHT, DM_PELSWIDTH, + }; + let name: Vec = device.encode_utf16().chain(Some(0)).collect(); + [ + DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY, + DM_PELSWIDTH | DM_PELSHEIGHT, + ] + .into_iter() + .any(|fields| { + let devmode = DEVMODEW { + dmSize: std::mem::size_of::() as u16, + dmFields: fields, + dmPelsWidth: mode.width, + dmPelsHeight: mode.height, + dmDisplayFrequency: mode.refresh_hz, + ..Default::default() + }; + (unsafe { + ChangeDisplaySettingsExW( + PCWSTR(name.as_ptr()), + Some(&devmode), + None, + CDS_UPDATEREGISTRY, + None, + ) + }) == DISP_CHANGE_SUCCESSFUL + }) +} + +/// The sizes Windows offers for the display `device`. +#[cfg(windows)] +pub fn offered_modes(device: &str) -> Vec<(u32, u32)> { + use windows::core::PCWSTR; + use windows::Win32::Graphics::Gdi::{ + EnumDisplaySettingsW, DEVMODEW, ENUM_DISPLAY_SETTINGS_MODE, + }; + let name: Vec = device.encode_utf16().chain(Some(0)).collect(); + let mut sizes = Vec::new(); + for index in 0..1024 { + let mut devmode = DEVMODEW { + dmSize: std::mem::size_of::() as u16, + ..Default::default() + }; + if !unsafe { + EnumDisplaySettingsW( + PCWSTR(name.as_ptr()), + ENUM_DISPLAY_SETTINGS_MODE(index), + &mut devmode, + ) + } + .as_bool() + { + break; + } + let size = (devmode.dmPelsWidth, devmode.dmPelsHeight); + if !sizes.contains(&size) { + sizes.push(size); + } + } + sizes +} + +#[cfg(not(windows))] +pub fn query() -> DeviceState { + DeviceState::Missing +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn the_enable_task_result_names_the_device_state() { + assert_eq!(DeviceState::from_task_result(0), None); + assert_eq!(DeviceState::from_task_result(1), None); + assert_eq!(DeviceState::from_task_result(2), Some(DeviceState::Missing)); + assert_eq!( + DeviceState::from_task_result(3), + Some(DeviceState::NoDriver) + ); + assert_eq!( + DeviceState::from_task_result(4), + Some(DeviceState::NotStarted) + ); + assert_eq!( + DeviceState::from_task_result(5), + Some(DeviceState::NoMonitor) + ); + assert_eq!( + DeviceState::from_task_result(122), + Some(DeviceState::Problem(22)) + ); + assert_eq!( + DeviceState::from_task_result(143), + Some(DeviceState::Problem(43)) + ); + // Task Scheduler's own codes are not device states. + assert_eq!(DeviceState::from_task_result(0x41301), None); + assert_eq!( + from_task_stderr("VDD task failed: EternalMonitor VDD Enable, result=152"), + Some(DeviceState::Problem(52)) + ); + assert_eq!( + from_task_stderr("VDD task failed: EternalMonitor VDD Enable, result=1"), + None + ); + assert_eq!(from_task_stderr("Previous VDD task timed out: x"), None); + } + + #[test] + fn devnode_status_maps_to_a_device_state() { + let started = DN_STARTED; + let problem = DN_HAS_PROBLEM; + assert_eq!( + DeviceState::from_devnode(problem, 22, true, None), + DeviceState::Problem(22) + ); + assert_eq!( + DeviceState::from_devnode(problem, 43, true, None), + DeviceState::Problem(43) + ); + assert_eq!( + DeviceState::from_devnode(problem, 28, false, None), + DeviceState::Problem(28) + ); + assert_eq!( + DeviceState::from_devnode(started, 0, false, None), + DeviceState::NoDriver + ); + assert_eq!( + DeviceState::from_devnode(0, 0, true, None), + DeviceState::NotStarted + ); + assert_eq!( + DeviceState::from_devnode(started, 0, true, None), + DeviceState::NoMonitor + ); + assert_eq!( + DeviceState::from_devnode(started, 0, true, Some(false)), + DeviceState::NoMonitor + ); + assert_eq!( + DeviceState::from_devnode(started, 0, true, Some(true)), + DeviceState::Ready + ); + } + + #[test] + fn the_device_closest_to_a_display_wins() { + assert_eq!(DeviceState::best([]), DeviceState::Missing); + assert_eq!( + DeviceState::best([DeviceState::Problem(43), DeviceState::Ready]), + DeviceState::Ready + ); + assert_eq!( + DeviceState::best([DeviceState::NoDriver, DeviceState::Problem(22)]), + DeviceState::Problem(22) + ); + } + + #[test] + fn each_device_state_says_what_to_do() { + use VddProblem::*; + for state in [ + DeviceState::Missing, + DeviceState::NoDriver, + DeviceState::Problem(1), + DeviceState::Problem(18), + DeviceState::Problem(28), + ] { + assert_eq!(state.problem(), NotInstalled, "{state}"); + } + assert_eq!(DeviceState::Problem(52).problem(), SignatureBlocked); + for code in [10, 22, 31, 39, 43] { + assert_eq!( + DeviceState::Problem(code).problem(), + DriverError(Some(code)) + ); + } + assert_eq!(DeviceState::NotStarted.problem(), DriverError(None)); + assert_eq!(DeviceState::NoMonitor.problem(), NoDisplay); + assert_eq!(DeviceState::Ready.problem(), NotExtended); + } + + /// Reads this PC's driver device through Configuration Manager: + /// `EM_EXPECT_VDD=problem_22 cargo test -p eternal-host --lib native_device_state -- --ignored` + #[cfg(windows)] + #[test] + #[ignore = "reads the virtual display device of this PC"] + fn native_device_state() { + let state = query(); + println!("vdd_device={state} problem={:?}", state.problem()); + // DXGI lists a virtual display under the adapter that renders it. + for output in crate::capture::enumerate_outputs() { + println!( + "output {} {}x{} at {},{} on {}", + output.device_name, + output.width, + output.height, + output.left, + output.top, + output.adapter_name + ); + } + if let Ok(expected) = std::env::var("EM_EXPECT_VDD") { + assert_eq!(state.to_string(), expected); + } + } + + #[test] + fn log_names_carry_no_instance_details() { + assert_eq!(DeviceState::Problem(43).to_string(), "problem_43"); + assert_eq!(DeviceState::NoMonitor.to_string(), "no_monitor"); + assert_eq!(DeviceState::Missing.to_string(), "missing"); + } +} diff --git a/host/src/vdd_settings.rs b/host/src/vdd_settings.rs index 15aa32cf..3eb68fae 100644 --- a/host/src/vdd_settings.rs +++ b/host/src/vdd_settings.rs @@ -7,8 +7,14 @@ //! document: one monitor, a global refresh rate, and a resolution list led by //! the client's native landscape mode. NEEDS_WINDOWS_VERIFY: the driver's //! acceptance of these exact modes is a hardware-runbook item. +//! +//! It also names the graphics card the driver renders on: the one driving the +//! main screen. Left unnamed, the driver picks the card with the most video +//! memory. On Intel + NVIDIA laptops that is the NVIDIA card, which drives no +//! screen, and in 0.3.1 diagnostics none of those laptops got the display. use std::io; +use std::ops::Range; use std::path::Path; /// A display mode for the virtual display's mode list. @@ -19,6 +25,12 @@ pub struct VddMode { pub refresh_hz: u32, } +impl std::fmt::Display for VddMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}x{}@{}", self.width, self.height, self.refresh_hz) + } +} + /// The modes to advertise for a client with the given native panel, best /// first. The iPad reports portrait-native pixels; the desktop wants /// landscape, so the long edge becomes the width. Odd dimensions are rounded @@ -58,13 +70,47 @@ pub fn modes_for_client(screen_px_w: u16, screen_px_h: u16, refresh_hz: u8) -> V modes } +/// The modes to offer when the iPad's screen size is not known yet. +pub fn default_modes() -> Vec { + vec![VddMode { + width: 1920, + height: 1080, + refresh_hz: 60, + }] +} + +/// The mode to set on the virtual display after it attached, when Windows +/// picked another one. The driver offers the iPad's mode first, but Windows +/// restores the mode it last saved for these displays: 800x600 when the +/// driver's own settings were in place the first time, which is how the +/// vendor installer leaves them. +pub fn mode_to_set(requested: Option, attached: (u32, u32)) -> Option { + requested.filter(|mode| (mode.width, mode.height) != attached) +} + +/// When the requested mode cannot be set (the driver offers only its own +/// list), the largest offered size that fits inside it, so the screen is +/// as sharp as it can be instead of staying at 800x600. +pub fn best_offered(requested: VddMode, offered: &[(u32, u32)]) -> Option<(u32, u32)> { + offered + .iter() + .copied() + .filter(|&(w, h)| w <= requested.width && h <= requested.height) + .max_by_key(|&(w, h)| u64::from(w) * u64::from(h)) +} + /// Render the settings document. Pure so tests can assert the exact bytes. -pub fn render(modes: &[VddMode]) -> String { +/// `gpu` is the DXGI adapter description the driver matches its render +/// adapter by; without it the driver chooses. +pub fn render(modes: &[VddMode], gpu: Option<&str>) -> String { let global_refresh = modes.first().map_or(60, |m| m.refresh_hz); let mut xml = String::with_capacity(512); xml.push_str("\n"); xml.push_str("\n"); xml.push_str(" \n 1\n \n"); + if let Some(gpu) = gpu { + xml.push_str(&gpu_element(gpu)); + } xml.push_str(" \n"); xml.push_str(&format!( " {global_refresh}\n" @@ -84,15 +130,554 @@ pub fn render(modes: &[VddMode]) -> String { xml } +fn escape(text: &str) -> String { + text.replace('&', "&") + .replace('<', "<") + .replace('>', ">") +} + +fn gpu_element(gpu: &str) -> String { + format!( + " \n {}\n \n", + escape(gpu) + ) +} + +/// The graphics card to name for the driver: the one driving the main +/// screen. `adapters` are in DXGI order, as the driver enumerates them. The +/// driver matches by description and takes the first match, so a name +/// several adapters share is pinned only when the first of them is the one +/// with screens and the others have none: Windows lists some laptops' +/// integrated card twice. Otherwise the error says why for the log. +pub fn gpu_to_pin<'a>( + main_screen: Option<&'a str>, + adapters: &[crate::vdd_snapshot::Adapter], +) -> Result<&'a str, &'static str> { + let name = main_screen + .filter(|name| !name.is_empty()) + .ok_or("no adapter drives the main screen")?; + let mut named = adapters + .iter() + .filter(|adapter| same_card(&adapter.description, name)); + if let Some(first) = named.next() { + let mut rest = named.peekable(); + if rest.peek().is_some() && (first.outputs == 0 || rest.any(|a| a.outputs > 0)) { + return Err("two graphics cards have the same name"); + } + } + Ok(name) +} + +/// Where a parsed settings document names its render adapter, and how to +/// make it name another one. +enum GpuSlot { + /// The text of the one ``, as a byte range. + Name(Range), + /// A self-closing `` or ``, to be written out. + Empty(Range, &'static str), + /// Right after the start tag of `` without a name, or of the root. + Insert(usize, &'static str), +} + +/// A character XML 1.0 allows in a document. +fn xml_char(c: char) -> bool { + matches!(c, '\t' | '\n' | '\r') || (c >= ' ' && !matches!(c, '\u{fffe}' | '\u{ffff}')) +} + +/// An XML name, loosely: what the driver's parser and Expat accept. +fn xml_name(name: &[u8]) -> bool { + let Ok(name) = std::str::from_utf8(name) else { + return false; + }; + let mut chars = name.chars(); + chars + .next() + .is_some_and(|c| c.is_alphabetic() || c == '_' || c == ':') + && chars.all(|c| c.is_alphanumeric() || matches!(c, '_' | ':' | '-' | '.')) +} + +/// Elements carry no attributes: neither this host nor the driver's own +/// file writes any, so a file with one is not a file this host edits. +fn check_tag(tag: &quick_xml::events::BytesStart) -> Result<(), &'static str> { + if !xml_name(tag.name().as_ref()) { + return Err("an element name is not valid XML"); + } + if !tag.attributes_raw().iter().all(u8::is_ascii_whitespace) { + return Err("an element has attributes, which this file never has"); + } + Ok(()) +} + +/// The declaration this host writes or the driver ships: +/// ``, +/// with either quote. +fn xml_declaration(raw: &str) -> bool { + let attribute = + |name: &str, value: &str| [format!(" {name}=\"{value}\""), format!(" {name}='{value}'")]; + let optional = |name: &str, values: &[&str]| { + std::iter::once(String::new()) + .chain(values.iter().flat_map(|value| attribute(name, value))) + .collect::>() + }; + let encodings = optional("encoding", &["utf-8", "UTF-8"]); + let standalones = optional("standalone", &["yes", "no"]); + attribute("version", "1.0").iter().any(|version| { + encodings.iter().any(|encoding| { + standalones + .iter() + .any(|standalone| raw == format!("")) + }) + }) +} + +/// Find the render adapter's place by parsing `xml`. Only the shapes this +/// host writes and the driver ships are edited: the declaration above at +/// the start, comments, whitespace, elements without attributes, and text +/// with only predefined entities and legal character references. Anything +/// else is refused, including a DOCTYPE, processing instructions, CDATA and +/// characters XML does not allow; so is anything the driver could read +/// differently from this host: a name outside ``, a second name or +/// ``, markup inside the name, or text or elements outside the +/// `` root. The driver matches `friendlyname` by local name +/// anywhere. +fn gpu_slot(xml: &str) -> Result { + use quick_xml::events::Event; + if !xml.chars().all(xml_char) { + return Err("the file has characters XML does not allow"); + } + let mut reader = quick_xml::Reader::from_str(xml); + reader.config_mut().trim_text(false); + let mut open: Vec> = Vec::new(); + let (mut root, mut closed, mut slot) = (None, false, None); + let (mut gpus, mut names, mut name_start) = (0, 0, 0); + loop { + let before = reader.buffer_position() as usize; + let event = reader + .read_event() + .map_err(|_| "the file is not well-formed XML")?; + let after = reader.buffer_position() as usize; + let in_name = open.last().is_some_and(|name| name == b"friendlyname"); + let outside = open.is_empty(); + match event { + Event::Start(_) | Event::Empty(_) if closed => { + return Err("there is markup after the root element") + } + Event::Start(_) | Event::Empty(_) if in_name => { + return Err("the graphics card name contains markup") + } + Event::Start(ref tag) | Event::Empty(ref tag) => { + check_tag(tag)?; + let empty = matches!(event, Event::Empty(_)); + let name = tag.local_name().as_ref().to_vec(); + match (open.len(), name.as_slice()) { + (0, b"vdd_settings") if !empty => root = Some(after), + (0, _) => return Err("the root element is not "), + (1, b"gpu") => { + gpus += 1; + slot = Some(if empty { + GpuSlot::Empty(before..after, "gpu") + } else { + GpuSlot::Insert(after, "gpu") + }); + } + (2, b"friendlyname") if open[1] == b"gpu" => { + names += 1; + name_start = after; + if empty { + slot = Some(GpuSlot::Empty(before..after, "friendlyname")); + } + } + (_, b"friendlyname") => return Err("a graphics card name is outside "), + (_, b"gpu") => return Err("a element is not directly in the root"), + _ => {} + } + if !empty { + open.push(name); + } + } + Event::End(_) => { + if in_name { + slot = Some(GpuSlot::Name(name_start..before)); + } + open.pop(); + closed = open.is_empty(); + } + Event::Text(text) if outside && !text.iter().all(u8::is_ascii_whitespace) => { + return Err("there is text outside the root element") + } + Event::GeneralRef(_) if outside => { + return Err("there is text outside the root element") + } + // Only the very first thing in the file may be the declaration. + Event::Decl(_) if before != 0 || !xml_declaration(&xml[before..after]) => { + return Err("the XML declaration is not one this file has") + } + Event::Decl(_) => {} + Event::GeneralRef(ref reference) => { + let known = if reference.is_char_ref() { + matches!(reference.resolve_char_ref(), Ok(Some(c)) if xml_char(c)) + } else { + matches!( + reference.as_ref(), + b"lt" | b"gt" | b"amp" | b"apos" | b"quot" + ) + }; + if !known { + return Err("the file uses an entity XML does not define"); + } + } + Event::Text(ref text) if text.windows(3).any(|w| w == b"]]>") => { + return Err("the file is not well-formed XML") + } + Event::Comment(ref text) + if text.windows(2).any(|w| w == b"--") || text.ends_with(b"-") => + { + return Err("a comment is not well-formed XML") + } + Event::Text(_) | Event::Comment(_) => {} + Event::Eof => break, + // A DOCTYPE, processing instructions, CDATA, or anything newer. + _ => return Err("the file uses XML this host does not edit"), + } + } + let root = root + .filter(|_| closed) + .ok_or("there is no complete element")?; + if gpus > 1 || names > 1 { + return Err("it names more than one graphics card"); + } + Ok(slot.unwrap_or(GpuSlot::Insert(root, "vdd_settings"))) +} + +/// `xml` naming `gpu` as the render adapter, everything else byte for byte +/// as it was. The edit is checked by parsing the result. `Err` says why the +/// document was left alone. +fn gpu_edit(xml: &str, gpu: &str) -> Result { + let (bom, body) = match xml.strip_prefix('\u{feff}') { + Some(body) => ("\u{feff}", body), + None => ("", xml), + }; + let nl = if body.contains("\r\n") { "\r\n" } else { "\n" }; + let name = escape(gpu); + let element = format!("{name}"); + let (range, text) = match gpu_slot(body)? { + GpuSlot::Name(range) => (range, name.clone()), + GpuSlot::Empty(range, "gpu") => (range, format!("{element}")), + GpuSlot::Empty(range, _) => (range, element), + GpuSlot::Insert(at, "gpu") => (at..at, format!("{nl} {element}")), + GpuSlot::Insert(at, _) => ( + at..at, + format!("{nl} {nl} {element}{nl} "), + ), + }; + let edited = format!("{}{text}{}", &body[..range.start], &body[range.end..]); + match gpu_slot(&edited) { + Ok(GpuSlot::Name(range)) if edited[range.clone()] == name => Ok(format!("{bom}{edited}")), + _ => Err("the edited file did not parse as intended"), + } +} + +/// `xml` naming `gpu` as the render adapter, everything else as it was; +/// `None` when the document is not one this can edit safely. +pub fn with_gpu(xml: &str, gpu: &str) -> Option { + gpu_edit(xml, gpu).ok() +} + +/// Point the settings file the host does not otherwise write at `gpu`. +/// True when it changed. A missing file stays missing: without one the +/// driver offers its built-in modes, and a document holding only a GPU +/// would leave it none. A file that does not parse as expected is left +/// alone, and the error says why. +pub fn pin_gpu_in(path: &Path, gpu: &str) -> io::Result { + let current = std::fs::read_to_string(path)?; + let updated = gpu_edit(¤t, gpu) + .map_err(|reason| io::Error::new(io::ErrorKind::InvalidData, reason))?; + if updated == current { + return Ok(false); + } + replace(path, &updated)?; + Ok(true) +} + +/// Whether two graphics card names are one card to the driver, which +/// compares them ignoring case. +pub fn same_card(a: &str, b: &str) -> bool { + a.to_lowercase() == b.to_lowercase() +} + +/// A name that leaves the card to the driver: none, empty or `default`. +fn driver_chooses(name: &str) -> bool { + let name = name.trim(); + name.is_empty() || name.eq_ignore_ascii_case("default") +} + +/// The graphics card the document names, `None` when it leaves the card to +/// the driver. `Err` when the document is not one this host edits. +pub fn named_gpu(xml: &str) -> Result, &'static str> { + let body = xml.strip_prefix('\u{feff}').unwrap_or(xml); + match gpu_slot(body)? { + GpuSlot::Name(range) => { + let text = quick_xml::escape::unescape(&body[range]) + .map_err(|_| "the graphics card name is not valid XML text")?; + Ok((!driver_chooses(&text)).then(|| text.trim().to_string())) + } + _ => Ok(None), + } +} + +/// What to do to the settings file's graphics card name. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PinChange { + /// Name this card. + Write(String), + /// Take back this host's earlier name: the driver chooses again. + Reset, + /// Nothing to change. + Leave, + /// Another app or the user named a card; it stays, and the driver + /// renders there. + Refuse(&'static str), +} + +/// Why a card someone else named stays. +pub const NAMED_ELSEWHERE: &str = "the settings file names a card this host did not"; + +/// Whose name the file may hold: this host replaces a name only when it +/// installed the driver (`owned`), when the file leaves the card to the +/// driver, or when the name is one it recorded writing (`ours`). Other apps +/// share this driver and its file. With no card to name (`wanted` is +/// `None`), this host's own earlier name is taken back, so the file never +/// keeps a pin the log says was not made. +pub fn pin_change( + named: Option<&str>, + wanted: Option<&str>, + owned: bool, + ours: &[&str], +) -> PinChange { + let is_ours = |name: &str| ours.iter().any(|ours| same_card(ours, name)); + match (wanted, named) { + (Some(card), Some(name)) if same_card(card, name) => PinChange::Leave, + (Some(_), Some(name)) if !owned && !is_ours(name) => PinChange::Refuse(NAMED_ELSEWHERE), + (Some(card), _) => PinChange::Write(card.to_string()), + (None, Some(name)) if is_ours(name) => PinChange::Reset, + (None, _) => PinChange::Leave, + } +} + +/// What `apply_pin` did. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PinOutcome { + Named, + Unchanged, + Reset, + Kept(&'static str), +} + +/// Whether Setup installed the virtual display driver beside the app in +/// `app_dir`, and it is still that install. Setup records the driver's +/// uninstaller and version in this marker (installer/EternalMonitor.iss); +/// another app that replaced or upgraded the driver since changed its +/// registration, and the marker no longer counts, as in +/// installer/scripts/vdd-driver-remove.ps1. +pub fn driver_owned(app_dir: &Path) -> bool { + let marker = app_dir + .join("driver") + .join("installed-by-eternalmonitor.txt"); + let Ok(marker) = std::fs::read_to_string(marker) else { + return false; + }; + driver_registration() + .is_some_and(|(location, version)| marker_matches(&marker, &location, &version)) +} + +/// Whether the marker records the driver registered at `location` with +/// `version`: its uninstaller's path and the version on two lines, compared +/// ignoring case as PowerShell's `-eq` does in the scripts. +fn marker_matches(marker: &str, location: &str, version: &str) -> bool { + let location = location.strip_suffix('\\').unwrap_or(location); + let expected = format!("{location}\\unins000.exe\r\n{version}"); + marker.to_lowercase() == expected.to_lowercase() +} + +/// Where the driver's own setup registered it, and its version, as the +/// scripts read it: the first uninstall entry with both. +#[cfg(windows)] +fn driver_registration() -> Option<(String, String)> { + [ + r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\VirtualDisplayDriver_is1", + r"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\VirtualDisplayDriver_is1", + ] + .into_iter() + .find_map(|key| { + let value = |name| crate::diagnostics::registry_value(key, name); + Some((value("InstallLocation")?, value("DisplayVersion")?)) + }) +} + +#[cfg(not(windows))] +fn driver_registration() -> Option<(String, String)> { + None +} + +/// Remember the cards this host may have written into the settings file +/// last, one per line, or that it wrote none. The record is replaced as the +/// settings file is, so a crash or power cut leaves the old or the new one. +pub fn remember_pins(record: &Path, names: &[&str]) -> io::Result<()> { + if names.is_empty() { + return match std::fs::remove_file(record) { + Err(error) if error.kind() != io::ErrorKind::NotFound => Err(error), + _ => Ok(()), + }; + } + if let Some(parent) = record.parent() { + std::fs::create_dir_all(parent)?; + } + replace(record, &names.join("\n")) +} + +/// Make the settings file at `path` name `wanted`, changing only the card, +/// under the rules of `pin_change`. `record` holds the names this host +/// recorded writing. A missing file stays missing, and one that does not +/// parse as expected is left alone; the error says why. +pub fn apply_pin( + path: &Path, + record: &Path, + owned: bool, + wanted: Option<&str>, +) -> io::Result { + let invalid = |reason| io::Error::new(io::ErrorKind::InvalidData, reason); + let current = std::fs::read_to_string(path)?; + let named = named_gpu(¤t).map_err(invalid)?; + let ours = recorded_pins(record); + let ours: Vec<&str> = ours.iter().map(String::as_str).collect(); + let (name, outcome) = match pin_change(named.as_deref(), wanted, owned, &ours) { + PinChange::Leave => return Ok(PinOutcome::Unchanged), + PinChange::Refuse(reason) => return Ok(PinOutcome::Kept(reason)), + PinChange::Write(card) => (card, PinOutcome::Named), + PinChange::Reset => ("default".to_string(), PinOutcome::Reset), + }; + let updated = gpu_edit(¤t, &name).map_err(invalid)?; + let new_record = (outcome == PinOutcome::Named).then_some(name.as_str()); + record_around(record, new_record, &ours, || replace(path, &updated))?; + Ok(outcome) +} + +/// `write` the settings file, `new` taking the place of `ours` as the names +/// this host wrote, with the record true wherever a crash or power cut +/// stops it: until the file is replaced it may hold either, so the record +/// holds both; once it is, only `new`. With `new` as `None` the record +/// keeps `ours` until the file no longer names them. When the write fails, +/// the record goes back to `ours`. +fn record_around( + record: &Path, + new: Option<&str>, + ours: &[&str], + write: impl FnOnce() -> io::Result<()>, +) -> io::Result<()> { + let mut both = ours.to_vec(); + both.extend(new.filter(|new| !ours.iter().any(|name| same_card(name, new)))); + let grew = both.len() > ours.len(); + if grew { + remember_pins(record, &both)?; + } + write().inspect_err(|_| { + if grew { + let _ = remember_pins(record, ours); + } + })?; + // A record left holding both is still true. + let _ = remember_pins(record, new.as_slice()); + Ok(()) +} + +/// The names this host recorded writing: one, or more after a write that +/// a crash or power cut stopped. +fn recorded_pins(record: &Path) -> Vec { + let Ok(ours) = std::fs::read_to_string(record) else { + return Vec::new(); + }; + ours.lines() + .map(str::trim) + .filter(|name| !name.is_empty()) + .map(String::from) + .collect() +} + +/// Write the whole settings file at `path` with `modes`, naming `wanted` +/// under the rules of `pin_change`: a card someone else named stays named +/// in the new file. A missing file names no card. A file this host cannot +/// read may name anyone's card, so it is replaced only where any name may +/// be, on a driver Setup installed and with a card to name; otherwise it is +/// left as it is and the error says why. `record` holds the names this +/// host recorded writing. +pub fn write_with_pin( + path: &Path, + record: &Path, + owned: bool, + modes: &[VddMode], + wanted: Option<&str>, +) -> io::Result { + let named = match std::fs::read_to_string(path) { + Ok(current) => match named_gpu(¤t) { + Ok(named) => named, + Err(_) if owned && wanted.is_some() => None, + Err(reason) => return Err(io::Error::new(io::ErrorKind::InvalidData, reason)), + }, + Err(error) if error.kind() == io::ErrorKind::NotFound => None, + Err(error) => return Err(error), + }; + let ours = recorded_pins(record); + let ours: Vec<&str> = ours.iter().map(String::as_str).collect(); + let (name, outcome) = match pin_change(named.as_deref(), wanted, owned, &ours) { + PinChange::Leave => (named, PinOutcome::Unchanged), + PinChange::Refuse(reason) => (named, PinOutcome::Kept(reason)), + PinChange::Write(card) => (Some(card), PinOutcome::Named), + PinChange::Reset => (None, PinOutcome::Reset), + }; + let write = || write_to(path, modes, name.as_deref()); + match outcome { + PinOutcome::Named => record_around(record, name.as_deref(), &ours, write)?, + PinOutcome::Reset => record_around(record, None, &ours, write)?, + _ => write()?, + } + Ok(outcome) +} + +/// What the snapshot reports as asked of the driver after `apply_pin`: the +/// card the file names for this host, or why it names none. +pub fn pin_result( + wanted: Result<&str, &'static str>, + applied: &io::Result, +) -> Result { + match (wanted, applied) { + (Err(reason), _) => Err(reason), + (Ok(_), Ok(PinOutcome::Kept(reason))) => Err(reason), + (Ok(_), Err(_)) => Err("the settings file was left as it is"), + (Ok(card), Ok(_)) => Ok(card.to_string()), + } +} + /// Write atomically (tmp + rename) so the driver never reads a torn file. /// Creates the directory if the driver hasn't made it yet. -pub fn write_to(path: &Path, modes: &[VddMode]) -> io::Result<()> { +pub fn write_to(path: &Path, modes: &[VddMode], gpu: Option<&str>) -> io::Result<()> { let parent = path.parent().ok_or_else(|| { io::Error::new(io::ErrorKind::InvalidInput, "settings path has no parent") })?; std::fs::create_dir_all(parent)?; - let tmp = path.with_extension("xml.tmp"); - std::fs::write(&tmp, render(modes))?; + replace(path, &render(modes, gpu)) +} + +/// The new contents reach the disk before they replace the file, so an +/// interrupted write leaves the old file whole. +fn replace(path: &Path, contents: &str) -> io::Result<()> { + use std::io::Write; + let mut tmp = path.as_os_str().to_owned(); + tmp.push(".tmp"); + let tmp = std::path::PathBuf::from(tmp); + let mut file = std::fs::File::create(&tmp)?; + file.write_all(contents.as_bytes())?; + file.sync_all()?; + drop(file); std::fs::rename(&tmp, path) } @@ -131,6 +716,44 @@ mod tests { ); } + #[test] + fn a_second_screen_at_another_size_is_set_to_the_requested_mode() { + let ipad = modes_for_client(1668, 2420, 120)[0]; + // Windows restored the 800x600 it saved the first time. + assert_eq!(mode_to_set(Some(ipad), (800, 600)), Some(ipad)); + assert_eq!(mode_to_set(Some(ipad), (2420, 1668)), None); + // Matching is off: the size is the user's. + assert_eq!(mode_to_set(None, (800, 600)), None); + assert_eq!( + default_modes(), + [VddMode { + width: 1920, + height: 1080, + refresh_hz: 60 + }] + ); + } + + #[test] + fn without_the_ipads_mode_the_largest_size_that_fits_is_used() { + // The driver's own list, as its installer leaves it. + let vendor = [ + (800, 600), + (1366, 768), + (1920, 1080), + (2560, 1440), + (3840, 2160), + ]; + let ipad = |w, h| VddMode { + width: w, + height: h, + refresh_hz: 60, + }; + assert_eq!(best_offered(ipad(2048, 1536), &vendor), Some((1920, 1080))); + assert_eq!(best_offered(ipad(2752, 2064), &vendor), Some((2560, 1440))); + assert_eq!(best_offered(ipad(640, 480), &vendor), None); + } + #[test] fn odd_dimensions_are_evened_for_420_encoders() { let modes = modes_for_client(1667, 2421, 60); @@ -153,7 +776,8 @@ mod tests { #[test] fn render_produces_the_driver_schema() { - let xml = render(&modes_for_client(1668, 2420, 120)); + let xml = render(&modes_for_client(1668, 2420, 120), None); + assert!(!xml.contains(""), "the driver chooses without a name"); assert!(xml.starts_with("")); assert!(xml.contains("")); assert!(xml.contains("1")); @@ -165,12 +789,759 @@ mod tests { assert!(xml.ends_with("\n")); } + #[test] + fn render_names_the_main_screens_graphics_card() { + let xml = render( + &modes_for_client(1668, 2420, 120), + Some("Intel(R) Iris(R) Xe Graphics"), + ); + assert!(xml.contains( + " \n Intel(R) Iris(R) Xe Graphics\n \n" + )); + // Before the modes, as in the driver's own file. + assert!(xml.find("") < xml.find("")); + let xml = render(&modes_for_client(1668, 2420, 60), Some("A&B ")); + assert!(xml.contains("A&B <GPU>")); + } + + /// The vendor's own file, as its installer leaves it (shortened). + const VENDOR: &str = "\r\n\r\n \r\n 1\r\n \r\n \r\n default\r\n \r\n\t\r\n\t\t60\r\n\t\r\n\t\r\n\t\ttrue\r\n\t\r\n\r\n"; + + /// Adapters in DXGI order, as (description, vendor:device, outputs). + fn cards(list: &[(&str, (u32, u32), u32)]) -> Vec { + list.iter() + .enumerate() + .map( + |(index, &(name, (vendor_id, device_id), outputs))| crate::vdd_snapshot::Adapter { + luid: 0x1000 + index as u64, + description: name.into(), + vendor_id, + device_id, + driver: String::new(), + outputs, + }, + ) + .collect() + } + + #[test] + fn the_main_screens_card_is_pinned_unless_its_name_is_shared() { + let hybrid = cards(&[ + ("Intel(R) Iris(R) Xe Graphics", (0x8086, 0x46a6), 1), + ("NVIDIA GeForce RTX 3050 Ti Laptop GPU", (0x10de, 0x25a0), 0), + ]); + assert_eq!( + gpu_to_pin(Some("Intel(R) Iris(R) Xe Graphics"), &hybrid), + Ok("Intel(R) Iris(R) Xe Graphics") + ); + // A 0.3.1 host listed "AMD Radeon 780M Graphics" twice; the driver + // would take whichever it enumerates first. + let twins = cards(&[ + ("AMD Radeon 780M Graphics", (0x1002, 0x15bf), 0), + ("AMD Radeon 780M Graphics", (0x1002, 0x15bf), 0), + ]); + assert!(gpu_to_pin(Some("AMD Radeon 780M Graphics"), &twins).is_err()); + assert!(gpu_to_pin(None, &hybrid).is_err()); + assert!(gpu_to_pin(Some(""), &hybrid).is_err()); + } + + /// An Intel HD 630 + GTX 1050 laptop on 0.3.2 listed its Intel card + /// twice with the virtual display off. Only the first drives the panel, + /// and the driver takes the first match, so the name is pinned. + #[test] + fn a_card_listed_twice_is_pinned_when_only_the_first_drives_a_screen() { + const INTEL: &str = "Intel(R) HD Graphics 630"; + let listed = |outputs: [u32; 2]| { + cards(&[ + (INTEL, (0x8086, 0x591b), outputs[0]), + ("NVIDIA GeForce GTX 1050", (0x10de, 0x1c8d), 0), + (INTEL, (0x8086, 0x591b), outputs[1]), + ]) + }; + assert_eq!(gpu_to_pin(Some(INTEL), &listed([1, 0])), Ok(INTEL)); + // The driver would find the one without screens first. + assert!(gpu_to_pin(Some(INTEL), &listed([0, 1])).is_err()); + // Both drive screens, or neither is known to: no telling which. + assert!(gpu_to_pin(Some(INTEL), &listed([1, 1])).is_err()); + assert!(gpu_to_pin(Some(INTEL), &listed([0, 0])).is_err()); + // The driver matches names ignoring case, so a listing in another + // case is the same card. + assert!(gpu_to_pin(Some("intel(r) hd graphics 630"), &listed([0, 1])).is_err()); + // The duplicate does not stop a pin to the other card. + assert_eq!( + gpu_to_pin(Some("NVIDIA GeForce GTX 1050"), &listed([1, 0])), + Ok("NVIDIA GeForce GTX 1050") + ); + } + + #[test] + fn with_matching_off_only_the_gpu_name_changes() { + let pinned = with_gpu(VENDOR, "Intel(R) Iris(R) Xe Graphics").unwrap(); + assert_eq!( + pinned, + VENDOR.replace( + "default", + "Intel(R) Iris(R) Xe Graphics" + ) + ); + // A file without a element gets one under the root. + let bare = "\n \n 1\n \n\n"; + assert_eq!( + with_gpu(bare, "A&B").unwrap(), + "\n \n A&B\n \n \n 1\n \n\n" + ); + assert_eq!(with_gpu("", "x"), None); + } + + /// 0.3.2 rewrote the file on every connect without a `` element. + /// After an upgrade, with matching off, only the card is added. + #[test] + fn a_file_written_by_0_3_2_gets_the_card_and_keeps_its_modes() { + let v032 = "\n\n \n 1\n \n \n 60\n \n \n \n 2160\n 1620\n 60\n \n \n\n"; + assert_eq!( + with_gpu(v032, "Intel(R) HD Graphics 630").as_deref(), + Some( + v032.replacen( + "\n", + "\n \n Intel(R) HD Graphics 630\n \n", + 1 + ) + .as_str() + ) + ); + } + + #[test] + fn a_name_is_replaced_only_when_this_host_may() { + use PinChange::*; + const INTEL: &str = "Intel(R) HD Graphics 630"; + const OTHER: &str = "NVIDIA GeForce GTX 1050"; + let write = || Write(INTEL.to_string()); + // The driver chooses: this host may name a card, owned or not. + for owned in [false, true] { + assert_eq!(pin_change(None, Some(INTEL), owned, &[]), write()); + } + // A card another app or the user named stays on a driver Setup did + // not install, and is replaced on one it did. + assert_eq!( + pin_change(Some(OTHER), Some(INTEL), false, &[]), + Refuse(NAMED_ELSEWHERE) + ); + assert_eq!( + pin_change(Some(OTHER), Some(INTEL), false, &[INTEL]), + Refuse(NAMED_ELSEWHERE) + ); + assert_eq!(pin_change(Some(OTHER), Some(INTEL), true, &[]), write()); + // This host's own earlier name, in any case, is replaced. + assert_eq!( + pin_change( + Some("nvidia geforce gtx 1050"), + Some(INTEL), + false, + &[OTHER] + ), + write() + ); + // Already named, ignoring case. + assert_eq!( + pin_change(Some("intel(r) hd graphics 630"), Some(INTEL), false, &[]), + Leave + ); + // No card to name: this host's earlier name is taken back, anyone + // else's stays. + assert_eq!(pin_change(Some(INTEL), None, false, &[INTEL]), Reset); + assert_eq!(pin_change(Some(INTEL), None, true, &[]), Leave); + assert_eq!(pin_change(None, None, false, &[INTEL]), Leave); + } + + #[test] + fn the_named_card_is_read_back_as_the_driver_reads_it() { + let file = |name: &str| VENDOR.replace("default", name); + assert_eq!(named_gpu(VENDOR), Ok(None)); + assert_eq!(named_gpu(&file(" DEFAULT ")), Ok(None)); + assert_eq!(named_gpu(&file("")), Ok(None)); + assert_eq!(named_gpu(&file("A&B")), Ok(Some("A&B".to_string()))); + assert_eq!(named_gpu(""), Ok(None)); + assert!(named_gpu("").is_err()); + } + + #[test] + fn apply_pin_records_its_own_name_and_takes_it_back() { + let dir = std::env::temp_dir().join(format!("vdd-pin-own-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let (path, record) = ( + dir.join("vdd_settings.xml"), + dir.join("app").join("vdd-pin.txt"), + ); + let named = || named_gpu(&std::fs::read_to_string(&path).unwrap()).unwrap(); + const INTEL: &str = "Intel(R) HD Graphics 630"; + + // The vendor's file, on a driver another app installed. + std::fs::write(&path, VENDOR).unwrap(); + assert_eq!( + apply_pin(&path, &record, false, Some(INTEL)).unwrap(), + PinOutcome::Named + ); + assert_eq!(named().as_deref(), Some(INTEL)); + assert_eq!(std::fs::read_to_string(&record).unwrap(), INTEL); + assert_eq!( + apply_pin(&path, &record, false, Some(INTEL)).unwrap(), + PinOutcome::Unchanged + ); + // The pin is refused later: the name this host wrote goes, and so + // does the record. + assert_eq!( + apply_pin(&path, &record, false, None).unwrap(), + PinOutcome::Reset + ); + assert_eq!(named(), None); + assert!(std::fs::read_to_string(&path) + .unwrap() + .contains("default")); + assert!(!record.exists()); + + // A card the user or another app named stays, and is reported. + let theirs = VENDOR.replace("default", "NVIDIA GeForce GTX 1050"); + std::fs::write(&path, &theirs).unwrap(); + let applied = apply_pin(&path, &record, false, Some(INTEL)); + assert_eq!( + applied.as_ref().unwrap(), + &PinOutcome::Kept(NAMED_ELSEWHERE) + ); + assert_eq!(std::fs::read_to_string(&path).unwrap(), theirs); + assert_eq!(pin_result(Ok(INTEL), &applied), Err(NAMED_ELSEWHERE)); + assert_eq!( + apply_pin(&path, &record, false, None).unwrap(), + PinOutcome::Unchanged + ); + assert_eq!(std::fs::read_to_string(&path).unwrap(), theirs); + // Setup installed this driver: the main screen's card wins. + assert_eq!( + apply_pin(&path, &record, true, Some(INTEL)).unwrap(), + PinOutcome::Named + ); + assert_eq!(named().as_deref(), Some(INTEL)); + assert_eq!( + pin_result(Ok(INTEL), &Ok(PinOutcome::Named)), + Ok(INTEL.to_string()) + ); + + // A missing file stays missing. + std::fs::remove_file(&path).unwrap(); + let applied = apply_pin(&path, &record, true, Some(INTEL)); + assert!(applied.is_err() && !path.exists()); + assert_eq!( + pin_result(Ok(INTEL), &applied), + Err("the settings file was left as it is") + ); + assert_eq!( + pin_result( + Err("no adapter drives the main screen"), + &Ok(PinOutcome::Unchanged) + ), + Err("no adapter drives the main screen") + ); + std::fs::remove_dir_all(&dir).unwrap(); + } + + /// Match the iPad's resolution rewrites the whole file, under the same + /// rule: on a driver Setup did not install, a card the user or another + /// app named stays named. + #[test] + fn a_whole_file_rewrite_keeps_a_card_someone_else_named() { + let dir = std::env::temp_dir().join(format!("vdd-pin-rewrite-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let (path, record) = ( + dir.join("vdd_settings.xml"), + dir.join("app").join("vdd-pin.txt"), + ); + let named = || named_gpu(&std::fs::read_to_string(&path).unwrap()).unwrap(); + let modes = default_modes(); + const INTEL: &str = "Intel(R) HD Graphics 630"; + const OTHER: &str = "NVIDIA GeForce GTX 1050"; + + // No file yet: this host names the card and records it. + assert_eq!( + write_with_pin(&path, &record, false, &modes, Some(INTEL)).unwrap(), + PinOutcome::Named + ); + assert_eq!(named().as_deref(), Some(INTEL)); + assert_eq!(std::fs::read_to_string(&record).unwrap(), INTEL); + assert!(std::fs::read_to_string(&path) + .unwrap() + .contains("1920")); + // No card to name later: its own name goes, and so does the record. + assert_eq!( + write_with_pin(&path, &record, false, &modes, None).unwrap(), + PinOutcome::Reset + ); + assert_eq!(named(), None); + assert!(!record.exists()); + + // Someone else's card stays, with or without a card to name. + std::fs::write(&path, VENDOR.replace("default", OTHER)).unwrap(); + let written = write_with_pin(&path, &record, false, &modes, Some(INTEL)); + assert_eq!( + written.as_ref().unwrap(), + &PinOutcome::Kept(NAMED_ELSEWHERE) + ); + assert_eq!(named().as_deref(), Some(OTHER)); + assert_eq!(pin_result(Ok(INTEL), &written), Err(NAMED_ELSEWHERE)); + assert_eq!( + write_with_pin(&path, &record, false, &modes, None).unwrap(), + PinOutcome::Unchanged + ); + assert_eq!(named().as_deref(), Some(OTHER)); + assert!(!record.exists()); + // On a driver Setup installed, the main screen's card wins. + assert_eq!( + write_with_pin(&path, &record, true, &modes, Some(INTEL)).unwrap(), + PinOutcome::Named + ); + assert_eq!(named().as_deref(), Some(INTEL)); + + // A file that does not read as expected is replaced, naming the + // card, only on a driver Setup installed. + std::fs::write(&path, "").unwrap(); + assert_eq!( + write_with_pin(&path, &record, true, &modes, Some(INTEL)).unwrap(), + PinOutcome::Named + ); + assert_eq!(named().as_deref(), Some(INTEL)); + std::fs::remove_dir_all(&dir).unwrap(); + } + + /// Review round 3, defect 2: a card named in a way this host does not + /// read (here CDATA, which the driver reads) counted as no card, and + /// Match the iPad's resolution replaced it on a driver another app + /// installed. + #[test] + fn a_whole_file_rewrite_keeps_a_file_it_cannot_read() { + let dir = std::env::temp_dir().join(format!("vdd-pin-unread-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let (path, record) = ( + dir.join("vdd_settings.xml"), + dir.join("app").join("vdd-pin.txt"), + ); + let modes = default_modes(); + const INTEL: &str = "Intel(R) HD Graphics 630"; + let cdata = VENDOR.replace(">default<", "><"); + for document in [cdata.as_str(), ""] { + for wanted in [Some(INTEL), None] { + std::fs::write(&path, document).unwrap(); + let written = write_with_pin(&path, &record, false, &modes, wanted); + assert!(written.is_err(), "{document:?} {wanted:?}"); + assert_eq!(std::fs::read_to_string(&path).unwrap(), document); + assert_eq!( + pin_result(Ok(INTEL), &written), + Err("the settings file was left as it is") + ); + assert!(!record.exists()); + } + // Nor is it replaced without a card to name on a driver Setup + // installed: the name in it may not be this host's. + let written = write_with_pin(&path, &record, true, &modes, None); + assert!(written.is_err(), "{document:?}"); + assert_eq!(std::fs::read_to_string(&path).unwrap(), document); + } + std::fs::remove_dir_all(&dir).unwrap(); + } + + /// Review round 3, defect 4: the record named the new card before the + /// file was replaced. When the replacement failed, the file still held + /// this host's earlier name, which the record no longer did, so every + /// later attempt refused to replace it. + #[test] + fn a_failed_write_keeps_this_hosts_earlier_name_its_own() { + let dir = std::env::temp_dir().join(format!("vdd-pin-failed-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let (path, record) = ( + dir.join("vdd_settings.xml"), + dir.join("app").join("vdd-pin.txt"), + ); + let named = || named_gpu(&std::fs::read_to_string(&path).unwrap()).unwrap(); + // A directory where the new file is written first makes it fail. + let blocker = path.with_extension("xml.tmp"); + let modes = default_modes(); + const INTEL: &str = "Intel(R) HD Graphics 630"; + const AMD: &str = "AMD Radeon(TM) Graphics"; + type Pin<'a> = dyn Fn(Option<&str>) -> io::Result + 'a; + let edit: &Pin = &|wanted| apply_pin(&path, &record, false, wanted); + let rewrite: &Pin = &|wanted| write_with_pin(&path, &record, false, &modes, wanted); + for pin in [edit, rewrite] { + std::fs::write(&path, VENDOR).unwrap(); + assert_eq!(pin(Some(INTEL)).unwrap(), PinOutcome::Named); + for wanted in [Some(AMD), None] { + std::fs::create_dir(&blocker).unwrap(); + assert!(pin(wanted).is_err(), "{wanted:?}"); + std::fs::remove_dir(&blocker).unwrap(); + assert_eq!(named().as_deref(), Some(INTEL)); + assert_eq!(std::fs::read_to_string(&record).unwrap(), INTEL); + } + // The next attempt replaces it. + assert_eq!(pin(Some(AMD)).unwrap(), PinOutcome::Named); + assert_eq!(named().as_deref(), Some(AMD)); + assert_eq!(std::fs::read_to_string(&record).unwrap(), AMD); + assert_eq!(pin(None).unwrap(), PinOutcome::Reset); + assert_eq!(named(), None); + assert!(!record.exists()); + } + std::fs::remove_dir_all(&dir).unwrap(); + } + + /// Run a pin that a crash or power cut stops just before the settings + /// file is replaced: what stays on disk is the record as it stood then, + /// and the old file. + fn crash_before_replace(record: &Path, new: Option<&str>) { + let ours = recorded_pins(record); + let ours: Vec<&str> = ours.iter().map(String::as_str).collect(); + let mut on_disk = None; + record_around(record, new, &ours, || { + on_disk = Some(std::fs::read_to_string(record).ok()); + Ok(()) + }) + .unwrap(); + let _ = std::fs::remove_file(record); + if let Some(names) = on_disk.unwrap() { + std::fs::write(record, names).unwrap(); + } + } + + /// Review P3-1: the record named the new card before the settings file + /// held it, so a crash or power cut between the two left this host's own + /// name looking like another app's, and later attempts kept it. + #[test] + fn a_crash_before_a_new_name_is_written_keeps_the_old_one_this_hosts() { + let dir = std::env::temp_dir().join(format!("vdd-pin-crash-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let (path, record) = ( + dir.join("vdd_settings.xml"), + dir.join("app").join("vdd-pin.txt"), + ); + let named = || named_gpu(&std::fs::read_to_string(&path).unwrap()).unwrap(); + let modes = default_modes(); + const INTEL: &str = "Intel(R) HD Graphics 630"; + const AMD: &str = "AMD Radeon(TM) Graphics"; + type Pin<'a> = dyn Fn(Option<&str>) -> io::Result + 'a; + let edit: &Pin = &|wanted| apply_pin(&path, &record, false, wanted); + let rewrite: &Pin = &|wanted| write_with_pin(&path, &record, false, &modes, wanted); + for pin in [edit, rewrite] { + std::fs::write(&path, VENDOR).unwrap(); + assert_eq!(pin(Some(INTEL)).unwrap(), PinOutcome::Named); + crash_before_replace(&record, Some(AMD)); + assert_eq!(named().as_deref(), Some(INTEL)); + // The next run still knows the name the file holds as its own. + assert_eq!(pin(Some(AMD)).unwrap(), PinOutcome::Named); + assert_eq!(named().as_deref(), Some(AMD)); + // Once the file is replaced, the record holds only the new name. + assert_eq!(std::fs::read_to_string(&record).unwrap(), AMD); + crash_before_replace(&record, Some(INTEL)); + assert_eq!(pin(None).unwrap(), PinOutcome::Reset); + assert_eq!(named(), None); + assert!(!record.exists()); + } + // Neither file is left half written beside the real one. + assert!(!record.with_extension("txt.tmp").exists()); + assert!(!path.with_extension("xml.tmp").exists()); + std::fs::remove_dir_all(&dir).unwrap(); + } + + /// Review P3-1: taking the name back deleted the record before the + /// settings file was replaced, so a crash or power cut between the two + /// left the file naming a card this host no longer knew it wrote. + #[test] + fn a_crash_before_the_name_is_taken_back_keeps_it_this_hosts() { + let dir = std::env::temp_dir().join(format!("vdd-pin-reset-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let (path, record) = ( + dir.join("vdd_settings.xml"), + dir.join("app").join("vdd-pin.txt"), + ); + let named = || named_gpu(&std::fs::read_to_string(&path).unwrap()).unwrap(); + let modes = default_modes(); + const INTEL: &str = "Intel(R) HD Graphics 630"; + const AMD: &str = "AMD Radeon(TM) Graphics"; + type Pin<'a> = dyn Fn(Option<&str>) -> io::Result + 'a; + let edit: &Pin = &|wanted| apply_pin(&path, &record, false, wanted); + let rewrite: &Pin = &|wanted| write_with_pin(&path, &record, false, &modes, wanted); + for pin in [edit, rewrite] { + std::fs::write(&path, VENDOR).unwrap(); + assert_eq!(pin(Some(INTEL)).unwrap(), PinOutcome::Named); + crash_before_replace(&record, None); + assert_eq!(named().as_deref(), Some(INTEL)); + // Still this host's: taken back on the next run. + assert_eq!(pin(None).unwrap(), PinOutcome::Reset); + assert_eq!(named(), None); + assert!(!record.exists()); + // And replaced by another card. + assert_eq!(pin(Some(INTEL)).unwrap(), PinOutcome::Named); + crash_before_replace(&record, None); + assert_eq!(pin(Some(AMD)).unwrap(), PinOutcome::Named); + assert_eq!(named().as_deref(), Some(AMD)); + assert_eq!(pin(None).unwrap(), PinOutcome::Reset); + } + std::fs::remove_dir_all(&dir).unwrap(); + } + + /// Review round 3, defect 3: the marker counted whatever it said, after + /// another app replaced or upgraded the driver. + #[test] + fn only_setups_marker_for_the_registered_driver_makes_it_this_hosts() { + let dir = std::env::temp_dir().join(format!("vdd-owned-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(dir.join("driver")).unwrap(); + assert!(!driver_owned(&dir)); + // A marker for no driver registered here. + std::fs::write( + dir.join("driver").join("installed-by-eternalmonitor.txt"), + "C:\\Elsewhere\\unins000.exe\r\n0.0.0", + ) + .unwrap(); + assert!(!driver_owned(&dir)); + std::fs::remove_dir_all(&dir).unwrap(); + + // What Setup writes, for the install the scripts find. + const LOCATION: &str = "C:\\VirtualDisplayDriver"; + let setup = "C:\\VirtualDisplayDriver\\unins000.exe\r\n25.5.2"; + assert!(marker_matches(setup, LOCATION, "25.5.2")); + assert!(marker_matches( + setup, + "C:\\VirtualDisplayDriver\\", + "25.5.2" + )); + assert!(marker_matches(&setup.to_uppercase(), LOCATION, "25.5.2")); + // Upgraded, or installed again elsewhere, by another app. + assert!(!marker_matches(setup, LOCATION, "25.7.23")); + assert!(!marker_matches(setup, "D:\\Tools\\VDD", "25.5.2")); + assert!(!marker_matches("x", LOCATION, "25.5.2")); + assert!(!marker_matches(&format!("{setup}\r\n"), LOCATION, "25.5.2")); + } + + #[test] + fn the_card_goes_inside_a_compact_root() { + // Sol's case: no newline inside the root, one after it. + let compact = "1\n"; + assert_eq!( + with_gpu(compact, "X").as_deref(), + Some("\n \n X\n 1\n") + ); + } + + #[test] + fn a_gpu_element_without_a_name_gets_one() { + assert_eq!( + with_gpu("\n \n \n\n", "X").as_deref(), + Some("\n \n X\n \n\n") + ); + assert_eq!( + with_gpu("", "X").as_deref(), + Some("X") + ); + assert_eq!( + with_gpu( + "", + "X" + ) + .as_deref(), + Some("X") + ); + } + + #[test] + fn comments_line_endings_and_bom_are_kept() { + let commented = "\n\n\n \n A&B \n \n\n"; + assert_eq!( + with_gpu(commented, "Intel(R) Iris(R) Xe Graphics"), + Some(commented.replace(">A&B<", ">Intel(R) Iris(R) Xe Graphics<")) + ); + let crlf = "\r\n\t\r\n\t\r\n\r\n"; + assert_eq!( + with_gpu(crlf, "X").as_deref(), + Some("\r\n \r\n X\r\n \r\n\t\r\n\t\r\n\r\n") + ); + let bom = format!("\u{feff}{VENDOR}"); + assert_eq!(with_gpu(&bom, "X"), Some(bom.replace(">default<", ">X<"))); + } + + #[test] + fn a_file_that_does_not_parse_as_expected_is_left_alone() { + let dir = std::env::temp_dir().join(format!("vdd-pin-refuse-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("vdd_settings.xml"); + for document in [ + "", + "1", + "\nx\n", + "junk", + "x", + "ab", + "a", + "a", + "", + ] { + assert_eq!(with_gpu(document, "X"), None, "{document}"); + std::fs::write(&path, document).unwrap(); + assert!(pin_gpu_in(&path, "X").is_err(), "{document}"); + assert_eq!(std::fs::read_to_string(&path).unwrap(), document, "changed: {document}"); + } + let _ = std::fs::remove_dir_all(&dir); + } + + #[test] + fn documents_expat_rejects_are_left_alone() { + let dir = std::env::temp_dir().join(format!("vdd-pin-strict-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("vdd_settings.xml"); + let name = "default"; + for document in [ + // Sol's four: each was rewritten, and Expat rejected the result. + format!("{name}\n"), + format!("{name}\n"), + format!("{name}\n\n"), + format!("{name}\u{1}\n"), + // More of the same kind. + format!("\n{name}"), + format!("{name}"), + format!("{name} "), + format!("{name}"), + format!("{name}"), + format!("{name}"), + format!("{name}]]>"), + format!("{name}<1count/>"), + ] { + assert_eq!(with_gpu(&document, "X"), None, "{document:?}"); + std::fs::write(&path, &document).unwrap(); + assert!(pin_gpu_in(&path, "X").is_err(), "{document:?}"); + assert_eq!( + std::fs::read_to_string(&path).unwrap(), + document, + "changed: {document:?}" + ); + } + // A name that is not valid XML is not written either. + let vendor = with_gpu(VENDOR, "Bad\u{1}Card"); + assert_eq!(vendor, None); + // References XML defines are fine. + let fine = format!("\n{name}A<\n"); + assert_eq!(with_gpu(&fine, "X"), Some(fine.replace(">default<", ">X<"))); + let _ = std::fs::remove_dir_all(&dir); + } + + /// The driver's own vdd_settings.xml from Virtual-Display-Driver 25.5.2 + /// (MIT), byte for byte: single-quoted declaration, many comments. + const VENDOR_FILE: &str = include_str!("../tests/fixtures/vdd_settings-25.5.2.xml"); + + #[test] + fn only_the_shapes_this_host_and_the_driver_write_are_edited() { + let dir = std::env::temp_dir().join(format!("vdd-pin-allow-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("vdd_settings.xml"); + let name = "default"; + for document in [ + // Sol's four: each was rewritten. + format!("{name}\n"), + format!("\n{name}\n"), + format!("]>\n{name}\n"), + format!("\n{name}\n"), + // Everything outside the allow-list. + format!( + "\n{}", + "default" + ), + format!("\n{name}"), + format!("\n{name}"), + format!("{name}"), + format!("{name}"), + format!("\n{name}"), + format!( + "\n{name}" + ), + format!( + "\n{name}" + ), + format!("\n{name}"), + format!(" \n{name}"), + ] { + assert_eq!(with_gpu(&document, "X"), None, "{document:?}"); + std::fs::write(&path, &document).unwrap(); + assert!(pin_gpu_in(&path, "X").is_err(), "{document:?}"); + assert_eq!( + std::fs::read_to_string(&path).unwrap(), + document, + "changed: {document:?}" + ); + } + // Every declaration shape the allow-list names, and none. + for declaration in [ + "", + "\n", + "\n", + "\n", + "\n", + ] { + let document = format!("{declaration}{name}\n"); + assert_eq!( + with_gpu(&document, "X"), + Some(document.replace(">default<", ">X<")), + "{document:?}" + ); + } + // The driver's own file is edited in place, and so is this host's. + assert_eq!( + with_gpu(VENDOR_FILE, "Intel(R) Iris(R) Xe Graphics"), + Some(VENDOR_FILE.replace( + "default", + "Intel(R) Iris(R) Xe Graphics" + )) + ); + std::fs::write(&path, VENDOR_FILE).unwrap(); + assert!(pin_gpu_in(&path, "X").unwrap()); + let ours = render(&modes_for_client(1668, 2420, 120), Some("A")); + assert_eq!(with_gpu(&ours, "B"), Some(ours.replace(">A<", ">B<"))); + let _ = std::fs::remove_dir_all(&dir); + } + + #[test] + fn a_stale_pin_is_corrected_and_a_current_one_left_alone() { + let dir = std::env::temp_dir().join(format!("vdd-pin-test-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("vdd_settings.xml"); + // Pinned earlier to a card that no longer drives the main screen. + let stale = with_gpu(VENDOR, "AMD Radeon(TM) Graphics").unwrap(); + std::fs::write(&path, &stale).unwrap(); + assert!(pin_gpu_in(&path, "NVIDIA GeForce RTX 5080").unwrap()); + let written = std::fs::read_to_string(&path).unwrap(); + assert_eq!( + written, + with_gpu(VENDOR, "NVIDIA GeForce RTX 5080").unwrap() + ); + assert!( + !pin_gpu_in(&path, "NVIDIA GeForce RTX 5080").unwrap(), + "no rewrite" + ); + // No file: none is made, so the driver keeps its built-in modes. + let missing = dir.join("missing.xml"); + assert!(pin_gpu_in(&missing, "NVIDIA GeForce RTX 5080").is_err()); + assert!(!missing.exists()); + let _ = std::fs::remove_dir_all(&dir); + } + #[test] fn write_is_atomic_and_creates_the_directory() { let dir = std::env::temp_dir().join(format!("vdd-settings-test-{}", std::process::id())); let _ = std::fs::remove_dir_all(&dir); let path = dir.join("nested").join("vdd_settings.xml"); - write_to(&path, &modes_for_client(1668, 2420, 120)).expect("write settings"); + write_to(&path, &modes_for_client(1668, 2420, 120), None).expect("write settings"); let written = std::fs::read_to_string(&path).expect("read back"); assert!(written.contains("2420")); assert!(!path.with_extension("xml.tmp").exists(), "tmp cleaned up"); diff --git a/host/src/vdd_snapshot.rs b/host/src/vdd_snapshot.rs new file mode 100644 index 00000000..93a66af1 --- /dev/null +++ b/host/src/vdd_snapshot.rs @@ -0,0 +1,596 @@ +//! One log record of how Windows sees the displays when the second screen +//! did not appear, written before the Disable task removes the virtual +//! display. It goes into the session log, so "Copy logs" and the support +//! diagnostics carry it. +//! +//! It holds graphics card names and IDs, display path numbers and flags, +//! and a few settings. Monitor device paths are read only to tell which +//! paths belong to the virtual display and are never written, and nothing +//! names the machine or the user. The record is bounded in size. + +use crate::vdd_device::DeviceState; + +const MAX_LINE: usize = 1800; +const MAX_ADAPTERS: usize = 8; +const MAX_TARGETS: usize = 16; +const MAX_NAME: usize = 48; + +/// The virtual display's monitor reports the driver's EDID ID in its path. +const VDD_MONITOR: &str = "MTT1337"; + +/// A DXGI adapter. +#[derive(Debug, Clone)] +pub struct Adapter { + pub luid: u64, + pub description: String, + pub vendor_id: u32, + pub device_id: u32, + pub driver: String, + pub outputs: u32, +} + +/// One display target from `QueryDisplayConfig(QDC_ALL_PATHS)`. +#[derive(Debug, Clone)] +pub struct Target { + pub adapter: u64, + pub id: u32, + /// Source adapter and id, for an active path. + pub source: Option<(u64, u32)>, + pub available: bool, + pub technology: i32, + /// Only to tell the virtual display's monitor; never logged. + pub monitor_path: String, +} + +impl Target { + fn is_vdd(&self) -> bool { + self.monitor_path.to_ascii_uppercase().contains(VDD_MONITOR) + } +} + +/// The adapters the driver can be pinned to, in DXGI order, without the +/// virtual display's own adapter. While the virtual display is on, DXGI +/// lists that adapter under the name of the card rendering it, which would +/// otherwise read as two cards with one name. +pub fn render_adapters(adapters: &[Adapter], targets: &[Target]) -> Vec { + let vdd: Vec = targets + .iter() + .filter(|t| t.is_vdd()) + .flat_map(|t| std::iter::once(t.adapter).chain(t.source.map(|(adapter, _)| adapter))) + .collect(); + adapters + .iter() + .filter(|a| !vdd.contains(&a.luid)) + .cloned() + .collect() +} + +/// The card the driver renders the virtual display on, as DXGI shows it: +/// the driver's own adapter is listed under that card's name. It is the +/// adapter holding the virtual display's path, or, before a monitor +/// arrives, the one adapter that was not there before the Enable +/// (`before`, by LUID). `None` when neither tells. +pub fn driver_render_card<'a>( + adapters: &'a [Adapter], + targets: &[Target], + before: &[u64], +) -> Option<&'a str> { + let on_path = targets + .iter() + .filter(|t| t.is_vdd()) + .flat_map(|t| std::iter::once(t.adapter).chain(t.source.map(|(adapter, _)| adapter))) + .find_map(|luid| adapters.iter().find(|a| a.luid == luid)); + let new = || { + let mut new = adapters.iter().filter(|a| !before.contains(&a.luid)); + match (new.next(), new.next()) { + (Some(adapter), None) if !before.is_empty() => Some(adapter), + _ => None, + } + }; + on_path.or_else(new).map(|a| a.description.as_str()) +} + +#[derive(Debug, Clone)] +pub struct Snapshot { + /// The device state, or why there is none. + pub device: Result, + /// The graphics card the host asked the driver for, or why it asked for none. + pub pin: Result, + /// The card the driver renders on (`driver_render_card`). + pub render: Option, + /// `HwSchMode`: 2 hardware-accelerated GPU scheduling on, 1 off. + pub hags: Option, + /// Whether the driver reads its settings from `C:\VirtualDisplayDriver`. + pub default_settings_path: Option, + pub adapters: Vec, + pub targets: Vec, +} + +/// Printable, without the separators this record uses, and short. +fn clean(text: &str) -> String { + text.chars() + .filter(|c| !c.is_control() && !matches!(c, '"' | '|' | '[' | ']' | '=')) + .take(MAX_NAME) + .collect::() + .trim() + .to_string() +} + +fn luid(value: u64) -> String { + format!("{:x}", value) +} + +/// The record's text, from data already read. +pub fn line(snapshot: &Snapshot) -> String { + let mut out = match snapshot.device { + Ok(device) => format!("device={device}"), + Err(reason) => format!("device=unknown({})", clean(reason)), + }; + match &snapshot.pin { + Ok(name) => { + let matched = snapshot + .adapters + .iter() + .any(|a| crate::vdd_settings::same_card(&a.description, name)); + out += &format!(" pin=\"{}\" pin_matched={matched}", clean(name)); + } + Err(reason) => out += &format!(" pin=none pin_reason=\"{}\"", clean(reason)), + } + match (&snapshot.render, &snapshot.pin) { + (Some(card), Ok(pin)) => { + let same = crate::vdd_settings::same_card(card, pin); + out += &format!(" render=\"{}\" render_is_pin={same}", clean(card)); + } + (Some(card), Err(_)) => out += &format!(" render=\"{}\"", clean(card)), + (None, _) => out += " render=unknown", + } + out += match snapshot.hags { + Some(2) => " hags=on", + Some(1) => " hags=off", + Some(_) => " hags=other", + None => " hags=unset", + }; + out += match snapshot.default_settings_path { + Some(true) => " settings_path=default", + Some(false) => " settings_path=other", + None => " settings_path=unknown", + }; + + let adapters: Vec = snapshot + .adapters + .iter() + .take(MAX_ADAPTERS) + .map(|a| { + format!( + "{} \"{}\" {:04x}:{:04x} driver={} outputs={}", + luid(a.luid), + clean(&a.description), + a.vendor_id, + a.device_id, + clean(&a.driver), + a.outputs + ) + }) + .collect(); + out += &format!(" adapters=[{}", adapters.join(" | ")); + if snapshot.adapters.len() > MAX_ADAPTERS { + out += &format!(" | +{} more", snapshot.adapters.len() - MAX_ADAPTERS); + } + out += "]"; + + let targets: Vec = snapshot + .targets + .iter() + .take(MAX_TARGETS) + .map(|t| { + let vdd = t.is_vdd(); + let state = match t.source { + Some((adapter, id)) => format!("active src={}/{id}", luid(adapter)), + None if t.available => "inactive available".to_string(), + None => "inactive unavailable".to_string(), + }; + format!( + "{state} tgt={}/{} tech={} vdd={}", + luid(t.adapter), + t.id, + t.technology, + if vdd { "yes" } else { "no" } + ) + }) + .collect(); + out += &format!(" paths=[{}", targets.join(" | ")); + if snapshot.targets.len() > MAX_TARGETS { + out += &format!(" | +{} more", snapshot.targets.len() - MAX_TARGETS); + } + out += "]"; + + if out.len() > MAX_LINE { + let mut end = MAX_LINE; + while !out.is_char_boundary(end) { + end -= 1; + } + out.truncate(end); + out += " ..."; + } + out +} + +/// Read the adapters, display paths and settings now. `before` holds the +/// adapters' LUIDs from before the Enable. +#[cfg(windows)] +pub fn collect( + device: Result, + pin: Result, + before: &[u64], +) -> Snapshot { + let settings_key = r"SOFTWARE\MikeTheTech\VirtualDisplayDriver"; + let path = crate::diagnostics::registry_string(settings_key, "VDDPATH"); + let (adapters, targets) = (adapters(), targets()); + Snapshot { + device, + pin, + render: driver_render_card(&adapters, &targets, before).map(str::to_string), + hags: crate::diagnostics::registry_dword( + r"SYSTEM\CurrentControlSet\Control\GraphicsDrivers", + "HwSchMode", + ) + .parse() + .ok(), + default_settings_path: (!path.starts_with("unavailable")).then(|| { + path.trim_end_matches('\\') + .eq_ignore_ascii_case(r"C:\VirtualDisplayDriver") + }), + adapters, + targets, + } +} + +/// `driver_render_card` read now. +#[cfg(windows)] +pub fn render_card_now(before: &[u64]) -> Option { + driver_render_card(&adapters(), &targets(), before).map(str::to_string) +} + +#[cfg(windows)] +pub use windows_impl::{adapters, targets}; + +#[cfg(windows)] +mod windows_impl { + use super::{Adapter, Target}; + use windows::core::Interface; + use windows::Win32::Devices::Display::{ + DisplayConfigGetDeviceInfo, GetDisplayConfigBufferSizes, QueryDisplayConfig, + DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME, DISPLAYCONFIG_MODE_INFO, + DISPLAYCONFIG_PATH_INFO, DISPLAYCONFIG_TARGET_DEVICE_NAME, QDC_ALL_PATHS, + }; + use windows::Win32::Foundation::{ERROR_SUCCESS, LUID}; + use windows::Win32::Graphics::Dxgi::{CreateDXGIFactory1, IDXGIDevice, IDXGIFactory1}; + + /// `DISPLAYCONFIG_PATH_ACTIVE`. + const PATH_ACTIVE: u32 = 1; + + fn value(luid: LUID) -> u64 { + ((luid.HighPart as u32 as u64) << 32) | luid.LowPart as u64 + } + + fn text(wide: &[u16]) -> String { + String::from_utf16_lossy(&wide[..wide.iter().position(|&c| c == 0).unwrap_or(wide.len())]) + } + + pub fn adapters() -> Vec { + let Ok(factory) = (unsafe { CreateDXGIFactory1::() }) else { + return Vec::new(); + }; + (0..) + .map_while(|index| unsafe { factory.EnumAdapters1(index) }.ok()) + .filter_map(|adapter| { + let desc = unsafe { adapter.GetDesc1() }.ok()?; + let driver = match unsafe { adapter.CheckInterfaceSupport(&IDXGIDevice::IID) } { + Ok(version) => { + let v = version as u64; + format!( + "{}.{}.{}.{}", + v >> 48, + (v >> 32) & 0xffff, + (v >> 16) & 0xffff, + v & 0xffff + ) + } + Err(_) => "unknown".to_string(), + }; + let outputs = (0..) + .take_while(|&index| unsafe { adapter.EnumOutputs(index) }.is_ok()) + .count() as u32; + Some(Adapter { + luid: value(desc.AdapterLuid), + description: text(&desc.Description), + vendor_id: desc.VendorId, + device_id: desc.DeviceId, + driver, + outputs, + }) + }) + .collect() + } + + /// Every active path, then each available target no active path uses. + pub fn targets() -> Vec { + let (mut paths, mut modes) = (0u32, 0u32); + if unsafe { GetDisplayConfigBufferSizes(QDC_ALL_PATHS, &mut paths, &mut modes) } + != ERROR_SUCCESS + { + return Vec::new(); + } + let mut path_info = vec![DISPLAYCONFIG_PATH_INFO::default(); paths as usize]; + let mut mode_info = vec![DISPLAYCONFIG_MODE_INFO::default(); modes as usize]; + if unsafe { + QueryDisplayConfig( + QDC_ALL_PATHS, + &mut paths, + path_info.as_mut_ptr(), + &mut modes, + mode_info.as_mut_ptr(), + None, + ) + } != ERROR_SUCCESS + { + return Vec::new(); + } + path_info.truncate(paths as usize); + let monitor_path = |path: &DISPLAYCONFIG_PATH_INFO| { + let mut name = DISPLAYCONFIG_TARGET_DEVICE_NAME::default(); + name.header.r#type = DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME; + name.header.size = std::mem::size_of::() as u32; + name.header.adapterId = path.targetInfo.adapterId; + name.header.id = path.targetInfo.id; + if unsafe { DisplayConfigGetDeviceInfo(&mut name.header) } == 0 { + text(&name.monitorDevicePath) + } else { + String::new() + } + }; + let mut targets: Vec = Vec::new(); + let (active, inactive): (Vec<&DISPLAYCONFIG_PATH_INFO>, Vec<_>) = path_info + .iter() + .partition(|path| path.flags & PATH_ACTIVE != 0); + for path in active { + targets.push(Target { + adapter: value(path.targetInfo.adapterId), + id: path.targetInfo.id, + source: Some((value(path.sourceInfo.adapterId), path.sourceInfo.id)), + available: path.targetInfo.targetAvailable.as_bool(), + technology: path.targetInfo.outputTechnology.0, + monitor_path: monitor_path(path), + }); + } + for path in inactive { + let (adapter, id) = (value(path.targetInfo.adapterId), path.targetInfo.id); + if !path.targetInfo.targetAvailable.as_bool() + || targets.iter().any(|t| t.adapter == adapter && t.id == id) + { + continue; + } + targets.push(Target { + adapter, + id, + source: None, + available: true, + technology: path.targetInfo.outputTechnology.0, + monitor_path: monitor_path(path), + }); + } + targets + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn snapshot() -> Snapshot { + Snapshot { + device: Ok(DeviceState::NoMonitor), + pin: Ok("Intel(R) Iris(R) Xe Graphics".into()), + render: None, + hags: Some(2), + default_settings_path: Some(true), + adapters: vec![ + Adapter { + luid: 0xd2c1, + description: "Intel(R) Iris(R) Xe Graphics".into(), + vendor_id: 0x8086, + device_id: 0x46a6, + driver: "32.0.101.7092".into(), + outputs: 1, + }, + Adapter { + luid: 0x1_0000_e5f0, + description: "NVIDIA GeForce RTX 3050 Ti Laptop GPU".into(), + vendor_id: 0x10de, + device_id: 0x25a0, + driver: "32.0.16.1742".into(), + outputs: 0, + }, + ], + targets: vec![ + Target { + adapter: 0xd2c1, + id: 4352, + source: Some((0xd2c1, 0)), + available: true, + technology: i32::MIN, + monitor_path: r"\\?\DISPLAY#BOE0A1C#4&2b5bd9a1&0&UID8388688#{e6f07b5f}".into(), + }, + Target { + adapter: 0x2_0000_0101, + id: 256, + source: None, + available: true, + technology: 5, + monitor_path: r"\\?\DISPLAY#MTT1337#1&28a6823a&3&UID256#{e6f07b5f}".into(), + }, + ], + } + } + + #[test] + fn the_snapshot_is_one_line_with_each_part() { + assert_eq!( + line(&snapshot()), + "device=no_monitor pin=\"Intel(R) Iris(R) Xe Graphics\" pin_matched=true render=unknown hags=on \ + settings_path=default adapters=[d2c1 \"Intel(R) Iris(R) Xe Graphics\" 8086:46a6 \ + driver=32.0.101.7092 outputs=1 | 10000e5f0 \"NVIDIA GeForce RTX 3050 Ti Laptop GPU\" \ + 10de:25a0 driver=32.0.16.1742 outputs=0] paths=[active src=d2c1/0 tgt=d2c1/4352 \ + tech=-2147483648 vdd=no | inactive available tgt=200000101/256 tech=5 vdd=yes]" + ); + let mut unpinned = snapshot(); + unpinned.pin = Err("two graphics cards have the same name"); + unpinned.hags = None; + unpinned.adapters.truncate(1); + let text = line(&unpinned); + assert!( + text.contains(" pin=none pin_reason=\"two graphics cards have the same name\""), + "{text}" + ); + assert!(text.contains(" hags=unset")); + // An Enable that failed without a device state says why. + let mut unknown = snapshot(); + unknown.device = Err("display tasks missing"); + assert!( + line(&unknown).starts_with("device=unknown(display tasks missing) pin="), + "{}", + line(&unknown) + ); + let mut unmatched = snapshot(); + unmatched.pin = Ok("Intel(R) UHD Graphics".into()); + assert!(line(&unmatched).contains("pin_matched=false")); + // The driver compares names ignoring case. + let mut cased = snapshot(); + cased.pin = Ok("intel(r) iris(r) xe graphics".into()); + cased.render = Some("Intel(R) Iris(R) Xe Graphics".into()); + let text = line(&cased); + assert!( + text.contains( + "pin_matched=true render=\"Intel(R) Iris(R) Xe Graphics\" render_is_pin=true" + ), + "{text}" + ); + let mut elsewhere = snapshot(); + elsewhere.render = Some("NVIDIA GeForce RTX 3050 Ti Laptop GPU".into()); + assert!(line(&elsewhere).contains("render_is_pin=false")); + elsewhere.pin = Err("no adapter drives the main screen"); + assert!( + line(&elsewhere).contains(" render=\"NVIDIA GeForce RTX 3050 Ti Laptop GPU\" hags=") + ); + } + + /// The driver's own adapter carries the render card's name: found by + /// the virtual display's path, or as the one adapter new since the + /// Enable when no monitor has arrived. + #[test] + fn the_render_card_is_the_drivers_adapter() { + let card = |luid, name: &str| Adapter { + luid, + description: name.into(), + vendor_id: 0, + device_id: 0, + driver: String::new(), + outputs: 0, + }; + // mainpc with the driver named to its AMD iGPU. + let adapters = [ + card(0x11e00, "NVIDIA GeForce RTX 5080"), + card(0x13c3b, "AMD Radeon(TM) Graphics"), + card(0x66f8f4a0, "AMD Radeon(TM) Graphics"), + ]; + let mut vdd = snapshot().targets[1].clone(); + (vdd.adapter, vdd.source) = (0x66f8f4a0, None); + let before = [0x11e00, 0x13c3b]; + assert_eq!( + driver_render_card(&adapters, &[vdd.clone()], &[]), + Some("AMD Radeon(TM) Graphics") + ); + // No monitor yet, so no path: the adapter that is new. + assert_eq!( + driver_render_card(&adapters, &[], &before), + Some("AMD Radeon(TM) Graphics") + ); + // Nothing new, nothing known before, or more than one new: no telling. + assert_eq!(driver_render_card(&adapters[..2], &[], &before), None); + assert_eq!(driver_render_card(&adapters, &[], &[]), None); + assert_eq!(driver_render_card(&adapters, &[], &[0x11e00]), None); + } + + #[test] + fn the_virtual_displays_own_adapter_is_not_a_second_card() { + // mainpc with the virtual display on: DXGI lists its adapter under + // the RTX 5080's name, and its path runs on that adapter. + let card = |luid, name: &str| Adapter { + luid, + description: name.into(), + vendor_id: 0, + device_id: 0, + driver: String::new(), + outputs: 0, + }; + let adapters = [ + card(0x11e00, "NVIDIA GeForce RTX 5080"), + card(0x13c3b, "AMD Radeon(TM) Graphics"), + card(0x66f8f4a0, "NVIDIA GeForce RTX 5080"), + ]; + let mut vdd = snapshot().targets[1].clone(); + (vdd.adapter, vdd.source) = (0x66f8f4a0, Some((0x66f8f4a0, 0))); + let monitor = Target { + adapter: 0x11e00, + source: Some((0x11e00, 0)), + ..snapshot().targets[0].clone() + }; + let cards = render_adapters(&adapters, &[monitor.clone(), vdd]); + let names: Vec<&str> = cards.iter().map(|a| a.description.as_str()).collect(); + assert_eq!( + names, + ["NVIDIA GeForce RTX 5080", "AMD Radeon(TM) Graphics"] + ); + assert_eq!( + crate::vdd_settings::gpu_to_pin(Some("NVIDIA GeForce RTX 5080"), &cards), + Ok("NVIDIA GeForce RTX 5080") + ); + // Two cards with one name, neither known to drive a screen, still + // are not pinned. + let twins = render_adapters(&adapters, &[monitor]); + assert!(crate::vdd_settings::gpu_to_pin(Some("NVIDIA GeForce RTX 5080"), &twins).is_err()); + } + + /// Prints the card the driver renders on while the virtual display is + /// on: `cargo test -p eternal-host --lib native_render_card -- --ignored` + #[cfg(windows)] + #[test] + #[ignore = "reads this PC's adapters and display paths"] + fn native_render_card() { + println!("render={:?}", render_card_now(&[])); + } + + #[test] + fn monitor_paths_and_odd_names_stay_out_and_the_line_is_bounded() { + let mut odd = snapshot(); + odd.adapters[0].description = + "Card \"one\"\n| [x]=y with a very long name that keeps going and going".into(); + let text = line(&odd); + // Paths hold monitor serial-style instance IDs. + for secret in ["UID256", "UID8388688", "BOE0A1C", "28a6823a", "#", "\\\\?"] { + assert!(!text.contains(secret), "{secret} in {text}"); + } + assert!(!text.contains('\n')); + assert!( + text.contains("\"Card one xy with a very long name that keeps goi\""), + "{text}" + ); + + let mut many = snapshot(); + many.adapters = vec![many.adapters[1].clone(); 40]; + many.targets = vec![many.targets[1].clone(); 200]; + let text = line(&many); + assert!(text.len() <= MAX_LINE + 4, "{}", text.len()); + assert!(text.contains("| +32 more]"), "{text}"); + } +} diff --git a/host/tests/fixtures/README.md b/host/tests/fixtures/README.md new file mode 100644 index 00000000..767c2c01 --- /dev/null +++ b/host/tests/fixtures/README.md @@ -0,0 +1,7 @@ +# Test fixtures + +`vdd_settings-25.5.2.xml` is the default `vdd_settings.xml` from +[Virtual-Display-Driver](https://github.com/VirtualDrivers/Virtual-Display-Driver) +25.5.2 (`Virtual Display Driver (HDR)/vdd_settings.xml`), byte for byte. It is +MIT licensed by its authors. `host/src/vdd_settings.rs` tests that the host +edits this file in place. diff --git a/host/tests/fixtures/vdd_settings-25.5.2.xml b/host/tests/fixtures/vdd_settings-25.5.2.xml new file mode 100644 index 00000000..d70a01d7 --- /dev/null +++ b/host/tests/fixtures/vdd_settings-25.5.2.xml @@ -0,0 +1,97 @@ + + + + 1 + + + default + + + + 60 + 90 + 120 + 144 + 165 + 244 + + + + 800 + 600 + 30 + + + 1366 + 768 + 30 + + + 1920 + 1080 + 30 + + + 2560 + 1440 + 30 + + + 3840 + 2160 + 30 + + + + + + true + false + + false + + + + + + + false + false + RGB + + + + true + + 128 + + 128 + + true + + 2 + + + + + false + + false + + false + + + \ No newline at end of file diff --git a/host/tests/snapshots/feedback-panel-long-minimum.png b/host/tests/snapshots/feedback-panel-long-minimum.png index d11560f1250e0e5ae6a712f743368ded6ee4349a..9d4a77c953f5474f3338bbf1a8903597d484a192 100644 GIT binary patch delta 35107 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

KHBNl*ll@7hfbDrPvkX&u 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 z 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௚n1(9`HoarmM+O66N}Dx`=6 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 delta 97860 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 s`;L|`>F)ZA#o5C$N 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%0D2J!x37<3G+w2g#GU`q`Vz(k{ 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%0DK^$LP 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 delta 114029 zcma%kd0dqB_W#VFlDSNZin5J6nG3FfvW$JTxMoUaX}N$)N#=q`%03Tjisph!`ltw0 zW>$t|hDa;}vWYCCxuCMlu#UkbA9Jno3)qS!d?m=~&XRXq zZLdG#_UqNFc}YD_leD`U`H zpw2aLKu!^3D$*Sn8HYrwt(oab7Gsj^&}R-l*`1rEM`O(oV;fr@U5#w5kT~*G9e%1} zvtQi=zdftArSIcbZ7UX#Z!pAKLcO))#CwE23Sm=FY=i?ZttNbOVEdba19SW>9sWUi zgWR^`6s)o4&VoFZU3!D>!hF4hzSfb{(4DR7HnjU%KCi(t#0OeB1C^;Y{Pr_P?tfOWEUD{#9wTWAkerfrXUgi|jSU|k zXetcslM|o)G+z6CVAuDax0^G|H|v72v8Kh6?lk6*b+|Szsg(A0=FxG1+KGX;;J;8=Gn2fs285wN|i_r|fk(T>);}=;J&scWMkrrqj)a=-A(nmbKns_Ln8!Xj)#ID~#9e4-DGlYupoW zxR#O~6KH-G;cQ@{Z(+Z7-e*pzOy#P&W!{a;LUF0w5)Jvzzow@b$65@&>iKNUeu3jE z*^;3yUpDS2Hh;gV&i_(sq4`wF1X=3}relTikkl~1>50r@atVDm@{VoP5T5O{)e8FI zQ0y~*{zA1aqfwso3_Fq0_ItdvJ3~tQGt8|fbli~LE&ia?5@v0&!>msUK{^(NtIpk z;n@azQR(E&$F7z;(-K8Sv)oe_r60&!9lS<%N!m7P>A7bQtENS^2UOj>l+`vk$@ERy z4!xt5C1%L;sCMT2gVLU6Y3&$c*h?aPkSJy=Zg%#U*5R@G(APtjCd?}cU^r&>+Dszo0@1Tt{d#r#zjM)}w z*dKV4LWTO?MemapjM+_bJT`JzcXxLtf`yIcMDGK*?|e2}-o8X$>K1j^jpE$Z%$nvb zr^?DoF0Rq?=h1cXvvkO#eY~~rwy0FPa+PVRN$X;HnMe2FhMDfIeYCjys#uwBzVB}y z&YR(4{==orye++PL}L6fPqEZgA{FG&9o{b0OH}%P3RzE!EWP&>{&cB_xz1J#1dEwI zbih8a9L#QO&K|rdv!x_^pfA&PHyxos_<4702v0pkq^XT8NsAo7;8NzTzrs`O6&v;% z5DTMtwD>1tO*dn|Zk{L6Ix{Ddlb7_=ih2?$rqmZnT73A1d%ot`J*kLPJ-o`y>dLIC zBHav8S)NnpbidP5hE7{&WuUUrudUILEbm&9jjtR^>Ha;Xah)(?BtncOM_SaHCg&lVINs5-N3oQO#}UnC;fA| z>x-Xi-4Yn6yI$mV)V6@)_0?1+wb&mEsO3JkE5$O;#iDhQ1)3iOYT~8F_;YiE_fvjU zWME&6l>M%yrG-`1I~CET531anrrF`5-(l)eS)7ou#AWo1S63z*ngV+^@@B^Qh`Zj+ z#9^95Yx+noSI+UPzi-?hXb8uaNZ6H=8R2gk871SKG~2z6apHp&lGci*-8e%%N4ex3 zkMQJIaKO_RjBaeDP0 zQ+sSx-m2`vB<&&bP7$x7`nHa~w?{bcI-P?q-}ZP0FX`au$X9#Gby=Qia#NYSfj+Y_=+9ZFNygbufDfJR}ZV;DqvAwO)?{G? zpM*GaD7O20tTMa1t=_*`tv*J((3pSFCbG0Ba$sP`^1uRV!yvi($S4Dr*?wELaigDZ zj(;3uh+!1fHLcY~J^R5C!5Bv8DAzc0ktT1t`dLcPv#Kb4wdp~i*;Tgn>_l3T6V6^w z7iBhW&hD3@u}dkh7a=z`ZSX#2TY%$Nq**5JxLdk0xaP@`nq$Gra;-*W>0cG5OnCcl zKK9B-`<=7b!HUST3I**lO1I1g@stYSK%PM!Y zRn6sLqa+Ch>vJzgJ-#SV|CrRZ>6}N%QMZVzZs$`Algy8jH1ly%<+r8}jC>L!d6i;S zNpJ+);>36I=-oMNpCw9UxKp8dIptaz^PSk#Iv}zk!t9tg&&UpJZWt$TRvGf-*}NQz zmmXK{3FnyW%BPPex5HaTp&u%P3bXS z;%&b!56xn`JUXY%=^2;c9&|QCm7QRD`A|VW{iwE)CuI3n zb70*!u~iEHX8O0f#xk)@pCP?g#QoIJzbys#J6Mmm`6A|dva;^}DW&U8r>@&En)$I| zrH6DU{f&c6K3xD_o;CU?@d0ILdQx{m=3S304~vt|c#cwXZHd&>Y`B$T>X)q;kbQWC zq;W+(^Cjhh$i@{CD@*0pK$ZFf|BfS(ZA!@unGE=TMeheMuCbWKU-)+a#>i^Ld6TpL zxTFQvN#!lr8f||U#ii0$z!{-kYtAXYli#Fq!oBl zH}wxbUi({_R!7PU$5Xm~3Ty!Kwl~u2cq2R};eMpm$?GO(*ENToVZ-Jk^VKA|81}f- z%j@ro&AL#z-aOjx=}EtnF5SPTG|KA{))yFl^X+yZ0A(2&!P?0?=jd_ZKJf1z@89j^ z^166H&jj-n!(&xgf4`1Y-yPCGru$jM!;E_d@qu3W^nt8?fw=wr^`^7UhTmQ`1&CivA|c+%}uX$t8=h+5osz222K z_GMwj!*LHEK2)gH(UzrDcN9c=EAi^K*PmHpsMe}|@?`ljgfVHo^zmM&_O*oHPgp+n z@A$y~Ms3ewB(n3b=pCj#9CCx>qa^a~*1ZrF} zyDmvb02+6;M1A#M=<7?9yvvkK4}0axmE77|zT9G#m!${m*`HtIj~U+XxU%h z5!jCE%2uBDepa8}_Pag6d!}v|VXspi;brWdy?h%dDv;DQDl>meplbTS&64Ic$zIAO zJOvQot8Dm5fgStD5W%oF8vOrwoR9iol?EB3tV%VgMsZ}**0JC6-g&DLyFV_y@sza{ zA+s|C+ZwRM%1%^#-*`TIH@5MTv}^I)h)!>I`w+o!1~8y7^TFnHK z+pIlr14%EY64WWO>mnoYH|+E0=UkLL3y3>I)wR2Dc9P79_v~{yy=i4j!$IFjfxL68 z{ANjJX^F;3rO753<>eb$SD#L2vB9@zpv1^BiE_gce>$SQ54rkpPnoTvPYk`B(bjKf z91vszU};LDl~|n@_GE#mU(SlC`)*obUgs(m>LGeACwW)0{3i0Co|pOXP*xxAq6b7h zxcEz2$U(N|E38<~UH(W$)*4l#vt>#uH{GGYLB zWH8mb`wrQ<{MVI~nX6oyaCDo1*xZ$}r23Xe-2`*5J~~`q1)xdm*hM|l!B zP%x5RhF9lD#{H2!)jo8aNBCd>`!kL4 z(m6c*Hu+7XPHAZL@9E3qA|D(6BfHMQTB37f5(q!`;f^CtmMEvQb8y}-O;iLM5byi4 zbH3oO=r0FqDcLJBy~4^S^6geCJOAyH@D3{YUv<|JR?@3g4vc9YM!>tr+W7$K_{4tA=tGzDz_S{}&-0dGei~ykMX>Dvx zLy5Gt!0?-kWW3Z8uS{1Z@}oCjZVv0H@G3aC%u)EcY>gF_URq{kZf^m4g*_a@x8u)X zeGl**1419MQDJ`io$_cfH;cGKed+#}^+mC|J)SM=ViiC7M$M!{n6o3j!OB~)zH4-Ui&C)HhI<5N2ME^R`Cp9rWnRb)YejKR(_vv zA2+(ijtOv$*PsjuI$VD$tKzIN@J;b4+s2;sGTRLj>J!&)}@pJPK0vUSdqIv+z}pvew(Dy4|tA3+saCNq`# zs&UJzDIpBYtjx%A(`RT3;|*t1j5{RQs-tYu6u!N$F|s^H^28??#V(?Cg%(lvBAxC@ zy=jv`ZH&l#N7R54I{+cusSqqjv(6>#kGRB~^CI)ewiTO;mbg{lh)Znm)25HxoM*=z zR(7J&DEI8T<5CDp#cfA-Ynlefv)Dl_IVbA+M%9RXeUX^CJddoq8F%zvR1}t@x}j98 zDe!FF8+jGco`h9Jq4m~1wwJ3Xlh1^+-* z`%+b*zuDowzxsNS*-NGK7R{e@l4>kaD{W2S64+2S$c67Pi~IR!_cjrsvj?=9stH$9 z3EZA#{5_@n<;2e4F75zq*3J;=z5#fb2=yLpvFK_u;zy zyi>E~o6c^&+&HzSZYz)b{dSWJ%}ypo47Wd#1uG(l@bJsLA|fJs<15I< z(7kS<8=Q2fZIy9lPiNa-Nlh&S?PUGy>S@NKD#J!6i-A#qI&=qq@*OU4m;^vF8Hh&L z>RH=ARyoICw=)zR={P!uEFG*ST;&fQ)zs9oD{KTJR?oK@%?d{drnu(?M0<@a za$|zLp(MLyz*BavjlgL(@myim6;W1+UH6GpinIpq~UcNXR&}K+9s=WTR*Iq6+YigY29c$z-=Tt;huh4Q3 zI)8EIY>M|ch*l1gT8j;1h|QZQYh5Mnun)^~i(KZ1yKyGgc-nV;mYzVRIcp12ysDiQ z2}I6#@<0Wo+*}~)+Rn-!^GD71Ok1@r>=hnmL<)(NwQdn6eCx;6!DXqYDw%m0(LH^0 zYHB7>I1iT65vm;}3!O)#!<($wNifn1356B=^Bugca{%O3I)oT!US{FuOxM`(dmD%| z+39zhFj3u%*svh(al!J78wmk%l)jad`H8I+d(@S0ZyzT%p085v^w$px1@at30U*}d zvo`3f_vW6sY3oP`a_A5+PQkcQ>aFt5C7JgYXAL~T(R%FKO1{0V!b7TCWyq9uzvtb3 zReG#oeff&W0(`ue8{d99Xo^Jyek3SPz9fo7Kw%!-@p&U_GA3F2r$IPxdCkf@hQf$E_tqmwv6ApbiDkSg7Op&G$H zi=JoFh4*`y7Fo|}O6av0{JTEkAInc;y4?UH!id<`BIobSgr+e5#DSNx!7`lNyElp$ zuA?ztEyougQ10q#A2l;BSlLz&xaS#Cz0(<}r`rrdXfSviro<*u9h%*~Sb7$}TEsnC z4`_@%2&NTHpd3=upIUt%{~-Z@4uA@O*wkrc1l^?&BUuu5q|zZo>FWXsT0?x z`geC+mlHxF>OYQxOy3Olsz@+7ZK4N%UZ0QXEB>9y{!v1)X-z`q)QcA1cR1FwOXzxZ9BAQ`@@0wI@ojp%+D|`M zZ04~&8$ew{T@lxx^n3b&krI4Mg9K?4=OCo^)T(W@k`oWBsv7#HBmbw%aUG-N!IfKO zYqR`fsqq5*nV zGyqpt@Iy;C_#IX%+1L&I(QDSl|K&eWE^_LAXsfhl)DdA{gQj23Rnv4Aik7yet)3S4 zFwZHMBL|S>8Xp#uPGIpPeWYwv*8R#LU0K@pmWMMW?LJJjbvS#wztu=4Sc)oy4xUrz zFFslv?OduehNe~Tvge7-cg5w&Y2HV=A2s1rp0vT$(%1 zDm(GCV?c0Hns~Yz$CJ=nP{~x`Mz-oz!Dzmo{b?b8j39G*+-lh<#_w2i^49U|?4xmY z)Q1B+6Rg9r)aQ^b-0EmW4T$&4sF*z`Gmh<<5Km(8Z+*O{iJ|&&UEi8^*jQ ztlv%GlhV3YrP&pzU&@uI_Svt9*x|4E9XpKsvBsh!NEGJUR#@2A4-R_((jHx_%T{=NxgX z$u|RT1N|U!AK6fJG+3GzkrBdLtrd)7{^Ml<2_1f?WzA=%2I0ASXGY<$!W=WUmzx%L zf|=dTg=O&e31PSQI+cRy&(J^Q{EZSI~ zT=Q(}kEuJ%9d25vt(wJL3}+xw+>Fdt0N5|!@Usuo?mYIOZ38%L5}?JglJ*V81F?n= zS8g_;W}U<1E=HM~_y921H-x#o0U7sC6^Kxs22K&!$8G<^o*|NA<`NrG$GajKU2-+3 zv~5rnmzbdYPI4)oeWazWP5WkG`}diTFJ(<6q3G7)%>2b#(>d>`{ZMCw3TQZ~A5M#f zXuodcr(5f)(yww=1nzB09!DR5bx3^Jlx{AQn8b;fjA#GDgslK&2yQx_{ z(?z#~Xh!F2{Og}T9Sarg-edl;Go>46wIL+wE2|=PdqY8V+~S~*35>-4bsulu9M91N zpd0$6j_{uuR}p%oqAt(7<-9x2a(R+?Kn}u?@N=YQ2xa1EO1A6MJ=Z(<>|dww|MkNu zBljQ2MX#xKtl5V&KIGoY(^M&);5aI$jvtEDP-k-S3;^+xLve|QQT)^F?rDON0#9*V z_8nV6iiWj+P2j+ux*~AwkNkK!t24YOmA(BIe>fZXCf}AxuZ~c8ef1ty4E|b*>mR$; zJWH-QwQ_QWKQm5bJdT3`E=e1>vo3$pOq=Ndy@92~Uh%i%#H#7&6Nb%1E*|Hio9R*n z&=sMsLaZk3qzEd6$2*%FL1LATzp9!4Z^xSHJ=YvK`S%r&6vZYf*2p`uD36>Y^=`x2 zi9KMwPLzJ^8qeSUpB{;){*gprEcp(8_2sfI?nSV^qdMzWvmSnbxcvIi7TX7sn z^#j2Y4+i`W#3|Eo5X{Uj7c!SKSMwG0!B1ImFb>+pn%uqXyqUXkXB zk9hxz@Cz&YDl)R0E3YdPGx*nmqry-i+Kk;pGUW|dSu2Dk8Qcv6x!BD`S^7BwpkNqM zE~FO1b0FV=+^7bX{TX+Q&lKw;Wof~hy1q{O>r&-@`)v(Kk&U4jR)i0qw&rnTHGtjC)X3VnQHBIiOB ze2quDcwN55wSK3cvI8ZmaS67TSadMH&4y-a&j3<8_s4Z9(T1Nld!uV|oRJB(N@>;=m)tf;(?gb|z~DdTWn zo)!1X$lq>_#6}P)9L8`w7z)F9!V5NH%{U@l$2NXGw&AHS zV@JmxZv$GMPP-k6X(3;7OD~}uc!PjAyevh*07@Dz6F?by8PTah z6Kk~iW#(*()lY+x8o<1-ddIjIo_G}#l9_Fxs7-9=-}i3^)5j-J_Pu{#c}YnuN|(Z42xS{g*=A^$s9&B z{LTLokY-Xu;uN=tw)A*p+}ccR^vpBGs1-wqX#g)526b}|$I)d&>spdb9}saTfO(b(n4ruOG9$HMK$mz2hV3Y?gkYL;XuB-0Q}P zwPsVqzkCFwICzSnb_7BR#UVodG%oR=g3E%ICV=x>rQMV zE}%W(6L{~k4^cVGuz&yd-tWd!iu=5f`A4N4ZsAApS@l)^O|REDMccA_={fhZzliWF z%uI-;Zf;NPH@)uD_yP9 z&Q4cwv4492i_QCZVc3pr=!h~OC3Sq{nMQU&lHM+nD!ZtrZfeaQaEw~AIg!dSEl@Ke zu)H|V`J}wo+q);148?wdR`F_(m7-)`4d8u^9qanT*g?_4Ve<8C)&% z-1_&EW!&xZ@*p^O;&wh7&R9OBG&QQ|IAQsw-H69U$cD{2#MTd>a}W~<#awq4c3U-l z4W^Yf1c2tGbgWD%C@kzp@frx7Tx@@=tcO6jp0PyRMqDXEdiluJ&CLj|%!Z<|)gpj> zp%Q7U7PUqQ=aq_LK-1ZA=28lE^8 z_cleBLV$@9n;{=hlx=00qzV9rz6x?6amFBk>LPW%vp3jaLN#GX<|0D%XURv$N5?JYw2?L!JcU;(EHYo?C134!K$QO-3HwhDtGe8ST&U5G0nphQ60VDq+}(kWwsA zkt#D@m>In;9=L%ASQ&p^m7VdhvhtIW4|<;tw8QkDA5~U10#AOMda$*%e&Mhl;R1H` z2YmYlTxRJ_FO#}a)w1;N>?J=h_kp;9qGxVfT6%g} zVd04G?hI3ad)$;Xd*=BH^~7!6vSr4=$%BP@5XZLn!50+uk@~ATUEeU@4?5^3oriM!QY>W z->oP^JRLoYd%z=Z-Um?ng66Xg4Hi8r#}5X(X#iYf39rr^4h%7=k9Fa0hgazTOA)X zPq3`QqJR(O0frw?SVr&Rg4;j;u`C?&`bIDWe(&py{qD}zN(*KmDOK9@K6V}Z?+!qQ z?v7=2>hhU!mZ>x2&|XmV?hC(i?~`=-D+s)XWu!$o!%MVpJWg;vBqbLOM3c?Erk^kp zZg=fEimV)Js<$Ktc3%r@kXC}e^-&Xdn7WIu3G#TH>5j%m``7kDGiW^h?`K$}W zz#bKmQmZu-S*$Dx9Bd~qNB~Hu(gyNpQSJTFwAkx%bWvFA(4R82R5<{ZC@FDsNJZRj z8B8T0kj>;UfQlgM07&ePy5St@iRvUp1}z|O-vC<_PiAd##QHv$!=3)s&1lIh+c=9e5I)*c?qa$jQzN?C+*=t679?84ZjGxNB(|UN!}gKd14=!NfAn>kdSTQ=47}tg0^qZ zL22r8cc&MTRx(a>@NWgQ@-Pq&K+AI0I=mtz%9cDe3GO}pHo`YILd#uvR>Cq?3uCtu^d zhlEZNoxL@h%+W(~NdeFRplx+zM3XvP^Y>b@c>96+XpMPCh##6}Jgwj&qZ zC)#%!xx0%wk0VLxo|=QP%6+|`4KnA%MaHZ3N0ZN&FL~F`@=iip4o1ZT1E=>P<`CzO zq9PfrpHb4|7D@^X@g!QbAUX0f$wR5qPE+L;MODSXp93^qB@6YuN^oU5Unci>K%y+ z%{O~PL}GD-&ct^8AY4DPU(cnEC>ip#8+DzL73~ZUY%ED8LCwj&O48{#I|R(JlHEjO zSu+Z9pj`EC9uQV%Bch*Wwengh2W!a4u>hwf`z%=*`3hvGIF3tjj{3ugpgSIo{p~&SL~gT`)!PsFN|OEd;#MtfS-T{8`o@CGfY`-az6{3+W$CwEsW& zIdDk|J#UjaZ;)Hnf$>{({*9<+nW5lp<3dyP;l9|J@HRb3H=ef%`n>a2E##7gec)a6 ze7eXNy&6wqdrl*S9OqgPhy^M9K=yyn3+&w4M^HL}@T9MUztX%)B;QGYQ$bSW2Jfag z14#F`2+nIsM5g|qNw5H!=bF99S$KrB)jHU=CiAF?X3NYtat11iM|kmr0yQibM$$$j zQQHh)=lsE+!un4_Ib{`v%V9-lK95d<%@AU_03ZDe;9?=OdDjoV3eor*5vJJJzAkz&O(?BAe-%LJvQEdK^O7RIic&#Jv`9yglQv%QtBibEG5PH+jj6 z|M0i3lYgcJe)LsVYH9*FCuWXPO~PYq%>tKo3NCjLIszWyxF(R-UsP_3=qGE1>v=CZ zA00e6?;#mf1!~%#NO%<{_(nWl#y6D8OpyqMyIkGUNOb#)ySUfI7ZEqI(Q!RL6h6jV z(1Wah$;!%`{xek~gD4d9qoJX?HNZf11fhij1=NNFx+i@UWZw%#4K@{<37+%S3)`jt zb@|d+*(rgYofR5>qufcl<{2pIz!fJsNk>Pa2ipWRqb&_DlH;o0+wz<)vn4}*X3-GMPSeF9wZp{f1Jj!&)+bdM+nIEOfkB^ zQglS8a^`Lx*TO-Taf+E^!4rsuIb^EW9uBC&S8lne*>5KBhsSB2*o;Y!Qdmn~rBGx< z$@(6+XxGHb=>t9EHzi+LJ&pg%AolnqzU`YFJ&iCEn2#<3v=o3DR5c&~jg_^&2oL3n zUITJIOI@G78f90S4(V$FT+5lBRB{)C;DQ?#p5IXnPt>_iR7PgN@ftGstnLu0$V$Zd ziv18(6G-V(;^fyOvd$k$<^^FpX9J?^&FM?Mv2{&EYX|3L;2c{Zc{#J02oBs5f+)eb zAji%S@#}f}!r}}Ek6q_1(-;PapM!lGEKzNwOdshXjCztsFybTLTazgD@D)^;hkjid z$n-;VsZGV|;HmnW2%%*eOtw3T;p*r^92*GQtcn05Yluk$XbkpM$^GvbEMttMSh9V;Xl{+tPRGz)CO%I;r z3yz7F0!RaGLy_40g@(XKpn$2@NwbjW2=d0FU1Sy*a>sUGkY~P zwZPYuAFGBnR^!5{#avK8U_erz~#=(&(Y$R_@ z=r%TYIP8vqv23fr%AS><71%qDIMf%NKrXW8>^6Jqi#P~ECM6(sWf0uUytv)XBK!Yp zzg4EQA3fx|O!+^#9$m1RbE$n5T5*A`4!`>hZcfOAS;jAX4X^T0go)$1+#2V5!;#^w z^-dyLEE0y&era-vy{whiza~OO?%vJt%_w0Nj^a}*aerbHzr-OG5mWOSRgcz@IWhcy zJ!PcvVE6!i#k^9leBKFxx!I1G)s(;yYVA%vTYxp1L z3R+rYIlCi4Fq{2u27e)Q{h9ayff*A#@1u!^T$+566N`{I3tXWo&!WcmY5df2X??Dw#3ioTV;{)6_yri(d8*fQ&pBHG}@r| z1vcC@rp;ckN)qqv!efse=8u^AS*v?g*T6HyXm`?pyG^7s_h?I@P6uAmjrhc|JU}j_ zjkNDo0c_F{s75?;HRh8}sjdS4J@^t$7eM7hcwnO6vkQLx7Eu_Q%~?y4al;VbIQP)i zN9Q(QPCrG4`a05Zc@HZy2=#qRKLqc`|L_oV)*`x7f}Gi$$Vr`aba879ntBs->hTP` z=mT=_jxsRWC0ZA9BXR|((?5pFTG^xa0$YKwJuX)@Z1xwuI%Zrb&Mpf-dZyp}&w1ntYp^Y+ub0XN?+PsoU+ z+L>(5RE2#Cmnn;LY_q&i4h)ZoYcV{|guyvyP}xuAalR+gg5I2Tk{*Qm6%4R_Oyyxc ziJ)2V){khd9z;Vt3g}qzf4Op{n3{OBHzL`0AHhcRojc!tJi>|Zl``^Kz(?tF6EclZ zO@bNiu-=8bLP&Q*8uc`rgO0FcG;kmzoc4F$$0|4hCg`l;7(4?ByC%FJI4DQ{F^6H z-j0f15s_N)UFyMt^?U#KX+S)GnN!riMzC~K0I$p;v2BHSqWX&>jY>m^YjtMmZa6cF zxq6Fpo|925==0BSlFB1cQ#)5Axboq|sLME&{syFlNGJWOlnQF0pFdI8+MgIuQ?M^k%bAg!13hrS3!^bP#y+}Wy;bED)@OA@E3E$L zx2WTB<{pdVwycg#yoCq4JIn*4K}~WM2JH;FNG1}MGB_u-!p{ezM9fb^H1en5WFi=j zAK{E;WW7WOjtdCR&o{B;GT#cyY&LK`-|kgUF$KjUbB{@8`HJF46LnnhLNA7dyAff3 zM5+Aaf>&ZvoOh|?Pzc3n_W3XyMg4B>k#@5F;{_8Ld0djIPg}_L?ei--$Zg5vOzHXv z5SP2TE)ak6ig3k(upJ#xS3J{JPEMZVbkkMX4oGvnyH%x{mE>~0@zkon{pPCl?$CF+ z52Ws5d#-OYDPC>LezWHq;di;Qi)$=W#xCU}TZK@T_(jh)5+r3@B~(btS%M>e#UeJp>A>xT5z z+C2}UxS+Bt3f4AE`hL^5UlFPU$h?UGG$~rb_nEo&nKNvso#PsLG3nsmV>oktlhy?X zAL6Jl$%#-yo+!88NiTf3ab!QgV_8{?t5iolwpowbw$LWwve`}C{L)xGio6D z+C@VaSF4Vn!HwrfKbW{O{$WiGak1o`rV6ggml+D=Jg&Ek@-($fZG;`Jx_V7#=d>Ux z{WN)}GF9D@S1t?#^>hM$M^H`aMEd61PyCu1$vxboWn3(WVZdf$D5LE_Kp zKpX~ej<_Vnz4^R>@Xn4Xoei*dZYxB!Iq#r5%YP#d6Vj-;A`2SKTusH4fy>$CUJ z45C`j%#4iJpBq_$5ld$%<<~!Xa^Nq40wZm%w_XcS-yrsc5N+t!)aTql>76X>x~eU~ zaBPmz0#go;hN;x=7ywgcp!5iJuGH`5dd*RM?TwpWZp#q;51ACCVjA0F+J#vuevA!>_@tP(szPNjt&N~^^u{i{7b40L2E#|5Uc zn_lOS_j`9>-dUmqgkNn+tmJqSq_L(<%zxpNku;x1TrAs+*;@bV)i-Uf-c`AkX95H5 zP93Nqfq|w5L=yq4gnV0Q-lD7G+9}wYI)1-6OSuzs=ozj_kt?*ql$%sfTXD?ILo&Uq zekr33(V|Fg=5afEjW{HW9nm738I=Mx-ta6*vy`A^J*P8RgQ!igAdM-bCd!PjO7A4ZVFcTqMP1xIUI^ zWDy}FOYZC?T*W<+90x_jQ8@Le86WD^=2}q6V<;_g45=z4#HRt5{w7TdnuTJMtBnXT zl$)tD>A27kEo|f(-7p{NG#n=qkV@y^Xpk$JFVS-_<{-81D(T|+(FmmjIGstg?N9HL z;b#E&N>y2H?HFPZm_o1_&fD+no{vS7HxO>(N-X%&gyL zWd#@G=)2^IG7*TIqOT5d>F$o8bkra1L`Bnop36=@hYpU+>C}z{pHO{6@iJ~jfLK4C zc;*N}qei;)oT5QJ#264)a~vk)Q!>M0-xqxZ$#YP{gn}U!lFxe&nR^=?YiieUj(3i? z;QWM>GHXyFd_E^2k{@8?&GSIq3gO zM#S#7sngnOD!Z7BH%H@~eKj7FHYq=TDEQ#12CGC}4;Efbk?Ra#g^ zUgvt)NwmX=pBs~Pi+uZZ2QBLnL3KGafA%$9qYb{O}AckYaV zu>^fX=iC=<6f|+RWqLf)l=^Yf!XC`L8g~$LqquP-)Ps+9KkO)tbfWGKFz|Jx-oQ{W zepd08-&|i_M;^@~XrU6Z*@=Akm?BK*v6=>UQk2gK@@D@YrNS0$$) z;%F2iy#a)-r;M};b)beF{0(MbRiz6?P3CbTb8J-0Asx*tLb#5!MSg7rFK6bw3FOD1 zV%6zMob-W{lf{lkBjm$%5qc>t+=Ec{St=ojD6&$-ExzyYHmXQRd_;Y)xAG9p#$4@p z8sik*B=r>p@kzTml@=BsqM#oktH(~iVR_TJK352jZKVFWk}2DWGToc~I*C7QtC<>= z_r3R;f9EIudC=V>G~ceA#?pk*L1f>@FWe9opB84#^{l`%>8}JT3N(_!3*(gEaWIm; zy-g7qIk<>=BDqvc(Vzih?08Z8>!NaW!C*K;KLx!e_#gK6dA_3{bX^>C-^mK$kq!Tr zZ~w{`9kC`{0OF=Kv7X=Z$M7TKF5VlxY2Vf!G~Ar!8mh4R&FOOL=0w9M?4wp(;T)H( z!Z<+ByR(1wOX2PyX7+Gu5;X+CVb}8&HNZ9Xy|C+Q_nw;$vK0@3jy&bCW{&~Fr4iXg1M<5OaVX47TC@tC#I$7 zpv}ef?zT$KLWNLU2g=~Y853Aq@{ld^?TYvpS$HxmZa#-c{Bwi_v-5s1 zo!(gwn&)5)R1!6wzOa5WhfCo=pN3ULN75i*6Zq-SMO9jevCM%(-u)qUW|9jexb-8+ z@TX-Qo|95*)2g(iIa^mk!UlAvbA3?+6*$G2 zx!0_&&ZG8S_7nvU(@kk>sG5VUab;sCfuvQljdWG_aHh{@TA3 zop3S@eAn^R;p+ekh1IomUAcz=(%8!@<2gN+M(sKc2D2Ot9H0H;H3i#sTqp1G1Bl_vU$)9RQe&(EJQH3%S->f27wP&o=+XO{L(lsybXUY#q?sG z&sw?JN5pN@sHyYk$BkY1_dyyj=4kZm9i+j&Z6|QBGLPNEe(fl*>w~nCK)S)O`DV_5 zi0q`pHyX*DSoQs;{G}})r>}nRt;W(bn+@pJpG2JP0K%%!eQS<&zYuni+-i&L7#ezo zu?#d=ij+sVKKw}zdKsgo8mMqC{#W6G-swf`k&b+R>*f`Dhsjv!rvQgAN<)kRF{P*_P*qzoU|xeDhH2nK|AcEx9>fiKppDhi5Fl#C^A1Fj zT)J<75)WtW3*xPJ+|2qbYtK$3;QB54?c$H;Q~KN?CF-o6@H+`sJa$^E#sx#`{>JNe zlAnT`7wOM_^pjx762PRboDGH=F))6j4lD2)$$#a~x0LGuGTS~Mydxr)eR(e#lv)NK z5Rckr3Q$9!M^4B}`~)MMOq=2Ab}1)DS3@_JW2DKgLo*?HAaxw=kYK!(G?XBe#x;7k zFA#r8(67;d2g`p|U^n#rf1M1$4O_6cf|r?HQi~Zff8UDdc4_8o_P{56rxEZV4u)tj z&JVC@H+dEo3`NHk`|}3DsQ2Ke;tD@PVqZVF)m!{nDS*{|DQ(;!q_|yICaj}*B1ZN% zSL!_DIzG8lka|$L;V1<4{oY4x`kw1_AB8>|GC7POj9mbS$o3x7M?5N-*`-bV5zKq9 zK_h%|c5sTiT>i9p-Nq%xT5(G;pPN|${*PlvX&%gr-d~I`^d4F1**M9!jzosxezhk_ zWh^~ZNBj)e9{eA^h1!ZFk`87Ca~#NKg^~uwe1r)Py9n>c_z1huR^IDaQPX#4Rb3hV3` zQZ?$J1in%KX|jgu4T@{IM9oHI7IkYARYu9eK2)xd&`6WX1G%9bR5TO+{I>=3-Os)` zTj0d5=@Pueu96A{zx>ZJv~18WK|LF-;b)}PeZoV$ZdEQLU!#^TH^I>j2N|U#e9eHPpYicHtn6n>rJ!i9> z2`fR$O`MSmbW(=Jr8RUl2Y%Z;k4%FYD4{{Ov(I0;){x^rux#L(AS|jZSZ)N#bDDI0 zsa*DNhOa_E-r7>C<$lFjO{&7su_7iuqyz&aP7`h=rp`Sh!tYIb zsw`RLyLA>D`(r9G&6U!md=kHo`A~;=WKC^QuchuHg2+KOb?kM>zI4>{5!+pYz!+&^ z{@#GhNP_0Ce|h}BJbj86{!#<1qNv@JM8nWf1%saK-GDlnMCF+lzb*U~9_d+mR^WK= zo{`zE-pTx}@9+6Sq+}5#1O0t5yNhI;$uNOq6&vKfo^FeH51V#xnHHD-*Vuz@#-Y>> z(P3G>5QWqn%wqO_ve)}dTK_%@YWe1)Z`XkbrKrRV5~Fzbk<8IRLa&Tv_J9^_Os~dZ znBL)tPVf+gzjW#T%a#A~n&UW}-6Ic&+Bs=F8Krd?%D^MPUjkNk2#J+359~-0xIt?3 z_GkA-ZOhb<>vxprvGHA!LQ+n3lritl)?k%vJ=01HS;-75I|90Y*|VOaZ>h)9EbTa5 zl-Y-P;qc~V>2gA8YPbW+J=?#DRc!K&m`VA(5YyLJQD>s-!(!NO%D@ZCFbNoQtca~! zvXP2*`r3&{p%NOHwFeX7)0)tAG?Kmt#ILEtpB*&I%KDXmSv-lu|4$Lkn(tuhz;&~e* zLIbpDFiJJ~cAzcp3TKdm_OTtajuh7_6LT?@ei$a=G0|mZOWH#4r!%x@d9TOcJFsv~ znNY8oWd=xJo2a$0XR--~XZN(~&VfAQR}OMbfqz>|n?DCLbz;X#JPoLUC|8~F@L%>A?J@J8`*+`2$o-WR%o;lc ztW=?tkZxRvpgH-^zZwGH0zYSKn%7<=EGIMbqzGe!s&3K5NA#6xhC%|<&}6LSi$t`l5n+|B)&3ev^MRxxErZgN`9Ex`Yb?f9!HYII8^kXO-( zJ~9{c_jt5PNHm_L?g?^~7*?vmRGENI_`ryCgwaxF_HMrF9ft!)Ib5JdczO?NeWVxt zT?t$>I1k;q#x{^je?8yk$$yT|zV$YEg7&A&NE;fckAUlJ0Ik)L2X=8|E{&k>#!ujM z_l7Gw+bWv$!MXzATmz!gDtHtu$LO!7P~ox8H9qLsy-Cz+bL{R#UmaAZ5`@7hLTIi; z(H5A?&&|z)zizV@%w`c}RV@zB{YcCQ9ZT|d@n@-Z)F4Zw#Bjg&FWn!yXki~m(iO>e zK!403-zo~`xRGT3rH*BI+1`%E5Jj-aBsk#cM37E{E}tSyN#f=Kt4){|HHg|UN4m)) zXVJj0y!2ot4OtjP_vOsrPNds&jY8Bi3mqngJuU_TvG!+i>WF~ZLqs52@DxIx0Zi=} z>c9LFe_ViTg0=!uB^&GS9G%`Lt!l>r`dczIU5@3eUk8iCHD=OB+ytA10Z^WFe)BLx zV&P}W1X)N^kD@<8FpBqCGN}_|04Hn-

p>hM|JXB0+A>PQhP5$svOy-dg8vV!*%+ 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�NlI#r5M)-%5 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 D = steps.iter().map(|step| at(step)).collect(); @@ -31,4 +32,9 @@ fn helpers_then_single_instance_then_the_display_reset() { main[..positions[0]].find("vdd::").is_none(), "main touches the virtual display before dispatching helper modes" ); + // The display reset no longer holds up startup. + assert!( + !main.contains(" vdd::disable();\n"), + "main waits for the startup Disable again" + ); } diff --git a/installer/EternalMonitor.iss b/installer/EternalMonitor.iss index a3b62c97..603c630b 100644 --- a/installer/EternalMonitor.iss +++ b/installer/EternalMonitor.iss @@ -82,6 +82,7 @@ Source: "scripts\vdd-tasks-setup.ps1"; DestDir: "{app}\scripts"; Flags: ignorev Source: "scripts\vdd-tasks-remove.ps1"; DestDir: "{app}\scripts"; Flags: ignoreversion Source: "scripts\vdd-driver-remove.ps1"; DestDir: "{app}\scripts"; Flags: ignoreversion Source: "scripts\vdd-driver-verify.ps1"; DestDir: "{app}\scripts"; Flags: ignoreversion +Source: "scripts\vdd-driver-repair.ps1"; DestDir: "{app}\scripts"; Flags: ignoreversion Source: "scripts\vdd-toggle.ps1"; DestDir: "{app}\scripts"; Flags: ignoreversion #endif @@ -103,7 +104,8 @@ Filename: "{sys}\netsh.exe"; Parameters: "advfirewall firewall add rule name=""E Filename: "{app}\driver\vdd-setup-x64.exe"; Parameters: "/VERYSILENT /SUPPRESSMSGBOXES /NORESTART"; StatusMsg: "Installing the virtual display driver (this enables the extended screen)..."; Flags: waituntilterminated; Check: NeedsVddInstall; AfterInstall: RecordVddOwnership ; Register the enable/disable scheduled tasks and leave the virtual display OFF by default — ; EternalMonitor turns it on only while streaming to it, so there's no phantom monitor. -Filename: "powershell.exe"; Parameters: "-NoProfile -ExecutionPolicy Bypass -File ""{app}\scripts\vdd-tasks-setup.ps1"""; StatusMsg: "Configuring the on-demand virtual display..."; Flags: runhidden waituntilterminated; Check: VddInstallSucceeded; AfterInstall: VerifyVddDriver +; A preserved driver whose device was removed or broke is repaired first. +Filename: "powershell.exe"; Parameters: "-NoProfile -ExecutionPolicy Bypass -File ""{app}\scripts\vdd-tasks-setup.ps1"""; StatusMsg: "Configuring the on-demand virtual display..."; Flags: runhidden waituntilterminated; Check: VddInstallSucceeded; BeforeInstall: RepairVddDriver; AfterInstall: VerifyVddDriver #endif ; Launch the app at the end. Filename: "{app}\EternalMonitor-host.exe"; Description: "Launch EternalMonitor now"; Flags: nowait postinstall skipifsilent; Check: VddInstallSucceeded @@ -181,6 +183,30 @@ begin end; end; +procedure RepairVddDriver; +var + ExitCode: Integer; + Message: String; +begin + { Setup keeps an existing driver instead of running its setup again. When + Setup installed it and no device works, put one back (exit codes in the + script). A driver another app installed is left alone. } + if not Exec(ExpandConstant('{sys}\WindowsPowerShell\v1.0\powershell.exe'), + '-NoProfile -ExecutionPolicy Bypass -File "' + ExpandConstant('{app}\scripts\vdd-driver-repair.ps1') + '"', + '', SW_HIDE, ewWaitUntilTerminated, ExitCode) then + ExitCode := -1; + if ExitCode = 0 then + exit; + case ExitCode of + 10: Message := 'Another app installed the virtual display driver, and none of its displays works. EternalMonitor does not change another app''s driver. Repair it in that app, or uninstall "Virtual Display Driver" in Windows Settings > Apps > Installed apps, then run this setup again.'; + 11: Message := 'The virtual display driver''s files are missing. Uninstall "Virtual Display Driver" in Windows Settings > Apps > Installed apps, then run this setup again.'; + else + Message := 'Setup could not repair the virtual display driver. Restart the PC and run this setup again. If it still fails, uninstall "Virtual Display Driver" in Windows Settings > Apps > Installed apps, then run this setup again.'; + end; + VddInstallFailed := True; + RaiseException(Message); +end; + procedure RecordVddOwnership; var Directory, Version: String; diff --git a/installer/scripts/vdd-driver-repair.ps1 b/installer/scripts/vdd-driver-repair.ps1 new file mode 100644 index 00000000..3243c5e0 --- /dev/null +++ b/installer/scripts/vdd-driver-repair.ps1 @@ -0,0 +1,106 @@ +# Puts back the Virtual Display Driver's device when EternalMonitor installed the driver and no +# working device is left. Setup keeps an existing driver instead of running the vendor setup +# again, so a device removed in Device Manager, or left without its driver or with a driver +# error, stayed broken through every reinstall. Run elevated by the installer, before the +# scheduled tasks disable the device again. +# +# It changes nothing while any device works, and nothing for a driver another app installed. +# The driver package is staged first, and a replacement device must work before the broken +# ones are removed, so a failure leaves the devices as they were. A rerun after success does +# nothing. +# +# Exit codes, which Setup turns into its messages (installer/EternalMonitor.iss): +# 0 working or repaired 1 repair failed 10 another app installed the driver 11 files missing +$ErrorActionPreference = 'Stop' +$hardwareId = 'Root\MttVDD' +$classGuid = '4D36E968-E325-11CE-BFC1-08002BE10318' + +function Get-VddDevices { + @(Get-PnpDevice -Class Display -PresentOnly | Where-Object { + $ids = (Get-PnpDeviceProperty -InstanceId $_.InstanceId -KeyName 'DEVPKEY_Device_HardwareIds').Data + $ids -contains $hardwareId + }) +} + +# Bound to an installed driver package, and either off (code 22, between streams) or running. +function Test-Healthy($device) { + $inf = (Get-PnpDeviceProperty -InstanceId $device.InstanceId -KeyName 'DEVPKEY_Device_DriverInfPath').Data + ($inf -match '^oem[0-9]+\.inf$') -and ([int]$device.Problem -in @(0, 22)) +} + +function Get-Working { @(Get-VddDevices | Where-Object { Test-Healthy $_ }) } + +function Invoke-PnpUtil([string[]]$Arguments) { + pnputil.exe @Arguments | Out-Null + # 3010: done, takes effect after a restart. 259: staged, no device needed updating. + if ($LASTEXITCODE -notin @(0, 3010, 259)) { + throw "pnputil $($Arguments[0]) exited $LASTEXITCODE." + } +} + +# pnputil can report success, 259 or a pending restart and leave the device in +# place, so a removal counts only once the device is gone. +function Remove-Device([string]$Id) { + Invoke-PnpUtil @('/remove-device', $Id) + if (@(Get-VddDevices | Where-Object { $_.InstanceId -eq $Id }).Count -gt 0) { + throw "Windows did not remove the Virtual Display Driver device $Id." + } +} + +try { + if (@(Get-Working).Count -gt 0) { + Write-Output 'Virtual Display Driver device is installed.' + exit 0 + } + + # The ownership rule of vdd-driver-remove.ps1: Setup recorded this exact vendor install + # when it ran the vendor setup itself. + $marker = Join-Path $PSScriptRoot '..\driver\installed-by-eternalmonitor.txt' + $registration = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\VirtualDisplayDriver_is1', + 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\VirtualDisplayDriver_is1' -ErrorAction SilentlyContinue | + Where-Object { $_.InstallLocation -and $_.DisplayVersion } | Select-Object -First 1 + $owned = $registration -and (Test-Path $marker) -and + ([IO.File]::ReadAllText($marker) -eq ((Join-Path $registration.InstallLocation 'unins000.exe') + "`r`n" + $registration.DisplayVersion)) + if (!$owned) { + [Console]::Error.WriteLine('Another app installed the Virtual Display Driver. EternalMonitor leaves it alone.') + exit 10 + } + $inf = Join-Path $registration.InstallLocation 'MttVDD.inf' + $nefcon = Join-Path $registration.InstallLocation 'nefconw.exe' + if (!(Test-Path $inf) -or !(Test-Path $nefcon)) { + [Console]::Error.WriteLine("The Virtual Display Driver files are missing from $($registration.InstallLocation).") + exit 11 + } + + # Stage the package and bind it to matching devices. This alone repairs a device whose + # driver package was deleted, and nothing is removed if it fails. + Invoke-PnpUtil @('/add-driver', $inf, '/install') + if (@(Get-Working).Count -gt 0) { + Write-Output 'Repaired the Virtual Display Driver by reinstalling its driver package.' + exit 0 + } + + # Still nothing works: add a device as the vendor install does, and bind it. + $broken = @(Get-VddDevices | ForEach-Object { $_.InstanceId }) + $process = Start-Process -FilePath $nefcon -WindowStyle Hidden -Wait -PassThru ` + -ArgumentList "--create-device-node --hardware-id $hardwareId --class-name Display --class-guid $classGuid" + $added = @(Get-VddDevices | Where-Object { $broken -notcontains $_.InstanceId }) + if ($process.ExitCode -ne 0 -or $added.Count -ne 1) { + foreach ($device in $added) { Remove-Device $device.InstanceId } + throw "Could not add a Virtual Display Driver device: nefconw exited $($process.ExitCode)." + } + $new = $added[0].InstanceId + # A failure here is judged by the device's state below, which undoes the new device. + try { Invoke-PnpUtil @('/add-driver', $inf, '/install') } catch {} + if (@(Get-Working | Where-Object { $_.InstanceId -eq $new }).Count -eq 0) { + Remove-Device $new + throw 'Windows did not start the new Virtual Display Driver device.' + } + # The new device works: only now remove the broken ones, by instance. + foreach ($id in $broken) { Remove-Device $id } + Write-Output "Repaired the Virtual Display Driver device: $new" + exit 0 +} catch { + [Console]::Error.WriteLine("Virtual Display Driver repair failed: $($_.Exception.Message)") + exit 1 +} diff --git a/installer/scripts/vdd-toggle.ps1 b/installer/scripts/vdd-toggle.ps1 index 52c967bd..52fc1e6a 100644 --- a/installer/scripts/vdd-toggle.ps1 +++ b/installer/scripts/vdd-toggle.ps1 @@ -7,6 +7,12 @@ # correct across driver versions, friendly-name changes, and PnP enumeration order. The # VirtualDrivers/Virtual-Display-Driver enumerates under ROOT\DISPLAY and reports a friendly # name containing "Virtual Display", so we match on either — a real monitor is never ROOT\. +# +# Enable fails when the display cannot appear: no device, no driver bound, or a Windows problem +# code. The exit code says why, and the host reads it as the task's result (host/src/vdd_device.rs): +# 2 no device 3 no driver bound 100 + N Windows problem code N (122: still disabled) +# A device that has not started yet or has no monitor yet still succeeds: its monitor can arrive +# later, and the host waits for the display itself and reads the device's state if it never comes. param( [Parameter(Mandatory = $true)] [ValidateSet('enable', 'disable')] @@ -22,21 +28,48 @@ $devices = @(Get-PnpDevice -Class Display -PresentOnly | Where-Object { if ($devices.Count -eq 0) { if ($Action -eq 'disable') { exit 0 } - throw 'The bundled Virtual Display Driver device is not present. Reinstall EternalMonitor with its display driver.' + [Console]::Error.WriteLine('The bundled Virtual Display Driver device is not present. Run EternalMonitor Setup again to reinstall it.') + exit 2 +} + +# Why an enabled device cannot show a display yet, or $null when it can. Each PnP call takes about +# half a second while the device starts, so the check makes two: the device's properties, then the +# device and its monitors together. +function Get-EnableProblem($instance) { + $properties = @(Get-PnpDeviceProperty -InstanceId $instance -KeyName 'DEVPKEY_Device_DriverInfPath', 'DEVPKEY_Device_Children') + $inf = ($properties | Where-Object KeyName -eq 'DEVPKEY_Device_DriverInfPath').Data + if ($inf -notmatch '^oem[0-9]+\.inf$') { return @{Code=3; Text="$instance has no driver bound"} } + $children = @(($properties | Where-Object KeyName -eq 'DEVPKEY_Device_Children').Data | Where-Object { $_ }) + $found = @(Get-PnpDevice -InstanceId (@($instance) + $children)) + $device = $found | Where-Object InstanceId -eq $instance + if ([int]$device.Problem -ne 0) { + return @{Code=100 + [int]$device.Problem; Text="$instance reports problem code $([int]$device.Problem)"} + } + if ($device.Status -ne 'OK') { return @{Code=4; Text="$instance has not started"} } + $monitor = $found | Where-Object { $_.InstanceId -ne $instance -and $_.Status -eq 'OK' } + if (!$monitor) { return @{Code=5; Text="$instance started, but no monitor arrived"} } + return $null } foreach ($device in $devices) { # PnPUtil can report this root device as disconnected even when its # present devnode is disabled. The PnpDevice cmdlets address that devnode. if ($Action -eq 'enable') { + # A device the Disable task turned off reports code 22 until this runs. Enable-PnpDevice -InstanceId $device.InstanceId -Confirm:$false + $problem = Get-EnableProblem $device.InstanceId + if ($problem -and $problem.Code -in 4, 5) { + Write-Output "Virtual display enabled; $($problem.Text) yet" + } elseif ($problem) { + [Console]::Error.WriteLine("Virtual display enable failed: $($problem.Text)") + exit $problem.Code + } } else { Disable-PnpDevice -InstanceId $device.InstanceId -Confirm:$false + $actual = Get-PnpDevice -InstanceId $device.InstanceId + if ([int]$actual.Problem -ne 22) { + throw "Virtual display disable failed: $($actual.InstanceId) reports $($actual.Problem)" + } } - $actual = Get-PnpDevice -InstanceId $device.InstanceId - $expectedProblem = if ($Action -eq 'enable') { 0 } else { 22 } - if ([int]$actual.Problem -ne $expectedProblem) { - throw "Virtual display $Action failed: $($actual.InstanceId) reports $($actual.Problem)" - } - Write-Output "Virtual display $Action completed: $($actual.InstanceId)" + Write-Output "Virtual display $Action completed: $($device.InstanceId)" } diff --git a/installer/tests/Test-VddDriverRepair.ps1 b/installer/tests/Test-VddDriverRepair.ps1 new file mode 100644 index 00000000..003638b3 --- /dev/null +++ b/installer/tests/Test-VddDriverRepair.ps1 @@ -0,0 +1,189 @@ +# Pure script checks for the Setup step that repairs a Virtual Display Driver EternalMonitor +# installed. PnP, the registry, pnputil and the vendor's nefconw are replaced in this process, +# and the script runs from a scratch copy; nothing is installed or removed. +param([string]$RepairPath) +$ErrorActionPreference = 'Stop' +$source = if ($RepairPath) { $RepairPath } else { Join-Path $PSScriptRoot '..\scripts\vdd-driver-repair.ps1' } +$iss = Get-Content -Raw (Join-Path $PSScriptRoot '..\EternalMonitor.iss') +$root = Join-Path $env:TEMP ('em-driver-repair-test-' + [guid]::NewGuid().ToString('N')) +New-Item -ItemType Directory -Force "$root\scripts", "$root\driver", "$root\vendor" | Out-Null +$script = "$root\scripts\vdd-driver-repair.ps1" +Copy-Item $source $script +$vendor = "$root\vendor" +$nefcon = "$vendor\nefconw.exe" +$inf = "$vendor\MttVDD.inf" +$marker = "$root\driver\installed-by-eternalmonitor.txt" + +function Reset-Fixture { + foreach ($file in $nefcon, $inf) { [IO.File]::WriteAllText($file, 'test fixture; never executed') } + [IO.File]::WriteAllText($marker, "$vendor\unins000.exe`r`n25.05.03") + $global:EMRepair = @{ + Registration = [pscustomobject]@{InstallLocation=$vendor; DisplayVersion='25.05.03'} + Calls = @(); Next = 1 + # The driver package in the Windows driver store, which binds to new devices. + InStore = $true; StageExit = 0 + # Whether a device the package binds to then starts. + NewStarts = $true + # pnputil reports removing a device that stays (259 or a pending restart). + RemoveSticks = $false + Devices = @([pscustomobject]@{InstanceId='PCI\GPU';HardwareIds=@('PCI\GPU');Inf='oem1.inf';Problem=0}) + } +} +function Add-Vdd([string]$Inf, [int]$Problem) { + $id = 'ROOT\DISPLAY\{0:D4}' -f ($global:EMRepair.Next++) + $global:EMRepair.Devices += [pscustomobject]@{InstanceId=$id;HardwareIds=@('Root\MttVDD');Inf=$(if ($Inf) { $Inf } else { $null });Problem=$Problem} + $id +} +function Get-Vdd { @($global:EMRepair.Devices | Where-Object { $_.HardwareIds -contains 'Root\MttVDD' }) } +function Get-PnpDevice { + [CmdletBinding()] param([string]$Class,[switch]$PresentOnly) + if ($Class -ne 'Display' -or !$PresentOnly) { throw 'Must query present display devices' } + $global:EMRepair.Devices +} +function Get-PnpDeviceProperty { + [CmdletBinding()] param([string]$InstanceId,[string]$KeyName) + $device = $global:EMRepair.Devices | Where-Object { $_.InstanceId -eq $InstanceId } + $value = switch ($KeyName) { + 'DEVPKEY_Device_HardwareIds' { $device.HardwareIds } + 'DEVPKEY_Device_DriverInfPath' { $device.Inf } + default { throw "Unexpected property $KeyName" } + } + [pscustomobject]@{Data=$value} +} +function Get-ItemProperty { [CmdletBinding()] param([string[]]$Path) $global:EMRepair.Registration } +# Binds the staged package to every driverless device, as Windows does. +function Bind-Driverless { + foreach ($device in Get-Vdd | Where-Object { !$_.Inf }) { + $device.Inf = 'oem40.inf' + $device.Problem = if ($global:EMRepair.NewStarts) { 0 } else { 43 } + } +} +function global:pnputil.exe { + $verb = $args[0] + $global:EMRepair.Calls += "pnputil $verb $(if ($verb -eq '/remove-device') { $args[1] })".Trim() + switch ($verb) { + '/add-driver' { + if ($args[1] -ne $inf -or $args[2] -ne '/install') { throw "Bad add-driver: $args" } + $global:LASTEXITCODE = $global:EMRepair.StageExit + if ($global:EMRepair.StageExit -in @(0, 3010)) { $global:EMRepair.InStore = $true; Bind-Driverless } + } + '/remove-device' { + $id = $args[1] + if ($global:EMRepair.RemoveSticks) { $global:LASTEXITCODE = 259; return } + $global:EMRepair.Devices = @($global:EMRepair.Devices | Where-Object { $_.InstanceId -ne $id }) + $global:LASTEXITCODE = 0 + } + default { throw "Unexpected pnputil $verb" } + } +} +# nefconw 1.10.0. --remove-device-node looks up a compatible driver before it removes +# anything, so it fails while a device has no driver and the store has no package. +function Start-Process { + [CmdletBinding()] param([string]$FilePath,[string]$ArgumentList,[switch]$Wait,[switch]$PassThru,[string]$WindowStyle) + if ($FilePath -ne $nefcon -or !$Wait -or !$PassThru) { throw "Unexpected process $FilePath" } + $verb = $ArgumentList.Split(' ')[0] + $global:EMRepair.Calls += "nefconw $verb" + $exit = 0 + switch ($verb) { + '--create-device-node' { + if ($ArgumentList -notmatch '--hardware-id Root\\MttVDD --class-name Display --class-guid 4D36E968') { throw "Bad create: $ArgumentList" } + Add-Vdd $null 28 | Out-Null + } + '--remove-device-node' { + if (!$global:EMRepair.InStore -and (Get-Vdd | Where-Object { !$_.Inf })) { $exit = 1 } + else { $global:EMRepair.Devices = @($global:EMRepair.Devices | Where-Object { $_.HardwareIds -notcontains 'Root\MttVDD' }) } + } + '--install-driver' { $global:EMRepair.InStore = $true; Bind-Driverless } + default { throw "Unexpected nefconw verb $verb" } + } + [pscustomobject]@{ExitCode=$exit} +} +function Invoke-Repair { + $global:LASTEXITCODE = 0 + try { & $script 2>$null | Out-Null } catch { $global:LASTEXITCODE = "threw: $($_.Exception.Message)" } + $LASTEXITCODE +} +$failures = @() +function Expect([string]$Case, $Code, [string]$Calls, [scriptblock]$State = { $true }) { + $actual = Invoke-Repair + $ran = $global:EMRepair.Calls -join ', ' + if ("$actual" -ne "$Code" -or $ran -ne $Calls -or !(& $State)) { + $script:failures += "$Case`: exit $actual (want $Code), ran [$ran] (want [$Calls]), devices $((Get-Vdd | ForEach-Object { "$($_.InstanceId):$($_.Inf):$($_.Problem)" }) -join ' ')" + } +} +function Working { @(Get-Vdd | Where-Object { $_.Inf -and $_.Problem -in @(0, 22) }) } +$stage = 'pnputil /add-driver' +try { + # Between streams the device is off with code 22; running is fine too. + foreach ($problem in 22, 0) { + Reset-Fixture; Add-Vdd 'oem33.inf' $problem | Out-Null + Expect "working device, code $problem" 0 '' + } + # A working device next to a broken one: nothing is touched. + Reset-Fixture; $good = Add-Vdd 'oem33.inf' 22; Add-Vdd 'oem33.inf' 43 | Out-Null + Expect 'working and broken devices' 0 '' { @(Get-Vdd).Count -eq 2 } + + # A driver another app installed is never changed. + Reset-Fixture; Remove-Item $marker; Add-Vdd 'oem33.inf' 43 | Out-Null + Expect 'no ownership marker' 10 '' { @(Get-Vdd).Count -eq 1 } + Reset-Fixture; [IO.File]::WriteAllText($marker, "$vendor\unins000.exe`r`n24.12.24") + Expect 'vendor install changed since Setup recorded it' 10 '' + Reset-Fixture; $global:EMRepair.Registration = $null + Expect 'no vendor registration' 10 '' + Reset-Fixture; Remove-Item $nefcon + Expect 'vendor files missing' 11 '' + + # Driverless device and the package gone from the driver store: staging the package + # binds it, and nothing is removed. nefconw's remove fails in this state. + Reset-Fixture; $global:EMRepair.InStore = $false; $kept = Add-Vdd $null 28 + Expect 'driverless device, package missing' 0 $stage { (Working).InstanceId -eq $kept } + # Removed in Device Manager, with or without its package. + foreach ($inStore in $true, $false) { + Reset-Fixture; $global:EMRepair.InStore = $inStore + Expect "missing device, package in store=$inStore" 0 "$stage, nefconw --create-device-node, $stage" { @(Working).Count -eq 1 } + } + # A device with a driver error is replaced, and removed only once the new one works. + Reset-Fixture; $old = Add-Vdd 'oem33.inf' 43 + Expect 'driver error' 0 "$stage, nefconw --create-device-node, $stage, pnputil /remove-device $old" { + @(Get-Vdd).Count -eq 1 -and (Working).InstanceId -ne $old } + # The replacement fails too: only the device this run added is removed. + Reset-Fixture; $old = Add-Vdd 'oem33.inf' 43; $global:EMRepair.NewStarts = $false + Expect 'replacement fails' 1 "$stage, nefconw --create-device-node, $stage, pnputil /remove-device ROOT\DISPLAY\0002" { + @(Get-Vdd).Count -eq 1 -and @(Get-Vdd)[0].InstanceId -eq $old -and @(Get-Vdd)[0].Problem -eq 43 } + # pnputil reports removing a broken device that stays: not a success. + Reset-Fixture; $old = Add-Vdd 'oem33.inf' 43; $global:EMRepair.RemoveSticks = $true + Expect 'broken device not removed' 1 "$stage, nefconw --create-device-node, $stage, pnputil /remove-device $old" { + @(Get-Vdd).Count -eq 2 -and @(Working).Count -eq 1 } + # The same when undoing a replacement that did not start. + Reset-Fixture; $old = Add-Vdd 'oem33.inf' 43; $global:EMRepair.NewStarts = $false; $global:EMRepair.RemoveSticks = $true + Expect 'failed replacement not removed' 1 "$stage, nefconw --create-device-node, $stage, pnputil /remove-device ROOT\DISPLAY\0002" + # Windows rejects the package: nothing else happens. + Reset-Fixture; $old = Add-Vdd 'oem33.inf' 52; $global:EMRepair.StageExit = 5 + Expect 'package rejected' 1 $stage { @(Get-Vdd).Count -eq 1 -and @(Get-Vdd)[0].Problem -eq 52 } + # A restart-required result still counts once the device works. + Reset-Fixture; $global:EMRepair.StageExit = 3010 + Expect 'restart required' 0 "$stage, nefconw --create-device-node, $stage" + # A rerun after a repair changes nothing. + $global:EMRepair.Calls = @(); $global:EMRepair.StageExit = 0 + Expect 'rerun' 0 '' + + # Setup ships the step, runs it before the tasks disable the device, also when the + # existing driver is preserved, and names each outcome. + if ($iss -notmatch 'Source: "scripts\\vdd-driver-repair\.ps1"') { $failures += 'Setup does not install the repair script' } + $tasks = ($iss -split "`n") | Where-Object { $_ -match 'vdd-tasks-setup\.ps1' -and $_ -match '^Filename:' } + if ($tasks -notmatch 'BeforeInstall: RepairVddDriver' -or $tasks -match 'Check: NeedsVddInstall') { + $failures += 'Setup does not repair a preserved driver before registering the tasks' + } + $code = [regex]::Match($iss, '(?s)procedure RepairVddDriver;.*?\nend;').Value + if ($code -notmatch 'vdd-driver-repair\.ps1' -or $code -notmatch "10: Message := 'Another app installed" -or + $code -notmatch '11: Message' -or $code -notmatch 'VddInstallFailed := True' -or $code -notmatch 'RaiseException') { + $failures += 'Setup does not report each repair failure' + } + if ($failures) { throw "Repair checks failed:`n " + ($failures -join "`n ") } + Write-Output 'PASS: working, working beside broken, not owned, changed, unregistered, files missing, driverless with package gone, missing device, driver error, failed replacement, removal not done, rejected package, restart, rerun, Setup wiring' +} finally { + Remove-Item $root -Recurse -Force + Remove-Item function:global:pnputil.exe -ErrorAction SilentlyContinue + Remove-Variable EMRepair -Scope Global -ErrorAction SilentlyContinue + $global:LASTEXITCODE = 0 +} diff --git a/scripts/win/Test-VddToggle.ps1 b/scripts/win/Test-VddToggle.ps1 index 6973c38a..c51d2c85 100644 --- a/scripts/win/Test-VddToggle.ps1 +++ b/scripts/win/Test-VddToggle.ps1 @@ -2,50 +2,145 @@ param([string]$TogglePath) $ErrorActionPreference = 'Stop' $toggle = if ($TogglePath) { $TogglePath } else { Join-Path $PSScriptRoot '..\..\installer\scripts\vdd-toggle.ps1' } -$global:EMVddTestState = @{ Calls=@(); Fail=$false; Ignore=$false; Devices=@( - [pscustomobject]@{InstanceId='PCI\REAL_GPU';HardwareIds=@('PCI\GPU');Problem=0}, - [pscustomobject]@{InstanceId='ROOT\DISPLAY\0001';HardwareIds=@('Root\OtherDriver');Problem=0}, - [pscustomobject]@{InstanceId='ROOT\DISPLAY\0007';HardwareIds=@('Root\MttVDD');Problem=22} -) } +function Reset-State { + $global:EMVddTestState = @{ Calls=@(); Fail=$false; Ignore=$false + # What the device reports once Enable-PnpDevice has run. + Enabled=@{Problem=0; Status='OK'; Monitor='OK'} + Devices=@( + [pscustomobject]@{InstanceId='PCI\REAL_GPU';HardwareIds=@('PCI\GPU');Problem=0;Status='OK';Inf='oem1.inf';Children=@()}, + [pscustomobject]@{InstanceId='ROOT\DISPLAY\0001';HardwareIds=@('Root\OtherDriver');Problem=0;Status='OK';Inf='oem2.inf';Children=@()}, + [pscustomobject]@{InstanceId='ROOT\DISPLAY\0007';HardwareIds=@('Root\MttVDD');Problem=22;Status='Error';Inf='oem33.inf';Children=@()}, + [pscustomobject]@{InstanceId='DISPLAY\MTT1337\1&2&3&UID256';HardwareIds=@('MONITOR\MTT1337');Problem=24;Status='Unknown';Inf='monitor.inf';Children=@()} + ) } +} +function Find-Device($InstanceId) { $global:EMVddTestState.Devices | Where-Object InstanceId -eq $InstanceId } +# Like the real cmdlets, each takes several instance IDs or keys at once. function Get-PnpDevice { - param($Class, [switch]$PresentOnly, $InstanceId) - $global:EMVddTestState.Devices | Where-Object { !$InstanceId -or $_.InstanceId -eq $InstanceId } + param($Class, [switch]$PresentOnly, [string[]]$InstanceId) + $global:EMVddTestState.Calls += 'get' + $global:EMVddTestState.Devices | Where-Object { + ($InstanceId -and $InstanceId -contains $_.InstanceId) -or (!$InstanceId -and $_.InstanceId -notlike 'DISPLAY\*') + } } function Get-PnpDeviceProperty { - param($InstanceId, $KeyName) - [pscustomobject]@{Data=(Get-PnpDevice -InstanceId $InstanceId).HardwareIds} + param($InstanceId, [string[]]$KeyName) + $global:EMVddTestState.Calls += 'property' + $device = Find-Device $InstanceId + foreach ($key in $KeyName) { + $data = switch ($key) { + 'DEVPKEY_Device_HardwareIds' { $device.HardwareIds } + 'DEVPKEY_Device_DriverInfPath' { $device.Inf } + 'DEVPKEY_Device_Children' { $device.Children } + default { throw "Unexpected property $key" } + } + [pscustomobject]@{KeyName=$key; Data=$data} + } } function Enable-PnpDevice { param($InstanceId, $Confirm) if ($global:EMVddTestState.Fail) { throw 'mock driver failure' } $global:EMVddTestState.Calls += "enable:$InstanceId" - if (!$global:EMVddTestState.Ignore) { (Get-PnpDevice -InstanceId $InstanceId).Problem = 0 } + if ($global:EMVddTestState.Ignore) { return } + $device = Find-Device $InstanceId + $enabled = $global:EMVddTestState.Enabled + $device.Problem = $enabled.Problem + $device.Status = $enabled.Status + $monitor = Find-Device 'DISPLAY\MTT1337\1&2&3&UID256' + if ($enabled.Monitor) { + $device.Children = @($monitor.InstanceId) + $monitor.Status = $enabled.Monitor + } } function Disable-PnpDevice { param($InstanceId, $Confirm) $global:EMVddTestState.Calls += "disable:$InstanceId" - (Get-PnpDevice -InstanceId $InstanceId).Problem = 22 + $device = Find-Device $InstanceId + $device.Problem = 22 + $device.Status = 'Error' + $device.Children = @() } function Expect-Failure([scriptblock]$Run, [string]$Message) { $caught = $null try { & $Run } catch { $caught = $_.Exception.Message } if (!$caught -or !$caught.Contains($Message)) { throw "Expected failure containing '$Message', got '$caught'" } } +# Runs one enable with a short start wait, and checks its exit code. +function Expect-Enable([int]$Code, [string]$Case) { + $global:LASTEXITCODE = 0 + & $toggle -Action enable 2>$null | Out-Null + if ($LASTEXITCODE -ne $Code) { throw "$Case`: expected exit $Code, got $LASTEXITCODE" } +} try { - & $toggle -Action enable - & $toggle -Action disable - if (($global:EMVddTestState.Calls -join ',') -ne 'enable:ROOT\DISPLAY\0007,disable:ROOT\DISPLAY\0007') { + Reset-State + # The Disable task leaves the device at code 22; Enable turns it back on. + $global:LASTEXITCODE = 0 + & $toggle -Action enable | Out-Null + if ($LASTEXITCODE -ne 0) { throw "A disabled device was not re-enabled: exit $LASTEXITCODE" } + & $toggle -Action disable | Out-Null + if ((($global:EMVddTestState.Calls | Where-Object { $_ -like '*:*' }) -join ',') -ne 'enable:ROOT\DISPLAY\0007,disable:ROOT\DISPLAY\0007') { throw 'The toggle selected another GPU or a different virtual driver' } + $vdd = Get-PnpDevice -InstanceId 'ROOT\DISPLAY\0007' + if ($vdd.Problem -ne 22) { throw 'Disable did not leave code 22' } + + # Each PnP call costs about half a second while the device starts: a ready device is checked + # with two after Enable-PnpDevice. + Reset-State + & $toggle -Action enable | Out-Null + $calls = $global:EMVddTestState.Calls + $after = $calls[([array]::IndexOf($calls, 'enable:ROOT\DISPLAY\0007') + 1)..($calls.Count - 1)] + if (($after -join ',') -ne 'property,get') { throw "A ready device took these calls to check: $($after -join ',')" } + & $toggle -Action disable | Out-Null + $global:EMVddTestState.Fail = $true Expect-Failure { & $toggle -Action enable } 'mock driver failure' $global:EMVddTestState.Fail = $false + + # Enable-PnpDevice returned, but Windows kept the device off. $global:EMVddTestState.Ignore = $true - Expect-Failure { & $toggle -Action enable } 'reports 22' - $global:EMVddTestState.Devices = @() - Expect-Failure { & $toggle -Action enable } 'not present' - & $toggle -Action disable - Write-Output 'VDD toggle checks passed: selection, failure, state verification, absent device' + Expect-Enable 122 'still disabled' + $global:EMVddTestState.Ignore = $false + + # Problem codes Windows reports once the device is on: driver error, start + # failure, unverified signature. + foreach ($code in 43, 31, 10, 52, 28) { + Reset-State + $global:EMVddTestState.Enabled = @{Problem=$code; Status='Error'; Monitor=$null} + Expect-Enable (100 + $code) "problem $code" + } + + # Not started or no monitor yet: the monitor can still arrive, so Enable succeeds and the + # host's own wait decides. 0.3.2 works on PCs whose monitor arrives late. + Reset-State + $global:EMVddTestState.Enabled = @{Problem=0; Status='Unknown'; Monitor=$null} + Expect-Enable 0 'not started yet' + + Reset-State + $global:EMVddTestState.Enabled = @{Problem=0; Status='OK'; Monitor=$null} + Expect-Enable 0 'no monitor yet' + $global:EMVddTestState.Enabled = @{Problem=0; Status='OK'; Monitor='Error'} + Expect-Enable 0 'monitor not ready yet' + if (((& $toggle -Action enable) -join ' ') -notmatch 'no monitor arrived yet') { throw 'A device without a monitor yet was not reported' } + + # No waiting: one check after Enable-PnpDevice, whatever it finds. + $global:EMVddTestState.Calls = @() + function Start-Sleep { throw 'Enable waited' } + & $toggle -Action enable | Out-Null + Remove-Item function:Start-Sleep + + Reset-State + (Get-PnpDevice -InstanceId 'ROOT\DISPLAY\0007').Inf = $null + Expect-Enable 3 'no driver bound' + + Reset-State + $global:EMVddTestState.Devices = @($global:EMVddTestState.Devices[0]) + Expect-Enable 2 'absent device' + $global:LASTEXITCODE = 0 + & $toggle -Action disable | Out-Null + if ($LASTEXITCODE -ne 0) { throw 'Disabling an absent device failed' } + Write-Output 'VDD toggle checks passed: selection, re-enable after code 22, two calls per check, still disabled, problem codes, not started or no monitor yet succeed, no waiting, unbound driver, absent device' + # The failure cases leave their exit codes behind; CI ends with this one. + $global:LASTEXITCODE = 0 } finally { - Remove-Variable EMVddTestState -Scope Global + Remove-Variable EMVddTestState -Scope Global -ErrorAction SilentlyContinue }