APUメモ
APUが1割くらい理解できた(NesDevWikiを読んだだけですが)ので、既存のエミュレータの実装も合わせて少し書いてみます。
まずAPUはフレームカウンタを内蔵しており、フレームカウンタはCPUサイクルに連動するシーケンサを持ちます。このシーケンサの動作サイクルをAPUサイクルと呼びます。
フレームカウンタ内のシーケンサには1つ飛びでCPUクロックが送られます。よって1APUサイクル=2CPUサイクルです。ちなみに1CPUサイクル=3PPUサイクルなので、まとめると1APUサイクル=2CPUサイクル=6PPUサイクルとなります。ただ三角波のタイマは1CPUサイクル単位で動作するようなので、APUサイクルを別単位として考えることにさほど意味はないかもしれません。
いずれにしろNES実機ではCPU, PPU, APUが上記の関係を保ちつつ並列に動作しています。そして、例えばフレームカウンタの動作表を見ればわかるように、実機の動作を完璧にエミュレートするにはサイクル単位での処理が必要です。
しかし大抵のエミュレータはサイクルベースでの処理は行っていません(コーディングが難しい上に遅くなるからだと思われます)。私は主にFCEUXとbjneを参考にしていますが、これらはどちらも「PPUを1ラインずつ処理し、CPU, APUは可能な範囲でこれに同期させる」という感じになっています。私が書いているのもこれらの真似なので同様です。
PPUとCPUの同期についてはサイクル数を計算してその分CPUを動かすだけ(それで完璧とは言ってない)なので割と簡単ですが、APUをPPUに同期させるのは結構面倒な感じです。まずAPUフレームカウンタのフレームの定義からしてPPUの1フレームとはわずかにズレています。このため、APUの処理はエミュレータによって個性が出るところみたいです。
FCEUXはCPUで1命令実行するごとにAPUを処理しているようです(X6502_Run() から FCEU_SoundCPUHook() を呼び出している部分)。これは比較的実機に近い動作かもしれません。bjneはAPU関連レジスタへの書き込みをサイクル数も含めてキューに記録し、後からそれを処理して辻褄を合わせる形にしているようです。
とりあえずの目標としてはFCEUXとムービーレベルで互換性を持たせたいので、FCEUXを真似して書いてみようかと思います。
ついでに言うとサウンド再生自体が結構面倒なんですよね…。今のところLinux上で開発していますが、音声のレイテンシを気にし始めるときりがなさそうな感じです。Linux版のFCEUXからして明らかに音が遅れてますし…(仮想環境だからかもしれませんが)。まあムービーレベルで再現性があれば出力の品質はあまり気にしない方向で行こうかと思っています。
映像/音声出力は今のところSDL2を使っているのですが、最新版の2.0.4でSDL_QueueAudioなるAPIが追加されたようです。SDLの音声出力は基本的にコールバック方式で、メインスレッドで音を溜めてコールバックで出力する、というのが基本みたいですが、このAPIを使うと音声データを突っ込んでやれば後は再生処理を丸投げできるっぽいです。まだ試してませんが、これを使っておけばスレッド間の同期問題はひとまずスルーできるかも?
bitfield templateでハマった件
C++ではテンプレートを使ってビットフィールドを実現するというテクニックがあるようです(検索すると色々出てきます)。しかしこれは注意して実装しないとハマるであろう落とし穴があった(私もハマりました)のでメモしておきます(gcc 4.9.2, clang 3.5.0で検証しました)。
私もC++は詳しくないので最初から順を追って書いていきます。まず、C/C++には言語機能としてのビットフィールドがありますが:
union Regs{ uint8_t raw; struct{ uint8_t a : 2; uint8_t b : 3; uint8_t c : 3; } r; };
これを使った場合、実際のビットのレイアウトは処理系定義となるようです。よってビットレイアウトが問題になる場合(その方が多いと思う)、この方法は事実上使えません。
自力でビット演算すればいいじゃないか、というのは全くその通りで、例えば16bit値のbit2-4とbit8-12をクリアしたい、という場合は
value &= 0xE0E3;
のようにすればいいわけですが、毎回こんなことをするのは頭のいい人でないと辛いかなと思います。私は頭が悪いのでこんなことしてたらハゲちゃいます^^;
そこで「ビットフィールドを自前で実装できないか」ということになります。これはC++ではテンプレートを使うとわりと短く書けて、実装も一見簡単です:
// 実は問題がある!! template<typename T, unsigned int Offset, unsigned int Bits=1> class BitField{ private: static constexpr T MASK = ((T(1)<<Bits)-1) << Offset; T value_; public: operator T() const { return (value_ & MASK) >> Offset; } BitField& operator=(T rhs) { value_ = (value_ & ~MASK) | ((rhs & (MASK>>Offset)) << Offset); return *this; } };
例えばこんな風に使えます:
template<unsigned int Offset, unsigned int Bits=1> using BitField16 = BitField<std::uint16_t, Offset, Bits>; int main() { union Addr{ uint16_t raw; BitField16<0,5> x; BitField16<5,5> y; }; Addr v; v.raw = 0; // 全bitを0に v.x = 0b11111; // bit0-4へ代入 v.y = 0b10101; // bit5-9へ代入 // ... }
では何が問題かというと、このコードはビットフィールド同士で代入を行った場合に期待した動作になりません:
Addr v, t;
v.x = t.x; // 全bitが上書きされる!!
当然ここでは v の下位5bitを t の下位5bitで上書きすることを期待していますが、実際には全bitの上書きが行われます。これは BitField クラスがデフォルトの代入演算子を生成しており、そちらが使われてしまったためです。
検索すると BitField クラスの代入演算子を以下のようにメンバテンプレートにしている例もあります:
// このテンプレートがあってもデフォルトの代入演算子が使われる template<typename T2> BitField& operator=(T2 rhs) { value_ = (value_ & ~MASK) | ((rhs & (MASK>>Offset)) << Offset); return *this; }
しかし、これでも BitField 同士の代入ではクラスが生成したデフォルトの代入演算子が使われてしまうようで、やはり全bitが上書きされてしまいます。ネット上で見かけるコードは大体この罠にハマってしまっているようです。
ビットフィールド同士の代入も正しく処理するには、自分で代入演算子を定義すればいいようです(もっといい方法があるかもしれませんが):
BitField& operator=(const BitField& rhs) { return *this = T(rhs); }
ただし、これを定義すると今度は union 同士での代入がコンパイルエラーになります。エラーメッセージを読む限り、union のメンバが non-trivial なコピー代入演算子を持っているとき、その union の代入演算子は生成されないということのようです。よって、union 側にも自分で代入演算子を定義してやる必要があります:
union Addr{ uint16_t raw; Addr& operator=(const Addr& rhs) { raw = rhs.raw; return *this; } // ... };
実はNESエミュレータを自作(他の人の真似してるだけ)していて、このときbitfield templateが非常に便利だったのですが、この罠にハマったためにレジスタ間のコピーで値がおかしくなってBGが描画されず、丸一日悩んでました^^;
とりあえずSMB1の画面が出るところまではできましたが、APUについては知識が皆無なため音を出すまでの道のりが長そう…。
FC『チャレンジャー』の乱数について
$EA-$EC が乱数、$87D9 が乱数生成器のようです。
$87D9 は乱数更新後に1Pおよび2P入力($10, $11)を加えた値を結果として返すので、適切に2P入力を行うことでかなり自由に乱数調整ができると思われます。ただし乱数が1Fで複数消費される場合は完璧とまではいかないかも。
とりあえずSCENE2では2P入力によってアイテム調整が可能なことを確認しています。また、SCENE1でも2P入力によってジャンプの着地フレーム(開幕含む)が変化するようです。
GB『ルクル』testrun
なぜかYouTubeの一覧に出なかったので一応こっちに貼り。超適当プレイです。
地味に良ゲーだけどTASだとどうにも段差の待ち時間がダレるんですよね…。あと最適化を真面目に考えると氏ねそう\(^o^)/
たまにスクロールが追いつかなくなって画面ズレが発生する(氷床がある面で顕著)ので、以前作成したマップを利用して自前で描画を行うスクリプトを書いて対処しました。
- movie: http://tasvideos.org/userfiles/info/32739513993484008
- HUDスクリプト: http://tasvideos.org/userfiles/info/32739579729619750
- 画面ズレ対処用スクリプト: http://www.mediafire.com/download/w6907pazcop0rw6/Lucle-draw.zip
このゲームはクリア時の残りTIMEによってEDが変化します。どこかでアイテムを取って稼がないとTASでもベストEDにはならないかもしれません。
タイトル画面で U, D, L, R, A, B, B, A, B, A, B, A, A と入力してスタートするとオプションメニューが出現します。ここから各種EDを見ることもできます(twitter情報)。
このゲームは「まわるんだぁ」なるタイトーの没ゲーが元になっているとの情報をtwitterで見かけましたが、他のソースが見当たらないため詳細はよくわかりません。