undefined

bokuweb.me

Rust+wasmでライフゲーム


Rustとwasmの入門にライフゲームを書いてみた

成果物

github.com

以下のURLで動作を確認できますが、わらわらしてますので苦手な方は注意してください。 windowサイズを小さくすると60FPSでて楽しいです。

Game of life with rust + wasm

環境構築

環境構築は以下の記事を参考にさせてもらっています。

sbfl.net

また「Think Web」の「Rust + WebAssembly でブラウザで動くLisp処理系を作る」も合わせて参考にさせてもらってます。

techbooster.booth.pm

実装

JS側からRust側へポインタを渡しておき、JS側のrequestAnimaionFrameからRust側から公開されているupdate関数を叩き、更新されたメモリをcanvasに反映するという構成を取っています。

Rust側とメモリを共有

簡略化してますが以下のようにwindowサイズ分の領域を確保して初期化(実際にはランダムにtrue / falseで埋めてますが省略)ポインタとサイズをupdateに渡してます。

  • js
const bufsize = window.innerHeight * window.innerWidth;
const bufptr = Module._malloc(bufsize);

Module._memset(bufptr, 0, bufsize);
let buf = new Uint8Array(Module.HEAPU8.buffer, bufptr, bufsize);

  ... 省略 ...
  
  update(bufsize, buf.byteOffset, column);

Rust側でupdateは以下のようになっていて、もらったポインタ、サイズからsliceを作成して次のステートを作成、バッファに戻す、という処理を行っています。

  • Rust
#[no_mangle]
pub extern "C" fn update(len: usize, ptr: *mut bool, col: usize) {
    let row = len / col;
    let buf: &mut [bool] = unsafe { std::slice::from_raw_parts_mut(ptr, len) };
    let game: Vec<bool> = Game::new(buf, row, col).next();
    buf.clone_from_slice(game.as_slice())
}

また、jsからupdateをを使用するために、公開する関数をbuild時に指定してやる必要があります。 具体的には -C link-args="-s EXPORTED_FUNCTIONS=['_update']"のように指定する必要があります。 実際buildオプションは以下から確認できます。

github.com

Rust側のGame

数十行なので載せときます。 もらったSliceをVec<Vec>に変換して、あとはゲームのルールに従い次のステートを算出しています。

この間、コンパイラにはめちゃめちゃ叱られたし、未だ書ける気がしてこないが、入門にはいい題材だったっぽい。 パターンマッチ好きです。

type Field<T> = Vec<Vec<T>>;

pub struct Game {
    field: Field<bool>,
}

impl Game {
    pub fn new(buf: &[bool], row_size: usize, col_size: usize) -> Game {
        let field = Game::create(buf, row_size, col_size);
        Game { field }
    }

    pub fn next(self) -> Vec<bool> {
        self.field
            .iter()
            .enumerate()
            .map(|(y, r)| self.next_row(r, y))
            .flat_map(|x| x)
            .collect()
    }


    fn next_row(&self, row: &Vec<bool>, y: usize) -> Vec<bool> {
        row.iter()
            .enumerate()
            .map(|(x, _)| self.next_cell(y as i32, x as i32))
            .collect()
    }

    fn next_cell(&self, y: i32, x: i32) -> bool {
        let alive_num = [
            (y - 1, x - 1),
            (y, x - 1),
            (y + 1, x - 1),
            (y - 1, x),
            (y + 1, x),
            (y - 1, x + 1),
            (y, x + 1),
            (y + 1, x + 1),
        ].iter()
            .map(|&(y, x)| self.get_cell_state(y, x))
            .filter(|cell| *cell)
            .collect::<Vec<_>>()
            .len();
        match alive_num {
            3 => true,
            2 if self.is_alive(y as usize, x as usize) => true,
            _ => false,
        }
    }

    fn is_alive(&self, y: usize, x: usize) -> bool {
        self.field[y][x]
    }

    fn create(buf: &[bool], row_size: usize, col_size: usize) -> Field<bool> {
        (0..row_size)
            .into_iter()
            .map(|i| {
                let start = i * col_size;
                let end = start + col_size;
                buf[start..end].to_vec()
            })
            .collect()
    }

    fn get_cell_state(&self, row: i32, column: i32) -> bool {
        match self.field.iter().nth(row as usize) {
            Some(r) => {
                match r.iter().nth(column as usize) {
                    Some(c) => *c,
                    None => false,
                }
            }
            None => false,
        }
    }
}

速度

Rustのコードが完成してから、そのコードをJSにざくっと移植して、速度を測ってみたところ約5倍ほどwasmの方が早い結果となってる。 この結果はJSの最適化がされていないのが主要因だと思ってるので眉唾なんですが、フィボナッチで比較した際3倍程度との記事を見かけたことがあるので最適化を施していくとその辺に落ち着くのかもしれない。この辺は宿題。

今後

書く量が圧倒的が足りないのでテーマアップして継続していきたい。ひとまずはテトリスとか、ファミコンエミュレータを考えてる。キーボードも自作したいしテーマはが尽きなそう。時間が足りない。