2023年9月23日土曜日

Particle3 魔法陣

前回に引き続きパーティクルの拡張をする。

今回は魔法陣の演出をやってみたい。


円盤メッシュ


まずパーティクル用メッシュに用意したのは矩形メッシュ。
今回はこれに加えて、円盤のメッシュを追加しようと思う。
ただ、今まで円盤のメッシュを扱ったことがない。
やりたいことは、テクスチャの画像が円に沿ってぐるっと張り付いて、UVのU方向のスクロールが回転、V方向のスクロールが中心に吸い込まれていく感じに動いてほしい。

円盤のUVの展開について色々調べたけど全然見つからなかった。
仕方がないので自分で考えた方法がこれ。

UV展開図


円の分割数を指定すると各頂点の位置を計算する。
x = cos( i ÷ 分割数 ✕ 2π ) ※ i は0~分割数までのループカウンタ
y = sin( i ÷ 分割数 ✕ 2π )
u = i  ÷ 分割数

この図の場合、分割数3で中心に点0、UVは点0が一番上の中心。
そこから半径 (x,y) ✕ 0.5の位置に点2、(x,y) ✕ 約10%の位置に1の点を置く。
Vは点2が1、点1が上から約10%の位置。
後は分割数分ループする。


謎のエネルギー


とりあえずはメッシュにテクスチャを貼り付けて確認してみる。

セルラーノイズにフラクタルノイズを掛けたもの

まずノイズテクスチャを用意する。


円盤メッシュにテクスチャ展開

メッシュに貼り付けた結果。
なんかうまく行ってるっぽい。

マスクイメージ

上記にこのマスクを掛けて


ひねりを加えるとこうなって


色をつけたらこうなる。


最後にUVスクロールでvのマイナス方向に動かすとこんな感じになった。

シェーダ


float2 uv = In.UV + float2( -0.7 * In.UV.y, 0.0 ) ;	// ひねり
Out.Col = Tex.Sample( Sampler, uv + float2( 0.0, -Param.Time )) ;	// マイナス:外側 / プラス:内側
float mask = ( 1 - cos( In.UV.y*2*3.14159265 )) * 0.5 ;
Out.Col = pow( Out.Col, 1.25 ) * mask ;
Out.Col *= In.Col ;
Out.Emissive = Out.Col ;




魔法陣


魔法陣テクスチャ

絵かきツールで適当に画像を用意して、背景をアルファ0になるようにして保存。


シェーダ


Out.Col = Tex.Sample( Sampler, In.UV + float2( Param.Time*-(( step( 0.5,In.UV.y ) + step( 0.5,In.UV.y ) - step( 0.75,In.UV.y ))*2-1)*0.1, 0.0f )) ;
Out.Col *= float4( 0.25f, In.Col.gba ) ;
Out.Emissive = Out.Col * 2 + In.Col.r * float4(1,1,1,1) ;

3つの階層に分かれているので、中心を時計回り、真ん中を反時計回りで高速、外側も反時計回りで低速で回すようにしてみた。
あと、出現時にEmissiveを最大にして光らせるようにした。
更新関数から受け取るrの値で制御している。


魔法陣




2023年9月19日火曜日

Particle2 炎

前回パーティクルの仕組みを作ったので今回はその続き

前回からの課題は、パーティクルを追加とともにシェーダ作成も行っていたので、当たり前なんだけどシェーダ作成は切り離すことにした。


パーティクルシェーダ登録


これまでのシェーダは基本的には固定で、決められたデータを追加するとそれに応じて描画されていた。
パーティクル用のシェーダはそれぞれがカスタムシェーダで、パーティクルの種類毎にシェーダが増えていくことになる。
システムから渡される固定のリソースは、カメラ、パーティクルオブジェクトのWorldマトリックス、時間。
これに加えて前回は入れてなかったカラー、ID、インデックスも追加した。

オプションでサンプラ、テクスチャ、ピクセルシェーダソースを指定できるようにした。



炎のエフェクト


パーティクルの仕組みが出来たので、なにか作ってみようと思う。
定番の炎にチャレンジする。

よく見るのが、ノイズのテクスチャを用意してUVスクロールさせるというもの。

まず、セルラーノイズを動的に作成してテクスチャとして登録する。
パーティクルは適当に散らばせて、上昇させる。
表面にノイズテクスチャを貼り付けるとこんな感じ。

シェーダ

Out.Col = Tex.Sample( Sampler, ( In.UV * 0.5f )) ;


