C++, ComputeShader, HLSL, Unreal Engine 4

[UE4][ComputeShader][HLSL][C++]Unreal Engine 4で(RW)StructuredBufferを用いたComputeShaderを利用する(その3:実際に用いる)

その1:イントロその2:C++側シェーダクラスに続き、今回でようやく完結です。

FGlobalShaderクラスを継承したクラスをどのように呼び出すのかを見ていきましょう。

サンプルソースコード一式をGitHub上で公開しています。
https://github.com/HSeo/ComputeShaderUE419Test

今回やりたかったことはこれでした。

そこで、test_input_position, test_input_scalar, test_offset_X, OffsetYZをBluePrints上で指定できるようにC++ Actorを作ります。
このActorの中で、変数をシェーダ側に渡したり、ComputeShaderを起動したりします。

今回は説明の都合上、.hpp / .cppを適宜同時に見ていきます。

1. (RW)StructuredBufferへの値の設定

1. GPU側に渡したい配列をTResourceArrayに代入
2. そのTResourceArrayのポインタをFRHIResourceCreateInfoに渡す
3. そのFRHIResourceCreateInfoと、配列1要素の大きさ(uint32 Stride)、確保したい配列の大きさ(uint32 Size)、使い方の指定(uint32 InUsage)をRHICreateStructuredBufferに渡してFStructuredBufferRHIRefを作る
4. FRHIResourceCreateInfoにShader Resource View (SRV)またはUnordered Access View (UAV)のどちらか、或いは両方を割り当てる

という4段階になります。

サンプルではATestComputeShaderActor::InitializeInputPositions及びATestComputeShaderActor::InitializeInputScalarsにて入力値をShader Resource Viewに渡しています。
TResourceArrayTArrayを継承したクラスで、公式記事によると

A array which allocates memory which can be used for UMA rendering resources. In the dynamically bound RHI, it isn’t any different from the default array type, since none of the dynamically bound RHI implementations have UMA.

とのことなのですが、RHICreateStructuredBufferがFDynamicRHIのメンバ関数であることから考えると、dynamically bound RHIなのではないかという気がしなくもないのですが、良くわかりません…。教えて、詳しい人!

UMAはUnified Memory Architechtureのことではないかと予想しているのですが、よくわかりません…。教えて、詳しい人!

UAV且つUAVをComputeShaderでの計算結果の代入のみに使う場合(入力値が無い場合)には、TResourceArrayの処理は必要ありません。

なお、ほとんどのサンプルコードではこの4段階の処理をRenderThreadの中で行っており、FShaderResourceViewRHIRefなどをローカル変数として定義しているのですが、一度入力したら値が変わらないような場合や、滅多に値が変わらない場合にはあまりにも非効率です。
実は、今回作ったサンプルのように、メンバ変数として持つことが出来、一度値を代入してしまえばずっとその値を保持するようにすることが可能です。こちらのほうがずっと効率的ですね。

TArrayやstd::vectorの中身をTResourceArrayに渡す部分は、おそらくFMemory::Memcpyで一気にコピーするしかないのではないかと思うのですが、何とかコピーしないで先頭ポインタのみを渡してあげればOKにするようなことって出来ないのですかねぇ。
TArrayとTResouceArray、std::vectorとTResouceArray、std::vectorとTArrayとの値のやり取りの効率的な方法がいまだにわかりません…。どなたか方法ご存知でしょうか…?教えて、詳しい人!

RHICreateStructuredBufferでは、BUF_ShaderResourceやBUF_UnorderedAccessなどを正確に指定することが重要です。EBufferUsageFlagsとして定義されています。

で、FStructuredBufferRHIRef, FShaderResourceViewRHIRef, FUnorderedAccessViewRHIRefはいずれも(おそらく)手動で解放してあげる必要があり、デストラクタで行えばよいと思うのですが、Unreal EngineのActorの場合、どれがデストラクタに該当するのかイマイチよくわからず、ここではEndPlayをoverrideしました。

UE4 Unreal C++でデストラクタ – PaperSloth’s diaryによれば、EndPlayで良い…のでしょうか??教えて、詳しい人!

2.ComputeShaderを呼び出す

Unreal EngineのRenderingThreadで呼び出します。

