Doom 3 BFGソースコードレビュー:マルチスレッド(パート2/4)

パート1:はじめに
パート2:マルチスレッド
パート3:レンダリング(注ごと-翻訳中)
パート4:Doomクラシック-統合(注ごと-翻訳中)

Doom IIIのエンジンは、ほとんどのPCがシングルプロセッサであった2000年から2004年の間に書かれました。 idTech4エンジンアーキテクチャはSMPサポートを念頭に置いて設計されましたが、最終的にマルチスレッドサポートを行うことになりました( John Carmackのインタビューを参照 )。

それ以来、多くの変更が行われました。Microsoftの「 マルチコアシステム用プログラミング 」という優れた記事があります。

長年にわたって、プロセッサのパフォーマンスは着実に向上してきており、ゲームや他のプログラムは、労力を必要とせずにこの電力の増加から恩恵を受けています。
ルールが変更されました。 シングルコアプロセッサのパフォーマンスは現在、非常にゆっくりと成長しています。 ただし、パソコンとコンソールの計算能力は成長し続けています。 唯一の違いは、マルチコアプロセッサの存在により、基本的にこのような増加が得られることです。
プロセッサのパワーの増加は以前と同様に印象的ですが、開発者はこのパワーの可能性を完全に引き出すためにマルチスレッドコードを記述する必要があります。


ターゲットプラットフォームとDoom III BFGはマルチコアです。

その結果、idTech4はマルチスレッドのサポートだけでなく、idTech5のJob Processing Systemでも強化され、マルチコアシステムのサポートが追加されました。

注:少し前まで、Xbox OneとPS4の仕様が発表されました。どちらも8つのコアを備えています。 ゲーム開発者がマルチスレッドプログラミングに長けているもう1つの理由。

Doom 3 BFGスレッドモデル


PCでは、ゲームは3つのスレッドで実行されます。
  1. バックエンドインターフェイスレンダリングストリーム(GPUコマンドの送信)
  2. ゲームロジックとフロントエンドインターフェイスレンダリングのフロー
  3. 高周波ジョイスティックからのデータ入力ストリーム(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のフォーカスを維持できるようにするため、推奨されます。 それらの実装については、このページの下部で詳しく説明します

建築


サブシステムの頭脳はParalleleJobManagerです。 彼は、スレッドハンドラーの生成と、タスクを格納するキューの作成を担当しています。

そして、同期をバイパスする最初のアイデア:ジョブリストをいくつかのセクションに分割し、各セクションに1つのスレッドのみがアクセスするため、同期は不要です。 エンジンでは、このようなキューはidParallelJobListと呼ばれます。

Doom III BFGには3つのセクションしかありません。

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(); 


方法





ハンドラーの実行方法


ハンドラーは無限ループで実行されます。 各反復で、リングバッファがチェックされ、タスクが見つかった場合、ローカルスタックへの参照によってコピーされます。

ローカルスタック:スレッドスタックは、メカニズムが停止しないように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
指定されたオブジェクトがシグナル状態になるか、タイムアウトが期限切れになるまで待機します。
信号は、フローを停止するために使用されます。 ハンドラーは、ジョブが欠落している場合、idSysSignal.Wait()を使用してオペレーティングシステムスケジューラーから自身を削除します。

2.ミューテックス(idSysMutex):
抽象化
運営
実装
ご注意
idSysMutex

クリティカルセクションオブジェクト


ロックする
EnterCriticalSection
指定されたクリティカルセクションオブジェクトが受信されるまで待機します。 関数は、呼び出し元のスレッドがプロパティを受け取ったときに戻ります。


ロック解除
LeaveCriticalSection
クリティカルセクションの指定されたオブジェクトの受信を実装します。





3.アトミック操作(idSysInterlockedInteger):
抽象化
運営
実装
ご注意
idSysInterlockedInteger

連動変数


増加
InterlockedIncrementAcquire
特定の32ビット変数の値をアトミック操作としてインクリメントする操作には、「取得」のセマンティクスがあります。

デクリメント
InterlockedDecrementRelease
特定の32ビット変数の値をアトミック操作としてデクリメントします。 操作には「リリース」のセマンティクスがあります。

Source: https://habr.com/ru/post/J181081/


All Articles