Ch6-2 动态渲染

动态渲染(dynamic rendering),简而言之是让渲染通道对象和帧缓冲一起滚蛋的渲染方式。
所谓动态,指渲染相关的参数大都在渲染开始时指定。

不需要渲染通道对象和帧缓冲,意味着省去了对它们进行管理的麻烦!尤其是,不再需要“因窗口大小改变而在重建交换链后重建渲染通道和帧缓冲”。
渲染通道对象在子通道开始和结束时会执行子通道依赖,并进行图像内存布局转换。而在动态渲染中,原先子通道依赖的作用和图像内存布局转换得通过图像内存屏障来实行。

动态渲染相比渲染通道对象有一个显著劣势:不能使用输入附件。而输入附件的设备内存往往可以被指定为惰性分配以优化内存开销。

Note

渲染通道的定义
留意到上文我使用了渲染通道对象(render pass object)这个词,而非直接说渲染通道。
Vulkan标准中将动态渲染也定义为一种渲染通道实例。

本节在之前Ch2中绘制三角形代码的基础上,使用动态渲染的方法来进行绘制,步骤如下:
1.开启设备特性
2.更改管线创建信息
3.定义动态渲染前后的内存屏障
4.调用命令,开始/结束动态渲染

开启动态渲染设备特性

动态渲染是在Vulkan1.2.203版本中引入的功能,Vulkan1.3版本之前由设备级扩展"VK_KHR_dynamic_rendering"提供,从1.3版本开始为Vulkan的核心功能。

取得Vulkan在本机上可用的最高版本,只有1.0.XXX或1.1.XXX的话直接返回。
Vulkan版本达到1.3的话,设备特性已在graphicsBase::CreateDevice(...)中“能开尽开”,主函数中只检查设备特性是否支持:

int main() {
    graphicsBase::Base().UseLatestApiVersion();
    if (graphicsBase::Base().ApiVersion() < VK_API_VERSION_1_2)
        return -1;
    if (graphicsBase::Base().ApiVersion() < VK_API_VERSION_1_3) {
        /*待填充*/
    }
    else
        if (!InitializeWindow({ 1280, 720 }) ||
            !graphicsBase::Base().PhysicalDeviceVulkan13Features().dynamicRendering)
            return -1;

    /*...*/
}

若Vulkan版本达到1.2但未到1.3,使用以下代码通过扩展开启:

int main() {
    graphicsBase::Base().UseLatestApiVersion();
    if (graphicsBase::Base().ApiVersion() < VK_API_VERSION_1_2)
        return -1;
    if (graphicsBase::Base().ApiVersion() < VK_API_VERSION_1_3) {
        graphicsBase::Base().AddDeviceExtension(VK_KHR_DYNAMIC_RENDERING_EXTENSION_NAME);
        VkPhysicalDeviceDynamicRenderingFeatures physicalDeviceDynamicRenderingFeatures = {
            VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_FEATURES,
        };
        graphicsBase::Base().AddNextStructure_PhysicalDeviceFeatures(physicalDeviceDynamicRenderingFeatures);
        if (!InitializeWindow({ 1280, 720 }) ||
            !physicalDeviceDynamicRenderingFeatures.dynamicRendering)
            return -1;
    }
    else
        if (!InitializeWindow({ 1280, 720 }) ||
            !graphicsBase::Base().PhysicalDeviceVulkan13Features().dynamicRendering)
            return -1;

    /*...*/
}

更改管线创建信息

为什么要改管线创建信息?先来看看原先的管线创建信息:

graphicsPipelineCreateInfoPack pipelineCiPack;
pipelineCiPack.createInfo.layout = pipelineLayout_triangle;
pipelineCiPack.createInfo.renderPass = RenderPassAndFramebuffers().renderPass;
pipelineCiPack.inputAssemblyStateCi.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
pipelineCiPack.viewports.emplace_back(0.f, 0.f, float(windowSize.width), float(windowSize.height), 0.f, 1.f);
pipelineCiPack.scissors.emplace_back(VkOffset2D{}, windowSize);
pipelineCiPack.multisampleStateCi.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
pipelineCiPack.colorBlendAttachmentStates.push_back({ .colorWriteMask = 0b1111 });
pipelineCiPack.UpdateAllArrays();
pipelineCiPack.createInfo.stageCount = 2;
pipelineCiPack.createInfo.pStages = shaderStageCreateInfos_triangle;

