
パート2:マルチスレッド
パート3:レンダリング(注ごと-翻訳中)
パート4:Doomクラシック-統合(注ごと-翻訳中)
Doom IIIのエンジンは、ほとんどのPCがシングルプロセッサであった2000年から2004年の間に書かれました。 idTech4エンジンアーキテクチャはSMPサポートを念頭に置いて設計されましたが、最終的にマルチスレッドサポートを行うことになりました( John Carmackのインタビューを参照 )。
それ以来、多くの変更が行われました。Microsoftの「 マルチコアシステム用プログラミング 」という優れた記事があります。
長年にわたって、プロセッサのパフォーマンスは着実に向上してきており、ゲームや他のプログラムは、労力を必要とせずにこの電力の増加から恩恵を受けています。
ルールが変更されました。 シングルコアプロセッサのパフォーマンスは現在、非常にゆっくりと成長しています。 ただし、パソコンとコンソールの計算能力は成長し続けています。 唯一の違いは、マルチコアプロセッサの存在により、基本的にこのような増加が得られることです。
プロセッサのパワーの増加は以前と同様に印象的ですが、開発者はこのパワーの可能性を完全に引き出すためにマルチスレッドコードを記述する必要があります。
ターゲットプラットフォームとDoom III BFGはマルチコアです。
- Xbox 360には、キセノントライコアプロセッサが1つ搭載されています。 プラットフォームの同時マルチスレッド化は6つの論理コアです。
- PS3には、PowerPCプロセッサと8つの相乗的コア(SPE)に基づくメインユニット(PPE)があります。
- 多くの場合、PCにはクアッドコアプロセッサが搭載されています。 ハイパースレッディングにより、このプラットフォームは8つの論理コアを取得します。
その結果、idTech4はマルチスレッドのサポートだけでなく、idTech5のJob Processing Systemでも強化され、マルチコアシステムのサポートが追加されました。
注:少し前まで、Xbox OneとPS4の仕様が発表されました。どちらも8つのコアを備えています。 ゲーム開発者がマルチスレッドプログラミングに長けているもう1つの理由。
Doom 3 BFGスレッドモデル

