2023年8月11日金曜日

DirectX12 Multithread対応の続き2

マルチスレッド対応をしたけど、Releaseビルドで動かすと何故かカクカクの動きになる。
Debugビルドでは問題なく動いているように見えたけど、なにか問題のある書き方をしてるんだろう。

PIXでフレームの時間を見ると400us程度で終わっている。
この時間で1フレームが終わっているなら、2500fps出る計算になるが、実際に出るのは200fps程度で、60fsp固定している場合でも頻繁に30台~50台になり安定していない。


マルチスレッド処理の見直し1


メインスレッドと、コマンドリスト追加用のスレッド間の同期を必要以上に行っているので、シングルスレッドよりも非効率になってしまっていた。

メインスレッドで1つのコマンドをキューに追加したら裏で同時に動いてもらいたかったので、キューに追加するたびに通知して並列に動かしていたが、これがあまり良くないみたいだ。

ある一定量をキューに追加して、コマンドリストにはその単位でまとめて追加してもらうように修正。コマンドリスト追加用のスレッドの同期は、起こされたときに自動的にロックされる仕組みをそのまま利用して処理を行い、全て終わったらそのまま寝る。余計なLock/Unlockを行わないようにした。

この改善により、Releaseビルドでもまともに動くようになり、fpsも700ぐらい出るようになった。


マルチスレッド処理の見直し2


上記の見直しで、実はちゃんとしたダブルバッファリングが出来ていないのではないかという疑惑が浮かんだ。

一番最初の知識は、マイクロソフトのD3D12Fullscreenというサンプルだった。
サンプルでは、いくつかのリソースをフレーム数分用意してバックバッファインデックスによって使うリソースを切り替えて使っていくという方法。これを真似ればダブルバッファリングの仕組みになっていると思っていたが、罠が潜んでいた。
ライブラリ開発当初からの疑問で、定数バッファなどはフレーム数分用意しているのに、中間レンダーターゲットは1つしか用意していない。
あるフレームで、中間レンダーターゲットに書き込んでその結果をレンダーターゲットに書き込む。次のフレームの描画はこの中間レンダーターゲットの利用が済んでいないとできない。

中間レンダーターゲットもバッファ数分用意しないといけないのではないか?
マイクロソフトのサンプルが1つで処理しているから不要なのか?
実際の動作は1つでも動いている様に見える。
1年以上この疑問を持ちながら開発してきたけど、ついに確信を持ってバッファ数分必要だということがわかった。

1つで良かったのはシングルスレッドで処理しているからだ。マルチスレッドで、前のフレームリソースをGPU処理している最中に次のフレームのコマンドリストに同じ中間レンダーターゲットを追加しようとすればデバッグレイヤーで怒られる。今まではキッチリ中間レンダーターゲットの利用が終わるまで待って次のフレームの処理を行っていたんだと思う。

この確信により、さらなる改善案はExecuteCommandListsの呼び出しも別スレッドにすることだった。

見直し1まではコマンドリスト追加を別スレッドにやってもらって、ExecuteCommandLists呼び出しはメインスレッドで行っていたが、各追加スレッドが終わるのを待って、ExecuteCommandListsを呼び出すのに足止めを食らってしまう。実際には待たずに次のコマンドリスト追加を行いたい。

というわけで、ExecuteCommandListsを呼び出す部分も別スレッドにした。
これに伴い、書き換えが必要な各リソースを完全にフレーム数分用意する必要が出てきた。
リソースはその仕組を用意していたので、必要なリソースに関してはフレーム毎に用意するように修正した。

2つ目の改善で、FPSは1200を超えるようになった。


この改造をするに当たり、一旦壊して再構築を2回行ったのでめちゃくちゃ時間がかかった。だけど、かなり良い結果になって満足。



ハマりポイント


見直し1について


まず、マルチスレッド化するときにコマンドクラスと、コマンドラインクラスを作った。
コマンドクラスをメインスレッドで用意して、キューに追加してコマンドラインクラスに通知。
コマンドラインクラスが起きて、キューがある場合コマンドラインに追加してまた寝るを繰り返す。
コマンドラインを数ライン準備できたらExecuteCommandListsを呼び出すためにメインスレッドで各コマンドラインクラスのスレッドの終了を確認する際、キューの中身が空になっているのを確認しつつ、更にそのスレッドが寝たことを確認するために別のMutexを用意して確認していた。

結果的に処理がスムーズに流れず、シングルスレッドで動かすより遅くなっていた。

解決策は同期を最小限にすること。
Mutexのロックして、条件変数のWaitに渡したら手動では一切ロック操作は行わない。
起こされて寝るまではロックしっぱなしで処理を行う。
メインで追加したコマンドをコマンドラインに追加する処理を完全に並列にしたかったが、それを諦めてある程度の単位でそれを行う事により、スムーズに動くようになった。

