没有任何秘密的 API: Vulkan* 简介第 3 部分: 第一个三角形

签署人: Pawel Lapinski

已发布:04/06/2016   最后更新时间:04/06/2016

下载  [PDF 885 KB]

Github 示例代码链接


请前往: 没有任何秘密的 API: Vulkan* 简介第 2 部分: 交换链


目录

教程 3: 第一个三角形 — 图形管道和绘制

在本教程中我们将最后在屏幕上绘制一些图形。 简单的三角形就是 Vulkan 生成一个比较好的的“图像”。

一般来说,图形管道和绘制操作要求 Vulkan 做许多准备工作(以在许多结构中填充复杂字段的形式)。 我们在很多方面都有可能犯错误,而且在 Vulkan 中,即使简单的错误也会造成应用无法按预期运行、显示空白屏幕,而且我们无法得知到达哪里出现了错误。 在这种情况下,验证层可为我们提供帮助。 但我不希望深入探讨 Vulkan API 的细节。 因此,我尽可能准备小型、简单的代码。

这样我们创建的应用就可以按照预期正常运行并显示简单的三角形,但它还使用不建议使用、不灵活,而且可能不高效(尽管正确)的机制。 我不想讨论不建议使用的情况,但它可显著简化教程,并支持我们仅专注于所需的最小 API 用法集。 只要遇到了“有争议的”功能,我都会指出来。 在下一教程中,我将介绍一些绘制三角形的推荐方法。

要绘制第一个简单的三角形,我们需创建渲染通道、帧缓冲器和图形管道。 当然也需要命令缓冲器,但我们已对其有所了解。 我们将创建简单的 GLSL 着色器,并将其编译成 Khronos 的 SPIR*-V 语言 — Vulkan(官方)理解着色器的(目前)唯一形式。

如果您的电脑屏幕没有显示任何内容,请尝试尽可能地简化代码,或者回到教程 2。 检查命令缓冲区是否按预期仅清空了图形行为,而且图形清空的颜色是否显示在屏幕上。 如果是,请通过本教程修改代码并添加一些部分。 如果不是 VK_SUCCESS,请检查每个返回值。 如果这些方法没有用,请等待本教程的验证层。

关于源代码示例

为方便本教程及其随后的教程,我更换了示例项目。 之前教程中介绍的 Vulkan 准备阶段放在单独文件(标头和源)的“VulkanCommon”类。 面向特定教程的类负责演示特定教程中介绍的主题、承袭“VulkanCommon”类并访问(所需)的 Vulkan,比如设备或交换链。 这样我们可以重新使用 Vulkan 创建代码,并准备仅专注于已演示主题的较小的类。 之前章节的代码能够正常运行,因此比较容易找出潜在错误。

我还为部分实用程序函数添加了独立的文件集。 此处我们将通过二进制文件读取 SPIR-V 着色器,因此我添加了一个函数,可检查二进制文件内容的加载。 它位于 Tools.cpp 和 Tools.h 文件。

创建渲染通道

为在屏幕上进行绘制,我们需要一个图形管道。 但现在创建这一管道需要其他结构的指示器,其中还可能需要另外其他结构的指示器。 因此我们从渲染通道开始。

什么是渲染通道? 常见图片可为我们提供一个用于许多常用渲染技巧(比如延迟着色)的“逻辑”渲染通道。 该技巧包含许多子通道。 第一个子通道使用填充 G-Buffer 的着色器绘制几何图形:将漫射颜色保存在一种纹理中,将标准矢量保存在另一纹理中、将亮度保存在另一纹理中,而将深度(位置)保存在另一纹理中。 接下来是各个光源,执行的绘制包括读取数据(标准矢量、亮度、深度/位置)、计算照明,并将其保存在另一纹理中。 最后一个通道整合照明数据和漫射颜色。 这只是有关延期着色的(粗略)解释,但它介绍了渲染通道 — 执行部分绘制操作所需的数据集:将数据保存在纹理中,并从其他纹理读取数据。

在 Vulkan 中,渲染通道代表(或描述)执行绘制操作所需的帧缓冲区附件(图像)集,以及排列绘制操作的子通道集合。 它是一种收集所有颜色、深度与模板附件,以及操作的构造,对它们进行修改后,驱动程序无需自己推断这种信息,从而为部分 GPU 提供了重要的优化机会。 子通道包含使用(或多或少)相同附件的绘制操作。 每种绘制操作都从部分输入附件读取数据,并将数据渲染至其他(颜色、深度、模板)附件。 渲染通道还描述这些附件之间的相关性:我们在一个子通道中渲染纹理,而在另一子通道中该纹理将用作数据源(即通过其进行采样)。 所有这些数据都可帮助图形硬件优化绘制操作。

为在 Vulkan 中创建渲染通道,我们调用 vkCreateRenderPass() 函数,它要求具有结构指示器,该结构描述所有涉及渲染的附件和所有形成渲染通道的子通道。 像往常一样,使用的附件和子通道越多,所需的包含相应字段结构的阵列要素越多。 在这一简单示例中,我们仅通过单个通道绘制到单个纹理(颜色附件)。

渲染通道附件描述

VkAttachmentDescription attachment_descriptions[] = {
  {
    0,                                   // VkAttachmentDescriptionFlags   flags
    GetSwapChain().Format,               // VkFormat                       format
    VK_SAMPLE_COUNT_1_BIT,               // VkSampleCountFlagBits          samples
    VK_ATTACHMENT_LOAD_OP_CLEAR,         // VkAttachmentLoadOp             loadOp
    VK_ATTACHMENT_STORE_OP_STORE,        // VkAttachmentStoreOp            storeOp
    VK_ATTACHMENT_LOAD_OP_DONT_CARE,     // VkAttachmentLoadOp             stencilLoadOp
    VK_ATTACHMENT_STORE_OP_DONT_CARE,    // VkAttachmentStoreOp            stencilStoreOp
    VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,     // VkImageLayout                  initialLayout;
    VK_IMAGE_LAYOUT_PRESENT_SRC_KHR      // VkImageLayout                  finalLayout
  }
};

1.Tutorial03.cpp,函数 reateRenderPass()

为创建渲染通道,我们首先准备一个阵列,其中包含描述各个附件(无论附件类型)及其在渲染通道中的用法的要素。 该阵列要素的类型为 VkAttachmentDescription 并包含以下字段:

  • flags –描述附件的其他属性。 目前仅别名标记可用,它告知驱动程序附件与其他附件共享相同物理内存;这种情况不适用此处,因此我们将该参数设置为零。
  • format –用于附件的图像格式;此处我们直接渲染至交换链,因此需要采用这种格式。
  • samples – 图像样本数量;我们此处不使用任何多点采用,因此仅使用一个样本。
  • loadOp – 指定如何处理渲染通道开头的图像内容,是希望清空、保存,还是不管这些内容(我们将覆写这些内容)。 此处我们希望将图像清空至指定的值。 该参数还表示深度/模板图像的深度部分。
  • storeOp –告知驱动程序如何处理渲染通道后的图像内容(最后一次使用图像的子通道之后)。 此处我们希望在渲染通道后保存图像内容,因为我们想在屏幕上显示这些内容。 该参数还表示深度/模板图像的深度部分。
  • stencilLoadOp – 与 loadOp 相同,但面向深度/模板图像的模板部分;对颜色附件来说它已被忽略。
  • stencilStoreOp – 与 storeOp 相同,但面向深度/模板图像的模板部分;对颜色附件来说该参数已被忽略。
  • initialLayout – 渲染通道启动时特定附件将呈现的布局(应用为布局图像提供的内容)。
  • finalLayout – 渲染通道结束后驱动程序自动将特定图像过渡至的布局。

还需要一些其他信息亿完成加载和保存操作,以及初始和最终布局。

加载选项指渲染通道开头的附件内容。 该操作描述图形硬件如何处理附件:清空、在现有内容上操作(不触碰内容),或者不管它们,因为应用打算覆写这些内容。 这样硬件将有机会优化内存操作。 例如,如果我们希望覆写这些内容,硬件将不会打扰这些内容,而且如果速度加快,可能为附件分配所有新内存。

保存选项,顾名思义,用在渲染通道结尾部分,告知硬件我们是希望在渲染通道后使用附件内容,还是不关心并有可能舍弃这些内容。 在一些场景中(舍弃这些内容时)它将支持硬件在临时快速内存中创建图像,因为图像将仅在渲染通道期间“活跃”,而且实施操作可能会节省一些内存带宽,以避免在不需要的时候回写数据。

如果附件有深度格式(并可能有模板组件),加载和保存选项仅表示深度组件。 如果出现模板,将以模板加载和保存选项描述的方式处理模板值。 模板选项与颜色附件无关。

我们交换链教程中介绍过,布局指图像内部内存的安排形式。 图像数据整理后,相邻“图像像素”也是内存中的邻居,这样图像用作数据源时(即在纹理采样期间),可提高缓存命中率(加快内存读取速度)。 如果图像用作绘制操作的对象,并不一定要执行高速缓存,而且可能以完全不同的方法来整理用于该图像的内存。 图像可能呈现线性布局(支持 CPU 读取或填充图像的内存内容),也可能呈现最佳布局(面向性能优化,但依然依赖硬件/厂商)。 因此一些硬件可能针对一些操作类型有特定的内存组织形式;其他硬件可能适用于任何操作类型。 部分内存布局可能更适合预期的图像“用法”。 或从另一角度来说,部分用法可能要求特定的内存布局。 同时也存在一种通用布局,兼容所有操作类型。 但从性能的角度来说,最好设置符合预期图像用法的布局,而且应用负责将布局过渡告知驱动程序。

可使用图像内存壁垒更改图像布局。 我们在交换链教程中这样做过,首先将布局演示源(演示引擎使用的图像)改成转移目标(希望使用特定颜色清空图像)。 但布局与图像内存壁垒不同,也可由渲染通道内的硬件自动更改。 如果我们指定不同的初始布局、子通道布局(稍后介绍)和最终布局,硬件将在适当时自动过渡。

初始布局将应用为特定附件“提供”(或“保留”)的布局告知硬件。 图像在渲染通道开头开始呈现这种布局(在本示例中,我们从演示引擎获取图像,因此图像呈现“演示源”布局)。 渲染通道的每个子通道都使用不同的布局,子通道之间的硬件自动进行过渡。 最终布局是特定附件将在渲染通道结尾时(渲染通道完成后)(自动)过渡至的布局。

必须为将用于渲染通道的每个附件准备这类信息。 图形硬件收到此类信息后,可能会在渲染通道期间优化操作和内存,以实现最佳性能。

子通道描述

VkAttachmentReference color_attachment_references[] = {
  {
    0,                                          // uint32_t                       attachment
    VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL    // VkImageLayout                  layout
  }
};

VkSubpassDescription subpass_descriptions[] = {
  {
    0,                                          // VkSubpassDescriptionFlags      flags
    VK_PIPELINE_BIND_POINT_GRAPHICS,            // VkPipelineBindPoint            pipelineBindPoint
    0,                                          // uint32_t                       inputAttachmentCount
    nullptr,                                    // const VkAttachmentReference   *pInputAttachments
    1,                                          // uint32_t                       colorAttachmentCount
    color_attachment_references,                // const VkAttachmentReference   *pColorAttachments
    nullptr,                                    // const VkAttachmentReference   *pResolveAttachments
    nullptr,                                    // const VkAttachmentReference   *pDepthStencilAttachment
    0,                                          // uint32_t                       preserveAttachmentCount
    nullptr                                     // const uint32_t*                pPreserveAttachments
  }
};

2.Tutorial03.cpp,函数 CreateRenderPass()

