Better virtual file system, keyboard through MMIO&VirtIO

This commit is contained in:
2026-03-05 14:41:28 +01:00
parent 041e544330
commit 9b6aec28f5
37 changed files with 1191 additions and 355 deletions

View File

@@ -7,7 +7,12 @@ use core::arch::naked_asm;
use crate::{
clear_csr,
interrupt::{setup_machine_trap_handler, setup_supervisor_trap_handler},
mret, set_csr, supervisor_mode_entry, write_csr,
mret, set_csr, supervisor_mode_entry,
virtio::{
Virtqueue,
input::{VirtioInputDriver, init_plic_m_mode},
},
write_csr,
};
pub mod sbi;
@@ -45,6 +50,9 @@ pub extern "C" fn machine_mode_entry() {
set_csr!(mideleg, 1 << 5);
set_csr!(medeleg, 1 << 8);
// Delegate PLIC keyboard interrupt
set_csr!(mideleg, 1 << 9);
unsafe {
setup_supervisor_trap_handler();
};

124
src/draw.rs Normal file
View File

@@ -0,0 +1,124 @@
use kernel_macros::include_font_plate;
/// 24-bit RGB color used by the framebuffer.
#[repr(transparent)]
#[derive(Clone, Copy)]
pub struct Color(u32);
#[allow(unused)]
impl Color {
pub const fn from_rgb(r: u8, g: u8, b: u8) -> Self {
Self((r as u32) << 16 | (g as u32) << 8 | b as u32)
}
pub fn as_bytes(&self) -> [u8; 4] {
self.0.to_ne_bytes()
}
pub const WHITE: Color = Color::from_rgb(255, 255, 255);
pub const GRAY: Color = Color::from_rgb(128, 128, 128);
pub const LIGHT_GRAY: Color = Color::from_rgb(128, 64, 64);
pub const BLACK: Color = Color::from_rgb(0, 0, 0);
pub const RED: Color = Color::from_rgb(255, 0, 0);
pub const GREEN: Color = Color::from_rgb(0, 255, 0);
pub const BLUE: Color = Color::from_rgb(0, 0, 255);
}
pub trait Draw {
/// # Safety
/// `x` must be less than `WIDTH` and `y` must be less than `HEIGHT`
/// Write a single pixel into the framebuffer (unsafe).
///
/// Caller must ensure `x < WIDTH` and `y < HEIGHT`.
unsafe fn write_pixel_unsafe(&mut self, x: u16, y: u16, color: Color);
fn get_width(&self) -> usize;
fn get_height(&self) -> usize;
/// Draw a single character with a background color at (x,y).
///
/// Uses the embedded font plate to render glyphs into the framebuffer.
unsafe fn draw_char_bg(&mut self, x: u16, y: u16, c: char, color: Color, bg_color: Color) {
let c = if (c as u8 > b'~') || ((c as u8) < b' ') {
b'/' - b' '
} else {
c as u8 - b' '
};
// Get char position within font plate
let char_x = (c as usize % 32) * FONT_WIDTH;
let char_y = (c as usize / 32) * FONT_HEIGHT;
for i in 0..(FONT_WIDTH as u16) {
for j in 0..(FONT_HEIGHT as u16) {
let xx = x + i;
let yy = y + j;
if xx < (self.get_width() as u16) && yy < (self.get_height() as u16) {
if unsafe { Self::font_plate_index(char_x as u16 + i, char_y as u16 + j) } {
unsafe { self.write_pixel_unsafe(xx, yy, color) }
} else {
unsafe { self.write_pixel_unsafe(xx, yy, bg_color) }
}
}
}
}
}
/// # Safety
/// The text must have a length that can fit within a `u16`
/// Draw a UTF-8 string at the given position.
///
/// Newlines (`\n`) advance `y` by `FONT_HEIGHT` and carriage return (`\r`)
/// resets to the starting `x` position.
unsafe fn draw_string<T: AsRef<str>>(
&mut self,
x: u16,
mut y: u16,
str: T,
color: Color,
bg_color: Color,
) {
let mut current_x = x;
str.as_ref().chars().for_each(|c| unsafe {
match c {
'\n' => {
current_x = x;
y += FONT_HEIGHT as u16;
}
'\r' => {
current_x = x;
}
c => {
self.draw_char_bg(current_x, y, c, color, bg_color);
current_x += FONT_WIDTH as u16;
}
}
});
}
/// Fill the entire framebuffer with a single color.
fn clear_screen(&mut self, color: Color) {
for y in 0..self.get_height() {
for x in 0..self.get_width() {
unsafe { self.write_pixel_unsafe(x as u16, y as u16, color) };
}
}
}
/// Return whether a pixel inside the embedded font plate is set.
unsafe fn font_plate_index(x: u16, y: u16) -> bool {
let pixel_index = (y as usize) * FONTPLATE_WIDTH + (x as usize);
let byte_index = pixel_index / 8;
let bit_index = pixel_index % 8;
(FONTPLATE[byte_index] >> bit_index) & 0b1 == 0b1
}
}
pub const FONT_WIDTH: usize = 6;
pub const FONT_HEIGHT: usize = 13;
pub const FONTPLATE_WIDTH: usize = 32 * FONT_WIDTH;
pub const FONTPLATE_HEIGHT: usize = 3 * FONT_HEIGHT;
pub const FONTPLATE_SIZE: usize = FONTPLATE_WIDTH * FONTPLATE_HEIGHT / 8;
pub static FONTPLATE: [u8; FONTPLATE_SIZE] = include_font_plate! {"assets/fontplate.png"};

118
src/fs.rs
View File

@@ -2,44 +2,18 @@
//!
//! Implements a minimal disk backend and exposes a global FILE_SYSTEM used by
//! the kernel to load user binaries.
use core::{cell::UnsafeCell, ops::Deref};
use bffs::{
io::{IoBase, Read, Seek},
Fat32FileSystem,
};
use core::fmt::Debug;
use alloc::boxed::Box;
use bffs::{Fat32FileSystem, ReadSeek, entry::DirEntry, file::File};
use io::{IoBase, Read, Seek, Write};
use crate::virtual_fs::{VirtualFileSystem, VirtualNode};
const DISK_ADDR: *const u8 = 0x9000_0000 as *const _;
/// Lazy holder for the kernel's filesystem instance.
///
/// The inner `UnsafeCell` allows one-time initialization at early boot while
/// exposing a shared `&'static` reference through `Deref` once initialized.
pub struct FSTemp(UnsafeCell<Option<Fat32FileSystem<Disk>>>);
unsafe impl Sync for FSTemp {}
impl FSTemp {
/// Initialize the global filesystem from the in-memory disk image.
///
/// Safety: must be called exactly once during early kernel initialization
/// before any other filesystem operations occur.
pub unsafe fn init(&self) {
unsafe {
*self.0.get() = Some(Fat32FileSystem::new(Disk::new(1024 * 1024 * 16)).unwrap());
}
}
}
impl Deref for FSTemp {
type Target = Fat32FileSystem<Disk>;
fn deref(&self) -> &Self::Target {
unsafe { (&*self.0.get()).as_ref().unwrap_unchecked() }
}
}
pub static FAT32_FILE_SYSTEM: FSTemp = FSTemp(UnsafeCell::new(None));
#[derive(Debug)]
/// Simple disk backend that reads from a fixed memory region.
///
@@ -62,11 +36,11 @@ impl IoBase for Disk {
}
impl Seek for Disk {
fn seek(&mut self, pos: bffs::io::SeekFrom) -> Result<u64, bffs::error::Error<Self::Error>> {
fn seek(&mut self, pos: io::SeekFrom) -> Result<u64, Self::Error> {
match pos {
bffs::io::SeekFrom::Start(pos) => self.pos = pos,
bffs::io::SeekFrom::End(_) => unimplemented!(),
bffs::io::SeekFrom::Current(pos) => self.pos = (self.pos as i64 + pos) as u64,
io::SeekFrom::Start(pos) => self.pos = pos,
io::SeekFrom::End(_) => unimplemented!(),
io::SeekFrom::Current(pos) => self.pos = (self.pos as i64 + pos) as u64,
}
Ok(self.pos)
}
@@ -74,7 +48,7 @@ impl Seek for Disk {
impl Read for Disk {
/// Read bytes from the in-memory disk image into `buf`.
fn read(&mut self, buf: &mut [u8]) -> Result<usize, bffs::error::Error<Self::Error>> {
fn read(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error> {
if self.pos >= self.size {
return Ok(0);
}
@@ -90,3 +64,69 @@ impl Read for Disk {
Ok(size)
}
}
#[derive(Debug)]
pub struct Fat32VirtualNode<'a, T> {
kind: Fat32VirtualNodeType<'a, T>,
_entry: DirEntry<'a, T>,
}
#[derive(Debug)]
enum Fat32VirtualNodeType<'a, T> {
Dir,
File(File<'a, T>),
}
impl<'a, T: ReadSeek> Fat32VirtualNode<'a, T> {
pub unsafe fn new(entry: DirEntry<'a, T>) -> Self {
let kind = if entry.is_dir() {
Fat32VirtualNodeType::Dir
} else {
Fat32VirtualNodeType::File(entry.to_file())
};
Self {
kind,
_entry: entry,
}
}
}
impl<T: ReadSeek + Debug> VirtualNode for Fat32VirtualNode<'_, T> {}
impl<T> IoBase for Fat32VirtualNode<'_, T> {
type Error = ();
}
impl<T: ReadSeek + Debug> Seek for Fat32VirtualNode<'_, T> {
fn seek(&mut self, pos: io::SeekFrom) -> Result<u64, Self::Error> {
match &mut self.kind {
Fat32VirtualNodeType::Dir => todo!(),
Fat32VirtualNodeType::File(file) => file.seek(pos).map_err(|_| ()),
}
}
}
impl<T: ReadSeek + Debug> Read for Fat32VirtualNode<'_, T> {
fn read(&mut self, buf: &mut [u8]) -> Result<usize, ()> {
match &mut self.kind {
Fat32VirtualNodeType::Dir => unimplemented!(),
Fat32VirtualNodeType::File(file) => file.read(buf).map_err(|_| ()),
}
}
}
impl<T: ReadSeek + Debug> Write for Fat32VirtualNode<'_, T> {
fn write(&mut self, _buf: &[u8]) -> Result<usize, ()> {
todo!()
}
fn flush(&mut self) -> Result<(), Self::Error> {
todo!()
}
}
impl<T: ReadSeek + Debug> VirtualFileSystem for Fat32FileSystem<T> {
fn open(&mut self, path: &bffs::path::Path) -> Result<Box<dyn VirtualNode + '_>, ()> {
let entry = self.open_entry(path).unwrap();
Ok(Box::new(unsafe { Fat32VirtualNode::new(entry) }))
}
}

View File

@@ -4,18 +4,22 @@
//! This module contains the low-level trap handlers for machine and supervisor
//! modes and the syscall dispatch implementation used by user processes.
use alloc::str;
use io::SeekFrom;
use log::info;
use shared::syscall::SysCall;
use crate::{
KBD_DRIVER,
boot::sbi::{EextensionID, TimerFunctionID},
clear_csr,
process::{exit_process, sleep, ExecutionContext},
clear_csr, println,
process::{ExecutionContext, exit_process, sleep},
read_csr,
riscv::disable_interrupt,
scheduler::SCHEDULER,
set_csr, syscall,
time::{setup_next_timer_interrupt, IRQ_M_TIMER},
time::{IRQ_M_EXTERNAL, IRQ_M_TIMER, setup_next_timer_interrupt},
virtio::input::S_MODE_CLAIM_COMPLETE,
virtual_fs::{FILE_SYSTEM, VirtualFileSystem},
write_csr,
};
use core::{alloc::Layout, arch::naked_asm, time::Duration};
@@ -89,6 +93,9 @@ unsafe extern "C" fn machine_trap_handler(
setup_next_timer_interrupt();
set_csr!(mip, 1 << 5);
}
11 => {
set_csr!(mip, 1 << 9);
}
_ => {}
}
}
@@ -123,7 +130,7 @@ unsafe extern "C" fn supervisor_trap_handler(
match syscall {
SysCall::Open => {
let path = unsafe { str::from_raw_parts(a1 as *const u8, a2 as usize) };
let virtual_node = syscall::open(path, false).unwrap();
let virtual_node = unsafe { FILE_SYSTEM.open(path.as_ref()).unwrap() };
let mut scheduler = SCHEDULER.lock();
let current_process = scheduler.get_current_process();
@@ -132,6 +139,29 @@ unsafe extern "C" fn supervisor_trap_handler(
current_process.next_fd += 1;
unsafe { (*interrupt_state).a[0] = fd };
}
SysCall::Write => {
let fd = a1;
let buf =
unsafe { core::slice::from_raw_parts(a2 as *const u8, a3 as usize) };
let mut scheduler = SCHEDULER.lock();
let current_process = scheduler.get_current_process();
let vnode = current_process.fd_table.get_mut(&fd).unwrap();
vnode.write(buf).unwrap();
}
SysCall::Seek => {
let fd = a1;
let seek = match a2 {
0 => SeekFrom::Start(a3),
1 => SeekFrom::End(a3 as i64),
2 => SeekFrom::Current(a3 as i64),
_ => unimplemented!(),
};
let mut scheduler = SCHEDULER.lock();
let current_process = scheduler.get_current_process();
let vnode = current_process.fd_table.get_mut(&fd).unwrap();
vnode.seek(seek).unwrap();
}
SysCall::Alloc => {
let layout = Layout::from_size_align(a1 as usize, a2 as usize).unwrap();
// Allocate memory and put the pointer in a0
@@ -176,6 +206,20 @@ unsafe extern "C" fn supervisor_trap_handler(
timer_interrupt();
SCHEDULER.lock().schedule(&mut interrupt_state);
}
9 => {
println!("click");
let irq = core::ptr::read_volatile(S_MODE_CLAIM_COMPLETE);
if irq != 0 {
// ... Traiter l'interruption VirtIO ici ...
// 2. Écrire l'IRQ (Complete) <--- INDISPENSABLE
core::ptr::write_volatile(S_MODE_CLAIM_COMPLETE, irq);
KBD_DRIVER.handle_interrupt();
} else {
panic!()
}
}
_ => {}
}
}
@@ -186,11 +230,13 @@ unsafe extern "C" fn supervisor_trap_handler(
pub unsafe fn setup_machine_trap_handler() {
write_csr!(mtvec, _machine_mode_trap);
set_csr!(mie, IRQ_M_TIMER);
set_csr!(mie, IRQ_M_EXTERNAL);
}
/// Install the supervisor-mode trap entry point and configure periodic timer.
pub unsafe fn setup_supervisor_trap_handler() {
write_csr!(stvec, _supervisor_mode_trap);
set_csr!(sie, 1 << 9);
setup_timer_interrupt();
}

View File

@@ -5,32 +5,29 @@
#![no_std]
#![no_main]
// #![warn(clippy::pedantic)]
#![feature(
riscv_ext_intrinsics,
const_trait_impl,
iter_map_windows,
str_from_raw_parts,
macro_metavar_expr,
macro_metavar_expr_concat,
ptr_metadata
)]
#![allow(static_mut_refs)]
#![feature(riscv_ext_intrinsics, str_from_raw_parts)]
use core::sync::atomic::AtomicBool;
use alloc::boxed::Box;
use embedded_alloc::LlffHeap as Heap;
use log::info;
use crate::{
fs::FAT32_FILE_SYSTEM,
io::init_log,
riscv::enable_supervisor_interrupt,
scheduler::{idle, SCHEDULER},
scheduler::{SCHEDULER, idle},
user::{proc2, test},
vga::{Color, Vga},
vga::Vga,
virtio::{Virtqueue, input::{VirtioInputDriver, init_plic_m_mode}},
virtual_fs::init_file_system,
};
extern crate alloc;
mod boot;
mod critical_section;
mod draw;
mod fs;
mod interrupt;
mod io;
@@ -41,30 +38,41 @@ mod scheduler;
mod sync;
mod syscall;
mod time;
mod tty;
mod uart;
mod user;
mod vga;
mod virtio;
mod virtual_console;
mod virtual_fs;
pub const HEAP_SIZE: usize = 1024 * 1024 * 32; // 32Mo RAM
#[global_allocator]
static HEAP: Heap = Heap::empty();
static HEAP_INITIALIZED: AtomicBool = AtomicBool::new(false);
// Usize is assumed to be an u64 in the whole kernel
const _: () = assert!(core::mem::size_of::<usize>() == core::mem::size_of::<u64>());
// 1. Allouer de la mémoire statique alignée pour la queue
static mut KBD_QUEUE: Virtqueue = unsafe { core::mem::zeroed() };
// 2. Initialisation (adresse 0x10001000 typique pour QEMU virt machine)
pub static mut KBD_DRIVER: VirtioInputDriver =
unsafe { VirtioInputDriver::new(0x10001000, &mut KBD_QUEUE) };
#[unsafe(no_mangle)]
pub extern "C" fn supervisor_mode_entry() {
unsafe {
embedded_alloc::init!(HEAP, HEAP_SIZE);
HEAP_INITIALIZED.store(true, core::sync::atomic::Ordering::Relaxed);
init_log().unwrap();
Vga::init();
FAT32_FILE_SYSTEM.init();
SCHEDULER.lock().init();
init_file_system();
}
info!("Hello World !");
unsafe { Vga::draw_string(10, 10, "Hello World !", Color::WHITE, Color::BLACK) };
// unsafe { Vga.draw_string(10, 10, "Hello World !", Color::WHITE, Color::BLACK) };
SCHEDULER.lock().create_process(Box::new(test), "proc1");
SCHEDULER.lock().create_process(Box::new(proc2), "proc2");
@@ -74,5 +82,10 @@ pub extern "C" fn supervisor_mode_entry() {
.create_process_from_file("/usr/bin/test_pic");
enable_supervisor_interrupt();
unsafe {
KBD_DRIVER.init();
init_plic_m_mode();
}
idle();
}

View File

@@ -7,29 +7,38 @@ use core::arch::riscv64::wfi;
use alloc::{format, string::ToString};
use log::error;
use crate::vga::{Color, Vga, FONT_HEIGHT};
use crate::{
HEAP, HEAP_INITIALIZED,
draw::{Color, Draw, FONT_HEIGHT},
uart::write_uart,
vga::Vga,
};
#[panic_handler]
/// Kernel panic handler that displays the panic message on the framebuffer and halts.
fn panic(panic_info: &core::panic::PanicInfo) -> ! {
error!("PANIC !");
let mut panic_message = panic_info.message().to_string();
if let Some(location) = panic_info.location() {
panic_message = format!("{panic_message} at {}:{}", location.file(), location.line());
}
error!("{panic_message}");
if !HEAP_INITIALIZED.load(core::sync::atomic::Ordering::Relaxed) {
write_uart("EARLY PANIC !");
} else {
error!("PANIC !");
let mut panic_message = panic_info.message().to_string();
if let Some(location) = panic_info.location() {
panic_message = format!("{panic_message} at {}:{}", location.file(), location.line());
}
error!("{panic_message}");
Vga::clear_screen(Color::WHITE);
unsafe { Vga::draw_string(0, 0, "PANIC !", Color::BLACK, Color::WHITE) };
unsafe {
Vga::draw_string(
0,
FONT_HEIGHT as u16,
panic_message,
Color::BLACK,
Color::WHITE,
)
};
Vga.clear_screen(Color::WHITE);
unsafe { Vga.draw_string(0, 0, "PANIC !", Color::BLACK, Color::WHITE) };
unsafe {
Vga.draw_string(
0,
FONT_HEIGHT as u16,
panic_message,
Color::BLACK,
Color::WHITE,
)
};
}
loop {
unsafe { wfi() }

View File

@@ -10,17 +10,17 @@
use core::time::Duration;
use alloc::{boxed::Box, format, string::String, vec::Vec};
use bffs::{io::Read, path::Path};
use bffs::path::Path;
use goblin::elf::reloc::R_RISCV_RELATIVE;
use hashbrown::HashMap;
use log::info;
use shared::syscall::exit;
use crate::{
fs::FAT32_FILE_SYSTEM,
println,
scheduler::{Scheduler, SCHEDULER},
scheduler::{SCHEDULER, Scheduler},
time::elapsed_time_since_startup,
virtual_fs::VirtualNode,
virtual_fs::{FILE_SYSTEM, VirtualFileSystem, VirtualNode},
};
/// Size of the stack allocated to each process (in 64-bit words).
@@ -84,7 +84,7 @@ pub struct Process {
/// Current state of the process.
pub state: ProcessState,
/// Optional entry point for the process code.
pub entry: Option<Box<dyn Fn() + Send>>,
pub entry: Option<Box<dyn Fn()>>,
/// Wake time for sleeping processes.
pub wake_time: Duration,
/// Saved execution context.
@@ -92,7 +92,7 @@ pub struct Process {
/// Process stack.
pub stack: [u64; STACK_SIZE],
/// File descriptor table.
pub fd_table: HashMap<u64, Box<dyn VirtualNode + Send>>,
pub fd_table: HashMap<u64, Box<dyn VirtualNode>>,
/// Next available file descriptor.
pub next_fd: u64,
}
@@ -164,7 +164,9 @@ impl Scheduler {
let name = path.as_str();
// Open and read the binary file
let mut bin = FAT32_FILE_SYSTEM.open_file(path).unwrap();
info!("ue");
let mut bin = unsafe { FILE_SYSTEM.open(path).unwrap() };
info!("ue");
println!("Creating process");
let mut content: Vec<u8> = Vec::new();
bin.read_to_end(&mut content).unwrap();
@@ -190,7 +192,7 @@ impl Scheduler {
if min_vaddr != u64::MAX {
let size = (max_vaddr - min_vaddr) as usize;
use alloc::alloc::{alloc_zeroed, Layout};
use alloc::alloc::{Layout, alloc_zeroed};
let layout = Layout::from_size_align(size, 0x1000).unwrap();
let base = unsafe { alloc_zeroed(layout) };

View File

@@ -45,7 +45,7 @@ impl<T> Drop for MutexGuard<'_, T> {
}
}
impl<'a, T> Deref for MutexGuard<'a, T> {
impl<T> Deref for MutexGuard<'_, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
@@ -53,7 +53,7 @@ impl<'a, T> Deref for MutexGuard<'a, T> {
}
}
impl<'a, T> DerefMut for MutexGuard<'a, T> {
impl<T> DerefMut for MutexGuard<'_, T> {
fn deref_mut(&mut self) -> &mut Self::Target {
unsafe { &mut *self.mutex.value.get() }
}

View File

@@ -1,13 +1,5 @@
use core::alloc::Layout;
use alloc::boxed::Box;
use bffs::{error::Error, path::Path};
use crate::{
fs::{Disk, FAT32_FILE_SYSTEM},
virtual_fs::VirtualNode,
};
pub unsafe fn alloc(layout: Layout) -> *mut u8 {
unsafe { alloc::alloc::alloc(layout) }
}
@@ -15,17 +7,3 @@ pub unsafe fn alloc(layout: Layout) -> *mut u8 {
pub unsafe fn dealloc(ptr: *mut u8, layout: core::alloc::Layout) {
unsafe { alloc::alloc::dealloc(ptr, layout) }
}
pub fn open<P: AsRef<Path>>(
path: P,
in_kernel: bool,
) -> Result<Box<dyn VirtualNode + Send>, Error<<Disk as bffs::io::IoBase>::Error>> {
let path = path.as_ref();
let file = match path.split_path() {
("dev", path) => {
todo!()
}
_ => FAT32_FILE_SYSTEM.open_file(path)?,
};
todo!()
}

View File

@@ -7,14 +7,16 @@ use core::{
};
use crate::{
draw::{Color, Draw, FONT_WIDTH},
set_csr,
vga::{Color, Vga, FONT_WIDTH, WIDTH},
vga::{Vga, WIDTH},
};
/// Supervisor timer interrupt enable bit for the SIE CSR.
pub const IRQ_S_TIMER: u8 = 1 << 5;
/// Machine timer interrupt enable bit for the MIE CSR (not used here, but provided for completeness).
pub const IRQ_M_TIMER: u8 = 1 << 7;
pub const IRQ_M_EXTERNAL: u64 = 1 << 11;
/// Memory-mapped address for the CLINT timer compare register.
const CLINT_TIMER_CMP: *mut u64 = 0x0200_4000 as *mut u64;
@@ -60,7 +62,7 @@ pub fn timer_interrupt() {
let formatted_time = format!("{:02}:{:02}:{:02}", hours, minutes, seconds);
unsafe {
Vga::draw_string(
Vga.draw_string(
(WIDTH - formatted_time.len() * FONT_WIDTH) as u16,
0,
formatted_time,

73
src/tty.rs Normal file
View File

@@ -0,0 +1,73 @@
use core::cell::RefCell;
use alloc::{boxed::Box, rc::Rc};
use io::{IoBase, Read, Seek, Write};
use crate::{
virtual_console::VirtualConsole,
virtual_fs::{VirtualFileSystem, VirtualNode},
};
#[derive(Debug)]
pub struct Tty {
console: Rc<RefCell<VirtualConsole>>,
}
impl Tty {
pub fn new() -> Self {
Self {
console: RefCell::new(VirtualConsole::new()).into(),
}
}
}
#[derive(Debug)]
struct TtyNode {
console: Rc<RefCell<VirtualConsole>>,
}
impl VirtualFileSystem for Tty {
fn open(
&mut self,
path: &bffs::path::Path,
) -> Result<alloc::boxed::Box<dyn crate::virtual_fs::VirtualNode + '_>, ()> {
if !path.is_empty() {
Err(())
} else {
Ok(Box::new(TtyNode {
console: self.console.clone(),
}))
}
}
}
impl IoBase for TtyNode {
type Error = ();
}
impl Read for TtyNode {
fn read(&mut self, _buf: &mut [u8]) -> Result<usize, Self::Error> {
unimplemented!()
}
}
impl Seek for TtyNode {
fn seek(&mut self, _pos: io::SeekFrom) -> Result<u64, Self::Error> {
unimplemented!()
}
}
impl Write for TtyNode {
fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
self.console
.borrow_mut()
.write_str(str::from_utf8(buf).unwrap());
Ok(buf.len())
}
fn flush(&mut self) -> Result<(), Self::Error> {
todo!()
}
}
impl VirtualNode for TtyNode {}

View File

@@ -2,9 +2,10 @@
//!
//! Provides primitives to initialize the Bochs-compatible frame buffer and
//! draw text using an embedded font plate.
use kernel_macros::include_font_plate;
use log::info;
use crate::draw::{Color, Draw};
const PCI_ECAM_BASE_ADDRESS: *mut u32 = 0x30000000 as *mut _;
const BOCHS_DISPLAY_BASE_ADDRESS: *mut u32 = 0x50000000 as *mut _;
const BOCHS_CONFIG_BASE_ADDRESS: *mut u16 = 0x40000000 as *mut _;
@@ -12,28 +13,8 @@ pub const VGA_ADDRESS: *mut Color = BOCHS_DISPLAY_BASE_ADDRESS as *mut Color;
pub const WIDTH: usize = 1600;
pub const HEIGHT: usize = 900;
/// 24-bit RGB color used by the framebuffer.
#[repr(transparent)]
#[derive(Clone, Copy)]
pub struct Color(u32);
#[allow(unused)]
impl Color {
pub const fn from_rgb(r: u8, g: u8, b: u8) -> Self {
Self((r as u32) << 16 | (g as u32) << 8 | b as u32)
}
pub const WHITE: Color = Color::from_rgb(255, 255, 255);
pub const GRAY: Color = Color::from_rgb(128, 128, 128);
pub const LIGHT_GRAY: Color = Color::from_rgb(128, 64, 64);
pub const BLACK: Color = Color::from_rgb(0, 0, 0);
pub const RED: Color = Color::from_rgb(255, 0, 0);
pub const GREEN: Color = Color::from_rgb(0, 255, 0);
pub const BLUE: Color = Color::from_rgb(0, 0, 255);
}
/// Framebuffer driver type providing text rendering helpers.
pub struct Vga {}
pub struct Vga;
impl Vga {
/// Initialize the Bochs framebuffer and configure VGA parameters.
@@ -42,7 +23,7 @@ impl Vga {
/// programs the Bochs config registers accordingly.
pub unsafe fn init() {
for i in 0..32 {
let addr = PCI_ECAM_BASE_ADDRESS.wrapping_byte_add(i << 11);
let addr = PCI_ECAM_BASE_ADDRESS.wrapping_byte_add(i << 15);
let header = unsafe { core::ptr::read_volatile(addr) };
if header >> 16 == 0x1111 && header & 0xFFFF == 0x1234 {
info!("VGA Bochs PCI found");
@@ -71,101 +52,26 @@ impl Vga {
core::ptr::write_volatile(BOCHS_CONFIG_BASE_ADDRESS.byte_add(0x508), 0x41)
};
Vga::clear_screen(Color::BLACK);
Vga.clear_screen(Color::BLACK);
}
/// # Safety
/// `x` must be less than `WIDTH` and `y` must be less than `HEIGHT`
/// Write a single pixel into the framebuffer (unsafe).
///
/// Caller must ensure `x < WIDTH` and `y < HEIGHT`.
pub unsafe fn write_pixel_unsafe(x: u16, y: u16, color: Color) {
pub unsafe fn write_u8_unsafe(offset: usize, value: u8) {
unsafe { *(VGA_ADDRESS.byte_add(offset) as *mut u8) = value }
}
}
impl Draw for Vga {
unsafe fn write_pixel_unsafe(&mut self, x: u16, y: u16, color: Color) {
let pixel_index = x as usize + y as usize * WIDTH;
unsafe { *VGA_ADDRESS.add(pixel_index) = color }
}
/// Draw a single character with a background color at (x,y).
///
/// Uses the embedded font plate to render glyphs into the framebuffer.
pub unsafe fn draw_char_bg(x: u16, y: u16, c: char, color: Color, bg_color: Color) {
let c = if (c as u8 > b'~') || ((c as u8) < b' ') {
b'/' - b' '
} else {
c as u8 - b' '
};
// Get char position within font plate
let char_x = (c as usize % 32) * FONT_WIDTH;
let char_y = (c as usize / 32) * FONT_HEIGHT;
for i in 0..(FONT_WIDTH as u16) {
for j in 0..(FONT_HEIGHT as u16) {
let xx = x + i;
let yy = y + j;
if xx < (WIDTH as u16) && yy < (HEIGHT as u16) {
if unsafe { Self::font_plate_index(char_x as u16 + i, char_y as u16 + j) } {
unsafe { Self::write_pixel_unsafe(xx, yy, color) }
} else {
unsafe { Self::write_pixel_unsafe(xx, yy, bg_color) }
}
}
}
}
fn get_width(&self) -> usize {
WIDTH
}
/// # Safety
/// The text must have a length that can fit within a `u16`
/// Draw a UTF-8 string at the given position.
///
/// Newlines (`\n`) advance `y` by `FONT_HEIGHT` and carriage return (`\r`)
/// resets to the starting `x` position.
pub unsafe fn draw_string<T: AsRef<str>>(
x: u16,
mut y: u16,
str: T,
color: Color,
bg_color: Color,
) {
let mut current_x = x;
str.as_ref().chars().for_each(|c| unsafe {
match c {
'\n' => {
current_x = x;
y += FONT_HEIGHT as u16;
}
'\r' => {
current_x = x;
}
c => {
Self::draw_char_bg(current_x, y, c, color, bg_color);
current_x += FONT_WIDTH as u16;
}
}
});
}
/// Fill the entire framebuffer with a single color.
pub fn clear_screen(color: Color) {
for i in 0..WIDTH * HEIGHT {
unsafe { *VGA_ADDRESS.add(i) = color }
}
}
/// Return whether a pixel inside the embedded font plate is set.
unsafe fn font_plate_index(x: u16, y: u16) -> bool {
let pixel_index = (y as usize) * FONTPLATE_WIDTH + (x as usize);
let byte_index = pixel_index / 8;
let bit_index = pixel_index % 8;
(FONTPLATE[byte_index] >> bit_index) & 0b1 == 0b1
fn get_height(&self) -> usize {
HEIGHT
}
}
pub const FONT_WIDTH: usize = 6;
pub const FONT_HEIGHT: usize = 13;
pub const FONTPLATE_WIDTH: usize = 32 * FONT_WIDTH;
pub const FONTPLATE_HEIGHT: usize = 3 * FONT_HEIGHT;
pub const FONTPLATE_SIZE: usize = FONTPLATE_WIDTH * FONTPLATE_HEIGHT / 8;
pub static FONTPLATE: [u8; FONTPLATE_SIZE] = include_font_plate! {"assets/fontplate.png"};

67
src/virtio.rs Normal file
View File

@@ -0,0 +1,67 @@
pub mod input;
use core::sync::atomic::AtomicU16;
// --- Constantes VirtIO ---
const VIRTIO_MMIO_MAGIC_VALUE: usize = 0x000;
const VIRTIO_MMIO_VERSION: usize = 0x004;
const VIRTIO_MMIO_DEVICE_ID: usize = 0x008;
const VIRTIO_MMIO_STATUS: usize = 0x070;
const VIRTIO_MMIO_GUEST_PAGE_SIZE: usize = 0x028;
const VIRTIO_MMIO_QUEUE_SEL: usize = 0x030;
const VIRTIO_MMIO_QUEUE_NUM_MAX: usize = 0x034;
const VIRTIO_MMIO_QUEUE_NUM: usize = 0x038;
const VIRTIO_MMIO_QUEUE_PFN: usize = 0x040;
const VIRTIO_MMIO_INTERRUPT_ACK: usize = 0x064;
const STATUS_ACKNOWLEDGE: u32 = 1;
const STATUS_DRIVER: u32 = 2;
const STATUS_DRIVER_OK: u32 = 4;
const STATUS_FEATURES_OK: u32 = 8;
const QUEUE_SIZE: usize = 128; // Puissance de 2 obligatoire
// --- Structures de la Virtqueue ---
#[repr(C, align(4096))]
pub struct Virtqueue {
pub descriptors: [Descriptor; QUEUE_SIZE],
pub available: AvailableRing,
pub used: UsedRing,
}
#[repr(C)]
pub struct Descriptor {
pub addr: u64,
pub len: u32,
pub flags: u16,
pub next: u16,
}
#[repr(C)]
pub struct AvailableRing {
pub flags: u16,
pub idx: AtomicU16,
pub ring: [u16; QUEUE_SIZE],
}
#[repr(C, align(4))]
pub struct UsedRing {
pub flags: u16,
pub idx: AtomicU16,
pub ring: [UsedElement; QUEUE_SIZE],
}
#[repr(C)]
pub struct UsedElement {
pub id: u32,
pub len: u32,
}
// --- Structure de l'événement Clavier ---
#[repr(C)]
#[derive(Copy, Clone, Debug)]
pub struct VirtioInputEvent {
pub event_type: u16,
pub code: u16,
pub value: u32,
}

192
src/virtio/input.rs Normal file
View File

@@ -0,0 +1,192 @@
use core::{
ptr::{read_volatile, write_volatile},
sync::atomic::Ordering,
};
use crate::{
println,
uart::write_char_uart,
virtio::{
QUEUE_SIZE, STATUS_ACKNOWLEDGE, STATUS_DRIVER, STATUS_DRIVER_OK, VIRTIO_MMIO_INTERRUPT_ACK,
VIRTIO_MMIO_QUEUE_NUM, VIRTIO_MMIO_QUEUE_NUM_MAX, VIRTIO_MMIO_QUEUE_PFN,
VIRTIO_MMIO_QUEUE_SEL, VIRTIO_MMIO_STATUS, VirtioInputEvent, Virtqueue,
},
};
pub struct VirtioInputDriver {
base_addr: usize,
queue: &'static mut Virtqueue,
event_pool: [VirtioInputEvent; QUEUE_SIZE],
last_used_idx: u16,
}
impl VirtioInputDriver {
pub const unsafe fn new(base_addr: usize, queue_mem: &'static mut Virtqueue) -> Self {
Self {
base_addr,
queue: queue_mem,
event_pool: [VirtioInputEvent {
event_type: 0,
code: 0,
value: 0,
}; QUEUE_SIZE],
last_used_idx: 0,
}
}
pub unsafe fn init(&mut self) {
unsafe {
// 1. Reset & Status (Ack + Driver)
self.write_reg(0x070, 0);
self.write_reg(0x070, 1 | 2);
// 2. Négociation Features (Obligatoire en Modern pour débloquer les queues)
self.write_reg(0x024, 1); // Select Page 1
let f1 = self.read_reg(0x010);
self.write_reg(0x020, f1 | 1); // On accepte VERSION_1 (Bit 32 global)
self.write_reg(0x070, 1 | 2 | 8); // STATUS_FEATURES_OK
if (self.read_reg(0x070) & 8) == 0 {
panic!("Features rejected");
}
// 3. Configuration de la Queue
self.write_reg(0x030, 0); // Select Queue 0
let max = self.read_reg(0x034); // QUEUE_NUM_MAX
self.write_reg(0x038, QUEUE_SIZE as u32);
// 4. Envoi des adresses 64 bits (Plus besoin de PFN !)
let desc_addr = &self.queue.descriptors as *const _ as u64;
self.write_reg(0x080, (desc_addr & 0xffffffff) as u32);
self.write_reg(0x084, (desc_addr >> 32) as u32);
let avail_addr = &self.queue.available as *const _ as u64;
self.write_reg(0x090, (avail_addr & 0xffffffff) as u32);
self.write_reg(0x094, (avail_addr >> 32) as u32);
let used_addr = &self.queue.used as *const _ as u64;
self.write_reg(0x0a0, (used_addr & 0xffffffff) as u32);
self.write_reg(0x0a4, (used_addr >> 32) as u32);
// 5. REMPLISSAGE INITIAL
for i in 0..QUEUE_SIZE {
self.queue.descriptors[i].addr = &self.event_pool[i] as *const _ as u64;
self.queue.descriptors[i].len = core::mem::size_of::<VirtioInputEvent>() as u32;
self.queue.descriptors[i].flags = 2; // Writeable
self.queue.available.ring[i] = i as u16;
}
self.queue
.available
.idx
.store(QUEUE_SIZE as u16, Ordering::Release);
// 6. ACTIVATION (L'étape que tout le monde oublie en Modern)
self.write_reg(0x044, 1); // QUEUE_READY
// 7. DRIVER_OK
self.write_reg(0x070, 1 | 2 | 4 | 8);
self.activate_queue();
}
}
unsafe fn activate_queue(&mut self) {
unsafe {
// 1. Sélectionner la queue
self.write_reg(0x030, 0);
// 2. Écrire la taille (doit correspondre à tes structures Rust)
self.write_reg(0x038, QUEUE_SIZE as u32);
// 3. Écrire les adresses (ORDRE CRITIQUE : LOW puis HIGH)
let desc_addr = &self.queue.descriptors as *const _ as u64;
self.write_reg(0x080, desc_addr as u32); // DescLow
self.write_reg(0x084, (desc_addr >> 32) as u32); // DescHigh
let avail_addr = &self.queue.available as *const _ as u64;
self.write_reg(0x090, avail_addr as u32); // AvailLow
self.write_reg(0x094, (avail_addr >> 32) as u32); // AvailHigh
let used_addr = &self.queue.used as *const _ as u64;
self.write_reg(0x0a0, used_addr as u32); // UsedLow
self.write_reg(0x0a4, (used_addr >> 32) as u32); // UsedHigh
// 4. LE KICK : Activer la queue
self.write_reg(0x044, 1); // QUEUE_READY = 1
// 5. SYNC : On s'assure que le périphérique a bien pris le READY
if self.read_reg(0x044) == 0 {
panic!("La queue refuse de passer en READY. Vérifiez les adresses !");
}
}
}
/// Appelé lors d'une interruption clavier
pub fn handle_interrupt(&mut self) {
let used_idx = self.queue.used.idx.load(Ordering::Acquire);
while self.last_used_idx != used_idx {
let ring_slot = self.last_used_idx as usize % QUEUE_SIZE;
let used_elem = &self.queue.used.ring[ring_slot];
let event = &self.event_pool[used_elem.id as usize];
if event.event_type == 1 {
// EV_KEY
self.on_key(event.code, event.value);
}
// Recyclage du descripteur : on le rend disponible à nouveau
let avail_idx = self.queue.available.idx.load(Ordering::Relaxed) as usize % QUEUE_SIZE;
self.queue.available.ring[avail_idx] = used_elem.id as u16;
self.queue.available.idx.fetch_add(1, Ordering::Release);
self.last_used_idx = self.last_used_idx.wrapping_add(1);
}
// Acquitter l'interruption
unsafe {
self.write_reg(VIRTIO_MMIO_INTERRUPT_ACK, 1);
}
}
fn on_key(&self, code: u16, value: u32) {
let state = match value {
1 => "Pressed",
0 => "Released",
2 => "Repeat",
_ => "Unknown",
};
// write_char_uart((b'0' + (code / 10) as u8 % 10) as char);
// write_char_uart((b'0' + (code % 10) as u8) as char);
write_char_uart(code as u8 as char);
write_char_uart('\n');
write_char_uart('\r');
// Ici, implémentez votre conversion Scancode -> ASCII
// println!("Key Code: {} - State: {}", code, state);
}
unsafe fn write_reg(&self, offset: usize, val: u32) {
unsafe { write_volatile((self.base_addr + offset) as *mut u32, val) };
}
unsafe fn read_reg(&self, offset: usize) -> u32 {
unsafe { read_volatile((self.base_addr + offset) as *const u32) }
}
}
pub const PLIC_BASE: usize = 0x0c00_0000;
pub const IRQ_VIRTIO: u32 = 1;
pub const S_MODE_CLAIM_COMPLETE: *mut u32 = 0x0c20_1004 as *mut u32;
pub unsafe fn init_plic_m_mode() {
// 1. Priority : identique pour tous les modes
let priority_ptr = (PLIC_BASE + 4 * IRQ_VIRTIO as usize) as *mut u32;
unsafe { priority_ptr.write_volatile(1) };
// 2. Enable : Pour Hart 0 M-Mode, l'offset est 0x2000
let enable_ptr = (PLIC_BASE + 0x2080) as *mut u32;
unsafe { enable_ptr.write_volatile(1 << IRQ_VIRTIO) };
// 3. Threshold : Pour Hart 0 M-Mode, l'offset est 0x200000
let threshold_ptr = (PLIC_BASE + 0x201000) as *mut u32;
unsafe { threshold_ptr.write_volatile(0) };
}

120
src/virtual_console.rs Normal file
View File

@@ -0,0 +1,120 @@
use alloc::boxed::Box;
use io::SeekFrom;
use crate::{
draw::{Color, Draw, FONT_HEIGHT, FONT_WIDTH},
vga::{self, Vga},
virtual_fs::{self, FILE_SYSTEM, VirtualFileSystem},
};
const TAB_SIZE: u64 = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Cursor {
x: u64,
y: u64,
}
impl Cursor {
pub fn new() -> Self {
Self { x: 0, y: 0 }
}
}
#[derive(Debug)]
pub struct VirtualConsole {
cursor: Cursor,
framebuffer: Box<dyn virtual_fs::VirtualNode>,
}
impl VirtualConsole {
pub fn new() -> Self {
VirtualConsole {
cursor: Cursor::new(),
framebuffer: unsafe { FILE_SYSTEM.open("/dev/fb0".as_ref()).unwrap() },
}
}
pub fn write_str(&mut self, s: &str) {
s.chars().for_each(|c| self.write_char(c));
}
pub fn write_char(&mut self, c: char) {
let mut last_cursor = self.cursor;
match c {
'\n' => {
self.cursor.x = 0;
self.cursor.y += 1;
}
'\r' => {
self.cursor.x = 0;
}
'\x08' if self.cursor.x > 0 => {
// Backspace
self.cursor.x -= 1;
for y in 0..FONT_HEIGHT as u16 {
for x in 0..FONT_WIDTH as u16 {
unsafe {
self.write_pixel_unsafe(
(self.cursor.x * FONT_WIDTH as u64) as u16 + x,
(self.cursor.y * FONT_HEIGHT as u64) as u16 + y,
Color::BLACK,
)
}
}
}
}
'\t' => {
self.cursor.x = (self.cursor.x / TAB_SIZE + 1) * TAB_SIZE;
}
_ if c <= 127 as char && c >= 32 as char => {
unsafe {
self.draw_char_bg(
(self.cursor.x * FONT_WIDTH as u64) as u16,
(self.cursor.y * FONT_HEIGHT as u64) as u16,
c,
Color::WHITE,
Color::BLACK,
)
};
self.cursor.x += 1;
}
_ => {}
}
if self.cursor.x as usize * FONT_WIDTH >= vga::WIDTH {
self.cursor.x = 0;
self.cursor.y += 1;
}
if self.cursor.y as usize * FONT_HEIGHT >= vga::HEIGHT {
self.line_up();
if last_cursor.y > 0 {
last_cursor.y -= 1;
}
}
if last_cursor != self.cursor {
self.move_cursor_line(last_cursor);
}
}
fn line_up(&mut self) {}
fn move_cursor_line(&mut self, _last: Cursor) {}
}
impl Draw for VirtualConsole {
unsafe fn write_pixel_unsafe(&mut self, x: u16, y: u16, color: crate::draw::Color) {
self.framebuffer
.seek(SeekFrom::Start(
(y as u64 * self.get_width() as u64 + x as u64) * size_of::<Color>() as u64,
))
.unwrap();
self.framebuffer.write(&color.as_bytes()).unwrap();
}
fn get_width(&self) -> usize {
Vga.get_width()
}
fn get_height(&self) -> usize {
Vga.get_height()
}
}

View File

@@ -1,24 +1,116 @@
use core::{cell::LazyCell, fmt::Debug};
use alloc::boxed::Box;
use bffs::path::{Path, PathBuf};
use bffs::{
Fat32FileSystem,
path::{Path, PathBuf},
};
use hashbrown::HashMap;
use io::{IoBase, Read, Seek, Write};
pub trait VirtualNode {
fn read(&mut self, buf: &mut [u8]) -> Result<usize, ()>;
}
pub trait VirtualFileSystem {
fn open(&mut self, path: &Path) -> Result<Box<dyn VirtualNode + Send>, ()>;
use crate::{fs::Disk, tty::Tty, vga::Vga};
pub trait VirtualNode: IoBase<Error = ()> + Read + Write + Seek + Debug {}
pub trait VirtualFileSystem: Debug {
fn open(&mut self, path: &Path) -> Result<Box<dyn VirtualNode + '_>, ()>;
}
#[derive(Debug)]
pub struct MainFileSystem {
root: Box<dyn VirtualFileSystem>,
mounts: HashMap<PathBuf, Box<dyn VirtualFileSystem>>,
}
impl MainFileSystem {
pub fn mount(&mut self, path: PathBuf, fs: Box<dyn VirtualFileSystem>) {
self.mounts.insert(path, fs);
}
}
impl VirtualFileSystem for MainFileSystem {
fn open(&mut self, path: &Path) -> Result<Box<dyn VirtualNode + Send>, ()> {
for mount in self.mounts.iter() {
fn open(&mut self, path: &Path) -> Result<Box<dyn VirtualNode + '_>, ()> {
let mut max = &mut self.root;
let mut max_path = Path::new("/");
let mut path_remaining = path;
for (mount, fs) in self.mounts.iter_mut() {
if path.starts_with(mount) && mount.starts_with(max_path) {
max = fs;
max_path = mount;
path_remaining = path.without(mount);
}
}
max.open(path_remaining)
}
}
pub static mut FILE_SYSTEM: LazyCell<MainFileSystem> = LazyCell::new(|| MainFileSystem {
root: Box::new(Fat32FileSystem::new(Disk::new(1024 * 1024 * 16)).unwrap()),
mounts: HashMap::new(),
});
pub unsafe fn init_file_system() {
unsafe {
FILE_SYSTEM.mount("/dev/fb0".into(), Box::new(VGAFileSystem));
FILE_SYSTEM.mount("/dev/tty0".into(), Box::new(Tty::new()));
}
}
#[derive(Debug)]
struct VGAFileSystem;
#[derive(Debug)]
struct VGAVirtualNode {
position: u64,
}
impl VirtualNode for VGAVirtualNode {}
impl IoBase for VGAVirtualNode {
type Error = ();
}
impl Seek for VGAVirtualNode {
fn seek(&mut self, pos: io::SeekFrom) -> Result<u64, Self::Error> {
self.position = match pos {
io::SeekFrom::Start(v) => v,
io::SeekFrom::End(v) => {
((crate::vga::WIDTH * crate::vga::HEIGHT * size_of::<crate::draw::Color>()) as i64
+ v) as u64
}
io::SeekFrom::Current(v) => (self.position as i64 + v) as u64,
};
Ok(self.position)
}
}
impl Read for VGAVirtualNode {
fn read(&mut self, _buf: &mut [u8]) -> Result<usize, ()> {
todo!()
}
}
impl Write for VGAVirtualNode {
fn write(&mut self, buf: &[u8]) -> Result<usize, ()> {
let start = self.position;
buf.iter().for_each(|val| {
unsafe { Vga::write_u8_unsafe(self.position as usize, *val) };
self.position += 1;
});
Ok((self.position - start) as usize)
}
fn flush(&mut self) -> Result<(), Self::Error> {
todo!()
}
}
impl VirtualFileSystem for VGAFileSystem {
fn open(&mut self, path: &Path) -> Result<Box<dyn VirtualNode + '_>, ()> {
if !path.is_empty() {
Err(())
} else {
Ok(Box::new(VGAVirtualNode { position: 0 }))
}
}
}