考察2 - スレッドモデルの変遷

 音ゲーでは、サウンドの再生、入力の判定、画面の描画が主な作業になります。そしてこれらはそのまま「サウンドのズレ」「入力のズレ」「描画のズレ」と関連します。

 初期の DTXMania では、サウンド、入力、描画をすべて 1 つのスレッドで逐次処理していました。そのため「描画のズレ」がサウンドと入力にも大きな影響を与えていて、それを制御するために VSync オプションがあるということもお話ししました。

 現在のプログラミングは、マルチコア・マルチスレッドの時代です。
 いやまだ死語じゃないです。

 描画はもうこれ以上早くするとティアリングが起きるレベルに達しているのですが、サウンドと入力にとってはまだまだ遅すぎます。そこで、描画とその他とでスレッドを分離すれば、描画の精度はそのままに、サウンドと入力の精度を上げることができ、問題をもっとスマートに解決するのではないでしょうか?

スレッドモデルの検討

 DTXMania におけるスレッドの分離は、実は Windows 98 の時代から何度も何度も試みて、そのたびに失敗してきました。Windows 98 のスレッド機能が貧弱だったり Direct3D をマルチスレッドモードにするとくっそ重くなったり

 最終的に現在の方式(3スレッドモデル)に落ち着くまでの検討メモがありましたので、簡単に振り返ってみます。

1スレッドモデル

「進行(サウンド+入力)」と「描画」を、 1 つのスレッドで逐次処理するモデルです。

 垂直帰線同期の存在により、進行処理の精度は下がります。VSync オプションで垂直帰線期間を待つか待たないかを設定できますが、すでに述べた通り、画面の品質や発熱とのトレードオフになります。

 また、ウィンドウメッセージ処理もこのスレッドで行なわれるため、VSync で停止している間はウィンドウメッセージも処理されなくなります。昔ならともかく、最近の Windows は「色々なリアルタム処理も全部ウィンドウメッセージで行なう、だからウィンドウメッセージの処理ループは止めるな」という方向に向かっていますので、遅延に敏感な音ゲーにとってこれはかなり致命的です。

 無印版/派生版の DTXMania は未だにこのモデルです。

2スレッドモデル

「進行(サウンド+入力)」と「描画」を、それぞれ独立したスレッドで動作させるモデルです。

 この場合、デバイスリソースを使う作業(各ステージの活性化と非活性化など)は、描画スレッドにて行われることになります。その結果、進行スレッドと描画スレッドとでフロー制御(ステージの終了判定や遷移など)が 2 つ混在することになり、スレッド間でのキャッチボールが増え、非常にソースコードの見通しが悪くなりました。最終的に没です。

3スレッドモデル

「進行(サウンド+入力)」、「描画」、「フロー制御」の3つの独立したスレッドを使うモデルです。

 フロー制御が独立したため、設計時には非常に整理された図が描けました。しかし、思ったよりもソースコードの見通しは良くならず、さらにデバイスリソースを使う作業だけは依然として描画スレッドが担当しなければならなかったので、独立させるはずだったフロー自体が、実装では分断されるという問題が発生してしまいました。結果、キャッチボールをするのが3スレッドになりました。MVCモデルなどにも近い考え方なのでかなり頑張ったのですが、やっぱり没です。

