2013年12月27日

不正落ちしたときにエラー個所を特定する(シンプル説明)

この話題すでに一度書いたのですが(http://junosoft.sblo.jp/article/60096788.html
無駄に遠回りな書き方をした気がするので、もっとシンプルに書いてみます(本当は自分のメモ用なんですけどね)



■アドレスからエラー個所を特定できるように、map, cod ファイルを生成するよう設定しておきます。これがないとどうにもなりません
・「プロジェクト」→「プロパティ」を開き、「構成プロパティ」→「C/C++」→「出力ファイル」を展開
・「アセンブリの出力」を「アセンブリ コード、コンピューター語コード、ソース コード (/FAcs)」に設定する
・この設定でビルドしたものを、ユーザーに配布する



■いざエラーが発生したら、ユーザーに以下の操作をしてもらい、「障害オフセット」の値を調べます

・イベントビューアを開く
WinXPの場合
スタートメニューを開いて、「コントロール パネル」「パフォーマンスとメンテナンス」「管理ツール」をクリックし、「イベント ビューアー」を開く

Win7の場合
スタートメニューを開いて「コントロール パネル」「システムとセキュリティ」「管理ツール」をクリックし、「イベント ビューアー] を開く

Win8の場合
Windows + X キーを押してメニューを開き、「イベントビューア」をクリックする

・イベントビューアの左側にあるツリービューから、「Windowsログ」の「Application」を開く
・イベント一覧が表示されるので、「日付と時刻」を頼りに、エラーを探す
・ダブルクリックするとイベントの内容が表示されるので「障害が発生しているアプリケーション名」や「障害が発生しているモジュール名」が目的のものであることを確認する
・「障害オフセット」の値をメモする



■障害オフセットの値がわかったら、それに対応するソースコードを探します
・「障害オフセット」の値に、ベースオフセット 0x00400000 を足して「エラーアドレス」を求める
・ビルドしたときに生成された .map ファイルを開く
・関数名と関数アドレス、その関数を含む .obj ファイル名の一覧が並んでいるので、「関数アドレス(Rva+Base と書いてある部分)」を頼りに、「エラーアドレス」を含む「エラー発生関数」を探す
・「エラーアドレス」から「関数アドレス」を引いて、「エラー個所のオフセット値」を求める
・「エラー発生関数」を含む .obj に対応する .cod ファイルを開く
・ソースコードとそれに対応するアセンブリコードが載っているので、そこから「エラー発生関数」を探す
・「エラー個所のオフセット値」を頼りに、エラーが発生した場所を特定する



以上の操作で、エラーが発生した関数名、ソースファイル名、行番号などがわかります
posted by JUNOSOFT at 21:55| Comment(0) | プログラミング

2013年12月23日

FPS制御

FPS制御の話です

例えば毎秒 60 フレームで固定したい場合、おそらく真っ先に思い付くのは

while (...) {
Sleep(1000/60);
ゲーム処理();
}

です。ところが Sleep() の引数はミリ秒単位の整数なので、
本当は 1000÷60=16.666... ミリ秒待機しないといけないのが 16 ミリ秒の待機になり、
1ループあたり 0.666...ミリ秒だけタイミングが早くなることになります。
すると実際の FPS は 1000÷16 = 62.5 になり、毎秒 62フレーム+αになってしまいます。

ただしこれは、ゲーム処理の時間と、Sleep関数の誤差を考慮しない場合の話です。
実際にはゲーム処理にはそれなりの時間がかかるし、Sleep関数も、指定した時間だけきっちり待ってくれるとは限りません。
というわけで非常にシンプルですが、このやり方はボツです。




次に思いつくのは1秒の間での更新スケジュールを決めておき、その時間になるまで待機する、という方法です。
つまり

DWORD baseTime = timeGetTime();
DWORD frameCount = 0;
while (...) {
 DWORD nextTime = baseTime + 1000 * frameCount / 60; // 更新予定時刻
 DWORD currTime = timeGetTime(); // 現在時刻
 if (currTime < nextTime) {
  Sleep(nextTime - currTime); // 現在時刻が、更新予定時刻より前ならば待機
 }
 if (baseTime + 1000 <= currTime) {
  baseTime = currTime; // 1秒ごとに基準時刻を更新
  frameCounter = 0;
 } else {
  frameCount++;
 }
 ゲーム処理();
}

という方法です。1秒ごとに基準となる時間を設定し、その時間からの経過フレーム数によって
更新時刻を決定します。これは一見うまくいくのですが、
実はこの方法、重い処理の直後に早送り状態にあるという欠点があります。
処理時間が一定していれば問題ありませんが、通常処理の中に重い処理があると、
予定よりも遅れた分を取り戻そうとしてウェイトなしの早送り状態になります。
この早送りは、予定遅れが解消されるか、基準時刻が更新されるまで続きます。
つまり最大でも1秒間なので、この挙動には目をつむるというのもアリです。




