帧缓冲我们前面的章节已经讨论很多了,而且我们已经建立了渲染通道,以便得到单个的帧缓冲,有着和交换链图像一样的格式,但是我们还没有真正创建什么东西呢。

        在渲染通道创建过程中指定的附件通过把它们包装成一个VkFramebuffer对象来绑定到一起。帧缓冲对象引用了所有表示附件的VkImageView对象。我们这里就一个附件,即颜色附件。但是,我们为了这个附件要用的图像依赖于当我们获取图像用于呈现的时候交换链返回的是哪个图像。也就是说我们要为交换链中所有的图像创建一个帧缓冲,然后使用一个和绘制时获取的图像对应的图像。

        创建一个std::vector类型的类成员,存储帧缓冲用:

std::vector<VkFramebuffer> swapChainFramebuffers;

        我们会在一个新的方法中为这个数组创建对象,这个方法是createFramebuffers,在initVulkan方法的创建图形管线之后调用。

        一开始要调整容器大小以容纳所有帧缓冲:

void createFramebuffers() {
    swapChainFramebuffers.resize(swapChainImageViews.size());
}

        我们接着会遍历图像视图并从中创建帧缓冲:

for (size_t i = 0; i < swapChainImageViews.size(); i++) {
    VkImageView attachments[] = {
        swapChainImageViews[i]
    };

    VkFramebufferCreateInfo framebufferInfo = {};
    framebufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
    framebufferInfo.renderPass = renderPass;
    framebufferInfo.attachmentCount = 1;
    framebufferInfo.pAttachments = attachments;
    framebufferInfo.width = swapChainExtent.width;
    framebufferInfo.height = swapChainExtent.height;
    framebufferInfo.layers = 1;

    if (vkCreateFramebuffer(device, &framebufferInfo, nullptr, &swapChainFramebuffers[i]) != VK_SUCCESS) {
        throw std::runtime_error("failed to create framebuffer!");
    }
}

        正如你看到的,创建帧缓冲是比较直白的。首先需要指定帧缓冲和哪个renderPass兼容。只能用兼容的,也就是说它们使用相同个数和类型的附件。

        attachmentCount和pAttachments参数指定在渲染通道pAttachment数组中要绑定到各自附件描述的VkImageView对象。

        width和height参数不用解释,layers指的是图像数组中的层的个数。我们这里交换链图像是单图像的,所以层个数就是1。我们应该在图像视图和渲染通道之前删除帧缓冲,但是要在完成渲染之后:

for (auto framebuffer : swapChainFramebuffers) {
    vkDestroyFramebuffer(device, framebuffer, nullptr);
}

        现在已经完成了渲染所需的各项要求,下一章我们将写一个真正的绘制命令。

        Vulkan中的命令,比如绘制操作和内存转移,并不是直接用方法调用来执行的。你必须把所有操作记录到命令缓冲对象中。这么做的优势是建立绘制命令这种困难的工作能够提前做好,且是多线程做的。这样,你就能告诉Vulkan来执行主循环中的命令了。

        在我们创建命令缓冲之前,必须要创建一个命令池。命令池管理用于存储缓冲的内存,命令缓冲也是从它们中分配的。添加一个新的类成员来存储VkCommandPool:

VkCommandPool commandPool;

        然后创建一个新的方法createCommandPool,然后从initVulkan中调用,调用时机是在创建帧缓冲之后。命令池创建只需要两个参数:

QueueFamilyIndices queueFamilyIndices = findQueueFamilies(physicalDevice);

VkCommandPoolCreateInfo poolInfo = {};
poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
poolInfo.queueFamilyIndex = queueFamilyIndices.graphicsFamily.value();
poolInfo.flags = 0;

        命令缓冲通过提交到某个设备队列上执行,如我们获取到的图形和呈现队列。每个命令池只能分配单一类型队列中提交的命令缓冲。我们会记录命令来进行绘制,这也是为什么我们选择了图形队列族。

        命令池有两种可能的标记:

        VK_COMMAND_POOL_CREATE_TRANSIENT_BIT:表明命令缓冲经常用新的命令记录(可能改变内存分配行为);

        VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT:允许命令缓冲逐个记录,没有这个标记则它们会统一进行重置。

        我们仅仅在程序开始的时候记录命令缓冲,然后在主循环中把它们执行很多次,所以我们并不会用到上面的两种标记:

if (vkCreateCommandPool(device, &poolInfo, nullptr, &commandPool) != VK_SUCCESS) {
    throw std::runtime_error("failed to create command pool!");
}

        使用vkCreateCommandPool方法完成命令池创建。程序整个生命周期都会用到命令,因此它们要在结束的时候销毁,就放在cleanup的第一行:

vkDestroyCommandPool(device, commandPool, nullptr);

        现在我们可以分配内存缓冲了,另外记录它们的绘制命令。因为有一个绘制命令涉及到绑定正确的VkFramebuffer,我们要为交换链中的每一个图像再次记录一个命令缓冲。为此创建一个VkCommandBuffer列表,作为类成员。命令缓冲会在命令池销毁的时候自动释放,所以不用在cleanup方法中进行显式处理。

std::vector<VkCommandBuffer> commandBuffers;

        现在开始创建一个createCommandBuffers方法,它负责分配和记录每个交换链图像的命令:

void createCommandBuffers() {
    commandBuffers.resize(swapChainFramebuffers.size());
}

        该方法就在initVulkan的最后调用。

        命令缓冲分配用的是vkAllocateCommandBuffers方法,接收一个VkCommandBufferAllocateInfo结构体作为参数,指定命令池和要分配的缓冲个数:

