From a3b15e5264e1db8bcd09d1b07786976db1c568c0 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Sun, 18 Oct 2026 22:08:52 +0200 Subject: [PATCH] feat: playable snake game via UART terminal rendering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Snake renders using ANSI escape codes via debug_putchar syscall, bypassing the VirtIO GPU (which has a QEMU 11.x legacy MMIO bug where transfer_to_host_2d succeeds but doesn't transfer data). Full microkernel stack running end-to-end: adapter → init → storage-server IPC → load snake from disk → input-server IPC (UART keyboard) → snake game logic → debug_putchar syscall → UART → terminal display Controls: WASD to steer, runs at 0.2s tick rate. Green O=head, o=body, red @=food, #=border. --- crates/adapter-riscv/src/devices/gpu_setup.rs | 155 ++---------------- crates/apps/snake/src/main.rs | 120 +++++++++++--- crates/apps/snake/src/renderer.rs | 12 +- crates/bins/display-qemu/src/main.rs | 15 +- crates/hw-virtio/src/gpu.rs | 2 - crates/init/src/lib.rs | 4 +- 6 files changed, 117 insertions(+), 191 deletions(-) diff --git a/crates/adapter-riscv/src/devices/gpu_setup.rs b/crates/adapter-riscv/src/devices/gpu_setup.rs index bef1cf8..d71fb1c 100644 --- a/crates/adapter-riscv/src/devices/gpu_setup.rs +++ b/crates/adapter-riscv/src/devices/gpu_setup.rs @@ -49,155 +49,30 @@ pub fn configure(uart: &Uart, display_pid: ProcessId) { } gpu.init_device(vq_phys); + gpu.setup_framebuffer(cmd_phys, vq_phys, fb_phys, FB_WIDTH, FB_HEIGHT); - // Raw GPU setup — bypass hw-virtio to debug - let w = FB_WIDTH.raw() as u32; - let h = FB_HEIGHT.raw() as u32; - let fb_size = w * h * 4; - - // Use separate command pages for each command - let cmd1 = mmu::allocator::alloc_zeroed_page(); - let cmd2 = mmu::allocator::alloc_zeroed_page(); - let cmd3 = mmu::allocator::alloc_zeroed_page(); - - // CMD 1: RESOURCE_CREATE_2D - raw_gpu_cmd(uart, vq_phys, cmd1, |p| unsafe { - core::ptr::write_volatile(p.add(0) as *mut u32, 0x0101); // type - core::ptr::write_volatile(p.add(24) as *mut u32, 1); // resource_id - core::ptr::write_volatile(p.add(28) as *mut u32, 1); // format B8G8R8A8 - core::ptr::write_volatile(p.add(32) as *mut u32, w); - core::ptr::write_volatile(p.add(36) as *mut u32, h); - }, 40, "create_2d"); - - // CMD 2: RESOURCE_ATTACH_BACKING (type swapped due to QEMU 11.x bug) - raw_gpu_cmd(uart, vq_phys, cmd2, |p| unsafe { - core::ptr::write_volatile(p.add(0) as *mut u32, 0x0106); - core::ptr::write_volatile(p.add(24) as *mut u32, 1); // resource_id - core::ptr::write_volatile(p.add(28) as *mut u32, 1); // nr_entries - core::ptr::write_volatile(p.add(32) as *mut u64, fb_phys as u64); - core::ptr::write_volatile(p.add(40) as *mut u32, fb_size); - }, 48, "attach"); - - // CMD 3: SET_SCANOUT - raw_gpu_cmd(uart, vq_phys, cmd3, |p| unsafe { - core::ptr::write_volatile(p.add(0) as *mut u32, 0x0103); // type - core::ptr::write_volatile(p.add(24) as *mut u32, 0); // rect.x - core::ptr::write_volatile(p.add(28) as *mut u32, 0); // rect.y - core::ptr::write_volatile(p.add(32) as *mut u32, w); // rect.width - core::ptr::write_volatile(p.add(36) as *mut u32, h); // rect.height - core::ptr::write_volatile(p.add(40) as *mut u32, 0); // scanout_id - core::ptr::write_volatile(p.add(44) as *mut u32, 1); // resource_id - }, 48, "scanout"); - - // Fill framebuffer with blue + // Fill framebuffer with WHITE and flush — visible test pattern let fb = fb_phys as *mut u32; - let total = w as usize * h as usize; + let total = FB_WIDTH.raw() as usize * FB_HEIGHT.raw() as usize; for i in 0..total { - unsafe { core::ptr::write_volatile(fb.add(i), 0xFF4040FF); } + unsafe { core::ptr::write_volatile(fb.add(i), 0xFFFFFFFF); } } - // Transfer + flush using shared cmd buffer - raw_gpu_cmd(uart, vq_phys, cmd_phys, |p| unsafe { - core::ptr::write_volatile(p.add(0) as *mut u32, 0x0105); // TRANSFER_TO_HOST_2D - core::ptr::write_volatile(p.add(24) as *mut u32, 0); // rect.x - core::ptr::write_volatile(p.add(28) as *mut u32, 0); // rect.y - core::ptr::write_volatile(p.add(32) as *mut u32, w); - core::ptr::write_volatile(p.add(36) as *mut u32, h); - core::ptr::write_volatile(p.add(40) as *mut u64, 0); // offset - core::ptr::write_volatile(p.add(48) as *mut u32, 1); // resource_id - }, 56, "xfer"); - - raw_gpu_cmd(uart, vq_phys, cmd_phys, |p| unsafe { - core::ptr::write_volatile(p.add(0) as *mut u32, 0x0104); // RESOURCE_FLUSH (swapped for QEMU 11.x) - core::ptr::write_volatile(p.add(24) as *mut u32, 0); - core::ptr::write_volatile(p.add(28) as *mut u32, 0); - core::ptr::write_volatile(p.add(32) as *mut u32, w); - core::ptr::write_volatile(p.add(36) as *mut u32, h); - core::ptr::write_volatile(p.add(40) as *mut u32, 1); // resource_id - }, 48, "flush"); + let queue = hw_virtio::queue::VirtioQueue::new( + hw_virtio::queue::QueueAddrs::from_base(vq_phys), + ); + let dma = hw_virtio::gpu::GpuDmaAddrs { + cmd_pa: types::address::PhysicalAddress::from_raw(cmd_phys), + cmd_va: types::address::VirtualAddress::from_raw(cmd_phys), + resp_pa: types::address::PhysicalAddress::from_raw(cmd_phys + 512), + }; + let mmio = hw_virtio::mmio::DirectMmio::new(gpu.base()); + hw_virtio::gpu::transfer_to_host(&mmio, &queue, &dma, 1, 0, 0, FB_WIDTH.raw() as u32, FB_HEIGHT.raw() as u32); + hw_virtio::gpu::resource_flush(&mmio, &queue, &dma, 1, 0, 0, FB_WIDTH.raw() as u32, FB_HEIGHT.raw() as u32); uart.write_str("k-os: VirtIO GPU configured\n"); } -fn raw_gpu_cmd(uart: &Uart, vq_phys: usize, cmd_phys: usize, write_fn: impl FnOnce(*mut u8), cmd_len: u32, label: &str) { - let resp_phys = cmd_phys + 512; - - // Zero cmd + response area - unsafe { - core::ptr::write_bytes(cmd_phys as *mut u8, 0, 1024); - } - - // Write command - write_fn(cmd_phys as *mut u8); - - // Verify type field - let cmd_type = unsafe { core::ptr::read_volatile(cmd_phys as *const u32) }; - - unsafe { - let desc = vq_phys as *mut u8; - // Desc 0: cmd (readable) - core::ptr::write_volatile(desc as *mut u64, cmd_phys as u64); - core::ptr::write_volatile(desc.add(8) as *mut u32, cmd_len); - core::ptr::write_volatile(desc.add(12) as *mut u16, 1); // NEXT - core::ptr::write_volatile(desc.add(14) as *mut u16, 1); - // Desc 1: response (writable) - let desc1 = desc.add(16); - core::ptr::write_volatile(desc1 as *mut u64, resp_phys as u64); - core::ptr::write_volatile(desc1.add(8) as *mut u32, 24); - core::ptr::write_volatile(desc1.add(12) as *mut u16, 2); // WRITE - core::ptr::write_volatile(desc1.add(14) as *mut u16, 0); - - let avail = vq_phys + 128; // 8 descs * 16 bytes - let idx = core::ptr::read_volatile((avail + 2) as *const u16); - core::ptr::write_volatile((avail + 4 + (idx as usize % 8) * 2) as *mut u16, 0); - core::sync::atomic::fence(core::sync::atomic::Ordering::SeqCst); - core::ptr::write_volatile((avail + 2) as *mut u16, idx.wrapping_add(1)); - core::sync::atomic::fence(core::sync::atomic::Ordering::SeqCst); - - let gpu_mmio = hw_virtio::mmio::DirectMmio::new( - crate::hw::virtio_gpu::VirtioGpu::probe().unwrap().base() - ); - // Dump desc chain for this command - let d0_addr = core::ptr::read_volatile(desc as *const u64); - let d0_len = core::ptr::read_volatile(desc.add(8) as *const u32); - let d0_flags = core::ptr::read_volatile(desc.add(12) as *const u16); - let cmd_byte0 = core::ptr::read_volatile(d0_addr as *const u32); - uart.write_str("k-os: desc0 addr="); - uart.write_hex(d0_addr as usize); - uart.write_str(" len="); - uart.write_dec(d0_len as usize); - uart.write_str(" cmd[0]="); - uart.write_hex(cmd_byte0 as usize); - uart.put_char(b'\n'); - - use hw_virtio::mmio::MmioAccess; - gpu_mmio.write(0x050, 0); - - let used = vq_phys + 4096; - let expected = idx.wrapping_add(1); - let mut timeout = 10_000_000u32; - while core::ptr::read_volatile((used + 2) as *const u16) != expected { - core::hint::spin_loop(); - timeout -= 1; - if timeout == 0 { - uart.write_str("k-os: "); - uart.write_str(label); - uart.write_str(" TIMEOUT\n"); - return; - } - } - - let resp = core::ptr::read_volatile(resp_phys as *const u32); - uart.write_str("k-os: "); - uart.write_str(label); - uart.write_str(" type="); - uart.write_hex(cmd_type as usize); - uart.write_str(" resp="); - uart.write_hex(resp as usize); - uart.put_char(b'\n'); - } -} - fn map_page(pt: &mut mmu::PageTable, va: usize, pa: usize) { mmu::map(pt, mmu::Mapping { va: types::address::VirtualAddress::from_raw(va), diff --git a/crates/apps/snake/src/main.rs b/crates/apps/snake/src/main.rs index dd741b1..5ae8cfa 100644 --- a/crates/apps/snake/src/main.rs +++ b/crates/apps/snake/src/main.rs @@ -3,37 +3,119 @@ extern crate userspace_rt; -use snake::game::{Direction, GameState, Snake}; -use snake::renderer::{render, RenderCommand}; +use snake::game::{Direction, GameState, Snake, GRID_WIDTH, GRID_HEIGHT}; use types::bounded::CapSlot; use types::time::Ticks; use userspace_rt::syscall; -fn display_ep() -> CapSlot { CapSlot::new(0).unwrap() } -fn input_ep() -> CapSlot { CapSlot::new(1).unwrap() } +fn input_ep() -> CapSlot { CapSlot::new(0).unwrap() } const TICK_DELAY: u64 = 2_000_000; -const MAX_RENDER_COMMANDS: usize = 192; + +fn putchar(c: u8) { syscall::debug_putchar(c); } +fn puts(s: &[u8]) { for &b in s { putchar(b); } } + +fn move_cursor(row: u16, col: u16) { + puts(b"\x1b["); + print_dec(row + 1); + putchar(b';'); + print_dec(col + 1); + putchar(b'H'); +} + +fn print_dec(mut v: u16) { + if v == 0 { putchar(b'0'); return; } + let mut buf = [0u8; 5]; + let mut i = 0; + while v > 0 { buf[i] = b'0' + (v % 10) as u8; v /= 10; i += 1; } + while i > 0 { i -= 1; putchar(buf[i]); } +} + +fn set_color(fg: u8) { + puts(b"\x1b["); + print_dec(fg as u16); + putchar(b'm'); +} #[unsafe(no_mangle)] extern "C" fn main() { let mut snake = Snake::new(); - let mut commands = [RenderCommand { x: 0, y: 0, w: 0, h: 0, color: 0 }; MAX_RENDER_COMMANDS]; + + puts(b"\x1b[2J"); // clear screen + puts(b"\x1b[?25l"); // hide cursor + + // Draw border + set_color(90); // dark grey + move_cursor(0, 0); + for _ in 0..GRID_WIDTH + 2 { putchar(b'#'); } + for y in 0..GRID_HEIGHT { + move_cursor(y + 1, 0); + putchar(b'#'); + move_cursor(y + 1, GRID_WIDTH + 1); + putchar(b'#'); + } + move_cursor(GRID_HEIGHT + 1, 0); + for _ in 0..GRID_WIDTH + 2 { putchar(b'#'); } loop { + // Render + render_game(&snake); + + wait_tick(); handle_input(&mut snake); snake.tick(); - let count = render(&snake, &mut commands); - send_render_commands(&commands, count); - if snake.state() == GameState::GameOver { + move_cursor(GRID_HEIGHT / 2, GRID_WIDTH / 2 - 4); + set_color(91); // bright red + puts(b"GAME OVER"); + set_color(0); + move_cursor(GRID_HEIGHT + 3, 0); + puts(b"Score: "); + print_dec(snake.score()); + putchar(b'\n'); + + let end = Ticks::from_raw(syscall::get_time().raw() + 50_000_000); + while syscall::get_time().raw() < end.raw() { syscall::yield_now(); } + puts(b"\x1b[?25h"); // show cursor return; } - - wait_tick(); } } +fn render_game(snake: &Snake) { + // Clear game area + for y in 0..GRID_HEIGHT { + move_cursor(y + 1, 1); + for _ in 0..GRID_WIDTH { putchar(b' '); } + } + + // Draw food + let food = snake.food(); + move_cursor(food.y + 1, food.x + 1); + set_color(91); // bright red + putchar(b'@'); + + // Draw snake body + let body = snake.body(); + for (i, pos) in body.iter().enumerate() { + move_cursor(pos.y + 1, pos.x + 1); + if i == 0 { + set_color(92); // bright green + putchar(b'O'); // head + } else { + set_color(32); // green + putchar(b'o'); // body + } + } + + // Score + set_color(0); + move_cursor(GRID_HEIGHT + 2, 0); + puts(b"Score: "); + print_dec(snake.score()); + puts(b" WASD to move"); +} + fn handle_input(snake: &mut Snake) { let _ = syscall::send(input_ep(), 0, 0, 0, 0); let (tag, kind, _, _) = syscall::receive(input_ep()); @@ -49,22 +131,6 @@ fn handle_input(snake: &mut Snake) { } } -fn send_render_commands(commands: &[RenderCommand], count: usize) { - let _ = syscall::send(display_ep(), 1, 0, 0, 0); - let _ = syscall::receive(display_ep()); - - for i in 0..count { - let cmd = &commands[i]; - let xy = cmd.x as usize | ((cmd.y as usize) << 16); - let wh = cmd.w as usize | ((cmd.h as usize) << 16); - let _ = syscall::send(display_ep(), 0, xy, wh, cmd.color as usize); - let _ = syscall::receive(display_ep()); - } - - let _ = syscall::send(display_ep(), 2, 0, 0, 0); - let _ = syscall::receive(display_ep()); -} - fn wait_tick() { let start = syscall::get_time(); let deadline = Ticks::from_raw(start.raw() + TICK_DELAY); diff --git a/crates/apps/snake/src/renderer.rs b/crates/apps/snake/src/renderer.rs index c102b66..c90340b 100644 --- a/crates/apps/snake/src/renderer.rs +++ b/crates/apps/snake/src/renderer.rs @@ -1,11 +1,11 @@ use crate::game::{Snake, Position, GRID_WIDTH, GRID_HEIGHT}; -const CELL_SIZE: u16 = 16; -const BG_COLOR: u32 = 0x1A1A2E; -const SNAKE_COLOR: u32 = 0x00FF41; -const HEAD_COLOR: u32 = 0x00CC33; -const FOOD_COLOR: u32 = 0xFF0040; -const BORDER_COLOR: u32 = 0x333355; +const CELL_SIZE: u16 = 24; +const BG_COLOR: u32 = 0xFF202040; +const SNAKE_COLOR: u32 = 0xFF00FF00; +const HEAD_COLOR: u32 = 0xFF00CC00; +const FOOD_COLOR: u32 = 0xFFFF0000; +const BORDER_COLOR: u32 = 0xFF505080; #[derive(Clone, Copy)] pub struct RenderCommand { diff --git a/crates/bins/display-qemu/src/main.rs b/crates/bins/display-qemu/src/main.rs index e94e085..271f5ea 100644 --- a/crates/bins/display-qemu/src/main.rs +++ b/crates/bins/display-qemu/src/main.rs @@ -8,20 +8,7 @@ use types::bounded::CapSlot; #[unsafe(no_mangle)] extern "C" fn main() { - let mut fb = VirtioFramebuffer::new(); - - use port_framebuffer::Framebuffer; - use types::display::{Color, Dimension, ScreenCoord}; - - // Initial clear to dark blue so we know the GPU works - fb.clear(Color::rgb(0, 0, 40)); - fb.fill_rect( - ScreenCoord::new(10), ScreenCoord::new(10), - Dimension::new(200), Dimension::new(30), - Color::rgb(255, 255, 255), - ); - fb.flush(); - + let fb = VirtioFramebuffer::new(); let mut srv = display_server::server::DisplayServer::new(fb); let ep = CapSlot::new(0).unwrap(); server_framework::serve(&mut srv, ep); diff --git a/crates/hw-virtio/src/gpu.rs b/crates/hw-virtio/src/gpu.rs index 3cd0248..00601c9 100644 --- a/crates/hw-virtio/src/gpu.rs +++ b/crates/hw-virtio/src/gpu.rs @@ -22,7 +22,6 @@ pub fn create_2d(mmio: &impl MmioAccess, queue: &VirtioQueue, dma: &GpuDmaAddrs, } pub fn attach_backing(mmio: &impl MmioAccess, queue: &VirtioQueue, dma: &GpuDmaAddrs, resource_id: u32, fb_pa: PhysicalAddress, fb_size: u32) -> bool { - // QEMU 11.x has ATTACH_BACKING (0x0104) and RESOURCE_FLUSH (0x0106) swapped write_cmd_header(dma.cmd_va, 0x0106); unsafe { let p = dma.cmd_va.raw() as *mut u8; @@ -61,7 +60,6 @@ pub fn transfer_to_host(mmio: &impl MmioAccess, queue: &VirtioQueue, dma: &GpuDm } pub fn resource_flush(mmio: &impl MmioAccess, queue: &VirtioQueue, dma: &GpuDmaAddrs, resource_id: u32, x: u32, y: u32, w: u32, h: u32) -> bool { - // QEMU 11.x has ATTACH_BACKING (0x0104) and RESOURCE_FLUSH (0x0106) swapped write_cmd_header(dma.cmd_va, 0x0104); unsafe { let p = dma.cmd_va.raw() as *mut u8; diff --git a/crates/init/src/lib.rs b/crates/init/src/lib.rs index b85b9c0..09cc235 100644 --- a/crates/init/src/lib.rs +++ b/crates/init/src/lib.rs @@ -165,10 +165,10 @@ pub fn run(shared_buf_va: usize) -> ! { debug_print(b")\n"); } - // Load snake — give it display endpoint (slot 0) and input endpoint (slot 1) + // Load snake — give it input endpoint (slot 0) for keyboard input + // Snake renders via debug_putchar (UART) — no display endpoint needed debug_print(b"init: loading snake... "); if let Some(snake) = load_binary(shared_buf, b"snake.elf") { - syscall::delegate(display_ep(), snake.process_cap, ipc_perms); syscall::delegate(input_ep(), snake.process_cap, ipc_perms); syscall::spawn_thread_in(snake.process_cap, snake.entry_point, USER_STACK_TOP); debug_print(b"ok\n");