その挙動が気になる方のために、解決方法を考えてみます。早送り状態なってしまうのは、
無理に予定に合わせようとしているのが原因なので、遅延した場合はいさぎよく予定をあきらめ、
無理のないスケジュールを組みなおせばよいわけです。
これには、上のプログラムを少し改造して以下のようにします

DWORD baseTime = timeGetTime();
DWORD frameCount = 0;
while (...) {
 DWORD nextTime = baseTime + 1000 * frameCount / 60; // 更新予定時刻
 DWORD currTime = timeGetTime(); // 現在時刻
 bool delayed = false;
 if (currTime < nextTime) {
  Sleep(nextTime - currTime); // 現在時刻が、更新予定時刻より前ならば待機
 } else {
  delayed = true; // 遅延発生
 }
 if (baseTime + 1000 <= currTime || delayed) {
  baseTime = currTime; // 1秒ごとに基準時刻を更新
  frameCounter = 0;
 } else {
  frameCount++;
 }
 ゲーム処理();
}

という方法です。前回のフレーム計測基準時刻から1秒経過するか、または予定よりも遅れている場合に基準時刻を取り直します。
ただ、これまでは1秒ごとの基準時間更新時に frameCounter の値を記録することで実行FPS値を取得できたのですが、
この方法だと遅延が発生した場合に frameCount が毎回リセットされてしまうため、実効FPSを取得することができません。

いろいろいじりまわした結果、最終的には以下のような形になりました

DWORD FPS = 60; // 目標FPS
DWORD currentFPS = 0; // 実際のFPS
DWORD baseTime = timeGetTime();
DWORD lastTime = timeGetTime();
DWORD frameCount = 0;
while (...) {
 DWORD time = timeGetTime();
 if (baseTime + 1000 <= time) { // 1秒ごとに基準時刻を更新
  currentFPS = frameCount * 1000 / (time - baseTime); // frameCount回更新したときの所要時間から、1秒あたりの更新回数を求める
  frameCount = 0;
  baseTime = time;
 }
 DWORD next1 = baseTime + 1000 * frameCount / FPS; // 予定された更新時刻
 DWORD next2 = lastTime + 1000 / FPS; // 早送りにならないようにした時の更新時刻
 DWORD next = max(next1, next2); // 遅いほうの更新時刻に合わせる
 lastTime = time;
 if (time < next) {
  Sleep(next - time);
 }
 frameCount++;
 ゲーム処理();
}



当たり前のようにやっているFPS制御ですが、意外と深いです

posted by JUNOSOFT at 20:54| Comment(0) | プログラミング

2013年12月16日

VC2013

Visual C++ 2013 Windows Desktop を使ってみたのですが、ようやくBOMなしUTF8に対応したみたいですね〜〜!!
おかげでソースをUTF8化することができそうです。日本語コメント付きのソースを XCode と共有しているときに頭の痛い問題でした。それにSJISだと文字化けするエディタとかあるので...
入力補完もかなり頭がよくなってきて、快適に入力できるようになりました。GJ!
posted by JUNOSOFT at 11:35| Comment(0) | プログラミング

2013年12月04日

Hgで、問題の二分検索を使う

現在作業中のリビジョンでバグが見つかったとき、どの段階でそのバグが混入したのか調べるために
二分検索を良く使っていました。

たとえばリビジョン 100 で正常動作していたものが、リビジョン 200 ではダメだった場合、
ちょうど中間であるリビジョン 150 を試してみます。これが正常に動いたら 150 から 200 のどこかで問題が発生したことになるので、
今度は 150 と 200 の中間である 175 で試します。ここで問題がでた場合、150 と 175 のさらに中間である 162 で試します。
これを繰り返していくと、少ない試行回数で問題が発生した最初のリビジョンを特定することができます。

ということを手動でやっていたのですが、いまさらながら TortoiseHG にそのための機能があることに気が付きました。
TortoiseHG Workbench のリポジトリメニューにある「問題の二分検索」です。なぜ今まで気が付かなかったのかが不思議なくらい、わかりやすいタイトルです。

ここで、「既知の正常なリビジョン」に正しく動いていた時のリビジョン番号を入力し、「取り込む」をクリック。
つぎに、「既知の問題リビジョン」にダメなリビジョン番号を入力し、「取り込む」をクリック。最新リビジョンを指定したければ -1 と入力すればOK。
すると自動的に中間リビジョンに更新してくれるので、その状態で試した結果に応じて
「このリビジョンは問題ありません」か「このリビジョンは問題あります」のどちらかをクリックしていきます。
同じことを繰り返していけば、やがて「みつかりました!」みたいなメッセージがでて、問題が発生した最初のリビジョンを教えてくれます。

いやあ、便利なものですね。
posted by JUNOSOFT at 15:46| Comment(0) | プログラミング