diff --git a/drivers/graphics/fbcond/src/display.rs b/drivers/graphics/fbcond/src/display.rs index a0bc8200f1..32d9719d96 100644 --- a/drivers/graphics/fbcond/src/display.rs +++ b/drivers/graphics/fbcond/src/display.rs @@ -22,6 +22,44 @@ impl Display { Ok(display) } + /// Resolve the display path to hand to a worker performing the blocking open. + pub fn display_path(&self) -> io::Result { + self.input_handle.display_path_v2() + } + + /// Install a display from a file opened elsewhere (see `display_path`). + /// + /// Returns whether the display is now mapped. Both steps here talk to a + /// driver that has already answered our open, so they do not reintroduce the + /// unbounded wait that opening it does. + pub fn install_display_file(&mut self, display_file: std::fs::File) -> bool { + let new_display_handle = match V2GraphicsHandle::from_file(display_file) { + Ok(handle) => handle, + Err(err) => { + log::debug!("fbcond: display not ready at handoff (from_file: {err}); will retry"); + return false; + } + }; + + log::info!("fbcond: Opened new display"); + + match V2DisplayMap::new(new_display_handle) { + Ok(map) => { + log::info!( + "fbcond: Mapped new display with size {}x{}", + map.buffer.buffer().size().0, + map.buffer.buffer().size().1, + ); + self.map = Some(map); + true + } + Err(err) => { + log::error!("fbcond: failed to map new display: {err}"); + false + } + } + } + /// Re-open the display after a handoff. pub fn reopen_for_handoff(&mut self) { let display_file = match self.input_handle.open_display_v2() { diff --git a/drivers/graphics/fbcond/src/main.rs b/drivers/graphics/fbcond/src/main.rs index 0139450fc7..993b318bb3 100644 --- a/drivers/graphics/fbcond/src/main.rs +++ b/drivers/graphics/fbcond/src/main.rs @@ -84,6 +84,13 @@ fn daemon(daemon: daemon::SchemeDaemon) -> ! { for event in event_queue { let event = event.expect("fbcond: failed to read event from event queue"); + // Pick up any display handoff whose (blocking) open has completed on its + // worker. Cheap and non-blocking; done per event so the swap lands as + // soon as the driver starts serving, without the console ever waiting on + // it. Output produced meanwhile is buffered and flushed on install. + for vt in scheme.vts.values_mut() { + vt.poll_pending_handoff(); + } if let Some(fd) = serial_fd { if event.user_data == VtIndex::SERIAL_SENTINEL { let mut buf = [0u8; 256]; diff --git a/drivers/graphics/fbcond/src/text.rs b/drivers/graphics/fbcond/src/text.rs index e74ffad4a0..cc6d718bf9 100644 --- a/drivers/graphics/fbcond/src/text.rs +++ b/drivers/graphics/fbcond/src/text.rs @@ -29,6 +29,9 @@ pub struct TextScreen { /// VT). Mirroring makes the same console — boot log and login prompt — /// visible on serial too. serial_mirror: Option, + /// In-flight handoff: a worker performing the (blocking) open of the new + /// display. See `handle_handoff`. + pending_handoff: Option>>, } impl TextScreen { @@ -48,33 +51,81 @@ impl TextScreen { scrollback: VecDeque::with_capacity(SCROLLBACK_LINES), keymap: Keymap::us(), serial_mirror, + pending_handoff: None, + } + } + + /// Pick up a handoff started by `handle_handoff`, if the worker has finished. + /// + /// Never blocks. Called from the event loop, so the console keeps being + /// serviced while the open is outstanding; writes made in the meantime are + /// buffered in `pending_writes` and flushed once the display is mapped. + pub fn poll_pending_handoff(&mut self) { + let Some(rx) = &self.pending_handoff else { + return; + }; + match rx.try_recv() { + Ok(Ok(display_file)) => { + self.pending_handoff = None; + if self.display.install_display_file(display_file) { + self.flush_pending_writes(); + } else { + log::warn!( + "fbcond: handoff display opened but could not be mapped; \ + framebuffer console stays on the previous display" + ); + } + } + Ok(Err(err)) => { + self.pending_handoff = None; + log::warn!( + "fbcond: handoff open failed ({err}); framebuffer console stays \ + on the previous display" + ); + } + Err(std::sync::mpsc::TryRecvError::Empty) => {} + Err(std::sync::mpsc::TryRecvError::Disconnected) => { + self.pending_handoff = None; + log::warn!("fbcond: handoff worker exited without a result"); + } } } pub fn handle_handoff(&mut self) { log::info!("fbcond: Performing handoff"); - // The handoff is one-shot: inputd signals it the moment the real display - // driver registers its path, but the driver's scheme may not serve the - // v2 open / capability query for a few more milliseconds. Without a - // retry, a transient not-ready driver leaves the framebuffer permanently - // blank (the login prompt is drawn to the framebuffer VT, so the console - // appears dead even though boot continues on the serial mirror). Retry - // briefly — the driver becomes ready well within this bound in practice. - for attempt in 0..250u32 { - self.display.reopen_for_handoff(); - if self.display.map.is_some() { - if attempt > 0 { - log::info!("fbcond: handoff completed after {attempt} retries"); - } - self.flush_pending_writes(); + + if self.pending_handoff.is_some() { + log::debug!("fbcond: handoff already in flight; ignoring repeat signal"); + return; + } + + // Resolving the path only talks to inputd and returns promptly; the + // subsequent open of the graphics driver's scheme is what can block + // indefinitely (O_NONBLOCK does not bound the scheme-open handshake, so + // a driver that has registered its path but is not yet serving leaves us + // waiting on it). fbcond is single-threaded and owns the console, so + // doing that open inline stops us draining console writers -- getty, + // login and the login shell then block on their writes, and if the + // driver is itself waiting on the console the two deadlock and the whole + // console dies right after this message. Hand the open to a worker and + // keep serving the console; writes are buffered until it lands. + let display_path = match self.display.display_path() { + Ok(path) => path, + Err(err) => { + log::warn!("fbcond: could not resolve display path for handoff: {err}"); return; } - std::thread::sleep(std::time::Duration::from_millis(10)); + }; + + let (tx, rx) = std::sync::mpsc::channel(); + match std::thread::Builder::new() + .name("fbcond-handoff".to_owned()) + .spawn(move || { + let _ = tx.send(inputd::ConsumerHandle::open_display_path_v2(&display_path)); + }) { + Ok(_) => self.pending_handoff = Some(rx), + Err(err) => log::warn!("fbcond: could not spawn handoff worker: {err}"), } - log::warn!( - "fbcond: display still not ready after handoff retries; \ - framebuffer console will remain blank (serial mirror unaffected)" - ); } pub fn input(&mut self, event: &Event) { diff --git a/drivers/inputd/src/lib.rs b/drivers/inputd/src/lib.rs index b42ad7ee80..a3026650bc 100644 --- a/drivers/inputd/src/lib.rs +++ b/drivers/inputd/src/lib.rs @@ -78,6 +78,17 @@ impl ConsumerHandle { } pub fn open_display_v2(&self) -> io::Result { + let display_path_str = self.display_path_v2()?; + Self::open_display_path_v2(&display_path_str) + } + + /// Resolve the v2 display path for this consumer handle. + /// + /// This only talks to inputd (an `fpath` on our own handle), so it returns + /// promptly. Separated from the open below so that a caller with a + /// single-threaded event loop can resolve the path inline and perform the + /// blocking open elsewhere. + pub fn display_path_v2(&self) -> io::Result { let mut buffer = [0; 1024]; let fd = self.0.as_raw_fd(); let written = libredox::call::fpath(fd as usize, &mut buffer)?; @@ -120,6 +131,18 @@ impl ConsumerHandle { ) })?; + Ok(display_path_str.to_owned()) + } + + /// Open a v2 display path previously resolved by [`Self::display_path_v2`]. + /// + /// NOTE: this can block indefinitely. `O_NONBLOCK` governs later reads, not + /// the scheme-open handshake, so if the graphics driver has registered its + /// path but is not yet serving its scheme, the open waits for it. Callers + /// that own a single-threaded event loop (fbcond owns the console) must not + /// call this inline, or they stop servicing their own clients while it + /// waits — which deadlocks if the driver is in turn waiting on them. + pub fn open_display_path_v2(display_path_str: &str) -> io::Result { let display_file = libredox::call::open(display_path_str, (O_CLOEXEC | O_NONBLOCK | O_RDWR) as _, 0) .map(|socket| unsafe { File::from_raw_fd(socket as RawFd) })