Ch2-2 创建渲染通道和帧缓冲

到三角形为止的流程

构建完渲染循环后,直到渲染出三角形为止需经历以下步骤:
1.创建渲染通道
2.为每张交换链图像创建对应的帧缓冲
3.书写顶点和片段着色器
4.创建着色器模组
5.创建渲染管线
6.在命令缓冲区中录制命令

以下Q&A仅为简要说明,以便理解所涉及的各个Vulkan对象的必要性,具体概念会在后文详述。

什么是渲染通道?
渲染通道(render pass)指定渲染过程中,所绑定帧缓冲的参数(格式、内存布局)及各个渲染步骤之间的关系。
举例而言,实现延迟渲染会涉及两个步骤,各为一个子通道(subpass),第一个通道中生成G-buffer,第二个通道中渲染到屏幕。

什么是帧缓冲?
帧缓冲是在一个渲染通道中所必要的一组图像附件(attachment)的集合。

什么是着色器?
着色器是在渲染过程中的可编程阶段运行的程序,书写完后在Vulkan程序中将其读取为着色器模组,然后提供给管线使用。

什么是管线?
这里只解释图形管线,一条图形管线指定渲染过程中所用的着色器模组及各种状态参数(混色方式、模板和深度测试方式、视口等)。

在命令缓冲区中录制命令时,首先开始一个渲染通道并同时指定所用的帧缓冲,然后绑定渲染管线,由此便指定了渲染所必须的所有参数。

EasyVulkan.hpp

创建EasyVulkan.hpp,这个文件会用于书写本套教程中一些常用的渲染通道和帧缓冲的创建函数。
然后在其中包含VKBase.h,并加入以下代码:

#include "VKBase.h"

using namespace vulkan;
const VkExtent2D& windowSize = graphicsBase::Base().SwapchainCreateInfo().imageExtent;

namespace easyVulkan {
    /*待填充*/
}
  • 之后会不止一处用到交换链图像大小,为其创建别名windowSize以简化书写。

渲染通道和帧缓冲

参见Ch3-4 渲染通道和帧缓冲,我在该节中具体叙述了何为渲染通道及帧缓冲,并对它们进行了简单封装,请先阅览该节并完成封装。
EasyVulkan.hpp,easyVulkan命名空间中,定义renderPassWithFramebuffers结构体,将渲染通道与其对应的帧缓冲放在一起:

namespace easyVulkan {
    using namespace vulkan;
    struct renderPassWithFramebuffers {
        renderPass renderPass;
        std::vector<framebuffer> framebuffers;
    };
}

创建一个最简单的渲染通道

在easyVulkan命名空间中,定义函数CreateRpwf_Screen(),这个函数会创建一个直接渲染到交换链图像,且不做深度测试等任何测试的渲染通道,及其对应的帧缓冲:

namespace easyVulkan {
    using namespace vulkan;
    struct renderPassWithFramebuffers {
        renderPass renderPass;
        std::vector<framebuffer> framebuffers;
    };
    const auto& CreateRpwf_Screen() {
        static renderPassWithFramebuffers rpwf;

        /*待后续填充*/

        return rpwf;
    }
}
  • renderPassWithFramebuffers类型的对象rpwf(rpwf当然是renderPassWithFramebuffers的缩写)被定义为函数内的静态变量,并且函数返回其常引用。

首先描述图像附件,这里描述的是交换链图像:

VkAttachmentDescription attachmentDescription = {
    .format = graphicsBase::Base().SwapchainCreateInfo().imageFormat,
    .samples = VK_SAMPLE_COUNT_1_BIT,
    .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR,
    .storeOp = VK_ATTACHMENT_STORE_OP_STORE,
    .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED,
    .finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR
};
  • 将initialLayout设定为VK_IMAGE_LAYOUT_UNDEFINED可能导致丢弃图像附件原有的内容,因为我们会在每个渲染循环中清空图像,此举无妨。

  • 将finalLayout设定为VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,以便将交换链图像用于呈现。

只有一个子通道,该子通道只使用一个颜色附件,于是很容易地写完子通道描述:

VkAttachmentReference attachmentReference = { 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL };
VkSubpassDescription subpassDescription = {
    .pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
    .colorAttachmentCount = 1,
    .pColorAttachments = &attachmentReference
};

书写子通道依赖,覆盖渲染通道开始时的隐式依赖:

VkSubpassDependency subpassDependency = {
    .srcSubpass = VK_SUBPASS_EXTERNAL,
    .dstSubpass = 0,
    .srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
    .dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,//不早于提交命令缓冲区时等待semaphore对应的waitDstStageMask
    .srcAccessMask = 0,
    .dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
    .dependencyFlags = VK_DEPENDENCY_BY_REGION_BIT
};

这里的srcSubpass为VK_SUBPASS_EXTERNAL,那么前一次使用同一图像附件的渲染也可以被纳入同步范围,确保前一次的颜色附件输出(color attachment output)阶段在dstStageMask指定的阶段前完成。由于每次向交换链图像渲染时,先前的内容都可以被舍弃,srcAccessMask可以为0(srcAccessMask的用途是用来确保写入操作的结果可以被后续操作正确访问)。
注:由于(在这套教程的示例中)在每次渲染循环中使用同个栅栏对同个命令缓冲区进行同步,执行当前渲染命令时,前一次渲染必定已经结束,因此这里的srcStageMask其实没有实质作用。填写为VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT更多是出于演示子通道依赖用法的目的。

图像内存布局的转换最迟可以在这里dstStageMask指定的阶段发生,而这件事当然得发生在获取到交换链图像之后,那么dstStageMask便不得早于提交命令缓冲区时等待(vkAcquireNextImageKHR(...)所置位的)信号量对应的waitDstStageMask。

书写渲染通道的创建信息,以创建渲染通道:

VkRenderPassCreateInfo renderPassCreateInfo = {
    .attachmentCount = 1,
    .pAttachments = &attachmentDescription,
    .subpassCount = 1,
    .pSubpasses = &subpassDescription,
    .dependencyCount = 1,
    .pDependencies = &subpassDependency
};
rpwf.renderPass.Create(renderPassCreateInfo);

创建一组最简单的帧缓冲

为每张交换链图像创建帧缓冲,首先将rpwf.framebuffers的元素数量resize(...)到与交换链图像一样多:

rpwf.framebuffers.resize(graphicsBase::Base().SwapchainImageCount());

填写帧缓冲的创建信息,为每张交换链图像创建对应的帧缓冲:

VkFramebufferCreateInfo framebufferCreateInfo = {
    .renderPass = rpwf.renderPass,
    .attachmentCount = 1,
    .width = windowSize.width,
    .height = windowSize.height,
    .layers = 1
};
for (size_t i = 0; i < graphicsBase::Base().SwapchainImageCount(); i++) {
    VkImageView attachment = graphicsBase::Base().SwapchainImageView(i);
    framebufferCreateInfo.pAttachments = &attachment;
    rpwf.framebuffers[i].Create(framebufferCreateInfo);
}

由于帧缓冲的大小与交换链图像相关,重建交换链时也会需要重建帧缓冲,于是将创建和销毁帧缓冲的代码扔进各自的lambda表达式,以用作回调函数:

auto CreateFramebuffers = [] {
    rpwf.framebuffers.resize(graphicsBase::Base().SwapchainImageCount());
    VkFramebufferCreateInfo framebufferCreateInfo = {
        .renderPass = rpwf.renderPass,
        .attachmentCount = 1,
        .width = windowSize.width,
        .height = windowSize.height,
        .layers = 1
    };
    for (size_t i = 0; i < graphicsBase::Base().SwapchainImageCount(); i++) {
        VkImageView attachment = graphicsBase::Base().SwapchainImageView(i);
        framebufferCreateInfo.pAttachments = &attachment;
        rpwf.framebuffers[i].Create(framebufferCreateInfo);
    }
};
auto DestroyFramebuffers = [] {
    rpwf.framebuffers.clear();//清空vector中的元素时会逐一执行析构函数
};
/*待后续填充*/
graphicsBase::Base().AddCallback_CreateSwapchain(CreateFramebuffers);
graphicsBase::Base().AddCallback_DestroySwapchain(DestroyFramebuffers);

最后调用CreateFramebuffers()来创建帧缓冲,整个CreateRpwf_Screen()函数如下:

