Skip to content
Merged
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
61 changes: 46 additions & 15 deletions crates/boot/src/menu/graphical.rs
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,12 @@ struct Screen {
gop: ScopedProtocol<GraphicsOutput>,
/// What is drawn before it is blitted to the display.
fb: Framebuffer,
/// What was last blitted to the display, which only has to be updated where it differs.
shown: Framebuffer,
/// The parts of the menu that never change, which each frame starts from.
base: Framebuffer,
/// Whether the display has had a frame blitted to it.
drawn: bool,
/// Where the entries are on this display.
layout: Layout,
/// The index of the first entry that is on this display.
Expand Down Expand Up @@ -399,13 +405,21 @@ impl Screen {
continue;
};
let (width, height) = gop.current_mode_info().resolution();
let Ok(fb) = Framebuffer::new(width, height) else {
let (Ok(fb), Ok(shown), Ok(mut base)) = (
Framebuffer::new(width, height),
Framebuffer::new(width, height),
Framebuffer::new(width, height),
) else {
continue;
};
let layout = Layout::new((width, height), entries);
render_base(&mut base, &layout);
screens.push(Self {
gop,
fb,
shown,
base,
drawn: false,
layout,
offset: 0,
});
Expand Down Expand Up @@ -598,9 +612,9 @@ fn draw_logo(fb: &mut Framebuffer, layout: &Layout, ticks: usize) {
let y = floor - rise;

for row in 0..height {
let source_row = row * LOGO_HEIGHT / height * LOGO_WIDTH;
for column in 0..width {
let source =
((row * LOGO_HEIGHT / height) * LOGO_WIDTH + column * LOGO_WIDTH / width) * 4;
let source = (source_row + column * LOGO_WIDTH / width) * 4;
let [red, green, blue, alpha] = LOGO[source..source + 4] else {
continue;
};
Expand Down Expand Up @@ -657,16 +671,9 @@ fn draw_cursor(fb: &mut Framebuffer, scale: usize, position: (usize, usize)) {
}
}

/// Draw the whole menu to the framebuffer.
fn render(
fb: &mut Framebuffer,
layout: &Layout,
scene: &Scene,
offset: usize,
cursor: Option<(usize, usize)>,
) {
/// Draw the parts of the menu that never change to the framebuffer.
fn render_base(fb: &mut Framebuffer, layout: &Layout) {
draw_background(fb, BACKGROUND_TOP, BACKGROUND_BOTTOM);
draw_logo(fb, layout, scene.ticks);
if let Some((x, y, scale)) = layout.title {
draw_text(fb, x, y, scale, TITLE.len(), TITLE, ACCENT);
}
Expand All @@ -692,6 +699,23 @@ fn render(
PANEL,
);

// The line between the entries and the status line.
let inner = panel_width.saturating_sub(layout.cell * 3);
let inner_x = panel_x + layout.cell * 3 / 2;
fb.fill_rect(inner_x, layout.footer, inner, layout.scale, PANEL_EDGE);
}

/// Draw the whole menu to the framebuffer, which already has the [render_base] drawn.
fn render(
fb: &mut Framebuffer,
layout: &Layout,
scene: &Scene,
offset: usize,
cursor: Option<(usize, usize)>,
) {
draw_logo(fb, layout, scene.ticks);

let (panel_x, _, panel_width, _) = layout.panel;
for (slot, (index, entry)) in scene
.entries
.iter()
Expand Down Expand Up @@ -739,10 +763,9 @@ fn render(
draw_scrollbar(fb, layout, offset, scene.entries.len());
}

// The status line is below a line across the panel, with the position when it scrolls.
// The status line is below the line across the panel, with the position when it scrolls.
let inner = panel_width.saturating_sub(layout.cell * 3);
let inner_x = panel_x + layout.cell * 3 / 2;
fb.fill_rect(inner_x, layout.footer, inner, layout.scale, PANEL_EDGE);
let status_row = layout.footer + layout.scale + layout.cell / 2;
// The position is at the right edge, so the status keeps clear of it on both sides.
let keep_clear = if layout.scrolls { layout.cell * 8 } else { 0 };
Expand Down Expand Up @@ -818,14 +841,22 @@ fn run(
for screen in screens.iter_mut() {
screen.scroll_to(scene.selected);
let cursor = cursor.map(|position| screen.locate(position, bounds));
screen.fb.copy_from(&screen.base);
render(
&mut screen.fb,
&screen.layout,
&scene,
screen.offset,
cursor,
);
screen.fb.blit(&mut screen.gop)?;
// Only the first frame has to fill the display, the rest only change parts of it.
if screen.drawn {
screen.fb.blit_changes(&screen.shown, &mut screen.gop)?;
} else {
screen.fb.blit(&mut screen.gop)?;
screen.drawn = true;
}
core::mem::swap(&mut screen.fb, &mut screen.shown);
}

// Wake up every tick to move the logo and update the countdown.
Expand Down
69 changes: 68 additions & 1 deletion crates/eficore/src/framebuffer.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
use alloc::vec;
use alloc::vec::Vec;
use anyhow::{Context, Result};
use anyhow::{Context, Result, bail};
use uefi::proto::console::gop::{BltOp, BltPixel, BltRegion, GraphicsOutput};

/// How many unchanged rows may sit between changed rows before they are blitted separately.
const BAND_GAP: usize = 8;

/// Represents the EFI framebuffer.
pub struct Framebuffer {
/// The width of the framebuffer in pixels.
Expand Down Expand Up @@ -68,6 +71,11 @@ impl Framebuffer {
self.pixels.get_mut(index)
}

/// Replace the pixels of the framebuffer with those of `other`, which must be the same size.
pub fn copy_from(&mut self, other: &Framebuffer) {
self.pixels.copy_from_slice(&other.pixels);
}

/// Blit the framebuffer to the specified `gop` [GraphicsOutput].
pub fn blit(&self, gop: &mut GraphicsOutput) -> Result<()> {
gop.blt(BltOp::BufferToVideo {
Expand All @@ -79,4 +87,63 @@ impl Framebuffer {
.context("unable to blit framebuffer")?;
Ok(())
}

/// Blit only what is different from `previous`, which must be the same size, to the specified
/// `gop` [GraphicsOutput]. Changes that are far apart vertically are blitted separately, so
/// that the pixels between them are not sent to the display.
pub fn blit_changes(&self, previous: &Framebuffer, gop: &mut GraphicsOutput) -> Result<()> {
if self.width != previous.width || self.height != previous.height {
bail!("framebuffer sizes differ");
}

// The top, bottom, left and right of the changed rows that haven't been blitted yet.
let mut band: Option<(usize, usize, usize, usize)> = None;
let mut quiet = 0;
for row in 0..self.height {
let range = row * self.width..(row + 1) * self.width;
let (now, before) = (&self.pixels[range.clone()], &previous.pixels[range]);
let differs = |(now, before): (&BltPixel, &BltPixel)| {
(now.red, now.green, now.blue) != (before.red, before.green, before.blue)
};
let first = now.iter().zip(before).position(differs);
let Some(first) = first else {
quiet += 1;
if quiet > BAND_GAP
&& let Some(band) = band.take()
{
self.blit_region(gop, band)?;
}
continue;
};
let last = now.iter().zip(before).rposition(differs).unwrap_or(first);
quiet = 0;
band = Some(match band {
Some((top, _, left, right)) => (top, row + 1, left.min(first), right.max(last + 1)),
None => (row, row + 1, first, last + 1),
});
}
if let Some(band) = band {
self.blit_region(gop, band)?;
}
Ok(())
}

/// Blit the region of the `top`, `bottom`, `left` and `right` to the specified `gop`.
fn blit_region(
&self,
gop: &mut GraphicsOutput,
(top, bottom, left, right): (usize, usize, usize, usize),
) -> Result<()> {
gop.blt(BltOp::BufferToVideo {
buffer: &self.pixels,
src: BltRegion::SubRectangle {
coords: (left, top),
px_stride: self.width,
},
dest: (left, top),
dims: (right - left, bottom - top),
})
.context("unable to blit framebuffer region")?;
Ok(())
}
}
Loading