Refactor framebuffer

This commit is contained in:
Qingsong Chen 2025-04-29 08:45:08 +00:00 committed by Tate, Hongliang Tian
parent ef898e572c
commit 88f08de3af
10 changed files with 494 additions and 204 deletions

2
Cargo.lock generated
View File

@ -116,6 +116,7 @@ dependencies = [
name = "aster-framebuffer"
version = "0.1.0"
dependencies = [
"aster-console",
"component",
"font8x8",
"log",
@ -184,6 +185,7 @@ dependencies = [
"aster-bigtcp",
"aster-block",
"aster-console",
"aster-framebuffer",
"aster-input",
"aster-logger",
"aster-mlsdisk",

View File

@ -11,6 +11,7 @@ aster-input = { path = "comps/input" }
aster-block = { path = "comps/block" }
aster-network = { path = "comps/network" }
aster-console = { path = "comps/console" }
aster-framebuffer = { path = "comps/framebuffer" }
aster-softirq = { path = "comps/softirq" }
aster-logger = { path = "comps/logger" }
aster-mlsdisk = { path = "comps/mlsdisk" }

View File

@ -8,11 +8,10 @@ edition = "2021"
[dependencies]
ostd = { path = "../../../ostd" }
component = { path = "../../libs/comp-sys/component" }
aster-console = { path = "../console" }
log = "0.4"
spin = "0.9.4"
font8x8 = { version = "0.2.5", default-features = false, features = [
"unicode",
] }
font8x8 = { version = "0.2.5", default-features = false, features = [ "unicode" ] }
[lints]
workspace = true

View File

@ -0,0 +1,212 @@
// SPDX-License-Identifier: MPL-2.0
use alloc::{sync::Arc, vec::Vec};
use aster_console::{AnyConsoleDevice, ConsoleCallback};
use font8x8::UnicodeFonts;
use ostd::{
sync::{LocalIrqDisabled, SpinLock},
Error, Result,
};
use spin::Once;
use crate::{FrameBuffer, Pixel, FRAMEBUFFER};
/// The font width in pixels when using `font8x8`.
const FONT_WIDTH: usize = 8;
/// The font height in pixels when using `font8x8`.
const FONT_HEIGHT: usize = 8;
/// A text console rendered onto the framebuffer.
#[derive(Debug)]
pub struct FramebufferConsole {
state: SpinLock<ConsoleState, LocalIrqDisabled>,
}
pub static CONSOLE_NAME: &str = "Framebuffer-Console";
pub static FRAMEBUFFER_CONSOLE: Once<Arc<FramebufferConsole>> = Once::new();
pub(crate) fn init() {
let Some(fb) = FRAMEBUFFER.get() else {
log::warn!("Framebuffer not initialized");
return;
};
FRAMEBUFFER_CONSOLE.call_once(|| Arc::new(FramebufferConsole::new(fb.clone())));
}
impl AnyConsoleDevice for FramebufferConsole {
fn send(&self, buf: &[u8]) {
self.state.lock().send_buf(buf);
}
fn register_callback(&self, _: &'static ConsoleCallback) {
// Unsupported, do nothing.
}
}
impl FramebufferConsole {
/// Creates a new framebuffer console.
pub fn new(framebuffer: Arc<FrameBuffer>) -> Self {
let bytes = alloc::vec![0u8; framebuffer.size()];
Self {
state: SpinLock::new(ConsoleState {
enabled: true,
x_pos: 0,
y_pos: 0,
fg_color: Pixel::WHITE,
bg_color: Pixel::BLACK,
bytes,
backend: framebuffer,
}),
}
}
/// Returns whether the console is enabled.
pub fn is_enabled(&self) -> bool {
self.state.lock().enabled
}
/// Enables the console.
pub fn enable(&self) {
self.state.lock().enabled = true;
}
/// Disables the console.
pub fn disable(&self) {
self.state.lock().enabled = false;
}
/// Returns the current cursor position.
pub fn cursor(&self) -> (usize, usize) {
let state = self.state.lock();
(state.x_pos, state.y_pos)
}
/// Sets the cursor position.
pub fn set_cursor(&self, x: usize, y: usize) -> Result<()> {
let mut state = self.state.lock();
if x > state.backend.width() - FONT_WIDTH || y > state.backend.height() - FONT_HEIGHT {
log::warn!("Invalid framebuffer cursor position: ({}, {})", x, y);
return Err(Error::InvalidArgs);
}
state.x_pos = x;
state.y_pos = y;
Ok(())
}
/// Returns the foreground color.
pub fn fg_color(&self) -> Pixel {
self.state.lock().fg_color
}
/// Sets the foreground color.
pub fn set_fg_color(&self, val: Pixel) {
self.state.lock().fg_color = val;
}
/// Returns the background color.
pub fn bg_color(&self) -> Pixel {
self.state.lock().bg_color
}
/// Sets the background color.
pub fn set_bg_color(&self, val: Pixel) {
self.state.lock().bg_color = val;
}
}
#[derive(Debug)]
struct ConsoleState {
// FIXME: maybe we should drop the whole `ConsoleState` when it's disabled.
enabled: bool,
x_pos: usize,
y_pos: usize,
fg_color: Pixel,
bg_color: Pixel,
bytes: Vec<u8>,
backend: Arc<FrameBuffer>,
}
impl ConsoleState {
fn carriage_return(&mut self) {
self.x_pos = 0;
}
fn newline(&mut self) {
if self.y_pos >= self.backend.height() - FONT_HEIGHT {
self.shift_lines_up();
}
self.y_pos += FONT_HEIGHT;
self.x_pos = 0;
}
fn shift_lines_up(&mut self) {
let offset = self.backend.calc_offset(0, FONT_HEIGHT).as_usize();
self.bytes.copy_within(offset.., 0);
self.bytes[self.backend.size() - offset..].fill(0);
self.backend.write_bytes_at(0, &self.bytes).unwrap();
self.y_pos -= FONT_HEIGHT;
}
/// Sends a single character to be drawn on the framebuffer.
fn send_char(&mut self, c: char) {
if c == '\n' {
self.newline();
return;
} else if c == '\r' {
self.carriage_return();
return;
}
if self.x_pos + FONT_WIDTH > self.backend.width() {
self.newline();
}
let rendered = font8x8::BASIC_FONTS
.get(c)
.expect("character not found in basic font");
let fg_pixel = self.backend.render_pixel(self.fg_color);
let bg_pixel = self.backend.render_pixel(self.bg_color);
let mut offset = self.backend.calc_offset(self.x_pos, self.y_pos);
for byte in rendered.iter() {
for bit in 0..8 {
let on = *byte & (1 << bit) != 0;
let pixel = if on { fg_pixel } else { bg_pixel };
// Cache the rendered pixel
self.bytes[offset.as_usize()..offset.as_usize() + pixel.nbytes()]
.copy_from_slice(pixel.as_slice());
// Write the pixel to the framebuffer
self.backend.write_pixel_at(offset, pixel).unwrap();
offset.x_add(1);
}
offset.x_add(-(FONT_WIDTH as isize));
offset.y_add(1);
}
self.x_pos += FONT_WIDTH;
}
/// Sends a buffer of bytes to be drawn on the framebuffer.
///
/// # Panics
///
/// This method will panic if the buffer contains any characters
/// other than Basic Latin characters (`U+0000` - `U+007F`).
fn send_buf(&mut self, buf: &[u8]) {
if !self.enabled {
return;
}
// TODO: handle ANSI escape sequences.
for &byte in buf.iter() {
if byte != 0 {
let char = char::from_u32(byte as u32).unwrap();
self.send_char(char);
}
}
}
}

View File

@ -0,0 +1,147 @@
// SPDX-License-Identifier: MPL-2.0
use alloc::sync::Arc;
use ostd::{boot::boot_info, io::IoMem, mm::VmIo, Result};
use spin::Once;
use crate::{Pixel, PixelFormat, RenderedPixel};
/// The framebuffer used for text or graphical output.
///
/// # Notes
///
/// It is highly recommended to use a synchronization primitive, such as a `SpinLock`, to
/// lock the framebuffer before performing any operation on it.
/// Failing to properly synchronize access can result in corrupted framebuffer content
/// or unspecified behavior during rendering.
#[derive(Debug)]
pub struct FrameBuffer {
io_mem: IoMem,
width: usize,
height: usize,
pixel_format: PixelFormat,
}
pub static FRAMEBUFFER: Once<Arc<FrameBuffer>> = Once::new();
pub(crate) fn init() {
let Some(framebuffer_arg) = boot_info().framebuffer_arg else {
log::warn!("Framebuffer not found");
return;
};
if framebuffer_arg.address == 0 {
log::error!("Framebuffer address is zero");
return;
}
// FIXME: There are several pixel formats that have the same BPP. We lost the information
// during the boot phase, so here we guess the pixel format on a best effort basis.
let pixel_format = match framebuffer_arg.bpp {
8 => PixelFormat::Grayscale8,
16 => PixelFormat::Rgb565,
24 => PixelFormat::Rgb888,
32 => PixelFormat::BgrReserved,
_ => {
log::error!(
"Unsupported framebuffer pixel format: {} bpp",
framebuffer_arg.bpp
);
return;
}
};
let framebuffer = {
let fb_base = framebuffer_arg.address;
let fb_size = framebuffer_arg.width
* framebuffer_arg.height
* (framebuffer_arg.bpp / u8::BITS as usize);
let io_mem = IoMem::acquire(fb_base..fb_base + fb_size).unwrap();
FrameBuffer {
io_mem,
width: framebuffer_arg.width,
height: framebuffer_arg.height,
pixel_format,
}
};
framebuffer.clear();
FRAMEBUFFER.call_once(|| Arc::new(framebuffer));
}
impl FrameBuffer {
/// Returns the size of the framebuffer in bytes.
pub fn size(&self) -> usize {
self.io_mem.length()
}
/// Returns the width of the framebuffer in pixels.
pub fn width(&self) -> usize {
self.width
}
/// Returns the height of the framebuffer in pixels.
pub fn height(&self) -> usize {
self.height
}
/// Returns the pixel format of the framebuffer.
pub fn pixel_format(&self) -> PixelFormat {
self.pixel_format
}
/// Renders the pixel according to the pixel format of the framebuffer.
pub fn render_pixel(&self, pixel: Pixel) -> RenderedPixel {
pixel.render(self.pixel_format)
}
/// Calculates the offset of a pixel at the specified position.
pub fn calc_offset(&self, x: usize, y: usize) -> PixelOffset {
PixelOffset {
fb: self,
offset: ((y * self.width + x) * self.pixel_format.nbytes()) as isize,
}
}
/// Writes a pixel at the specified position.
pub fn write_pixel_at(&self, offset: PixelOffset, pixel: RenderedPixel) -> Result<()> {
self.io_mem.write_bytes(offset.as_usize(), pixel.as_slice())
}
/// Writes raw bytes at the specified offset.
pub fn write_bytes_at(&self, offset: usize, bytes: &[u8]) -> Result<()> {
self.io_mem.write_bytes(offset, bytes)
}
/// Clears the framebuffer with default color (black).
pub fn clear(&self) {
let frame = alloc::vec![0u8; self.size()];
self.write_bytes_at(0, &frame).unwrap();
}
}
/// The offset of a pixel in the framebuffer.
#[derive(Debug, Clone, Copy)]
pub struct PixelOffset<'a> {
fb: &'a FrameBuffer,
offset: isize,
}
impl PixelOffset<'_> {
/// Adds the specified delta to the x coordinate.
pub fn x_add(&mut self, x_delta: isize) {
let delta = x_delta * self.fb.pixel_format.nbytes() as isize;
self.offset += delta;
}
/// Adds the specified delta to the y coordinate.
pub fn y_add(&mut self, y_delta: isize) {
let delta = y_delta * (self.fb.width * self.fb.pixel_format.nbytes()) as isize;
self.offset += delta;
}
pub fn as_usize(&self) -> usize {
self.offset as _
}
}

View File

@ -6,209 +6,18 @@
extern crate alloc;
use alloc::{vec, vec::Vec};
use core::{
fmt,
ops::{Index, IndexMut},
};
mod console;
mod framebuffer;
mod pixel;
use component::{init_component, ComponentInitError};
use font8x8::UnicodeFonts;
use ostd::{
boot::{boot_info, memory_region::MemoryRegionType},
io::IoMem,
mm::{VmIo, PAGE_SIZE},
sync::SpinLock,
};
use spin::Once;
pub use console::{FramebufferConsole, CONSOLE_NAME, FRAMEBUFFER_CONSOLE};
pub use framebuffer::{FrameBuffer, FRAMEBUFFER};
pub use pixel::{Pixel, PixelFormat, RenderedPixel};
#[init_component]
fn framebuffer_init() -> Result<(), ComponentInitError> {
init();
fn init() -> Result<(), ComponentInitError> {
framebuffer::init();
console::init();
Ok(())
}
pub(crate) static WRITER: Once<SpinLock<Writer>> = Once::new();
// ignore the warnings since we use the `todo!` macro.
#[expect(unused_variables)]
#[expect(unreachable_code)]
#[expect(clippy::diverging_sub_expression)]
pub(crate) fn init() {
let mut writer = {
let Some(framebuffer) = boot_info().framebuffer_arg else {
return;
};
let mut size = 0;
for region in boot_info().memory_regions.iter() {
if region.typ() == MemoryRegionType::Framebuffer {
size = region.len();
}
}
let page_size = size / PAGE_SIZE;
let start_paddr = framebuffer.address;
let io_mem = todo!("IoMem is private for components now, should fix it.");
let mut buffer: Vec<u8> = vec![0; size];
log::debug!("Found framebuffer:{:?}", framebuffer);
Writer {
io_mem,
x_pos: 0,
y_pos: 0,
bytes_per_pixel: (framebuffer.bpp / 8),
width: framebuffer.width,
height: framebuffer.height,
buffer: buffer.leak(),
}
};
writer.clear();
WRITER.call_once(|| SpinLock::new(writer));
}
pub(crate) struct Writer {
io_mem: IoMem,
/// FIXME: remove buffer. The meaning of buffer is to facilitate the various operations of framebuffer
buffer: &'static mut [u8],
bytes_per_pixel: usize,
width: usize,
height: usize,
x_pos: usize,
y_pos: usize,
}
impl Writer {
fn newline(&mut self) {
self.y_pos += 8;
self.carriage_return();
}
fn carriage_return(&mut self) {
self.x_pos = 0;
}
/// Erases all text on the screen
pub fn clear(&mut self) {
self.x_pos = 0;
self.y_pos = 0;
self.buffer.fill(0);
self.io_mem.write_bytes(0, self.buffer).unwrap();
}
/// Everything moves up one letter in size
fn shift_lines_up(&mut self) {
let offset = self.bytes_per_pixel * 8;
self.buffer.copy_within(offset.., 0);
self.io_mem.write_bytes(0, self.buffer).unwrap();
self.y_pos -= 8;
}
fn width(&self) -> usize {
self.width
}
fn height(&self) -> usize {
self.height
}
fn write_char(&mut self, c: char) {
match c {
'\n' => self.newline(),
'\r' => self.carriage_return(),
c => {
if self.x_pos >= self.width() {
self.newline();
}
while self.y_pos >= (self.height() - 8) {
self.shift_lines_up();
}
let rendered = font8x8::BASIC_FONTS
.get(c)
.expect("character not found in basic font");
self.write_rendered_char(rendered);
}
}
}
fn write_rendered_char(&mut self, rendered_char: [u8; 8]) {
for (y, byte) in rendered_char.iter().enumerate() {
for (x, bit) in (0..8).enumerate() {
let on = *byte & (1 << bit) != 0;
self.write_pixel(self.x_pos + x, self.y_pos + y, on);
}
}
self.x_pos += 8;
}
fn write_pixel(&mut self, x: usize, y: usize, on: bool) {
let pixel_offset = y * self.width + x;
let color = if on {
[0x33, 0xff, 0x66, 0]
} else {
[0, 0, 0, 0]
};
let bytes_per_pixel = self.bytes_per_pixel;
let byte_offset = pixel_offset * bytes_per_pixel;
self.buffer
.index_mut(byte_offset..(byte_offset + bytes_per_pixel))
.copy_from_slice(&color[..bytes_per_pixel]);
self.io_mem
.write_bytes(
byte_offset,
self.buffer
.index(byte_offset..(byte_offset + bytes_per_pixel)),
)
.unwrap();
}
/// Writes the given ASCII string to the buffer.
///
/// Wraps lines at `BUFFER_WIDTH`. Supports the `\n` newline character. Does **not**
/// support strings with non-ASCII characters, since they can't be printed in the VGA text
/// mode.
fn write_string(&mut self, s: &str) {
for char in s.chars() {
self.write_char(char);
}
}
}
impl fmt::Write for Writer {
fn write_str(&mut self, s: &str) -> fmt::Result {
self.write_string(s);
Ok(())
}
}
/// Like the `print!` macro in the standard library, but prints to the VGA text buffer.
#[macro_export]
macro_rules! print {
($($arg:tt)*) => ($crate::_print(format_args!($($arg)*)));
}
/// Like the `println!` macro in the standard library, but prints to the VGA text buffer.
#[macro_export]
macro_rules! println {
() => ($crate::print!("\n"));
($($arg:tt)*) => ($crate::print!("{}\n", format_args!($($arg)*)));
}
/// Prints the given formatted string to the VGA text buffer
/// through the global `WRITER` instance.
#[doc(hidden)]
pub fn _print(args: fmt::Arguments) {
use core::fmt::Write;
WRITER
.get()
.unwrap()
.disable_irq()
.lock()
.write_fmt(args)
.unwrap();
}

View File

@ -0,0 +1,104 @@
// SPDX-License-Identifier: MPL-2.0
/// Individual pixel data containing raw channel values.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub struct Pixel {
pub red: u8,
pub green: u8,
pub blue: u8,
}
/// Pixel format that defines the memory layout of each pixel in the framebuffer.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum PixelFormat {
/// Each pixel uses 8 bits to represent its grayscale intensity, ranging from 0 (black) to 255 (white).
Grayscale8,
/// Each pixel uses 16 bits, with 5 bits for Red, 6 bits for Green, and 5 bits for Blue.
Rgb565,
/// Each pixel uses 24 bits, with 8 bits for Red, 8 bits for Green, and 8 bits for Blue.
Rgb888,
/// Each pixel uses 32 bits, with 8 bits for Blue, 8 bits for Green, 8 bits for Red, and 8 bits reserved.
BgrReserved,
}
/// A rendered pixel in a specific format.
#[derive(Debug, Copy, Clone)]
pub struct RenderedPixel {
buf: [u8; 4],
len: u8,
}
impl Pixel {
/// Renders the pixel into a specific format.
pub fn render(&self, format: PixelFormat) -> RenderedPixel {
let mut buf = [0; 4];
match format {
PixelFormat::Grayscale8 => {
// Calculate the grayscale value
let red_weight = 77 * self.red as u16; // Equivalent to 0.299 * 256
let green_weight = 150 * self.green as u16; // Equivalent to 0.587 * 256
let blue_weight = 29 * self.blue as u16; // Equivalent to 0.114 * 256
let grayscale = (red_weight + green_weight + blue_weight) >> 8; // Normalize to 0-255
buf[0] = grayscale as u8;
RenderedPixel { buf, len: 1 }
}
PixelFormat::Rgb565 => {
let r = (self.red >> 3) as u16; // Red (5 bits)
let g = (self.green >> 2) as u16; // Green (6 bits)
let b = (self.blue >> 3) as u16; // Blue (5 bits)
let rgb565 = (r << 11) | (g << 5) | b; // Combine into RGB565 format
buf[0..2].copy_from_slice(&rgb565.to_be_bytes());
RenderedPixel { buf, len: 2 }
}
PixelFormat::Rgb888 => {
buf[0] = self.red;
buf[1] = self.green;
buf[2] = self.blue;
RenderedPixel { buf, len: 3 }
}
PixelFormat::BgrReserved => {
buf[0] = self.blue;
buf[1] = self.green;
buf[2] = self.red;
RenderedPixel { buf, len: 4 }
}
}
}
}
impl PixelFormat {
/// Returns the number of bytes per pixel (color depth).
pub fn nbytes(&self) -> usize {
match self {
PixelFormat::Grayscale8 => 1,
PixelFormat::Rgb565 => 2,
PixelFormat::Rgb888 => 3,
PixelFormat::BgrReserved => 4,
}
}
}
impl RenderedPixel {
/// Returns the number of bytes in the rendered pixel.
pub fn nbytes(&self) -> usize {
self.len as usize
}
/// Returns a slice to the rendered pixel data.
pub fn as_slice(&self) -> &[u8] {
&self.buf[..self.nbytes()]
}
}
impl Pixel {
pub const WHITE: Pixel = Pixel {
red: 0xFF,
green: 0xFF,
blue: 0xFF,
};
pub const BLACK: Pixel = Pixel {
red: 0x00,
green: 0x00,
blue: 0x00,
};
}

