タイムバグパート2:EAブラックボックスの興味深いソリューション

こんにちは、Habr! 前回の記事で、古いおもちゃの興味深いバグについて話し、丸め誤差の蓄積現象を明確に示し、リバースエンジニアリングの経験を簡単に共有しました。 これで終わりになればと思っていましたが、非常に間違っていました。 そのため、カットの下で、Timebugという名前の獣、約60フレーム/秒、およびゲーム開発時の非常に興味深いソリューションについての話を続けます。







背景



誤って計算されたトラック時間に関するこの叙事詩の前の部分の執筆中に、私は意図せずにできるだけ多くのゲームに影響を与えようとしました。 余計な仕事をするのは面倒だったので、当時持っていたNFSシリーズのすべてのゲームで症状を探しました。 Underground 2もディストリビューションに含まれていますが、最初の症状は見つかりませんでした。







画像からわかるように(クリック可能です)、IGTは別の方法で計算されます。これは明らかにintに関連付けられているため、エラーの蓄積はないはずです。 私たちのコミュニティで喜んでこれを発表し、それを忘れることを計画しましたが、いいえ:Ewilは手動でいくつかのビデオを数え上げ、時間差を再び見つけました。 私たちはこれまでこれに対処する時間がないと判断し、その時点で既知の問題に集中しましたが、今では自分に時間を割り当ててこの特定のゲームを開始することができました。



症状



グローバルタイマーの動作を調べたところ、レースが再開されるたびに「リセット」され、メニューが終了するたびに、通常は都合のよいときに「リセット」されることがわかりました。 既知の問題との接続が完全に失われたため、これは私の計画の一部ではありませんでした。 絶望から、私は10ラップを書き留め、手で時間を数えました。 驚いたことに、ラップタイムは100%正確でした。



興味深い事実
実際、別のタイマーが発見され、フロート内の時間もカウントされましたが、少し役に立たないことが判明しました。 変更しても、目に見える結果は得られませんでした。

そして、何らかの理由でこのintタイマーは0にリセットされず、4000に設定されます。





残された唯一のことは、分解してそこで何が間違っていたかを確認することでした。 詳細に進むことなく、この不幸なIGTを考慮した手順の擬似コードを示します。



