Files
RedBear-OS/drivers/graphics/fbcond/src/text.rs
T
Red Bear OS 54e89a337a fbcond: perform the display handoff open off the console loop
fbcond blocked indefinitely in open_display_v2() while handing the console
over to the real graphics driver: boot stopped at "fbcond: Performing
handoff" and never reached the "Opened new display" that follows a
successful open.

fbcond is single-threaded and owns the console, so while it waited there it
stopped draining console writers. getty, login and the login shell all
blocked on their writes, which made the failure look like a slow or hung
shell -- the stall point just tracked whichever process next wrote to the
console. If the graphics driver was itself waiting on the console, the two
deadlocked outright and the console never came back.

The wait comes from the scheme-open handshake: inputd signals the handoff as
soon as the driver registers its path, but the driver may not be serving its
scheme yet, and O_NONBLOCK does not bound the open (it governs later reads).

Split the two halves in inputd's ConsumerHandle: display_path_v2() only does
an fpath on our own handle and returns promptly, while open_display_path_v2()
carries the blocking open. fbcond now resolves the path inline and hands the
open to a worker thread, then picks the result up from the event loop via
poll_pending_handoff(). Console output produced meanwhile is buffered in
pending_writes (already the behaviour while the display is unmapped) and
flushed once the new display is mapped.
2026-07-20 18:10:59 +09:00

272 lines
10 KiB
Rust

use std::collections::VecDeque;
use orbclient::{Event, EventOption};
use syscall::error::*;
use crate::display::Display;
use crate::keymap::Keymap;
/// Number of most-recent output lines retained for scrollback, exposed via the
/// `/scheme/fbcon/<vt>/scrollback` read path.
const SCROLLBACK_LINES: usize = 1000;
pub struct TextScreen {
pub display: Display,
inner: console_draw::TextScreen,
ctrl: bool,
input: VecDeque<u8>,
pending_writes: Vec<Vec<u8>>,
/// Ring buffer of the most recent output lines (each ends with '\n'),
/// capped at SCROLLBACK_LINES.
scrollback: VecDeque<Vec<u8>>,
/// Active keyboard layout. Defaults to US QWERTY.
pub keymap: Keymap,
/// Optional mirror of all rendered console output to the kernel serial/debug
/// scheme. Without this, once fbcond takes over the framebuffer console the
/// serial line goes silent — so a headless operator (QEMU `-serial`, a real
/// serial header) sees the boot stop right after "Performing handoff" and
/// never sees the `getty` login prompt (which is drawn to the framebuffer
/// VT). Mirroring makes the same console — boot log and login prompt —
/// visible on serial too.
serial_mirror: Option<std::fs::File>,
/// In-flight handoff: a worker performing the (blocking) open of the new
/// display. See `handle_handoff`.
pending_handoff: Option<std::sync::mpsc::Receiver<std::io::Result<std::fs::File>>>,
}
impl TextScreen {
pub fn new(display: Display) -> TextScreen {
// Open the kernel debug (serial) scheme for write-only mirroring. Best
// effort: if it is unavailable, console output simply isn't mirrored.
let serial_mirror = std::fs::OpenOptions::new()
.write(true)
.open("/scheme/debug")
.ok();
TextScreen {
display,
inner: console_draw::TextScreen::new(),
ctrl: false,
input: VecDeque::new(),
pending_writes: Vec::new(),
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");
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;
}
};
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}"),
}
}
pub fn input(&mut self, event: &Event) {
let mut buf = vec![];
match event.to_option() {
EventOption::Key(key_event) => {
if key_event.scancode == 0x1D {
self.ctrl = key_event.pressed;
} else if key_event.pressed {
let sc = key_event.scancode as u8;
if let Some(seq) = self.keymap.lookup(sc) {
buf.extend_from_slice(seq);
} else {
let c = match key_event.character {
c @ 'A'..='Z' if self.ctrl => ((c as u8 - b'A') + b'\x01') as char,
c @ 'a'..='z' if self.ctrl => ((c as u8 - b'a') + b'\x01') as char,
c => c,
};
if c != '\0' {
let mut b = [0; 4];
buf.extend_from_slice(c.encode_utf8(&mut b).as_bytes());
}
}
}
}
_ => (), //TODO: Mouse in terminal
}
for &b in buf.iter() {
// Translate a bare CR (what the Enter key produces on most keymaps)
// to LF, matching the serial input path (push_input_bytes). Without
// this, line-oriented readers such as a shell's read_line never see
// a completed line from keyboard input and appear to hang.
self.input.push_back(if b == b'\r' { b'\n' } else { b });
}
}
pub fn can_read(&self) -> bool {
!self.input.is_empty()
}
}
impl TextScreen {
pub fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
let mut i = 0;
while i < buf.len() && !self.input.is_empty() {
buf[i] = self.input.pop_front().unwrap();
i += 1;
}
Ok(i)
}
/// Inject raw bytes into this console's input queue, exactly as if they had
/// been typed on the keyboard. Used to feed serial-line input (from the
/// kernel debug scheme) into the console so a headless operator can log in
/// over serial through the same getty that serves the framebuffer VT.
pub fn push_input_bytes(&mut self, bytes: &[u8]) {
for &b in bytes {
// Translate a bare CR (what most serial terminals send on Enter)
// into LF, which is what the tty/line-discipline and login expect.
self.input.push_back(if b == b'\r' { b'\n' } else { b });
}
}
pub fn write(&mut self, buf: &[u8]) -> Result<usize> {
// Retain output for scrollback (ring-buffered by line). Captured
// regardless of display-map state so the boot log before handoff is
// also scrollable.
for line in buf.split_inclusive(|&b| b == b'\n') {
self.scrollback.push_back(line.to_vec());
}
while self.scrollback.len() > SCROLLBACK_LINES {
self.scrollback.pop_front();
}
// Mirror everything rendered to the framebuffer console onto the serial
// line so a headless operator sees the same output (boot log + the getty
// login prompt) instead of silence after the display handoff.
if let Some(serial) = &mut self.serial_mirror {
use std::io::Write;
let _ = serial.write_all(buf);
let _ = serial.flush();
}
if let Some(map) = &mut self.display.map {
Display::handle_resize(map, &mut self.inner);
let damage = self.inner.write(map, buf, &mut self.input);
self.display.sync_rect(damage);
} else {
log::warn!(
"fbcond: TextScreen::write() called while display map is None; \
buffering {} bytes ({} pending chunks). Will flush after handoff.",
buf.len(),
self.pending_writes.len() + 1,
);
self.pending_writes.push(buf.to_vec());
}
Ok(buf.len())
}
/// Return the retained scrollback buffer (oldest line first) as a flat byte
/// stream. Read via the `/scheme/fbcon/<vt>/scrollback` handle.
pub fn read_scrollback(&self) -> Vec<u8> {
let mut result = Vec::new();
for line in &self.scrollback {
result.extend_from_slice(line);
}
result
}
pub fn flush_pending_writes(&mut self) {
if self.pending_writes.is_empty() {
return;
}
let count = self.pending_writes.len();
log::info!(
"fbcond: Flushing {} pending write chunks after display handoff",
count,
);
if let Some(map) = &mut self.display.map {
for pending in self.pending_writes.drain(..) {
Display::handle_resize(map, &mut self.inner);
let damage = self.inner.write(map, &pending, &mut self.input);
map.dirty_fb(damage).unwrap();
}
} else {
log::error!(
"fbcond: flush_pending_writes called but display map is still None; \
keeping {} buffered chunks",
self.pending_writes.len(),
);
}
}
}