这里有个渲染通道对象在,而动态渲染中不用渲染通道对象,相应地在VkGraphicsPipelineCreateInfo的pNext链中包含一个VkPipelineRenderingCreateInfoKHR结构体:

struct VkPipelineRenderingCreateInfo 的成员说明

VkStructureType sType

结构体的类型,本处必须是VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2

const void* pNext

如有必要,指向一个用于扩展该结构体的结构体

uint32_t viewMask

若开启了多视口,指定在哪些视口对应的图层上进行渲染,各个bit对应多层帧缓冲相应索引的图层

uint32_t colorAttachmentCount

颜色附件的数量

const VkFormat* pColorAttachmentFormats

指向VkFormat的数组,用于描述届时各个颜色附件的格式

VkFormat depthAttachmentFormat

深度附件的格式,若届时不使用深度附件,留作VK_FORMAT_UNDEFINED即可

VkFormat stencilAttachmentFormat

模板附件的格式,若届时不使用模板附件,留作VK_FORMAT_UNDEFINED即可

  • 这里允许使用两张不同的图像各自作为深度/模板附件。

颜色附件的数量为1,格式可从交换链创建信息取得。
不使用深度或模板附件,VK_FORMAT_UNDEFINED即0,于是:

VkPipelineRenderingCreateInfoKHR pipelineRenderingCreateInfo = {
    .sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO,
    .colorAttachmentCount = 1,
    .pColorAttachmentFormats = &graphicsBase::Base().SwapchainCreateInfo().imageFormat
};
graphicsPipelineCreateInfoPack pipelineCiPack;
pipelineCiPack.createInfo.pNext = &pipelineRenderingCreateInfo;
pipelineCiPack.createInfo.layout = pipelineLayout_triangle;
//pipelineCiPack.createInfo.renderPass = RenderPassAndFramebuffers().renderPass;
pipelineCiPack.inputAssemblyStateCi.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
pipelineCiPack.viewports.emplace_back(0.f, 0.f, float(windowSize.width), float(windowSize.height), 0.f, 1.f);
pipelineCiPack.scissors.emplace_back(VkOffset2D{}, windowSize);
pipelineCiPack.multisampleStateCi.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
pipelineCiPack.colorBlendAttachmentStates.push_back({ .colorWriteMask = 0b1111 });
pipelineCiPack.UpdateAllArrays();
pipelineCiPack.createInfo.stageCount = 2;
pipelineCiPack.createInfo.pStages = shaderStageCreateInfos_triangle;

录制渲染前后的内存屏障

这一步要做的,就是把子通道依赖和渲染通道中图像内存布局的变化,改写成图像内存屏障。往下阅读之前,请确保已经明白了内存屏障的使用方法和作用(如果真的明白了的话,下面的反倒全成了复习+废话)。

先看看图像内存屏障有哪些参数要填:

commandBuffer.Begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT);
//渲染开始前的内存屏障
VkImageMemoryBarrier imageMemoryBarrier = {
    .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
    .pNext = nullptr,
    .srcAccessMask = /*待填充*/,
    .dstAccessMask = /*待填充*/,
    .oldLayout = /*待填充*/,
    .newLayout = /*待填充*/,
    .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
    .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
    .image = graphicsBase::Base().SwapchainImage(i),
    .subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }
};
vkCmdPipelineBarrier(
    commandBuffer,
    /*srcStageMask*/,
    /*dstStageMask*/,
    /*dependencyFlags*/,
    0, nullptr,
    0, nullptr,
    1, &imageMemoryBarrier);

/*渲染,待填充*/

//渲染结束后的内存屏障
imageMemoryBarrier.srcAccessMask = /*待填充*/;
imageMemoryBarrier.dstAccessMask = /*待填充*/;
imageMemoryBarrier.oldLayout = /*待填充*/;
imageMemoryBarrier.newLayout = /*待填充*/;
vkCmdPipelineBarrier(
    commandBuffer,
    /*srcStageMask*/,
    /*dstStageMask*/,
    /*dependencyFlags*/,
    0, nullptr,
    0, nullptr,
    1, &imageMemoryBarrier);
