Edited at

RenderPassを攻略する

Vulkanを扱う上で最も難解な要素とされるRenderPassをとりあえず一通り使えるくらいな感じで解説していきます。

具体的な用法とかは明日


対象

完全な初学者が対象ではなく、書かれている内容はある程度コンピュータグラフィックスプログラミングに精通している方を対象として書かれています。少なくともDirectX9またはDirectX11で一通り動きのあるものを作成できる程度の知識が必要です(OpenGLではスワップチェーンの概念が存在しないため若干躓くところがあるかと思います)。


概要

RenderPassとはVulkanで初めて採用された概念で、簡単にまとめてしまうと「利用される/生成されるリソースを処理ごとに定義してそれらの依存性をまとめたもの」となります。

CG全般においては最終的に画面に何かを表示することが目的となりますが、「画面に何かを表示する」ということは「スワップチェーンに関連付けられたVRAM領域にデータを生成する」というように言い換えることができるかと思います。この時「スワップチェーンに関連付けられたVRAM領域」を生成されるリソース、「画面に何かを表示する」ための「描画コマンドの列」を処理として、前者の生成されるリソースをRenderPass(とFramebuffer)を使って定義するといったようになります。

RenderPassにおいて、「処理」のことをSubpassと呼び、「利用、または生成されるリソース」のことをAttachmentと呼びます。依存性についてはそのままDependencyとなります。


利点

なぜこのような、必要なのかそうでないのかよくわからない構造が必要なのか?

RenderPassとは、見方を変えると処理間の依存性を実行に先立ってドライバに知らせるためのものであるとも考えることができます。一般的なコンパイラがそうであるように、実行前に予め定義することができると様々な最適化を施すことが可能になります。

その一つとして、Vulkanでは「一つ前の処理が全てのピクセルに対して完了するより前に次の処理を行うようにする」ということができるようになっています(ユーザーによる指定が必要ですが)。また、生成したリソースをいつまで保持しておくかなども指定できるようになっており、メモリアクセス効率を改善することもできるようになっています(場合による)。

ただし、この利点を最大限活用できるようにするにはプログラマに深い知識が必要なのと、アプリケーション全体の仕様がかなり決まっている必要があります。後者の理由があるためVulkanをプロトタイプ制作に使うのはなかなかおすすめしにくいような感じがします(とかいいつつ私は使ってるんですけど)。


Tile-based Renderingについて

RenderPassを上手に使うと題名の「Tile-based Rendering」というものを最大限活用できるようになります。これは何かというと、「画面をタイル状に分割してその上でシェーディングを行う」といったGPUの実装手法で、PowerVRをはじめモバイル環境では定番の手法となっています。デスクトップ向けGPUでも、NVIDIA GeForceシリーズであればMaxwellアーキテクチャからはこの手法を採用しており、モバイル同様の効果を見込むことができます。

この手法を使うと、(必要がなければ)最小限のメモリだけで済むということとメモリ帯域を大幅に削減できるといったメリットがあります。後者は特にモバイル環境では電池の持ちに関わるので重要です。


ユースケース

RenderPassを効果的に使える用途として、「同じピクセルに対して処理を行う」といったようなものがあげられます。例えば固定値で値をクリップする簡易トーンマップやネガポジエフェクトなどはRenderPassを使うことで効率よく処理することができるようになります。逆にそれ以外のポストエフェクトなどはRenderPassを使っても効果が出ないというか、RenderPassの制限として「同一のRenderPass内において、アタッチメントとして使用されているリソースをアタッチメント以外(テクスチャなど)として使用するべきではない」というものがあるため従来通りのポストエフェクト手法をそのまま使うことになります。


RenderPassによる依存性の記述

本日のメインテーマです。