接下来我们指定渲染通道将包含的子通道描述。 该步骤可通过VkSubpassDescription 结构完成,其中包含以下字段:

  • flags – 留作将来使用的参数。
  • pipelineBindPoint – 供子通道使用的管道类型(图形或计算)。 当然我们的示例使用图形管道。
  • inputAttachmentCount – pInputAttachments 阵列中的要素数量。
  • pInputAttachments – 包含描述哪些附件用作输入,并可从内部着色器读取的要素阵列。 此处我们不使用任何输入附件,因此将其设为 0。
  • colorAttachmentCount – pColorAttachments 和 pResolveAttachments 阵列中的要素数量。
  • pColorAttachments – 描述(指示)将用作颜色渲染对象(渲染图像)的附件阵列。
  • pResolveAttachments – 与颜色附件紧密相连的阵列。 该阵列的每个要素都分别对应颜色附件阵列中的一个要素;此类颜色附件将分解成特定分解附件(如果相同索引中的分解附件或整个指示器不是 null)。 该参数为可选项,而且可设为 null。
  • pDepthStencilAttachment – 将用于深度(和/或模板)数据的附件描述。 我们这里不使用深度信息,因此将其设为 null。
  • preserveAttachmentCount – pPreserveAttachments 阵列中的要素数量。
  • pPreserveAttachments – 描述将被保留的附件阵列。 如有多个子通道,并非所有通道都使用所有附件。 如果子通道不使用其中的附件,但在后续的通道中需要它们,那么我们必须在这里指定这些附件。

pInputAttachments、pColorAttachments、pResolveAttachments、pPreserveAttachments 和 pDepthStencilAttachment 参数的类型均为 VkAttachmentReference。 该结构仅包含两个字段:

  • attachment – VkRenderPassCreateInfo 的 attachment_descriptions 阵列索引。
  • layout – 附件在特定子通道期间请求(所需)的布局。 在特定通道之前,硬件将帮助自动过渡至提供的布局。

该结果包含 VkRenderPassCreateInfo 的 attachment_descriptions 阵列的参考信息(索引)。 创建渲染通道时,我们必须提供有关用于渲染通道期间的所有附件的描述。 之前创建 attachment_descriptions 阵列时,我们已在“渲染通道附件描述”部分准备了该描述。 现在它仅包含一个要素,但在高级场景中将有多个附件。 因此这种所有渲染通道附件的“通用”集合将用作参考点。 在子通道描述中,当填充 pColorAttachments 或 pDepthStencilAttachment members 成员时,我们提供这种“通用”集合的索引,像这样:从渲染通道附件提取第一个附件,并将其用作颜色附件。 该阵列的第二个附件将用于深度数据。

整个渲染通道与其子通道是独立的,因为子通道可能以不同的方式使用多个附件,即我们在一个子通道中渲染颜色附件,而在下一个子通道中读取该附件。 这样我们准备用于整个渲染通道的附件列表,同时可以指定每个附件在通道中的使用方式。 由于各子通道可能以自己独有的方式使用特定附件,因此我们必须为各子通道指定每个图像的布局。

因此指定所有子通道(包含 VkSubpassDescription 类型要素的阵列)之前,必须为用于各子通道的附件创建引用。 这就是创建 color_attachment_references 变量的目的所在。 编写纹理渲染教程时,该用法会变得更加明显。

渲染通道创建

现在我们有创建渲染通道需要的所有数据。

vkRenderPassCreateInfo render_pass_create_info = {
  VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO,    // VkStructureType                sType
  nullptr,                                      // const void                    *pNext
  0,                                            // VkRenderPassCreateFlags        flags
  1,                                            // uint32_t                       attachmentCount
  attachment_descriptions,                      // const VkAttachmentDescription *pAttachments
  1,                                            // uint32_t                       subpassCount
  subpass_descriptions,                         // const VkSubpassDescription    *pSubpasses
  0,                                            // uint32_t                       dependencyCount
  nullptr                                       // const VkSubpassDependency     *pDependencies
};

if( vkCreateRenderPass( GetDevice(), &render_pass_create_info, nullptr, &Vulkan.RenderPass ) != VK_SUCCESS ) {
  printf( "Could not create render pass!\n" );
  return false;
}

return true;

3.Tutorial03.cpp,函数 CreateRenderPass()

我们首先填充 VkRenderPassCreateInfo 结构,其中包含以下字段:

  • sType – 结构类型(此处为 VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO)。
  • pNext – 目前不使用的参数。
  • flags – 留作将来使用的参数。
  • attachmentCount – 整个渲染通道(此处仅一个)期间使用的不同附件(pAttachments 阵列中的要素)数量。
  • pAttachments – 指定所有用于渲染通道的附件阵列。
  • subpassCount – 渲染通道包含的子通道数量(以及 pSubpasses 阵列(本示例中仅一个)中的要素数量)。
  • pSubpasses – 包含所有子通道描述的阵列。
  • dependencyCount – pDependencies 阵列中的要素数量(此处为 0)。
  • pDependencies – 描述子通道配对之间相关性的阵列。 我们的子通道不多,因此不存在相关性(此处设为 null)。

相关性描述图形管道的哪些部分以怎样的方式使用内存资源。 每个子通道使用资源的方式都各不相同。 资源布局不仅仅定义它们如何使用资源。 部分子通道可能渲染图像或通过着色器图像保存数据。 其他子通道可能不使用图像,也可能在不同的图像管道阶段(即顶点或碎片)读取图像。

该信息可帮助驱动程序优化自动布局过渡,更常见的是优化子通道之间的壁垒。 仅在顶点着色器中写入图像时,等待碎片着色器执行(当前以已用图像的形式)的意义不大。 执行完所有顶点操作后,图像立即更改布局和内存访问类型,部分图形硬件甚至会开始执行后续操作(引用或读取特定图像),无需等待完成特定子通道的其他命令。 现在只需记住,相关性对性能非常重要。

现在我们已经准备了创建渲染通道需要的所有信息,可以安全地调用 vkCreateRenderPass() 函数。

创建帧缓冲器

我们创建了渲染通道。 它描述渲染通道期间使用的所有附件和子通道。 但这种描述非常抽象。 我们指定了所有附件(本示例中仅一个)的格式,并描述了子通道(同样只有一个)如何使用附件。 但我们没有指定使用哪些附件,换句话说,哪些图像将用作这些附件。 这一过程将通过帧缓冲器完成。

帧缓冲器描述供渲染通道操作的特定图像。 在 OpenGL*中,帧缓冲器是我们将渲染的纹理(附件)集。 在 Vulkan 中,该术语的意义更加广泛。 它描述渲染通道期间使用的所有纹理(附件),不仅包括即将渲染的图像(颜色和深度/模板附件),还包括用作数据源的图像(输入附件)。

渲染通道和帧缓冲器的分开为我们提高了灵活性。 特定渲染通道可用于不同的帧缓冲器,特定帧缓冲器也可用于不同的渲染通道,如果它们相互兼容,表示它们能在具有相同类型和用法的图像上以相同的方式操作。

创建帧缓冲器之前,我们必须为每个用作帧缓冲器和渲染通道附件的图像创建图像视图。 在 Vulkan 中,不仅在有帧缓冲器的情况下,一般情况下我们都不操作图像本身。 不能直接访问图像。 为此,我们使用图像视图。 图像视图代表图像,它们“包装”图像并提供其他(元)数据。

创建图像视图

在该简单应用中,我们想直接渲染交换链图像。 我们创建了包含多个图像的交换链,因此必须为每个图像创建图像视图。

const std::vector<VkImage> &swap_chain_images = GetSwapChain().Images;
Vulkan.FramebufferObjects.resize( swap_chain_images.size() );

for( size_t i = 0; i < swap_chain_images.size(); ++i ) {
  VkImageViewCreateInfo image_view_create_info = {
    VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,   // VkStructureType                sType
    nullptr,                                    // const void                    *pNext
    0,                                          // VkImageViewCreateFlags         flags
    swap_chain_images[i],                       // VkImage                        image
    VK_IMAGE_VIEW_TYPE_2D,                      // VkImageViewType                viewType
    GetSwapChain().Format,                      // VkFormat                       format
    {                                           // VkComponentMapping             components
      VK_COMPONENT_SWIZZLE_IDENTITY,              // VkComponentSwizzle             r
      VK_COMPONENT_SWIZZLE_IDENTITY,              // VkComponentSwizzle             g
      VK_COMPONENT_SWIZZLE_IDENTITY,              // VkComponentSwizzle             b
      VK_COMPONENT_SWIZZLE_IDENTITY               // VkComponentSwizzle             a
    },
    {                                           // VkImageSubresourceRange        subresourceRange
      VK_IMAGE_ASPECT_COLOR_BIT,                  // VkImageAspectFlags             aspectMask
      0,                                          // uint32_t                       baseMipLevel
      1,                                          // uint32_t                       levelCount
      0,                                          // uint32_t                       baseArrayLayer
      1                                           // uint32_t                       layerCount
    }
  };

  if( vkCreateImageView( GetDevice(), &image_view_create_info, nullptr, &Vulkan.FramebufferObjects[i].ImageView ) != VK_SUCCESS ) {
    printf( "Could not create image view for framebuffer!\n" );
    return false;
  }

4.Tutorial03.cpp, function CreateFramebuffers()

为创建图像视图,必须首先创建类型变量 VkImageViewCreateInfo。 它包含以下字段:

  • sType – 结构类型,此处应设为 VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO。
  • pNext – 通常设为 null 的参数。
  • flags – 留作将来使用的参数。
  • image – 为其创建图像视图的图像的句柄。
  • viewType – 希望创建的视图类型。 视图类型必须兼容相应的图像。  (即我们可以为包含多个阵列层的图像创建 2D 视图,也可为包含 6 个层级的 2D 图像创建 CUBE 视图)。
  • format – 图像视图的格式;必须兼容图像的格式,不能为相同的格式(即可以是不同的格式,但没像素的位数必须相同)。
  • components – 将图像组件映射到通过纹理操作返回到着色器中的顶点。 这仅适用于读取操作(采样),但既然我们将图像用作颜色附件(渲染图像),那么必须设置身份映射(R 组件为 R,G -> G 等等)或仅使用“身份”值 (VK_COMPONENT_SWIZZLE_IDENTITY)。
  • subresourceRange – 描述视图可访问的 mipmap 层和阵列层集。 如果对图像进行 mipmap 处理,我们可以指定希望渲染的特定 mipmap 层(如果有渲染对象,必须精确指定某个阵列层的某个 mipmap 层)。

大家可以看这里,我们获取所有交换链图像的句柄,并在循环内引用它们。 这样我们填充创建图像视图所需的结构,这样我们前往 vkCreateImageView() 函数。 每个与交换链一起创建的图像都进行这样的处理。

指定帧缓冲器参数

现在我们可以创建帧缓冲器。 为此我们调用 vkCreateFramebuffer() 函数。

VkFramebufferCreateInfo framebuffer_create_info = {
    VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO,  // VkStructureType                sType
    nullptr,                                    // const void                    *pNext
    0,                                          // VkFramebufferCreateFlags       flags
    Vulkan.RenderPass,                          // VkRenderPass                   renderPass
    1,                                          // uint32_t                       attachmentCount
    &Vulkan.FramebufferObjects[i].ImageView,    // const VkImageView             *pAttachments
    300,                                        // uint32_t                       width
    300,                                        // uint32_t                       height
    1                                           // uint32_t                       layers
  };

  if( vkCreateFramebuffer( GetDevice(), &framebuffer_create_info, nullptr, &Vulkan.FramebufferObjects[i].Handle ) != VK_SUCCESS ) {
    printf( "Could not create a framebuffer!\n" );
    return false;
  }
}
return true;

5.Tutorial03.cpp,函数 CreateFramebuffers()

