1
0

More than 1 year has passed since last update.

【iOS】Metal Best Practicesの解説(10)レンダーコマンドエンコーダー

Posted at

Metal Best Practicesは、iOS/MacOS/tvOSのAPIであるMetalを用いた設計のベストプラクティスガイドです。

本稿では、何回かに分けてこのガイドを読み解き、コード上での実験を交えて解説していきます。
読んでそのまま理解できそうなところは飛ばしますので、原文を読みながら原文のガイドとしてご利用下さい。
また、iOSの記事なので他のOS(MacOS, tvOS)についての記載は割愛します。

他の記事の一覧は、初回記事よりご覧下さい。

Render Command Encoders (iOS and tvOS)(レンダーコマンドエンコーダー)

ベストプラクティス:可能な場合は、レンダーコマンドエンコーダーをマージします。

レンダーコマンドエンコーダーをマージできるならマージして不要なものを減らすと、パフォーマンスが向上する、という話です。

レンダーコマンドエンコーダーをマージできるかどうかは依存関係によります。大まかに書くと次のようになります。

  • 同じフレームの中で、あるレンダーコマンドエンコーダーの結果をそのまま利用しているレンダーコマンドエンコーダーがある場合は、それらをマージできる

  • あるレンダーコマンドエンコーダーの結果にサンプリングの依存関係がある場合はそれらをマージできない

本家のページには、マージするための条件が細かく記載されていましたが、概ね上の条件で考え、細かい条件を詰めるときに本家のページを参照すると良いかもしれません。

コードで確認してみる

コードで確認してみます。
今回はこちらのサンプルコードを改変して検証してみます。

仕上がりのイメージ
しあがりは、このように赤い図形と青い図形が重なるようになります。

IMG_04DB019AD2C2-1.jpeg

その1:レンダーコマンドエンコーダーを複数作成した場合

まずは、レンダーコマンドエンコーダーを複数作成した場合を作っていきます。

検証しやすいように、シェーダーには無駄なループを入れて処理を重くします。

SimpleShapeShader.metal
// 赤い図形を描くシェーダー
fragment float4 simpleShapeFragmentShader(
                    ColorInOut in [[ stage_in ]],
                    constant Uniforms &uniforms [[buffer(1)]]) {
    float3 destColor = float3(1.0, 1.0, 1.0);
    float2 position = (in.position.xy * 2.0 - uniforms.resolution.xy) / min(uniforms.resolution.x, uniforms.resolution.y);

    // 無駄にループして重くする
    for(float i = -1 ; i < 1 ; i += 0.001) {
        if (inCircle (position, float2( i, -0.1), 0.5)) {
            destColor *= float3(1.0, 0.0, 0.0);
        }
    }

    return float4(destColor, 1);
}

// 青い図形を描くシェーダー
fragment float4 simpleShapeFragmentShader2(
                    ColorInOut in [[ stage_in ]],
                    constant Uniforms &uniforms [[buffer(1)]]) {
    float3 destColor = float3(1.0, 1.0, 1.0);
    float2 position = (in.position.xy * 2.0 - uniforms.resolution.xy) / min(uniforms.resolution.x, uniforms.resolution.y);

    // 無駄にループして重くする
    for(float i = -1 ; i < 1 ; i += 0.001) {
        if (inRect(position, float2(i, -0.5), 0.25)) {
            destColor *= float3(0.0, 0.0, 1.0);
        }
    }
    // 赤い図形の上から描くので、色をつけたくない場所は何もしないようにする
    if (destColor.r == 1) {
        discard_fragment();
    }

    return float4(destColor, 1);
}

次に、レンダーコマンドエンコーダーを2つ用意します。