ExecuteCommandListsを呼び出すためにメインスレッド側の確認は、Mutexの同期をしてキューの数を数えるだけで良くなった。
前は仮に0だとしても、キューから抜いた後まだ動いている可能性があったため寝た確認まで必要だったけど、ロックしっぱなしにすることによりロックが取れたということは処理済み、もしくはまだ起きていないということになる。

ここで少し話が逸れるが、標準ライブラリの条件変数の反応が良くない気がする。
キューに追加し終わって通知を行ってを何ラインか繰り返して、いざ実行する段になって各スレッドの状況を確認する際、ロックを取りに行って固まればちょうど実行中で取れた段階で処理完了が確認出来る。メインスレッド側もうまく待てる。これがロックを取りに行ってロックが取れた段階でキューを確認するとまだ入っているラインがいくつかある場合がある。通知をして余ってるCPUがあれば即座にスレッドが動作するものと思っていたけど、全然起きない。このループを2000回くらいやってやっと処理が終わるということもあった。
ロックが取れてキューが残っている場合は改めて通知をする様にしても変わらず反応が鈍い。昔のCPUはコア数が少なかったから通知したら自分自身のコアがそのまま通知先のスレッドの処理に切り替わるなどして、反応がよかったのか?
Mutexをロックしてから通知するのが一番効率がよいはずだったけど、今のCPUだとそのあたりの定石も変わってしまったのかもしれない。


見直し2について


コマンドキューもクラス化して、ExecuteCommandListsを別スレッドに実行してもらうことにより、コマンドラインの完了をメインスレッドで待たなくて良くする対応を行ったが、ライブラリの根幹部分を変更する必要があるため、すべてが動かなくなる。

自分のコーディングスタイルが少し修正して動かして確認する方法なので、この状況は非常にストレスだった。途中手がつけられず放置した期間もあるけど、この期間が長くなると取り返しがつかなくなるので、無理矢理にでも手を動かした。

マルチスレッドで、シングルスレッドよりも動きの予想が立ちづらい状況で一切動かさず組み上げるのはかなり困難だ。
とりあえず組み終わって、コンパイルが通っても最初は全く動かなかった。
呼び出し方もわかっているAPIで、今まで動いていたにもかかわらずマルチスレッドにしたらエラーが出まくっている。ログを入れまくり、実際の呼び出し順番を把握していった結果、なんとか原因を突き止めた。

原因はフレームを切り替えた(Present)後に、アプリケーション側のフレーム切り替え処理を行っていることだった。
フレーム切り替え処理は2つのことを行っており、1つは前フレームのリソースの開放、もう1つは、今フレームのGPU要求処理(ExecuteCommandLists)の完了を待つ事。
GPU要求処理に関しては、フレーム切り替え前に完了している必要があるため、場所を切り替え前に移した。

他にも細かい修正をいくつか行った末にやっと元の表示が出来るまで復活した。

と思ったけど、パフォーマンスを見るためにFPSを表示しようとしたところ、表示されない。何回か表示/非表示を繰り返すと一瞬表示されて消える現象が確認できた。表示してないわけではないけどすぐに消えてしまうようだ。

この原因はDirect2Dの処理は3D処理の後に行っているが、こちらはマルチスレッド化対象外になっている。
フレーム切り替え直前で3DのGPU要求完了を待っている為、2D処理が被ってしまいおかしくなっているようだ。
上記のフレーム切り替え処理を更に前に移して、2D処理前に完了させるようにしたところ正常に表示されるようになった。

ただ、2Dの処理はシングルスレッドなので、大量のコマンドを送信する場合はボトルネックになってくる。今後こちらも別途マルチスレッド化するか、3D側で文字表示出来るようにしてしまうかを考える必要がある。




2023年5月18日木曜日

Outline Shader

今回はアウトラインシェーダについて

調べたところ、ここで4つの方法が紹介されていた。

1つ目の方法は背面法と呼ばれるもので、主流?の方法らしい。
ただ色々問題があって、手間を掛けないとちゃんとした枠線が描けない。

2つ目はステンシルバッファを使った方法。
まだ自分のライブラリにステンシルバッファが組み込まれていないので後回し。
3Dオブジェクトの裏をキャラクタが通った時、ステンシルのアウトラインがそのままシルエット代わりになる方法が使えそうなので、今後やってみる予定。

3つ目は深度テクスチャを使う方法。

4つ目は法線テクスチャを使う方法。

他にも調べていたところ、深度テクスチャ、法線テクスチャの両方を使ってアウトライン描画を行うここのサンプルがシンプルで分かりやすかったので参考にさせてもらい実装してみた。


深度テクスチャを使ったアウトライン描画


対象の点と付近の点(サンプルでは4点)の差分を合算する。


