This post is part of a larger effort you can view over here: https://osblog.stephenmarz.com.
Video
Contents
- Overview
- Application Programmer’s Interface (API)
- Starting Routines
- System Calls
- Drawing Primitives
- Event Handling
- Start Our Game
- Game Loop
- PLAY
Overview
We last left off writing a graphics driver and an event driver for our operating system. We also added several system calls to handle drawing primitives as well as handling keyboard and mouse inputs. We are now going to use those to animate the simple game of pong. Just like hello world is the test of every programming language, pong is a test of all of our graphics and event systems.
Application Programmer’s Interface (API)
Since we’re writing in user space, we need to use the system calls we developed to (1) enumerate the framebuffer, (2) invalidate sections of the framebuffer, (3) read keyboard/click events, and (4) read motion events–mouse or tablet.
For Rust, we are still using the riscv64gc-unknown-none-elf target, which doesn’t contain a runtime. Now, riscv64gc-unknown-linux-gnu does exist, however it uses Linux system calls, which we haven’t implemented, yet. So, we are required to create our own system call interface as well as runtime!
Starting Routines
Many programmers know that ELF allows our entry point to start anywhere. However, most linkers will look for a symbol called _start. So, in Rust, we have to create this symbol. Unfortunately, after many attempts, I could not get it to work fully in Rust. So, instead, I used the global_asm! macro to import an assembly file. All it does is call main and invokes the exit system call when main returns.
.section .text.init .global _start _start: .option push .option norelax la gp, __global_pointer$ .option pop li a0, 0 li a1, 0 call main # Exit system call after main li a7, 93 ecall .type _start, function .size _start, .-_start
I did fail to mention about the global pointer. Since global variables can be stored far away, we use a register to store the top of this location. Luckily, most linkers have a symbol called __global_pointer$ (yes, even with the $) that we put into the gp (global pointer) register. I’m not going to cover relaxation, but this is necessary.
This is all that we need to get into Rust. However, since we’re still in baremetal Rust, we have to define the same symbols we did for our OS, including the panic and abort handlers.
#![no_std] #![feature(asm, panic_info_message, lang_items, start, global_asm)] #[lang = "eh_personality"] extern "C" fn eh_personality() {} #[panic_handler] fn panic(info: &core::panic::PanicInfo) -> ! { print!("Aborting: "); if let Some(p) = info.location() { println!("line {}, file {}: {}", p.line(), p.file(), info.message().unwrap()); } else { println!("no information available."); } abort(); } #[no_mangle] extern "C" fn abort() -> ! { loop { unsafe { asm!("wfi"); } } }
This allows our code to at least compile, but as you can see we need to import the assembly file as well as define a main.
global_asm!(include_str!("start.S")); #[start] fn main(_argc: isize, _argv: *const *const u8) -> isize { 0 }
Now we have an entry point for Rust. Now, we can create the println and print macros to make Rust be more Rusty for us.
#[macro_export] macro_rules! print { ($($args:tt)+) => ({ use core::fmt::Write; let _ = write!(crate::syscall::Writer, $($args)+); }); } #[macro_export] macro_rules! println { () => ({ print!("\r\n") }); ($fmt:expr) => ({ print!(concat!($fmt, "\r\n")) }); ($fmt:expr, $($args:tt)+) => ({ print!(concat!($fmt, "\r\n"), $($args)+) }); }
System Calls
We need to create an API for system calls, since that will be how we get and put certain data to our operating system. Generally, the runtime will establish this for us, but again, we’re in baremetal Rust.
use core::fmt::{Write, Error}; use crate::event::Event; pub struct Writer; impl Write for Writer { fn write_str(&mut self, out: &str) -> Result<(), Error> { for c in out.bytes() { putchar(c); } Ok(()) } } pub fn putchar(c: u8) -> usize { syscall(2, c as usize, 0, 0, 0, 0, 0, 0) } pub fn sleep(tm: usize) { let _ = syscall(10, tm, 0, 0, 0, 0, 0, 0); } pub fn get_fb(which_fb: usize) -> usize { syscall(1000, which_fb, 0, 0, 0, 0, 0, 0) } pub fn inv_rect(d: usize, x: usize, y: usize, w: usize, h: usize) { let _ = syscall(1001, d, x, y, w, h, 0, 0); } pub fn get_keys(x: *mut Event, y: usize) -> usize { syscall(1002, x as usize, y, 0, 0, 0, 0, 0) } pub fn get_abs(x: *mut Event, y: usize) -> usize { syscall(1004, x as usize, y, 0, 0, 0, 0, 0) } pub fn get_time() -> usize { syscall(1062, 0, 0, 0, 0, 0, 0, 0) } pub fn syscall(sysno: usize, a0: usize, a1: usize, a2: usize, a3: usize, a4: usize, a5: usize, a6: usize) -> usize { let ret; unsafe { asm!("ecall", in ("a7") sysno, in ("a0") a0, in ("a1") a1, in ("a2") a2, in ("a3") a3, in ("a4") a4, in ("a5") a5, in ("a6") a6, lateout("a0") ret); } ret }
MAN, I love the new Rust asm! It is very clear what’s happening when you look at it. Usually when I use inline assembly, register selection is extremely important. This makes it rather foolproof–dare I actually say that?
Just like we did for our operating system, we use the Write trait to hook into the format macro that is given to us by Rust.
Drawing Primitives
Ok, with the system calls and the startup code in start.S, we now have everything we need to start programming the game. So, let’s make some drawing primitives. Since this is pong, we only really care about drawing rectangles.
#[repr(C)] #[derive(Clone,Copy)] pub struct Pixel { pub r: u8, pub g: u8, pub b: u8, pub a: u8, } pub type Color = Pixel; impl Pixel { pub fn new(r: u8, g: u8, b: u8) -> Self { Self { r, g, b, a: 255 } } } pub struct Vector { pub x: i32, pub y: i32 } impl Vector { pub fn new(x: i32, y: i32) -> Self { Self { x, y } } } pub struct Rectangle { pub x: i32, pub y: i32, pub width: i32, pub height: i32, } impl Rectangle { pub fn new(x: i32, y: i32, width: i32, height: i32) -> Self { Self { x, y, width, height } } } pub struct Framebuffer { pixels: *mut Pixel } impl Framebuffer { pub fn new(pixels: *mut Pixel) -> Self { Self { pixels } } pub fn set(&mut self, x: i32, y: i32, pixel: &Pixel) { unsafe { if x < 640 && y < 480 { let v = (y * 640 + x) as isize; self.pixels.offset(v).write(*pixel); } } } pub fn fill_rect(&mut self, rect: &Rectangle, color: &Pixel) { let row_start = rect.y; let row_finish = row_start + rect.height; let col_start = rect.x; let col_finish = col_start + rect.width; for row in row_start..row_finish { for col in col_start..col_finish { self.set(col, row, color); } } } } pub fn lerp(value: i32, mx1: i32, mx2: i32) -> i32 { let r = (value as f64) / (mx1 as f64); return r as i32 * mx2; }
Now we have a pixel structure, a vector, a rectangle, and a framebuffer. The pixel comes from the operating system, so it is important that we control that structure, which necessitates #[repr(C)].
Event Handling
We can draw, so now we need to be able to handle input. We can create an event structure to handle this.
#[repr(C)] #[derive(Copy, Clone)] pub struct Event { pub event_type: u16, pub code: u16, pub value: u32 } impl Event { pub fn empty() -> Self { Self { event_type: 0, code: 0, value: 0 } } pub fn new(event_type: u16, code: u16, value: u32) -> Self { Self { event_type, code, value } } } // Key codes pub const KEY_RESERVED: u16 = 0; pub const KEY_ESC: u16 = 1; pub const KEY_1: u16 = 2; pub const KEY_2: u16 = 3; // ... CLIP ... pub const KEY_END: u16 = 107; pub const KEY_DOWN: u16 = 108; // mouse buttons pub const BTN_MOUSE: u16 = 0x110; pub const BTN_LEFT: u16 = 0x110; pub const BTN_RIGHT: u16 = 0x111; pub const BTN_MIDDLE: u16 = 0x112; // mouse movement pub const ABS_X: u16 = 0x00; pub const ABS_Y: u16 = 0x01; pub const ABS_Z: u16 = 0x02;
Many of the constants came from libevdev’s input-event-codes.h. Unfortunately, that’s in C, so a little Python script could make it Rust.
Just like the Pixel structure, the Event structure is defined by our operating system, so we are required to control it ourselves (hence #[repr(C)]).
Start Our Game
const MAX_EVENTS: usize = 25; const GAME_FRAME_TIMER: usize = 1000; #[start] fn main(_argc: isize, _argv: *const *const u8) -> isize { use drawing::Framebuffer; use drawing::Pixel; let ufb = syscall::get_fb(6) as *mut Pixel; let mut fb = Framebuffer::new(ufb); let background_color = drawing::Pixel::new(25, 36, 100); let mut event_list = [event::Event::empty(); MAX_EVENTS]; let event_list_ptr = event_list.as_mut_ptr(); let player_color = drawing::Pixel::new(255, 0, 0); let npc_color = drawing::Pixel::new(0, 255, 0); let ball_color = drawing::Pixel::new(255, 255, 255); let mut game = pong::Pong::new(player_color, npc_color, ball_color, background_color); // GAME LOOP HERE println!("Goodbye :)"); 0 }
You can see the first thing we do is grab a framebuffer. If you recall from the operating system tutorial, our operating system will map the pixels into our application’s memory space. We can update the pixels as we see fit, but to actually realize it on the screen, we must invalidate the given pixels, which is a separate system call in our OS.
You can see we have a structure called Pong. This structure contains the routines we need to make this somewhat a game. We have a timing function called advance_frame, and we have a way to get the game onto the screen using the draw function.
This is pretty gross, but here’s the Pong structure’s advance frame and draw routines (the rest can be found on GitHub):
impl Pong { pub fn advance_frame(&mut self) { if !self.paused { self.move_ball(self.ball_direction.x, self.ball_direction.y); let miss = if self.ball.location.x < 40 { // This means we're in the player's paddle location. Let's // see if this is a hit or a miss! let paddle = (self.player.location.y, self.player.location.y + self.player.location.height); let ball = (self.ball.location.y, self.ball.location.y + self.ball.location.height); if paddle.0 <= ball.0 && paddle.1 >= ball.0 { false } else if paddle.0 <= ball.1 && paddle.1 >= ball.1 { false } else { true } } else { false }; if miss { self.reset(); self.paused = true; } else { if self.ball.location.x < 40 || self.ball.location.x > 580 { self.ball_direction.x = -self.ball_direction.x; } if self.ball.location.y < 20 || self.ball.location.y > 430 { self.ball_direction.y = -self.ball_direction.y; } let new_loc = self.ball.location.y - self.npc.location.height / 2; self.npc.location.y = if new_loc > 0 { new_loc } else { 0 }; } } } pub fn draw(&self, fb: &mut Framebuffer) { fb.fill_rect(&Rectangle::new(0, 0, 640, 480), &self.bgcolor); fb.fill_rect(&self.player.location, &self.player.color); fb.fill_rect(&self.npc.location, &self.npc.color); fb.fill_rect(&self.ball.location, &self.ball.color); } }
So our game will move the ball given the direction vector every frame due to advance_frame being called from our game loop. We also have the ability to pause the game (or unpause). Finally, we have very basic collision detection for the player’s paddle.
Game Loop
Now we need to put all of this together using a game loop, which will handle input events as well as advance the animation.
// GAME LOOP gameloop: loop { // handle mouse buttons and keyboard inputs // println!("Try get keys"); let num_events = syscall::get_keys(event_list_ptr, MAX_EVENTS); for e in 0..num_events { let ref ev = event_list[e]; // println!("Key {} Value {}", ev.code, ev.value); // Value = 1 if key is PRESSED or 0 if RELEASED match ev.code { event::KEY_Q => break 'gameloop, event::KEY_R => game.reset(), event::KEY_W | event::KEY_UP => game.move_player(-20), event::KEY_S | event::KEY_DOWN => game.move_player(20), event::KEY_SPACE => if ev.value == 1 { game.toggle_pause(); if game.is_paused() { println!("GAME PAUSED"); } else { println!("GAME UNPAUSED") } }, _ => {} } } game.advance_frame(); game.draw(&mut fb); syscall::inv_rect(6, 0, 0, 640, 480); syscall::sleep(GAME_FRAME_TIMER); }
PLAY
Our event loop uses W to move the paddle up or D to move the paddle down. Space toggles the pause and R resets the game.
Hmmm. When I started writing this, it seemed more impressive in my mind. Oh well, have fun!