commandBuffer.End();

先前创建渲染通道对象时,唯一的子通道开始时的依赖是这样的:

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
};
  • 前情回顾:这里这个.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT只是当初出于演示而这么写的,因为有其他粒度更粗的同步手段加之前面没有任何要阻塞的其他命令,那么实际上srcStageMask是啥都行。

srcStageMask按常规做法指定为VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,其余各项对应的直接照抄。
图像内存布局从VK_IMAGE_LAYOUT_UNDEFINED变为最适合渲染的VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL

VkImageMemoryBarrier imageMemoryBarrier = {
    .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
    .srcAccessMask = 0,
    .dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
    .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED,
    .newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
    .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
    .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
    .image = graphicsBase::Base().SwapchainImage(i),
    .subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }
};
vkCmdPipelineBarrier(
    commandBuffer,
    VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
    VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
    VK_DEPENDENCY_BY_REGION_BIT,
    0, nullptr,
    0, nullptr,
    1, &imageMemoryBarrier);

渲染结束后没有其他命令,命令提交后到呈现为止使用的是信号量来同步,所以渲染后图像内存屏障的dstAccessMask是0,dstStageMask按常规做法指定为VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT
图像内存布局转到最适合呈现的VK_IMAGE_LAYOUT_PRESENT_SRC_KHR

VkImageMemoryBarrier imageMemoryBarrier = {
    .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
    .srcAccessMask = 0,
    .dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
    .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED,
    .newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
    .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
    .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
    .image = graphicsBase::Base().SwapchainImage(i),
    .subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }
};
vkCmdPipelineBarrier(
    commandBuffer,
    VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
    VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
    VK_DEPENDENCY_BY_REGION_BIT,
    0, nullptr,
    0, nullptr,
    1, &imageMemoryBarrier);

/*渲染,待填充*/

imageMemoryBarrier.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
imageMemoryBarrier.dstAccessMask = 0;
imageMemoryBarrier.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
imageMemoryBarrier.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
vkCmdPipelineBarrier(
    commandBuffer,
    VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
    VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
    VK_DEPENDENCY_BY_REGION_BIT,
    0, nullptr,
    0, nullptr,
    1, &imageMemoryBarrier);
commandBuffer.End();

录制动态渲染命令

vkCmdBeginRendering(...)开始渲染,用vkCmdEndRendering(...)结束渲染。
先等等,在这之前还有个问题(不关心Vulkan版本的话请直接下拉到函数说明)。

你用的Vulkan SDK肯定是1.3版本以上,所以必定定义了这两个符号,在Vulkan1.3中也不需要干任何额外的事。
但是,如果Vulkan的运行版本是1.2,这两个函数不是核心功能的一部分,那么程序是无法在运行期找到这两个函数的实现的。跟PFN_vkCreateDebugUtilsMessengerEXT的情况类似,需要手动获取这两个函数的指针,因为他们从属于设备扩展功能,通过vkGetDeviceProcAddr(...)来取得:

PFN_vkCmdBeginRenderingKHR vkCmdBeginRendering =
    reinterpret_cast<PFN_vkCmdBeginRenderingKHR>(vkGetDeviceProcAddr(graphicsBase::Base().Device(), "vkCmdBeginRenderingKHR"));
PFN_vkCmdEndRenderingKHR vkCmdEndRendering =
    reinterpret_cast<PFN_vkCmdBeginRenderingKHR>(vkGetDeviceProcAddr(graphicsBase::Base().Device(), "vkCmdEndRenderingKHR"));

考虑同时兼容Vulkan1.2和1.3的代码:

int main() {
    PFN_vkCmdBeginRenderingKHR vkCmdBeginRendering = ::vkCmdBeginRendering;
    PFN_vkCmdEndRenderingKHR vkCmdEndRendering = ::vkCmdEndRendering;

    graphicsBase::Base().UseLatestApiVersion();
    if (graphicsBase::Base().ApiVersion() < VK_API_VERSION_1_2)
        return -1;
    if (graphicsBase::Base().ApiVersion() < VK_API_VERSION_1_3) {
        graphicsBase::Base().AddDeviceExtension(VK_KHR_DYNAMIC_RENDERING_EXTENSION_NAME);
        VkPhysicalDeviceDynamicRenderingFeatures physicalDeviceDynamicRenderingFeatures = {
            VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_FEATURES,
        };
        graphicsBase::Base().AddNextStructure_PhysicalDeviceFeatures(physicalDeviceDynamicRenderingFeatures);
        if (!InitializeWindow({ 1280, 720 }) ||
            !physicalDeviceDynamicRenderingFeatures.dynamicRendering)
            return -1;
        vkCmdBeginRendering = reinterpret_cast<PFN_vkCmdBeginRenderingKHR>(vkGetDeviceProcAddr(graphicsBase::Base().Device(), "vkCmdBeginRenderingKHR"));
        vkCmdEndRendering = reinterpret_cast<PFN_vkCmdBeginRenderingKHR>(vkGetDeviceProcAddr(graphicsBase::Base().Device(), "vkCmdEndRenderingKHR"));
    }
    else
        if (!InitializeWindow({ 1280, 720 }) ||
            !graphicsBase::Base().PhysicalDeviceVulkan13Features().dynamicRendering)
            return -1;

    /*...*/
}
  • ::前无名称表示全局命名空间,可用来消歧义。

调用vkCmdBeginRendering(...)和vkCmdEndRendering(...),俩函数本身的参数列表看以下代码应该一眼就能懂:

VkRenderingInfo renderingInfo = {
    .sType = VK_STRUCTURE_TYPE_RENDERING_INFO,
    /*待填充*/
};
vkCmdBeginRendering(commandBuffer, &renderingInfo);
vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_triangle);
vkCmdDraw(commandBuffer, 3, 1, 0, 0);
vkCmdEndRendering(commandBuffer);

struct VkRenderingInfo 的成员说明

VkStructureType sType

结构体的类型,本处必须是VK_STRUCTURE_TYPE_RENDERING_INFO

const void* pNext

如有必要,指向一个用于扩展该结构体的结构体

VkRenderingFlags flags

VkRect2D renderArea

渲染区域

uint32_t layerCount

帧缓冲的图层数,若viewMask非0,该项被无视

uint32_t viewMask

若开启了多视口,指定在哪些视口对应的图层上进行渲染,各个bit对应多层帧缓冲相应索引的图层

uint32_t colorAttachmentCount

颜色附件的数量

const VkRenderingAttachmentInfo* pColorAttachments

指向VkRenderingAttachmentInfo的数组,用于指定各个颜色附件及相关参数

const VkRenderingAttachmentInfo* pDepthAttachment

指向VkRenderingAttachmentInfo结构体,用于指定深度附件及相关参数

const VkRenderingAttachmentInfo* pStencilAttachment

指向VkRenderingAttachmentInfo结构体,用于指定模板附件及相关参数

版本要求

VkRenderingFlagBits 的枚举项

1.3

VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT 表示可以调用vkCmdExecuteCommands(...)执行二级命令缓冲区

1.3

VK_RENDERING_SUSPENDING_BIT 表示下一次vkCmdEndRendering(...)会将当前渲染通道挂起

1.3

VK_RENDERING_RESUMING_BIT 表示本次vkCmdBeginRendering(...)恢复挂起的渲染通道

  • VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BITVK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS不同,后者使得子通道中只能执行二级缓冲区。

  • 挂起和恢复可以分别录制在不同的命令缓冲区中,但这些命令缓冲区必须在同一批次中提交,提交顺序必须使得一个渲染通道首次开始时只挂起,最后一次时只恢复。
    这意味着你可以无序录制命令(在不同CPU线程中录制,或先录制“恢复”,再录制“挂起”)!只要最后命令提交顺序使得命令执行顺序合乎逻辑即可。这在某种程度上可以取代“在其他线程中录制二级命令缓冲区,然后在主线程中让一级调用”(以节省命令录制时间)的做法。

  • 恢复被挂起的渲染通道时,VkRenderingInfo的各成员除flags外,以及图像附件的诸多参数,必须与按命令提交顺序,前一个注明VK_RENDERING_SUSPENDING_BITvkCmdBeginRendering(...)命令相同。

  • 提示:恢复时当然可以同时注明VK_RENDERING_SUSPENDING_BITVK_RENDERING_RESUMING_BIT,表示之后还会挂起。

  • 从挂起到恢复之间(指命令执行先后,而非录制先后),不能有绘制、传输等动作命令,不能有同步命令(这使得绑定管线等状态命令是唯一可行的命令),不能开始其他的渲染通道。
    挂起时不会应用storeOp、内存布局转换等渲染结束时的选项或操作,恢复时同理。