vkCreateFramebuffer() 函数要求我们提供类型变量 VkFramebufferCreateInfo 的指示器,因此我们必须首先准备。 它包含以下字段:

  • sType – 结构类型,此处设为 VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO。
  • pNext – 大多数时候设为 null 的参数。
  • flags – 留作将来使用的参数。
  • renderPass – 帧缓冲器将兼容的渲染通道。
  • attachmentCount – 帧缓冲器中的附件(pAttachments 阵列中要素)数量。
  • pAttachments – 图像视图阵列,表示用于帧缓冲器和渲染通道的所有附件。 阵列中的要素(图像视图)对应渲染通道中的附件。
  • width – 帧缓冲器的宽度。
  • height – 帧缓冲器的高度。
  • layers – 帧缓冲器中的层数(OpenGL 借助几何图形着色器进行分层渲染,可将层级指定为将渲染哪些通过特定多边形实现光栅化的碎片)。

帧缓冲器指定哪些图像用作供渲染通道操作的附件。 可以说,它可将图像(图像视图)过渡成特定附件。 为帧缓冲器指定的图像数量必须等于为其创建帧缓冲器的渲染通道中的附件数量。 而且,每个 pAttachments 阵列的要素直接对应渲染通道描述结构中的附件。 渲染通道和帧缓冲器紧密相连,因此我们必须在帧缓冲器创建期间指定渲染通道。 不过,我们不仅可以将帧缓冲器用于指定的渲染通道,还可用于兼容某一指定通道的渲染通道。 一般来说,兼容的渲染通道必须有相同数量的附件,而且对应附件的格式和样本数量必须相同。 但图像布局(初始布局、最终布局,以及面向各子通道的布局)可能各不相同,也不涉及渲染通道兼容性。

完成创建并填充 VkFramebufferCreateInfo 结构后,我们调用 vkCreateFramebuffer() 函数。

上述代码在循环中执行。 帧缓冲器引用图像视图。 此处图像视图针对各交换链图像创建。 因此我们要为交换链图像及其视图创建帧缓冲器。 我们这样做的目的是为了简化渲染循环中调用的代码。 在正常、真实的场景中,我们不(可能)为交换链图像创建帧缓冲器。 我假设了一种更好的解决方法,即渲染单个图像(纹理),然后使用命令缓冲区将该图像的渲染结构拷贝至特定交换链图像。 这样我们就只有三个连接至交换链的简单命令缓冲区。 其他渲染命令独立于交换链,因此更加易于维护。

创建图形管道

现在我们准备好创建图形管道。 管道是逐个处理数据的阶段集合。 Vulkan 中目前有计算管道和图形管道。 计算管道支持我们执行计算工作,比如对游戏中的对象执行物理计算。 图形管道用于绘制操作。

OpenGL 中有多个可编程阶段(顶点、镶嵌、碎片着色器等)和一些固定功能阶段(光栅器、深度测试、混合等)。 Vulkan 中的情况相似。 有些阶段比较类似(如果不相同)。 但整个管道的状态聚集在一个整体对象中。 OpenGL 支持我们随时更改影响渲染操作的状态,我们(大部分时候)可以独立更改各阶段的参数。 我们可以设置着色器程序、深度测试、混合,以及希望的各种状态,然后还可以渲染一些对象。 接下来我们可以仅更改一小部分状态,并渲染另一对象。 在 Vulkan 中不能执行这类操作(可以说管道具有“免疫力”)。 我们必须准备整个状态,设置管道阶段的参数,并将它们分成管道对象组。 对我来说,一开始这是最令人震惊的信息。 我不能随时更改着色器程序? 为什么?

最简单有效的解释是,因为这种状态会改变性能影响。 更改整个管道的单个状态可能导致图形硬件执行状态、错误检查等多项后台操作。 不同的硬件厂商可能(并通常)以不同的方式实施此功能。 如果在不同的图形硬件上执行,这样会导致应用以不同的方式执行(意味着会不可预测地影响性能)。 因此对开发人员来说,能够随时更改是一项非常方便的功能。 但遗憾的是,硬件因此会不太方便。

所以在 Vulkan 中,整个管道的状态聚集在一个单个对象中。 创建管道对象时执行所有相关的状态和错误检查。 如果出现问题(比如管道的不同部分设置的不兼容),管道对象创建将失败。 但我们提前了解了这点。 驱动程序无需担心,可以放心地使用毁坏的管道。 它会立即告诉我们这一问题。 但在真正使用期间,在应用的性能关键部分,一切都要正确设置和使用。

这种方法的缺点是,如果以不同的方式(一些不透明、一些半透明,一些启用深度测试等等)绘制对象,我们必须创建多个管道对象,管道对象的多个变量。 遗憾的是,由于着色器不同,不得不创建不同的管道对象。 如果想使用不同的着色器绘制对象,还必须创建多个管道对象,逐个整合着色器程序。 着色器还连接至整个管道状态。 它们使用不同的资源(比如纹理和缓冲区)、渲染不同的颜色附件,并读取不同的附件(可能是之前已渲染过的)。 必须初始化、准备并正确设置这些连接。 我们知道我们的目的,但驱动程序不知道。 因此由我们(而非驱动程序)进行是符合逻辑的最好办法。 一般来说这种方法比较有意义。

开始管道创建流程时,先从着色器开始。

创建着色器模块

创建图形管道要求我们以结构或结构阵列的形式准备大量数据。 第一个数据是所有着色器阶段和着色器程序(在渲染期间用于绑定的特定图形管道)的集合。

在 OpenGL 中我们用 GLSL 编写着色器。 它们经过编译,然后直接链接至应用中的着色器程序。 我们可以在应用中随时使用或停止着色器程序。

而 Vulkan 仅接收着色器的二进制形式 — 一种称为 SPIR-V 的中间语言。不能像在 OpenGL 中那样提供 GLSL 代码。 但有一种官方的独立编译器能够将用 GLSL 编写的着色器转换成二进制 SPIR-V 语言。 为使用该编译器,我们必须离线操作。 准备 SPIR-V 汇编后,我们可以通过它创建着色器模块。 然后将模块合成 VkPipelineShaderStageCreateInfo 结构阵列,从而与其他参数一起用于创建图形管道。

以下代码可通过包含二进制 SPIR-V 的指定文件创建着色器模块。

const std::vector<char> code = Tools::GetBinaryFileContents( filename );
if( code.size() == 0 ) {
  return Tools::AutoDeleter<VkShaderModule, PFN_vkDestroyShaderModule>();
}

VkShaderModuleCreateInfo shader_module_create_info = {
  VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,    // VkStructureType                sType
  nullptr,                                        // const void                    *pNext
  0,                                              // VkShaderModuleCreateFlags      flags
  code.size(),                                    // size_t                         codeSize
  reinterpret_cast<const uint32_t*>(&code[0])     // const uint32_t                *pCode
};

VkShaderModule shader_module;
if( vkCreateShaderModule( GetDevice(), &shader_module_create_info, nullptr, &shader_module ) != VK_SUCCESS ) {
  printf( "Could not create shader module from a %s file!\n", filename );
  return Tools::AutoDeleter<VkShaderModule, PFN_vkDestroyShaderModule>();
}

return Tools::AutoDeleter<VkShaderModule, PFN_vkDestroyShaderModule>( shader_module, vkDestroyShaderModule, GetDevice() );

6.Tutorial03.cpp,函数 CreateShaderModule()

首先准备包含以下字段的 VkShaderModuleCreateInfo 结构:

  • sType – 结构类型,本示例中设为 VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO。
  • pNext – 目前不使用的指示器。
  • flags – 留作将来使用的参数。
  • codeSize – 传递至 pCode 参数中的代码大小(字节)。
  • pCode –包含源代码(二进制 SPIR-V 汇编)的阵列指示器。

为获取文件内容,我准备了一个简单的效用函数 GetBinaryFileContents(),可读取指定文件的所有内容。 它以字符矢量的形式返回内容。

准备该结构后,我们调用 vkCreateShaderModule() 函数并检查一切是否正常运行。

Tools 命名空间的 AutoDeleter<> 类是一种帮助类,可包装特定的 Vulkan 对象句柄,并提取用于删除该对象的函数。 该类与智能指示器类似,可在对象(智能指示器)超出范围时删除分配的内存。 AutoDeleter<> 可提取特定对象的句柄,并在这类对象的类型超出范围时运用提供的函数删除该对象。

template<class T, class F>
class AutoDeleter {
public:
  AutoDeleter() :
    Object( VK_NULL_HANDLE ),
    Deleter( nullptr ),
    Device( VK_NULL_HANDLE ) {
  }

  AutoDeleter( T object, F deleter, VkDevice device ) :
    Object( object ),
    Deleter( deleter ),
    Device( device ) {
  }

  AutoDeleter( AutoDeleter&& other ) {
    *this = std::move( other );
  }

  ~AutoDeleter() {
    if( (Object != VK_NULL_HANDLE) && (Deleter != nullptr) && (Device != VK_NULL_HANDLE) ) {
      Deleter( Device, Object, nullptr );
    }
  }

  AutoDeleter& operator=( AutoDeleter&& other ) {
    if( this != &other ) {
      Object = other.Object;
      Deleter = other.Deleter;
      Device = other.Device;
      other.Object = VK_NULL_HANDLE;
    }
    return *this;
  }

  T Get() {
    return Object;
  }

  bool operator !() const {
    return Object == VK_NULL_HANDLE;
  }

private:
  AutoDeleter( const AutoDeleter& );
  AutoDeleter& operator=( const AutoDeleter& );
  T         Object;
  F         Deleter;
  VkDevice  Device;
};

7.Tools.h

为何我们如此费力地处理一个简单对象? 着色器模块是创建图形管道所需的对象之一。 但创建管道后,我就不再需要这些着色器模块。 将它们保留下来有时会非常方便,因为我们可能需要创建其他类似的管道。 但在本示例中,创建完图形管道后,我们需要安全地毁坏它们。 通过调用 vkDestroyShaderModule() 函数毁坏着色器模块。 但在本示例中,我们需要在多个地方(多个“ifs”中和整个函数末尾)调用该函数。 因为我们不想忘记哪里需要调用该函数,同时不想出现内存泄漏情况,因此为了方便我准备了这个简单的类。 现在我们不需要记住删除创建的着色器模块,因为它会自动删除。

准备着色器阶段描述

知道如何创建和毁坏着色器模块后,现在我们可以创建着色器阶段数据,以组成图形管道。 正如我所写的,描述哪些着色器阶段应在绑定特定图形管道时处于活跃状态的数据,其形式是包含类型 VkPipelineShaderStageCreateInfo 的要素阵列。 以下代码可创建着色器模块并准备此类阵列:

Tools::AutoDeleter<VkShaderModule, PFN_vkDestroyShaderModule> vertex_shader_module = CreateShaderModule( "Data03/vert.spv" );
Tools::AutoDeleter<VkShaderModule, PFN_vkDestroyShaderModule> fragment_shader_module = CreateShaderModule( "Data03/frag.spv" );

if( !vertex_shader_module || !fragment_shader_module ) {
  return false;
}

std::vector<VkPipelineShaderStageCreateInfo> shader_stage_create_infos = {
  // Vertex shader
  {
    VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,        // VkStructureType                                sType
    nullptr,                                                    // const void                                    *pNext
    0,                                                          // VkPipelineShaderStageCreateFlags               flags
    VK_SHADER_STAGE_VERTEX_BIT,                                 // VkShaderStageFlagBits                          stage
    vertex_shader_module.Get(),                                 // VkShaderModule                                 module
    "main",                                                     // const char                                    *pName
    nullptr                                                     // const VkSpecializationInfo                    *pSpecializationInfo
  },
  // Fragment shader
  {
    VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,        // VkStructureType                                sType
    nullptr,                                                    // const void                                    *pNext
    0,                                                          // VkPipelineShaderStageCreateFlags               flags
    VK_SHADER_STAGE_FRAGMENT_BIT,                               // VkShaderStageFlagBits                          stage
    fragment_shader_module.Get(),                               // VkShaderModule                                 module
    "main",                                                     // const char                                    *pName
    nullptr                                                     // const VkSpecializationInfo                    *pSpecializationInfo
  }
};