深度テクスチャのアウトライン描画

深度テクスチャだけを使ったアウトライン描画だと、外枠のみが描画される


法線テクスチャを使ったアウトライン描画


深度テクスチャと同じように対象の点と付近の点(サンプルでは4点)の差分を取って、xyzの成分を足し合わせて合算する。

ただこちらの方は結果が思ったようにならず、オブジェクトの正面から見ると線が描画されるけど(でも不完全)、裏に回ってみると、線が全く描画されなくなった。



表から見た場合

裏から見た場合

色々調べてみると法線がView空間になっているらしく、こちらの実装もView空間に変換したら裏側でも線が描画されるようになった。


法線テクスチャのアウトライン描画

法線テクスチャだけを使ったアウトライン描画だと、オブジェクトの中の線が描画される。


深度テクスチャと法線テクスチャを使ったアウトライン描画


両方のテクスチャの値を使って組み合わせるとお互いの結果が補完されていい感じの描画になる。

組み合わせたアウトライン描画

アウトラインシェーダ


float4 OutlineDepthNormal(
	in		float4			Col,		// ベース色
	in		float4			LineCol,	// 枠線色
	in		Texture2D		TexDepth,	// 深度テクスチャ
	in		Texture2D		TexNormal,	// 法線テクスチャ
	in		SamplerState	Smpler,		// サンプラ
	in		float2			UV,			// UV
	in		float			Depth,		// ベース深度
	in		float3			Normal,		// ベース法線
	in		float2			Screen,		// 画面サイズ
	in		float			FarZ,		// Far Z
	in		float4x4		TransV		// View変換行列
) {
	float2 sp = float2( rcp( Screen.x ), rcp( Screen.y )) ;
	float2 Offset[4] = {
		float2( UV.x		, UV.y + sp.y	), // 上
		float2( UV.x		, UV.y - sp.y	), // 下
		float2( UV.x + sp.x	, UV.y			), // 右
		float2( UV.x - sp.x	, UV.y			), // 左
	} ;

	float BaseZ = Depth * FarZ ;
	float OZ = 0.0f ;

	[unroll]
	for( uint i = 0 ; i < 4 ; i++ ) {
		float DZ ;
		DZ = TexDepth.Sample( Smpler, Offset[i]).r * FarZ ;
		OZ += BaseZ - DZ ;
	}

	OZ = saturate( OZ ) ;

	float ON = 0.0f ;
	float3 BaseN = mul( TransV, float4( Normal, 1.0f )).xyz ;

	[unroll]
	for( uint i = 0 ; i < 4 ; i++ ) {
		float3 N ;
		DeferredDecodeNomal( TexNormal.Sample( Smpler, Offset[i] ), N ) ;
		N = BaseN - mul( TransV, float4( N, 1.0f )).xyz ;
		ON += N.x + N.y + N.z ;
	}

	ON = saturate( ON ) ;

	return lerp( Col, LineCol, OZ + ON ) ;
}







2023年5月15日月曜日

Point Light

ポイントライトを実装してみることにした。

ディファードレンダリングを見つけた時、このポイントライトがきっかけだった気がする。

ライトの処理は重くなりがちで、数個の制限を駆使してシーンを作らないと行けないらしい。
でもそんな面倒なことはしたくない。
ディファードレンダリングなら今までの制限を大幅に超えてライトが自由に配置できるとのこと。

HLSLの魔導書に書かれているのをアレンジして実装してみた。

ポイントライト1000個

1000個のポイントライトをランダムに配置してみた。
まあ、普通に動きそう。

更に視錐台カリングも実装してみた。

視錐台カリング


Tile Based Rendering


問題はここから。 
Deferred Renderingで何も考えずにライトを描画出来るけど、やはり1ピクセル単位ですべてのライトを処理するのは無駄すぎる。

そこで、描画時に画面を分割して、そこに含まれるライトのみを対象とすることで高速化する。

視錐台を分割する

タイル1つ分の視錐台を用意して、そこに含まれるポイントライトを調べる。

タイル内に含まれるポイントライト

あるタイルで描画対象のライトをUAVに書き出して、Deferred Renderingでそのバッファを元に対象のライトのみを描画することにより、重なっているライト数だけ処理をすれば良くなる。


この話題になると絶対に出てくるIntelのサンプルプログラム。

HLSLの魔導書でもこのプログラムの言及があり、視錐台分割部分で下記のようなコメントがある。

    // Intelのサンプルと微妙に違うのは右手系でやっているから
    // あと、Intelのサンプルは微妙に間違ってると思う

実物のソースと見比べてみても全く同じで、間違ったところが直されていると思ったがそのままだった。もしかするとIntelのソースが直されたのかと思ったがそうでもないらしい。