テクスチャを貼り付けただけ


全部同じ見た目になってるので、ランダムにテクスチャの表示位置をずらして、タイマーでUVスクロールをする。
distanceを使って、円形にくり抜く。

シェーダ

float d = 1 - distance( float2( 0.5, 0.5 ), In.UV ) ;
float2 r = rand( In.ID * 0.0001 ) ;
Out.Col = Tex.Sample( Sampler, ( In.UV * 0.5f ) + r + float2( 0.0f, Param.Time*0.3 )) ;
Out.Col.a *= step( 0.5, d ) * d ;


丸くなった


登録時に色をランダムに設定する。(赤は192~255、緑は96~159、青は64~95)
また、更新タイミングで全体を減らしていく。
アルファは時間により255~0にする。
Emissiveにも同じように書き込む。

シェーダ

float d = 1 - distance( float2( 0.5, 0.5 ), In.UV ) ;
float2 r = rand( In.ID * 0.0001 ) ;
float c = Tex.Sample( Sampler, In.UV * 0.5f + r + float2( 0.0f, Param.Time*0.3 )).r ;
Out.Col = In.Col * step( 0.5, c ) * c ;
Out.Col.a *= step( 0.5, d ) * d ;
Out.Emissive = Out.Col ;


それっぽくなった


ランダムのパラメータにIDを指定しているが、これをインデックスにすることにより、別のオブジェクトが消えたときにインデックスがずれて、テクスチャの表示する場所がワープすることにより、メラメラ感が増す。

シェーダ

float d = 1 - distance( float2( 0.5, 0.5 ), In.UV ) ;
float2 r = rand( In.Idx * 0.0001 ) ;
float c = Tex.Sample( Sampler, In.UV * 0.5f + r + float2( 0.0f, Param.Time*0.3 )).r ;
Out.Col = In.Col * step( 0.5, c ) * c ;
Out.Col.a *= step( 0.5, d ) * d ;
Out.Emissive = Out.Col ;

メラメラ増し


パフォーマンス


調子に乗って連打しまくっていると、FPSがどんどん落ちて30台まで落ちた。
5000パーティクルぐらいでこれだと、先が辛そう。

1パーティクルに1スレッドをその場で生成してそれぞれ更新処理を任せてみたけど、1スレッドだけで処理したほうが速かった。
パーティクル専用スレッドを1つ用意して、処理対象を2つに分けてメインスレッドと分担するようにしてみたら、多少マシになった。
試しにリリースビルドでやってみたら60FPSから落ちることもなく、シングルスレッドでも60FPS維持してた。速くなることは確認できてるので、パーティクル専用スレッドはそのまま採用することにする。


まとめ


初めてエフェクト作ってみたけど、意外とすごいのが出来た。
エフェクトは1つだけでなく、複数のエフェクトを重ねて1つのエフェクトを作ってるみたいで、例えば今回の炎にしても、別途煙を炎の消えたあたりからモクモク出すといいらしい。


2023年9月14日木曜日

Particle

パーティクルの仕組みを実装してみる。

いろいろ調べていくと既存のツールではやれることがいろいろありすぎて、それと同じものを作ろうと考えるとなかなか進まない。

すでに1つのメッシュに対してオブジェクトを追加すると描画する仕組み自体はあるので、その仕組みにパーティクル要素を追加して肉付けしていくことにした。


パーティクル用メッシュ


色々なメッシュ形状が必要になると思うけど、とりあえず矩形を用意。
パーティクル用のメッシュの頂点データは、x,y,z,u,vのみにした。

後々、円盤とか、円柱とか用意する予定。


パーティクル用シェーダ


とりあえずパーティクル単位でシェーダを作るようにしてみる。

VS

Out.Pos = mul( Cam.TransPV, mul( World[ In.IID ], float4( In.Pos, 1.0f ))) ;
Out.UV = In.UV ;

頂点にはWorld、View、Projectionマトリックスを掛けて、UVはそのままPSに渡す。

PS

Out.Col = float4( 1, 1, 1, 1 ) ;

固定で白を出力。


パーティクル用パラメータ


既存のツールではかなり多くのパラメータが設定できる。汎用ツールなので殆ど使われないものも含んで細かくなってしまっているのだろう。
自分の実装としては、パラメータ化はせず生成と更新の関数を渡せるようにする。
たくさん作っていくうちに傾向、パターン化ができるようになれば毎回関数を作るのではなく、ライブラリ化したものを渡せるようになるはず。


