0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TypeScriptでCPUエミュレータ自作

Posted at

TypeScriptでCPUエミュレータ(Chip8)を実装しました。
GitHub Pagesで公開しており、誰でもアクセスして確認できるようにしています。
https://hir-247-30.github.io/chip8.ts/

自分が実装したCPUを使って、テトリスやインベーダーゲームを遊ぶことができます。

Chip8とは?

みんな大好きAI先生に聞いてみましょう。(丸投げ)

CHIP-8(チップエイト) は、1970年代に登場した 簡易な仮想マシン(インタープリタ) であり、主に8ビットマイクロコンピュータ向けにゲームを実行するために設計されました。

仮想マシン(バーチャルマシン)の一種
オリジナルのハードウェアは存在せず、エミュレーション向け
非常にシンプルな命令セット(オペコード)を持ち、ゲームの開発が容易
1970年代後半に、コスモス(COSMAC VIP)やTI-99/4Aなどのコンピュータで採用
現在では学習目的でエミュレーターを実装する題材として人気

なるほど、分かるような気もするし、分からないような気もします。

詳しく調べていくとCPUの入門にちょうどよい難易度で、仕様も公開されており、先駆者のソースコードもたくさん落ちていそうでした。

エミュレータを作成するにあたりスタックポインタやレジスタ、プログラムカウンタなども自分で実装する必要があります。
普段意識しない低レイヤの動きも理解ができるチャンスでもあります。

Chip8の動き

CPUと聞くと難解で理解不能なものと感じますが、仕組みは意外と単純です。

  1. ROM(ゲームのカセットのようなもの)を読みこむ
  2. プログラムカウンタを初期化する(固定)
  3. メモリにフォントセット(固定)を配置する
  4. メモリに読み込んだROMを配置する
  5. プログラムカウンタが示している番地からメモリを2バイト読み込む
  6. 読んだ2バイトをCPUの命令にデコードする
  7. 命令を実行する
  8. プログラムカウンタを進める
  9. 5〜8 を繰り返す

これがCPUの一連の流れです。合間にディスプレイを更新・描画したりもします。

プログラムカウンタが示す位置のメモリを読み込み、そこから命令にデコードして実行、プログラムカウンタを進めて同じことを繰り返す・・・と書けば意外と単純だということがわかります。

Chip8の構成要素

詳しい仕様は下記になります。
https://yukinarit.github.io/cowgod-chip8-tech-reference-ja/2-1_memory.html

まとめると以下になります。

項目 内容
メモリ 4KB(4096バイト)のアドレス空間
汎用レジスタ(V) 16個の 8ビット汎用レジスタ
インデックスレジスタ(I) メモリアドレスの格納に使用する16ビットのレジスタ
プログラムカウンタ(PC) 命令のアドレスを保持する
スタック サブルーチン用の16段スタック
スタックポインタ(SP) 8ビットでスタックの先頭のインデックスを表す
サウンドタイマー 8ビットのレジスタ
ディレイタイマー 8ビットのレジスタ
ディスプレイ 64x32ピクセルのモノクロディプレイ
キーボード 16進数のキーボード

TypeScriptで実装する場合、16ビットと8ビットの表現がデフォルトで存在しないため 自分で作成して表現する必要がありました。

命令のデコード

メモリから2バイトフェッチした時、Chip8の命令にデコードする必要があります。
具体的にはフィッチした時はリトルエンディアンになっているので、これをビッグエンディアンに変換する必要があります。

このあたりは私のリポジトリにコメント付きでコミットしてあるので それを見ていただけると嬉しいです。(急な丸投げ)
https://github.com/hir-247-30/chip8.ts/blob/992d778b5e04f59ff54739c409150ceb9076ea0c/src/cpu.ts#L194

命令実行

デコードした命令を実行します。
命令は下記の数だけ存在しており、マッチしたものを実行します。
https://yukinarit.github.io/cowgod-chip8-tech-reference-ja/3_chip8_instructions.html

例えばビッグエンディアンに変換したものが 7ABC だった場合、リンク先の 7xkk - ADD Vx, byte に該当します。
「VxにVx + kkをセットする。」とのことなので、汎用レジスタV[A]にV[A] + BCを代入すればよい、となります。

TypeScriptだとmatch文のようなものが存在しないので、switchで分岐する以外になく非常にもどかしい実装となってしまいました。
※ 2025年3月時点での話

ディスプレイ

64x32を配列で表現します。
具体的には配列の要素に0または1を入れるようにし、それぞれで色を変えて描画するようにします。

描画の方法は後述のCLIとWebとで変わります。

CLI実行

CLI上でディスプレイを描画するケースでは blessed を使用しました。
https://www.npmjs.com/package/blessed/v/0.1.4

keypressを簡単に実装できるのが優れものです。
最初はconsole.logで描画していましたが、チカチカして目に悪くて仕方なかったです。blessedならそういったことにもなりません。
64x32の配列を確認し、1の位置に を描画するように実装しました。

一方でblessedにはkeyupがありません。
これに関しては「300ms後にkeyupとする」という苦しい実装をしています。300msという数値に関しては完全に私の感覚値です。自分でプレイしてみて違和感が少なく感じたのがこの値でした。

また余談ですが、シングルプロセス・シングルスレッドのNode.jsがどうやってkeypressを受け付けているのかと思って調べたところ、 process.stdin を使って非同期で入力受付をしているみたいです。

Web実行

Webに関してはHTML内にcanvasで描画するようにしています。
64x32の配列を確認し、0と1とで色を変えてfillRect()すればOKです。

TypeScriptだとCLIでもHTMLでも簡単に動作させることができていいですね。
HTMLで動作させたものに関しては、GitHub Pagesで公開するようにすればポートフォリオにもなります。

終わりに

総評して「難しそうだがやってみると案外できた」という感想です。
自分の知らない領域の知識を得られたので有意義でした。普段の業務では得ることは難しいでしょう。

リポジトリを以下に公開しています。よければどうぞ。
https://github.com/hir-247-30/chip8.ts/tree/main

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?