Skip to content

Commit 84f85b7

Browse files
committed
feat(graphics): Implement framebuffer text console
- Implement framebuffer console capable of drawing crist readable text via pixel plotting - Numbers and symbols are not printed at all TODO - Add global console feature to graphics crate - Implement macro to use framebuffer console print throughout the workspace
1 parent c3157ae commit 84f85b7

File tree

8 files changed

+246
-79
lines changed

8 files changed

+246
-79
lines changed

graphics/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,13 @@ name = "graphics"
33
version = "0.1.0"
44
edition = "2024"
55

6+
[features]
7+
default = []
8+
global-console = ["spin"]
9+
610
[dependencies]
711
limine = "0.5.0"
12+
spin = { version = "0.10.0", optional = true }
813

914
[profile.release]
1015
panic = "abort"

graphics/src/console/font8x16.bin

2 KB
Binary file not shown.

graphics/src/console/mod.rs

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
use core::fmt;
2+
use core::fmt::Write;
3+
use core::ptr::write_volatile;
4+
5+
#[cfg(feature = "global-console")]
6+
use spin::{Mutex, MutexGuard};
7+
8+
//Simple 8x16 font bitmap for ASCII 32..127
9+
//Each character: 16 bytes, each byte: 8 pixels
10+
const FONT_8X16: &[u8] = include_bytes!("font8x16.bin");
11+
12+
#[cfg(feature = "global-console")]
13+
static GLOBAL_CONSOLE: Mutex<Option<FramebufferConsole>> = Mutex::new(None);
14+
15+
pub struct FramebufferConsole {
16+
framebuffer: *mut u8,
17+
width: usize,
18+
height: usize,
19+
pitch: usize,
20+
cursor_x: usize,
21+
cursor_y: usize,
22+
}
23+
24+
unsafe impl Sync for FramebufferConsole {}
25+
unsafe impl Send for FramebufferConsole {}
26+
27+
28+
impl FramebufferConsole {
29+
pub unsafe fn new(framebuffer: *mut u8, width: usize, height: usize, pitch: usize) -> Self {
30+
Self {
31+
framebuffer,
32+
width,
33+
height,
34+
pitch,
35+
cursor_x: 0,
36+
cursor_y: 0,
37+
}
38+
}
39+
40+
fn put_char(&mut self, c: char) {
41+
if c == '\n' {
42+
self.cursor_x = 0;
43+
self.cursor_y += 1;
44+
self.scroll_if_needed();
45+
return;
46+
}
47+
if c == '\r' {
48+
self.cursor_x = 0;
49+
return;
50+
}
51+
self.draw_char(c, self.cursor_x, self.cursor_y);
52+
53+
self.cursor_x += 1;
54+
if self.cursor_x * 8 >= self.width {
55+
self.cursor_x = 0;
56+
self.cursor_y += 1;
57+
self.scroll_if_needed();
58+
}
59+
}
60+
61+
fn scroll_if_needed(&mut self) {
62+
let max_lines = self.height / 16;
63+
if self.cursor_y >= max_lines {
64+
self.scroll_up();
65+
self.cursor_y = max_lines - 1;
66+
}
67+
}
68+
69+
fn scroll_up(&mut self) {
70+
unsafe {
71+
let fb = self.framebuffer;
72+
let pitch = self.pitch;
73+
let height_bytes = self.height * pitch;
74+
75+
//Move framebuffer lines up by one character height (16 pixels)
76+
let src = fb.add(16 * pitch);
77+
core::ptr::copy(src, fb, height_bytes - 16 * pitch);
78+
79+
//Clear the last character line by writing zero
80+
let clear_start = fb.add(height_bytes - 16 * pitch);
81+
for i in 0..(16 * pitch) {
82+
write_volatile(clear_start.add(i), 0);
83+
}
84+
}
85+
}
86+
87+
fn draw_char(&mut self, c: char, x_char: usize, y_char: usize) {
88+
let c = c as u8;
89+
let glyph = if c < 32 || c > 127 {
90+
&FONT_8X16[(b'?' - 32) as usize * 16..][..16]
91+
} else {
92+
&FONT_8X16[(c - 32) as usize * 16..][..16]
93+
};
94+
95+
let fb = self.framebuffer;
96+
let pitch = self.pitch;
97+
let x_pixel = x_char * 8;
98+
let y_pixel = y_char * 16;
99+
100+
unsafe {
101+
for (row, &bits) in glyph.iter().enumerate() {
102+
for bit in 0..8 {
103+
let pixel_on = (bits & (1 << (7 - bit))) != 0;
104+
let pixel = if pixel_on { [0xFF, 0xFF, 0xFF, 0x00] } else { [0x00, 0x00, 0x00, 0x00] };
105+
let offset = (y_pixel + row) * pitch + (x_pixel + bit) * 4;
106+
for p in 0..4 {
107+
write_volatile(fb.add(offset + p), pixel[p]);
108+
}
109+
}
110+
}
111+
}
112+
}
113+
114+
fn write_string(&mut self, s: &str) {
115+
for c in s.chars() {
116+
self.put_char(c);
117+
}
118+
}
119+
}
120+
121+
impl Write for FramebufferConsole {
122+
fn write_str(&mut self, s: &str) -> fmt::Result {
123+
self.write_string(s);
124+
Ok(())
125+
}
126+
}
127+
128+
#[cfg(feature = "global-console")]
129+
pub fn init_console(framebuffer: *mut u8, width: usize, height: usize, pitch: usize) {
130+
let mut con = GLOBAL_CONSOLE.lock();
131+
*con = Some(unsafe { FramebufferConsole::new(framebuffer, width, height, pitch) });
132+
}
133+
134+
#[cfg(feature = "global-console")]
135+
pub fn console() -> impl Write + 'static {
136+
struct ConsoleGuard<'a> {
137+
guard: MutexGuard<'a, Option<FramebufferConsole>>,
138+
}
139+
140+
impl<'a> Write for ConsoleGuard<'a> {
141+
fn write_str(&mut self, s: &str) -> fmt::Result {
142+
if let Some(console) = &mut *self.guard {
143+
console.write_string(s);
144+
Ok(())
145+
} else {
146+
Err(fmt::Error)
147+
}
148+
}
149+
}
150+
ConsoleGuard {
151+
guard: GLOBAL_CONSOLE.lock(),
152+
}
153+
}
154+
155+
#[macro_export]
156+
macro_rules! fb_print {
157+
($($arg:tt)*) => {{
158+
use core::fmt::Write as _;
159+
let _ = $crate::console::console().write_fmt(format_args!($($arg)*));
160+
}};
161+
}
162+
163+
#[macro_export]
164+
macro_rules! fb_println {
165+
() => {
166+
$crate::fb_print!("\n")
167+
};
168+
($fmt:expr) => {
169+
$crate::fb_print!(concat!($fmt, "\n"))
170+
};
171+
($fmt:expr, $($arg:tt)*) => {
172+
$crate::fb_print!(concat!($fmt, "\n"), $($arg)*)
173+
};
174+
}