View File

@ -1,5 +1,8 @@
// SPDX-License-Identifier: MPL-2.0
use alloc::string::ToString;
use aster_framebuffer::{CONSOLE_NAME, FRAMEBUFFER_CONSOLE};
use log::info;
pub fn init() {
@ -7,4 +10,8 @@ pub fn init() {
for (name, _) in aster_input::all_devices() {
info!("Found Input device, name:{}", name);
}
if let Some(console) = FRAMEBUFFER_CONSOLE.get() {
aster_console::register_device(CONSOLE_NAME.to_string(), console.clone());
}
}

View File

@ -32,6 +32,7 @@
#![feature(associated_type_defaults)]
#![register_tool(component_access_control)]
use aster_framebuffer::FRAMEBUFFER_CONSOLE;
use kcmdline::KCmdlineArg;
use ostd::{
arch::qemu::{exit_qemu, QemuExitCode},
@ -145,6 +146,13 @@ fn init_thread() {
print_banner();
// FIXME: CI fails due to suspected performance issues with the framebuffer console.
// Additionally, userspace program may render GUIs using the framebuffer,
// so we disable the framebuffer console here.
if let Some(console) = FRAMEBUFFER_CONSOLE.get() {
console.disable();
};
let karg: KCmdlineArg = boot_info().kernel_cmdline.as_str().into();
let initproc = Process::spawn_user_process(

View File

@ -12,6 +12,7 @@
# - VSOCK: "off" or "on";
# - SMP: number of CPUs;
# - MEM: amount of memory, e.g. "8G".
# - VNC_PORT: VNC port, default is "42".
OVMF=${OVMF:-"on"}
VHOST=${VHOST:-"off"}
@ -78,7 +79,7 @@ COMMON_QEMU_ARGS="\
-m ${MEM:-8G} \
--no-reboot \
-nographic \
-display none \
-display vnc=0.0.0.0:${VNC_PORT:-42} \
-serial chardev:mux \
-monitor chardev:mux \
-chardev stdio,id=mux,mux=on,signal=off,logfile=qemu.log \