8.Tutorial03.cpp,函数 CreatePipeline()

一开始我们创建两个面向顶点和碎片阶段的着色器模块。 用之前所述的函数创建。 如果出现错误,我们从 CreatePipeline() 函数返回,创建的模块将通过包装程序类和提供的删除函数自动删除。

面向着色器模块的代码从包含二进制 SPIR-V 汇编的文件中读取。 这些文件由应用“glslangValidator”生成。 它是一种通过 Vulkan SDK 正式发布的工具,设计目的是为了验证 GLSL 着色器。 但“glslangValidator”也具备编译或将GLSL 着色器转换为 SPIR-V 二进制文件的功能。 官方 SDK 网站提供了有关该用法命令行的完整解释。 我用以下命令生成用于本教程的 SPIR-V 着色器:

glslangValidator.exe -V -H shader.vert > vert.spv.txt

glslangValidator.exe -V -H shader.frag > frag.spv.txt

“glslangValidator”提取指定文件并通过该文件生成 SPIR-V 文件。 输入文件的扩展文件(顶点着色器的“.vert”、几何图形着色器的“.geom”等)将自动检测着色器阶段的类型。 生成文件的名称可以指定,也可以默认为“<stage>.spv”形式。 因此在本示例中将生成“vert.spv”和“frag.spv”文件。

SPIR-V 文件为二进制格式,不容易读取和分析 — 但也有可能。 使用“-H”选项时,“glslangValidator”以易于读取的形式输出 SPIR-V。 这种形式打印为标准输出,因此我使用“> *.spv.txt”重定向运算符。

以下内容是为顶点阶段生成 SPIR-V 汇编的“shader.vert”文件内容:

#version 400

void main() {
    vec2 pos[3] = vec2[3]( vec2(-0.7, 0.7), vec2(0.7, 0.7), vec2(0.0, -0.7) );
    gl_Position = vec4( pos[gl_VertexIndex], 0.0, 1.0 );
}

9.shader.vert

大家看,我对用于渲染三角形的所有顶点位置进行了硬编码。 通过特定于 Vulkan 的“gl_VertexIndex”内置变量为它们编入了索引。 在最简单的场景中,当(此时)使用非索引绘制命令时,该数值从绘制命令的“firstVertex”参数的数值(在提供的示例中为 0)开始。

我之前写过这一具有争议的部分 — 这种方法可接受,也有效,但不太便于维护,也支持我们跳过创建图形管道所需的“结构填充”部分。 我选择使用这种方法,是为了尽可能地缩短和简化本教程。 在下一教程中,我将演示一种更常用的顶点数量绘制方法,类似于在 OpenGL 中使用顶点阵列和索引。

以下是“shader.frag”文件的碎片着色器的源代码,用于生成面向碎片阶段的 SPIRV-V 汇编:

#version 400

layout(location = 0) out vec4 out_Color;

void main() {
  out_Color = vec4( 0.0, 0.4, 1.0, 1.0 );
}

10.shader.frag

在 Vulkan 着色器中(当从 GLSL 转换成 SPIR-V 时),需要使用布局限定符。 这里我们指定哪些输出(颜色)附件保存碎片着色器生成的颜色值。 因为我们仅使用一个附件,所以必须指定第一个可用位置(零)。

现在了解如何为使用 Vulkan 的应用准备着色器后,就可以进行下一步了。 创建两个着色器模块后,检查这些操作是否成功。 如果成功,我们可以开始准备着色器阶段描述,以继续创建图形管道。

我们需要为每个启用的着色器阶段准备 VkPipelineShaderStageCreateInfo 结构实例。 这些结构阵列及其要素数量一起用于图形管道创建信息结构(提供给创建图形管道的函数)。 VkPipelineShaderStageCreateInfo 结构包含以下字段:

  • sType – 我们所准备的结构类型,此处必须等于 VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO。
  • pNext – 为扩展功能预留的指示器。
  • flags – 留作将来使用的参数。
  • stage – 我们描述的着色器阶段类型(比如顶点、镶嵌控制等)。
  • module – 着色器模块句柄,包含用于特定阶段的着色器。
  • pName – 提供的着色器的切入点名称。
  • pSpecializationInfo – VkSpecializationInfo 结构指示器,留作现在使用,设为 null。

创建图形管道时,我们不创建太多 (Vulkan) 对象。 大部分数据只用此类结构的形式展示。

准备顶点输入描述

现在我们必须提供用于绘制的输入数据描述。 这类似于 OpenGL 的顶点数据:属性、组件数量、供数据提取的缓冲区、数据步长,或步进率。 当然,在 Vulkan 中准备这类数据的方式不同,但大体意思相同。 幸运的是,在本教程中由于顶点数据已硬编码成顶点着色器,因此我们几乎可以完全跳过这一步骤,并用 null 和零填充 VkPipelineVertexInputStateCreateInfo:

VkPipelineVertexInputStateCreateInfo vertex_input_state_create_info = {
  VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO,    // VkStructureType                                sType
  nullptr,                                                      // const void                                    *pNext
  0,                                                            // VkPipelineVertexInputStateCreateFlags          flags;
  0,                                                            // uint32_t                                       vertexBindingDescriptionCount
  nullptr,                                                      // const VkVertexInputBindingDescription         *pVertexBindingDescriptions
  0,                                                            // uint32_t                                       vertexAttributeDescriptionCount
  nullptr                                                       // const VkVertexInputAttributeDescription       *pVertexAttributeDescriptions
};

11. Tutorial03.cpp,函数 CreatePipeline()

为清晰起见,以下为 VkPipelineVertexInputStateCreateInfo 结构的成员描述:

  • sType – 结构类型,此处为 VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO。
  • pNext – 特定于扩展的结构指示器。
  • flags – 留作将来使用的参数。
  • vertexBindingDescriptionCount – pVertexBindingDescriptions 阵列中的要素数量。
  • pVertexBindingDescriptions – 包含描述输入顶点数据(步长和步进率)的要素阵列。
  • vertexAttributeDescriptionCount – pVertexAttributeDescriptions 阵列中的要素数量。
  • pVertexAttributeDescriptions – 包含描述顶点属性(位置、格式、位移)的要素阵列。

准备输入汇编描述

下一步骤要求描述如何将顶点汇编成基元。 和 OpenGL 一样,我们必须指定欲使用的拓扑:点、线、三角形、三角扇等。

VkPipelineInputAssemblyStateCreateInfo input_assembly_state_create_info = {
  VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO,  // VkStructureType                                sType
  nullptr,                                                      // const void                                    *pNext
  0,                                                            // VkPipelineInputAssemblyStateCreateFlags        flags
  VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST,                          // VkPrimitiveTopology                            topology
  VK_FALSE                                                      // VkBool32                                       primitiveRestartEnable
};

12.Tutorial03.cpp,函数 CreatePipeline()

我们通过 VkPipelineInputAssemblyStateCreateInfo 结构完成该步骤,其中包含以下成员:

  • sType – 结构类型,此处设为 VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO。
  • pNext – 目前不使用的指示器。
  • flags – 留作将来使用的参数。
  • topology – 描述如何组织顶点以形成基元的参数。
  • primitiveRestartEnable – 告知特定索引值(执行索引绘制时)是否重新开始汇编特定基元的参数。

准备视口描述

我们已处理完输入数据。 现在必须指定输出数据的形式,图形管道连接碎片(比如光栅化、窗口(视口)、深度测试等)的所有部分。 这里必须准备的第一个数据集为视口状态,以指定我希望绘制哪部分图形(或纹理,或窗口)。

VkViewport viewport = {
  0.0f,                                                         // float                                          x
  0.0f,                                                         // float                                          y
  300.0f,                                                       // float                                          width
  300.0f,                                                       // float                                          height
  0.0f,                                                         // float                                          minDepth
  1.0f                                                          // float                                          maxDepth
};

VkRect2D scissor = {
  {                                                             // VkOffset2D                                     offset
    0,                                                            // int32_t                                        x
    0                                                             // int32_t                                        y
  },
  {                                                             // VkExtent2D                                     extent
    300,                                                          // int32_t                                        width
    300                                                           // int32_t                                        height
  }
};

VkPipelineViewportStateCreateInfo viewport_state_create_info = {
  VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO,        // VkStructureType                                sType
  nullptr,                                                      // const void                                    *pNext
  0,                                                            // VkPipelineViewportStateCreateFlags             flags
  1,                                                            // uint32_t                                       viewportCount
  &viewport,                                                    // const VkViewport                              *pViewports
  1,                                                            // uint32_t                                       scissorCount
  &scissor                                                      // const VkRect2D                                *pScissors
};

13.Tutorial03.cpp,函数 CreatePipeline()

在本示例中,用法很简单:仅将视口坐标设置为预定义的值。 不用检查待渲染的交换链图像的大小。 但请记住,在真实生产应用中,必须执行这一操作,因为规范规定,视口的尺寸不能超过待渲染的附件尺寸。

为指定视口参数,我们填充包含以下字段的 VkViewport 结构:

  • x – 视口左侧。
  • y – 视口上侧。
  • width – 视口的宽度。
  • height – 视口的高度。
  • minDepth – 用于深度计算的最小深度值。
  • maxDepth – 用于深度计算的最大深度值。

指定视口坐标时,请记住,起点与 OpenGL 中的不同。 此处我们指定视口的左上角(而非左下角)。

另外一点值得注意的是,minDepth 和 maxDepth 值必须位于 0.0 - 1.0(包含 1.0)之间,但 maxDepth 可以小于 minDepth。 这样会以“相反”的顺序计算深度。

接下来必须指定用于 scissor 测试的参数。 scissor 测试与 OpenGL 类似,将碎片生成仅限制在指定的矩形区域。 但在 Vulkan 中,scissor 测试始终处于启用状态,无法关闭。 我们仅提供与为视口提供的相似的值。 尝试更改这些值,看看对生成的图像产生怎样的影响。

scissor 测试没有专用的结构。 为提供用于该测试的数据,我们填充 VkRect2D 结构,其中包含两个类似的结构成员。 第一个是 VkOffset2D,包含以下成员:

  • x – 用于 scissor 测试的矩形区域的左侧
  • y – 矩形区域的上侧

第二个成员的类型为 VkExtent2D,并包含以下字段:

  • width – scissor 矩形区域的宽度
  • height – scissor 区域的高度

一般来说,通过 VkRect2D 结构为 scissor 测试提供的数据与为视口准备的数据在意义上相似。

为视口和 scissor 测试准备数据后,我们最后可以填充用于创建管道的结构。 该结构称为 VkPipelineViewportStateCreateInfo,并包含以下字段:

  • sType – 结构类型,此处为 VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO。
  • pNext – 为扩展功能预留的指示器。
  • flags – 留作将来使用的参数。
  • attachmentCount – pViewports 阵列中的要素数量。
  • pViewports – 描述绑定特定管道时所使用的视口参数的要素阵列。
  • scissorCount – pScissors 阵列中的要素数量。
  • pScissors – 描述针对各视口的 scissor 测试参数的要素阵列。

请记住,viewportCount 和 scissorCount 参数必须相等。 我们还允许指定更多视口,但之后必须启用 multiViewport 特性。

准备光栅化状态描述