その根拠が、このサイト
ここによると、左面と底面の式が間違っている。

float4 c1 = float4(mtxProj._11 * tileScale.x, 0.0, tileBias.x, 0.0);
float4 c2 = float4(0.0, -mtxProj._22 * tileScale.y, tileBias.y, 0.0);
float4 c4 = float4(0.0, 0.0, 1.0, 0.0);

frustumPlanes[0] = c4 - c1; // Right
frustumPlanes[1] = c4 + c1; // Left
frustumPlanes[2] = c4 - c2; // Top
frustumPlanes[3] = c4 + c2; // Bottom

上記は間違いで、下記が正解。

float4 c1 = float4(mtxProj._11 * tileScale.x, 0.0, tileBias.x, 0.0);
float4 c2 = float4(0.0, -mtxProj._22 * tileScale.y, tileBias.y, 0.0);
float4 c4 = float4(0.0, 0.0, 1.0, 0.0);

frustumPlanes[0] = c4 - c1; // Right
frustumPlanes[1] =      c1; // Left
frustumPlanes[2] = c4 - c2; // Top
frustumPlanes[3] =      c2; // Bottom

また、このIntelのサンプルプログラムは1つのCompute Shader内で、異なる単位の処理が行われている。説明なしには理解するのは難しい。

// [ドット単位]のZを取得(dot/thread)
float2 uv = In.DTID.xy ;
float4 Pos = DepthToPos( TexDepth[uv], uv, Cam.InverseP ) ;
uint Base = ( In.GID.y * (( Cam.Screen.x + TILE_SIZE-1 ) / TILE_SIZE ) + In.GID.x ) * PLIGHT_PER_TILE ;

if( In.GI == 0 ) {
	sMinZ = asuint( Pos.z ) ;
	sMaxZ = asuint( Pos.z ) ;
	sCount = 0 ;
}

GroupMemoryBarrierWithGroupSync() ;

// [タイル(グループ)単位]の最大/最小のZを書き込む(tile/256thread)
InterlockedMin( sMinZ, asuint( Pos.z )) ;
InterlockedMax( sMaxZ, asuint( Pos.z )) ;

GroupMemoryBarrierWithGroupSync() ;

float MinZ = asfloat( sMinZ ) ;
float MaxZ = asfloat( sMaxZ ) ;

float4 Frustum[ 6 ] ;
GetTileFrustumPlane( Frustum, Cam.TransP, In.GID.xy, Cam.Screen, MinZ, MaxZ, TILE_SIZE ) ;

// [ライト単位]で視錐台に含まれるかチェック(1024lightの場合 4light/thread)
for( uint lightIndex = In.GI ; lightIndex < Cam.PLightCount ; lightIndex += TILE_SIZE * TILE_SIZE ) {
	_PointLight light = PointLight[ lightIndex ] ;
	float4 PLPos = mul( Cam.TransV, float4( light.Pos, 1.0f )) ;

	// タイルとの判定
	bool inFrustum = true ;
	[unroll]
	for( uint i = 0 ; i < 6 ; ++i ) {
		// ライトの座標と平面の法線とで内積を使って、
		// ライトと平面との距離(正負あり)を計算する
		float d = dot( Frustum[i], PLPos ) ;

		// ライトと平面の距離を使って、衝突判定を行う
		inFrustum = inFrustum && ( d >= -light.Range ) ;
	}

	// タイルと衝突している場合
	if( inFrustum ) {
		// 衝突したポイントライトの番号を影響リストに積んでいく
		uint Index ;
		InterlockedAdd( sCount, 1, Index ) ;
		if( Index >= PLIGHT_PER_TILE ) break ;	// 1タイル内の上限に達した
		Dst[ Base + Index ] = lightIndex ;
	}
}

GroupMemoryBarrierWithGroupSync() ;

if( In.GI == 0 && sCount < PLIGHT_PER_TILE ) {
	Dst[ Base + sCount ] = 0xffffffff ;
}

GroupMemoryBarrierWithGroupSync関数を呼ぶと、グループ内のスレッドがこの呼出に達するまでブロックされる。

最初のブロックでは、グループID0番だけにグループの共有メモリを初期化させる。

次のブロックでは16×16ドットのZを調べて、最小と最大深度を共有メモリに書き込む。
ここでは1ドット/1スレッド単位で処理される。

次のブロックではポイントライト情報を参照して視錐台に含まれるかをチェックする。
ポイントライトが1024個あるとしたら、4ライト/1スレッド単位で処理される。

IntelのサンプルではCompute Shader内でG-Bufferも参照してライトの計算までしていたけど、自分のプログラムではUAVにタイル別のライト配列を出力するまでとして、Defferd/Forward Rendering両方に使えるようにしてみた。

