From 815a2f9fe2e43943250bea20c2733681ccdfa16e Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Wed, 28 Oct 2026 14:47:06 +0100 Subject: [PATCH] perf(display): present copies window rows as-is (scanout R8G8B8X8 = window byte order, Host hands out row bytes, Framebuffer::write_rgba_row); Window copy_from/copy_to one fenced memcpy; full present ~65ms -> ~3ms --- crates/drivers/framebuffer/virtio/src/lib.rs | 24 +++++++++++--- crates/hw-virtio/src/gpu.rs | 5 +-- crates/libkos/src/window.rs | 22 ++++++++----- crates/ports/framebuffer/src/lib.rs | 15 +++++++++ crates/servers/display-server/src/host.rs | 32 ++++++++----------- crates/servers/display-server/src/server.rs | 18 ++++------- .../display-server/src/tests/server.rs | 27 ++++++++++------ 7 files changed, 88 insertions(+), 55 deletions(-) diff --git a/crates/drivers/framebuffer/virtio/src/lib.rs b/crates/drivers/framebuffer/virtio/src/lib.rs index 20a31c7..462b57b 100644 --- a/crates/drivers/framebuffer/virtio/src/lib.rs +++ b/crates/drivers/framebuffer/virtio/src/lib.rs @@ -37,6 +37,11 @@ pub struct VirtioFramebuffer { dirty: Option<(usize, usize, usize, usize)>, } +/// `c` as the screen stores it: the bytes R, G, B, then one ignored. +fn word(c: Color) -> u32 { + u32::from_le_bytes(c.to_rgba()) +} + impl VirtioFramebuffer { /// Takes over the virtio GPU behind `mmio` and makes a framebuffer its /// scanout. `None` if it is not a GPU or any step fails. @@ -113,6 +118,17 @@ impl Framebuffer for VirtioFramebuffer { self.height } + fn write_rgba_row(&mut self, x: ScreenCoord, y: ScreenCoord, bytes: &[u8]) { + let (w, h) = (self.width.raw() as usize, self.height.raw() as usize); + let (x, y) = (x.raw() as usize, y.raw() as usize); + if x >= w || y >= h { + return; + } + let n = (bytes.len() / FB_BPP).min(w - x); + self.touch(x, y, x + n, y + 1); + self.pixels.copy_from(self.pixel(x, y), &bytes[..n * FB_BPP]); + } + fn write_pixel(&mut self, x: ScreenCoord, y: ScreenCoord, color: Color) { self.write_row(x, y, &[color]); } @@ -129,7 +145,7 @@ impl Framebuffer for VirtioFramebuffer { let mut at = self.pixel(x, y); for chunk in pixels[..n].chunks(RUN) { for (slot, c) in run.iter_mut().zip(chunk) { - *slot = c.raw(); + *slot = word(*c); } self.pixels.write_u32s(at, &run[..chunk.len()]); at = at + chunk.len() * FB_BPP; @@ -144,14 +160,14 @@ impl Framebuffer for VirtioFramebuffer { let bottom = (y + h.raw() as usize).min(fh); self.touch(x, y, x + w, bottom); for row in y..bottom { - self.pixels.fill_u32(self.pixel(x, row), w, color.raw()); + self.pixels.fill_u32(self.pixel(x, row), w, word(color)); } } fn clear(&mut self, color: Color) { let (w, h) = (self.width.raw() as usize, self.height.raw() as usize); self.touch(0, 0, w, h); - self.pixels.fill_u32(ByteOffset::ZERO, w * h, color.raw()); + self.pixels.fill_u32(ByteOffset::ZERO, w * h, word(color)); } fn scroll_up(&mut self, top: ScreenCoord, height: Dimension, by: Dimension, fill: Color) { @@ -163,7 +179,7 @@ impl Framebuffer for VirtioFramebuffer { self.touch(0, top, self.width.raw() as usize, bottom); self.pixels.copy_within(self.pixel(0, top + by), self.pixel(0, top), moved * self.stride); let width = self.width.raw() as usize; - self.pixels.fill_u32(self.pixel(0, bottom - by), by * width, fill.raw()); + self.pixels.fill_u32(self.pixel(0, bottom - by), by * width, word(fill)); } /// Sends the host only what changed since the last flush. diff --git a/crates/hw-virtio/src/gpu.rs b/crates/hw-virtio/src/gpu.rs index 6140bd6..747a892 100644 --- a/crates/hw-virtio/src/gpu.rs +++ b/crates/hw-virtio/src/gpu.rs @@ -28,9 +28,10 @@ const DISPLAY_H: ByteOffset = ByteOffset::new(24 + 12); pub fn create_2d(mmio: &impl MmioAccess, queue: &VirtioQueue, dma: &GpuDmaAddrs, resource_id: u32, width: Dimension, height: Dimension) -> bool { write_cmd_header(&dma.cmd, 0x0101); - // Resource id, format (1: B8G8R8A8), width, height. + // Resource id, format (134: R8G8B8X8, a window's own byte order, so + // its rows are copied as they are), width, height. dma.cmd.write(BODY, resource_id); - dma.cmd.write(BODY + 4, 1u32); + dma.cmd.write(BODY + 4, 134u32); dma.cmd.write(BODY + 8, width.raw() as u32); dma.cmd.write(BODY + 12, height.raw() as u32); submit_cmd(mmio, queue, dma, 40) diff --git a/crates/libkos/src/window.rs b/crates/libkos/src/window.rs index d5d909c..e7f5a45 100644 --- a/crates/libkos/src/window.rs +++ b/crates/libkos/src/window.rs @@ -104,18 +104,24 @@ impl Window { Window { base, len } } + /// `src` into the window at `offset`, as one copy: for memory (pages, + /// DMA buffers), not device registers. Fenced, so a device told + /// afterwards sees all of it. pub fn copy_from(&self, offset: ByteOffset, src: &[u8]) { - self.at(offset, src.len(), 1); - for (i, b) in src.iter().enumerate() { - self.write(offset + i, *b); - } + let addr = self.at(offset, src.len(), 1); + // SAFETY: in range of memory mapped for this process; `src` is + // ordinary memory, so the two cannot overlap. + unsafe { core::ptr::copy_nonoverlapping(src.as_ptr(), addr as *mut u8, src.len()) } + core::sync::atomic::fence(core::sync::atomic::Ordering::SeqCst); } + /// The window's bytes at `offset` into `dst`, as one copy (see + /// `copy_from`); what a device wrote before is seen. pub fn copy_to(&self, offset: ByteOffset, dst: &mut [u8]) { - self.at(offset, dst.len(), 1); - for (i, b) in dst.iter_mut().enumerate() { - *b = self.read(offset + i); - } + let addr = self.at(offset, dst.len(), 1); + core::sync::atomic::fence(core::sync::atomic::Ordering::SeqCst); + // SAFETY: as above. + unsafe { core::ptr::copy_nonoverlapping(addr as *const u8, dst.as_mut_ptr(), dst.len()) } } /// `count` u32s from `offset` set to `value`: one check, then a run of diff --git a/crates/ports/framebuffer/src/lib.rs b/crates/ports/framebuffer/src/lib.rs index 9c55081..5ab21f8 100644 --- a/crates/ports/framebuffer/src/lib.rs +++ b/crates/ports/framebuffer/src/lib.rs @@ -21,6 +21,21 @@ pub trait Framebuffer { } } + /// A run of a window's pixels from (x, y) rightwards, each the bytes + /// R, G, B, A (see `types::display::window::BUFFER`). + fn write_rgba_row(&mut self, x: ScreenCoord, y: ScreenCoord, bytes: &[u8]) { + let mut run = [Color::from_raw(0); 64]; + let mut at = x; + for chunk in bytes.chunks(4 * run.len()) { + let n = chunk.len() / 4; + for (c, px) in run.iter_mut().zip(chunk.as_chunks::<4>().0) { + *c = Color::from_rgba(*px); + } + self.write_row(at, y, &run[..n]); + at = ScreenCoord::new(at.raw().saturating_add(n as u16)); + } + } + /// Moves the band of rows from `top`, `height` tall, up by `by` rows, /// and fills the `by` rows left at its bottom with `fill`. fn scroll_up(&mut self, top: ScreenCoord, height: Dimension, by: Dimension, fill: Color); diff --git a/crates/servers/display-server/src/host.rs b/crates/servers/display-server/src/host.rs index 61f81da..f80815d 100644 --- a/crates/servers/display-server/src/host.rs +++ b/crates/servers/display-server/src/host.rs @@ -3,13 +3,10 @@ //! the display's memory has a fixed ceiling whatever apps do. use libkos::syscall; -use libkos::window::Window; -use types::address::{ByteOffset, VirtualAddress}; +use types::address::VirtualAddress; use types::badge::Badge; use types::bounded::{CapSlot, PageCount}; -use types::display::Color; use types::display::window::PIXEL_BYTES; -use types::mapping::PagePerms; use types::permission::{Permission, PermissionSet}; use crate::server::Host; @@ -26,12 +23,13 @@ pub struct KernelHost { /// from. ep: CapSlot, input: Option, - buffers: [Option<(CapSlot, Window)>; MAX_WINDOWS], + /// Each slot's region and its pixels, mapped here as plain memory. + buffers: [Option<(CapSlot, &'static mut [u8])>; MAX_WINDOWS], } impl KernelHost { pub fn new(ep: CapSlot, input: Option) -> Self { - Self { ep, input, buffers: [None; MAX_WINDOWS] } + Self { ep, input, buffers: [const { None }; MAX_WINDOWS] } } } @@ -43,8 +41,8 @@ impl Host for KernelHost { let pages = PageCount::new(u16::try_from(bytes.div_ceil(PAGE)).ok()?)?; let region = syscall::create_region(pages).ok()?; let va = VirtualAddress::from_raw(BUFFERS_VA + index * BUFFER_SPAN); - match libkos::window::map(region, va, PagePerms::ReadWrite) { - Ok(window) => *slot = Some((region, window)), + match libkos::window::map_buffer(region, va) { + Ok(pixels) => *slot = Some((region, pixels)), Err(_) => { syscall::drop_cap(region); return None; @@ -55,20 +53,16 @@ impl Host for KernelHost { } fn blank(&mut self, index: usize) { - if let Some((_, window)) = self.buffers.get(index).and_then(Option::as_ref) { - window.fill_u32(ByteOffset::ZERO, window.len() / PIXEL_BYTES, 0); + if let Some((_, pixels)) = self.buffers.get_mut(index).and_then(Option::as_mut) { + pixels.fill(0); } } - fn read(&self, index: usize, from: usize, out: &mut [Color]) { - let Some((_, window)) = self.buffers.get(index).and_then(Option::as_ref) else { return }; - for (i, px) in out.iter_mut().enumerate() { - let at = (from + i) * PIXEL_BYTES; - if at + PIXEL_BYTES > window.len() { - return; - } - *px = Color::from_rgba(window.read::(ByteOffset::new(at)).to_le_bytes()); - } + fn pixels(&self, index: usize, from: usize, count: usize) -> &[u8] { + let Some((_, pixels)) = self.buffers.get(index).and_then(Option::as_ref) else { return &[] }; + let start = (from * PIXEL_BYTES).min(pixels.len()); + let end = from.saturating_add(count).saturating_mul(PIXEL_BYTES).min(pixels.len()); + &pixels[start..end] } fn reclaim(&mut self, index: usize) { diff --git a/crates/servers/display-server/src/server.rs b/crates/servers/display-server/src/server.rs index adb991c..c673e72 100644 --- a/crates/servers/display-server/src/server.rs +++ b/crates/servers/display-server/src/server.rs @@ -20,8 +20,6 @@ use crate::windows::{MAX_WINDOWS, Windows}; /// The OS bar: one row of text. pub const BAR_HEIGHT: u16 = font::HEIGHT; -/// Pixels copied per row at a time. -const RUN: usize = 256; /// What the display needs from the kernel. pub trait Host { @@ -31,8 +29,10 @@ pub trait Host { /// Blanks slot `index`'s buffer, so a new window shows nothing of the /// last one's. fn blank(&mut self, index: usize); - /// Pixels of slot `index`'s buffer from pixel `from` on, into `out`. - fn read(&self, index: usize, from: usize, out: &mut [Color]); + /// Up to `count` pixels of slot `index`'s buffer from pixel `from` on, + /// as its bytes (R, G, B, A each); fewer past its end. The app may be + /// drawing into it meanwhile: a torn frame, no worse. + fn pixels(&self, index: usize, from: usize, count: usize) -> &[u8]; /// Takes back every copy of slot `index`'s buffer handed out. fn reclaim(&mut self, index: usize); /// A copy of the display's endpoint carrying `badge`. @@ -141,15 +141,9 @@ impl DisplayServer { let (ww, wh) = (self.window_size().0.raw(), self.window_size().1.raw()); let (x0, y0) = (x.raw().min(ww), y.raw().min(wh)); let (x1, y1) = (x0.saturating_add(width.raw()).min(ww), y0.saturating_add(height.raw()).min(wh)); - let mut run = [Color::from_raw(0); RUN]; for row in y0..y1 { - let mut col = x0; - while col < x1 { - let n = usize::from(x1 - col).min(RUN); - self.host.read(index, row as usize * ww as usize + col as usize, &mut run[..n]); - self.fb.write_row(ScreenCoord::new(col), ScreenCoord::new(row + BAR_HEIGHT), &run[..n]); - col += n as u16; - } + let bytes = self.host.pixels(index, row as usize * ww as usize + x0 as usize, usize::from(x1 - x0)); + self.fb.write_rgba_row(ScreenCoord::new(x0), ScreenCoord::new(row + BAR_HEIGHT), bytes); } self.fb.flush(); } diff --git a/crates/servers/display-server/src/tests/server.rs b/crates/servers/display-server/src/tests/server.rs index 1a3ce41..85c9c19 100644 --- a/crates/servers/display-server/src/tests/server.rs +++ b/crates/servers/display-server/src/tests/server.rs @@ -15,7 +15,8 @@ const H: usize = 16 + 8; /// Buffers in memory; a capability is its slot number plus 40. #[derive(Default)] struct FakeHost { - buffers: Vec>>, + /// Each slot's pixels, the bytes R, G, B, A. + buffers: Vec>>, minted: Vec, revoked: Vec, reclaimed: Vec, @@ -28,17 +29,16 @@ impl Host for FakeHost { if self.buffers.len() <= index { self.buffers.resize(index + 1, None); } - self.buffers[index].get_or_insert_with(|| vec![Color::from_raw(0); pixels]); + self.buffers[index].get_or_insert_with(|| vec![0; pixels * 4]); CapSlot::new(40 + index as u8) } fn blank(&mut self, index: usize) { if let Some(b) = self.buffers[index].as_mut() { - b.fill(Color::from_raw(0)); + b.fill(0); } } - fn read(&self, index: usize, from: usize, out: &mut [Color]) { - let b = self.buffers[index].as_ref().unwrap(); - out.copy_from_slice(&b[from..from + out.len()]); + fn pixels(&self, index: usize, from: usize, count: usize) -> &[u8] { + &self.buffers[index].as_ref().unwrap()[from * 4..(from + count) * 4] } fn reclaim(&mut self, index: usize) { self.reclaimed.push(index); @@ -58,6 +58,13 @@ impl Host for FakeHost { } } +/// A window buffer all one colour, as an app would draw it. +fn fill(buf: &mut [u8], c: Color) { + for px in buf.chunks_exact_mut(4) { + px.copy_from_slice(&c.to_rgba()); + } +} + type Srv = DisplayServer, FakeHost>; fn server() -> Srv { @@ -76,7 +83,7 @@ fn open(s: &mut Srv, opener: usize, name: &[u8]) -> usize { /// Paints window `slot`'s buffer one colour and presents all of it. fn paint(s: &mut Srv, badge: usize, slot: usize, color: Color) -> [usize; 4] { // The app writes its buffer directly; here, through the fake. - s.host_mut_for_test().buffers[slot].as_mut().unwrap().fill(color); + fill(s.host_mut_for_test().buffers[slot].as_mut().unwrap(), color); let rect = w::pack_rect( types::display::ScreenCoord::new(0), types::display::ScreenCoord::new(0), @@ -156,9 +163,9 @@ fn closing_takes_the_window_and_its_buffer_back() { assert_eq!(shown(&s), Some(BLUE), "the terminal is in front again"); assert_eq!(s.handle_call(snake, [w::POLL, 0, 0, 0], None).0[0], MALFORMED, "closed"); // Its buffer is reused, blank, for the next window. - s.host_mut_for_test().buffers[1].as_mut().unwrap().fill(RED); + fill(s.host_mut_for_test().buffers[1].as_mut().unwrap(), RED); open(&mut s, 2, b"Snake"); - assert!(s.host().buffers[1].as_ref().unwrap().iter().all(|c| *c == Color::from_raw(0))); + assert!(s.host().buffers[1].as_ref().unwrap().iter().all(|b| *b == 0)); } #[test] @@ -176,7 +183,7 @@ fn a_launcher_gone_takes_its_windows_with_it() { fn a_present_outside_the_window_is_clipped() { let mut s = server(); let t = open(&mut s, 1, b"Terminal"); - s.host_mut_for_test().buffers[0].as_mut().unwrap().fill(RED); + fill(s.host_mut_for_test().buffers[0].as_mut().unwrap(), RED); let rect = w::pack_rect( types::display::ScreenCoord::new(30), types::display::ScreenCoord::new(5),