图形管道创建的下一部分适用于光栅化状态。 我们必须指定如何对多边形进行光栅化(改成碎片),是希望为整个多边形生成碎片,还是仅为边缘生成碎片(多边形模式),或者希望看到多边形的正面或背面,还是同时看到这两面(背面剔除)。 我们还可以提供深度偏差参数,或指明是否希望启用深度夹紧 (depth clamp)。 整个状态将封装至 VkPipelineRasterizationStateCreateInfo。 它包含以下成员:

  • sType – 结构类型,本示例中为 VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO。
  • pNext – 为扩展功能预留的指示器。
  • flags – 留作将来使用的参数。
  • depthClampEnable – 描述是希望将光栅化基元的深度值夹在视锥上(真值),还是希望进行正常裁剪(假值)的参数。
  • rasterizerDiscardEnable – 禁用碎片生成(在光栅化关闭碎片着色器之前舍弃基元)。
  • polygonMode – 控制如何为特定基元生成碎片(三角形模式):为整个三角形生成,仅为其边缘生成,或是仅为其顶点生成。
  • cullMode – 选择用于剔除的三角形面(如果启用)。
  • frontFace – 选择将哪个面视作正面(取决于缠绕顺序)。
  • depthBiasEnable – 启用或禁用偏置碎片的深度值。
  • depthBiasConstantFactor – 启用偏置时添加至碎片深度值的常数因子。
  • depthBiasClamp – 适用于碎片深度的最大(或最小)偏差值。
  • depthBiasSlopeFactor – 启用偏置时在深度计算期间适用于碎片斜度的因子。
  • lineWidth – 光栅化线条的宽度。

以下源代码负责设置本示例中的光栅化状态:

VkPipelineRasterizationStateCreateInfo rasterization_state_create_info = {
  VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO,   // VkStructureType                                sType
  nullptr,                                                      // const void                                    *pNext
  0,                                                            // VkPipelineRasterizationStateCreateFlags        flags
  VK_FALSE,                                                     // VkBool32                                       depthClampEnable
  VK_FALSE,                                                     // VkBool32                                       rasterizerDiscardEnable
  VK_POLYGON_MODE_FILL,                                         // VkPolygonMode                                  polygonMode
  VK_CULL_MODE_BACK_BIT,                                        // VkCullModeFlags                                cullMode
  VK_FRONT_FACE_COUNTER_CLOCKWISE,                              // VkFrontFace                                    frontFace
  VK_FALSE,                                                     // VkBool32                                       depthBiasEnable
  0.0f,                                                         // float                                          depthBiasConstantFactor
  0.0f,                                                         // float                                          depthBiasClamp
  0.0f,                                                         // float                                          depthBiasSlopeFactor
  1.0f                                                          // float                                          lineWidth
};

14.Tutorial03.cpp,函数 CreatePipeline()

在本教程中,我们禁用尽可能多的参数,以简化流程、代码和渲染操作。 这里的重要参数可设置适用于多边形光栅化、背面剔除,以及类似于 OpenGL 的逆时针正面的(典型)填充模式。 深度偏置和夹紧也处于禁用状态(要启用深度夹紧,我们首先需要在逻辑设备创建期间启用专用特定;同样,对多边形模式来说,我们也必须进行相同的操作,而非“填充”)。

设置多点采样状态描述

在 Vulkan 中,创建图形管道时,我们还必须指定与多点采用相关的状态。 该步骤可使用 VkPipelineMultisampleStateCreateInfo 结构来完成。 它包含以下成员:

  • sType – 结构类型,此处为 VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO。
  • pNext – 为扩展功能预留的指示器。
  • flags – 留作将来使用的参数。
  • rasterizationSamples – 用于光栅化的每像素样本数量。
  • sampleShadingEnable – 指定是按照样本着色(启用),还是按照碎片着色(禁用)的参数。
  • minSampleShading – 指定应用于特定碎片着色期间的独有样本位置的最少数量。
  • pSampleMask – 静态覆盖范围样本掩码的阵列指示器;可设为 null。
  • alphaToCoverageEnable – 控制碎片的阿尔法值是否用于覆盖范围计算。
  • alphaToOneEnable – 控制是否替换碎片的阿尔法值。

在本示例中,我希望最大限度减少问题的发生,因此将参数设为通常禁用多点采样的值 — 每特定像素仅一个样本,其他参数均处于关闭状态。 请记住,如果我们希望启用样本着色或将阿尔法设为 1,还必须分别启用两个特性。 以下是用于准备 VkPipelineMultisampleStateCreateInfo 结构的源代码:

VkPipelineMultisampleStateCreateInfo multisample_state_create_info = {
  VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO,     // VkStructureType                                sType
  nullptr,                                                      // const void                                    *pNext
  0,                                                            // VkPipelineMultisampleStateCreateFlags          flags
  VK_SAMPLE_COUNT_1_BIT,                                        // VkSampleCountFlagBits                          rasterizationSamples
  VK_FALSE,                                                     // VkBool32                                       sampleShadingEnable
  1.0f,                                                         // float                                          minSampleShading
  nullptr,                                                      // const VkSampleMask                            *pSampleMask
  VK_FALSE,                                                     // VkBool32                                       alphaToCoverageEnable
  VK_FALSE                                                      // VkBool32                                       alphaToOneEnable
};

15.Tutorial03.cpp,函数 CreatePipeline()

设置混合状态描述

在创建图形管道期间,我们另外还需准备混合状态(它还包括逻辑操作)。

VkPipelineColorBlendAttachmentState color_blend_attachment_state = {
  VK_FALSE,                                                     // VkBool32                                       blendEnable
  VK_BLEND_FACTOR_ONE,                                          // VkBlendFactor                                  srcColorBlendFactor
  VK_BLEND_FACTOR_ZERO,                                         // VkBlendFactor                                  dstColorBlendFactor
  VK_BLEND_OP_ADD,                                              // VkBlendOp                                      colorBlendOp
  VK_BLEND_FACTOR_ONE,                                          // VkBlendFactor                                  srcAlphaBlendFactor
  VK_BLEND_FACTOR_ZERO,                                         // VkBlendFactor                                  dstAlphaBlendFactor
  VK_BLEND_OP_ADD,                                              // VkBlendOp                                      alphaBlendOp
  VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |         // VkColorComponentFlags                          colorWriteMask
  VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT
};

VkPipelineColorBlendStateCreateInfo color_blend_state_create_info = {
  VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO,     // VkStructureType                                sType
  nullptr,                                                      // const void                                    *pNext
  0,                                                            // VkPipelineColorBlendStateCreateFlags           flags
  VK_FALSE,                                                     // VkBool32                                       logicOpEnable
  VK_LOGIC_OP_COPY,                                             // VkLogicOp                                      logicOp
  1,                                                            // uint32_t                                       attachmentCount
  &color_blend_attachment_state,                                // const VkPipelineColorBlendAttachmentState     *pAttachments
  { 0.0f, 0.0f, 0.0f, 0.0f }                                    // float                                          blendConstants[4]
};

16.Tutorial03.cpp,函数 CreatePipeline()

VkPipelineColorBlendStateCreateInfo 结构可用于设置最终颜色操作。 它包含以下字段:

  • sType – 结构类型,在本示例中设置为 VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO。
  • pNext – 留作将来特定用于扩展的指示器。
  • flags – 同样留作将来使用的参数。
  • logicOpEnable – 指示是否希望启用有关像素的逻辑操作。
  • logicOp – 预执行的逻辑操作类型(比如拷贝、清空等)。
  • attachmentCount – pAttachments 阵列中的要素数量。
  • pAttachments – 包含颜色附件状态参数的阵列,其中这些颜色附件用于子通道,以便其绑定特定图形管道。
  • blendConstants – 包含四个要素以及用于混合操作的颜色值的阵列(使用专用混合因子时)。

attachmentCount 和 pAttachments 需要更多信息。 如果想执行绘制操作,我们需要设置参数,其中最重要的是图形管道、渲染通道和帧缓冲器。 显卡需要知道绘制的方式(描述渲染状态、着色器、测试等的图形管道)和绘制的位置(渲染通道提供通用设置;帧缓冲器指定使用哪些图像)。 我之前说过,渲染通道指定如何排列操作、具有哪些相关性、何时渲染特定附件,以及何时读取相同附件。 这些阶段以子通道的形式执行。 我们可以(但不是必须)为每项绘制操作启用/使用不同的管道。 我们进行绘制时,必须记住要将绘制成附件集。 该集合在渲染通道中定义,其中描述所有的颜色、输入和深度附件(帧缓冲器仅指定哪些图像用于这些附件)。 就混合状态而言,我们可以指定是否希望启用混合。 这一操作通过 pAttachments 阵列来完成。 每个要素都必须对应渲染通道中定义的颜色附件。 因此 pAttachments 阵列中 attachmentCount 要素的值必须等于渲染通道中定义的颜色附件数量。

还有一个限制条件。 在默认情况下,pAttachments 阵列中的所有要素都必须包含相同的值,必须以相同的方式指定,而且必须相同。 默认情况下,执行混合(和颜色掩码)的方式与所有附件相同。 为何是一个阵列? 为什么只需指定一个值? 因为有一项特性可以支持我们执行为每个活跃的颜色附件独立、独特的混合。 如果在创建设备期间启用独立混合,我们可以为各颜色附件提供不同的值。

pAttachments 阵列要素的类型为 VkPipelineColorBlendAttachmentState。 该结构包含以下成员:

  • blendEnable – 指示是否希望启用混合。
  • srcColorBlendFactor – 面向源(入站)碎片颜色的混合因子。
  • dstColorBlendFactor – 面向目标颜色的混合因子(保存在帧缓冲器中,位置与入站碎片相同)。
  • colorBlendOp – 待执行操作的类型(乘法、加法等)。
  • srcAlphaBlendFactor – 面向源(入站)碎片阿尔法值的混合因子。
  • dstAlphaBlendFactor – 面向目标阿尔法值的混合因子(保存在帧缓冲器中)。
  • alphaBlendOp – 面向阿尔法混合执行的操作类型。
  • colorWriteMask – 选择(启用)编写哪个 RGBA 组件的位掩码。

在本示例中,我们禁用混合操作,这样其他所有参数都将处于不相关状态。 除 colorWriteMask 外,我们选择编写所有组件,但您可以自由检查该参数变成其他 R、G、B、A 组合后,将会发生什么。

创建管道布局

创建管道之前,我们需要做的最后一件事是创建相应的管道布局。 管道布局描述管道可访问的所有资源。 在本示例中,我们必须指定着色器将使用多少纹理,以及哪些着色器阶段将访问它们。 当然还会涉及到其他资源。 除着色器阶段外,我们还必须描述资源的类型(纹理、缓冲区)、总数量,以及布局。 该布局可以比作 OpenGL 的活跃纹理和 shader uniform。 在 OpenGL 中,我们将纹理绑定至所需的纹理图像单元,而且不为 shader uniform 提供纹理句柄,而是提供纹理图像单元(绑定实际纹理)的 ID(我们提供与特定纹理相关的单元编号)。

Vulkan 中的情况类似。 我们创建某种内存布局形式:首先是两个缓冲区,接下来是三个纹理和一个图像。 这种内存“结构”称为集,这些集的集合将提供给管道。 在着色器中,我们使用这些集(布局)中的内存“位置”访问指定的资源。 这可通过布局 (set = X, binding = Y) 分类符来完成,也可以解释为:从 Y 内存位置和 X 集提取资源。

管道布局可视作着色器阶段和着色器资源之间的交互,因为它提取这些资源组,并描述如何收集并向管道提供这些资源。

该流程比较复杂,我计划为其另外编写一节教程。 这里我们不使用其他资源,因为我展示的是关于创建“空”管道布局的示例。

