ごちゃペディア

はなまるデジタル創作紀行(DTM、TAS、いろいろな技術)

なぜTASはdesyncする?原因追求とデバッグの仕方

エミュレータのバグによるdesyncの傾向と対策。直接直したい人や、開発者に効果的なデバッグ依頼をしたいときに。

なお、TAS向けエミュレータで満たすべき要件は TASVideos / Laws Of TAS にまとめられています。

原因:矛盾したステートのセーブ・ロード

矛盾したステートのセーブ・ロードをするとdesyncします。例えば、AAAAと入力されたムービーに対して、AABBと入力されたステートをロードして、続きを作ってしまった場合などです。

ここではそういう、ある種ヒューマンエラー的なdesyncはさておいて、矛盾なく扱ったはずなのにdesyncする場合に的を絞ります。

根気でdesyncを乗り切るのは体力の無駄です。公式の Snes9x 1.51 でムービーを作成していた時期がありましたが、はっきり言って無駄な戦いに時間を使いました。まずエミュレータを直すべきです。

原因:ステートに保存すべき変数が保存されていない

ステートに保存すべき変数が保存されていなくてdesyncする場合です。ステートというのはハードウェアの状態(メモリの値、動作をエミュレートするためのカウンタ変数など)をすべてファイルに格納したものです。格納内容が足りていないとロード時に状態が元に戻りませんが、中には「何か足りていなくても、プレイする分には全然支障がなくて気づかない」ことがあります。TASは動きが細かいので、そういう細かいものがdesyncとして現れてきたりします。

desyncが起きる例(及び内部の動き)
  1. ムービー再生中の適当なタイミングで、ステート1を作成(このときエミュレータ内部の count という変数が count = 1 であるとする)
  2. 適当に1000フレーム動作させる(count = 2 になったとする)
  3. ステート1をロードする(count = 1 に戻って欲しいが、ステートに count が含まれていないと count = 2 になってしまう)
  4. そのまま動かしていくと、desyncする
どう報告する?(デバッグ材料の用意)

材料を用意しましょう。まず、適当なムービーを用意しておきます。TASのように「少し動きが変わると破綻しやすい」ものほど良いです。

  1. エミュレータを起動して(起動から始めること!)ムービーを再生します。ムービー再生時に開くROMを指定できればベストです。0フレーム目でポーズするなど、事前に極力余計なエミュレーションを行わないようにしてください。
  2. 再生中にいくつかステートを作成する(多いほど良い、動きが緻密でずれが禁物な箇所ほど良い、再生中のロードは厳禁)
  3. ステート作成後、適当な場所で適当なステートをロード→再生させて上記の最初の再生結果とずれがないか確認 を繰り返す。
  4. ずれを確認できたら、フレームカウンタを見つつ「ムービーをNフレームまで再生後、ステートAをロードするとずれる」というようにどのタイミングでロードすると起きるかを厳密に調べる。
  5. 開発者にムービーとステートAを渡して、「ただ再生した場合と、Mフレーム時点(ステートA作成時点)でステートセーブしておいて、その後Nフレーム目まで再生してステートをロードしたときで再生結果が違う」と伝える。「再生結果が違う」では曖昧なので「Xフレームあたりでキャラクターの攻撃が当たるはずが外れる」など、ゲームの内容に疎くてもわかるような表現で具体的に伝えると良い。

だいたいこのくらいできれば材料になると思います。再現「手順」を明確にすることが大切です。

いつのバージョンから起きる?

過去のバージョンでは問題がなかったのに、今のバージョンでは問題が起きているという場合、まずいつのバージョンから起きているかを明確にするとよいです。開発者の方は、SVNやGitなどのどのコミットが原因なのか、古いバージョンを受信・コンパイルしつつ二分探索的に探していきましょう。開発者でない方は開発途中の細かいバージョンの特定がしづらいかもしれませんが、SkyDrive - Orleans / Emulation などから過去の開発時ビルドを探してテストすると、範囲を絞り込むことができます。

どうデバッグする?

まずは寄せられた材料で状況が再現できることを確認しましょう。

