その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への値の設定
69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
TResourceArray<FVector> input_positions_RA_; FRHIResourceCreateInfo input_positions_resource_; FStructuredBufferRHIRef input_positions_buffer_; FShaderResourceViewRHIRef input_positions_SRV_; TResourceArray<float> input_scalars_RA_; FRHIResourceCreateInfo input_scalars_resource_; FStructuredBufferRHIRef input_scalars_buffer_; FShaderResourceViewRHIRef input_scalars_SRV_; TResourceArray<FVector> output_RA_; // Not necessary. FRHIResourceCreateInfo output_resource_; FStructuredBufferRHIRef output_buffer_; FUnorderedAccessViewRHIRef output_UAV_; |
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
bool ATestComputeShaderActor::InitializeInputPositions( /* input */const TArray<FVector>& input_positions) { if (input_positions.Num() == 0) { UE_LOG(LogTemp, Warning, TEXT("Error: input_positions is empty at ATestComputeShaderActor::InitializeInputPosition.")); return false; } num_input_ = input_positions.Num(); // We need to copy TArray to TResourceArray to set RHICreateStructuredBuffer. input_positions_RA_.SetNum(num_input_); FMemory::Memcpy(input_positions_RA_.GetData(), input_positions.GetData(), sizeof(FVector) * num_input_); input_positions_resource_.ResourceArray = &input_positions_RA_; // Note: In D3D11StructuredBuffer.cpp, ResourceArray->Discard() function is called, but not discarded?? input_positions_buffer_ = RHICreateStructuredBuffer(sizeof(FVector), sizeof(FVector) * num_input_, BUF_ShaderResource, input_positions_resource_); input_positions_SRV_ = RHICreateShaderResourceView(input_positions_buffer_); return true; } bool ATestComputeShaderActor::InitializeInputScalars( /* input */const TArray<float>& input_scalars) { if (input_scalars.Num() == 0) { UE_LOG(LogTemp, Warning, TEXT("Error: input_scalars is empty at ATestComputeShaderActor::InitializeInputScalar.")); return false; } if (input_scalars.Num() != num_input_) { UE_LOG(LogTemp, Warning, TEXT("Error: input_scalars and input_positions do not have the same elements at ATestComputeShaderActor::InitializeInputScalar.")); return false; } input_scalars_RA_.SetNum(num_input_); FMemory::Memcpy(input_scalars_RA_.GetData(), input_scalars.GetData(), sizeof(float) * num_input_); input_scalars_resource_.ResourceArray = &input_scalars_RA_; input_scalars_buffer_ = RHICreateStructuredBuffer(sizeof(float), sizeof(float) * num_input_, BUF_ShaderResource, input_scalars_resource_); input_scalars_SRV_ = RHICreateShaderResourceView(input_scalars_buffer_); return true; } |
121 122 123 124 125 |
// In this sample code, output_buffer_ has not input values, so what we need here is just the pointer to output_resource_. //output_RA_.SetNum(num_input_); //output_resource_.ResourceArray = &output_RA_; output_buffer_ = RHICreateStructuredBuffer(sizeof(FVector), sizeof(FVector) * num_input_, BUF_ShaderResource | BUF_UnorderedAccess, output_resource_); output_UAV_ = RHICreateUnorderedAccessView(output_buffer_, /* bool bUseUAVCounter */ false, /* bool bAppendBuffer */ false); |
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に渡しています。
TResourceArrayはTArrayを継承したクラスで、公式記事によると
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しました。
23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
void ATestComputeShaderActor::EndPlay(const EEndPlayReason::Type EndPlayReason) { // I'm not sure where is the appropriate place to call the following SafeRelease methods. // Destructor? EndPlay? BeginDestroy?? input_positions_buffer_.SafeRelease(); input_positions_SRV_.SafeRelease(); input_scalars_buffer_.SafeRelease(); input_scalars_SRV_.SafeRelease(); output_buffer_.SafeRelease(); output_UAV_.SafeRelease(); Super::EndPlay(EndPlayReason); } |
UE4 Unreal C++でデストラクタ – PaperSloth’s diaryによれば、EndPlayで良い…のでしょうか??教えて、詳しい人!
2.ComputeShaderを呼び出す
Unreal EngineのRenderingThreadで呼び出します。
130 131 132 133 134 135 136 137 138 |
ENQUEUE_UNIQUE_RENDER_COMMAND_FOURPARAMETER(CalculateCommand, ATestComputeShaderActor*, compute_shader_actor, this, const FVector, offset, offset_, const bool, yz_updated, false, TArray<FVector>*, output, &output, { compute_shader_actor->Calculate_RenderThread(offset, yz_updated, output); }); render_command_fence_.BeginFence(); render_command_fence_.Wait(); // Waits for pending fence commands to retire. |
我々が普段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内で呼び出す関数を実装します。
175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 |
void ATestComputeShaderActor::Calculate_RenderThread( /* input */const FVector xyz, const bool yz_updated, /* output */TArray<FVector>* output) { check(IsInRenderingThread()); // Get global RHI command list FRHICommandListImmediate& rhi_command_list = GRHICommandList.GetImmediateCommandList(); // Get the actual shader instance off the ShaderMap TShaderMapRef<FTestComputeShader> test_compute_shader_(shader_map); rhi_command_list.SetComputeShader(test_compute_shader_->GetComputeShader()); test_compute_shader_->SetOffsetX(rhi_command_list, xyz.X); if (yz_updated) { test_compute_shader_->SetOffsetYZ(rhi_command_list, xyz.Y, xyz.Z); } test_compute_shader_->SetInputPosition(rhi_command_list, input_positions_SRV_); test_compute_shader_->SetInputScalar(rhi_command_list, input_scalars_SRV_); test_compute_shader_->SetOutput(rhi_command_list, output_UAV_); DispatchComputeShader(rhi_command_list, *test_compute_shader_, num_input_, 1, 1); test_compute_shader_->ClearOutput(rhi_command_list); test_compute_shader_->ClearParameters(rhi_command_list); const FVector* shader_data = (const FVector*)rhi_command_list.LockStructuredBuffer(output_buffer_, 0, sizeof(FVector) * num_input_, EResourceLockMode::RLM_ReadOnly); FMemory::Memcpy(output->GetData(), shader_data, sizeof(FVector) * num_input_); // If you would like to get the partial data, (*output)[index] = *(shader_data + index) is more efficient?? //for (int32 index = 0; index < num_input_; ++index) { // (*output)[index] = *(shader_data + index); //} rhi_command_list.UnlockStructuredBuffer(output_buffer_); } |
まず、check(IsInRenderingThread());でRenderingThread内で呼び出されたかどうかを簡単にチェックできます。
Shaderとのやり取りで必須になるFRHICommandListImmediateは、GRHICommandList.GetImmediateCommandList();で取得します。これ以外の方法があるのかどうかは全く分かりません…。教えて、詳しい人!
その後、Shaderのインスタンスを取得するために
184 |
TShaderMapRef<FTestComputeShader> test_compute_shader_(shader_map); |
とします。
ここで、shader_mapはconst TShaderMap
ERHIFeatureLevel::Typeの取得方法がいまいちよくわかりませんでした。
お作法的にはBeginPlay内で
16 |
ERHIFeatureLevel::Type shader_feature_level_test = GetWorld()->Scene->GetFeatureLevel(); |
とするようなのですが、これをBeginPlayではなくコンストラクタ内で行おうとするとエラーが出ます。おそらく、UWorld内にSpawnされていないとGetWorldで何も取得できないからだと思うのですが、これではメンバ変数として持たせることが出来ません。
色々調べると、
60 |
const TShaderMap<FGlobalShaderType>* shader_map = GetGlobalShaderMap(GMaxRHIFeatureLevel); |
でも取得できるようでしたので、今回はこちらを使いました。ただ、MaxRHIFeatureLevelが現在使われているFeatureLevelと本当にいつでもイコールなのかはよくわかりませんでした…。教えて、詳しい人!
そのあとは、見ればわかると思います。
DispatchComputeShaderで計算した後は、Shader内での解放関数をしっかり呼び出してあげましょう。
最後に計算結果を取得するために、LockStructuredBufferとUnlockStructuredBufferとで挟み、計算結果をCPU側にコピーしてあげれば完成です。
ちなみにTShaderMapRef<FTestComputeShader>を毎回作るのは非効率だと思い、メンバ変数として持たせようとしたのですが、
66 67 |
//// Get the actual shader instance off the ShaderMap //TShaderMapRef<FTestComputeShader> test_compute_shader_{ shader_map }; // Note: test_compute_shader_(shader_map) causes error. |
にコメントで記載したように、()で括るとエラーになり、試しに{}にしてみるとコンパイルが通るという謎の現象に悩まされました。
Twitter上で色々な方が検証してくださったのですが、何と遂に江添さんまで登場してくださりました。
いまいちよくわからないが変数宣言と関数宣言の文法が衝突しているために関数宣言が優先されるルールに引っかかっているのかな。
— Ryou Ezoe (@EzoeRyou) 2018年8月19日
要するにクラススコープにおける
Hoge a(b);
は、戻り値の型がHogeで引数の型がbの非staticメンバー関数aであって、
型Hogeの変数aで初期化子が(b)ではない。— Ryou Ezoe (@EzoeRyou) 2018年8月19日
C++が引数名の省略を可能にしてしまったため発生した文法上の曖昧性で、C++規格では一部の曖昧な文法に対して一方に強制的に解釈することで解決している。
— Ryou Ezoe (@EzoeRyou) 2018年8月20日
詳細はTwitterでのやり取りを追って頂ければと思いますが、test_compute_shader_(shader_map);と書くと、shader_map型の引数を取る関数とみなされてしまうようです。メンバ変数として定義した時にだけ出るエラーで謎過ぎたのですが、そういうことだったのですね。
で、めでたくメンバ変数に出来たつもりだったのですが、Shaderを更新した際にコンパイルエラーになるようになってしまいました…。コンパイル時に、先にシェーダコンパイルが完了されていれば問題ないようなのですが、コンパイルの順番指定の方法がわからず、結局RenderingThread内で毎回変数定義をする形になりました。
メンバ変数としてTShaderMapRef<FTestComputeShader>があれば、UniformBufferStructも保持されるだろうと思って色々試したのですが、残念ながら保持されませんでした…。
3.まとめ
…と、長い長い道のりを経て、以下のOutput Logを表示出来るようになります。
紐解いていけば何とか書けるようにはなるものの、Unityと比較すると激しく面倒臭く、とてもやる気が起きません…。
これだけやっても、その1:イントロで愚痴ったように、計算結果をそのままメッシュの頂点情報として使ったり、パーティクルやInstanced Static Meshのトランスフォーム値に直接使ったり出来ないのですよ…。
今回をきっかけに色々なサンプルが増えればよいなと思っています。