VkPipelineLayoutCreateInfo layout_create_info = {
  VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,  // VkStructureType                sType
  nullptr,                                        // const void                    *pNext
  0,                                              // VkPipelineLayoutCreateFlags    flags
  0,                                              // uint32_t                       setLayoutCount
  nullptr,                                        // const VkDescriptorSetLayout   *pSetLayouts
  0,                                              // uint32_t                       pushConstantRangeCount
  nullptr                                         // const VkPushConstantRange     *pPushConstantRanges
};

VkPipelineLayout pipeline_layout;
if( vkCreatePipelineLayout( GetDevice(), &layout_create_info, nullptr, &pipeline_layout ) != VK_SUCCESS ) {
  printf( "Could not create pipeline layout!\n" );
  return Tools::AutoDeleter<VkPipelineLayout, PFN_vkDestroyPipelineLayout>();
}

return Tools::AutoDeleter<VkPipelineLayout, PFN_vkDestroyPipelineLayout>( pipeline_layout, vkDestroyPipelineLayout, GetDevice() );

17.Tutorial03.cpp,函数 CreatePipelineLayout()

为创建管道布局,必须首先创建类型变量 VkPipelineLayoutCreateInfo。 它包含以下字段:

  • sType – 结构类型,本示例中为 VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO。
  • pNext – 为扩展功能预留的参数。
  • flags – 留作将来使用的参数。
  • setLayoutCount – 本布局中包含的描述符集的数量。
  • pSetLayouts – 包含描述符布局描述的阵列指示器。
  • pushConstantRangeCount – push constant 范围(稍后介绍)的数量。
  • pPushConstantRanges – 描述(特定管道中)着色器中使用的所有 push constant 范围的阵列。

在本示例中,我们创建“空”布局,因此几乎所有字段都设置为 null 或零。

我们在这里不使用 push constant,不过这一概念值得介绍。 Vulkan 中的 push constant 支持我们修改用于着色器的常量变量的数据。 这里为 push constant 预留了少量的特殊内存。 我们通过 Vulkan 命令(而非内存更新)更新其数值,push constant 值的更新速度预计快于正常内存写入。

如上述示例所示,我还会将管道布局包装至“AutoDeleter”对象。 管道创建、描述符集绑定(启用/激活着色器与着色器资源之间的交互),以及 push constant 设置期间都需要管道布局。 除管道创建外,本教程不执行任何其他操作。 因此在这里,创建管道后,不再需要使用布局。 为避免内存泄漏,离开创建图形管道的函数后,我立即使用了该帮助类毁坏布局。

创建图形管道

现在我们准备了创建图形管道需要的所有资源。 以下代码可帮助完成这一操作:

Tools::AutoDeleter<VkPipelineLayout, PFN_vkDestroyPipelineLayout> pipeline_layout = CreatePipelineLayout();
if( !pipeline_layout ) {
  return false;
}

VkGraphicsPipelineCreateInfo pipeline_create_info = {
  VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO,              // VkStructureType                                sType
  nullptr,                                                      // const void                                    *pNext
  0,                                                            // VkPipelineCreateFlags                          flags
  static_cast<uint32_t>(shader_stage_create_infos.size()),      // uint32_t                                       stageCount
  &shader_stage_create_infos[0],                                // const VkPipelineShaderStageCreateInfo         *pStages
  &vertex_input_state_create_info,                              // const VkPipelineVertexInputStateCreateInfo    *pVertexInputState;
  &input_assembly_state_create_info,                            // const VkPipelineInputAssemblyStateCreateInfo  *pInputAssemblyState
  nullptr,                                                      // const VkPipelineTessellationStateCreateInfo   *pTessellationState
  &viewport_state_create_info,                                  // const VkPipelineViewportStateCreateInfo       *pViewportState
  &rasterization_state_create_info,                             // const VkPipelineRasterizationStateCreateInfo  *pRasterizationState
  &multisample_state_create_info,                               // const VkPipelineMultisampleStateCreateInfo    *pMultisampleState
  nullptr,                                                      // const VkPipelineDepthStencilStateCreateInfo   *pDepthStencilState
  &color_blend_state_create_info,                               // const VkPipelineColorBlendStateCreateInfo     *pColorBlendState
  nullptr,                                                      // const VkPipelineDynamicStateCreateInfo        *pDynamicState
  pipeline_layout.Get(),                                        // VkPipelineLayout                               layout
  Vulkan.RenderPass,                                            // VkRenderPass                                   renderPass
  0,                                                            // uint32_t                                       subpass
  VK_NULL_HANDLE,                                               // VkPipeline                                     basePipelineHandle
  -1                                                            // int32_t                                        basePipelineIndex
};

if( vkCreateGraphicsPipelines( GetDevice(), VK_NULL_HANDLE, 1, &pipeline_create_info, nullptr, &Vulkan.GraphicsPipeline ) != VK_SUCCESS ) {
  printf( "Could not create graphics pipeline!\n" );
  return false;
}
return true;

18.Tutorial03.cpp,函数 CreatePipeline()

我们首先创建封装在“AutoDeleter” 对象中的管道布局。 接下来填充 VkGraphicsPipelineCreateInfo 类型的结构 。 它包含多个字段。 以下简要介绍它们:

  • sType – 结构类型,此处为 VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO。
  • pNext – 留作将来用于扩展的参数。
  • flags – 这次该参数不留做将来使用,而是用于控制如何创建管道:是创建衍生管道(如果承袭另一管道),还是支持从该管道创建衍生管道。 我们还可以禁用优化,这样可缩短创建管道的时间。
  • stageCount – pStages 参数中描述的阶段数据;必须大于 0。
  • pStages – 包含活跃着色器阶段(用着色器模块创建而成)描述的阵列;各阶段必须具有唯一性(指定特定阶段的数量不能超过一次)。 还必须展示一个顶点阶段。
  • pVertexInputState – 包含顶点输入状态描述的变量指示器。
  • pInputAssemblyState – 包含输入汇编描述的变量指示器。
  • pTessellationState – 镶嵌阶段描述指示器;如果镶嵌处于禁用状态,则可设置为 null。
  • pViewportState – 指定视口参数的变量指示器;如果光栅化处于禁用状态,则可设置为 null。
  • pRasterizationState – 指定光栅化行为的变量指示器。
  • pMultisampleState – 定义多点采样的变量指示器;如果镶嵌处于禁用状态,则可设置为 null。
  • pDepthStencilState – 深度/模板参数描述指示器;在两种情况下可设置为 null:光栅化处于禁用状态,或者我们不在渲染通道中使用深度/模板附件。
  • pColorBlendState – 包含颜色混合/写入掩码状态的变量指示器;也在两种情况下可设置为 null:光栅化处于禁用状态,或者我们不在渲染通道中使用任何颜色附件。
  • pDynamicState – 指定可动态设置哪部分图形管道的变量指示器;如果认为整个状态是静止状态(仅通过该创建信息结构定义),可设置为 null。
  • layout – 管道布局对象的句柄,该对象描述在着色器中访问的资源。
  • renderPass – 渲染通道对象句柄;管道可用于任何兼容已提供通道的渲染通道。
  • subpass – 供使用管道的子通道编号(索引)。
  • basePipelineHandle – 支持衍生该管道的管道句柄。
  • basePipelineIndex – 支持衍生该管道的管道索引。

创建新管道时,我们可以承袭其他管道的部分参数。 这意味着两个管道存在共同之处。 比较好的示例是着色器代码。 我们不指定哪些字段是相同的,但从其他管道继承的通用信息可显著加快管道创建速度。 但为什么有两个字段指示“父”管道? 我们不能使用两个 — 一次仅使用一个。 我们使用句柄时,表示“父”管道已创建完成,我们正从提供句柄的管道衍生新管道。 但管道创建函数支持我们一次创建多个管道。 使用第二个参数“父”管道索引可以帮助我们用相同的调用方式创建“父”管道和“子”管道。 我们仅指定图形管道创建信息结构阵列,而且该阵列提供给管道创建函数。 因此“basePipelineIndex”是该阵列中管道创建信息的索引。 我们只需记住,“父”管道在该阵列中必须先创建(索引必须小),而且必须通过“allow derivatives”标记集创建。

在本示例中,我们创建整体处于静止状态的管道(“pDynamicState”参数设为 null)。 但什么是静止状态? 为支持部分灵活性和减少管道对象的创建数量,我们引入了动态状态。 我们可以通过“pDynamicState”参数定义可通过其他 Vulkan 命令动态设置哪部分图形管道,以及在管道创建期间将哪部分一次设置为静态。 动态状态包括视口、线条宽度、混合常量等参数,或部分模板参数。 如果我们指定特定状态为动态,那么忽略管道创建信息结构中与该状态有关的参数。 在渲染期间,必须使用适当的命令设置特定状态,因为该状态的初始值可能没有定义。

因此经过这项繁重的准备工作后,我们可以创建图形管道。 这一过程通过调用 vkCreateGraphicsPipelines() 函数完成,提取管道创建信息结构的指示器阵列。 如果进展顺利,该函数将返回 VK_SUCCESS,图形管道句柄并保存在我们提供了地址的变量中。 现在我们可以开始进行绘制。

准备绘制命令

之前的教程介绍过命令缓冲区概念。 这里我简要介绍使用其中的哪些,以及如何使用。

命令缓冲区是 GPU 命令的容器。 如果想在设备上执行某项任务,我们可以通过命令缓冲区来完成。 这表示我们必须准备处理数据(即在屏幕上绘制图形)的命令集,并将这些命令记录在命令缓冲区中。 然后将整个缓冲区提交至设备的队列。 这种提交操作将告诉设备:我希望你现在替我执行一些任务。

为记录命令,我们必须首先分配命令缓冲区。 它们通过命令池进行分配,可视作内存块。 如果命令缓冲区希望变大(因为我们记录了许多复杂命令),它可以增长,并使用命令池(进行分配)的其他内存。 因此我们首先必须创建命令池。

创建命令池

创建命令池非常简单,如下所示:

VkCommandPoolCreateInfo cmd_pool_create_info = {
  VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,     // VkStructureType                sType
  nullptr,                                        // const void                    *pNext
  0,                                              // VkCommandPoolCreateFlags       flags
  queue_family_index                              // uint32_t                       queueFamilyIndex
};

if( vkCreateCommandPool( GetDevice(), &cmd_pool_create_info, nullptr, pool ) != VK_SUCCESS ) {
  return false;
}
return true;

19.Tutorial03.cpp,函数 CreateCommandPool()

首先准备类型变量 VkCommandPoolCreateInfo。 它包含以下字段:

  • sType – 标准结构类型,此处设置为 VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO。
  • pNext – 为扩展功能预留的指示器。
  • flags – 指示命令池及其分配的命令缓冲区的使用场景;即我们告知驱动程序,从该命令池分配的命令缓冲区将存在较短时间;如果没有特定用法,可将其设置为零。
  • queueFamilyIndex – 队列家族(我们为其创建命令池)索引。

请记住,从特定命令池分配的命令缓冲区只能提交至命令池创建期间指定的队列家族的队列。

要创建命令池,我们只需调用 vkCreateCommandPool() 函数。

分配命令缓冲区

现在我们准备好命令池后,可以通过它分配命令缓冲区。

VkCommandBufferAllocateInfo command_buffer_allocate_info = {
  VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, // VkStructureType                sType
  nullptr,                                        // const void                    *pNext
  pool,                                           // VkCommandPool                  commandPool
  VK_COMMAND_BUFFER_LEVEL_PRIMARY,                // VkCommandBufferLevel           level
  count                                           // uint32_t                       bufferCount
};

if( vkAllocateCommandBuffers( GetDevice(), &command_buffer_allocate_info, command_buffers ) != VK_SUCCESS ) {
  return false;
}
return true;

20.Tutorial03.cpp,函数 AllocateCommandBuffers()