生成関数


毎フレームオブジェクトを生成して100個作ったらやめるようにする。


更新関数


毎フレーム1ずつYを増やしていって、配置された位置から100行ったところで消滅するようにする。


実行結果


パーティクル自体は、オブジェクトが0になったタイミングで消滅するようにした。


この時点での実行結果

結果を見ると、とりあえず動いているように見えるが、カメラを動かすと背面カリングが効いて消えてしまう。使い方によるが、今回のパターンではビルボードの動きをしてほしい。

以前、草をGSとHSで生やした際、ビルボードについて調べたときにViewの逆行列を利用すれば良いというのを見つけたが結局出来ず、直接計算して頂点を操作した。GSではそれで問題なかったけど、今回の場合はそうは行かないので再度チャレンジしてみる。


VS

Out.Pos = mul( Cam.TransPV, mul( World[ In.IID ], mul( Cam.InverseV, float4( In.Pos, 1.0f )))) ;
Out.UV = In.UV ;

カメラの定数バッファにViewの逆行列(Cam.InverseV)を含めた。その際、移動部分_41~_43を0にする必要がある。用途によっては回転に対しての打ち消しも必要になる。
試行錯誤した結果、上記でうまく行った。

草のときには、カメラの上下には追従してしまうと、草が全部寝てしまうのでY軸を打ち消す様に_22は1、_21、_23は0にしてたことになる。
ただ、パーティクルとして使う場合は不要なので移動のみ打ち消す。


PS

Out.Col = float4( 1, 1, 1, 0.5 ) ;

パーティクルといえばアルファブレンドで加算合成だろということで、アルファ値を0.5に修正。


実行結果


向きはカメラの方を、向くようになった。

ビルボード版

ただ、アルファブレンドがおかしい。白いオブジェクトが連なってるのでずっと白のハズなのに途切れてる部分が見える。
これは深度バッファを利用して物体の前後関係は維持しつつ、パーティクル自体は深度バッファに書き込まないようにしないといけないけど、いつも通り書き込んでしまっているために同じZのオブジェクトが描かれなくなっているせいで起こっている。

機能を拡張して、シェーダを作るときに色々パラメータ指定をできるようにする必要がある。


ラスタライザ


D3D12_GRAPHICS_PIPELINE_STATE_DESC.RasterizerStateの項目でいくつか外部から設定できるようにする。


FillMode


たまにワイヤーフレームで確認したいときがあって、ライブラリを直接変えて実行していたけど、指定できるようにする。デフォルトはD3D12_FILL_MODE_SOLID。


CullMode


これもメッシュが表示されないとき、両面描画に変えたりして確認していたけど、パーティクルやエフェクトの描画の場合種類によって両面描画する必要があったりするので、指定できるようにする。デフォルトはD3D12_CULL_MODE_BACK。


アルファブレンド


アルファブレンドについては、D3D12_GRAPHICS_PIPELINE_STATE_DESC.BlendStateを設定する必要がある。
元々アルファブレンドOn/Off自体はあったんだけど、今回いろんなブレンド方法を追加しておこう。


アルファブレンドなし


D3D12_BLEND_DESC & oBD = oGPSD.BlendState ;
oBD.AlphaToCoverageEnable = FALSE ;
oBD.IndependentBlendEnable = FALSE ;
for( tNum i = 0 ; i < D3D12_SIMULTANEOUS_RENDER_TARGET_COUNT ; i++ ) {
	auto & oRT = oBD.RenderTarget[i] ;
	oRT.BlendEnable = FALSE ;
	oRT.LogicOpEnable = FALSE ;
	oRT.SrcBlend = D3D12_BLEND_ONE ;
	oRT.DestBlend = D3D12_BLEND_ZERO ;
	oRT.BlendOp = D3D12_BLEND_OP_ADD ;
	oRT.SrcBlendAlpha = D3D12_BLEND_ONE ;
	oRT.DestBlendAlpha = D3D12_BLEND_ZERO ;
	oRT.BlendOpAlpha = D3D12_BLEND_OP_ADD ;
	oRT.LogicOp = D3D12_LOGIC_OP_NOOP ;
	oRT.RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL ;
}

アルファブレンドあり


