Conway氏についてですが、公式にアナウンスがでたようです。ご冥福をお祈り申し上げます。
2003年に発売された「Linuxから目覚めるぼくらのゲームボーイ!」というC言語でゲームボーイアドバンスで動作する自作ゲームを作成していく書籍があります。
ゲームボーイアドバンスはARM7TDMI
というコアを使用しており、Rust
で自作ゲームを作ることも可能となっています。
この記事では「Linuxから目覚めるぼくらのゲームボーイ!」のステップをRust
で実施するための準備としてライフゲームが動くまでを書いてみます。
動機は今作っているWasm
インタープリタをGBA
で動かすことができないかの調査です。(たとえLチカレベルでも)AssemblyScript
とかでGBA
のゲームかけたら面白くないですか。
成果物
I succeeded to run Conway's Game of Life written in Rust on GameBoyAdvance. #rustlang https://t.co/A7rOJg3SwV pic.twitter.com/KEiokbDCI7
— bokuweb (@bokuweb17) April 5, 2020
環境のセットアップ
Rust
はARM
に対応しています。しかし、Coretex
シリーズなど比較的新しめのアーキテクチャのみの対応となっており、古いARM7TDMI
には対応していません。
幸いなことにLLVM
はこのCPUに対応しているので直接LLVM
にターゲット設定してやることでARM7TDMI
に対応したバイナリを出力することが可能となります。
With Docker
Dockerイメージは用意したのでmakefile
を書いておけば以下のように使えます
$ docker run --rm -v `pwd`:/code bokuweb/rust-gba make
Without Docker
Rust
Rust
現時点での最新のnightly
を使用しています。組み込みでRust
を使用する場合原則nightly
となるようです。
rust version 1.44.0-nightly (94d346360 2020-04-09)
また、クロスビルド用にcargo-xbuild
もインストールします。
$ rustup component add rust-src $ cargo install cargo-xbuild
Toolchain
ARM7TDMI
用のToolchainを用意します。ターゲットはarm-none-eabi
。
$ wget http://ftp.gnu.org/gnu/binutils/binutils-2.27.tar.gz $ tar -zxvf binutils-2.27.tar.gz $ cd binutils-2.27 $ ./configure --target=arm-none-eabi $ sudo make $ sudo make install
Hello World!
画面にdotを打つのがGBA
プログラミングにおけるHello World
のようです。
crt0.S
ではmain.rs
から書けるかと言うとそうではありません。まずはmain
関数を呼ぶためのスタートアップルーチンをアセンブラで書く必要があります。
といっても最低限でよければ以下を書くだけです。main
を呼び出しているだけです。割り込みを使用する際にはここでいくつか設定が必要になると思いますがとりあえずは不要です。詳しくは「Linuxから目覚めるぼくらのゲームボーイ!」に記載があります。
.arm __start: ldr r0, =main bx r0
linker.ld
今回のようなケースでは、どのようなメモリ構成になっていて、どこにどのセクションをロードするのか。を教えてあげる必要があります。そのためにリンカースクリプトを用意します。
ENTRY(__start) MEMORY { boot (w!x) : ORIGIN = 0x2000000, LENGTH = 256K wram (w!x) : ORIGIN = 0x3000000, LENGTH = 32K rom (rx) : ORIGIN = 0x8000000, LENGTH = 32M } SECTIONS { .text : { KEEP(target/crt0.o(.text)); *(.text .text.*); . = ALIGN(4); } >rom = 0xff }
「プログラムを内蔵ROM(0x8000000)に配置する」よう記述しています
main.rs
ようやくmain.rs
にとりかかります。以下は液晶ディスプレイの中央に青いドットを一つ表示するサンプルです。
#![no_std] #![feature(start)] #[panic_handler] fn panic(_info: &core::panic::PanicInfo) -> ! { loop {} } #[start] fn main(_argc: isize, _argv: *const *const u8) -> isize { unsafe { (0x400_0000 as *mut u16).write_volatile(0x0403); // BG2 Mode 3 (0x600_0000 as *mut u16) .offset((80 * 240 + 120) as isize) .write_volatile(0x001F); } 0 }
#![no_std]
Rust
の標準ライブラリはOSの機能を利用しているため今回のようなベアメタルなケースでは使用できません。そのため#![no_std]
をつけてstd
クレートではなくcore
クレートをリンクするよう示します。(アロケータも使用できなくなるため、この時点ではVec
やString
も使用不可能となります。)
#[panic_handler]
#[panic_handler]
アトリビュートにて、パニック発生時の動作を定義しています
main
(0x400_0000 as *mut u16).write_volatile(0x0403); // BG2 Mode 3
0x400_0000
番地はLCD制御を行うレジスタで0x0403
を書き込むことで複数枚ある背景のなかから2枚目、描画モード3を指定しています。
GBA
でもファミコンやゲームボーイのように8x8のスプライトを敷き詰めてゲームを描画するのが基本的な使用方法になるようなのですが、ピクセル単位で描画することができるモードがあり、モード3はそのうちの一つです。
スプライトの用意や設定をせずに直接描画できるので楽です。
write_volatile
は最適化により省略されたり並べ替えられたりしないことを保証するメソッドです。
例えば組み込みなどでは「レジスタのあるビットが変化するまでloopの中で待ち続ける」といった処理を書くことがあるかと思いますが、その場合volatile
がないと同じ番地にリードを繰り返す無意味な処理とみなされ省略されるケースがあると思います。xxxx_volatile
を使用することでこれを防げるという認識です。
(0x600_0000 as *mut u16) .offset((80 * 240 + 120) as isize) .write_volatile(0x001F);
0x0600_0000
はVRAM
で80 * 240 + 120
にオフセットすることで真ん中にドットを打つことができますできます。これは表示領域が240 * 160
のためです。
ここでは0x001F
を書き込んでいますが、これは赤色になります。GBA
でこのモードの場合各ドット色は15bitで表現されます。bit0~4が赤、bit5~9が緑、bit10~14が青となっています。そのためこのサンプルは赤いドットになります。
arm-none-eabi.json
前述したようにARM7TDMI
用のバイナリを吐くにはLLVM
に対してターゲットを指定してやる必要があります
{ "abi-blacklist": [ "stdcall", "fastcall", "vectorcall", "thiscall", "win64", "sysv64" ], "arch": "arm", "cpu": "arm7tdmi", "data-layout": "e-m:e-p:32:32-i64:64-v128:64:128-a:0:32-n32-S64", "executables": true, "linker": "arm-none-eabi-ld", "linker-flavor": "ld", "linker-is-gnu": true, "llvm-target": "thumbv4-none-eabi", "os": "none", "panic-strategy": "abort", "pre-link-args-crt": { "ld": ["crt0.o"] }, "pre-link-args": { "ld": ["-Tlinker.ld"] }, "relocation-model": "static", "target-c-int-width": "32", "target-endian": "little", "target-pointer-width": "32" }
llvm-target
がthumbv4-none-eabi
を指定したりlinker
にarm-none-eabi-ld
に指定し、pre-link-args-crt
でcrt0.o
をpre-links-args
でlinker.ld
を指定することでGBA
用のバイナリを吐くようになります。
このjsonはいくつかのサンプルを参考にしており、その際data-layout
について、初めて見たのでびっくりしたのですが、以下を見ると読み解くことができそうです。
どうやらターゲットのエンディアンがリトルエンディアンであることやポインタを32bitアライメントでレイアウトすること、64bit整数を64bitアライメントでレイアウトする等指定しているようです。
makefile
自分は以下のようなmakefileを用意しました。
arm-none-eabi-as
でcrt0.S
からcrt.o
を作成しtarget
配下に格納。
cargo xbuild
でビルドした後、arm-none-eabi-objcopy
でelf
をbin
に変換しています。
build: mkdir -p target arm-none-eabi-as src/crt0.S -o target/crt0.o cargo xbuild --target arm-none-eabi.json --release arm-none-eabi-objcopy -O binary target/arm-none-eabi/release/lifegameboy game.gba
ビルド
ここまででようやくビルドできそうです。
$ docker run --rm -v `pwd`:/code bokuweb/rust-gba make
でうまくいけばgame.gba
が生成されると思います。
実行
エミュレータで動作させてみます。自分はvisualboyadvance-m
を使用しました。
$ visualboyadvance-m game.gba
うまく行けば以下のように赤いドットが表示されると思います。これでようやくHello World!
が終了です。
Conway's Game of Life
もう少し動きのあるものを動かしてみたいので以前Wasm
の勉強用に作ったライフゲームを移植してみたいと思います。
Allocator
前述したように現時点ではVec
やString
が使用できません。が、上記のサンプルはVec
を使用しています。組み込みなどでは動的メモリ確保を禁止するルールなども多く、Vec
を利用しないよう書き換えることも可能ですがせっかくなのでVec
を使えるようにします。
linnker.ldの修正
linker.ld
を修正し、data
,bss
のセクションを追加し、各開始と終了アドレス、またwram
の終了アドレスを取れるようにしました。
SECTIONS { .text : { KEEP(target/crt0.o(.text)); *(.text .text.*); . = ALIGN(4); } >rom = 0xff .rodata : { *(.rodata .rodata.*); . = ALIGN(4); } >rom = 0xff .data : { __data_start = ABSOLUTE(.); *(.data .data.*); . = ALIGN(4); __data_end = ABSOLUTE(.); } >wram AT>rom = 0xff .bss : { __bss_start = ABSOLUTE(.); *(.bss .bss.*); . = ALIGN(4); __bss_end = ABSOLUTE(.); } >wram __sidata = LOADADDR(.data); __wram_end = ORIGIN(wram) + LENGTH(wram) -1 ; }
main.rsの修正
main.rs
を修正します。まずはMutex
とHeap
を作成します。これはCoretex-M
用のアロケータであるrust-embedded/alloc-cortex-m
から移植してきています。
Heap / Mutexの作成
pub struct Heap { heap: Mutex<linked_list_allocator::Heap>, } impl Heap { pub const fn empty() -> Heap { Heap { heap: Mutex::new(linked_list_allocator::Heap::empty()), } } pub unsafe fn init(&self, start_addr: usize, size: usize) { self.heap.lock(|heap| heap.init(start_addr, size)); } } pub struct Mutex<T> { inner: UnsafeCell<T>, } impl<T> Mutex<T> { pub const fn new(value: T) -> Self { Mutex { inner: UnsafeCell::new(value), } } } impl<T> Mutex<T> { pub fn lock<F, R>(&self, f: F) -> R where F: FnOnce(&mut T) -> R, { unsafe { let ie = core::ptr::read_volatile(REG_IE as *const u16); (REG_IE as *mut u16).write_volatile(0x0000); let ret = f(&mut *self.inner.get()); (REG_IE as *mut u16).write_volatile(ie); ret } } } unsafe impl<T> Sync for Mutex<T> {}
rust-embedded/alloc-cortex-m
の中身を見たところ、linked_list_allocator
クレートのHeap
をMutex
で包んだものを使用していました。no_std
なのでMutex
も用意する必要がありますが、リソース取得前に割り込みを禁止、取得後に割り込みを再設定をしてやれば良さそうです。以下の箇所が該当の箇所です。
let ie = core::ptr::read_volatile(REG_IE as *const u16); (REG_IE as *mut u16).write_volatile(0x0000); let ret = f(&mut *self.inner.get()); (REG_IE as *mut u16).write_volatile(ie);
global_allocatorの設定
上記で作成したHeap
をglobal_allocator
として設定し初期化することでヒープが使用できるようになりVec
が使用可能となります。
#[global_allocator] static ALLOCATOR: Heap = Heap::empty(); unsafe impl GlobalAlloc for Heap { unsafe fn alloc(&self, layout: Layout) -> *mut u8 { self.heap .lock(|heap| heap.allocate_first_fit(layout)) .ok() .map_or(0 as *mut u8, |allocation| allocation.as_ptr()) } unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { self.heap.lock(|heap| heap.deallocate(NonNull::new_unchecked(ptr), layout)); } }
Allocatorの初期化
以下がヒープの初期化コードです。liner.ld
の設定を参照し、ヒープの開始アドレスとサイズを求め設定しています。
extern "C" { static mut __bss_start: u8; static mut __bss_end: u8; static mut __data_start: u8; static mut __data_end: u8; static __sidata: u8; static __wram_end: u8; } fn init_heap() { unsafe { let heap_start = &__bss_end as *const u8 as usize; let heap_end = &__wram_end as *const u8 as usize; let heap_size = heap_end - heap_start; ALLOCATOR.init(heap_start, heap_size); } }
Vecを使用したmain
あとはinit_heap
を呼べば、ヒープを使用できるようになります。
#[macro_use] extern crate alloc; #[start] fn main(_argc: isize, _argv: *const *const u8) -> isize { init_heap(); let mut v = vec!(0); v.push(1); 0 }
ここで以下のようなリンクエラーがでてハマっていたのですが、lto = true
にすることで成功するようになりました。詳細は不明なのですが、リンクする必要のないものを探しにいき失敗していたところ、最適化により回避できるようになったように見えます。
error: linking with `arm-none-eabi-ld` failed: exit code: 1 | = note: "arm-none-eabi-ld" "-Tlinker.ld" "-L" "/home/bokuweb/ghq/github.com/bokuweb/gba-sandbox/target/sysroot/lib/rustlib/arm-none-eabi/lib" "/home/bokuweb/ghq/github.com/bokuweb/gba-sandbox/target/arm-none-eabi/release/deps/gba_sandbox-6d2b8af1d4d70cc2.gba_sandbox.dymocybm-cgu.0.rcgu.o" "/home/bokuweb/ghq/github.com/bokuweb/gba-sandbox/target/arm-none-eabi/release/deps/gba_sandbox-6d2b8af1d4d70cc2.gba_sandbox.dymocybm-cgu.1.rcgu.o" "/home/bokuweb/ghq/github.com/bokuweb/gba-sandbox/target/arm-none-eabi/release/deps/gba_sandbox-6d2b8af1d4d70cc2.gba_sandbox.dymocybm-cgu.2.rcgu.o" "/home/bokuweb/ghq/github.com/bokuweb/gba-sandbox/target/arm-none-eabi/release/deps/gba_sandbox-6d2b8af1d4d70cc2.gba_sandbox.dymocybm-cgu.3.rcgu.o" "/home/bokuweb/ghq/github.com/bokuweb/gba-sandbox/target/arm-none-eabi/release/deps/gba_sandbox-6d2b8af1d4d70cc2.gba_sandbox.dymocybm-cgu.4.rcgu.o" "/home/bokuweb/ghq/github.com/bokuweb/gba-sandbox/target/arm-none-eabi/release/deps/gba_sandbox-6d2b8af1d4d70cc2.gba_sandbox.dymocybm-cgu.5.rcgu.o" "/home/bokuweb/ghq/github.com/bokuweb/gba-sandbox/target/arm-none-eabi/release/deps/gba_sandbox-6d2b8af1d4d70cc2.gba_sandbox.dymocybm-cgu.6.rcgu.o" "/home/bokuweb/ghq/github.com/bokuweb/gba-sandbox/target/arm-none-eabi/release/deps/gba_sandbox-6d2b8af1d4d70cc2.gba_sandbox.dymocybm-cgu.7.rcgu.o" "-o" "/home/bokuweb/ghq/github.com/bokuweb/gba-sandbox/target/arm-none-eabi/release/deps/gba_sandbox-6d2b8af1d4d70cc2" "/home/bokuweb/ghq/github.com/bokuweb/gba-sandbox/target/arm-none-eabi/release/deps/gba_sandbox-6d2b8af1d4d70cc2.ivk3z60kvi15yiz.rcgu.o" "--gc-sections" "-O1" "-L" "/home/bokuweb/ghq/github.com/bokuweb/gba-sandbox/target/arm-none-eabi/release/deps" "-L" "/home/bokuweb/ghq/github.com/bokuweb/gba-sandbox/target/release/deps" "-L" "/home/bokuweb/ghq/github.com/bokuweb/gba-sandbox/target/sysroot/lib/rustlib/arm-none-eabi/lib" "-Bstatic" "/home/bokuweb/ghq/github.com/bokuweb/gba-sandbox/target/arm-none-eabi/release/deps/liblinked_list_allocator-50c4d42fee3618a7.rlib" "/home/bokuweb/ghq/github.com/bokuweb/gba-sandbox/target/sysroot/lib/rustlib/arm-none-eabi/lib/liballoc-65305f99407f1a06.rlib" "/home/bokuweb/ghq/github.com/bokuweb/gba-sandbox/target/sysroot/lib/rustlib/arm-none-eabi/lib/librustc_std_workspace_core-3a5b75d78f842520.rlib" "/home/bokuweb/ghq/github.com/bokuweb/gba-sandbox/target/sysroot/lib/rustlib/arm-none-eabi/lib/libcore-f024f2af9aa61b13.rlib" "/home/bokuweb/ghq/github.com/bokuweb/gba-sandbox/target/sysroot/lib/rustlib/arm-none-eabi/lib/libcompiler_builtins-fc44ec67faac1c70.rlib" "-Bdynamic" = note: arm-none-eabi-ld: warning: cannot find entry symbol __start; defaulting to 0000000008000000 /home/bokuweb/ghq/github.com/bokuweb/gba-sandbox/target/arm-none-eabi/release/deps/gba_sandbox-6d2b8af1d4d70cc2.ivk3z60kvi15yiz.rcgu.o:(.ARM.exidx.text.__rust_alloc+0x0): undefined reference to `__aeabi_unwind_cpp_pr0' /home/bokuweb/ghq/github.com/bokuweb/gba-sandbox/target/arm-none-eabi/release/deps/gba_sandbox-6d2b8af1d4d70cc2.ivk3z60kvi15yiz.rcgu.o:(.ARM.exidx.text.__rust_dealloc+0x0): undefined reference to `__aeabi_unwind_cpp_pr0' /home/bokuweb/ghq/github.com/bokuweb/gba-sandbox/target/arm-none-eabi/release/deps/gba_sandbox-6d2b8af1d4d70cc2.ivk3z60kvi15yiz.rcgu.o:(.ARM.exidx.text.__rust_realloc+0x0): undefined reference to `__aeabi_unwind_cpp_pr0' error: aborting due to previous error
ゲームの描画
今回は4x4
を1lifeとして作成しました。当初1pixel/lifeで実装しようとしたのですが、あっという間にヒープが枯渇したためです。Working RAM領域は32KiBなので当たり前感がありますね。メモリを節約するよう書き換えれば1pixel/lifeでもいけそうでしたが、あまりゲーム側に手を加えたくなかったので今回はこのようにしています。
wait_for_vsync
はVBlank
までブロックし待つ関数です。VRAM
を更新するのは基本的にはVBlank
の間に行うことになっています。VBlank
というのは描画を行っていない期間です。これは描画中にVRAM
を変更すると意図しない描画になる可能性があるためです。
VBlank
中であるかどうかは割り込みにて知ることも可能ですが、REG_VCOUNT
すなわち0x0400_0006
をリードすることで知ることもできます。REG_VCOUNT
を読むことで現在描画中のy座標を知ることができるのでこの値が160
以上であればVBlank
ということになります。
あとは特に特筆することはなく、lifeを計算し描画していくだけなので割愛します。
static REG_VCOUNT: usize = 0x0400_0006; fn wait_for_vsync() { unsafe { while core::ptr::read_volatile(REG_VCOUNT as *const u32) >= 160 {} while core::ptr::read_volatile(REG_VCOUNT as *const u32) < 160 {} } } #[start] fn main(_argc: isize, _argv: *const *const u8) -> isize { // ... ommited ... init_heap(); let mut game: Game = Game::new(40, 60); unsafe { (0x400_0000 as *mut u16).write_volatile(0x0403); // BG2 Mode 3 loop { let field = game.next(); wait_for_vsync(); for (i, cell) in field.iter().enumerate() { let col = i % 60; let row = i / 60; let color = if *cell { 0x7FFF } else { 0x0000 }; for j in 0..4 { (0x600_0000 as *mut u16) .offset(((row * 4 + j) * 240 + col * 4) as isize) .write_volatile(color); (0x600_0000 as *mut u16) .offset(((row * 4 + j) * 240 + col * 4 + 1) as isize) .write_volatile(color); (0x600_0000 as *mut u16) .offset(((row * 4 + j) * 240 + col * 4 + 2) as isize) .write_volatile(color); (0x600_0000 as *mut u16) .offset(((row * 4 + j) * 240 + col * 4 + 3) as isize) .write_volatile(color); } } } } }
正しく動作すれば以下のようにライフゲームが動作します。
実機への転送
「Linuxから目覚めるぼくらのゲームボーイ!」にはブートケーブルが付属しており、これを使うことで実機で動作させることが可能になります。
linker.ldの修正
ブートケーブル経由で動作させる場合リンカースクリプトの修正が必要になります。これはブートモード時はROM領域ではなく0x0200_0000
の外部RAM領域から起動するためです。
rom
の箇所をboot
に変更します。
SECTIONS { .text : { KEEP(target/crt0.o(.text)); *(.text .text.*); . = ALIGN(4); } >boot = 0xff .rodata : { *(.rodata .rodata.*); . = ALIGN(4); } >boot = 0xff .data : { __data_start = ABSOLUTE(.); *(.data .data.*); . = ALIGN(4); __data_end = ABSOLUTE(.); } >wram AT>boot = 0xff .bss : { __bss_start = ABSOLUTE(.); *(.bss .bss.*); . = ALIGN(4); __bss_end = ABSOLUTE(.); } >wram __sidata = LOADADDR(.data); __wram_end = ORIGIN(wram) + LENGTH(wram) -1 ; }
optusbのセットアップ
ブートケーブル経由で転送するためにoptusb
というツールを使います。optusb
に必要なlibusb
をセットアップするのですが、optusb
が使用しているAPIが古いためlibusb-compat
が必要となります。macであればbrewで入るようです。
ちなみに記事ではUbuntu19.10
、ThinkPad X1 Carbon gen 6th
で試しています。
$ wget http://downloads.sourceforge.net/libusb/libusb-compat-0.1.5.tar.bz2 $ tar -jxvf libusb-compat-0.1.5.tar.bz2 $ sudo make $ sudo make install $ sudo mount --bind /dev/bus /proc/bus $ sudo ln -s /sys/kernel/debug/usb/devices /proc/bus/usb/devices
次にoptusbのコードをダウンロード、展開します。
$ wget http://www.skyfree.org/jpn/unixuser/optusb-1.01.tar.gz $ tar -zxvf optusb-1.01.tar.gz
その後Makefileを書き換えビルドすることでセットアップは完了です。
optusb: optusb.c usb.h libusb.a gcc -I `brew --prefix libusb-compat`/include/usb.h -L `brew --prefix libusb-compat`/lib -lusb -Wall -o optusb optusb.c clean: rm -rf optusb *.o
make
以下のエントリが参考になりました。
データの転送
本体の電源を投入後、ケーブルを接続、以下のコマンドで転送ができます。
sudo optusb/optusb game.gba
動作すれば冒頭のtweetように動作します。
I succeeded to run Conway's Game of Life written in Rust on GameBoyAdvance. #rustlang https://t.co/A7rOJg3SwV pic.twitter.com/KEiokbDCI7
— bokuweb (@bokuweb17) April 5, 2020
結果
動機であったWasmインタプリタを動かせるか、ですが、インタプリタ側をno_std
に対応させれば動くのではないかと思っていますが、あとはメモリ容量との勝負になりそう。できたとしてもドットをちょっと動かす程度のものにはなると思いますが、もし成功したらまた記事にします。