为分配命令缓冲区,我们指定一个结构类型变量。 这次的类型为 VkCommandBufferAllocateInfo,其中包含以下三个成员:

  • sType – 结构类型;此处为 VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO。
  • pNext – 为扩展功能预留的指示器。
  • commandPool – 我们希望命令缓冲区从中提取内存的池。
  • level – 命令缓冲区级别;包含两个级别:主缓冲区和次缓冲区;此时我们仅有兴趣使用主命令缓冲区。
  • bufferCount – 希望分配的命令缓冲区数量。

为分配命令缓冲区,我们调用 vkAllocateCommandBuffers() 函数并检查是否调用成功。 通过一次函数调用可以一次性分配多个缓冲区。

我准备了一个简单的缓冲区分配函数,以向大家展示如果包装 Vulkan 函数,以方便使用。 以下是两个此类包装程序函数的用法,分别用于创建命令池和分配命令缓冲区。

if( !CreateCommandPool( GetGraphicsQueue().FamilyIndex, &Vulkan.GraphicsCommandPool ) ) {
  printf( "Could not create command pool!\n" );
  return false;
}

uint32_t image_count = static_cast<uint32_t>(GetSwapChain().Images.size());
Vulkan.GraphicsCommandBuffers.resize( image_count, VK_NULL_HANDLE );

if( !AllocateCommandBuffers( Vulkan.GraphicsCommandPool, image_count, &Vulkan.GraphicsCommandBuffers[0] ) ) {
  printf( "Could not allocate command buffers!\n" );
  return false;
}
return true;

21.Tutorial03.cpp,函数 CreateCommandBuffers()

大家看,我们正在为显卡队列家族索引创建命令池。 所有图像状态过渡和绘制操作都将在显卡队列上执行。 演示操作在另一队列上执行(如果演示队列与显卡队列不同),但执行该操作时不需要使用命令缓冲区。

而且我们还为各交换链图像分配命令缓冲区。 这里我们提取图像数量,并将其提供给简单的“wrapper”函数,以便分配命令缓冲区。

记录命令缓冲区

从命令池分配到命令缓冲区后,最后我们可以记录在屏幕上进行绘制的操作。 首先必须准备执行记录操作所需的数据集。 一部分数据与所有命令缓冲区相同,另一部分引用特定的交换链图像。 以下是独立于交换链图像的代码:

VkCommandBufferBeginInfo graphics_commandd_buffer_begin_info = {
  VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,    // VkStructureType                        sType
  nullptr,                                        // const void                            *pNext
  VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT,   // VkCommandBufferUsageFlags              flags
  nullptr                                         // const VkCommandBufferInheritanceInfo  *pInheritanceInfo
};

VkImageSubresourceRange image_subresource_range = {
  VK_IMAGE_ASPECT_COLOR_BIT,                      // VkImageAspectFlags             aspectMask
  0,                                              // uint32_t                       baseMipLevel
  1,                                              // uint32_t                       levelCount
  0,                                              // uint32_t                       baseArrayLayer
  1                                               // uint32_t                       layerCount
};

VkClearValue clear_value = {
  { 1.0f, 0.8f, 0.4f, 0.0f },                     // VkClearColorValue              color
};

const std::vector<VkImage>& swap_chain_images = GetSwapChain().Images;

22.Tutorial03.cpp,函数 RecordCommandBuffers()

执行命令缓冲区记录类似于 OpenGL 的绘制列表,其中我们通过调用 glNewList() 函数开始记录列表。 接下来准备绘制命令集,然后关闭列表并停止记录 (glEndList())。 因此我们首先要做的是准备类型变量 VkCommandBufferBeginInfo。 它用于开始记录命令缓冲区的时候,告知驱动程序有关命令缓冲区的类型、内容和用法等信息。 该类型变量包含以下成员:

  • sType – 标准结构类型,此处设置为 VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO。
  • pNext – 为扩展功能预留的指示器。
  • flags – 描述预期用法的参数(即我们是否想仅提交一次,并毁坏/重置该命令缓冲区,或者是否可以在之前的提交完成之前再次提交该缓冲区)。
  • pInheritanceInfo – 仅在记录次命令缓冲区时所使用的参数。

接下来描述为其设置图像内存壁垒的图像区域或部分。 此处我们设置壁垒以指定不同家族的列队将引用特定图像。 这一操作通过类型变量 VkImageSubresourceRange 完成,该变量包含以下成员:

  • aspectMask – 描述图像的”类型“,是否用于颜色、深度或模板数据。
  • baseMipLevel – 供我们执行操作的第一个 mipmap 层的编号。
  • levelCount – 供我们执行操作的 mipmap 层(包括基础层)数量。
  • baseArrayLayer – 参与操作的图像的第一个阵列层的编号。
  • layerCount – 将进行修改的层级(包括基础层)数量。

接下来设置面向图像的清空值。 进行绘制之前需要清空图像。 在之前的教程中,我们自己明确执行这一操作。 这里图像作为渲染通道附件加载操作的一部分进行清空。 我们要设置成“clear”,必须指定图像需清空的颜色。 这一操作通过类型变量 VkClearValue(其中我们提供了 R、G、B、A 四个值)完成。

到目前为止,我们创建的变量均独立于图像本身,因此我们在循环前完成了指定行为。 现在我们开始记录命令缓冲区:

for( size_t i = 0; i < Vulkan.GraphicsCommandBuffers.size(); ++i ) {
  vkBeginCommandBuffer( Vulkan.GraphicsCommandBuffers[i], &graphics_commandd_buffer_begin_info );

  if( GetPresentQueue().Handle != GetGraphicsQueue().Handle ) {
    VkImageMemoryBarrier barrier_from_present_to_draw = {
      VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,     // VkStructureType                sType
      nullptr,                                    // const void                    *pNext
      VK_ACCESS_MEMORY_READ_BIT,                  // VkAccessFlags                  srcAccessMask
      VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,       // VkAccessFlags                  dstAccessMask
      VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,            // VkImageLayout                  oldLayout
      VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,            // VkImageLayout                  newLayout
      GetPresentQueue().FamilyIndex,              // uint32_t                       srcQueueFamilyIndex
      GetGraphicsQueue().FamilyIndex,             // uint32_t                       dstQueueFamilyIndex
      swap_chain_images[i],                       // VkImage                        image
      image_subresource_range                     // VkImageSubresourceRange        subresourceRange
    };
    vkCmdPipelineBarrier( Vulkan.GraphicsCommandBuffers[i], VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier_from_present_to_draw );
  }

  VkRenderPassBeginInfo render_pass_begin_info = {
    VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO,     // VkStructureType                sType
    nullptr,                                      // const void                    *pNext
    Vulkan.RenderPass,                            // VkRenderPass                   renderPass
    Vulkan.FramebufferObjects[i].Handle,          // VkFramebuffer                  framebuffer
    {                                             // VkRect2D                       renderArea
      {                                           // VkOffset2D                     offset
        0,                                          // int32_t                        x
        0                                           // int32_t                        y
      },
      {                                           // VkExtent2D                     extent
        300,                                        // int32_t                        width
        300,                                        // int32_t                        height
      }
    },
    1,                                            // uint32_t                       clearValueCount
    &clear_value                                  // const VkClearValue            *pClearValues
  };

  vkCmdBeginRenderPass( Vulkan.GraphicsCommandBuffers[i], &render_pass_begin_info, VK_SUBPASS_CONTENTS_INLINE );

  vkCmdBindPipeline( Vulkan.GraphicsCommandBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, Vulkan.GraphicsPipeline );

  vkCmdDraw( Vulkan.GraphicsCommandBuffers[i], 3, 1, 0, 0 );

  vkCmdEndRenderPass( Vulkan.GraphicsCommandBuffers[i] );

  if( GetGraphicsQueue().Handle != GetPresentQueue().Handle ) {
    VkImageMemoryBarrier barrier_from_draw_to_present = {
      VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,       // VkStructureType              sType
      nullptr,                                      // const void                  *pNext
      VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,         // VkAccessFlags                srcAccessMask
      VK_ACCESS_MEMORY_READ_BIT,                    // VkAccessFlags                dstAccessMask
      VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,              // VkImageLayout                oldLayout
      VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,              // VkImageLayout                newLayout
      GetGraphicsQueue().FamilyIndex,               // uint32_t                     srcQueueFamilyIndex
      GetPresentQueue( ).FamilyIndex,               // uint32_t                     dstQueueFamilyIndex
      swap_chain_images[i],                         // VkImage                      image
      image_subresource_range                       // VkImageSubresourceRange      subresourceRange
    };
    vkCmdPipelineBarrier( Vulkan.GraphicsCommandBuffers[i], VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier_from_draw_to_present );
  }

  if( vkEndCommandBuffer( Vulkan.GraphicsCommandBuffers[i] ) != VK_SUCCESS ) {
    printf( "Could not record command buffer!\n" );
    return false;
  }
}
return true;

23.Tutorial03.cpp,函数 RecordCommandBuffers()

通过调用 vkBeginCommandBuffer() 函数开始记录命令缓冲区。 开始时设置一个壁垒,告知驱动程序之前某个家族的队列引用了特定图像,但现在不同家族的队列将引用该图像(这么做的原因是在交换链创建期间,我们指定了专用共享模式)。 该壁垒仅在显卡队列不同于演示队列时使用。 该步骤可通过调用 vkCmdPipelineBarrier() 函数进行。 我们必须指定何时将壁垒放在管道中 (VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT),以及如何设置该壁垒。 通过 VkImageMemoryBarrier 结构准备壁垒参数:

  • sType – 结构类型,此处设置为 VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER。
  • pNext – 为扩展功能预留的指示器。
  • srcAccessMask – 壁垒前执行的与特定图像有关的内存操作类型。
  • dstAccessMask – 连接至特定图像,在壁垒之后执行的内存操作类型。
  • oldLayout – 当前图像内存布局。
  • newLayout – 壁垒之后应该拥有的内存布局图像。
  • srcQueueFamilyIndex – 在壁垒之前引用图像的队列家族索引。
  • dstQueueFamilyIndex – 在壁垒之后将通过其引用图像的队列家族索引。
  • image – 图像本身的句柄。
  • subresourceRange – 我们希望进行过渡的图像部分。

在本示例中,我们不更改图像的布局,原因有两点: (1) 壁垒可以不用设置(如果图形和演示队列相同),(2) 布局过渡将作为渲染通道操作自动进行(在第一个 — 唯一 — 子通道开头)。

接下来启动渲染通道。 我们调用 vkCmdBeginRenderPass() 函数,而且必须为该函数提供 VkRenderPassBeginInfo 类型变量指示器。 它包含以下成员:

  • sType – 标准结构类型。 在本示例中,必须将其设置为 VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO 的值。
  • pNext – 留作将来使用的指示器。
  • renderPass – 我们希望启动的渲染通道的句柄。
  • framebuffer – 帧缓冲器的句柄,指定在该渲染通道中用作附件的图像。
  • renderArea – 受渲染通道内执行的操作所影响的图形区域。 它指定左上角(通过抵消成员 (offset member) 的 x 和 y 参数),以及渲染区域的宽度和高度(通过扩展成员 (extent member))。
  • clearValueCount – pClearValues 阵列中的要素数量。
  • pClearValues – 有关各附件的清空值阵列。

指定渲染通道的渲染区域时,必须确保渲染操作不会修改该区域外的像素。 这只是给驱动程序的提示,但可以优化其行为。 如果不使用相应的 scissor 测试将操作限制在提供的区域内,该区域外的像素可能变成未定义像素(不能依靠内容)。 而且,我们不能指定大于帧缓冲区尺寸的渲染区域(超出帧缓冲器)。

就 pClearValues 而言,它必须包含各渲染通道附件的要素。 loadOp 设置为清空时,每个要素均指定特定附件必须清空的颜色。 对于 loadOp 不是清空的附件来说,将忽略提供给它们的值。 但不能为阵列提供数量较少的要素。

