これはドクターメイト Advent Calendar 2024の24日目です。
クリスマスイブですね、クリスマスは楽しく過ごしたいなと思う今日この頃です。さて、せっかくRustの会社にいるしRustキャッチアップしたいなぁと思って*1Rustでコンピュータシステムの理論と実装(以下、nand2tetris本)*2のハードウェアシミュレータを実装してみました。空気も読まずにプログラマらしくちゃんと動くものを実装してみた系のテーマにしたらだいぶ時間を費やすことになりました。自分向けの備忘録でもあるので以下ダラダラと長くなっています。*3
以前のブログ記事では書籍1〜5章のハードウェアパーツのロジックをHDLで実装し書籍が用意したハードウェアシミュレータ上で実行しました。今回はRustでハードウェアシミュレータもろともフルスクラッチで全て実装します。以前に書いた1〜5章のブログ記事は下記ですね。
今回のコード
下記、タグv0.0.6
になります。
下記で環境をcloneできます。
git clone -b v0.0.6 https://github.com/nihemak/nand2tetris.git cd nand2tetris
概要
今回は冒頭で書いた通りRustでNANDゲートの関数を定義するところから始めてHACKコンピュータのバイナリコードが動作するところを目指して実装していきました。この記事の最後にはブラウザで動くところまでは辿り着けます。ただ最初にネタバレすると実用的な動作速度には達していません。ご容赦くださいませ。
実装は次のステップで行いました。
- ステップ1: RustとSDLによる実装
- ハードウェア実装に集中するためまずはScreenとKeyboardをSDLで実装
- ステップ2: RustとWasmによる実装
- SDLをWasmに置き換えてブラウザで動くように実装
- ステップ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にあります。
下記のように0
と1
はbool型にしました。型名はbit
です。false
が0
、true
が1
ですね。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にあります。
あと少しです。Screen
やKeyboard
、ROM32K
などの実装は書籍にはなかったので考慮が必要になります。アウトプットが再度インプットになる回路などだいぶハマりましたがテストが通っているので多分大丈夫なはず。知らんけど。
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
Keyboard
もScreen
と同様に押下キーの取得の責務を呼び出し元でする設計になってます。そのため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
ROM32K
はRAM4K
の読み書きだけです。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です。
下記をやってます。
- ここまで作ったコンピュータにHACKのバイナリコードを読み込んで起動
- 無限ループ
- Escが押下されたら終了
- 押下されたキーコードを取得しコンピュータを1クロック(
true
,false
)進める - コンピュータの画面情報を取得し描画
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クロック進むComputer
のstep
関数を呼び出します。
let state = event_pump.keyboard_state(); computer.step(reset, get_keyboard_press_code(&state)); reset = false;
Computer
のstep
関数はこちらです。
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
関数でしてます。Screen
のget_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
動作速度が遅い
プログラムを起動すると画面がすぐに真っ黒になるはずなのですが全然なりません。。画面上部からちょっとずつ黒く塗りつぶしされていくのを眺めるしかないです。速度改善をしていかないと使い物にならなそうです。
参考にした情報源
今回は第1版です。
NANDゲートからのハードウェアシミュレータを実装する部分はハマったとき下記を参考にさせていただきました。
SDLの実装部分は下記を参考にさせていただきました。
ステップ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章の内容の通りです。
- 13/HardwareSimulatorWasm/src/lib.rs
- エントリポイント
- 13/HardwareSimulatorWasm/src/browser.rs
- ブラウザコード
- 13/HardwareSimulatorWasm/src/engine.rs
- 書籍のゲームエンジンにあたるコード
実装を終えて
無事に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"
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実装よりも遅いです。。ブラウザで動かすのでしょうがないと思いますが。
参考にした情報源
この書籍をがっつり参考にしました。
書籍のソースコードも参考にしました。
ステップ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:本当はどの程度改善したか計測すべきと思いつつ目視で遅さがわかるレベルなのでやってません。本来は一瞬で画面全てが黒くなるはずなので・・・