struct VkRenderingAttachmentInfo 的成员说明

VkStructureType sType

结构体的类型,本处必须是VK_STRUCTURE_TYPE_RENDERING_INFO

const void* pNext

如有必要,指向一个用于扩展该结构体的结构体

VkImageView imageView

被用于渲染的图像附件的图像视图

VkImageLayout imageLayout

被用于渲染的图像附件的内存布局

VkResolveModeFlagBits resolveMode

如果进行多重采样,在这里指定解析模式

VkImageView resolveImageView

如果进行多重采样,在这里指定解析附件

VkImageLayout resolveImageLayout

如果进行多重采样,在这里指定解析附件的内存布局

VkAttachmentLoadOp loadOp

读取imageView对应的图像附件时进行的操作

VkAttachmentStoreOp storeOp

存储值到imageView对应的图像附件时的操作,或指定不在乎存储

VkClearValue clearValue

清屏值,用于loadOp为VK_ATTACHMENT_LOAD_OP_CLEAR的情况

  • resolveMode是个创建通常的渲染通道对象时没有的参数,但因为本节的重点不是多重渲染,在此不对VkResolveModeFlagBits进行解说(注:这套教程中预计不会对解析模式进行解说,有兴趣请自行了解)。

  • 如果resolveMode和resolveImageView都非0,imageView的底层图像被解析到resolveImageView的底层图像。storeOp不影响解析。

对于仅仅是画个三角形这种情况,应该是没有什么难点需要解说的:

VkRenderingAttachmentInfo colorAttachmentInfo = {
    .sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO,
    .imageView = graphicsBase::Base().SwapchainImageView(i),
    .imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
    .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR,
    .storeOp = VK_ATTACHMENT_STORE_OP_STORE,
    .clearValue = { .color = { 1.f, 0.f, 0.f, 1.f } }
};
VkRenderingInfo renderingInfo = {
    .sType = VK_STRUCTURE_TYPE_RENDERING_INFO,
    .renderArea = { {}, windowSize },
    .layerCount = 1,
    .colorAttachmentCount = 1,
    .pColorAttachments = &colorAttachmentInfo
};
vkCmdBeginRendering(commandBuffer, &renderingInfo);
vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_triangle);
vkCmdDraw(commandBuffer, 3, 1, 0, 0);
vkCmdEndRendering(commandBuffer);

运行程序,验证层没报错便对了。
本节中没有详细讲解的一些参数,今后可能在第八章的用例中进行讲解。

本节的示例代码参见:Ch6-2.hpp

多线程动态渲染

因为先前讲到了可以在多线程中挂起/恢复动态渲染,这里就简单扼要地给个示例。
对C++线程的简单、易用、基础的封装:QueueThread.h

#pragma once
#include <vector>
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <functional>

class queueThread {
    using work_t = std::function<void()>
    std::thread thread;
    bool joinable = true;
    std::queue<work_t> works;
    std::mutex mutex;
    std::condition_variable condition;
    //--------------------
    void DoWorks() {
        while (true) {
            std::unique_lock lock(mutex);
            condition.wait(lock, [this] { return !(works.empty() && joinable); });
            if (!joinable)
                return;
            works.front()();
            works.pop();
            condition.notify_one();
        }
    }
public:
    queueThread() {
        thread = std::thread(&queueThread::DoWorks, this);
    }
    queueThread(queueThread&&) = delete;
    ~queueThread() {
        std::unique_lock lock(mutex);
        condition.wait(lock, [this] { return works.empty(); });
        joinable = false;
        lock.unlock();
        condition.notify_one();
        thread.join();
    }
    //Non-const Function
    void PushWork(work_t work) {
        mutex.lock();
        works.push(work);
        mutex.unlock();
        condition.notify_one();
    }
    void Wait() {
        std::unique_lock lock(mutex);
        condition.wait(lock, [this] { return works.empty(); });
    }
};
  • 默认初始化后,用PushWork(...)将函数对象加入执行队列,用Wait()等待队列中所有函数对象执行结束。