大抵のエミュレータでは、主要な構造体がいくつかあって(CPU、PPU、SPU 等)その中にエミュレーションに関係する変数があります。時々そうではなく、何気なく使われているグローバル変数が重要なこともあります。手順としては、

  1. 主要な構造体、怪しげなグローバル変数をウォッチに追加する
  2. ステートセーブ後の位置にブレークポイント設置、ムービーを頭から再生して、Mフレーム時点でポーズしてステートセーブ、ウォッチ内容をコピペするなどして控える
  3. ステートロード後の位置にブレークポイント設置、Nフレームまで再生してステートロード、ウォッチ内容をコピペするなどして控える
  4. 2つのウォッチ内容を比較して差異のある箇所に着目する

こういう感じでしょうか。簡単にわかることもわからないこともあります。怪しい変数があればステートに変数を追加してみて、同じ手順を行なってみましょう。ずれがなくなれば修正成功です。

メモリの値で追えないならトレースログをとって比較するのも手段としてありそうです。HDD容量と実行速度がネックですが……。

原因:ステートロード時の誤った変数初期化

上記の亜種です。ステートに保存しないといけないのに、0に初期化している場合などです。

desyncが起きる例(及び内部の動き)
  1. ムービー再生中の適当なタイミングで、ステート1を作成(このとき count = 1 であるとする)
  2. セーブ直後に何もせずにステート1をロードする(ロード時の不当な初期化で count = 0 になる)
  3. そのまま動かしていくと、desyncする

同じ場所でセーブ・ロードしてもずれるのが特徴です。

どう報告する?(デバッグ材料の用意)

「ステートに保存すべき変数が保存されていない」ケースと同じで構いません。ただ、「その場でセーブロード」しても起きるのがこのケースの特徴なので、「Nフレームまで再生後、その場でセーブ・ロードして再生継続するとずれる」という形で報告可能であれば、そのほうが良いです。

どうデバッグする?

「ステートに保存すべき変数が保存されていない」ケースと同様です。

原因:リセット(ROM読込)時に初期化されない変数がある

これが結構厄介です。

desyncが起きる例(及び内部の動き)

適当なムービーを用意しておく。

  1. エミュレータを起動して(起動するところから始めること!)、「ステートに保存すべき変数が保存されていない」ケースと同様にムービー再生を開始します。(例:count = 0 で再生開始)
  2. エミュレータを起動して(起動するところから始めること!)、ROMを開いて、フレームカウンタがXフレームになったらポーズしてムービーを再生します(例:ROM起動後ちょっと動作させたことで count = 50 になっている、count = 0 にして再生開始して欲しいが、count = 50 を初期値としてムービー再生が始まる)
  3. 1回目と2回目で再生結果が異なる

特徴は、一切のステート操作を介さずにdesyncする点です。

どう報告する?(デバッグ材料の用意)

「例」のような手順が再現できたら、ムービーを渡してその手順をそのまま伝えてください。

どうデバッグする?

ムービー再生開始時のROMロード直後にブレークポイントを仕掛けて、1回目と2回目の各種変数の値を比較します。

コードを目視しながら「この変数ってリセット時に初期化しなくていいの?」と気になる点を探してもいいと思います。

「Reset」では初期化されないけれど、「LoadROM」では初期化されるような事例も Snes9x で過去にありました。せっかくなのでリセットとROM読み込みのコードを両方見ながら、変な箇所を潰すとよいです。

その他

  • 直面したことはないですが、マルチスレッド環境特有の desync 事例もあったとかなかったとか
  • 開発者向けの再現手順を作ろうとしてもうまく作れないこともあると思います。そういうのはなかなか直せなくて厄介です。「ムービー」と「変なステート」だけ送っても材料にはなると思うので、一応送ってみてください。Luaスクリプトを利用して操作を自動化していた場合、それが思わぬ影響を及ぼしている可能性もあるので、スクリプトを使っていた旨を伝えたり、そのスクリプトを渡したりするとよいと思います。期待は禁物。
  • 過去のバージョンでは起きないが、いつのバージョンから起きるという情報も有用?
  • みんなでがんばって desync をたおしましょう