また、1タイル分のリストの大きさはライト数分持つのが普通だけど、ライト数1024として、1タイルに1024個描くような状況は殆どないはずなので、色々調整して256個にした。


Deferred Renderingでは、ライト描画を加えた処理が2~4msぐらい掛かっていたけど、TBDRだと50~100usに短縮。ただし、UAVを作るCSが35~40usかかるので、トータルでは85usから140usぐらい。




2023年5月12日金曜日

GPU Thread

今までCPU側のスレッドについて対応してきたけど、GPU側のスレッドも効率的に使いたい。

Z Prepassや影、通常のメッシュ描画については何も考えずにDrawコマンドを発行すれば並列に処理してくれる。

GPU側で並列に処理されている様子


最近までシェーダを切り替える度にコマンドリストを実行して、その処理が終わるまで待っていた。なのでコマンドリストに詰めた単位では重なって並列に処理されているけど、コマンドリストの実行自体を1フレーム内で何回も呼んで、しかもGPU側の処理が終わるまで待っていたので処理の塊の間の隙間が大きくなっていた。

CPU側のマルチスレッド対応にともなって、複数のコマンドリストを用意してやること&シェーダを跨いでまとめて実行できるようになり、GPU側の処理の並列度が上がり、処理の塊の間隔も短くなった。


ライブラリ側の描画処理はDraw関数を呼び出すと、その中でZ Prepass、影、G Buffer描画、Defferdレンダリング、Emmisiveのポストエフェクトをまとめて実行するようになっている。

サブカメラを用意して、今のシーンをメインカメラと、サブカメラからレンダリング後メインカメラの描画をワイプで画面端に描画するようにしていた。

この時、レンダリングターゲットを変えて、2回Draw関数を呼び出している。
PIXで確認すると、同じような並びの処理の塊が2回続く。

Z Prepass、影、G Buffer描画、DefferdレンダリングまではGPU側で全体を並列で処理しているような形になっている。ただその後のEmmisiveの部分で並列にはならず、その前の処理を待って次の処理が始まっている。

Drawの処理を分解して、それぞれ独立して呼べるように修正した。
そして、メインカメラ、サブカメラのZ Prepass、影、G Buffer描画、Defferdレンダリングまでを先に処理して、それぞれのEmmisiveを最後に処理するようにした。

前半の処理をまとめた結果

こうすることで、かなりの処理が並列に動いているように見えるようになった。

最後のEmissive部分も並列にできないかと、リソースを2倍にしてそれぞれ被らないようにして実行してみたけど、全く変わらなかった。
バリアを張ってしまうと、対象のリソースは関係なしに全部が止まってしまうのか?

おそらく同じ意味のバリアなら、複数同時に出来る。
例えばテクスチャコピーを2つ連続なら並列になる。
だけど、テクスチャコピーして、そのコピーが完了してミップマップを生成するとなると、全く別のリソースに対して行ってるのに、並列に行われなかった。


複数のキュー


バリアを挟む複数の操作をしてしまうと、最小単位で同じ操作をまとめていかない限り並列には処理ができなさそう。

こういった場合でも処理を並列にしたい場合は、キュー自体を複数用意すればいいのでは無いか?
例えば2人用、4人用など複数人用に別々の画面をレンダリングするような場合、1つのキューではなく複数のキューでそれぞれ処理して、最後に揃ったところで画面にレンダリングすると、何も考えずに並列処理ができそう。



2023年5月1日月曜日

DirectX12 Multithread対応の続き

以前マルチスレッド対応をしたが、今回はその続き。

マルチスレッドに対応する際、参考にしたのがマイクロソフトのサンプルだけど、このサンプルって実は特殊なのでは無いかと思った。

複数のコマンドリストを用意して、それぞれを担当するスレッドがコマンドリストにコマンドを追加していく。
これはある1シーンのためにカスタマイズされた専用のプログラムで使う分には問題ないし効果も高そう。

ただ、自分が作っている汎用的な3Dエンジンの場合、ちょっと無理があるかもしれないと思い始めた。


今までは1つのコマンドリストにシーケンシャルにコマンドを追加していた。
必要なパラメータを順番に追加していけば描画はされた。

新しくマルチスレッド対応になった場合、シングルスレッドで追加するか、マルチスレッドで追加するかを切り替えられるようにした。シングルスレッドの場合は今までと同じ使用感。
マルチスレッドにした場合、最初のコマンドリストだけで実行するコマンド、全部のコマンドリストで同じ実行が必要なコマンド、パラレルに設定できるコマンドなど呼び側でかなりいろいろ考慮が必要で、投入コマンドがかなり多くないと寧ろ遅くなる結果になっていた。


DX12 Do's And Don'ts


nVIDIAのサイトでこんなドキュメントを見つけた。
全てきちんと理解できたらかなりクオリティが高くなりそうだけど、読み取れた一部でも改良していきたい。


