Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions tests/checks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,11 @@ mod checks {
screen_checks::self_test();
}

#[test]
fn firmware_rows_at_the_edge() -> Result<(), String> {
screen_checks::edge_self_test()
}

#[test]
fn metal_audio_judges() -> Result<(), String> {
audio_checks::judges_verdict()
Expand Down
109 changes: 109 additions & 0 deletions tests/checks/screen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,112 @@ fn console_self_test() {
console test could pass on a screen the console never touched"
);
}

/// [`Ppm::firmware_text_mode`], [`Ppm::edge_rows`] and [`TextMode::panel`]
/// against the T14's 1920x1080 panel drawn the way edk2's graphics console
/// draws it, in the two text modes firmware has chosen there: 80x50, whose
/// text starts 640 pixels in and which these lines scroll, and 240x56, whose
/// text starts at the edge and which they do not. In each, a panel that
/// stopped before the console's last lines, as the exclusive-GOP control's
/// does, must not read as carrying them.
pub fn edge_self_test() -> Result<(), String> {
let short: Vec<String> = (0..45).map(|n| format!("Kernel memory size: {n}")).collect();
let wide = format!("Slot A: signed header {} verifies", "0123456789abcdef".repeat(9));
let lines: Vec<&str> = short
.iter()
.map(String::as_str)
.chain([
wide.as_str(),
"Boot attempts: this image has had the machine 0 time(s) without reporting; now 1",
"",
" Kernel memory located past the edge",
"Loader log: the kernel handoff begins",
])
.collect();
// Counted by hand: at 80 columns the lines take 45 + 3 + 2 + 1 + 1 + 1 = 53
// rows and the first four scroll off; at 240 they take 50.
for (staged, lit) in [
(TextMode { columns: 80, rows: 50, left: 640, top: 65 }, 41 + 3 + 1 + 1),
(TextMode { columns: 240, rows: 56, left: 0, top: 8 }, 45 + 1 + 1 + 1),
] {
let dump = edk2_panel(1920, 1080, staged, &lines);
let mode = dump.firmware_text_mode()?;
let (carried, printed) = (dump.edge_rows(mode), mode.panel(lines.iter().copied()));
let count = |rows: &[bool]| rows.iter().filter(|&&row| row).count();
if (mode, count(&carried), count(&printed)) != (staged, lit, lit) || carried != printed {
return Err(format!(
"a panel staged in {staged:?} with {lit} rows lit at its edge was read as {mode:?}, \
carrying {} where the console put {}",
count(&carried),
count(&printed)
));
}
let stopped = edk2_panel(1920, 1080, staged, &lines[..short.len()]);
let read = stopped.firmware_text_mode()?;
if read != staged || stopped.edge_rows(read) == printed {
return Err(format!(
"a panel staged in {staged:?} that stopped after {} of the console's {} lines was \
read as {read:?}, carrying {} where the console put {}",
short.len(),
lines.len(),
count(&stopped.edge_rows(read)),
count(&printed)
));
}
}
Ok(())
}