ポイント

  • とにかくムービーファイルを送ってあげてください! 開発者はdesync確認に適したムービーファイルなんて持っていません。作成には手間がかかりますし、作成しても再現できない可能性も高まります。その他、ステートファイルとか関係するものは一式送ってあげてください。あればなんとかなるとも言えませんが、ないよりは喜ばれます。
  • できるだけ確実でシンプルな再現手順を教えてあげてください! 異なる「再生結果1」と「再生結果2」を再現する手順です。ステート作成は開発者側で行わせるような手順で。
  • 「ずれちゃう」ことを伝えるときは、「何フレーム目から○○の動きが変わる」などを、なるべくゲームを知らなくても理解できる表現で伝えるとよいです。

近鉄特急のネット予約とか買い方について調べたよ

突然ですが、よくわからなかったので駅員さんに聞いたりしていろいろ調べました。

間違いや変更がある可能性があるので、確実な情報は信頼できる情報源を当たってください。

基礎知識

  • 購入形態によらず乗車券特急券は別である。ネット予約・購入できるのは特急券であり、乗車券は別に購入する必要がある(当日券売機で普通に買う、事前に金券ショップで株主優待切符を買うなど)。
    • アーバンライナーなど上級の車両を利用する場合には加えて特別車両料金がかかるが、詳細は割愛(よく知らない)。
  • 特急券は当日に窓口や駅ホームで購入可能。ただし乗車直前だと満席の場合に苦労するため、事前購入を行っておけばその点で安心である。事前購入の手段としてインターネット予約・購入がある。本記事ではこれについて述べる。
  • 特急車両は指定席制であり、新幹線の指定席車両のように予約時に座席が決定されるシステム。
  • インターネット予約・購入の形態は3つある。ここでは主に「チケットレス」「購入」「予約」と称する。
  • 窓口で購入する場合も、インターネットを通じて購入する場合も価格は同一だが、後者はポイントがつくため頻繁に特急券利用がある場合はお得。

私的結論

  • インターネット予約は「チケットレス」こそ真価を発揮する、というか他が何かしら不便!
  • よくわからなければ窓口で相談して購入するのが一番良い(そうせずやってみたい人のための記事ではありますが)
  • インターネット購入するとポイントがついて、そのポイントで特急券が購入できる。しかし、計算上は年間10回(5回往復)の特急券利用をするのでなければどうでもよい*1

3種の購入形態

いずれの場合も近鉄 インターネット予約・発売サービスから会員登録した上で操作します。

以下、近鉄でいただいたパンフレットに記載されている内容です。

購入(チケットレス) 購入(駅でお受け取り) 予約(駅でお支払い・お受け取り)
クレジットカード・積立金カードで購入 ×
現金・特急カード・企画券などでお支払い × ×
受取り期限 受取り不要 特急列車発車直前まで 予約日を含め8日以内かつご乗車前日まで
netポイント加算 ご購入金額の10% ご購入金額の5% ×

おわかりいただけたでしょうか。個人的に重大だったポイントを下記に挙げます。

なお、わたしはチケットレス以外での購入はしたことがありません。他の購入方法に関しては画面だけ参照しました。

チケットレス
  • クレジットカードあるいは積立金カードで購入する。
  • 予約時に「シートマップ」画面で車両の予約状況を閲覧の上、好きな「座席」を予約できる。
  • 窓口等での発券は不要。代わりに購入した列車・座席情報を携帯電話の画面に表示するか、もしくはパソコンから印刷した内容を提示する。また決済に利用したカードを所持すること
    • なお「大人3名」のように購入すると「大人3名」のチケット風画像1枚が画面表示される。画像3つではない。
  • 3回まで列車変更できる(他のプランでどうであるかは不明)
  • 購入金額の10%のポイントが加算される。