ExecuteCommandListsとFence


以前コマンドリストのラッパを刷新した際、それまで対応してなかったReadbackも対応した。ExecuteCommandListsを呼び出したら、その後メインスレッドorクロージャスレッドでその実行完了を待つ。Readbackデータ読み取りたい場合は、メインスレッドで待ってその後参照する。読み取り不要な場合はクロージャスレッドで終わった段階で各種コマンドの開放を行うようにしていた。この時、フェンスを立てることになるが、このフェンスがパフォーマンスに多大な影響を与えてしまう。
PIXで見るとExecuteCommandLists単位で、隙間が開いている。
これが普通なのかと思っていたけど、全くダメな作りだった。


PSOの切り替え


シェーダが同じものをまとめて描画し、ExecuteCommandListsを呼び出していた。
それしかできないと思っていた。
勘違いしていたのが、Reset関数にPSOを指定するからなんだけど、Resetのタイミングで指定するのだから切り替えにはまたResetする必要があるのだろうと。だが実は違った。
SetPipelineStateという関数があり途中から設定できる。設定できるだけでなく切り替えも出来る。
これはnVIDIAのドキュメントに書いてあるわけではないけど、読んでいるうちにそういう事が出来るのだろうと、リファレンスを調べたらあった。
SetPipelineState、Set[Graphics|Compute]RootSignatureを呼べば途中でシェーダの切り替えも出来ることを知った。


コマンドリストは15~30


コマンドリストは15~30以下と書かれていて驚いた。
これをみて今までの考え方が根本から間違っているのではないかと思った。
シェーダを切り替える度にコマンドリストも切り替えて行くといいのではないか?
同時に複数のコマンドリストを扱うのではなく、常に1つのコマンドリストに対して追加し、切り替えのタイミングで次のコマンドリストに移る。ある程度コマンドリストが溜まったらExecuteCommandListsを実行する。


ドキュメントではすべてのコマンドリストを用意してから最後にExecuteCommandListsというのもだめパターンに書かれている。5回~10回に分けて、CPUとGPUが並行処理していく状態を作るのが望ましい。


マルチスレッド対応した際、コマンドリストに投入したいデータが出揃ったタイミングで、一斉にスレッドに追加させていた。
これもマイクロソフトのマルチスレッドのサンプルを最初に見てしまった弊害。
追加する度に別スレッドで並列にコマンドリストにも追加するように作り変えた。
全部のコマンドを作っていざ実行となったときに、今まではこのタイミングから追加し始めていたのが、裏で作るのと同時にコマンドリストに追加をしているため、待ち時間が短縮できた。

描画全体の時間(デバッグ)
揃ってからスレッド実行 :0.0958秒
追加と同時にスレッド実行:0.0707秒

描画全体の時間(リリース)
揃ってからスレッド実行 :0.0642秒
追加と同時にスレッド実行:0.0398秒

ただ残念なことに、マルチスレッドにせずメインスレッドで直接コマンドリストに追加した場合は下記となり、マルチスレッドが意味ないと思わせる結果となった。
デバッグ:0.0618
リリース:0.0289
コマンドリストに追加する重たい処理が少ないケースだからか?
改良版はそこまでシングルスレッドに負けてないので、マルチスレッドが勝つケースもあると信じる。


比較


シェーダを切り替える度にExecuteCommandListsを呼び出していたのを、まとめて1回で実行したものの比較。


シェーダ切り替え毎にExecuteCommandLists実行

Z Pre-passからDeferredRenderingまでをまとめて実行

修正前は、塊の間隔が空いていて無駄に待ってしまっている。全体は343usかかっている。
修正後は、まとめた部分が並列で動くようになっており、全体は297usに短縮している。
PIXでイベント名をつける単位がExecuteCommandListsの実行単位なので、今まできれいに分類されていたのが、1まとめになってしまったけど、そんなことはどうでもいいくらいにパフォーマンスがアップした。

拡張バリアにした結果、400us位かかっていたのが、350usぐらいになって、コマンドリストの実行をまとめた結果、350usから300usぐらいになった。
後半のEmissive部分がまだまとめる余地が残っているのでもう少し削れそう。








2023年4月30日日曜日

Enhanced Barriers

新しいバリアが使えるようになったので、ライブラリを書き換えてみた。


D3D12_RESOURCE_FLAG_ALLOW_SIMULTANEOUS_ACCESS


新しいバリアの説明を読んで、このフラグの存在を知った。
勝手に状態遷移をして、その条件が複雑で分かりづらいと古いバリアの欠点として書かれていた。

今まではこのフラグに頼らず、自前でリソースの読み込みと書き込みが切り替わるポイントで、自動的にリソースバリアを挿入するようにしていた。
だけどこのフラグを使うと一部のリソースを除いて、自動的に遷移してくれるし便利そう。