VkCommandBufferAllocateInfo allocInfo = {};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.commandPool = commandPool;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = (uint32_t)commandBuffers.size();

if (vkAllocateCommandBuffers(device, &allocInfo, commandBuffers.data()) != VK_SUCCESS) {
    throw std::runtime_error("failed to allocate command buffers!");
}

        level参数表明分配的命令缓冲是是主命令缓冲,还是次要命令缓冲:

        VK_COMMAND_BUFFER_LEVEL_PRIMARY:可以提交到队列执行,但是不能从其他命令缓冲中调用;

        VK_COMMAND_BUFFER_LEVEL_SECONDARY:不能直接提交,但是可以从主命令缓冲中调用。

        我们不会用次要命令缓冲,但是你可以想象下,从主命令缓冲中重用通用的操作是很有用的。

        我们用vkBeginCommandBuffer开始记录命令缓冲,传一个小结构体VkCommandBufferBeginInfo作为其参数,指定一些命令缓冲使用的细节信息:

for (size_t i = 0; i < commandBuffers.size(); i++) {
    VkCommandBufferBeginInfo beginInfo = {};
    beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
    beginInfo.flags = VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT;
    beginInfo.pInheritanceInfo = nullptr;  // optional

    if (vkBeginCommandBuffer(commandBuffers[i], &beginInfo) != VK_SUCCESS) {
        throw std::runtime_error("failed to begin recording command buffer!");
    }
}

        flags标记参数表明了我们如何使用命令缓冲,有以下可选项:

        VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT:命令缓冲将一旦执行后就被记录;

        VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT:这是完全在一个渲染通道中的次命令缓冲;

         VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT:命令缓冲可以在挂起执行的情况下重新提交。

        我们用了最后一个标记,因为我们可能在最后一帧还没完成的时候已经为下一帧计划绘制命令了。pInheritanceInfo参数只和次命令缓冲有关。它指定了从调用的主命令缓冲的哪个状态继承。

        如果命令缓冲已经记录了一次,那么调用vkBeginCommandBuffer会隐式地重置它。后面就不可能将命令追加到缓冲中了。

        绘制就从用vkCmdBeginRenderPass开启渲染通道开始。渲染通道使用一些VkRenderPassBeginInfo结构体中的参数配置:

VkRenderPassBeginInfo renderPassInfo = {};
renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
renderPassInfo.renderPass = renderPass;
renderPassInfo.framebuffer = swapChainFramebuffers[i];

        第一个参数是渲染通道自身和要绑定的附件。我们为每个交换链图像创建一个帧缓冲,把它作为颜色附件。

renderPassInfo.renderArea.offset = { 0, 0 };
renderPassInfo.renderArea.extent = swapChainExtent;

        接下来的这两个参数定义了渲染区域大小,渲染区域定义了着色器加载和存储的地点,在此之外的像素的值将会是未定义的。它应该和附件的大小一致,以便取得最佳性能。

VkClearValue clearColor = { 0.0f, 0.0f, 0.0f, 1.0f };
renderPassInfo.clearValueCount = 1;
renderPassInfo.pClearValues = &clearColor;

        最后的两个参数定义了VK_ATTACHMENT_LOAD_OP_CLEAR要用的清除值,我们用作颜色附件的加载操作。这里定义的清除颜色就是一个很简单的完全不透明的黑色。

vkCmdBeginRenderPass(commandBuffers[i], &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE);

        现在渲染通道可以开始了,所有的记录命令的功能都有vkCmd前缀。它们都返回void,所以直到我们完成记录之前都不会有错误处理。

        每个命令的第一个参数一直都是要记录命令的命令缓冲。第二个参数明确了我们提供的渲染通道的细节信息。最终的参数控制渲染通道内的绘制命令如何提供。有以下两种值可选:

        VK_SUBPASS_CONTENTS_INLINE:渲染通道命令将会嵌入到主命令缓冲中,次命令缓冲不会执行;

        VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS:渲染通道命令将会从次命令缓冲执行。

        我们不用次命令缓冲,所以这里就用第一个选项。

        现在我们可以绑定图形管线了:

vkCmdBindPipeline(commandBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline);

        第二个参数说明了该管线对象是否是一个图形或者计算管线。我们现在告诉了Vulkan在图形管线中执行哪个操作以及在片段着色器中使用哪个附件,所以现在剩下的就是告诉它绘制三角形:

vkCmdDraw(commandBuffers[i], 3, 1, 0, 0);

        实际的vkCmdDraw有些虎头蛇尾,但是它太简单了,因为所有的信息我们都提前说明了。除了命令缓冲外它还有以下参数:

        vertexCount:尽管我们没用顶点缓冲,但是严格来说还是有三个顶点要绘制的;

        instanceCount:用于实例渲染,如果没有这么做的话就设置为1;

        firstVertex:作为顶点缓冲的偏置,定义了gl_VertexIndex的最小值;

        firstInstance:作为实例渲染偏置,定义了gl_InstanceIndex的最小值。

        现在渲染通道可以结束了:

vkCmdEndRenderPass(commandBuffers[i]);

        现在已经完成了命令缓冲的记录:

if (vkEndCommandBuffer(commandBuffers[i]) != VK_SUCCESS) {
    throw std::runtime_error("failed to record command buffer!");
}

        下一章我们会写一些代码,放在主循环中,获取交换链图像,执行正确的命令缓冲并返回完成的图像到交换链中。