操作系統:Windows8.1
顯卡:Nivida GTX965M
開發工具:Visual Studio 2017
諸如繪制和內存操作相關命令,在Vulkan中不是通過函數直接調用的。我們需要在命令緩沖區對象中記錄我們期望的任何操作。這樣做的優點是可以提前在多線程中完成所有繪制命令相關的裝配工作,並在主線程循環結構中通知Vulkan執行具體的命令。
Command pools
我們在使用任何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); ... }
Command buffer allocation
現在我們開始分配命令緩沖區並通過它們記錄繪制指令。因為其中一個繪圖命令需要正確綁定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: 無法直接提交,但是可以從主命令緩沖區調用。
我們不會在這里使用輔助緩沖區功能,但是可以想像,對於復用主緩沖區的常用操作很有幫助。
Starting command buffer recording
通過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會隱式地重置它。否則將命令附加到緩沖區是不可能的。
Starting a render pass
繪制開始於調用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: 渲染通道命令將會從輔助命令緩沖區執行。
我們不會使用輔助命令緩沖區,所以我們選擇第一個。
Basic drawing commands
現在我們綁定圖形管線:
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的最小值。
Finishing up
render pass執行完繪制,可以結束渲染作業:
vkCmdEndRenderPass(commandBuffers[i]);
並停止記錄命令緩沖區的工作:
if (vkEndCommandBuffer(commandBuffers[i]) != VK_SUCCESS) { throw std::runtime_error("failed to record command buffer!"); }
在下一章節我們會嘗試在main loop中編寫代碼,用於從交換鏈中獲取圖像,執行命令緩沖區的命令,再將渲染后的圖像返還給交換鏈。
項目代碼 GitHub 地址。