1スレッドモデル(フロー制御統合版)

 1 つのスレッドで「進行(サウンド+入力」、「描画」、「フロー制御」を行いますが、常にこの3つを逐次処理するのではなく、描画のみ必要なときだけ行なうようにしたモデルです。具体的には、描画のみ WM_TIMER イベントのコールバックで行い、イベントが来ない間は全力で他の処理を回します。

 こうすると、進行やフロー制御のコードに何か入れなくても描画はイベントで勝手に割り込んでくるため、ソースコードは程よくまとまって見通しがよくなりました。しかし、WM_TIMER 自体の精度の悪さと、垂直帰線同期でブロックしている間は他の処理も回せなくなるという問題は残りました。結果、垂直帰線期間のとき以外でのみ精度がよくなるという非常に品質の悪いものになりました。没です。

2スレッドモデル(フロー制御統合版)

「進行(サウンド+入力)」と「描画」をそれぞれ別スレッドで行い、「フロー制御」は描画スレッド側に統合したモデルです。描画スレッドが垂直帰線同期でブロックしていても、進行スレッドは高い精度で処理を継続できます。高い精度と言ってもフル回転させているわけではなく、1~2ms のインターバルを挟み、秒間500回くらいに収まるようにしていました。

 実際には、高い精度のサウンドや入力が必要となるのは演奏ステージのみなので、選曲ステージや結果ステージなどその他のステージでは進行スレッドは停止させています。

 DTXmatixx と DTXMania2 の中盤まではこのモデルです。

 このモデルでは、進行スレッド側では描画関連のリソースには一切触れないこと、フロー制御に状態を示すために状態管理(状態遷移図)を行なうこと、などが条件となっています。状態管理のコードが冗長になりがちですが、綺麗に整理できます。

 これで、スレッドモデルについては無事解決しました。

 と思ったのですが。
 時代は待ってくれません。

3スレッドモデル(メッセージフル回転版)

 先述したように、時代と共に( Windows Updateと共に)、ウィンドウメッセージの重要性も変化してきました。具体的には、入力やサウンド、動画や汎用タスクなど、リアルタイム処理に関わる Windows 側の処理がアプリのウィンドウメッセージの処理頻度に依存するようになってきました。もう、メッセージ処理が VSync で 16.6ms もブロックするようなことが許される時代ではありません(あくまで音ゲーにとってですが)。

 DTXmatixx / DTXMania2 序盤の2スレッドモデルではウィンドウメッセージの処理を描画スレッドで行なっていたので、VSyncブロックの影響が発生してしまいます。よって、再びスレッドモデルを検討し直すことになりました。

 それで出来上がったのが、「高頻度進行(サウンド+入力)」、「通常進行+描画+フロー制御」、「ウィンドウメッセージ」の3スレッドモデルです。

 DTXMania2の中盤以降、現在まで、ずっとこのモデルです。

 ウィンドウメッセージスレッドは、UIスレッドでもあり最初に起動される STAThread に担当させる必要があります。このスレッドは、他の2つのスレッドを生成した後は、ただひたすらにウィンドウメッセージの処理に注力します。ハードウェアから生の入力を直接読み取る RawInput やそれを利用する DirectInput なども(そして恐らくMIDI入力も)ウィンドウメッセージに依存するので、もう全力で回します。

 DTXMania の進行・描画・フロー制御は、他の2つのスレッドが担当します。MTAThreadとして生成するので、DirectX の COM 関連のメッセージがウィンドウメッセージスレッド(STAThread)の邪魔をすることもありません。

 ただし、COMは基本的にSTAThreadで動作させるものなので、MTAThreadで扱う場合、どの COM がどのスレッドで生成されたかをきっちり把握していなければ、すぐにインターフェースの Cast 例外で落ちます。C# のレベルでも、RCW がないぞ!とかいう例外で落ちます。ええ。覚えてられん!

 1~2ms レベルもの高精度な進行処理は演奏ステージ以外では必要なかったので、「高頻度な進行処理」と「通常の進行処理」に分けて、後者は描画スレッドに含めています。そのため、演奏ステージ以外は、普通の1スレッドモデルと同じように、逐次的かつ簡潔にプログラミングできるようになりました。

ゲリラ戦の時代 - 秘密裏に変わってゆく仕様

 高頻度進行スレッドの1~2ms の安定したインターバルの確保も、大きな課題でした。もはや Sleep 関数ではとても実現できない時代になっていました。それでも、DTXMania2 の序盤くらいまでは、きっちり 1~2ms の精度を出せる別のタイマーAPI を何とか探しだすことに成功し、それで実装していました。

 しかし、その苦労も虚しく、とある時期の Windows Update を境に、その API の精度は 10ms 程度まで急落してしまいます。Microsoft の公式サイトには何の説明もありませんでした。まあ、元々この API については精度の話など載ってなかったのであまり文句は言えませんが……(自分で計測して精度を確認しただけ)。

 今(DTXMania3以降)ではまた新しい方法を見つけ出し、それで実現しています。しかし、これもまた、いつか仕様が秘密裏に変更されるかも知れません。

 いつの時代も、クライアントはプラットフォーマーに振り回されるものですね。
 まともな手続きを経ないゲリラ戦の様相です。
 戦争はいつ終わるの?

FPS と VPS

 最近の DTXMania の演奏画面では、進行処理の精度の目安として FPS(frame per second)値を表示していますが、同時に、描画処理の精度の目安として VPS(view per second)値も表示しています。(※「VPS」は私の造語なので注意。)

 DTXmatixx のとき、パフォーマンスの目標として以下のように定めました。

  • 描画 …… 60 vps 固定。垂直帰線周波数を 60Hz と想定するので精度は約 16.6㎳ になるが、それよりも画面の滑らかさ(画面のちらつきとテアリングの回避)を優先すべき。
  • サウンド、入力 …… 180 fps 平均。精度は約 5.5㎳ 平均。最近のパソコンなら全力で回せば 400 fps(同2.5㎳)くらいは出せるだろうが、あまり CPU パワーを食わないようにすべき。(うちのパソコンだと 3㎳ くらい sleep すればこのくらいの値になります。)

 DTXMania2/3以降では、目標は 60vps固定/500fps 平均に変更されました。感覚です。