Metal Best Practicesは、iOS/MacOS/tvOSのAPIであるMetalを用いた設計のベストプラクティスガイドです。
本稿では、何回かに分けてこのガイドを読み解き、コード上での実験を交えて解説していきます。
読んでそのまま理解できそうなところは飛ばしますので、原文を読みながら原文のガイドとしてご利用下さい。
また、iOSの記事なので他のOS(MacOS, tvOS)についての記載は割愛します。
他の記事の一覧は、初回記事よりご覧下さい。
Load and Store Actions(ロードと保存)
ベストプラクティス:レンダーターゲット(レンダー先)に適切なロードおよびストアアクションを設定します。
レンダリングパスの開始時のロードアクションと、終了時のストアアクションは、不要なコストを回避するために、適切なアクションを選択しましょうという話。
ロードアクション、ストアアクションの指定というのは要するに、今回レンダリングする対象にレンダリング前に前回のフレームのコンテンツを読み込んだり、クリアするかどうか(ロードアクション)、レンダリング後にコンテンツを保存するかどうか(ストアアクション)を指定する、ということです。
具体的には、ロードアクションでは、レンダーターゲットにある以前のコンテンツを、気にしない(.dontCare)/クリアする(.clear)/以前のコンテンツをロードする(.load)、のどれかを選択します。
ストアアクションでは、レンダリングパスの処理が終わったあと、レンダーターゲットの内容を、保持しない(.dontCare)/保持する(.store)/マルチサンプルの解決をして保持する(. storeAndMultisampleResolve)/マルチサンプルの解決をして破棄する(.multisampleResolve)する、のどれかを選択します。
通常、ドローアブルにレンダリングするときの内容を保持する必要があるためストアアクションは通常.storeになります。depthやstencilのレンダーターゲットの場合は、レンダリング中は値が必要ですが、レンダリング後には不要になるので一般的には.dontCareを使います。
なお、複数のレンダリングパスで使用されるレンダーターゲットは、レンダリングパス間のストアアクション、ロードアクションの組み合わせを気にする必要があります。
コードで確認してみる
コードで確認してみます。
こちらのサンプルコードを改変して、検証します。
ロードアクションに.clear
、ストアアクションに.store
を指定した場合
この指定をした場合、レンダリング前に以前のコンテンツはクリアします。レンダリング後は内容を保持しします。
まず表示するパーティクル数は負荷をかけるために100万にしておきます。
func draw(in view: MTKView) {
// 略
// パーティクルの数を100万にする
renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4, instanceCount: 1000000)
// 略
}
またレンダリングをしない領域も作りたいので、パーティクルは画面中央だけに表示するようにします。
vertex ColorInOut randomParticleVertexShader(
const device float4 *positions [[ buffer(0)]],
const device float2 *texCoords [[ buffer(1) ]],
constant Uniforms &uniforms [[buffer(2)]],
uint vid [[ vertex_id ]],
uint iid [[ instance_id ]]
) {
ColorInOut out;
float t = uniforms.time;
float4 pos = positions[vid];
float4 converted = flowDownParticle(pos, iid, t);
converted.x /= 2; // 追加
out.position = converted;
out.texCoords = texCoords[vid];
out.instanceId = iid;
return out;
}
そして今回のテーマである、loadActionを.clearに、storeActionを.storeにします。
func draw(in view: MTKView) {
guard let drawable = view.currentDrawable else {return}
let commandBuffer = metalCommandQueue.makeCommandBuffer()!
renderPassDescriptor.colorAttachments[0].texture = drawable.texture
renderPassDescriptor.colorAttachments[0].loadAction = .clear // 指定する
renderPassDescriptor.colorAttachments[0].storeAction = .store // 指定する
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(0.8, 0.7, 0.1, 1.0)
// 略
// パーティクルの数を100万にする
renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4, instanceCount: 1000000)
実行してみると、実機(iPhone 11)で、49〜50FPS出ていました。
実行イメージは次のようになります。レンダリングしていない領域は、clearColorに指定した色になっています。
ロードアクションに.dontCare
、ストアアクションに.store
を指定した場合
この指定の場合、レンダリング前に以前のコンテンツをロードすることもクリアすることもありません。レンダリング後は内容を保持しします。
先程の部分を次のように書き換えます。
renderPassDescriptor.colorAttachments[0].loadAction = .dontCare
renderPassDescriptor.colorAttachments[0].storeAction = .store
実行してみると、50〜51FPS出ています。少し早くなりましたね。
実行イメージは、レンダリングしていないは領域が黒くなりました。なお、このシェーダーは100万パーティクルをレンダリングしたあと描画をやめますが、loadActionが.dontCareの場合、描画をやめた後は未定義になるため、途中からこんな感じに画面が崩れます。
ロードアクションに.dontCare
、ストアアクションに.dontCare
を指定した場合
この指定の場合、レンダリング前に以前のコンテンツをロードすることもクリアしないし、レンダリング後も内容を保持しません。
先程の部分を次のように書き換えます。
renderPassDescriptor.colorAttachments[0].loadAction = .dontCare
renderPassDescriptor.colorAttachments[0].storeAction = .store
実行すると52〜53FPSになりました。さらに早くなりましたね。
ただ、この指定だとレンダリング後にも内容が保持されないので、画面の状態が未定義になりこのようなちらつく画面になってしまいました。
※チラつきが早いと目に悪いのでFPSを落とした動画にしています
結論
わずかな差でしたが、ロードアクション、ストアアクションで余計な処理をしないことで、パフォーマンスが上がることが確認できました。
最後に
iOSを使った3D処理やAR、ML、音声処理などの作品やサンプル、技術情報を発信しています。
作品ができたらTwitterで発信していきますのでフォローをお願いします🙏
Twitterは作品や記事のリンクを貼っています。
https://twitter.com/jugemjugemjugem
Qiitaは、iOS開発、とくにARや機械学習、グラフィックス処理、音声処理について発信しています。
https://qiita.com/TokyoYoshida
Noteでは、連載記事を書いています。
https://note.com/tokyoyoshida
Zennは機械学習が多めです。
https://zenn.dev/tokyoyoshida