購入(駅でお受け取り)
  • クレジットカードあるいは積立金カードで購入する。
  • 座席指定が不便。「禁煙・喫煙」「車両」「(1人の場合)窓側・通路側」を選べるが、車両の予約状況は閲覧できないため、複数人の際はどのような座席配置になるか事前に見られずとても不安。
  • 購入手続後、定期券・特急券自動発売機、特急券自動発売機、特急券発売窓口のいずれかで特急券を受け取る。
    • 受取期限:特急列車発車時刻直前まで(混雑する場合もあるので、多少時間の余裕は見ておくこと)
    • 受取には予約番号と乗車日および、決済に利用したカードが必要。
  • 購入金額の10%のポイントが加算される。
予約(駅でお支払い・お受け取り)
  • 支払いは駅で特急券を受け取る際に、現金・特急カード・企画券などで行う。
  • 座席指定が不便。「禁煙・喫煙」「車両」「(1人の場合)窓側・通路側」を選べるが、車両の予約状況は閲覧できないため、複数人の際はどのような座席配置になるか事前に見られずとても不安。
  • 購入手続後、定期券・特急券自動発売機、特急券自動発売機、特急券発売窓口のいずれかで特急券を受け取る。
    • 受取期限:予約日を含め8日以内かつ乗車前日まで。すなわち乗車当日の受取は不可、事前に駅で発券しなければならない。
    • 受取には会員番号と予約番号と乗車日が必要。
  • ポイント加算なし。
付記
  • カード決済した場合、そのカードを持参するように書かれています。ただ決済自体は済んでいるので、もしカードを持参していなくてもさほど問題にはならないのではないでしょうか?(検証したわけではない)
    • 「ごめんなさい。パパが予約してくれたから、カードは持ってないんです……ううっ(うる目)」とあざとイエローっぽく言えば、他の人に購入してもらった場合でも乗り切れると信じています(責任は負いかねます) とくにチケットレス購入の場合、印刷物の提示を行いますしそれで十分かなと思うのですが、どうなのでしょう?
  • 特急券を駅で当日購入することに関連して特急車両の混雑度を聞いたところ、ゴールデンウィークはそれなりに混雑するので、当日・直前だとどうなるかはわからないそうです。普通の土日はイベントの有無などに応じて混雑具合は異なるため、タイミングによっては同様に困難なこともあるそうです。
  • 特急券と関係ありませんが、近鉄株主優待切符はゴールデンウィーク中でも利用可能だそうです。
  • 特急券を利用しないのは、明快かつ経済的なソリューションのひとつです。 ;)

*1:1ポイントが1円換算。ポイントの有効期限は授受より1年間で失効分は累積から減算される。チケットレス購入の付与ポイントは購入金額の10%。それらを総合するとこのような結論となる。

VisualBoyAdvance でキー入力をAVIに出力するには?

Web拍手経由でご質問いただきました。

VisualboyAdvanceでLuaでの入力表示に関して色々とぐぐってたら、vba-rerecording rr22?とかいうのでjoypad.getdown(int port)という文章を見つけたので、RamWriter.lua(空白区切り版)という奴に file:write(string.format("%d ", joypad.get())) というのを書いて見たんですが、普通にエラーで出てしまいました。これってどういう風に Joypad.get を書けば入力キーを出力できるようになるんでしょうか?

AVIファイルにキー入力を表示させたいという観点でいくつかお答えします。

Lua を使わずにキー入力を AVI に書き出すアプローチ

ただ AVI に Input Display の表示を書き出すだけなら、Lua を使わなくてもできます。

  1. 「,」キーを押して Input Display を表示する。
  2. AVI にも表示されるように、HUD の表示を In Game に変更する(下記画像参照)。
  3. AVI の録画を行う。

利点は実現が楽なことです。欠点は表示形式をカスタマイズできないことです。

Lua を使ってキー入力を得るには?

まず最初に、以下の記事からスクリプトを再度ダウンロードしてください。数あるエミュレータの中でもVBAはAVI書き出し時にフレームが複製されて記録される問題があり、本日それに対する対応コードを加筆しました。

file:write(string.format("%d ", joypad.get())) というのを書いて見たんですが普通にエラーで出てしまいました。