switch( eABT ) {
case tAlphaBlendType::Normal :		// 通常ブレンド
	oRT.BlendEnable = TRUE ;
	oRT.SrcBlend = D3D12_BLEND_SRC_ALPHA ;
	oRT.DestBlend = D3D12_BLEND_INV_SRC_ALPHA ;
	break ;
case tAlphaBlendType::Add :			// 加算合成
	oRT.BlendEnable = TRUE ;
	oRT.SrcBlend = D3D12_BLEND_ONE ;
	oRT.DestBlend = D3D12_BLEND_ONE ;
	break ;
case tAlphaBlendType::AddAlpha :	// 加算半透明合成
	oRT.BlendEnable = TRUE ;
	oRT.SrcBlend = D3D12_BLEND_SRC_ALPHA ;
	oRT.DestBlend = D3D12_BLEND_ONE ;
	break ;
case tAlphaBlendType::Sub :			// 減算合成
	oRT.BlendEnable = TRUE ;
	oRT.SrcBlend = D3D12_BLEND_ZERO ;
	oRT.DestBlend = D3D12_BLEND_INV_SRC_COLOR ;
	break ;
case tAlphaBlendType::Mul :			// 乗算合成
	oRT.BlendEnable = TRUE ;
	oRT.SrcBlend = D3D12_BLEND_ZERO ;
	oRT.DestBlend = D3D12_BLEND_SRC_COLOR ;
	break ;
case tAlphaBlendType::Mul2 :		// 乗算合成2
	oRT.BlendEnable = TRUE ;
	oRT.SrcBlend = D3D12_BLEND_DEST_COLOR ;
	oRT.DestBlend = D3D12_BLEND_SRC_COLOR ;
	break ;
case tAlphaBlendType::Screen :		// スクリーン合成
	oRT.BlendEnable = TRUE ;
	oRT.SrcBlend = D3D12_BLEND_INV_DEST_COLOR ;
	oRT.DestBlend = D3D12_BLEND_ONE ;
	break ;
case tAlphaBlendType::Reverse :		// リバース
	oRT.BlendEnable = TRUE ;
	oRT.SrcBlend = D3D12_BLEND_INV_DEST_COLOR ;
	oRT.DestBlend = D3D12_BLEND_INV_SRC_COLOR ;
	break ;
}


深度バッファ


 
深度バッファについては、D3D12_GRAPHICS_PIPELINE_STATE_DESC.DepthStencilStateを設定する必要がある。


DepthWriteMask


深度バッファの書き込みをしない場合は、この値をD3D12_DEPTH_WRITE_MASK_ZEROにする。デフォルトはD3D12_DEPTH_WRITE_MASK_ALL。



シェーダコンパイル


パラメータを指定することで正しい描画になった。
調子に乗ってボタンを押しまくると、若干カクつく。
試しにパーティクル追加の時間を測ってみると、なんと追加に16~17msもかかっている。
シェーダの生成、コンパイル、PSO作成はかなり重い処理ということか。

パーティクルのシェーダはパーティクル追加とは切り離して、事前に行う必要がありそう。



とりあえず完成


PSをちょっと修正して、生成関数で各方向にランダム値を設定。更新関数で指定方向に進むように変更してみた。

それっぽい

一応それっぽいものが出来た。
後はシェーダとパーティクルの紐づけを変更しつつ、描画時に各種リソースを設定する部分ができればいろんなことができるようになる。




2023年9月6日水曜日

Radial Blur

モーションブラーについて調べていたら、このサイトを見つけて放射状ブラーのポストエフェクトの機能追加をしてみることにした。


シェーダ


float2 uv = In.UV - Param.CenterPos ;
float blur = Param.Blur * rcp( Param.Sample - 1 ) ;
float2 ofs = Param.Blur * rand( uv ) * 0.01 ;
float4 color = 0.0 ;
[unroll(16)]
for( uint i = 0 ; i < uint( Param.Sample ) ; i++ ) {
	float scale = Param.Scale + ( i * blur ) + ofs * ( Param.Sample - i ) ;
	color += Src.Sample( Sampler, uv * scale + Param.CenterPos ) ;
}
Out.Col = color / Param.Sample ;

パラメータ


Param.CenterPos


焦点。uv座標で、( 0.5, 0.5 )が中心。


Param.Blur


ぼかし強度。0.1~0.3ぐらいの範囲で使うのがよさそう。


Param.Scale


スケール。1を指定すると元画像そのままで、値を小さくすると大きくなる。
Param.Blurを大きくしていくと、画像の外側の参照が多くなるので、連動してスケールを調整すると、画像が近づくのと、外周の間延び感が消せていい感じになる。