詳細についてはAMDによるGDC2016での講演資料(http://32ipi028l5q82yhj72224m8j.wpengine.netdna-cdn.com/wp-content/uploads/2016/03/VulkanFastPaths.pdf )が一番参考になるかと思います。ここではこれを参考に噛み砕いて解説します。

SubPass間の依存性は一種の有向グラフとして表すことが可能です。参考資料では複数回に分けて解説がされていますが、RenderPassを使うとクリアのタイミングを制御できたり(参考資料中clear)、メモリバリアの変更や条件によりメインVRAMに書き戻しが発生する(参考資料中flush)ためそれの制御を逐次はさみながらの解説となっています。clearおよびflushはドライバ内部での処理なのでAPIには現れていません(別の形で現れています)。

依存性を記述する前に、まずは対象となるリソース(アタッチメント)の詳細を記述します。これはVkAttachmentDescription構造体を使います。

struct VkAttachmentDescription

{
VkAttachmentDescriptionFlags flags;
VkFormat format;
VkSampleCountFlagBits samples;
VkAttachmentLoadOp loadOp;
VkAttachmentStoreOp storeOp;
VkAttachmentLoadOp stencilLoadOp;
VkAttachmentStoreOp stencilStoreOp;
VkImageLayout initialLayout;
VkImageLayout finalLayout;
}



  • flagsには、リソースメモリがエイリアスされているかどうか(VK_ATTACHMENT_DESCRIPTION_MAY_ALIAS_BITまたは0)を指定します


  • formatにはリソースのピクセルフォーマットを指定します


  • samplesにはリソースのサンプル数を指定します


  • loadOpには、パスの開始時にデータをどのようにするかを指定します



    • VK_ATTACHMENT_LOAD_OP_LOADですでにあるデータをそのまま使用するようにします


    • VK_ATTACHMENT_LOAD_OP_CLEARですでにあるデータを特定の値で消去します


    • VK_ATTACHMENT_LOAD_OP_DONT_CAREでドライバやハードウェアに任せるようにします




  • storeOpには、パスの終了時にデータをメモリに保存するのかを指定します



    • VK_ATTACHMENT_STORE_OP_STOREでメモリに保存するようになります


    • VK_ATTACHMENT_STORE_OP_DONT_CAREでドライバやハードウェアに任せるようにします




  • stencilLoadOp stencilStoreOpは上記2つのステンシル版です


  • initialLayoutにはRenderPassの開始時にリソースがあるべきイメージレイアウトを指定します


  • finalLayoutにはRenderPassの終了時にリソースがなるイメージレイアウトを指定します。initialLayoutはRenderPassの開始と同時に自動的に遷移したりはしませんが、finalLayoutはRenderPassの終了時に自動的に遷移するようになります。

次に「処理」の詳細を記述します。これはVkSubpassDescription構造体を使います。

struct VkSubpassDescription

{
VkSubpassDescriptionFlags flags;
VkPipelineBindPoint pipelineBindPoint;
uint32_t inputAttachmentCount;
const VkAttachmentReference* pInputAttachments;
uint32_t colorAttachmentCount;
const VkAttachmentReference* pColorAttachments;
const VkAttachmentReference* pResolveAttachments;
const VkAttachmentReference* pDepthStencilAttachment;
uint32_t preserveAttachmentCount;
const uint32_t* pPreserveAttachments;
}



  • flagsは現在は使わないので0


  • pipelineBindPointには計算パス(Compute)なのか描画パス(Graphics)なのかを指定するのですが、現在は後者しか対応していません


  • inputAttachmentCountに入力アタッチメントの数、pInputAttachmentsに入力アタッチメントへの参照の配列を指定します。入力アタッチメントは条件付きでシェーダから参照できるテクスチャのようなもので、他パスの出力を取り込んで加工するといったようなパスで使用します


  • colorAttachmentCountに色アタッチメントの数、pColorAttachmentsに色アタッチメントへの参照の配列を指定します。ここで指定したアタッチメントへ出力が行われることになります


  • pResolveAttachmentsには、出力した色アタッチメントをマルチサンプルして格納する先のアタッチメントの参照の配列を指定します。要素数はcolorAttachmentCountと同じでなければならず、マルチサンプルをしないでほしい場合は参照インデックスをVK_ATTACHMENT_UNUSEDに指定します。使わないのであればnullptrを指定します。


  • pDepthStencilAttachmentには深度及びステンシルとして使用するアタッチメントへの参照のポインタを指定します(ここだけ配列ではないので注意)。参照インデックスをVK_ATTACHMENT_UNUSEDにするかnullptrを渡すと使わないようになります。


  • preserveAttachmentCountに保持しておいてほしいアタッチメントの数、pPreserveAttachmentsに保持しておいてほしいアタッチメントへの参照の配列を指定します。「メモリに書き戻さない」とアタッチメントを記述する際に指定した場合、パスの終了時に生成データが消えてしまうためここで指定しないとあとのパスでピクセルを参照することができなくなります(書き戻すとした場合でもここに指定する必要があります)。

基本的にはこれだけでRenderPassを使うことができるようになるのですが、必要な場合はパス間の依存性を定義する必要があります。コマンドは定義順やパスに限らず並列に実行される場合があるため、ほかのパスの入力を使用する場合は必ず定義する必要があります。これを定義するにはVkSubpassDependency構造体を使います。

struct VkSubpassDependency

{
uint32_t srcSubpass;
uint32_t dstSubpass;
VkPipelineStageFlags srcStageMask;
VkPipelineStageFlags dstStageMask;
VkAccessFlags srcAccessMask;
VkAccessFlags dstAccessMask;
VkDependencyFlags dependencyFlags;
}



  • srcSubpassからdstSubpassへ依存性を定義します。これはVkRenderPassCreateInfo::pSubpassesのインデックスで、srcSubpassdstSubpassの間には必ずdstSubpassで指定したパスがあとに現れるような関係が定義されます


  • srcStageMaskdstStageMaskで実行依存性を定義します


  • srcAccessMaskからdstAccessMaskへメモリ依存性を定義します


  • dependencyFlagsには同期操作をタイルレベルにするかどうか(VK_DEPENDENCY_BY_REGION_BITまたは0)を指定します

最後にこの3つの構造体を一つにまとめてAPIへ渡せるようにします。VkRenderPassCreateInfo構造体を使います。

struct VkRenderPassCreateInfo

{
VkStructureType sType;
const void* pNext;
VkRenderPassCreateFlags flags;
uint32_t attachmentCount;
const VkAttachmentDescription* pAttachments;
uint32_t subpassCount;
const VkSubpassDescription* pSubpasses;
uint32_t dependencyCount;
const VkSubpassDependency* pDependencies;
}



  • sTypeVK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO pNextnullptr


  • flagsも現在は使われていないので0


  • attachmentCountにアタッチメントの数、pAttachmentsにアタッチメントの配列を指定します


  • subpassCountにパスの数、pSubpassesにパスの配列を指定します


  • dependencyCountに依存性定義の数、pDependenciesに依存性定義の配列を指定します


RenderPassに合致するFramebufferを作る

RenderPassが作成できたら、実際にリソースとRenderPassをバインドしたFramebufferを作成します。Framebufferを作成するにはvkCreateFramebufferに、必要な情報を設定したVkFramebufferCreateInfo構造体への参照を渡します。

struct VkFramebufferCreateInfo

{
VkStructureType sType;
const void* pNext;
VkFramebufferCreateFlags flags;
VkRenderPass renderPass;
uint32_t attachmentCount;
const VkImageView* pAttachments;
uint32_t width;
uint32_t height;
uint32_t layers;
}



  • sTypeVK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO pNextnullptr flagsは0を指定します


  • renderPassにベースとなるRenderPassを指定します


  • attachmentCountにアタッチメントの数、pAttachmentsにアタッチメントの配列を指定します。定義からわかるように、FramebufferのアタッチメントにはImageViewしか指定できません


  • widthはFramebufferの幅、heightはFramebufferの高さを指定します


  • layersはFramebufferのレイヤー数を指定します


RenderPassをコマンドバッファ中で使う

コマンドバッファ中で実際にRenderPassを使用してFramebufferの各アタッチメントに対して処理を記述するにはvkCmdBeginRenderPassを使います。シグネチャを以下に示します。

void vkCmdBeginRenderPass(VkCommandBufferf commandBuffer,

const VkRenderPassBeginInfo* pRenderPassBegin,
VkSubpassContents contents);

commandBufferに記録対象のコマンドバッファ、pRenderPassBeginでRenderPassの開始に関わる情報を記述した構造体への参照、そしてcontentsではパス中のコマンドの種類を指定します。パス中には「各種バインドコマンドと通常のDraw/Dispatchコマンド」か「Secondary Command Bufferの実行コマンドのみ」のどちらかしか記録することができず、それをVkSubpassContentsで指定します。前者はVK_SUBPASS_CONTENTS_INLINE、後者はVK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERSが対応します。

VkRenderPassBeginInfo構造体は以下のような形になっています。

struct VkRenderPassBeginInfo

{
VkStructureType sType;
const void* pNext;
VkRenderPass renderPass;
VkFramebuffer framebuffer;
VkRect2D renderArea;
uint32_t clearValueCount;
const VkClearValue* pClearValues;
}



  • sTypeVK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO pNextnullptrです


  • renderPassにRenderPassを指定します


  • framebufferに対応するFramebufferを指定します


  • renderAreaに描画対象となる範囲を指定します。シザー矩形との違いは、パス中のコマンドによってrenderAreaの範囲外への描画が発生してもクリップされないという点です。そのため、利用する側がシザー矩形を設定するなどして描画する範囲を超えないようにする必要があります(超えてしまうと未定義動作となります)


  • clearValueCountにクリア値の数、pClearValuesにクリア値の配列を指定します。クリア値はVkClearColorValue colorVkClearDepthStencilValue depthStencilのunionとなっており、用途に合わせて柔軟にクリア値を指定できるようになっています

このコマンドによりRenderPassを用いた描画を開始した場合、0番目のパスから開始するようになっています。

パスへの描画コマンドの記録が終了して次のパスへ移動する場合はvkCmdNextSubpassを使用します。

void vkCmdNextSubpass(VkCommandBuffer commandBuffer, VkSubpassContents contents);

commandBufferに記録対象のコマンドバッファ、contentsにパス中のコマンドの種類を指定します。

すべてのパスへのコマンドの記録が終了してRenderPassによる描画を終了するにはvkCmdEndRenderPassを使用します。このとき、必ず現在のパスがRenderPass中の最後のパスである必要があります(足りない分が自動でスキップされたりはしない)。

void vkCmdEndRenderPass(VkCommandBuffer commandBuffer);

commandBufferに記録対象のコマンドバッファを指定します。必要であれば、VkAttachmentDescriptionfinalLayoutで指定したイメージレイアウトに自動で遷移するようになります。


明日: SubpassInputの使い方と実際の使用例

SubpassInput(InputAttachment)をシェーダ側から参照する方法とこれを使用したポストエフェクト例(単純なトーンマッピング)を紹介します