Vulkan填坑學習Day15—命令緩衝區

Vulkan 命令緩衝區

Vulkan 命令緩衝區,諸如繪製和內存操作相關命令,在Vulkan中不是通過函數直接調用的。我們需要在命令緩衝區對象中記錄我們期望的任何操作。這樣做的優點是可以提前在多線程中完成所有繪製命令相關的裝配工作,並在主線程循環結構中通知Vulkan執行具體的命令。

一、命令池

我們在使用任何command buffers之前需要創建命令對象池command pool。Command pools管理用於存儲緩衝區的內存,並從中分配命令緩衝區。添加新的類成員保存VkCommandPool:

VkCommandPool commandPool;

創建新的函數createCommandPool並在initVulkan函數創建完framebuffers後調用。

void initVulkan() {
    createInstance();
    setupDebugCallback();
    createSurface();
    pickPhysicalDevice();
    createLogicalDevice();
    createSwapChain();
    createImageViews();
    createRenderPass();
    createGraphicsPipeline();
    createFramebuffers();
    createCommandPool();
}

...

void createCommandPool() {

}

命令對象池創建僅僅需要兩個參數:

QueueFamilyIndices queueFamilyIndices = findQueueFamilies(physicalDevice);

VkCommandPoolCreateInfo poolInfo = {};
poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
poolInfo.queueFamilyIndex = queueFamilyIndices.graphicsFamily;
poolInfo.flags = 0; // Optional

命令緩衝區通過將其提交到其中一個設備隊列上來執行,如我們檢索的graphics和presentation隊列。每個命令對象池只能分配在單一類型的隊列上提交的命令緩衝區,換句話說要分配的命令需要與隊列類型一致。我們要記錄繪製的命令,這就說明爲什麼要選擇圖形隊列簇的原因。

有兩個標誌位用於command pools:

VK_COMMAND_POOL_CREATE_TRANSIENT_BIT: 提示命令緩衝區非常頻繁的重新記錄新命令(可能會改變內存分配行爲)
VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT: 允許命令緩衝區單獨重新記錄,沒有這個標誌,所有的命令緩衝區都必須一起重置

我們僅僅在程序開始的時候記錄命令緩衝區,並在主循環體main loop中多次執行,因此我們不會使用這些標誌。

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

通過vkCreateCommandPool函數完成command pool創建工作。它不需要任何特殊的參數設置。命令將被整個程序的生命週期使用以完成屏幕的繪製工作,所以對象池應該被在最後銷燬:

void cleanup() {
    vkDestroyCommandPool(device, commandPool, nullptr);

    ...
}

二、分配命令緩衝區

現在我們開始分配命令緩衝區並通過它們記錄繪製指令。因爲其中一個繪圖命令需要正確綁定VkFrameBuffer,我們實際上需要爲每一個交換鏈中的圖像記錄一個命令緩衝區。最後創建一個VkCommandBuffer對象列表作爲成員變量。命令緩衝區會在common pool銷燬的時候自動釋放系統資源,所以我們不需要明確編寫cleanup邏輯。

std::vector<VkCommandBuffer> commandBuffers;

現在開始使用一個createCommandBuffers函數來分配和記錄每一個交換鏈圖像將要應用的命令。

void initVulkan() {
    createInstance();
    setupDebugCallback();
    createSurface();
    pickPhysicalDevice();
    createLogicalDevice();
    createSwapChain();
    createImageViews();
    createRenderPass();
    createGraphicsPipeline();
    createFramebuffers();
    createCommandPool();
    createCommandBuffers();
}

...

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

命令緩衝區通過vkAllocateCommandBuffers函數分配,它需要VkCommandBufferAllocateInfo結構體作爲參數,用以指定command pool和緩衝區將會分配的大小:

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

    vkBeginCommandBuffer(commandBuffers[i], &beginInfo);
}

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開啓渲染通道。render pass使用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的清除值,我們將其用作顏色附件的加載操作。爲了簡化操作,我們定義了clear color爲100%黑色。

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

渲染通道現在可以啓用。所有可以被記錄的命令,被識別的前提是使用vkCmd前綴。它們全部返回void,所以在結束記錄之前不會有任何錯誤處理。

對於每個命令,第一個參數總是記錄該命令的命令緩衝區。第二個參數指定我們傳遞的渲染通道的具體信息。最後的參數控制如何提供render pass將要應用的繪製命令。它使用以下數值任意一個:

VK_SUBPASS_CONTENTS_INLINE: 渲染過程命令被嵌入在主命令緩衝區中,沒有輔助緩衝區執行。
VK_SUBPASS_CONTENTS_SECONDARY_COOMAND_BUFFERS: 渲染通道命令將會從輔助命令緩衝區執行。

我們不會使用輔助命令緩衝區,所以我們選擇第一個。

五、基本繪圖命令

現在我們綁定圖形管線:

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

第二個參數指定具體管線類型,graphics or compute pipeline。我們告訴Vulkan在圖形管線中每一個操作如何執行及哪個附件將會在片段着色器中使用,所以剩下的就是告訴它繪製三角形。

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

實際的vkCmdDraw函數有點與字面意思不一致,它是如此簡單,僅因爲我們提前指定所有渲染相關的信息。它有如下的參數需要指定,除了命令緩衝區:

vertexCount: 即使我們沒有頂點緩衝區,但是我們仍然有3個定點需要繪製。
instanceCount: 用於instanced 渲染,如果沒有使用請填1。
firstVertex: 作爲頂點緩衝區的偏移量,定義gl_VertexIndex的最小值。
firstInstance: 作爲instanced 渲染的偏移量,定義了gl_InstanceIndex的最小值。

六、結束渲染

render pass執行完繪製,可以結束渲染作業:

vkCmdEndRenderPass(commandBuffers[i]);

並停止記錄命令緩衝區的工作:

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

在下一章節我們會嘗試在main loop中編寫代碼,用於從交換鏈中獲取圖像,執行命令緩衝區的命令,再將渲染後的圖像返還給交換鏈。

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