VK_KHR_dynamic_rendering チュートリアル

2 か月前に発表された動的レンダリング Vulkan 拡張機能は、07 を取り除くことを約束します と 14 オブジェクトは、私が「理解できず、あまり気にしない」Vulkan の一部でした。

ダイナミック レンダリングが登場する前に Vulkan レンダラーを作成するには、常に多くの定型的なレンダーパス コードを作成する必要がありました。これは人間工学に基づいた API ではなく、複数のサブパスや入力アタッチメントの柔軟性が必要になることも少なくありません。対照的に、 DirectX 12 API には、「レンダラーがタイルベースの遅延レンダリングである場合にパフォーマンスを向上させる」ためにのみ使用されるオプションとしてレンダー パスがあります。

最近、ash crate を使用して Rust で新しい Vulkan Renderer をゼロから作成し始めました。このピカピカの新しい動的レンダリング拡張機能を試すのは自然なことでした。この拡張機能のリソースはまだまばらで、使用に関するチュートリアルはありません。 Sascha Willems の例がありますが、動的レンダリングを自分で実装して初めて見つけました。

私は拡張機能の仕様を読んだだけで、その使用法を理解するのに十分なほど読みやすい.しかし、私はこの投稿を、この拡張機能の使用方法を示すためのよりチュートリアルスタイルのアプローチで書いています.投稿をよりアクセスしやすくするために、私は書いています. Rust バインディングの代わりにオリジナルの C-API を使用しています。 crate は C-API への単純なマッピングですが、「翻訳」プロセス中にコード スニペットに誤りがあった場合は、私に連絡してください。

初期化

34 はデバイス拡張であるため、論理デバイスを作成するときに、46 などの他のデバイス拡張で有効にする必要があります。 .

拡張機能の可用性を確認

他のすべての拡張機能と同様に、物理デバイスが 59 をサポートしているかどうかを確認できます 66経由 . 79 から取得した結果の場合 86 を含まない 、ドライバー、Vulkan SDK およびランタイムを更新する必要があります。

注意 :97 この記事の執筆時点 (2021 年 1 月) ではまだ若いので、お使いのハードウェアの最新のドライバーがまだサポートされていない可能性があります。 Nvidia カードですが、現在はそうではありません。

機能を有効にして拡張機能をロード

論理デバイスを作成する前に、 102 も追加する必要があります 拡張リストへ:

std::vector<const char*> device_extensions = {
  // ...,
  "VK_KHR_dynamic_rendering"
};

さらに、動的レンダリングは機能フラグの背後に隠されているため、110 を作成する必要があります 構造体を作成し、それを 129 に渡します 論理デバイスを作成するときのチェーン:

constexpr VkPhysicalDeviceDynamicRenderingFeaturesKHR dynamic_rendering_feature {
    .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_FEATURES_KHR,
    .dynamicRendering = VK_TRUE,
};

const VkDeviceCreateInfo device_create_info = {
    .sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,
    .pNext = &dynamic_rendering_feature,
    // ...
    .enabledExtensionCount = static_cast<unsigned int>(device_extensions.size()),
    .ppEnabledExtensionNames = device_extensions.data(),
};

C++ を使用している場合は、vk-bootstrap ライブラリを試してみることをお勧めします。これにより、初期化プロセスが少しスムーズになります。

コマンド バッファーで動的レンダリングを使用する

Vulkan レンダラーでは、コマンド バッファーの記録に次のようなコードが含まれている可能性があります。

VK_CHECK(vkBeginCommandBuffer(command_buffer, &command_buffer_begin_info));

VkRenderPassBeginInfo render_pass_begin_info = {
    // ...
};

vkCmdBeginRenderPass(command_buffer, &render_pass_begin_info, VK_SUBPASS_CONTENTS_INLINE);

// Draw calls here

vkCmdEndRenderPass(command_buffer);

VK_CHECK(vkEndCommandBuffer(command_buffer));

動的レンダリングでは、138 を置き換える必要があります 構造と 142151 呼び出します。165 を使用する代わりに 、 170 を追加します 次のような構造:

typedef struct VkRenderingInfoKHR {
    VkStructureType                        sType;
    const void*                            pNext;
    VkRenderingFlagsKHR                    flags;
    VkRect2D                               renderArea;
    uint32_t                               layerCount;
    uint32_t                               viewMask;
    uint32_t                               colorAttachmentCount;
    const VkRenderingAttachmentInfoKHR*    pColorAttachments;
    const VkRenderingAttachmentInfoKHR*    pDepthAttachment;
    const VkRenderingAttachmentInfoKHR*    pStencilAttachment;
} VkRenderingInfoKHR;

184 のようないくつかのフィールドがあることがわかります 、以前に 195 に提供されました それでも、この構造の情報の大部分は、レンダー パス作成の一部として提供されます。特に、この新しい 205 があります。 217 の代わりの構造 添付ファイルの説明:

typedef struct VkRenderingAttachmentInfoKHR {
    VkStructureType          sType;
    const void*              pNext;
    VkImageView              imageView;
    VkImageLayout            imageLayout;
    VkResolveModeFlagBits    resolveMode;
    VkImageView              resolveImageView;
    VkImageLayout            resolveImageLayout;
    VkAttachmentLoadOp       loadOp;
    VkAttachmentStoreOp      storeOp;
    VkClearValue             clearValue;
} VkRenderingAttachmentInfoKHR;

これで、レンダー パス コードを上記の構造体の使用に置き換えることができます。この変更は、レンダー パス オブジェクトに提供した情報の一部がここに移動されるため、コマンド バッファー記録にさらに多くのコードを記述することを意味します。

