コンピュータシステムの理論と実装のハードウェアシミュレータをRustとWasmで実装してブラウザで動かしてみました

これはドクターメイト Advent Calendar 2024の24日目です。

クリスマスイブですね、クリスマスは楽しく過ごしたいなと思う今日この頃です。さて、せっかくRustの会社にいるしRustキャッチアップしたいなぁと思って*1Rustでコンピュータシステムの理論と実装(以下、nand2tetris本*2のハードウェアシミュレータを実装してみました。空気も読まずにプログラマらしくちゃんと動くものを実装してみた系のテーマにしたらだいぶ時間を費やすことになりました。自分向けの備忘録でもあるので以下ダラダラと長くなっています。*3

以前のブログ記事では書籍1〜5章のハードウェアパーツのロジックをHDLで実装し書籍が用意したハードウェアシミュレータ上で実行しました。今回はRustでハードウェアシミュレータもろともフルスクラッチで全て実装します。以前に書いた1〜5章のブログ記事は下記ですね。

nihemak.hatenablog.com

今回のコード

下記、タグv0.0.6になります。

github.com

下記で環境をcloneできます。

git clone -b v0.0.6 https://github.com/nihemak/nand2tetris.git
cd nand2tetris

概要

今回は冒頭で書いた通りRustでNANDゲートの関数を定義するところから始めてHACKコンピュータのバイナリコードが動作するところを目指して実装していきました。この記事の最後にはブラウザで動くところまでは辿り着けます。ただ最初にネタバレすると実用的な動作速度には達していません。ご容赦くださいませ。

実装は次のステップで行いました。

  1. ステップ1: RustとSDLによる実装
    • ハードウェア実装に集中するためまずはScreenとKeyboardをSDLで実装
  2. ステップ2: RustとWasmによる実装
    • SDLをWasmに置き換えてブラウザで動くように実装
  3. ステップ3: RustとWasmによる実装(ビルトイン版)
    • ハードウェア実装を最適化コード(Nand由来ではないズルコード)に置き換えて動作速度を改善

全体構成はこんな感じです。

ステップ1: RustとSDLによる実装

まずScreenやKeyboardをSDL(Simple DirectMedia Layer)で実現する構成でハードウェアシミュレータを実装しました。

13/HardwareSimulatorです。詳細はコードを参照して下さい。

下記で動かせます。(事前にSDL2をインストールしておく必要があります。自分はmacなので brew install sdl2 しました。)

cd 13/HardwareSimulator

# test all
# https://stackoverflow.com/questions/74637159/how-to-increase-stack-size-of-threads-used-by-cargo-test
RUST_MIN_STACK=3000000 cargo test

# execute
cargo run

# execute (release build)
cargo build --release
./target/release/HardwareSimulator

起動すると画面(ウインドウ)を白から黒に塗り替えていきます。Escキーで終了します。

遅いです。。

ブール論理

1章のブール論理の実装は13/HardwareSimulator/src/boolean_logic.rsです。対応するHDLの実装は01にあります。

下記のように01はbool型にしました。型名はbitです。false0true1ですね。nand関数から初めてその他のゲートも関数として定義しています。

pub type bit = bool;
pub type word = [bit; 16];

pub fn nand(a: bit, b: bit) -> bit {
    !(a && b)
}

pub fn not(a: bit) -> bit {
    nand(a, a)
}

pub fn and(a: bit, b: bit) -> bit {
    not(nand(a, b))
}

//...

テストもちゃんと書くようにしました。HDLの*.tstのテストコードを参考にしています。パラメタライズドテストにするためにrstestを使っています。

#[cfg(test)]
mod tests {
    use rstest::*;
    use super::*;

    #[rstest]
    #[case((false, false), true)]
    #[case((false, true),  true)]
    #[case((true,  false), true)]
    #[case((true,  true),  false)]
    fn test_nand(#[case] input: (bit, bit), #[case] output: bit) {
        let (a, b) = input;
        assert_eq!(output, nand(a, b));
    }

//...

あとtrue, falseの羅列はしんどいのでu16からwordに変換するu16_to_word関数などヘルパー関数を定義して読み書きを楽にする工夫もしました。

    #[rstest]
    #[case(u16_to_word(0b0000_0000_0000_0000), u16_to_word(0b1111_1111_1111_1111))]
    #[case(u16_to_word(0b1111_1111_1111_1111), u16_to_word(0b0000_0000_0000_0000))]
    #[case(u16_to_word(0b1010_1010_1010_1010), u16_to_word(0b0101_0101_0101_0101))]
    #[case(u16_to_word(0b0011_1100_1100_0011), u16_to_word(0b1100_0011_0011_1100))]
    #[case(u16_to_word(0b0001_0010_0011_0100), u16_to_word(0b1110_1101_1100_1011))]
    fn test_not16(#[case] input: word, #[case] output: word) {
        assert_eq!(output, not16(input));
    }

ヘルパー関数は13/HardwareSimulator/src/helper.rsです。

ブール算術

2章のブール算術の実装は13/HardwareSimulator/src/boolean_arithmetic.rsです。対応するHDLの実装は02にあります。

こちらも愚直にゲートの関数を定義していってます。テストもちゃんと書いてます。

pub fn half_adder(a: bit, b: bit) -> (bit, bit) {
    let sum = xor(a, b);
    let carry = and(a, b);
    (sum, carry)
}

pub fn full_adder(a: bit, b: bit, c: bit) -> (bit, bit) {
    let (sum0, carry0) = half_adder(a, b);
    let (sum, carry1) = half_adder(sum0, c);
    let carry = or(carry0, carry1);
    (sum, carry)
}

//...

順序回路

3章の順序回路の実装は13/HardwareSimulator/src/sequential_circuit.rsです。対応するHDLの実装は03にあります。

こちらも愚直な回路の定義です。クロックはbitにしました。時間の概念が入ってきてここから若干複雑になってきます。テストが重要になってきます*4

#[derive(Debug, Copy, Clone)]
pub struct DFF {
    past_bit: bit,
    new_bit: bit
}

impl DFF {
    pub fn new() -> Self {
        DFF {
            past_bit: false,
            new_bit: false
        }
    }

    pub fn update(&mut self, clk: bit, a: bit) {
        if clk {
            self.past_bit = self.new_bit;
            self.new_bit = a
        }
    }

    pub fn get(self, clk: bit) -> bit {
        if clk { self.past_bit } else { self.new_bit }
    }
}

//...

コンピュータアーキテクチャ

5章のコンピュータアーキテクチャの実装は13/HardwareSimulator/src/hardware.rsです。対応するHDLの実装は05にあります。

あと少しです。ScreenKeyboardROM32Kなどの実装は書籍にはなかったので考慮が必要になります。アウトプットが再度インプットになる回路などだいぶハマりましたがテストが通っているので多分大丈夫なはず。知らんけど。

Screen

今回は画面の情報を取得できるget_all関数を用意し描画の責務を呼び出し元が持つ設計にしました。後からUIをWasmに置き換えることも考慮した結果です。

基本、RAM4Kの読み書きになるのですが動かしたら動作が激重だったのでget_all関数はRAM4Kを使わないビルトインの処理にするズルをしています。RAM4Kを使うロジックはコメントアウトで残してあります。

#[derive(Copy, Clone)]
pub struct Screen {
    rams: [RAM4K; 2],
    screen: [bit; 131072],
}

impl Screen {
    pub fn new() -> Self {
        Screen { 
            rams: [RAM4K::new(); 2],
            screen: [false; 131072],
        }
    }

    fn update(&mut self, clk: bit, input: word, load: bit, address: [bit; 13]) {
        let (a, b) = dmux(load, address[12]);
        let address_low = bit13_to_bit12(address);
        self.rams[0].update(clk, input, a, address_low);
        self.rams[1].update(clk, input, b, address_low);

        if load {
            let address_num = bit13_to_u16(address);
            if address_num <= 24575 {
                let screen_address: u32 = 16 * (address_num as u32);
                for n in 0..16 {
                    self.screen[(screen_address + n) as usize] = input[n as usize];
                }
            }
        }
    }

    fn get(&self, clk: bit, address: [bit; 13]) -> word {
        let address_low = bit13_to_bit12(address);
        mux16(
            self.rams[0].get(clk, address_low),
            self.rams[1].get(clk, address_low),
            address[12]
        )
    }

    pub fn get_all(&self) -> [bit; 131072] {
        // let mut screen = [false; 131072];
        // let mut x = 0;
        // for i in 0..8192 {
        //     let address = u16_to_13bit(i);
        //     let word = self.get(false, address);
        //     for j in 0..16 {
        //         screen[x] = word[j];
        //         x += 1;
        //     }
        // }
        // screen
        self.screen
    }
}

Keyboard

KeyboardScreenと同様に押下キーの取得の責務を呼び出し元でする設計になってます。そのためupdate関数で指定されたキーコードを保持するだけになってます。

#[derive(Debug, Copy, Clone)]
pub struct Keyboard {
    key_code: Register,
}

impl Keyboard {
    pub fn new() -> Self {
        Keyboard { key_code: Register::new() }
    }

    fn update(&mut self, clk: bit, key_code: word) {
        self.key_code.update(clk, key_code, true);
    }

    fn get(&self, clk: bit) -> word {
        self.key_code.get(clk)
    }
}

ROM32K

ROM32KRAM4Kの読み書きだけです。HACKのバイナリコードを読み込むload関数を用意し呼び出し元で指定する設計にしました。*.hackをそのまま読み込めるように(逆順に)デコードして読み込むようにしてあります。

#[derive(Copy, Clone)]
pub struct ROM32K {
    rams: [RAM4K; 8]
}

impl ROM32K {
    pub fn new() -> Self {
        ROM32K {
            rams: [RAM4K::new(); 8]
        }
    }

    pub fn update(&mut self, clk: bit, input: word, address: [bit; 15]) {
        let address_low = bit15_to_bit12(address);
        let address_high = [address[12], address[13], address[14]];
        let (a, b, c, d, e, f, g, h) = dmux8way(true, address_high);
        self.rams[0].update(clk, input, a, address_low);
        self.rams[1].update(clk, input, b, address_low);
        self.rams[2].update(clk, input, c, address_low);
        self.rams[3].update(clk, input, d, address_low);
        self.rams[4].update(clk, input, e, address_low);
        self.rams[5].update(clk, input, f, address_low);
        self.rams[6].update(clk, input, g, address_low);
        self.rams[7].update(clk, input, h, address_low);
    }

    fn get(&self, clk: bit, address: [bit; 15]) -> word {
        let address_low = bit15_to_bit12(address);
        let address_high = [address[12], address[13], address[14]];
        mux8way16(
            self.rams[0].get(clk, address_low),
            self.rams[1].get(clk, address_low),
            self.rams[2].get(clk, address_low),
            self.rams[3].get(clk, address_low),
            self.rams[4].get(clk, address_low),
            self.rams[5].get(clk, address_low),
            self.rams[6].get(clk, address_low),
            self.rams[7].get(clk, address_low),
            address_high
        )
    }

    pub fn load(&mut self, instructions: Vec<&str>) {
        let mut counter = u16_to_word(0b0000000000000000);
        for instruction in instructions {
            let mut decorded_instruction = u16_to_word(0b0000000000000000);
            for (i, c) in instruction.chars().enumerate() {
                if c == '1' {
                    decorded_instruction[15 - i] = true;
                }
            }
            // println!("instruction: {}", word_to_u16(decorded_instruction));
    
            let address = word_to_bit15(counter);
            self.update(true, decorded_instruction, address);
            counter = add16(counter, u16_to_word(0b0000000000000001));
        }
    }
}

main(エントリポイント)

エントリポイントの実装は13/HardwareSimulator/src/main.rsです。

下記をやってます。

  1. ここまで作ったコンピュータにHACKのバイナリコードを読み込んで起動
  2. 無限ループ
    1. Escが押下されたら終了
    2. 押下されたキーコードを取得しコンピュータを1クロック(true, false)進める
    3. コンピュータの画面情報を取得し描画

1. コンピュータに読み込むHACKのバイナリコード

ファイル等から読み込めたらかっこいいのですが今回はソースコード内に命令列を決め打ちしてあります。内容は04/fill/Fill.asmのデフォルトを背景黒にしたバージョンです。実行すると最初に画面を真っ黒にして何かキーが押下されたら画面を真っ白にします。*5

    // Fill
    let instructions: Vec<&str> = vec![
                            //(LOOP_KBD)
        "0110000000000000", //        @KBD
        "1111110000010000", //        D=M
        "0000000000001000", //        @SELECT_BLACK
        "1110001100000010", //        D; JEQ
        "0000000000000000", //        @0
        "1110110000010000", //        D=A
        "0000000000001010", //        @SET_COLOR
        "1110101010000111", //        0; JMP
                            //(SELECT_BLACK)
        "0000000000000000", //        @0
        "1110110010010000", //        D=A-1
                            //(SET_COLOR)
        "0000000000010000", //        @color
        "1110001100001000", //        M=D

        "0100000000000000", //        @SCREEN
        "1110110000010000", //        D=A
        "0000000000010001", //        @pos
        "1110001100001000", //        M=D

                            //        // 32 * 256 = 8192
        "0010000000000000", //        @8192
        "1110110000010000", //        D=A
        "0000000000010010", //        @n
        "1110001100001000", //        M=D

                            //(LOOP_FILL)
        "0000000000010010", //        @n
        "1111110000010000", //        D=M
        "0000000000100011", //        @FILL_END
        "1110001100000010", //        D; JEQ

                            //        // print color
        "0000000000010000", //        @color
        "1111110000010000", //        D=M
        "0000000000010001", //        @pos
        "1111110000100000", //        A=M
        "1110001100001000", //        M=D

        "0000000000010001", //        @pos
        "1111110111001000", //        M=M+1
        "0000000000010010", //        @n
        "1111110010001000", //        M=M-1

        "0000000000010100", //        @LOOP_FILL
        "1110101010000111", //        0; JMP
                            //(FILL_END)
        "0000000000000000", //        @LOOP_KBD
        "1110101010000111", //        0; JMP

    ];

2-2. 押下されたキーコードの取得

get_keyboard_press_code関数でしてます。SDLで押下判定してキーコードへのマッピングするのみです。

fn get_keyboard_press_code(keystate: &KeyboardState) -> word {
    u16_to_word(
        if keystate.is_scancode_pressed(Scancode::Num0)  { 0b0000_0000_0011_0000 } else
        if keystate.is_scancode_pressed(Scancode::Num1)  { 0b0000_0000_0011_0001 } else
        if keystate.is_scancode_pressed(Scancode::Num2)  { 0b0000_0000_0011_0010 } else
        if keystate.is_scancode_pressed(Scancode::Num3)  { 0b0000_0000_0011_0011 } else
        if keystate.is_scancode_pressed(Scancode::Num4)  { 0b0000_0000_0011_0100 } else
        if keystate.is_scancode_pressed(Scancode::Num5)  { 0b0000_0000_0011_0101 } else
        if keystate.is_scancode_pressed(Scancode::Num6)  { 0b0000_0000_0011_0110 } else
        if keystate.is_scancode_pressed(Scancode::Num7)  { 0b0000_0000_0011_0111 } else
// ...

2-2. コンピュータを1クロック進める

1クロック進むComputerstep関数を呼び出します。

        let state = event_pump.keyboard_state();
        computer.step(reset, get_keyboard_press_code(&state));
        reset = false;

Computerstep関数はこちらです。

    pub fn step(&mut self, reset: bit, word: word) {
        let mut clk = true;
        self.update(clk, reset, word);
        clk = !clk;
        self.update(clk, reset, word);
    }

2-3. コンピュータの画面情報を取得し描画

display_screen関数でしてます。Screenget_all関数の結果をSDLで描画しているだけです。

fn display_screen(canvas: &mut WindowCanvas, computer: &Computer) {
    let screen = computer.get_screen();
    for px in 0..screen.len() {
        let x = px % 512;
        let y = px / 512;
        let color = if screen[px] { Color::RGB(0, 0, 0) } else { Color::RGB(255, 255, 255) };
        canvas.set_draw_color(color);
        let w = 2;
        canvas.fill_rect(Rect::new((x * w).try_into().unwrap(), (y * w).try_into().unwrap(), w as u32, w as u32)).unwrap();
    }
}

実装を終えて

正直、デバッグも難しくて実装はしんどかったです。いくつかトピックを紹介します。

cargo testでスタックサイズが足りないエラーになる

13/HardwareSimulator/README.mdの通りテストはデフォルトだと fatal runtime error: stack overflow になり途中で止まります。これは巨大な構造体を全てスタックに置いているためです。ヒープに置くようにすれば解消するかもですが後回しにしました。

# test all
# https://stackoverflow.com/questions/74637159/how-to-increase-stack-size-of-threads-used-by-cargo-test
RUST_MIN_STACK=3000000 cargo test

doc.rust-jp.rs

動作速度が遅い

プログラムを起動すると画面がすぐに真っ黒になるはずなのですが全然なりません。。画面上部からちょっとずつ黒く塗りつぶしされていくのを眺めるしかないです。速度改善をしていかないと使い物にならなそうです。

参考にした情報源

今回は第1版です。

www.oreilly.co.jp

NANDゲートからのハードウェアシミュレータを実装する部分はハマったとき下記を参考にさせていただきました。

zenn.dev

github.com

caddi.tech

SDLの実装部分は下記を参考にさせていただきました。

qiita.com

ステップ2: RustとWasmによる実装

実行速度が出ないとはいえ一応は動いたので次のステップとしてSDLの部分をWasm化しました。RustとWebAssemblyによるゲーム開発の1〜3章の内容を参考にしました。*6

13/HardwareSimulatorWasmです。詳細はコードを参照して下さい。

下記で動かせます。

cd 13/HardwareSimulatorWasm

# test all
TEST=1 RUST_MIN_STACK=3000000 cargo test

nvm i v14.15.1
npm --version
# 6.14.8
node --version
# v14.15.1

npm install
npm start

起動すると画面(ブラウザ)を白から黒に塗り替えていきます。

めちゃくちゃ遅いです。。

ハードウェア部分の実装

13/HardwareSimulatorのハードウェア部分の実装を13/HardwareSimulatorWasm/src/nand2tetrisにコピーして調整しました。ロジックは変更していないです。

SDL実装のmainにあたる部分の実装

13/HardwareSimulatorWasm/src/nand2tetris.rsにあります。

キーボードや画面描画のコードはWasm向けに変更しています。

キーボードはこんな感じ。SDL実装とロジックは同じです。

    fn get_keyboard_press_code(keystate: &KeyState) -> word {
        u16_to_word(
            if keystate.is_pressed("Digit0")     { 0b0000_0000_0011_0000 } else 
            if keystate.is_pressed("Digit1")     { 0b0000_0000_0011_0001 } else 
            if keystate.is_pressed("Digit2")     { 0b0000_0000_0011_0010 } else 
            if keystate.is_pressed("Digit3")     { 0b0000_0000_0011_0011 } else 
            if keystate.is_pressed("Digit4")     { 0b0000_0000_0011_0100 } else 
            if keystate.is_pressed("Digit5")     { 0b0000_0000_0011_0101 } else 
            if keystate.is_pressed("Digit6")     { 0b0000_0000_0011_0110 } else 
            if keystate.is_pressed("Digit7")     { 0b0000_0000_0011_0111 } else 
            if keystate.is_pressed("Digit8")     { 0b0000_0000_0011_1000 } else 
            if keystate.is_pressed("Digit9")     { 0b0000_0000_0011_1001 } else 
            if keystate.is_pressed("ArrowLeft")  { 0b0000_0000_1000_0010 } else 
// ...

画面描画はこんな感じ。こちらもSDL実装とロジックは同じですね。

    fn draw(&self, renderer: &Renderer) {
        let screen = self.computer.get_screen();
        for px in 0..screen.len() {
            let x = px % 512;
            let y = px / 512;
            let color = if screen[px] {"#000000"} else {"#FFFFFF"};
            renderer.draw_pixel(color, x.try_into().unwrap(), y.try_into().unwrap());
        }
    }

Wasm向けのコード

下記です。ここはRustとWebAssemblyによるゲーム開発の1〜3章の内容の通りです。

実装を終えて

無事にWasmで動いてホッとしました。いくつかトピックを紹介します。

nodeバージョンが新しいとエラーになる

RustとWebAssemblyによるゲーム開発のコードが古いせいかNode.js v22.6.0だとnpm startでエラーになります。仕方がないので13/HardwareSimulatorWasm/README.mdの通り古いバージョンにして動かしました。最新バージョンに対応させたいですね。

nvm i v14.15.1
npm --version
# 6.14.8
node --version
# v14.15.1

npm install
npm start

ブラウザでスタックサイズが足りないエラーになる

npm start するとブラウザで RuntimeError: memory access out of bounds になり途中で止まりました。cargo testで起きるエラーと同じですね。仕方がないので13/HardwareSimulatorWasm/build.rsの通り起動時にスタックサイズを増やして回避しました。cargo testだと邪魔になるので環境変数TESTを指定されなかった場合だけ動くようにしてます。カッコ悪いので直したいですね。

use std::env;

fn main() {
    // FIXME: Remove this
    if let Ok(val) = env::var("TEST") {
    } else {
        // https://doc.rust-jp.rs/rust-by-example-ja/cargo/build_scripts.html
        // https://github.com/rustwasm/wasm-bindgen/issues/3368#issuecomment-1483954797
        // https://github.com/aduros/wasm4/blob/main/cli/assets/templates/rust/.cargo/config.toml
        // https://doc.rust-jp.rs/rust-by-example-ja/std/box.html
        println!("cargo::rustc-link-arg=-zstack-size=6000000");
    }
}

build.rsは13/HardwareSimulatorWasm/Cargo.tomlで指定してます。

build = "build.rs"

doc.rust-jp.rs

13/HardwareSimulatorWasm/README.mdの通りcargo testでTEST=1を指定する必要があります。

# test all
TEST=1 RUST_MIN_STACK=3000000 cargo test

ブラウザでよくわからないエラーが起きると途方に暮れますね

UI部分をSDLからWasmに変えるだけでほぼOKだった

ハードウェア部分の実装はほぼ変えないで良かったです。Wasm実装もほぼRustとWebAssemblyによるゲーム開発の内容でできたので比較的楽な対応でした。

やっぱり速度が出ない

SDL実装よりも遅いです。。ブラウザで動かすのでしょうがないと思いますが。

参考にした情報源

この書籍をがっつり参考にしました。

www.oreilly.co.jp

書籍のソースコードも参考にしました。

github.com

ステップ3: RustとWasmによる実装(ビルトイン版)

流石に遅いので高速化のためにRust組み込みのビット演算などを使った最適化版(ズルコード版)のビルトイン版も実装してどの程度の高速化になるか試してみました。

13/HardwareSimulatorWasmBuiltInです。詳細はコードを参照して下さい。

下記で動かせます。動かし方は13/HardwareSimulatorWasmと同じです。

cd 13/HardwareSimulatorWasmBuiltIn

# test all
TEST=1 RUST_MIN_STACK=3000000 cargo test

nvm i v14.15.1
npm --version
# 6.14.8
node --version
# v14.15.1

npm install
npm start

起動すると画面(ブラウザ)を白から黒に塗り替えていきます。

遅さは少しマシになった。でも遅い。。

ビルトイン版のイメージ

雰囲気としては、例えばRAM16Kのビルトイン版は下記のような感じです。ビルトイン版にはサフィックスに構造体は BuiltIn 、関数は _built_in をつけています。

#[derive(Debug, Copy, Clone)]
pub struct RAM16KBuiltIn {
    ram: [word; 16384 /* 14bit */],
}

impl RAM16KBuiltIn {
    pub fn new() -> Self {
        RAM16KBuiltIn {
            ram: [u16_to_word(0b0000_0000_0000_0000); 16384],
        }
    }

    pub fn update(&mut self, clk: bit, input: word, load: bit, address: [bit; 14]) {
        if clk && load {
            self.ram[bit14_to_u16(address) as usize] = input;
        }
    }

    pub fn get(&self, clk: bit, address: [bit; 14]) -> word {
        self.ram[bit14_to_u16(address) as usize]
    }
}

オリジナルと比較すると違いがわかると思います。RAM4Kを使わず自前実装している感じですね。

#[derive(Debug, Copy, Clone)]
pub struct RAM16K {
    rams: [RAM4K; 4]
}

impl RAM16K {
    pub fn new() -> Self {
        RAM16K {
            rams: [RAM4K::new(); 4]
        }
    }

    pub fn update(&mut self, clk: bit, input: word, load: bit, address: [bit; 14]) {
        let address_low = [
            address[0], address[1], address[2], address[3],
            address[4], address[5], address[6], address[7],
            address[8], address[9], address[10], address[11]
        ];
        let address_high = [address[12], address[13]];
        let (a, b, c, d) = dmux4way(load, address_high);
        self.rams[0].update(clk, input, a, address_low);
        self.rams[1].update(clk, input, b, address_low);
        self.rams[2].update(clk, input, c, address_low);
        self.rams[3].update(clk, input, d, address_low);
    }

    pub fn get(&self, clk: bit, address: [bit; 14]) -> word {
        let address_low = [
            address[0], address[1], address[2], address[3],
            address[4], address[5], address[6], address[7],
            address[8], address[9], address[10], address[11]
        ];
        let address_high = [address[12], address[13]];
        mux4way16(
            self.rams[0].get(clk, address_low),
            self.rams[1].get(clk, address_low),
            self.rams[2].get(clk, address_low),
            self.rams[3].get(clk, address_low),
            address_high
        )
    }
}

実装を終えて

動作速度は少し改善しましたがまだまだ遅いです。。*7

まとめ

気の迷いから始めたハードウェアシミュレータのRust実装も無事にブラウザで動かすところまでは辿り着けました。なんとかクリスマスイブに間に合って良かったです。心残りも色々あるので改善していきたいなと思います。

  • やぱり動作速度はなんとかしたい
  • Rustらしいコードを書きたい
    • せっかく動かすところまではできたのでRustぽい実装にリファクタしたいですね。
  • HDLより仕組みの理解が進んだ(気がする)
    • HDLで定義するよりもRustでnand関数から組み上げていく方がプログラマにはコンピュータの仕組みが理解しやすいように感じました。

*1:最近はデータエンジニアに集中していてものづくり系のコードはしばらく書けていないです...プライベートの時間にオモチャのコードを書いて精神を保っています

*2:第2版も出ましたね。早速買いました。パラパラ見た感じでは第1版より読みやすくなってそうです。この記事が書き終わったら読みたいです。

*3:今回の作成内容は完全に趣味です。仕事とは全く関係ないですがRustに入門してみた記録です。未来の自分への備忘録なのでダラダラ長くなっています。言語入門には動くものを作ってみるのが一番早い。

*4:テストしんどいしデバッグ大変だしハードウェア開発のつらさを体験できました

*5:今回は遅すぎて真っ黒になるのに時間がかかりすぎ、処理に時間がかかりすぎてキーの認識もほぼされないので真っ白にもならないと思いますが。。。

*6:このブログでは説明のためにSDL実装からにしてますが、本当のところはこの書籍を読んでWasmでnand2tetris動かせるのでは?と思ってこのブログのネタが始まりました。実装もWasm版から始めたのですが動かすところまで行けずデバッグの困難さから諦めてネイティブ実装のSDL版に切り替えたのちに動かせたのでWasmに戻ってきて再挑戦しました。

*7:本当はどの程度改善したか計測すべきと思いつつ目視で遅さがわかるレベルなのでやってません。本来は一瞬で画面全てが黒くなるはずなので・・・