新しいバリアでも使えるみたいなのでこのフラグを立ててすべてのバリア処理を消してみた。


バッファ


まずリソースの生成関数が変わり、CreatePlacedResource2になった。
以前すべてのリソースをCreateCommittedResourceから、CreatePlacedResourceに変えたけど、今回はCreatePlacedResource2に変わる。
引数の変更点は、D3D12_RESOURCE_DESCからD3D12_RESOURCE_DESC1に変わる(中身はSamplerFeedbackMipRegionが増えた)のと、初期のリソースステータスを渡していたのを、初期のD3D12_BARRIER_LAYOUTを渡すようになったこと。

D3D12_BARRIER_LAYOUTでいうと、バッファに使うのはD3D12_BARRIER_LAYOUT_UNDEFINEDでいいらしい。
前の場合は、UploadとReadbackの場合は、D3D12_RESOURCE_USAGE_GENERIC_READで、それ以外はD3D12_RESOURCE_USAGE_COPY_DESTで始めていた。

そして、D3D12_RESOURCE_FLAG_ALLOW_SIMULTANEOUS_ACCESSを指定することによって、バッファの場合は遷移を何もする必要がない。
書き込む時にD3D12_RESOURCE_STATE_GENERIC_READにして、コピーする時D3D12_RESOURCE_STATE_COPY_SOURCEにして、使うときにそれぞれのステータスにわざわざ遷移させていたけど、新しいバリアでは?それともD3D12_RESOURCE_FLAG_ALLOW_SIMULTANEOUS_ACCESSのフラグのお陰で?何もしなくて良くなった。

というわけでバッファについてはバリア処理が0になった。

テクスチャ


テクスチャの方はバッファのようにバリア0にはならなかった。
ただ、単純なファイルから読み込んだテクスチャはバッファ同様バリア0で行ける。
初期のレイアウトはD3D12_BARRIER_LAYOUT_COMMONで、後は勝手に遷移してくれる。
自分で制御しないといけないのは、D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGETとD3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCILを指定する場合で、このフラグを付ける場合はD3D12_RESOURCE_FLAG_ALLOW_SIMULTANEOUS_ACCESSが付けられない。なので、主にこのリソースについて、書き込み時とテクスチャ参照時にバリアを使うことになる。


グローバル


新しいバリアの3つ目のグローバルバリアについてはよくわからない。

Global barriers control cache flush and synchronization for all indicated resource access types in a single command queue. Global Barriers have no effect on texture layout. Global Barriers are needed to provide functionality similar to legacy NULL UAV barriers and NULL/NULL aliasing barriers.

説明にはこう書かれているが、全く理解できない。とりあえずは置いておく。


中間レンダーターゲット


中間レンダーターゲットはD3D12_BARRIER_LAYOUT_RENDER_TARGETスタートで、テクスチャとして使う場合に、D3D12_BARRIER_LAYOUT_DIRECT_QUEUE_SHADER_RESOURCEに変える。


深度バッファ


深度バッファはD3D12_BARRIER_LAYOUT_DEPTH_STENCIL_WRITEスタートで、テクスチャとして使う場合に、D3D12_BARRIER_LAYOUT_DIRECT_QUEUE_SHADER_RESOURCEに変える。


レンダーターゲット


レンダーターゲットは直接リソースを作らないけど、LayoutBeforeを設定するために初回のレイアウトはD3D12_BARRIER_LAYOUT_PRESENTを設定しておく。
書き込む際はD3D12_BARRIER_LAYOUT_RENDER_TARGETに変える。

新しくAccessBefore/AfterとSyncBefore/Afterが増えた。
この指定がいまいち理解できていないが1つわかった事がある。


実際には良くない例だけど、Presentを呼ぶ前にレンダーターゲットの遷移のみのExecuteCommandListsを実行していた。
この時、下記のように指定。
 AccessBefore=D3D12_BARRIER_ACCESS_RENDER_TARGET
 AccessAfter=D3D12_BARRIER_ACCESS_COMMON
 SyncBefore=D3D12_BARRIER_SYNC_RENDER_TARGET
 SyncAfter=D3D12_BARRIER_SYNC_ALL
特にSyncAfterは何を指定していいのか全く検討がつかない。

実行してみると、デバッグレイヤーにはこんなメッセージが出ていた。