我々が普段Unreal C++でコードを書くとき、それは普通GameThreadでの処理(のはず)です。Unreal EngineはGameThreadとRenderingThreadの大きく2つから構成されています(スレッド化したレンダリング | Unreal Engine)。
で、ComputeShaderはRenderThreadから呼び出す必要があり、そのためのマクロが
ENQUEUE_UNIQUE_RENDER_COMMAND_xxxPARAMETER
です。xxxにはONE, TWO, THREE, FOUR, FIVEのいずれかが入ります。サンプルを見て頂ければ何となくわかるかと思うのですが、RenderingThreadに渡したい型、GameThread上での変数名、RenderThread上での変数名、を指定します。
一番最初のCalculateCommandと書かれているところは、おそらく文字列なら何でも良い…ような気がしているのですがいまいちよくわかりません…。
渡したい変数が6個以上の場合は、複数の変数をまとめて構造体変数とするのがわかりやすい解決方法です。このほかにラムダ式による記述方法もあるっぽいのですが、検証していないのでよくわかりません。教えて、詳しい人!

FRenderCommandFenceは必須なのか不要なのかイマイチよくわからないのですが、RenderingThreadでの処理が終わるまで待ってくれる?もののようです(…となると必須だと思うのですが、私が調べた複数のサンプルコードでこれを使っているのは1つしかありませんでした…)。やっぱりよくわかりません。教えて、詳しい人!

で、RenderingThread内で呼び出す関数を実装します。

まず、check(IsInRenderingThread());でRenderingThread内で呼び出されたかどうかを簡単にチェックできます。
Shaderとのやり取りで必須になるFRHICommandListImmediateは、GRHICommandList.GetImmediateCommandList();で取得します。これ以外の方法があるのかどうかは全く分かりません…。教えて、詳しい人!
その後、Shaderのインスタンスを取得するために

とします。
ここで、shader_mapはconst TShaderMap*で、GetGlobalShaderMap関数を用いて取得するのですが、この関数の引数である
ERHIFeatureLevel::Typeの取得方法がいまいちよくわかりませんでした。
お作法的にはBeginPlay内で

とするようなのですが、これをBeginPlayではなくコンストラクタ内で行おうとするとエラーが出ます。おそらく、UWorld内にSpawnされていないとGetWorldで何も取得できないからだと思うのですが、これではメンバ変数として持たせることが出来ません。
色々調べると、

でも取得できるようでしたので、今回はこちらを使いました。ただ、MaxRHIFeatureLevelが現在使われているFeatureLevelと本当にいつでもイコールなのかはよくわかりませんでした…。教えて、詳しい人!

そのあとは、見ればわかると思います。

DispatchComputeShaderで計算した後は、Shader内での解放関数をしっかり呼び出してあげましょう。

最後に計算結果を取得するために、LockStructuredBufferとUnlockStructuredBufferとで挟み、計算結果をCPU側にコピーしてあげれば完成です。

ちなみにTShaderMapRef<FTestComputeShader>を毎回作るのは非効率だと思い、メンバ変数として持たせようとしたのですが、

にコメントで記載したように、()で括るとエラーになり、試しに{}にしてみるとコンパイルが通るという謎の現象に悩まされました。
Twitter上で色々な方が検証してくださったのですが、何と遂に江添さんまで登場してくださりました。

詳細はTwitterでのやり取りを追って頂ければと思いますが、test_compute_shader_(shader_map);と書くと、shader_map型の引数を取る関数とみなされてしまうようです。メンバ変数として定義した時にだけ出るエラーで謎過ぎたのですが、そういうことだったのですね。

で、めでたくメンバ変数に出来たつもりだったのですが、Shaderを更新した際にコンパイルエラーになるようになってしまいました…。コンパイル時に、先にシェーダコンパイルが完了されていれば問題ないようなのですが、コンパイルの順番指定の方法がわからず、結局RenderingThread内で毎回変数定義をする形になりました。
メンバ変数としてTShaderMapRef<FTestComputeShader>があれば、UniformBufferStructも保持されるだろうと思って色々試したのですが、残念ながら保持されませんでした…。

3.まとめ

…と、長い長い道のりを経て、以下のOutput Logを表示出来るようになります。

紐解いていけば何とか書けるようにはなるものの、Unityと比較すると激しく面倒臭く、とてもやる気が起きません…

これだけやっても、その1:イントロで愚痴ったように、計算結果をそのままメッシュの頂点情報として使ったり、パーティクルやInstanced Static Meshのトランスフォーム値に直接使ったり出来ないのですよ…。

今回をきっかけに色々なサンプルが増えればよいなと思っています。

コメントを残す

メールアドレスが公開されることはありません。