Param.Sample


サンプリング数。8~16ぐらいの範囲で使うのがよさそう。
シェーダも「[unroll(16)]」を指定してるので、それ以上は指定不可。
Param.Blurを大きくすると間が目立つので、連動してサンプリング数も増やすといい。


ofsについて


このサイトでUnityのモーションブラーの実装について解説されており、その中でノイズを入れると少ないサンプル数でもきれいに見えるというふうに書かれていた。
そのアイデアを取り入れて、ランダム値を足すようにしてみた。


出力結果



元の画像

Blur=0.1 Sample=8 ノイズなし

Blur=0.3 Sample=16 ノイズなし

Blur=0.1 Sample=8 ノイズあり

Blur=0.3 Sample=16 ノイズあり

Blur=0.3 Sample=16 ノイズなし

Blur=0.3 Sample=16 ノイズあり

最後の2つを比べると、ノイズなしの方はアウトラインシェーダで書いた枠線が縦に何本もみえるが、ノイズありの方では縦線が目立たなくなっている。



2023年8月26日土曜日

Depth Of Field 2 (被写界深度)

以前被写界深度についてやったとき、結果がいまいちだったので再度チャレンジ。

このサイトを見つけて単純な仕組みっぽいので簡単にできそう。


準備


・出来上がりの絵にブラーを掛けて、全体をぼかした画像を用意する。
・描画位置の座標を利用するため、深度バッファを用意する。


描画


・UVから、描画位置の位置を算出する。
・元絵の色とぼかした絵の色を用意する。
・フォーカス位置からの距離によって、元絵とぼかした絵の使う割合を変える。


シェーダ

	float Depth = TexDepth.Sample( BorderSampler, In.UV ).r ;
	float3 Pos = DepthToPos( Depth, In.UV, Cam.InversePV ).xyz ;

	float4 Col  = Src.Sample( Sampler, In.UV ) ;
	float4 BCol = Blur.Sample( Sampler, In.UV ) ;

	float blur = smoothstep( Param.MinDistance, Param.MaxDistance, length( Pos - Param.FocusPos )) ;
	Out.Col = lerp( Col, BCol, blur ) ;

FocusPos

焦点を合わせる位置

MinDistance

焦点を合わせる範囲

MaxDistance

焦点が完全に合わなくなる範囲

MinDistanceとMaxDistanceを同じにすると、ある部分から焦点があっている部分と合わない部分がくっきり分かれる。


結果



前回の結果とは雲泥の差になった。

以前実装したときはミップマップの仕組みすらない状態だった。
現状はミップマップ&ぼかす仕組みがあり、それを呼び出すだけで今のシーンのぼかした画像が手に入るため、2つのテクスチャのブレンドをシェーダで書けば完成する。

昔の自分にこの手順を伝えてもすぐには作れないだろうから、手を出すタイミングも重要だな。


Outline Shader 2

前にアウトラインシェーダについて書いたけど、今回はその続き。

ディファードレンダリングの中で枠線を描くと全体に枠線が描かれてしまう為、ある特定のキャラにだけ枠線を書くとか、色を変えるとかが出来ない。

HLSLの魔導書には、別にしたい部分だけフォワードレンダリングでやれば実現できると書いてある。

やっては見たものの、色々と問題があった。


問題1:枠線がすべて描かれない

枠線が中途半端

法線のテクスチャが無いため深度バッファしか使えない。
一応枠線が描かれるが、中途半端にしか描かれない。


問題2:手前のオブジェクトに線が描かれてしまう

奥のキャラに青枠線を書いているが手前のキャラの枠線みたい

Z Prepassで作った深度バッファを元に描画しているため、他のオブジェクトと重なるような構図だと、手前のオブジェクトにも枠線が描かれてしまう。

これはフォワードレンダリング対象のみの深度バッファにすれば解決しそうだけど、複数色対応させる場合、フォワードレンダリング対象の各オブジェクトが重なったら同じことが起こってしまう。


問題3:カメラが近いと汚くなる


カメラが対象のオブジェクトに近づくと面が塗られてしまう。

カメラとオブジェクトの距離によって、パラメータを多少調整する必要がありそう。
ディファードレンダリングで枠線描いたときには、いくら近づいても問題なかったのに。



これらの問題を解決する方法を思いつかないので、個別のアウトラインは別の方法で対応したほうが良さそう。




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側で文字表示出来るようにしてしまうかを考える必要がある。