joypad.get() の戻り値は table 型です*1が、%d で number 型の出力を行おうとしているからエラーが出ます。

賢い方法はともかく、メモリの値を出力する場面で以下のようなコードを書けば、Aボタンが押されているかいないかを記録できます。

-- RamWriter.lua:
local pad = joypad.get(1)
framedata = framedata .. string.format("%d ", (pad.A and 1 or 0))

対応する読み込み側の一例は次のような感じです。

-- RamReader.lua:
i = file:read("*n"); frame[f].A = ((i ~= 0) and true or false)

*1:print( joypad.get() ) を行ってみるとより詳しく中身がわかります。

プロセスメモリエディタ「MHS」を利用したメモリ監視・操作

MHSはいわゆるプロセスメモリエディタと呼ばれるもので、ゲームの見えないパラメータを表示させたり、値を書き換えたりすることができます。類似のツールはほかにもありますが、TAS界隈ではよくこのMHSが利用されます*1

日本では同様のツールとして、うさみみハリケーンなどが知られています。噂に聞くところでは検索機能はこちらの方が高度とも聞きますし、可能であれば他のプロセスメモリエディタ、デバッガと組み合わせて利用すると、できることの幅は広がるでしょう。個人的にはMHSの基本のシンプルさがとても好きです。

それでは、導入から簡単な利用までを見てみましょう。

ダウンロードとインストール

MHS Download Page から MHS6.1.rar をダウンロードして展開するだけです。


起動〜対象プロセス選択

メモリ監視対象のプログラム(ゲーム)を起動した状態で MHS.exe を起動します。初回起動時には下記のダイアログが表示されます。

Would you like to personalize this software?
Doing this can allow you to avoid detection by anti-cheat software.


If you do not want to do this now, you can always do it later from the Tools/Modify Self menu. (はい/いいえ)

不正防止つきソフトウェアへの対策として、MHSを改変しますか?」というような内容です。TAS目的の方にはまず不要ですし、Tools/Modify Self メニューから後で改変を実施することもできますので、ここでは「いいえ」を選択して次へ進みます。

メインウィンドウが表示されたら、メニューから File → Open Process を開きます。

現在起動中のプロセスを一覧表示したダイアログが開くので、タイトルや実行ファイル名を参考に、メモリ監視対象のプロセスを選択して、OKを押してください。

これで、MHS でメモリにアクセスする準備ができました。

メモリアドレスを検索する

メモリアドレスを検索するには、Found Addresses ウィンドウにある、虫眼鏡(単一)アイコンのボタンをクリックします(あるいはメニューから Search → Data-Type Search を選択)。

すると、検索のための Data-Type Search ダイアログが表示されます。

まずは、Search グループで基本的な検索条件を指定します。選択肢の意味は下記の表のとおりです。

Data Type バイト数 値の型・範囲
Byte 1 0〜255 の整数
Char 1 -128〜-127 の整数
Short 2 -32768〜-32767 の整数
Unsigned Short 2 0〜65535 の整数
Long 4 -2147483648〜-2147483647 の整数
Unsigned Long 4 0〜4294967295 の整数
64-bit Integer 8 -9223372036854775808〜9223372036854775807 の整数
Unsigned 64-bit Integer 8 0〜18446744073709551615 の整数
Float 4 実数 (単精度浮動小数点数)
Double 8 実数 (倍精度浮動小数点数)
Evalutation Type 説明
Exact Value Value to Find に一致する値を検索する。
Not Equal To Value to Find に一致しない値を検索する。
Range From より大きく、To より小さい値を検索する。
Greater Than Find Values Greater Than よりも大きい値を検索する。
Lower Than Find Values Lower Than よりも小さい値を検索する。
Unknown (不明。指定した型に合う全候補を表示?)

その他の主なポイントは以下のとおりです。

  • デフォルトではマップされたメモリが検索対象に含まれない(エミュレータのメモリアドレスが見つけられない可能性がある)ので、Options の General Search Options から設定画面を開いて、General Search タブで MEM_MAPPED を検索対象とするよう、チェックを付けるとよいかもしれません(ただし検索速度は遅くなります)。
  • Search Range は検索対象とするメモリアドレスの範囲です。おおよそ変更する必要はありません。