VK_CHECK(vkBeginCommandBuffer(command_buffer, &command_buffer_begin_info));

const VkRenderingAttachmentInfoKHR color_attachment_info {
    .sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO_KHR,
    .imageView = swapchain_image_views_[swapchain_image_index_],
    .imageLayout = VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL_KHR,
    .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR,
    .storeOp = VK_ATTACHMENT_STORE_OP_STORE,
    .clearValue = clear_value,
};

const VkRenderingInfoKHR render_info {
    .sType = VK_STRUCTURE_TYPE_RENDERING_INFO_KHR,
    .renderArea = render_area,
    .layer_count = 1,
    .colorAttachmentCount = 1,
    .pColorAttachments = &color_attachment_info,
};

vkCmdBeginRenderingKHR(command_buffer, &render_info);

// Draw calls here

vkCmdEndRenderingKHR(command_buffer);

VK_CHECK(vkEndCommandBuffer(command_buffer));

パイプラインの作成

これで、レンダー パスとフレームバッファ オブジェクトを初期化するすべてのコードをかき出すことができるようになりました。また、パイプライン オブジェクトを作成するときに、レンダー パスを指定する必要はなくなりましたが、代わりに 228 を作成する必要があります。 添付ファイル形式を指定するオブジェクト:

const VkPipelineRenderingCreateInfoKHR pipeline_rendering_create_info {
    .sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO_KHR
    .colorAttachmentCount = 1,
    .pColorAttachmentFormats = &swapchain_image_format_,
};

const VkGraphicsPipelineCreateInfo pipeline_create_info {
  // ...
  .pNext = &pipeline_rendering_create_info,
  // ...
  .renderPass = nullptr, // We no longer need a render pass
  // ...
};

画像レイアウト遷移

すべてがそれほど単純であれば、私はこの拡張機能に非常に満足していたでしょう.しかし、レンダー パス オブジェクトが何か役に立つことが判明しました.

現在のコードでは、検証レイヤーはフレームごとにこの警告を生成します:

スワップチェーン イメージは 230 にあると表示されています レイアウトですが、画像を表示するには、244 のいずれかにする必要があります または 255 .swapchain イメージのレイアウトを手動で 267 に移行できます プレゼンテーションの前に:

// draw calls here

vkCmdEndRenderingKHR(command_buffer);

const VkImageMemoryBarrier image_memory_barrier {
    .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
    .srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
    .oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
    .newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
    .image = swapchain_images[swapchain_image_index_],
    .subresourceRange = {
      .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
      .baseMipLevel = 0,
      .levelCount = 1,
      .baseArrayLayer = 0,
      .layerCount = 1,
    }
};

vkCmdPipelineBarrier(
    command_buffer,
    VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,  // srcStageMask
    BOTTOM_OF_PIPE, // dstStageMask
    0,
    0,
    nullptr,
    0,
    nullptr,
    1, // imageMemoryBarrierCount
    &image_memory_barrier // pImageMemoryBarriers
);

VK_CHECK(vkEndCommandBuffer(command_buffer));

でも今は 278 次のフレームでのレンダリングに適したレイアウトではありません。したがって、レンダリングする前に、画像を 287 に戻す必要があります。 :

VK_CHECK(vkBeginCommandBuffer(command_buffer, &command_buffer_begin_info));

const VkImageMemoryBarrier image_memory_barrier {
    .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
    .dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
    .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED,
    .newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
    .image = swapchain_images[swapchain_image_index_],
    .subresourceRange = {
      .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
      .baseMipLevel = 0,
      .levelCount = 1,
      .baseArrayLayer = 0,
      .layerCount = 1,
    }
};

vkCmdPipelineBarrier(
    command_buffer,
    VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,  // srcStageMask
    VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, // dstStageMask
    0,
    0,
    nullptr,
    0,
    nullptr,
    1, // imageMemoryBarrierCount
    &image_memory_barrier // pImageMemoryBarriers
);

// begin dynamic rendering here

// draw calls

ほとんどすべての Vulkan レンダラーには、冗長性を減らすためにこれらの画像レイアウト遷移関数のヘルパー関数がありますが、それでもすべてのパラメーターを指定するのは非常に面倒です。マスク、パイプライン ステージ マスク、およびそれに応じて変更されるレイアウト。

最後の言葉

この単純なケースでは、ダイナミック レンダリング拡張機能は、レンダー パスとフレーム バッファ オブジェクトを作成するのと同じくらい冗長に見えます。ただし、従来のレンダー パス アプローチでは同期が複雑になるのに対し、ダイナミック レンダリングはマルチパス レンダリングでより価値があることがわかります。また、将来的に何らかの方法でダイナミック レンダリングの人間工学を改善します。

謝辞

この投稿の校正と編集をしてくれた友人の Charles Giessen に特に感謝します!

この投稿が最初にリリースされた後、多くの経験豊富なグラフィックス プログラマーが貴重な洞察とフィードバックを提供してくれました.Jeremy Ong は、この投稿に関する洞察に満ちた Twitter フィードバックを提供しています。バッファは、カラー バッファとは少し異なります。彼はまた、一部のハードウェアとドライバーでは、レンダー パスによって提供される自動画像レイアウト トランジションを使用するとアーティファクトが発生し、いずれにせよ手動の画像レイアウト トランジションが唯一の方法であると指摘しました。そのため、パイプライン作成の変更に関するセクションを追加しました。また、Twitter の Timmy は、Nvidia がゲーム対応ドライバーで VK_KHR_dynamic_rendering を出荷していることを指摘しました。