if ( g_fFrameLength != 0.0 ) { float v0 = g_fFrameDiff + g_fFrameLength; int v1 = FltToDword(v0); g_dwUnknown0 += v1; g_dwUnknown1 = v1; g_dwUnknown2 = g_dwUnknown0; g_fFrameDiff = v0 - v1 * 0.016666668; g_dwIGT += FltToDword(g_fFrameLength * 4000.0 + 0.5); LODWORD(g_fFrameLength) = 0; ++g_dwFrameCount; g_fIGT = (double)g_dwIGT * 0.00025000001; // Divides IGT by 4000 to get time in seconds }
      
      





まあ、まず:



 g_dwIGT += FltToDword(g_fFrameLength * 4000.0 + 0.5);
      
      









最初は、このコードはまったく無意味でした。 なぜこれに4000を掛けてから半分を追加するのですか?



実際、これは非常に難しい魔法です。 4000は開発者の頭に浮かんだ定数です...しかし、+ 0.5は数学の法則に従って丸める興味深い方法です。 4.7に半分を追加し、intに切り落とすと5を取得し、4.3を追加して丸めると4を取得します。 この方法は最も正確ではありませんが、おそらくより高速に動作します。 個人的に、私は注意します。



そして今、親愛なる読者の皆さん、私はあなたとゲームをプレイしたいです。 上記の完全な擬似コードを見て、そこでエラーを見つけてください。 疲れたり、興味がないだけなら、次のパートに進んでください。



エラー



g_fFrameDiff = v0-(double)v1 * 0.016666668;



誤ってスパイしないように、ネタバレの下でのエラー。 少し説明しましょう:0.01(6)は1/60秒です。 上記のコード全体は、明らかに、エンジンのクラッシュをカウントして補正する試みですが、誰もが60fpsでプレイするわけではないという事実を考慮していません。 これは、私のビデオですべての円が現実と一致したときに非常に興味深い結果が出た場所ですが、エウィルはそうではありませんでした。 彼は垂直同期をオフにしてプレイし、ゲームは最大120 fpsでロックされるため、コンピューターでコードが正しく機能しませんでした。 上記のコードをわずかに変更し、人間の形にしました。



 if ( g_fFrameLength != 0.0 ) { float tmpDiff = g_fFrameDiff + g_fFrameLength; int diffTime = FltToDword(v0); g_dwUnknown0 += diffTime; // Some unknown vars g_dwUnknown1 = diffTime; g_dwUnknown2 = g_dwUnknown0; g_fFrameDiff = tmpDiff - diffTime * 1.0/60; g_dwIGT += FltToDword(g_fFrameLength * 4000 + 0.5); g_fFrameLength = 0; ++g_dwFrameCount; g_fIGT = (float)g_dwIGT / 4000; // Divides IGT by 4000 to get time in seconds }
      
      





ここで、遅延を計算するときに、実際のフレーム時間が最初に使用され、後でハードコーディングされた60 fpsが使用されることがわかります。 疑わしい

vsyncをオフにして、1秒あたり120フレームを取得します。 動画を録画して、円で約0.3秒の差をつけます。 ビンゴ!



残っているのは、ハードコア60fpsをハードコア120fpsにパッチすることだけです。 これを行うには、アセンブラーコードを見て、このマジック定数が配置されているアドレス0x007875BCを見つけます。



ネタバレ
実際、この定数はfloat / double型であり、FPUにロードされることが知られています。 FPUはレジスタから読み込むことができないため、メモリ内のどこかにいることになっていた。 彼女がスタックにいなかったのはいいことです。そうでなければ私はそれほど簡単に降りられませんでした。 ゲームコードを直接変更する必要がありますが、これはしたくありませんでした。



今回は特別なプログラムを作成しませんでしたが、Cheat Engineを使用してこの変数の値を必要な値に変更しました。 その後、再び10ラップを記録し、時間をカウントしました。IGTとRTAは最終的に一致しました。



実際、それらは100%一致しませんでした。 しかし、ほとんどの場合、ビデオ録画によりフレームレートが大幅に浪費されたため、ゲームが時間差の適切な計算を停止しました。 しかし、一般的に、差は約0.02秒でした。



コードをもう少し検索しましたが、これは時間計算手順で非常に熱心に計算された変数の影響を受けます。 私はあまり見つけませんでしたが、g_fDiffTimeはg_fFrameTimeの隣のエンジンのどこかで使用されます。 おそらく、ジャグリングの補償についての私の仮定は真実であることが判明しました。 しかし、誰がこれらの開発者を知っていますか。



あとがき



毎秒60フレームのゲームに出くわす時間すら知りません。 これはゲームを書くのは非常に悪いスタイルであり、読者の皆さんはハードウェアの違いを考慮することを強くお勧めします。 特にあなたがインディー開発者である場合。 特に、PCで開発している場合。 コンソールの場合、さまざまな容量の鉄を実現することはできません。このため、PCで問題が常に発生します。 また、120 / 144Hzのリフレッシュレートなどのモニターがあります。 はい、g-syncはすでに到着しています。



ただし、NFSはコンソールからのポートであるため、多くの場合、ソリューションは純粋にコンソールアプローチを採用しています.FPSが60(30、25、任意の数)を超えないことを前提とし、多くのソリューションはこの1秒あたりのフレーム数に対して厳密にシャープ化されます。 悲しいかな、これはシリーズの新しい部分でより明らかになりました。



今回は多くの研究スペースがありますが、記事はそれほど大きくありませんでした。 これらのゲームには、何かおもしろいことがあるといいのですが。



All Articles