OKボタンを押して検索を行うと、Found Addresses の一覧に結果が表示されます。

多くの場合、ここから絞り込み検索を必要とします。Found Addresses ウィンドウにある、虫眼鏡(複数)アイコンのボタンをクリックします(あるいはメニューから Search → Sub Search を選択)。

すると Sub Search ダイアログが表示されます。使い方は先ほどのダイアログと同じなので割愛します。

探しているメモリアドレスが見つかったら、項目を右クリックして Add Selected を選択します。

メインウィンドウで特定のメモリアドレスの値を見ることができるようになりました。


おまけ:ポインタの検索

特定のアドレス領域を指しているポインタを検索する機能があります。メニューから Search → Pointer Search をクリックすると、下図のダイアログが表示されます。

動的に変化するアドレスを追いたいときには、ポインタの検索が必要になります。これを知らなくても MHS は使えますが、使い方が想像できる人は知っているとお得です。

ウォッチの編集、改造コード機能の利用

メインウィンドウにあるメモリアドレス一覧の項目をダブルクリックすると、Modify Address ダイアログが開きます。


  • Description を編集して、説明文を変更できます。
  • Type を変更すると、値のサイズ・型が変更できます。
  • Show as Hex にチェックを入れると、値を16進数で表示できます。

また、Locked のチェックボックスにチェックを付けると、改造コード機能が利用できます。Lock Type の意味は下記のとおりです。

Evalutation Type 説明
Exact 指定値を強制する。
Range 指定した下限値〜上限値の範囲を強制する。
No Lower Than 指定値より小さくならないよう強制する。
No Greater Than 指定値より大きくならないよう強制する。

OKを押すと変更が反映されます。

おまけ:アドレス計算に式を利用する

動的に確保されるメモリ領域はメモリアドレスが不定になるため、ここまでで紹介した方法では、起動後に毎回再検索しなければなりません。MHS にはポインタの参照先を辿って動的にアドレスを指定する方法が用意されています。

Modify Address ダイアログの Normal Address タブを開きます。

中断の Complex Address (Overrides Simple) というチェックボックスにチェックを付けると、下部の入力欄にて、メモリアドレスの表現に式が利用できます。

計算式の仕様は下記のとおりです。

  • 式ではC言語の基本的な演算子が利用可能。
  • 間接参照は [40B0F0] のように角括弧で行う(括弧は入れ子にしても問題ない)。
記述例: [40B0F0] + 0x1536 + ([40B0F4] & 0xff)
おまけ:値を計算式で加工して表示する

Expression Evaluator ウィンドウに式を入力して追加すると、値を計算式で加工して表示することができます。

計算式の仕様は、動的アドレス計算に使用するものと同じです(上記参照)。

メモリウォッチ内容の保存

MHS を終了する前に、メモリウォッチの内容を保存しておきましょう。

ファイルの保存は、メニューから File → Save Selected As を選択して行います。

保存したファイルは次回、File → Open Save File から開くことができます。

まとめ

とりとめのない紹介になりましたが、下記をもってまとめとします。

  • MHS を使えば簡単・強力にメモリの検索・監視・書き換えが行える。
  • 解析技術があれば、動的に変化するアドレスにも柔軟に対応できる。よくわからなければ、メモリアドレスによっては、起動ごとに毎回検索する必要があることに注意すべし。
  • MHS には Script など、まだ紹介されていない興味深い機能が多数ある。

Windows 上で動くゲームの TAS 制作を考えている方の参考になればうれしいです!

*1:多くのエミュレータは RAM Watch 機能を内蔵しているのでプロセスメモリエディタを利用する必要はないのですが、Mupen64 のようにそういった機能を持たないエミュレータに対してはプロセスメモリエディタが利用されます。また、最近では Windows でも Hourglass を利用した TAS 記録が行えるようになったため、Windows 向けゲームに対して TAS 目的で利用する事例も増えると考えられます。