- バックエンドインターフェイスレンダリングストリーム(GPUコマンドの送信)
- ゲームロジックとフロントエンドインターフェイスレンダリングのフロー
- 高周波ジョイスティックからのデータ入力ストリーム(250Hz)
さらに、idTech4はさらに2つのワークフローを作成します。 これらは、3つのメインストリームのいずれかを支援するために必要です。 それらは可能な限りスケジューラーによって管理されます。
主なアイデア
Id Softwareは、2009年のマルチコアプログラミングソリューションをBeyond Programming Shadersのプレゼンテーションで発表しました。 ここでの2つの主なアイデア:
- 異なるスレッドで処理するための個別のタスク処理(「ワーカー」による「ジョブ」)
- 同期をオペレーティングシステムに委任しないでください:アトミック操作のために自分で同期を行います
システムコンポーネント
システムは3つのコンポーネントで構成されています。
- タスク(ジョブ)
- 労働者
- 同期
タスクはまさにあなたが期待するものです:
struct job_t { void (* function )(void *); // Job instructions void * data; // Job parameters int executed; // Job end marker...Not used. };
注:コードのコメントによると、「タスクは、切り替えコストを上回るために少なくとも1000サイクル続く必要があります。 一方、複数のプロセス間で良好な負荷バランスを維持するために、タスクは数100,000サイクルを超えてはなりません。
ハンドラーは、シグナルを待機している間、非アクティブのままになるスレッドです。 アクティブ化されると、タスクを見つけようとします。 ハンドラーは、アトミック操作を使用して同期を回避し、一般リストからジョブを取得しようとします。
同期は、シグナル、ミューテックス、アトミック操作の3つのプリミティブを介して実行されます。 後者は、エンジンがCPUのフォーカスを維持できるようにするため、推奨されます。 それらの実装については、このページの下部で詳しく説明します 。
建築

そして、同期をバイパスする最初のアイデア:ジョブリストをいくつかのセクションに分割し、各セクションに1つのスレッドのみがアクセスするため、同期は不要です。 エンジンでは、このようなキューはidParallelJobListと呼ばれます。
Doom III BFGには3つのセクションしかありません。
- フロントエンドAをレンダリング
- バックエンドAをレンダリングする
- 公益事業
PCでは、起動時に2つのワークフローが作成されますが、おそらくXBox360とPS3でさらに多くのワークフローが作成されます。
2009年のプレゼンテーションによると、idTech5にはさらにセクションが追加されました。
- 欠陥検出
- アニメーション処理
- 障害物回避
- テクスチャ処理
- 粒子透過処理
- 組織シミュレーション
- 水面シミュレーション
- 詳細なモデル生成
注:プレゼンテーションでは、1フレーム遅延の概念についても言及していますが、コードのこの部分はDoom III BFGには適用されません。
タスク配布

使用する
タスクは1つのスレッドのみがアクセスできるセクションに分割されるため、同期は必要ありません。 ただし、システムハンドラにタスクを提供することは、ミューテックスを意味します。
//tr.frontEndJobList is a idParallelJobList object. for ( viewLight_t * vLight = tr.viewDef->viewLights; vLight != NULL; vLight = vLight->next ) { tr.frontEndJobList->AddJob( (jobRun_t)R_AddSingleLight, vLight ); } tr.frontEndJobList->Submit(); tr.frontEndJobList->Wait();
方法

- タスクの追加 :同期の必要なし、タスクはキューに追加されます
- ディスパッチ :ミューテックス同期。各ハンドラーは、ローカルのジョブリストから一般的なジョブリストを補充します。
- 同期信号(OS委任) :
ハンドラーの実行方法

ハンドラーは無限ループで実行されます。 各反復で、リングバッファがチェックされ、タスクが見つかった場合、ローカルスタックへの参照によってコピーされます。
ローカルスタック:スレッドスタックは、メカニズムが停止しないようにJobListsアドレスを格納するために使用されます。 スレッドがJobListを「ブロック」できない場合、RUN_STALLEDモードになります。 この停止は、ローカルJobListから一般リストにスタックを移動することによりキャンセルできます。
興味深いのは、すべてが相互メカニズムなしで行われることです:アトミック操作のみです。
無限ループ
int idJobThread::Run() { threadJobListState_t threadJobListState[MAX_JOBLISTS]; while ( !IsTerminating() ) { int currentJobList = 0; // fetch any new job lists and add them to the local list in threadJobListState {} if ( lastStalledJobList < 0 ) // find the job list with the highest priority else // try to hide the stall with a job from a list that has equal or higher priority currentJobList = X; // try running one or more jobs from the current job list int result = threadJobListState[currentJobList].jobList->RunJobs( threadNum, threadJobListState[currentJobList], singleJob ); // Analyze how job running went if ( ( result & idParallelJobList_Threads::RUN_DONE ) != 0 ) { // done with this job list so remove it from the local list (threadJobListState[currentJobList]) } else if ( ( result & idParallelJobList_Threads::RUN_STALLED ) != 0 ) { lastStalledJobList = currentJobList; } else { lastStalledJobList = -1; } } }
就職
int idParallelJobList::RunJobs( unsigned int threadNum, threadJobListState_t & state, bool singleJob ) { // try to lock to fetch a new job if ( fetchLock.Increment() == 1 ) { // grab a new job state.nextJobIndex = currentJob.Increment() - 1; // release the fetch lock fetchLock.Decrement(); } else { // release the fetch lock fetchLock.Decrement(); // another thread is fetching right now so consider stalled return ( result | RUN_STALLED ); } // Run job jobList[state.nextJobIndex].function( jobList[state.nextJobIndex].data ); // if at the end of the job list we're done if ( state.nextJobIndex >= jobList.Num() ) { return ( result | RUN_DONE ); } return ( result | RUN_PROGRESS ); }
IDソフトウェア同期ツール
Id Softwareは、3種類の同期メカニズムを使用します。
1.モニター(idSysSignal):
抽象化
| 運営
| 実装
| ご注意
|
idSysSignal
|
| イベントオブジェクト
|
|
| 上げる
| SetEvent
| オブジェクトの指定されたイベントをシグナル状態に設定します。
|
| クリア
| ResetEvent
| オブジェクトの指定されたイベントを非シグナル状態に設定します。
|
| 待って
| WaitForSingleObject
| 指定されたオブジェクトがシグナル状態になるか、タイムアウトが期限切れになるまで待機します。
|
2.ミューテックス(idSysMutex):
抽象化
| 運営
| 実装
| ご注意
|
idSysMutex
|
| クリティカルセクションオブジェクト
|
|
| ロックする
| EnterCriticalSection
| 指定されたクリティカルセクションオブジェクトが受信されるまで待機します。 関数は、呼び出し元のスレッドがプロパティを受け取ったときに戻ります。
|
| ロック解除
| LeaveCriticalSection
| クリティカルセクションの指定されたオブジェクトの受信を実装します。
|
|
|
|
3.アトミック操作(idSysInterlockedInteger):
抽象化
| 運営
| 実装
| ご注意
|
idSysInterlockedInteger
|
| 連動変数
|
|
| 増加
| InterlockedIncrementAcquire
| 特定の32ビット変数の値をアトミック操作としてインクリメントする操作には、「取得」のセマンティクスがあります。
|
| デクリメント
| InterlockedDecrementRelease
| 特定の32ビット変数の値をアトミック操作としてデクリメントします。 操作には「リリース」のセマンティクスがあります。
|