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
24 changes: 22 additions & 2 deletions build.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,32 @@
// build.rs

use bootloader::DiskImageBuilder;
use bootloader::{BootConfig, DiskImageBuilder};
use std::{env, path::PathBuf};

fn main() {
// set by cargo for the kernel artifact dependency
let kernel_path = env::var("CARGO_BIN_FILE_KERNEL").unwrap();
let disk_builder = DiskImageBuilder::new(PathBuf::from(kernel_path));
let mut disk_builder = DiskImageBuilder::new(PathBuf::from(kernel_path));

// Configure framebuffer resolution from environment variables
// Default: 1920x1080 (16:9, Full HD) - most widely supported by QEMU's UEFI GOP
// Note: 1920x1200 (16:10) is NOT available in QEMU's virtio-vga GOP mode list,
// causing fallback to 1280x800. Use 1920x1080 for reliable high-res graphics.
let fb_width: u64 = env::var("BREENIX_FB_WIDTH")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(1920);
let fb_height: u64 = env::var("BREENIX_FB_HEIGHT")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(1080);

let mut boot_config = BootConfig::default();
boot_config.frame_buffer.minimum_framebuffer_width = Some(fb_width);
boot_config.frame_buffer.minimum_framebuffer_height = Some(fb_height);
disk_builder.set_boot_config(&boot_config);

println!("cargo:warning=Configured framebuffer: {}x{}", fb_width, fb_height);

// specify output paths
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
Expand Down
17 changes: 15 additions & 2 deletions kernel/src/graphics/demo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,19 @@
//! Provides a visual demo of the graphics stack capabilities
//! that runs during kernel boot.

use alloc::string::String;
use super::font::Font;
use super::primitives::{
draw_circle, draw_line, draw_rect, draw_text, fill_circle, fill_rect, Canvas, Color, Rect,
TextStyle,
};

/// Format resolution as "WIDTHxHEIGHT" string
fn format_resolution(width: usize, height: usize) -> String {
use alloc::format;
format!("{}x{}", width, height)
}

/// Run a graphics demonstration on the given canvas.
///
/// This draws various shapes and text to showcase the graphics stack.
Expand Down Expand Up @@ -47,6 +54,13 @@ pub fn run_demo(canvas: &mut impl Canvas) {

draw_text(canvas, 20, 20, "Breenix Graphics Stack Demo", &title_style);

// Resolution info (right-aligned in header)
let res_style = TextStyle::new().with_color(Color::rgb(180, 220, 255));
let res_text = format_resolution(width as usize, height as usize);
// Position on the right side, accounting for approx text width
let res_x = width - 200;
draw_text(canvas, res_x, 20, &res_text, &res_style);

// Draw colorful rectangles
let colors = [
Color::RED,
Expand Down Expand Up @@ -156,7 +170,6 @@ pub fn run_demo(canvas: &mut impl Canvas) {
let red_style = TextStyle::new().with_color(Color::RED);
let green_style = TextStyle::new().with_color(Color::GREEN);
let blue_style = TextStyle::new().with_color(Color::BLUE);
let yellow_style = TextStyle::new().with_color(Color::rgb(255, 255, 0));

draw_text(canvas, 50, text_y + 30, "Red Text", &red_style);
draw_text(canvas, 150, text_y + 30, "Green Text", &green_style);
Expand Down Expand Up @@ -185,7 +198,7 @@ pub fn run_demo(canvas: &mut impl Canvas) {
canvas,
50,
height - 40,
"Phase 4: Text Rendering Complete!",
"Phase 5: Resolution Control Active",
&footer_style,
);

Expand Down
6 changes: 6 additions & 0 deletions kernel/src/graphics/double_buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,12 @@ impl DoubleBufferedFrameBuffer {
&mut self.shadow_buffer
}

/// Get read-only access to the shadow buffer.
#[inline]
pub fn buffer(&self) -> &[u8] {
&self.shadow_buffer
}

/// Copy the shadow buffer to the hardware framebuffer.
///
/// This is the "page flip" operation that makes rendered content visible.
Expand Down
2 changes: 0 additions & 2 deletions kernel/src/graphics/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,3 @@ pub mod font;
pub mod primitives;

pub use double_buffer::DoubleBufferedFrameBuffer;
pub use font::{Font, FontMetrics, FontSize, Glyph, Weight};
pub use primitives::TextStyle;
66 changes: 57 additions & 9 deletions kernel/src/graphics/primitives.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,27 @@ impl Color {

out
}

/// Create a Color from pixel bytes based on pixel format (BGR or RGB).
pub fn from_pixel_bytes(bytes: &[u8], bytes_per_pixel: usize, is_bgr: bool) -> Self {
if bytes_per_pixel == 0 || bytes.is_empty() {
return Color::BLACK;
}

let (r, g, b) = if is_bgr {
let b = bytes[0];
let g = if bytes_per_pixel > 1 && bytes.len() > 1 { bytes[1] } else { 0 };
let r = if bytes_per_pixel > 2 && bytes.len() > 2 { bytes[2] } else { 0 };
(r, g, b)
} else {
let r = bytes[0];
let g = if bytes_per_pixel > 1 && bytes.len() > 1 { bytes[1] } else { 0 };
let b = if bytes_per_pixel > 2 && bytes.len() > 2 { bytes[2] } else { 0 };
(r, g, b)
};

Color::rgb(r, g, b)
}
}

#[derive(Debug, Clone, Copy)]
Expand All @@ -84,8 +105,15 @@ pub trait Canvas {
/// Set a single pixel (must handle bounds checking).
fn set_pixel(&mut self, x: i32, y: i32, color: Color);

/// Get a single pixel color (must handle bounds checking).
/// Returns None if coordinates are out of bounds.
fn get_pixel(&self, x: i32, y: i32) -> Option<Color>;

/// Get buffer for direct access (optional optimization).
fn buffer_mut(&mut self) -> &mut [u8];

/// Get buffer for read access.
fn buffer(&self) -> &[u8];
}

fn to_i32_clamped(value: u32) -> i32 {
Expand Down Expand Up @@ -452,25 +480,25 @@ pub fn draw_char(canvas: &mut impl Canvas, x: i32, y: i32, c: char, style: &Text

/// Draw a glyph at the specified position with the given style.
fn draw_glyph(canvas: &mut impl Canvas, x: i32, y: i32, glyph: &Glyph, style: &TextStyle) {
let threshold = if style.background.is_some() { 0 } else { 32 };

for (gx, gy, intensity) in glyph.pixels() {
if intensity <= threshold {
if intensity == 0 {
continue;
}

let px = x + gx as i32;
let py = y + gy as i32;

let color = if let Some(bg) = style.background {
// Explicit background - blend foreground with specified background
blend_colors(style.foreground, bg, intensity)
} else {
// No background - use intensity to modulate foreground
Color::rgb(
(style.foreground.r as u16 * intensity as u16 / 255) as u8,
(style.foreground.g as u16 * intensity as u16 / 255) as u8,
(style.foreground.b as u16 * intensity as u16 / 255) as u8,
)
// No explicit background - blend with actual canvas pixel for proper anti-aliasing
if let Some(existing) = canvas.get_pixel(px, py) {
blend_colors(style.foreground, existing, intensity)
} else {
// Out of bounds, skip
continue;
}
};

canvas.set_pixel(px, py, color);
Expand Down Expand Up @@ -626,9 +654,29 @@ mod tests {
self.buffer[offset..offset + self.bpp].copy_from_slice(&pixel[..self.bpp]);
}

fn get_pixel(&self, x: i32, y: i32) -> Option<Color> {
if x < 0 || y < 0 {
return None;
}
let x = x as usize;
let y = y as usize;
if x >= self.width || y >= self.height {
return None;
}
let offset = y * self.stride + x * self.bpp;
if offset + self.bpp > self.buffer.len() {
return None;
}
Some(Color::from_pixel_bytes(&self.buffer[offset..offset + self.bpp], self.bpp, self.is_bgr))
}

fn buffer_mut(&mut self) -> &mut [u8] {
&mut self.buffer
}

fn buffer(&self) -> &[u8] {
&self.buffer
}
}

#[test]
Expand Down
34 changes: 34 additions & 0 deletions kernel/src/logger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -583,13 +583,47 @@ impl Canvas for ShellFrameBuffer {
}
}

fn get_pixel(&self, x: i32, y: i32) -> Option<Color> {
if x < 0 || y < 0 {
return None;
}
let x = x as usize;
let y = y as usize;
if x >= self.info.width || y >= self.info.height {
return None;
}

let bytes_per_pixel = self.info.bytes_per_pixel;
let pixel_offset = y * self.info.stride + x;
let byte_offset = pixel_offset * bytes_per_pixel;

let buffer = self.buffer();
if byte_offset + bytes_per_pixel > buffer.len() {
return None;
}

Some(Color::from_pixel_bytes(
&buffer[byte_offset..byte_offset + bytes_per_pixel],
bytes_per_pixel,
self.is_bgr(),
))
}

fn buffer_mut(&mut self) -> &mut [u8] {
if let Some(db) = &mut self.double_buffer {
db.buffer_mut()
} else {
unsafe { core::slice::from_raw_parts_mut(self.buffer_ptr, self.buffer_len) }
}
}

fn buffer(&self) -> &[u8] {
if let Some(db) = &self.double_buffer {
db.buffer()
} else {
unsafe { core::slice::from_raw_parts(self.buffer_ptr, self.buffer_len) }
}
}
}

#[cfg(feature = "interactive")]
Expand Down
4 changes: 4 additions & 0 deletions kernel/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -925,6 +925,10 @@ fn kernel_main_continue() -> ! {
log::info!("=== IPC TEST: Shell pipeline execution ===");
test_exec::test_shell_pipe();

// Test FbInfo syscall (framebuffer information)
log::info!("=== GRAPHICS TEST: FbInfo syscall ===");
test_exec::test_fbinfo();

// Run fault tests to validate privilege isolation
log::info!("=== FAULT TEST: Running privilege violation tests ===");
userspace_fault_tests::run_fault_tests();
Expand Down
2 changes: 2 additions & 0 deletions kernel/src/syscall/dispatcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ pub fn dispatch_syscall(
SyscallNumber::Grantpt => super::pty::sys_grantpt(arg1),
SyscallNumber::Unlockpt => super::pty::sys_unlockpt(arg1),
SyscallNumber::Ptsname => super::pty::sys_ptsname(arg1, arg2, arg3),
// Graphics syscalls (Breenix-specific)
SyscallNumber::FbInfo => super::graphics::sys_fbinfo(arg1),
// Testing/diagnostic syscalls (Breenix-specific)
SyscallNumber::CowStats => super::handlers::sys_cow_stats(arg1),
SyscallNumber::SimulateOom => super::handlers::sys_simulate_oom(arg1),
Expand Down
95 changes: 95 additions & 0 deletions kernel/src/syscall/graphics.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
//! Graphics-related system calls.
//!
//! Provides syscalls for querying framebuffer information.

#[cfg(feature = "interactive")]
use crate::logger::SHELL_FRAMEBUFFER;
use super::SyscallResult;

/// Framebuffer info structure returned by sys_fbinfo.
/// This matches the userspace FbInfo struct in libbreenix.
#[cfg(feature = "interactive")]
#[repr(C)]
pub struct FbInfo {
/// Width in pixels
pub width: u64,
/// Height in pixels
pub height: u64,
/// Stride (pixels per scanline, may be > width for alignment)
pub stride: u64,
/// Bytes per pixel (typically 3 or 4)
pub bytes_per_pixel: u64,
/// Pixel format: 0 = RGB, 1 = BGR, 2 = U8 (grayscale)
pub pixel_format: u64,
}

/// Maximum valid userspace address (canonical lower half)
/// Addresses above this are kernel space and must be rejected.
#[cfg(feature = "interactive")]
const USER_SPACE_MAX: u64 = 0x0000_8000_0000_0000;

/// sys_fbinfo - Get framebuffer information
///
/// # Arguments
/// * `info_ptr` - Pointer to userspace FbInfo structure to fill
///
/// # Returns
/// * 0 on success
/// * -EFAULT if info_ptr is invalid or in kernel space
/// * -ENODEV if no framebuffer is available
#[cfg(feature = "interactive")]
pub fn sys_fbinfo(info_ptr: u64) -> SyscallResult {
// Validate pointer: must be non-null and in userspace address range
if info_ptr == 0 {
return SyscallResult::Err(super::ErrorCode::Fault as u64);
}

// Reject kernel-space pointers to prevent kernel memory corruption
if info_ptr >= USER_SPACE_MAX {
log::warn!("sys_fbinfo: rejected kernel-space pointer {:#x}", info_ptr);
return SyscallResult::Err(super::ErrorCode::Fault as u64);
}

// Validate the entire FbInfo struct fits in userspace
let end_ptr = info_ptr.saturating_add(core::mem::size_of::<FbInfo>() as u64);
if end_ptr > USER_SPACE_MAX {
log::warn!("sys_fbinfo: buffer extends into kernel space");
return SyscallResult::Err(super::ErrorCode::Fault as u64);
}

// Get framebuffer info from the shell framebuffer
let fb = match SHELL_FRAMEBUFFER.get() {
Some(fb) => fb,
None => {
log::warn!("sys_fbinfo: No framebuffer available");
return SyscallResult::Err(super::ErrorCode::InvalidArgument as u64);
}
};

let fb_guard = fb.lock();

// Get info through Canvas trait methods
use crate::graphics::primitives::Canvas;
let info = FbInfo {
width: fb_guard.width() as u64,
height: fb_guard.height() as u64,
stride: fb_guard.stride() as u64,
bytes_per_pixel: fb_guard.bytes_per_pixel() as u64,
pixel_format: if fb_guard.is_bgr() { 1 } else { 0 },
};

// Copy to userspace (pointer already validated above)
unsafe {
let info_out = info_ptr as *mut FbInfo;
core::ptr::write(info_out, info);
}

SyscallResult::Ok(0)
}

/// sys_fbinfo - Stub for non-interactive mode (returns ENODEV)
#[cfg(not(feature = "interactive"))]
pub fn sys_fbinfo(_info_ptr: u64) -> SyscallResult {
// No framebuffer available in non-interactive mode
SyscallResult::Err(super::ErrorCode::InvalidArgument as u64)
}
2 changes: 2 additions & 0 deletions kernel/src/syscall/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,8 @@ pub extern "C" fn rust_syscall_handler(frame: &mut SyscallFrame) {
Some(SyscallNumber::Grantpt) => super::pty::sys_grantpt(args.0),
Some(SyscallNumber::Unlockpt) => super::pty::sys_unlockpt(args.0),
Some(SyscallNumber::Ptsname) => super::pty::sys_ptsname(args.0, args.1, args.2),
// Graphics syscalls
Some(SyscallNumber::FbInfo) => super::graphics::sys_fbinfo(args.0),
None => {
log::warn!("Unknown syscall number: {} - returning ENOSYS", syscall_num);
SyscallResult::Err(super::ErrorCode::NoSys as u64)
Expand Down
Loading
Loading