mirror of
https://github.com/edera-dev/sprout.git
synced 2026-11-04 17:42:20 +00:00
fix(boot): only redraw what changed in the graphical menu (#121)
Each frame redrew the whole screen and blitted the entire framebuffer. The static background and panel are now cached, and only the changed regions are blitted.
This commit is contained in:
2 files changed
+113
-15
No files matched your search
@@ -359,6 +359,12 @@ struct Screen {
|
|||||||
gop: ScopedProtocol<GraphicsOutput>,
|
gop: ScopedProtocol<GraphicsOutput>,
|
||||||
/// What is drawn before it is blitted to the display.
|
/// What is drawn before it is blitted to the display.
|
||||||
fb: Framebuffer,
|
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.
|
/// Where the entries are on this display.
|
||||||
layout: Layout,
|
layout: Layout,
|
||||||
/// The index of the first entry that is on this display.
|
/// The index of the first entry that is on this display.
|
||||||
@@ -399,13 +405,21 @@ impl Screen {
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let (width, height) = gop.current_mode_info().resolution();
|
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;
|
continue;
|
||||||
};
|
};
|
||||||
let layout = Layout::new((width, height), entries);
|
let layout = Layout::new((width, height), entries);
|
||||||
|
render_base(&mut base, &layout);
|
||||||
screens.push(Self {
|
screens.push(Self {
|
||||||
gop,
|
gop,
|
||||||
fb,
|
fb,
|
||||||
|
shown,
|
||||||
|
base,
|
||||||
|
drawn: false,
|
||||||
layout,
|
layout,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
});
|
});
|
||||||
@@ -598,9 +612,9 @@ fn draw_logo(fb: &mut Framebuffer, layout: &Layout, ticks: usize) {
|
|||||||
let y = floor - rise;
|
let y = floor - rise;
|
||||||
|
|
||||||
for row in 0..height {
|
for row in 0..height {
|
||||||
|
let source_row = row * LOGO_HEIGHT / height * LOGO_WIDTH;
|
||||||
for column in 0..width {
|
for column in 0..width {
|
||||||
let source =
|
let source = (source_row + column * LOGO_WIDTH / width) * 4;
|
||||||
((row * LOGO_HEIGHT / height) * LOGO_WIDTH + column * LOGO_WIDTH / width) * 4;
|
|
||||||
let [red, green, blue, alpha] = LOGO[source..source + 4] else {
|
let [red, green, blue, alpha] = LOGO[source..source + 4] else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
@@ -657,16 +671,9 @@ fn draw_cursor(fb: &mut Framebuffer, scale: usize, position: (usize, usize)) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw the whole menu to the framebuffer.
|
/// Draw the parts of the menu that never change to the framebuffer.
|
||||||
fn render(
|
fn render_base(fb: &mut Framebuffer, layout: &Layout) {
|
||||||
fb: &mut Framebuffer,
|
|
||||||
layout: &Layout,
|
|
||||||
scene: &Scene,
|
|
||||||
offset: usize,
|
|
||||||
cursor: Option<(usize, usize)>,
|
|
||||||
) {
|
|
||||||
draw_background(fb, BACKGROUND_TOP, BACKGROUND_BOTTOM);
|
draw_background(fb, BACKGROUND_TOP, BACKGROUND_BOTTOM);
|
||||||
draw_logo(fb, layout, scene.ticks);
|
|
||||||
if let Some((x, y, scale)) = layout.title {
|
if let Some((x, y, scale)) = layout.title {
|
||||||
draw_text(fb, x, y, scale, TITLE.len(), TITLE, ACCENT);
|
draw_text(fb, x, y, scale, TITLE.len(), TITLE, ACCENT);
|
||||||
}
|
}
|
||||||
@@ -692,6 +699,23 @@ fn render(
|
|||||||
PANEL,
|
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
|
for (slot, (index, entry)) in scene
|
||||||
.entries
|
.entries
|
||||||
.iter()
|
.iter()
|
||||||
@@ -739,10 +763,9 @@ fn render(
|
|||||||
draw_scrollbar(fb, layout, offset, scene.entries.len());
|
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 = panel_width.saturating_sub(layout.cell * 3);
|
||||||
let inner_x = panel_x + layout.cell * 3 / 2;
|
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;
|
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.
|
// 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 };
|
let keep_clear = if layout.scrolls { layout.cell * 8 } else { 0 };
|
||||||
@@ -818,6 +841,7 @@ fn run(
|
|||||||
for screen in screens.iter_mut() {
|
for screen in screens.iter_mut() {
|
||||||
screen.scroll_to(scene.selected);
|
screen.scroll_to(scene.selected);
|
||||||
let cursor = cursor.map(|position| screen.locate(position, bounds));
|
let cursor = cursor.map(|position| screen.locate(position, bounds));
|
||||||
|
screen.fb.copy_from(&screen.base);
|
||||||
render(
|
render(
|
||||||
&mut screen.fb,
|
&mut screen.fb,
|
||||||
&screen.layout,
|
&screen.layout,
|
||||||
@@ -825,7 +849,14 @@ fn run(
|
|||||||
screen.offset,
|
screen.offset,
|
||||||
cursor,
|
cursor,
|
||||||
);
|
);
|
||||||
|
// 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.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.
|
// Wake up every tick to move the logo and update the countdown.
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
use alloc::vec;
|
use alloc::vec;
|
||||||
use alloc::vec::Vec;
|
use alloc::vec::Vec;
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result, bail};
|
||||||
use uefi::proto::console::gop::{BltOp, BltPixel, BltRegion, GraphicsOutput};
|
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.
|
/// Represents the EFI framebuffer.
|
||||||
pub struct Framebuffer {
|
pub struct Framebuffer {
|
||||||
/// The width of the framebuffer in pixels.
|
/// The width of the framebuffer in pixels.
|
||||||
@@ -68,6 +71,11 @@ impl Framebuffer {
|
|||||||
self.pixels.get_mut(index)
|
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].
|
/// Blit the framebuffer to the specified `gop` [GraphicsOutput].
|
||||||
pub fn blit(&self, gop: &mut GraphicsOutput) -> Result<()> {
|
pub fn blit(&self, gop: &mut GraphicsOutput) -> Result<()> {
|
||||||
gop.blt(BltOp::BufferToVideo {
|
gop.blt(BltOp::BufferToVideo {
|
||||||
@@ -79,4 +87,63 @@ impl Framebuffer {
|
|||||||
.context("unable to blit framebuffer")?;
|
.context("unable to blit framebuffer")?;
|
||||||
Ok(())
|
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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user