const auto& CreateRpwf_Screen() {
    static renderPassWithFramebuffers rpwf;

    VkAttachmentDescription attachmentDescription = {
        .format = graphicsBase::Base().SwapchainCreateInfo().imageFormat,
        .samples = VK_SAMPLE_COUNT_1_BIT,
        .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR,
        .storeOp = VK_ATTACHMENT_STORE_OP_STORE,
        .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED,
        .finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR
    };
    VkAttachmentReference attachmentReference = { 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL };
    VkSubpassDescription subpassDescription = {
        .pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
        .colorAttachmentCount = 1,
        .pColorAttachments = &attachmentReference
    };
    VkSubpassDependency subpassDependency = {
        .srcSubpass = VK_SUBPASS_EXTERNAL,
        .dstSubpass = 0,
        .srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
        .dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
        .srcAccessMask = 0,
        .dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
        .dependencyFlags = VK_DEPENDENCY_BY_REGION_BIT
    };
    VkRenderPassCreateInfo renderPassCreateInfo = {
        .attachmentCount = 1,
        .pAttachments = &attachmentDescription,
        .subpassCount = 1,
        .pSubpasses = &subpassDescription,
        .dependencyCount = 1,
        .pDependencies = &subpassDependency
    };
    rpwf.renderPass.Create(renderPassCreateInfo);

    auto CreateFramebuffers = [] {
        rpwf.framebuffers.resize(graphicsBase::Base().SwapchainImageCount());
        VkFramebufferCreateInfo framebufferCreateInfo = {
            .renderPass = rpwf.renderPass,
            .attachmentCount = 1,
            .width = windowSize.width,
            .height = windowSize.height,
            .layers = 1
        };
        for (size_t i = 0; i < graphicsBase::Base().SwapchainImageCount(); i++) {
            VkImageView attachment = graphicsBase::Base().SwapchainImageView(i);
            framebufferCreateInfo.pAttachments = &attachment;
            rpwf.framebuffers[i].Create(framebufferCreateInfo);
        }
    };
    auto DestroyFramebuffers = [] {
        rpwf.framebuffers.clear();
    };
    CreateFramebuffers();

    ExecuteOnce(rpwf); //防止再次调用本函数时,重复添加回调函数
    graphicsBase::Base().AddCallback_CreateSwapchain(CreateFramebuffers);
    graphicsBase::Base().AddCallback_DestroySwapchain(DestroyFramebuffers);
    return rpwf;
}
  • 如果你需要重建逻辑设备,那么需要一并重建与之相关的Vulkan对象,则重建逻辑设备后势必会再次调用CreateRpwf_Screen(),而添加回调函数的两条语句只应该被执行一次,用Ch2-0中定义的宏ExecuteOnce来确保这一点。

更新主函数并测试

现在可以去主函数中调用easyVulkan::CreateRpwf_Screen(),并测试一下清屏值是否有效了:

#include "GlfwGeneral.hpp"
#include "EasyVulkan.hpp"

int main() {
    if (!InitializeWindow({1280,720}))
        return -1;

    /*新增*/const auto& [renderPass, framebuffers] = easyVulkan::CreateRpwf_Screen();

    fence fence;
    semaphore semaphore_imageIsAvailable;
    semaphore semaphore_renderingIsOver;

    commandBuffer commandBuffer;
    commandPool commandPool(graphicsBase::Base().QueueFamilyIndex_Graphics(), VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT);
    commandPool.AllocateBuffers(commandBuffer);

    /*新增*/VkClearValue clearColor = { .color = { 1.f, 0.f, 0.f, 1.f } };//红色

    while (!glfwWindowShouldClose(pWindow)) {
        while (glfwGetWindowAttrib(pWindow, GLFW_ICONIFIED))
            glfwWaitEvents();

        graphicsBase::Base().SwapImage(semaphore_imageIsAvailable);
        //因为帧缓冲与所获取的交换链图像一一对应,获取交换链图像索引
        /*新增*/auto i = graphicsBase::Base().CurrentImageIndex();

        commandBuffer.Begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT);
        /*新增,开始渲染通道*/renderPass.CmdBegin(commandBuffer, framebuffers[i], { {}, windowSize }, clearColor);
        /*渲染命令,待填充*/
        /*新增,结束渲染通道*/renderPass.CmdEnd(commandBuffer);
        commandBuffer.End();

        graphicsBase::Base().SubmitCommandBuffer_Graphics(commandBuffer, semaphore_imageIsAvailable, semaphore_renderingIsOver, fence);
        graphicsBase::Base().PresentImage(semaphore_renderingIsOver);

        glfwPollEvents();
        TitleFps();

        fence.WaitAndReset();
    }
    TerminateWindow();
    return 0;
}

上文代码中使用的清屏值为纯红色,运行程序,你应该会看到一个全红的画面。