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
のインデックスで、srcSubpass
とdstSubpass
の間には必ずdstSubpass
で指定したパスがあとに現れるような関係が定義されます -
srcStageMask
とdstStageMask
で実行依存性を定義します -
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;
}
-
sType
はVK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO
pNext
はnullptr
-
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;
}
-
sType
はVK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO
pNext
はnullptr
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;
}
-
sType
はVK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO
pNext
はnullptr
です -
renderPass
にRenderPassを指定します -
framebuffer
に対応するFramebufferを指定します -
renderArea
に描画対象となる範囲を指定します。シザー矩形との違いは、パス中のコマンドによってrenderArea
の範囲外への描画が発生してもクリップされないという点です。そのため、利用する側がシザー矩形を設定するなどして描画する範囲を超えないようにする必要があります(超えてしまうと未定義動作となります) -
clearValueCount
にクリア値の数、pClearValues
にクリア値の配列を指定します。クリア値はVkClearColorValue color
とVkClearDepthStencilValue depthStencil
のunionとなっており、用途に合わせて柔軟にクリア値を指定できるようになっています
このコマンドによりRenderPassを用いた描画を開始した場合、0番目のパスから開始するようになっています。
パスへの描画コマンドの記録が終了して次のパスへ移動する場合はvkCmdNextSubpass
を使用します。
void vkCmdNextSubpass(VkCommandBuffer commandBuffer, VkSubpassContents contents);
commandBuffer
に記録対象のコマンドバッファ、contents
にパス中のコマンドの種類を指定します。
すべてのパスへのコマンドの記録が終了してRenderPassによる描画を終了するにはvkCmdEndRenderPass
を使用します。このとき、必ず現在のパスがRenderPass中の最後のパスである必要があります(足りない分が自動でスキップされたりはしない)。
void vkCmdEndRenderPass(VkCommandBuffer commandBuffer);
commandBuffer
に記録対象のコマンドバッファを指定します。必要であれば、VkAttachmentDescription
のfinalLayout
で指定したイメージレイアウトに自動で遷移するようになります。
明日: SubpassInputの使い方と実際の使用例
SubpassInput(InputAttachment)をシェーダ側から参照する方法とこれを使用したポストエフェクト例(単純なトーンマッピング)を紹介します