MultiPassRenderingMetalView
func draw(in view: MTKView) {
    let commandBuffer = metalCommandQueue.makeCommandBuffer()!

    guard let drawable = view.currentDrawable else {return}

    renderPassDescriptor1.colorAttachments[0].texture = drawable.texture

    // 1つ目のコマンドエンコーダー
    let renderEncoder1 = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor1)!

    // 赤い図形を描くシェーダーを指定したRenderPipelineState(事前に準備しておく)
    guard let renderPipeline = offScreenRenderPipeline else {fatalError()}

    renderEncoder1.setRenderPipelineState(renderPipeline)
    renderEncoder1.setVertexBuffer(vertextBuffer, offset: 0, index: 0)
    renderEncoder1.setVertexBuffer(texCordBuffer, offset: 0, index: 1)
    renderEncoder1.setFragmentTexture(texture, index: 0)
    renderEncoder1.setFragmentBytes(&uniforms, length: MemoryLayout<Uniforms>.stride, index: 1)
    renderEncoder1.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4)

    renderEncoder1.endEncoding()

    renderPassDescriptor2.colorAttachments[0].texture = drawable.texture
    // 2つ目のコマンドエンコーダーは、.loadアクションにすることで、赤い図形の上に青い図形を描くことができる
    renderPassDescriptor2.colorAttachments[0].loadAction = .load

    // 2つ目のコマンドエンコーダー
    let renderEncoder2 = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor2)!

    // 青い図形を描くシェーダーを指定したRenderPipelineState(事前に準備しておく)
    guard let renderPipeline = onScreenRenderPipeline else {fatalError()}

    renderEncoder2.setRenderPipelineState(renderPipeline)
    renderEncoder2.setVertexBuffer(vertextBuffer, offset: 0, index: 0)
    renderEncoder2.setVertexBuffer(texCordBuffer, offset: 0, index: 1)
    renderEncoder2.setFragmentTexture(texture, index: 0)
    renderEncoder2.setFragmentBytes(&uniforms, length: MemoryLayout<Uniforms>.stride, index: 1)
    renderEncoder2.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4)

    renderEncoder2.endEncoding()

    commandBuffer.present(drawable)

    commandBuffer.commit()

    commandBuffer.waitUntilCompleted()
}

このコードを実行してみます。

Metal System Traceを使って処理の状況を確認してみましょう。
このツールの使い方はこちらをご覧下さい。

描画は4FPS程度となりました。リフレッシュにかかる時間は平均すると248msでした。

image.png

処理時間のほとんどはフラグメントシェーダによるものだということがわかります。
2つのフラグメントシェーダーは別々のレンダーコマンドエンコーダーでエンコードしたので、2つのコマンドとして表示されていました。

その2:レンダーコマンドエンコーダーをマージして1つにした場合

次に、レンダーコマンドエンコーダーをマージして1つにしてみます。

MultiPassRenderingMetalView
func draw(in view: MTKView) {
    let commandBuffer = metalCommandQueue.makeCommandBuffer()!

    guard let drawable = view.currentDrawable else {return}

    renderPassDescriptor1.colorAttachments[0].texture = drawable.texture

    let renderEncoder1 = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor1)!

    // 赤い図形を描くシェーダーを指定
    guard let renderPipeline = offScreenRenderPipeline else {fatalError()}

    renderEncoder1.setRenderPipelineState(renderPipeline)
    renderEncoder1.setVertexBuffer(vertextBuffer, offset: 0, index: 0)
    renderEncoder1.setVertexBuffer(texCordBuffer, offset: 0, index: 1)
    renderEncoder1.setFragmentTexture(texture, index: 0)
    renderEncoder1.setFragmentBytes(&uniforms, length: MemoryLayout<Uniforms>.stride, index: 1)
    renderEncoder1.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4)

    // 青い図形を描くシェーダーを指定
    guard let renderPipeline = onScreenRenderPipeline else {fatalError()}

    // 同じレンダーコマンドエンコーダーに別のRenderPipelineStateを指定して描画する
    renderEncoder1.setRenderPipelineState(renderPipeline)
    renderEncoder1.setVertexBuffer(vertextBuffer, offset: 0, index: 0)
    renderEncoder1.setVertexBuffer(texCordBuffer, offset: 0, index: 1)
    renderEncoder1.setFragmentTexture(texture, index: 0)
    renderEncoder1.setFragmentBytes(&uniforms, length: MemoryLayout<Uniforms>.stride, index: 1)
    renderEncoder1.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4)

    renderEncoder1.endEncoding()

    commandBuffer.present(drawable)

    commandBuffer.commit()

    commandBuffer.waitUntilCompleted()
}

このコードを実行してMetal System Traceで確認します。

描画は4.9FPS程度となりました。リフレッシュにかかる時間は平均すると201msでした。
レンダーコマンドエンコーダーをマージすることとでパフォーマンスが上がっていることがわかります。

image.png

コマンドは1つとして表示されました。レンダーパイプラインステートを切り替えても1つのコマンドとして認識されるんですね。

結論

レンダーコマンドエンコーダーをマージすることで処理が劇的に早くなり、ベストプラクティスのとおりになることがわかりました。

今回は検証のためにあえてコマンドエンコーダー同士がマージできるような依存関係にしてみましたが、実際にコードを書いていて、このような関係を見つけるのは難しそうです。ただ、うまくマージできると大きな効果が期待できますね。

最後に

今回のコードはGPUパワーを相当使うせいか、実行したまま放置していたらiPhoneがかなり熱くなっていました。同じ処理を試す場合は注意下さい😅

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

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0