graphics/src/lib.rs

Lines changed: 52 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,63 @@
11
#![no_std]
2+
3+
pub mod console;
4+
25
use limine::framebuffer::Framebuffer;
3-
use limine::memory_map::{Entry, EntryType};
6+
use limine::memory_map::Entry;
47

58
pub unsafe fn write_pixel(ptr: *mut u8, offset: usize, color: &[u8; 4]) {
6-
core::ptr::copy_nonoverlapping(color.as_ptr(), ptr.add(offset), 4);
9+
unsafe {
10+
core::ptr::copy_nonoverlapping(color.as_ptr(), ptr.add(offset), 4);
11+
}
712
}
813

914
pub fn fill_screen_blue(fb: &Framebuffer) {
10-
let width = fb.width() as usize;
11-
let height = fb.height() as usize;
12-
let pitch = fb.pitch() as usize;
13-
let bpp = fb.bpp() as usize;
14-
let ptr = fb.addr() as *mut u8;
15-
let blue_pixel = [0xFF, 0x00, 0x00, 0x00]; // BGRA
16-
17-
for y in 0..height {
18-
for x in 0..width {
19-
let offset = y * pitch + x * (bpp / 8);
20-
unsafe {
21-
write_pixel(ptr, offset, &blue_pixel);
22-
}
23-
}
24-
}
15+
let width = fb.width() as usize;
16+
let height = fb.height() as usize;
17+
let pitch = fb.pitch() as usize;
18+
let bpp = fb.bpp() as usize;
19+
let ptr = fb.addr() as *mut u8;
20+
let blue_pixel = [0xFF, 0x00, 0x00, 0x00]; // BGRA
21+
22+
for y in 0..height {
23+
for x in 0..width {
24+
let offset = y * pitch + x * (bpp / 8);
25+
unsafe {
26+
write_pixel(ptr, offset, &blue_pixel);
27+
}
28+
}
29+
}
2530
}
2631