/// `lines` printed in `mode` on a cleared `width` x `height` panel, over a
/// block the size of edk2's `Logo.bmp` centred on it. Every cell a line
/// reaches is blitted whole, a glyph lighting the pixels an 8x19 capital does;
/// a line wraps once its row is full; and a line feed on the last row moves
/// the text area's pixels, the logo's with them, up a row.
fn edk2_panel(width: usize, height: usize, mode: TextMode, lines: &[&str]) -> Ppm {
let (text, logo) = ([0x98u8; 3], [0xFFu8; 3]);
let (logo_w, logo_h) = (193, 58);
let mut pixels = vec![[0u8; 3]; width * height];
let (lx, ly) = ((width - logo_w) / 2, (height - logo_h) / 2);
for y in ly..ly + logo_h {
pixels[y * width + lx..][..logo_w].fill(logo);
}
let area = mode.columns * EFI_GLYPH_WIDTH;
let line_feed = |pixels: &mut Vec<[u8; 3]>, row: &mut usize| {
if *row + 1 < mode.rows {
*row += 1;
return;
}
for y in mode.top..mode.top + (mode.rows - 1) * EFI_GLYPH_HEIGHT {
let from = (y + EFI_GLYPH_HEIGHT) * width + mode.left;
pixels.copy_within(from..from + area, y * width + mode.left);
}
for y in mode.top + (mode.rows - 1) * EFI_GLYPH_HEIGHT..mode.top + mode.rows * EFI_GLYPH_HEIGHT {
pixels[y * width + mode.left..][..area].fill([0; 3]);
}
};
let (mut row, mut column) = (0, 0);
for line in lines {
for c in line.chars() {
for gy in 0..EFI_GLYPH_HEIGHT {
for gx in 0..EFI_GLYPH_WIDTH {
let lit = !c.is_whitespace() && (3..=14).contains(&gy) && gx <= 6;
let (x, y) = (mode.left + column * EFI_GLYPH_WIDTH + gx, mode.top + row * EFI_GLYPH_HEIGHT + gy);
pixels[y * width + x] = if lit { text } else { [0; 3] };
}
}
column += 1;
if column == mode.columns {
column = 0;
line_feed(&mut pixels, &mut row);
}
}
column = 0;
line_feed(&mut pixels, &mut row);
}

let mut ppm = format!("P6\n{width} {height}\n255\n").into_bytes();
for p in &pixels {
ppm.extend_from_slice(p);
}
Ppm::parse(&ppm)
}
156 changes: 112 additions & 44 deletions tests/common/screen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,51 +173,53 @@ impl Ppm {
None
}

/// How many rows of text are on the panel, counted without decoding a
/// glyph: no firmware font is committed here, so a row one drew is
/// invisible to [`Ppm::text`].
///
/// A band taller than the pitch is a logo or two rows that touch, and
/// neither is one row of text; the pitch is the median distance between
/// band tops, because a stray scanline sets a minimum of one and drags a
/// mean as well. A panel with fewer than two bands has no pitch to take,
/// and is refused rather than counted as none.
pub fn text_row_bands(&self) -> Result<usize, String> {
let mut bands: Vec<(usize, usize)> = Vec::new();
let mut top = None;
for y in 0..self.height {
let lit = (0..self.width)
.any(|x| self.pixels[y * self.width + x].iter().any(|c| *c >= FG_THRESHOLD));
match (lit, top) {
(true, None) => top = Some(y),
(false, Some(from)) => {
bands.push((from, y));
top = None;
}
_ => {}
}
}
if let Some(from) = top {
bands.push((from, self.height));
/// The firmware text mode this panel's text was drawn in, found without
/// decoding a glyph: no firmware font is committed here, so a row one drew
/// is invisible to [`Ppm::text`]. Of the modes [`TextMode::offered`], it is
/// the one whose first column of cells holds the panel's leftmost lit
/// pixel, where every row of its text starts, and whose rows hold all that
/// is lit in its first [`EDGE_CELLS`] cells.
pub fn firmware_text_mode(&self) -> Result<TextMode, String> {
let leftmost = (0..self.width)
.find(|&x| (0..self.height).any(|y| self.bit(x, y)))
.ok_or("the panel has no lit pixel, so no text mode drew on it")?;
let fits: Vec<TextMode> = TextMode::offered(self.width, self.height)
.into_iter()
.filter(|mode| (mode.left..mode.left + EFI_GLYPH_WIDTH).contains(&leftmost))
.filter(|mode| {
let area = mode.top..mode.top + mode.rows * EFI_GLYPH_HEIGHT;
(0..self.height).all(|y| area.contains(&y) || !self.edge_lit(*mode, y))
})
.collect();
match fits[..] {
[mode] => Ok(mode),
_ => Err(format!(
"the panel's text starts at x={leftmost}, and {} of the text modes edk2 offers on \
a {}x{} panel, not one, start there and hold it: {fits:?}",
fits.len(),
self.width,
self.height
)),
}
let mut gaps: Vec<usize> = bands.windows(2).map(|pair| pair[1].0 - pair[0].0).collect();
if gaps.is_empty() {
return Err(format!(
"the panel carries {} band(s) of lit scanlines, too few to take a row pitch from",
bands.len()
));
}
gaps.sort_unstable();
let pitch = gaps[gaps.len() / 2];
let rows = bands.iter().filter(|(from, to)| to - from <= pitch).count();
if rows == 0 {
return Err(format!(
"every one of the panel's {} band(s) is taller than the {pitch}-pixel pitch, so \
none of them is a row of text",
bands.len()
));
}
Ok(rows)
}

/// Each of `mode`'s rows, `true` where a pixel of its first [`EDGE_CELLS`]
/// cells is lit. A firmware console blits a glyph's whole cell and nothing
/// outside it, and edk2 centres its boot logo as it centres `mode`, so a
/// logo narrower than 512 pixels never reaches these cells of a mode 80 or
/// more columns wide.
pub fn edge_rows(&self, mode: TextMode) -> Vec<bool> {
(0..mode.rows)
.map(|row| {
let top = mode.top + row * EFI_GLYPH_HEIGHT;
(top..top + EFI_GLYPH_HEIGHT).any(|y| self.edge_lit(mode, y))
})
.collect()
}

/// Whether scanline `y` is lit in `mode`'s first [`EDGE_CELLS`] cells.
fn edge_lit(&self, mode: TextMode, y: usize) -> bool {
(mode.left..mode.left + EDGE_CELLS * EFI_GLYPH_WIDTH).any(|x| self.bit(x, y))
}

/// The fill colour, read from the bottom-right pixel. The renderer paints
Expand All @@ -238,6 +240,72 @@ impl Ppm {
}
}

/// A firmware console's glyph cell: UEFI's `EFI_GLYPH_WIDTH` by `EFI_GLYPH_HEIGHT`.
pub const EFI_GLYPH_WIDTH: usize = 8;
pub const EFI_GLYPH_HEIGHT: usize = 19;

/// The cells at the start of a firmware console's row that [`Ppm::edge_rows`]
/// reads: every row of its text starts there, and its boot logo never reaches them.
pub const EDGE_CELLS: usize = 8;

/// A text mode of edk2's graphics console: `columns` by `rows` cells centred on
/// the panel, the first of them `left` pixels in and `top` down.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TextMode {
pub columns: usize,
pub rows: usize,
pub left: usize,
pub top: usize,
}

