Vulkan教程 - 11 幀緩衝和命令緩衝

幀緩衝我們前面的章節已經討論很多了,而且我們已經建立了渲染通道,以便得到單個的幀緩衝,有着和交換鏈圖像一樣的格式,但是我們還沒有真正創建什麼東西呢。

在渲染通道創建過程中指定的附件通過把它們包裝成一個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!");
}

下一章我們會寫一些代碼,放在主循環中,獲取交換鏈圖像,執行正確的命令緩衝並返回完成的圖像到交換鏈中。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章