上述代码就不做详细解释了,有兴趣的话请自己啃C++线程库。
包含该头文件后,在适当的位置默认初始化一个queueThread

int main() {
    PFN_vkCmdBeginRenderingKHR vkCmdBeginRendering = ::vkCmdBeginRendering;
    PFN_vkCmdEndRenderingKHR vkCmdEndRendering = ::vkCmdEndRendering;

    graphicsBase::Base().UseLatestApiVersion();
    if (graphicsBase::Base().ApiVersion() < VK_API_VERSION_1_2)
        return -1;
    if (graphicsBase::Base().ApiVersion() < VK_API_VERSION_1_3) { /*...*/ }
    else
        if (!InitializeWindow({ 1280, 720 }) ||
            !graphicsBase::Base().PhysicalDeviceVulkan13Features().dynamicRendering)
            return -1;

    queueThread queueThread;

    /*...*/
}

多线程录制命令缓冲区时,各个线程的命令缓冲区需要分配自各自不同的命令池,即每个线程要有专用的命令池(命令池的创建和命令缓冲区的分配在当前线程即可):

commandBuffer commandBuffers[2];
commandPool commandPool0(graphicsBase::Base().QueueFamilyIndex_Graphics(), VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT);
commandPool commandPool1(graphicsBase::Base().QueueFamilyIndex_Graphics(), VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT);
commandPool0.AllocateBuffers(commandBuffers[0]);
commandPool1.AllocateBuffers(commandBuffers[1]);

然后把渲染流程拆开成能在主线程和另一线程里分别录制的部分。
就清个屏画个三角形的这堆代码,也没啥好拆的,就拆成“在挂起前清屏”和“在恢复后画三角形”好了,在渲染循环前定义分别的lambda:

auto BeforeSuspending = [&] {
    commandBuffers[0].Begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT);

    VkImageMemoryBarrier imageMemoryBarrier = {
        .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,
        .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
        .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
        .image = graphicsBase::Base().SwapchainImage(graphicsBase::Base().CurrentImageIndex()),
        .subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }
    };
    vkCmdPipelineBarrier(
        commandBuffer,
        VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
        VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
        VK_DEPENDENCY_BY_REGION_BIT,
        0, nullptr,
        0, nullptr,
        1, &imageMemoryBarrier);

    VkRenderingAttachmentInfo colorAttachmentInfo = {
        .sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO,
        .imageView = graphicsBase::Base().SwapchainImageView(graphicsBase::Base().CurrentImageIndex()),
        .imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
        .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR,
        .storeOp = VK_ATTACHMENT_STORE_OP_STORE,
        .clearValue = { .color = { 1.f, 0.f, 0.f, 1.f } }
    };
    VkRenderingInfo renderingInfo = {
        .sType = VK_STRUCTURE_TYPE_RENDERING_INFO,
        .flags = VK_RENDERING_SUSPENDING_BIT,
        .renderArea = { {}, windowSize },
        .layerCount = 1,
        .colorAttachmentCount = 1,
        .pColorAttachments = &colorAttachmentInfo
    };
    vkCmdBeginRendering(commandBuffers[0], &renderingInfo);
    vkCmdEndRendering(commandBuffers[0]);

    commandBuffers[0].End();
};
auto AfterResuming = [&] {
    commandBuffers[1].Begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT);

    VkRenderingAttachmentInfo colorAttachmentInfo = {
        .sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO,
        .imageView = graphicsBase::Base().SwapchainImageView(graphicsBase::Base().CurrentImageIndex()),
        .imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
        .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR,
        .storeOp = VK_ATTACHMENT_STORE_OP_STORE,
        .clearValue = { .color = { 1.f, 0.f, 0.f, 1.f } }
    };
    VkRenderingInfo renderingInfo = {
        .sType = VK_STRUCTURE_TYPE_RENDERING_INFO,
        .flags = VK_RENDERING_RESUMING_BIT,
        .renderArea = { {}, windowSize },
        .layerCount = 1,
        .colorAttachmentCount = 1,
        .pColorAttachments = &colorAttachmentInfo
    };
    vkCmdBeginRendering(commandBuffers[1], &renderingInfo);
    vkCmdBindPipeline(commandBuffers[1], VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_triangle);
    vkCmdDraw(commandBuffers[1], 3, 1, 0, 0);
    vkCmdEndRendering(commandBuffers[1]);

    VkImageMemoryBarrier imageMemoryBarrier = {
        .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,
        .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
        .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
        .image = graphicsBase::Base().SwapchainImage(graphicsBase::Base().CurrentImageIndex()),
        .subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }
    };
    vkCmdPipelineBarrier(
        commandBuffer,
        VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
        VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
        VK_DEPENDENCY_BY_REGION_BIT,
        0, nullptr,
        0, nullptr,
        1, &imageMemoryBarrier);

    commandBuffers[1].End();
};
  • [&]的意思是指定默认捕获为引用捕获(这里只捕获了commandBuffers),实质上是存储了一个指针常量。

  • 妈的,早知道当初就写个graphicsBase::CurrentImageView()和graphicsBase::CurrentImage()了!