D3D12 WARNING: ID3D12CommandQueue::ExecuteCommandLists: ExecuteCommandLists references command lists that have recorded only Barrier commands. Since there is no other GPU work to synchronize against, all barriers should use AccessAfter / AccessBefore = D3D12_BARRIER_ACCESS_NO_ACCESS and SyncBefore / SyncAfter = D3D12_BARRIER_SYNC_NONE. This information can be used as an optimization hint by some drivers. [ EXECUTION WARNING #1356: NON_OPTIMAL_BARRIER_ONLY_EXECUTE_COMMAND_LISTS]

バリアのみのコマンドリストは何も同期するものも無いからD3D12_BARRIER_ACCESS_NO_ACCESSとD3D12_BARRIER_SYNC_NONEをBefore/Afterに設定すればいいとのこと。
なるほど、今までExecuteCommandListsの枠を超えて、その前のコマンドも含めて考えないといけないのかと思っていたけど、今回の実行単位の中だけで考えればいいということがわかった。

コマンドリストの先頭で遷移させる場合はBeforeにD3D12_BARRIER_ACCESS_NO_ACCESSとD3D12_BARRIER_SYNC_NONEを設定出来るし、次の為にコマンドリストの最後でLayoutだけ変えておくような場合、AfterにD3D12_BARRIER_ACCESS_NO_ACCESSとD3D12_BARRIER_SYNC_NONEを設定する事が出来る。


UAV


テクスチャをロードした後、ミップマップを自動でつくるときにミップマップの階層分UAVを用意して、1つ上の階層を参照して、下の階層を作るというのを順番に行う。
この時PIXのヒントに出ていたのは、「UAVバリアが要らなさそうだから消してみれば?」という提案。
実際に消すと、同期が取れていないのでテクスチャがきちんと書き込まれなくなって真っ黒のテクスチャが出来上がる。
新しいバリアではUAVバリアというものが無くなったので、どうするのかと思ったらドキュメントには下記のように書かれていた。
 LayoutBefore = D3D12_BARRIER_LAYOUT_UNORDERED_ACCESS
 LayoutAfter = D3D12_BARRIER_LAYOUT_UNORDERED_ACCESS
 AccessBefore = D3D12_BARRIER_ACCESS_UNORDERED_ACCESS
 AccessAfter = D3D12_BARRIER_ACCESS_UNORDERED_ACCESS
 SyncBefore = D3D12_BARRIER_SYNC_COMPUTE_SHADING
 SyncAfter = D3D12_BARRIER_SYNC_COMPUTE_SHADING
実際にやってみると自分が使っている場面ではエラーがでて、レイアウトを両方ともCOMMONに変えたらうまくいった。
PIXで確認してみたところ、古いバリアの場合各階層ごとに待ちが発生して結構時間がかかっていたけど、新しいバリアではその待ちが消えていた。結果全体で200usぐらいかかっていた処理が150usぐらいに短縮された。新しいバリア恐るべし。


今回の修正でリソースバリアをライブラリで挿入していた処理を一切なくして、今のところ必要な場面で自分で指定する形に変えた。
もう少し新しいバリアを理解して、このまま行くのか、自動で挿入するかを見極める。


2023年4月24日月曜日

DirectX12 Agility SDK

Agility SDKをインストールはしたけど、結局使えなかった。
ID3D12Device10を指定して、D3D12CreateDeviceをしてもエラーになる。

ここを見つけて、ついにID3D12Device10のCreateに成功した。


NuGetでのインストールは簡単で、NuGetパッケージマネージャから「DirectX agility」で検索してインストールするだけ。


現在の最新は1.610.2で、1.7を使いたい場合は上部の「プレリリースを含める」をチェックしないと選択できない。

インストール後、ビルドするとexeの場所に「D3D12」というフォルダの中に、「D3D12Core.dll」と「D3D12SDKLayers.dll」が配置される。
Agility機能を使ったプログラムをリリースする場合はこの「D3D12Core.dll」を一緒に配布すると、配布した「D3D12Core.dll」を使ってくれるけど、OSのアップデートが進んでSystem32の下の「D3D12Core.dll」の方が新しくなると、新しい方の「D3D12Core.dll」を読み込むようになっているらしい。


肝心のD3D12CreateDeviceだけど、実行するとエラーになる。
実行時のコンソールを見ると、System32の方の「D3D12Core.dll」が読み込まれている。
先程のページを読み進めると、「D3D12SDKVersion」と「D3D12SDKPath」をExportする必要があると書いてある。

extern "C" { __declspec ( dllexport ) extern const UINT D3D12SDKVersion = 710 ; }
extern "C" { __declspec ( dllexport ) extern const char * D3D12SDKPath = R"(.\D3D12\)" ; }

書いてある通り宣言してみる。
でも、System32の方が読み込まれる。
宣言する場所は3DライブラリのDLLのソースに記述していたのを、Exeのソースに移すと直下のD3D12のDLLが読み込まれるようになった。

これでID3D12Device10のCreateに成功して、CheckFeatureSupportのD3D12_FEATURE_D3D12_OPTIONS12も正常に返るようになった。

ついに拡張バリアを試すときが来た。