impl TextMode {
/// The text modes edk2's graphics console offers on a `width` x `height`
/// panel, each once and each centred (`InitializeGraphicsConsoleTextMode`):
/// UEFI's 80x25 and 80x50, then `mGraphicsConsoleModeData`'s, the last of
/// them the whole panel, of all these the ones that fit it.
pub fn offered(width: usize, height: usize) -> Vec<TextMode> {
let whole = (width / EFI_GLYPH_WIDTH, height / EFI_GLYPH_HEIGHT);
let mut modes: Vec<TextMode> = Vec::new();
for (columns, rows) in [(80, 25), (80, 50), (100, 31), (128, 40), (160, 42), (240, 56), whole] {
let (w, h) = (columns * EFI_GLYPH_WIDTH, rows * EFI_GLYPH_HEIGHT);
if w > width || h > height {
continue;
}
let mode = TextMode { columns, rows, left: (width - w) >> 1, top: (height - h) >> 1 };
if !modes.contains(&mode) {
modes.push(mode);
}
}
modes
}

/// Each of this mode's rows once `lines` are printed onto a cleared panel,
/// `true` where one of its first [`EDGE_CELLS`] characters draws. A line
/// wraps every `columns` characters, and one that fills its last row wraps
/// before its own line feed moves the cursor again, which leaves a blank
/// row (`GraphicsConsoleConOutOutputString`); a line feed on the last row
/// scrolls the text up a row and blanks the last.
pub fn panel<'a>(self, lines: impl IntoIterator<Item = &'a str>) -> Vec<bool> {
let mut panel = vec![false; self.rows];
let mut cursor = 0;
for line in lines {
let chars: Vec<char> = line.chars().collect();
let blank = chars.len().is_multiple_of(self.columns).then_some(false);
let rows = chars.chunks(self.columns).map(|row| row.iter().take(EDGE_CELLS).any(|c| !c.is_whitespace()));
for row in rows.chain(blank) {
panel[cursor] = row;
if cursor + 1 == self.rows {
panel.remove(0);
panel.push(false);
} else {
cursor += 1;
}
}
}
panel
}
}

/// Cells of the console's font, in the alpha values it blits.
const CELL: usize = GLYPH_W * GLYPH_H;

Expand Down
Loading
Loading