以防你对并发编程的问题不敏感,注意不能用下面的写法,多线程中对同一变量的并行读写没有明确顺序可言,让两个lambda共用这些信息结构体会导致错误:

VkRenderingAttachmentInfo colorAttachmentInfo = { /*...*/ };
VkRenderingInfo renderingInfo = { /*...*/ };
VkImageMemoryBarrier imageMemoryBarrier = { /*...*/ };

auto BeforeSuspending = [&] {
    /*...*/

    imageMemoryBarrier.srcAccessMask = 0;
    imageMemoryBarrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
    imageMemoryBarrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
    imageMemoryBarrier.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;

    /*...*/

    colorAttachmentInfo.imageView = graphicsBase::Base().SwapchainImageView(graphicsBase::Base().CurrentImageIndex());
    renderingInfo.flags = VK_RENDERING_SUSPENDING_BIT;
    renderingInfo.renderArea = { {}, windowSize };

    /*...*/
};
auto AfterResuming = [&] {
    /*...*/

    colorAttachmentInfo.imageView = graphicsBase::Base().SwapchainImageView(graphicsBase::Base().CurrentImageIndex());
    renderingInfo.flags = VK_RENDERING_RESUMING_BIT,
    renderingInfo.renderArea = { {}, windowSize };

    /*...*/

    imageMemoryBarrier.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
    imageMemoryBarrier.dstAccessMask = 0;
    imageMemoryBarrier.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
    imageMemoryBarrier.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;

    /*...*/
};

如果你觉得代码太冗余,或者日后改起来不方便的话,可以这么写,在每个lambda里复制一份lambda外的结构体,然后再修改这些结构体:

VkImageMemoryBarrier imageMemoryBarrier = {
    .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
    .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
    .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
    .subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }
};
VkRenderingAttachmentInfo colorAttachmentInfo = {
    .sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO,
    //imageView将在渲染循环中赋值
    .imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
    .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR,
    .storeOp = VK_ATTACHMENT_STORE_OP_STORE,
    .clearValue = { .color = { 1.f, 0.f, 0.f, 1.f } }
};
VkRenderingInfo renderingInfo = {
    .sType = VK_STRUCTURE_TYPE_RENDERING_INFO,
    .layerCount = 1,
    .colorAttachmentCount = 1,
    .pColorAttachments = &colorAttachmentInfo
};