2732
pub fn draw_memory_map(fb: &Framebuffer, entries: &[&Entry]) {
28-
use limine::memory_map::EntryType;
29-
30-
let width = fb.width() as usize;
31-
let height = fb.height() as usize;
32-
let pitch = fb.pitch() as usize;
33-
let bpp = fb.bpp() as usize;
34-
let ptr = fb.addr() as *mut u8;
35-
36-
let count = entries.len();
37-
let max_count = width.min(count);
38-
let bar_width = width / max_count.max(1);
39-
40-
for (i, entry) in entries.iter().take(max_count).enumerate() {
41-
let color = match entry.entry_type {
42-
EntryType::USABLE => [0x00, 0xFF, 0x00, 0x00], // green
43-
EntryType::BOOTLOADER_RECLAIMABLE => [0xFF, 0xFF, 0x00, 0x00], // yellow
44-
_ => [0x80, 0x80, 0x80, 0x00], // gray
45-
};
46-
let x_start = i * bar_width;
47-
let x_end = (x_start + bar_width).min(width);
48-
49-
for x in x_start..x_end {
50-
for y in (height - 40)..height {
51-
let offset = y * pitch + x * (bpp / 8);
52-
unsafe {
53-
write_pixel(ptr, offset, &color);
54-
}
55-
}
56-
}
57-
}
33+
use limine::memory_map::EntryType;
34+
35+
let width = fb.width() as usize;
36+
let height = fb.height() as usize;
37+
let pitch = fb.pitch() as usize;
38+
let bpp = fb.bpp() as usize;
39+
let ptr = fb.addr() as *mut u8;
40+
41+
let count = entries.len();
42+
let max_count = width.min(count);
43+
let bar_width = width / max_count.max(1);
44+
45+
for (i, entry) in entries.iter().take(max_count).enumerate() {
46+
let color = match entry.entry_type {
47+
EntryType::USABLE => [0x00, 0xFF, 0x00, 0x00], // green
48+
EntryType::BOOTLOADER_RECLAIMABLE => [0xFF, 0xFF, 0x00, 0x00], // yellow
49+
_ => [0x80, 0x80, 0x80, 0x00], // gray
50+
};
51+
let x_start = i * bar_width;
52+
let x_end = (x_start + bar_width).min(width);
53+
54+
for x in x_start..x_end {
55+
for y in (height - 40)..height {
56+
let offset = y * pitch + x * (bpp / 8);
57+
unsafe {
58+
write_pixel(ptr, offset, &color);
59+
}
60+
}
61+
}
62+
}
5863
}

kernel/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ edition = "2024"
77
limine = "0.5.0"
88
x86_64 = "0.15.2"
99
apic = { path = "../apic" }
10-
graphics = { path = "../graphics" }
10+
graphics = { path = "../graphics", features = ["global-console"] }
1111
hal = { path = "../hal" }
1212
idt = { path = "../idt" }
1313
memory = { path = "../memory" }
1414
util = { path = "../util" }
1515
task = { path = "../task" }
16-
keyboard = { path = "../keyboard" }
16+
spin = "0.10.0"
1717

1818
[profile.release]
1919
panic = "abort"

kernel/src/main.rs

Lines changed: 10 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
extern crate alloc;
55

66
use core::panic::PanicInfo;
7+
use graphics::console::init_console;
78
use graphics::{draw_memory_map, fill_screen_blue};
89
use hal::serial_println;
910
use limine::request::{FramebufferRequest, MemoryMapRequest};
@@ -20,34 +21,6 @@ static FRAMEBUFFER_REQ: FramebufferRequest = FramebufferRequest::new();
2021
static MMAP_REQ: MemoryMapRequest = MemoryMapRequest::new();
2122
static SCHEDULER: TaskManager = TaskManager::new();
2223

23-
/*unsafe extern "C" {
24-
fn context_switch(old: *mut CPUContext, new: *const CPUContext) -> !;
25-
}*/
26-
27-
unsafe extern "C" fn idle_task() -> ! {
28-
serial_println!("Idle task started!");
29-
loop {
30-
serial_println!("Idle task running");
31-
for _ in 0..10000000 {
32-
core::hint::spin_loop();
33-
}
34-
serial_println!("Idle task calling yield");
35-
task::task_yield();
36-
serial_println!("Idle task returned from yield");
37-
}
38-
}
39-
40-
unsafe extern "C" fn task1() -> ! {
41-
serial_println!("Task 1 started!");
42-
loop {
43-
serial_println!("Task 1 running");
44-
for _ in 0..10000000 {
45-
core::hint::spin_loop();
46-
}
47-
task::task_yield();
48-
}
49-
}
50-
5124
#[panic_handler]
5225
pub fn panic(info: &PanicInfo) -> ! {
5326
serial_println!("[KERNEL PANIC]");
@@ -74,7 +47,7 @@ pub extern "C" fn _start() -> ! {
7447
idt::init_idt(); // Setup CPU exception handlers and load IDT
7548

7649
//Init keyboard
77-
hal::serial_println!("Keyboard ready for input!");
50+
serial_println!("Keyboard ready for input!");
7851

7952
//Enable interrupts globally
8053
x86_64::instructions::interrupts::enable();
@@ -125,8 +98,16 @@ pub extern "C" fn _start() -> ! {
12598
draw_memory_map(&fb, mmap_response.entries());
12699
}
127100

101+
let fb = fb_response.framebuffers().next().expect("No framebuffer");
102+
init_console(fb.addr(), fb.width() as usize, fb.height() as usize, fb.pitch() as usize);
103+
128104
// Initialize global scheduler
129105
Scheduler::init_global();
106+
107+
// Test framebuffer console with macros
108+
graphics::fb_println!("Welcome to Serix OS!");
109+
graphics::fb_println!("Memory map entries: {}", mmap_response.entries().len());
110+
130111
loop {
131112
hlt()
132113
}

keyboard/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ edition = "2024"
55

66
[dependencies]
77
hal = { path = "../hal" }
8-
x86_64 = "0.15.2"
8+
x86_64 = "0.15.2"
9+
graphics = { path = "../graphics" }

0 commit comments

Comments
 (0)