├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Makefile ├── README ├── crt0.s ├── gba.json ├── linker.ld └── src ├── base.rs ├── font.bin ├── gba.rs └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | /out/ 3 | /target/ 4 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "gba-snake" 3 | version = "0.1.0" 4 | 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gba-snake" 3 | version = "0.1.0" 4 | authors = ["dennis.ranke@gmail.com"] 5 | 6 | [profile.dev] 7 | panic = "abort" 8 | lto = true 9 | 10 | [profile.release] 11 | panic = "abort" 12 | lto = true 13 | 14 | [lib] 15 | crate-type = ["staticlib"] 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2014-2016 Dennis Ranke 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BINUTILS_PREFIX=arm-none-eabi- 2 | RUST_LIBS=libs 3 | 4 | default: out out/snake.gba 5 | 6 | debug: out out/snake-debug.gba 7 | 8 | out: 9 | mkdir -p out 10 | 11 | cargo-build-release: 12 | rustup run nightly `which xargo` build --release --target=gba 13 | 14 | cargo-build-debug: 15 | rustup run nightly `which xargo` build --target=gba 16 | 17 | out/snake.gba: cargo-build-release crt0.s 18 | $(BINUTILS_PREFIX)as -o out/crt0.o crt0.s 19 | $(BINUTILS_PREFIX)ld -T linker.ld -o out/snake.elf out/crt0.o target/gba/release/libgba_snake.a 20 | $(BINUTILS_PREFIX)objcopy -O binary out/snake.elf out/snake.gba 21 | 22 | out/snake-debug.gba: cargo-build-debug crt0.s 23 | $(BINUTILS_PREFIX)as -o out/crt0.o crt0.s 24 | $(BINUTILS_PREFIX)ld -T linker.ld -o out/snake-debug.elf out/crt0.o target/gba/debug/libgba_snake.a 25 | $(BINUTILS_PREFIX)objcopy -O binary out/snake-debug.elf out/snake-debug.gba 26 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | GBA-RUST 2 | ======== 3 | 4 | In order to learn more about the low-level aspects of rust, I 5 | decided to implement some simple proof-of-concept games for the 6 | gameboy advance in rust. 7 | 8 | For now there is only a very simple implementation of snake. 9 | 10 | To build, you need to install nightly rust + sources (probably using rustup.rs) 11 | as well as [xargo](https://github.com/japaric/xargo). 12 | The build has been tested with `rustc 1.15.0-nightly (ac635aa95 2016-11-18)`. 13 | 14 | You also need arm-none-eabi binutils in your path. On linux you can 15 | probably install those through your package manager. 16 | -------------------------------------------------------------------------------- /crt0.s: -------------------------------------------------------------------------------- 1 | .TEXT 2 | .GLOBAL _start 3 | _start: 4 | .ALIGN 5 | .CODE 32 6 | b _header_end 7 | .fill 188, 1, 0 8 | _header_end: 9 | mov r0, #0 10 | ldr lr, =_header_end 11 | ldr r5, =main 12 | bx r5 13 | 14 | .GLOBAL __aeabi_memset 15 | __aeabi_memset: 16 | movs r2, r2 17 | bxle lr 18 | 1: strb r1, [r0], #1 19 | subs r2, r2, #1 20 | bgt 1b 21 | bx lr 22 | 23 | .GLOBAL __aeabi_memclr4 24 | __aeabi_memclr4: 25 | movs r1, r1 26 | bxle lr 27 | mov r2, #0 28 | 1: str r2, [r0], #4 29 | subs r1, r1, #4 30 | bgt 1b 31 | bx lr 32 | 33 | .GLOBAL __aeabi_memclr 34 | __aeabi_memclr: 35 | movs r1, r1 36 | bxle lr 37 | mov r2, #0 38 | 1: strb r2, [r0], #1 39 | subs r1, r1, #1 40 | bgt 1b 41 | bx lr 42 | 43 | .GLOBAL __aeabi_memcpy4 44 | __aeabi_memcpy4: 45 | 1: subs r2, r2, #4 46 | ldrgeb r3, [r1], #4 47 | strgeb r3, [r0], #4 48 | bgt 1b 49 | bx lr 50 | 51 | .GLOBAL __aeabi_memcpy 52 | __aeabi_memcpy: 53 | 1: subs r2, r2, #1 54 | ldrgeb r3, [r1], #1 55 | strgeb r3, [r0], #1 56 | bgt 1b 57 | bx lr 58 | 59 | @ floating point operations are not supported 60 | .GLOBAL __aeabi_ul2f 61 | .GLOBAL __aeabi_ul2d 62 | __aeabi_ul2f: 63 | __aeabi_ul2d: 64 | 1: b 1b 65 | 66 | @ multi-threading is also not supportd 67 | .GLOBAL __sync_val_compare_and_swap_1 68 | .GLOBAL __sync_val_compare_and_swap_2 69 | .GLOBAL __sync_val_compare_and_swap_4 70 | __sync_val_compare_and_swap_1: 71 | __sync_val_compare_and_swap_2: 72 | __sync_val_compare_and_swap_4: 73 | 1: b 1b 74 | 75 | @ 64 bit integers are not supported 76 | .GLOBAL __aeabi_uldivmod 77 | __aeabi_uldivmod: 78 | b __aeabi_uldivmod 79 | 80 | .GLOBAL __aeabi_uidivmod 81 | .GLOBAL __aeabi_uidiv 82 | __aeabi_uidivmod: 83 | __aeabi_uidiv: 84 | cmp r1, #0 85 | bxeq lr 86 | mov r3, #1 87 | 1: cmp r2, r1 88 | bge 2f 89 | movs r2, r1, lsl#1 90 | movcc r1, r2 91 | movcc r3, r3, lsl#1 92 | bcc 1b 93 | 2: mov r2, #0 94 | 3: cmp r0, r1 95 | subge r0, r0, r1 96 | addge r2, r2, r3 97 | movs r3, r3, lsr#1 98 | movne r2, r2, lsr#1 99 | bne 3b 100 | mov r1, r0 101 | mov r0, r2 102 | bx lr 103 | 104 | .GLOBAL memcmp 105 | memcmp: 106 | mov r3, r0 107 | mov r0, #0 108 | movs r2, r2 109 | bxeq lr 110 | 1: ldrb r0, [r3], #1 111 | ldrb r12, [r0], #1 112 | subs r0, r0, r12 113 | bxne lr 114 | subs r2, r2, #1 115 | bgt 1b 116 | bx lr 117 | 118 | .GLOBAL __mulodi4 119 | __mulodi4: 120 | smull r0, r3, r1, r0 121 | movs r3, r3 122 | addnes r3, r3, #1 123 | movne r3, #1 124 | str r3, [r2] 125 | bx lr 126 | 127 | .END 128 | -------------------------------------------------------------------------------- /gba.json: -------------------------------------------------------------------------------- 1 | { 2 | "llvm-target": "arm-none-eabi", 3 | "target-endian": "little", 4 | "target-pointer-width": "32", 5 | "target-c-int-width": "32", 6 | "linker-flavor": "ld", 7 | "data-layout": "e-m:e-p:32:32-i64:64-v128:64:128-a:0:32-n32-S64", 8 | "arch": "arm", 9 | "target-os": "none", 10 | "os": "none", 11 | "target-env": "", 12 | "target-vendor": "unknown", 13 | "cpu": "arm7tdmi" 14 | } 15 | -------------------------------------------------------------------------------- /linker.ld: -------------------------------------------------------------------------------- 1 | OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm") 2 | OUTPUT_ARCH(arm) 3 | /*ENTRY(_start)*/ 4 | 5 | SECTIONS { 6 | . = 0x8000000; 7 | 8 | .text : { 9 | *(.text .rodata) 10 | } 11 | 12 | .data : { *(.data) } 13 | .bss : { *(.bss) } 14 | 15 | __exidx_start = .; 16 | /* Exception handling, exidx needs a dedicated section */ 17 | .ARM.exidx : ALIGN(4) 18 | { 19 | *(.ARM.exidx* .gnu.linkonce.armexidx.*) 20 | } 21 | __exidx_end = .; 22 | } 23 | -------------------------------------------------------------------------------- /src/base.rs: -------------------------------------------------------------------------------- 1 | use core::fmt::{write, Error, Write}; 2 | use core::panic::PanicInfo; 3 | use gba; 4 | 5 | #[lang = "eh_personality"] 6 | pub extern "C" fn rust_eh_personality() {} 7 | 8 | struct BgWriter(u32); 9 | 10 | impl Write for BgWriter { 11 | fn write_str(&mut self, s: &str) -> Result<(), Error> { 12 | for c in s.chars() { 13 | let c = c as u32; 14 | let clamped = if c < 32 || c > 126 { 32 } else { c }; 15 | gba::hw::write_vram16(self.0, (clamped - 32) as u16); 16 | if c == 10 || ((self.0 & 31) >= 29) { 17 | self.0 = (self.0 + 32) & !31; 18 | } else { 19 | self.0 += 1; 20 | } 21 | } 22 | Ok(()) 23 | } 24 | } 25 | 26 | #[no_mangle] 27 | #[panic_handler] 28 | pub extern "C" fn rust_begin_unwind(info: &PanicInfo) -> ! { 29 | let (line, file) = if let Some(location) = info.location() { 30 | (location.line(), location.file()) 31 | } else { 32 | (0, "") 33 | }; 34 | let msg = info.payload().downcast_ref::<&str>().unwrap(); 35 | 36 | load_font(0); 37 | gba::hw::write_pal(0, 0); 38 | gba::hw::write_pal(15, 0x7fff); 39 | gba::hw::write_dispcnt(1 << 8); 40 | gba::hw::write_bg0cnt(2 << 8); 41 | for i in 0..(32 * 20) { 42 | gba::hw::write_vram16(0x800 + i, 0); 43 | } 44 | let mut writer = BgWriter(0x800); 45 | write( 46 | &mut writer, 47 | format_args!("Panic in line {} of\n{}\n\n{}", line, file, msg), 48 | ).unwrap(); 49 | loop {} 50 | } 51 | 52 | pub fn load_font(offset: u32) { 53 | let font = include_bytes!("font.bin"); 54 | for (index, byte) in font.iter().enumerate() { 55 | let mut line = 0u32; 56 | for bit in 0..7 { 57 | if (byte & (1 << bit)) != 0 { 58 | line |= 15 << (bit * 4); 59 | } 60 | } 61 | gba::hw::write_vram16(offset + index as u32 * 2, (line & 0xffff) as u16); 62 | gba::hw::write_vram16(offset + index as u32 * 2 + 1, (line >> 16) as u16); 63 | } 64 | } 65 | 66 | #[allow(dead_code)] 67 | pub mod rand { 68 | pub struct Rand { 69 | state: u32, 70 | } 71 | 72 | impl Rand { 73 | pub fn new(seed: u32) -> Rand { 74 | Rand { state: seed } 75 | } 76 | pub fn next_bool(&mut self) -> bool { 77 | self.state = self 78 | .state 79 | .wrapping_mul(1664525u32) 80 | .wrapping_add(1013904223u32); 81 | self.state & 0x80000000u32 != 0 82 | } 83 | pub fn next_u8(&mut self) -> u8 { 84 | let mut result = 0u8; 85 | for i in 0..8 { 86 | result |= (self.next_bool() as u8) << i; 87 | } 88 | result 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/font.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exoticorn/gba-rust/a55d6efd04cc796bb73bf1ef9873ff169a3d2248/src/font.bin -------------------------------------------------------------------------------- /src/gba.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | pub mod hw { 4 | use core::ptr::{read_volatile, write_volatile}; 5 | 6 | unsafe fn read16(addr: u32) -> u16 { 7 | read_volatile(addr as *const u16) 8 | } 9 | 10 | unsafe fn write16(addr: u32, value: u16) { 11 | write_volatile(addr as *mut u16, value); 12 | } 13 | 14 | macro_rules! hw_reg { 15 | (rw $addr: expr, $read:ident, $write: ident) => { 16 | #[allow(dead_code)] 17 | pub fn $read() -> u16 { 18 | unsafe { read16($addr) } 19 | } 20 | 21 | #[allow(dead_code)] 22 | pub fn $write(value: u16) { 23 | unsafe { write16($addr, value) } 24 | } 25 | }; 26 | (r $addr: expr, $read: ident) => { 27 | #[allow(dead_code)] 28 | pub fn $read() -> u16 { 29 | unsafe { read16($addr) } 30 | } 31 | }; 32 | (w $addr: expr, $write: ident) => { 33 | #[allow(dead_code)] 34 | pub fn $write(value: u16) { 35 | unsafe { write16($addr, value) } 36 | } 37 | }; 38 | } 39 | 40 | hw_reg!(rw 0x4000000, read_dispcnt, write_dispcnt); 41 | hw_reg!(rw 0x4000004, read_dispstat, write_dispstat); 42 | hw_reg!(rw 0x4000008, read_bg0cnt, write_bg0cnt); 43 | hw_reg!(rw 0x400000a, read_bg1cnt, write_bg1cnt); 44 | hw_reg!(rw 0x400000c, read_bg2cnt, write_bg2cnt); 45 | hw_reg!(rw 0x400000e, read_bg3cnt, write_bg3cnt); 46 | hw_reg!(w 0x4000010, write_bg0hofs); 47 | hw_reg!(w 0x4000012, write_bg0vofs); 48 | hw_reg!(w 0x4000014, write_bg1hofs); 49 | hw_reg!(w 0x4000016, write_bg1vofs); 50 | hw_reg!(w 0x4000018, write_bg2hofs); 51 | hw_reg!(w 0x400001a, write_bg2vofs); 52 | hw_reg!(w 0x400001c, write_bg3hofs); 53 | hw_reg!(w 0x400001e, write_bg3vofs); 54 | hw_reg!(r 0x4000130, read_keyinput); 55 | 56 | pub fn write_pal(index: u32, col: u16) { 57 | if index < 512 { 58 | unsafe { write16(0x5000000u32 + (index * 2) as u32, col) } 59 | } 60 | } 61 | 62 | pub fn write_vram16(offset: u32, data: u16) { 63 | if offset < 0xc000 { 64 | unsafe { write16(0x6000000u32 + offset * 2, data) } 65 | } 66 | } 67 | } 68 | 69 | pub struct KeyState { 70 | state: u32, 71 | } 72 | pub enum Key { 73 | A = 1, 74 | B = 2, 75 | Select = 4, 76 | Start = 8, 77 | Right = 16, 78 | Left = 32, 79 | Up = 64, 80 | Down = 128, 81 | R = 256, 82 | L = 512, 83 | } 84 | 85 | impl KeyState { 86 | pub fn new() -> KeyState { 87 | KeyState { state: 0 } 88 | } 89 | pub fn update(&mut self) { 90 | let pressed = hw::read_keyinput() ^ 0xffffu16; 91 | let triggered = pressed & !self.get_pressed(); 92 | self.state = (pressed as u32) | ((triggered as u32) << 16); 93 | } 94 | fn get_pressed(&self) -> u16 { 95 | self.state as u16 96 | } 97 | fn get_triggered(&self) -> u16 { 98 | (self.state >> 16) as u16 99 | } 100 | #[allow(dead_code)] 101 | pub fn is_pressed(&self, key: Key) -> bool { 102 | self.get_pressed() & (key as u16) != 0 103 | } 104 | #[allow(dead_code)] 105 | pub fn is_triggered(&self, key: Key) -> bool { 106 | self.get_triggered() & (key as u16) != 0 107 | } 108 | } 109 | 110 | pub fn wait_vblank() { 111 | while hw::read_dispstat() & 1 != 0 {} 112 | while hw::read_dispstat() & 1 == 0 {} 113 | } 114 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![feature(lang_items)] 3 | 4 | mod base; 5 | mod gba; 6 | 7 | use base::rand::Rand; 8 | pub use base::rust_begin_unwind; 9 | 10 | #[derive(Copy, Clone)] 11 | enum Tile { 12 | Empty, 13 | Snake, 14 | Food, 15 | } 16 | const WIDTH: usize = 30; 17 | const HEIGHT: usize = 20; 18 | const MAX_LENGTH: usize = 100; 19 | 20 | struct Arena { 21 | data: [Tile; WIDTH * HEIGHT], 22 | } 23 | 24 | impl Arena { 25 | pub fn new() -> Arena { 26 | Arena { data: [Tile::Empty; WIDTH * HEIGHT] } 27 | } 28 | 29 | pub fn set(&mut self, x: usize, y: usize, tile: Tile) { 30 | if x < WIDTH && y < HEIGHT { 31 | self.data[x + y * WIDTH] = tile; 32 | let bg_tile = match tile { 33 | Tile::Empty => 1u16, 34 | Tile::Snake => 0u16, 35 | Tile::Food => 1u16 << 12, 36 | }; 37 | gba::hw::write_vram16((0x400 + x + y * 32) as u32, bg_tile); 38 | } 39 | } 40 | 41 | pub fn get(&self, x: usize, y: usize) -> Tile { 42 | if x < WIDTH && y < HEIGHT { 43 | self.data[x + y * WIDTH] 44 | } else { 45 | Tile::Snake 46 | } 47 | } 48 | } 49 | 50 | #[derive(Copy, Clone)] 51 | struct Pos { 52 | x: usize, 53 | y: usize, 54 | } 55 | 56 | enum Dir { 57 | Up, 58 | Down, 59 | Left, 60 | Right, 61 | } 62 | 63 | struct Game { 64 | arena: Arena, 65 | pos: Pos, 66 | snake: [Pos; MAX_LENGTH], 67 | length: usize, 68 | target_length: usize, 69 | dir: Dir, 70 | rand: Rand, 71 | food_count: u32, 72 | } 73 | 74 | impl Game { 75 | fn new() -> Game { 76 | Game { 77 | arena: Arena::new(), 78 | pos: Pos { x: 15, y: 12 }, 79 | snake: [Pos { x: 0, y: 0 }; MAX_LENGTH], 80 | length: 0, 81 | target_length: 5, 82 | dir: Dir::Up, 83 | rand: Rand::new(1234), 84 | food_count: 0, 85 | } 86 | } 87 | fn reset(&mut self) { 88 | for y in 0..HEIGHT { 89 | for x in 0..WIDTH { 90 | self.arena.set(x, y, Tile::Empty); 91 | } 92 | } 93 | self.pos.x = WIDTH / 2; 94 | self.pos.y = HEIGHT / 2; 95 | self.length = 0; 96 | self.target_length = 5; 97 | self.dir = Dir::Up; 98 | self.food_count = 0; 99 | self.arena.set(self.pos.x, self.pos.y, Tile::Snake); 100 | } 101 | 102 | fn update(&mut self, key_state: &gba::KeyState) { 103 | if key_state.is_triggered(gba::Key::Up) { 104 | self.dir = Dir::Up 105 | } 106 | if key_state.is_triggered(gba::Key::Down) { 107 | self.dir = Dir::Down 108 | } 109 | if key_state.is_triggered(gba::Key::Left) { 110 | self.dir = Dir::Left 111 | } 112 | if key_state.is_triggered(gba::Key::Right) { 113 | self.dir = Dir::Right 114 | } 115 | self.snake[self.length].x = self.pos.x; 116 | self.snake[self.length].y = self.pos.y; 117 | if self.length < self.target_length { 118 | self.length += 1; 119 | } else { 120 | self.arena 121 | .set(self.snake[0].x, self.snake[0].y, Tile::Empty); 122 | for i in 0..self.length { 123 | self.snake[i].x = self.snake[i + 1].x; 124 | self.snake[i].y = self.snake[i + 1].y; 125 | } 126 | } 127 | let food_x = (self.rand.next_u8() & 31) as usize; 128 | let food_y = (self.rand.next_u8() & 31) as usize; 129 | if self.food_count < 4 && food_x < WIDTH && food_y < HEIGHT { 130 | match self.arena.get(food_x, food_y) { 131 | Tile::Empty => { 132 | self.arena.set(food_x, food_y, Tile::Food); 133 | self.food_count += 1; 134 | } 135 | _ => {} 136 | } 137 | } 138 | match self.dir { 139 | Dir::Up => self.pos.y = self.pos.y.wrapping_sub(1), 140 | Dir::Down => self.pos.y += 1, 141 | Dir::Left => self.pos.x = self.pos.x.wrapping_sub(1), 142 | Dir::Right => self.pos.x += 1, 143 | } 144 | match self.arena.get(self.pos.x, self.pos.y) { 145 | Tile::Snake => self.reset(), 146 | Tile::Food => { 147 | self.food_count -= 1; 148 | self.target_length += 5; 149 | if self.target_length > MAX_LENGTH - 1 { 150 | self.target_length = MAX_LENGTH - 1; 151 | } 152 | } 153 | _ => {} 154 | }; 155 | self.arena.set(self.pos.x, self.pos.y, Tile::Snake); 156 | } 157 | } 158 | 159 | #[no_mangle] 160 | pub extern "C" fn main() { 161 | let mut key_state = gba::KeyState::new(); 162 | gba::hw::write_dispcnt(1 << 8); 163 | gba::hw::write_bg0cnt(1 << 8); 164 | gba::hw::write_pal(15, 0x7fff); 165 | gba::hw::write_pal(31, 31 << 5); 166 | for i in 1..7 { 167 | gba::hw::write_vram16(i * 2, 0xfff0); 168 | gba::hw::write_vram16(i * 2 + 1, 0x0fff); 169 | } 170 | let mut game = Game::new(); 171 | game.reset(); 172 | loop { 173 | key_state.update(); 174 | game.update(&key_state); 175 | for _ in 0..4 { 176 | gba::wait_vblank(); 177 | } 178 | } 179 | } 180 | --------------------------------------------------------------------------------