我们已经开始创建命令缓冲区、设置壁垒(如有必要),并启动渲染通道。 启动渲染通道时,我们还将启动其第一个子通道。 我们可以通过调用 vkCmdNextSubpass() 函数切换至下一个子通道。 执行这些操作期间,可能会出现布局过渡和清空操作。 清空操作在首先使用(引用)图像的子通道内进行。 如果子通道布局与之前的通道或(如果是第一个子通道或第一个引用图像时)初始布局(渲染通道之前的布局)不同,将出现布局过渡。 因此在本示例中,启动渲染通道时,交换链图像的布局将从“presentation source”布局自动变成 “color attachment optimal”布局。

现在我们绑定图形管道。 该步骤可通过调用 vkCmdBindPipeline() 函数完成。 这样可“激活”所有着色器程序(类似于 glUseProgram() 函数,并设置必要的测试、混合操作等。

绑定管道后,我们可以通过调用 vkCmdDraw() 函数,进行最终的绘制操作。 在本函数中,我们指定希望绘制的顶点数量(3 个)、应绘制的实例数量(仅 1 个),以及第一个顶点和第一个实例的索引编号(均为 0)。

接下来调用 vkCmdEndRenderPass() 函数,结束特定的渲染通道。 这里,如果为渲染通道指定的最终布局与引用特定图像的最后一个子通道所使用的布局不同,所有最终布局都将进行过渡。

之后将设置壁垒,其中我们告知驱动程序,显卡队列已使用完特定图像,而且从现在开始演示队列将使用该图像。 仅在显卡队列和演示队列不同的情况下,再次执行该步骤。 在壁垒之后,我们停止为特定图像记录命令缓冲区。 所有这些操作都会为每个交换链图像重复一次。

绘制

绘制函数与教程 2 中的 Draw() 函数相同。 我们获取图形索引、提交相应的命令缓冲区,并演示图像。 使用旗语的方式与之前的相同:一个旗语用于获取图像,并告知显卡队列等待可用的图像。 第二个命令缓冲区用于指示显卡队列上的绘制操作是否已经完成。 演示图像之前,演示队列需等待该旗语。 以下是 Draw() 函数的源代码:

VkSemaphore image_available_semaphore = GetImageAvailableSemaphore();
VkSemaphore rendering_finished_semaphore = GetRenderingFinishedSemaphore();
VkSwapchainKHR swap_chain = GetSwapChain().Handle;
uint32_t image_index;

VkResult result = vkAcquireNextImageKHR( GetDevice(), swap_chain, UINT64_MAX, image_available_semaphore, VK_NULL_HANDLE, &image_index );
switch( result ) {
  case VK_SUCCESS:
  case VK_SUBOPTIMAL_KHR:
    break;
  case VK_ERROR_OUT_OF_DATE_KHR:
    return OnWindowSizeChanged();
  default:
    printf( "Problem occurred during swap chain image acquisition!\n" );
    return false;
}

VkPipelineStageFlags wait_dst_stage_mask = VK_PIPELINE_STAGE_TRANSFER_BIT;
VkSubmitInfo submit_info = {
  VK_STRUCTURE_TYPE_SUBMIT_INFO,                // VkStructureType              sType
  nullptr,                                      // const void                  *pNext
  1,                                            // uint32_t                     waitSemaphoreCount
  &image_available_semaphore,                   // const VkSemaphore           *pWaitSemaphores
  &wait_dst_stage_mask,                         // const VkPipelineStageFlags  *pWaitDstStageMask;
  1,                                            // uint32_t                     commandBufferCount
  &Vulkan.GraphicsCommandBuffers[image_index],  // const VkCommandBuffer       *pCommandBuffers
  1,                                            // uint32_t                     signalSemaphoreCount
  &rendering_finished_semaphore                 // const VkSemaphore           *pSignalSemaphores
};

if( vkQueueSubmit( GetGraphicsQueue().Handle, 1, &submit_info, VK_NULL_HANDLE ) != VK_SUCCESS ) {
  return false;
}

VkPresentInfoKHR present_info = {
  VK_STRUCTURE_TYPE_PRESENT_INFO_KHR,           // VkStructureType              sType
  nullptr,                                      // const void                  *pNext
  1,                                            // uint32_t                     waitSemaphoreCount
  &rendering_finished_semaphore,                // const VkSemaphore           *pWaitSemaphores
  1,                                            // uint32_t                     swapchainCount
  &swap_chain,                                  // const VkSwapchainKHR        *pSwapchains
  &image_index,                                 // const uint32_t              *pImageIndices
  nullptr                                       // VkResult                    *pResults
};
result = vkQueuePresentKHR( GetPresentQueue().Handle, &present_info );

switch( result ) {
  case VK_SUCCESS:
    break;
  case VK_ERROR_OUT_OF_DATE_KHR:
  case VK_SUBOPTIMAL_KHR:
    return OnWindowSizeChanged();
  default:
    printf( "Problem occurred during image presentation!\n" );
    return false;
}

return true;

24.Tutorial03.cpp,函数 Draw()

教程 3 执行

在本教程中我们执行了“真正的”绘制操作。 简单的三角形似乎没有太大的说服力,但对于 Vulkan 创建的第一个图像来说,它是一个良好的开端。 该三角形如下所示:

如果您想知道图像中为什么会出现黑色部分,原因如下: 为简化整个代码,我们创建了大小固定(宽度和高度均为 300 像素)的帧缓冲区。 但窗口尺寸(和交换链图像的尺寸)可能大于 300 x 300 像素。 超出帧缓冲区尺寸的图像部分没有被应用清空和修改。 这可能包含部分“人为因素”,因为供驱动程序分配交换链图像的内存之前可能用于其他目的,可能包含一些数据。 正确的行为是创建大小与交换链图像相同的帧缓冲区,并在窗口尺寸大小变化后重新创建。 但如果在橙色/金色背景上渲染蓝色三角形,表示该代码可正常运行。

清空

本教程结束之前,我们最后需要学习的是如何使用在本教程中创建的资源。 释放资源所需的代码已在之前章节中创建,这里不再叙述。 只需查看 VulkanCommon.cpp 文件。 以下代码可毁坏特定于本章节的资源:

if( GetDevice() != VK_NULL_HANDLE ) {
  vkDeviceWaitIdle( GetDevice() );

  if( (Vulkan.GraphicsCommandBuffers.size() > 0) && (Vulkan.GraphicsCommandBuffers[0] != VK_NULL_HANDLE) ) {
    vkFreeCommandBuffers( GetDevice(), Vulkan.GraphicsCommandPool, static_cast<uint32_t>(Vulkan.GraphicsCommandBuffers.size()), &Vulkan.GraphicsCommandBuffers[0] );
    Vulkan.GraphicsCommandBuffers.clear();
  }

  if( Vulkan.GraphicsCommandPool != VK_NULL_HANDLE ) {
    vkDestroyCommandPool( GetDevice(), Vulkan.GraphicsCommandPool, nullptr );
    Vulkan.GraphicsCommandPool = VK_NULL_HANDLE;
  }

  if( Vulkan.GraphicsPipeline != VK_NULL_HANDLE ) {
    vkDestroyPipeline( GetDevice(), Vulkan.GraphicsPipeline, nullptr );
    Vulkan.GraphicsPipeline = VK_NULL_HANDLE;
  }

  if( Vulkan.RenderPass != VK_NULL_HANDLE ) {
    vkDestroyRenderPass( GetDevice(), Vulkan.RenderPass, nullptr );
    Vulkan.RenderPass = VK_NULL_HANDLE;
  }

  for( size_t i = 0; i < Vulkan.FramebufferObjects.size(); ++i ) {
    if( Vulkan.FramebufferObjects[i].Handle != VK_NULL_HANDLE ) {
      vkDestroyFramebuffer( GetDevice(), Vulkan.FramebufferObjects[i].Handle, nullptr );
      Vulkan.FramebufferObjects[i].Handle = VK_NULL_HANDLE;
    }

    if( Vulkan.FramebufferObjects[i].ImageView != VK_NULL_HANDLE ) {
      vkDestroyImageView( GetDevice(), Vulkan.FramebufferObjects[i].ImageView, nullptr );
      Vulkan.FramebufferObjects[i].ImageView = VK_NULL_HANDLE;
    }
  }
  Vulkan.FramebufferObjects.clear();
}

25.Tutorial03.cpp,函数 ChildClear()

像往常一样,首先检查是否有设备。 如果没有设备,就没有资源。 接下来等待设备空闲下来,并删除所有已创建的资源。 我们首先通过调用 vkFreeCommandBuffers() 函数,开始删除命令缓冲区。 接下来通过 vkDestroyCommandPool() 函数毁坏命令池,然后破坏图形管道。 该步骤可通过 vkDestroyPipeline() 函数完成。 然后调用 vkDestroyRenderPass() 函数,以释放渲染通道的句柄。 最后删除与交换链图像相关的所有帧缓冲区和图像视图。

破坏对象之前,首先检查是否创建了特定资源。 如果没有,我们则跳过破环资源这一流程。

结论

在本教程中,我们创建了包含一个子通道的渲染通道。 接下来创建交换链图像图像视图和帧缓冲区。 其中一个最重要的部分是创建图形管道,因为这一过程要求我们准备大量数据。 我们需要创建着色器模块,并描述应该绑定图形管道时处于活跃状态的着色器阶段。 需要准备与输入顶点、布局,以及将其汇编成拓扑等相关的信息。 还需要准备视口、光栅化、多点采样和颜色混合信息。 然后创建简单的管道布局,之后才能创建管道。 接下来我们创建了命令池,并为各交换链图像分配了命令缓冲区。 记录在命令缓冲区中的操作涉及设置图像内存壁垒、启动渲染通道、绑定图形通道,以及绘制。 接下来结束渲染通道,并设置另一图像内存壁垒。 执行绘制的方式与之前教程 (2) 中所述的相同。

在接下来的教程中,我们将学习顶点属性、图像和缓冲区等相关知识。


请前往: 没有任何秘密的 API: Vulkan* 简介第 4 部分: 顶点属性


声明

本文件不构成对任何知识产权的授权,包括明示的、暗示的,也无论是基于禁止反言的原则或其他。

英特尔明确拒绝所有明确或隐含的担保,包括但不限于对于适销性、特定用途适用性和不侵犯任何权利的隐含担保,以及任何对于履约习惯、交易习惯或贸易惯例的担保。

本文包含尚处于开发阶段的产品、服务和/或流程的信息。 此处提供的信息可随时改变而毋需通知。 联系您的英特尔代表,了解最新的预测、时间表、规格和路线图。

本文件所描述的产品和服务可能包含使其与宣称的规格不符的设计缺陷或失误。 英特尔提供最新的勘误表备索。

如欲获取本文提及的带订购编号的文档副本,可致电 1-800-548-4725,或访问 www.intel.com/design/literature.htm

该示例源代码根据英特尔示例源代码许可协议发布。

英特尔和 Intel 标识是英特尔在美国和/或其他国家的商标。

*其他的名称和品牌可能是其他所有者的资产。

英特尔公司 © 2016 年版权所有。

产品和性能信息

1

英特尔的编译器针对非英特尔微处理器的优化程度可能与英特尔微处理器相同(或不同)。这些优化包括 SSE2、SSE3 和 SSSE3 指令集和其他优化。对于在非英特尔制造的微处理器上进行的优化,英特尔不对相应的可用性、功能或有效性提供担保。该产品中依赖于微处理器的优化仅适用于英特尔微处理器。某些非特定于英特尔微架构的优化保留用于英特尔微处理器。关于此通知涵盖的特定指令集的更多信息,请参阅适用产品的用户指南和参考指南。

通知版本 #20110804