プログラミングや低レイヤで遊ぶ人

基本的には遊んだことを記事にしていく

ファミコンのわかりづらい仕様について

筆者がweb初心者のくせに調子のって記事書いているので、レイアウトの崩れがあるかもしれませんがお許しください。
スーパーマリオブラザーズを動かせるレベルのファミコンエミュレータもどきをつくったが、やばみがやばみだった。なぜgolangで初めてしまったのか。 終わった後なので笑い話で済むが個人的にグラフィカルなものは golangではおすすめしない。javascriptあたりがやりやすいかな?
作るにあたってファミコンの仕様を色々調べたわけだが、紛らわしいものが多かったのでいくつかをここに書いていく(単純に私が知識不足の人間というのもある)。
それだけだとスーパーマイナー記事になるので、最後に面白いスーパーマリオブラザースの小ネタを載せました。
ファミコンの仕様に興味がない方はそちらをどうぞ。

01Sub/Cmp命令について

sub命令とcmp命令は内部的な計算は同じで、減算を行いその結果をファミコンのステータスレジスタに書き込みます(キャリーだったり、オバーフローだったりの結果を保存するレジスタ)。 sub命令で変換されるフラッグは、キャリー、オバーフロー、ゼロ、ネガティブの四つです。cmp命令ではオーバーフローは更新されません。 sub命令は計算の結果を保存しますが、cmp命令は結果は虚無へ捨てます。まぁ、それだけの大したことないよくある命令です。
問題はキャリーとオバーフローのタイミング
(正直オーバーフローはあまりファミコンでは使われないから気にしなくていいらしいが)
ファミコンではsub,cmpでの減算をおこなう際、
  1. Aレジスタからメモリまたは即値を減算する
  2. キャリーフラッグを反転したものを減算する
といった手順で計算する。
これら手順が終了した際に四つのフラッグを変更するものだと思ってました。
勘違いしていた間違っている仕様
  1. Aレジスタからメモリまたは即値を減算する
  2. キャリーフラッグを反転したものを減算する
  3. 計算結果に応じて4つの各フラッグを書き換える
しかし、実際の仕様は以下でした。
実際の仕様
  1. Aレジスタからメモリまたは即値を減算する
  2. キャリー/オバーフローフラッグを更新する
  3. キャリーフラッグを反転したものを減算する
  4. 計算結果に応じてゼロ/ネガティブフラッグを更新する
まさかのタイミングが違う。これが違うとスーパーマリオブラザースでは正しいタイミングで敵が現れてくれない。
mario
メニュー画面に乱入してしまうクリボー
これに関してはsub命令、cmp命令が間違っていることさえわかってしまえば気づくことができる。ファミコンにはnestestromという便利なものがあり、それで簡単にわかる。 逆に言えばnestestromの存在を知らないとかなりきつい。

02$2007のreadについて

この問題はソフトウェアだから発生する問題な気がする。
ファミコンはメモリマップトioなので、PPUのメモリを読み取る時は$2007番地を利用するのだが、ここにトラップがひそんでいる。 読み込む際、一回目の読み込みはバッファの影響で読み込まれない。なので、$2006番地(PPUの読み込みする番地を指定するメモリ)を更新した後、 一回読み込みをスルーしなければならない。これを知らないとCPUは正しく動作しているのに、うまく動作しないという地獄が始まる。
これはアセンブラを読んでいると気づく問題な気がする。明らかに無駄に$2007を読み込んでいる箇所が見つかるのでそれで気づくことができる。機械語と対話せよ。

03Zero Page IndexX

トラップ。完全なトラップである。ファミコンのアドレス指定には Zero Page Index X なるものがある。これは、オペランドの値とXレジスタの値を足した結果をアドレスとして使うものだ。
で、これの問題が、
addr(8bit) = oprand(8bit) + Xregister(8bit)
という計算になることだ。
つまり、計算であふれた値は切られるということだ。ファミコンのアドレスは16bitなので、これに気づかないと意外とはまる。
でも、冷静に考えるとファミコンは加算減算などの計算を基本8bitでおこなっているということを考えるとすぐに気づける問題でもある。つまりは、みんなもNesDevよく読もう(私もあまり読んでいない)。

04Scroll指定について

完全なト(ry。ファミコンは$2005番地にxとyの値を二回書き込むことで画面をスクロールすることができる。このあたりの詳しい説明は他にゆずるが問題はそのスクロールの基準点である。
メモリの$2000番地を使うと描画の基準点を設定できるのだが、PPUメモリの$2000を基準と設定すると画像の青い点の部分が描画の基準となる。
mario
ファミコンの描画されていない部分を含むゲーム画面
そしてこの基準点に対し例えば、x+10,y+10 と指定すると以下のようになる(画像は正確にx+10,y+10わけではなく大体だと思っていください)。青枠の 部分が描画されていると思ってください。
mario
基準点からxy+10の部分の描画
これはまぁ、わかりやすいと思います。これが基準の位置が変わるとこうなります。
mario
別の基準点からxy+10の部分の描画
ファミコン関連の知識を初見で見ている人は、当り前じゃね?と思うだろうが、この情報どこにも載ってない。 いやまぁ、ファミコンエミュレータを作ってる人はたくさんいるので、知っている人もたくさんいるだろうがこの手の情報はなぜか少ない。 これを知らないとスクロールの基準点は常に左上だと勘違いする。ちなみに、私はこれのせいで二日溶けた。

05まとめ

とてもやりがいがあった。なによりファミコンだとグラフィカルでうまくいくと動きがでて面白かった。まぁただかなり大変ではあった。やろうと思っている人はそれなりに時間がかかると思ったほうがよい。

06小ネタ

ミラー表示

ファミコンはゲームROMの情報量削減のためにスプライトなどをミラーを使って少ない情報量で表現している。具体的には、マリオの体やクリボーだ。
mario
ミラーを実装していないため空中分解するマリオ
mario
ミラーを実装していないため美味しそうに割かれるクリボー
そのため、マリオやクリボーの体は左右対象でデザインされている。これのおかげで少しではあるがROMの容量を空けることができる。

草と雲

スーパーマリオブラザーズをよく見ると実は草と雲は全く同じ形で色が違うだけである。
mario
錬金された草と雲
これによって少ない量の情報量でより多くのものを表現することに成功している。これはどちらかというとデザインが優秀。