auto BeforeSuspending = [&, imageMemoryBarrier, renderingInfo]() mutable {
    commandBuffers[0].Begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT);

    imageMemoryBarrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
    imageMemoryBarrier.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
    imageMemoryBarrier.image = graphicsBase::Base().SwapchainImage(graphicsBase::Base().CurrentImageIndex());
    vkCmdPipelineBarrier(
        commandBuffer,
        VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
        VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
        VK_DEPENDENCY_BY_REGION_BIT,
        0, nullptr,
        0, nullptr,
        1, &imageMemoryBarrier);

    renderingInfo.flags = VK_RENDERING_SUSPENDING_BIT;
    renderingInfo.renderArea = { {}, windowSize };
    vkCmdBeginRendering(commandBuffers[0], &renderingInfo);
    vkCmdEndRendering(commandBuffers[0]);

    commandBuffers[0].End();
};
auto AfterResuming = [&, imageMemoryBarrier, renderingInfo]() mutable {
    commandBuffers[1].Begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT);

    renderingInfo.flags = VK_RENDERING_RESUMING_BIT;
    renderingInfo.renderArea = { {}, windowSize };
    vkCmdBeginRendering(commandBuffers[1], &renderingInfo);
    vkCmdBindPipeline(commandBuffers[1], VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_triangle);
    vkCmdDraw(commandBuffers[1], 3, 1, 0, 0);
    vkCmdEndRendering(commandBuffers[1]);

    imageMemoryBarrier.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
    imageMemoryBarrier.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
    imageMemoryBarrier.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
    imageMemoryBarrier.image = graphicsBase::Base().SwapchainImage(graphicsBase::Base().CurrentImageIndex());
    vkCmdPipelineBarrier(
        commandBuffer,
        VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
        VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
        VK_DEPENDENCY_BY_REGION_BIT,
        0, nullptr,
        0, nullptr,
        1, &imageMemoryBarrier);

    commandBuffers[1].End();
};
  • [&, imageMemoryBarrier, renderingInfo]的意思是指定默认捕获为引用捕获,然后复制捕获imageMemoryBarrierrenderingInfomutable使得复制捕获的变量可修改。

复制捕获的变量被存储为函数对象的成员变量。每次执行函数对象时,复制捕获的变量不会被重置到捕获时的原值,所以这里诸如renderingInfo.flags = VK_RENDERING_RESUMING_BIT;等,等号右边是常量的语句其实是在每次执行该lambda的过程中进行着重复赋值。
(笔者并不太喜欢有捕获的lambda,不过要去掉捕获,还得对代码做一些我懒得说明的修改,就这样了!)

顺着上述代码继续往下写,两个lambda里所需的图像附件的信息是一致的,所以就不做复制捕获了。两个lambda中的renderingInfo.pColorAttachments指向了同一个VkRenderingAttachmentInfo,只需在渲染循环中更新colorAttachmentInfo.imageView即可:

/*...前面略*/

graphicsBase::Base().SwapImage(semaphore_imageIsAvailable);
colorAttachmentInfo.imageView = graphicsBase::Base().SwapchainImageView(graphicsBase::Base().CurrentImageIndex());

/*...后面略*/

AfterResuming先扔进另一个线程开始执行,然后在主线程中执行BeforeSuspending试试。
再在BeforeSuspending后等待另一个线程中的AfterResuming执行结束:

queueThread.PushWork(AfterResuming);
BeforeSuspending();
queueThread.Wait();

然后一次性提交两个缓冲区即可,commandBuffers[0]对应的是到挂起为止,commandBuffers[1]对应的是恢复之后,提交顺序上没有问题:

graphicsBase::Base().SwapImage(semaphore_imageIsAvailable);
colorAttachmentInfo.imageView = graphicsBase::Base().SwapchainImageView(graphicsBase::Base().CurrentImageIndex());

queueThread.PushWork(AfterResuming);
BeforeSuspending();
queueThread.Wait();

static constexpr VkPipelineStageFlags waitDstStage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
VkSubmitInfo submitInfo = {
    .waitSemaphoreCount = 1,
    .pWaitSemaphores = semaphore_imageIsAvailable.Address(),
    .pWaitDstStageMask = &waitDstStage,
    .commandBufferCount = 2,
    .pCommandBuffers = commandBuffers[0].Address(),
    .signalSemaphoreCount = 1,
    .pSignalSemaphores = semaphore_renderingIsOver.Address(),
};
graphicsBase::Base().SubmitCommandBuffer_Graphics(submitInfo, fence);
graphicsBase::Base().PresentImage(semaphore_renderingIsOver);

运行程序,还是一样的红背景和蓝三角形,如果蓝三角形没了,那说明某些地方顺序出问题了(可能先画三角形再清了屏)。
就清屏+画三角形这种程度的事,多线程录制命令对程序执行效率完全不会有提升。。。

示例代码参见:Ch6-2_MT.hpp