Vulkan Tutorial 21 Staging buffer
操作系統:Windows8.1
顯卡:Nivida GTX965M
開發工具:Visual Studio 2017
Introduction
頂點緩沖區現在已經可以正常工作,但相比於顯卡內部讀取數據,單純從CPU訪問內存數據的方式性能不是最佳的。最佳的方式是采用VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT標誌位,通常來說用在專用的圖形卡,CPU是無法訪問的。在本章節我們創建兩個頂點緩沖區。一個緩沖區提供給CPU-HOST內存訪問使用,用於從頂點數組中提交數據,另一個頂點緩沖區用於設備local內存。我們將會使用緩沖區拷貝的命令將數據從預緩沖區COPY到實際的圖形卡內存中。
Transfer queue
緩沖區拷貝的命令需要隊列簇支持傳輸操作,可以通過VK_QUEUE_TRANSFER_BIT標誌位指定。好消息是任何支持VK_QUEUE_GRAPHICS_BIT 或者 VK_QUEUE_COMPUTE_BIT標誌位功能的隊列簇都默認支持VK_QUEUE_TRANSFER_BIT操作。這部分的實現不需要在queueFlags顯示的列出。
如果需要挑戰,甚至可以嘗試為不同的隊列簇指定具體的傳輸操作。這部分實現需要對代碼做出如下修改:
- 修改QueueFamilyIndices和findQueueFamilies,明確指定隊列簇需要具備VK_QUEUE_TRANSFER
- 修改createLogicalDevice函數,請求一個傳輸隊列句柄。
- 創建兩個命令對象池分配命令緩沖區,用於向傳輸隊列簇提交命令。
- 修改資源的sharingMode為VK_SHARING_MODE_CONCURRENT,並指定為graphics和transfer隊列簇。
- 提交任何傳輸命令,諸如vkCmdCopyBuffer(本章節使用)到傳輸隊列,而不是圖形隊列。
需要一些額外的工作,但是它我們更清楚的了解資源在不同隊列簇如何共享的。
Abstracting buffer creation
考慮到我們在本章節需要創建多個緩沖區,比較理想的是創建輔助函數來完成。新增函數createBuffer
void createBuffer(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties, VkBuffer& buffer, VkDeviceMemory& bufferMemory) { VkBufferCreateInfo bufferInfo = {}; bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; bufferInfo.size = size; bufferInfo.usage = usage; bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; if (vkCreateBuffer(device, &bufferInfo, nullptr, &buffer) != VK_SUCCESS) { throw std::runtime_error("failed to create buffer!"); } VkMemoryRequirements memRequirements; vkGetBufferMemoryRequirements(device, buffer, &memRequirements); VkMemoryAllocateInfo allocInfo = {}; allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; allocInfo.allocationSize = memRequirements.size; allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties); if (vkAllocateMemory(device, &allocInfo, nullptr, &bufferMemory) != VK_SUCCESS) { throw std::runtime_error("failed to allocate buffer memory!"); } vkBindBufferMemory(device, buffer, bufferMemory, 0); }
該函數需要傳遞緩沖區大小,內存屬性和usage最終創建不同類型的緩沖區。最後兩個參數保存輸出的句柄。
我們可以從createVertexBuffer函數中移除創建緩沖區和分配內存的代碼,並使用createBuffer替代:
void createVertexBuffer() { VkDeviceSize bufferSize = sizeof(vertices[0]) * vertices.size(); createBuffer(bufferSize, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, vertexBuffer, vertexBufferMemory); void* data; vkMapMemory(device, vertexBufferMemory, 0, bufferSize, 0, &data); memcpy(data, vertices.data(), (size_t) bufferSize); vkUnmapMemory(device, vertexBufferMemory); }
運行程序確保頂點緩沖區仍然正常工作。
Using a staging buffer
我們現在改變createVertexBuffer函數,僅僅使用host緩沖區作為臨時緩沖區,並且使用device緩沖區作為最終的頂點緩沖區。
void createVertexBuffer() { VkDeviceSize bufferSize = sizeof(vertices[0]) * vertices.size(); VkBuffer stagingBuffer; VkDeviceMemory stagingBufferMemory; createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer, stagingBufferMemory); void* data; vkMapMemory(device, stagingBufferMemory, 0, bufferSize, 0, &data); memcpy(data, vertices.data(), (size_t) bufferSize); vkUnmapMemory(device, stagingBufferMemory); createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, vertexBuffer, vertexBufferMemory); }
我們使用stagingBufferMemory劃分新的stagingBuffer預緩沖區用來映射、拷貝頂點數據。在本章節我們使用兩個新的緩沖區usage標致類型:
- VK_BUFFER_USAGE_TRANSFER_SRC_BIT:緩沖區可以用於源內存傳輸操作。
- VK_BUFFER_USAGE_TRANSFER_DST_BIT:緩沖區可以用於目標內存傳輸操作。
vertexBuffer現在使用device類型作為分配的內存類型,意味著我們不可以使用vkMapMemory內存映射。然而我們可以從stagingBuffer向vertexBuffer拷貝數據。我們需要指定stagingBuffer的傳輸源標誌位,還要為頂點緩沖區vertexBuffer的usage設置傳輸目標的標誌位。
我們新增函數copyBuffer,用於從一個緩沖區拷貝數據到另一個緩沖區。
void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) { }
使用命令緩沖區執行內存傳輸的操作命令,就像繪制命令一樣。因此我們需要分配一個臨時命令緩沖區。或許在這裏希望為短期的緩沖區分別創建command pool,那麽可以考慮內存分配的優化策略,在command pool生成期間使用VK_COMMAND_POOL_CREATE_TRANSIENT_BIT標誌位。
void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) { VkCommandBufferAllocateInfo allocInfo = {}; allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; allocInfo.commandPool = commandPool; allocInfo.commandBufferCount = 1; VkCommandBuffer commandBuffer; vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer); }
立即使用命令緩沖過去進行記錄:
VkCommandBufferBeginInfo beginInfo = {}; beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; vkBeginCommandBuffer(commandBuffer, &beginInfo);
應用於繪制命令緩沖區的VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT標誌位在此不必要,因為我們之需要使用一次命令緩沖區,等待該函數返回,直到復制操作完成。告知driver驅動程序使用VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT是一個好的習慣。
VkBufferCopy copyRegion = {}; copyRegion.srcOffset = 0; // Optional copyRegion.dstOffset = 0; // Optional copyRegion.size = size; vkCmdCopyBuffer(commandBuffer, srcBuffer, dstBuffer, 1, ©Region);
緩沖區內容使用vkCmdCopyBuffer命令傳輸。它使用source和destination緩沖區及一個緩沖區拷貝的區域作為參數。這個區域被定義在VkBufferCopy結構體中,描述源緩沖區的偏移量,目標緩沖區的偏移量和對應的大小。與vkMapMemory命令不同,這裏不可以指定VK_WHOLE_SIZE。
vkEndCommandBuffer(commandBuffer);
此命令緩沖區僅包含拷貝命令,因此我們可以在此之後停止記錄。現在執行命令緩沖區完成傳輸:
VkSubmitInfo submitInfo = {}; submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; submitInfo.commandBufferCount = 1; submitInfo.pCommandBuffers = &commandBuffer; vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE); vkQueueWaitIdle(graphicsQueue);
與繪制命令不同的是,這個時候我們不需要等待任何事件。我們只是想立即在緩沖區執行傳輸命令。這裏有同樣有兩個方式等待傳輸命令完成。我們可以使用vkWaitForFences等待屏障fence,或者只是使用vkQueueWaitIdle等待傳輸隊列變為空間idle。一個屏障允許安排多個連續的傳輸操作,而不是一次執行一個。這給了驅動程序更多的優化空間。
vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);
不要忘記清理用於傳輸命令的命令緩沖區。
我們可以從createVertexBuffer函數中調用copyBuffer,拷貝頂點數據到設備緩沖區中:
createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, vertexBuffer, vertexBufferMemory);
copyBuffer(stagingBuffer, vertexBuffer, bufferSize)
當從預緩沖區拷貝數據到圖形卡設備緩沖區完畢後,我們應該清理它:
...
copyBuffer(stagingBuffer, vertexBuffer, bufferSize);
vkDestroyBuffer(device, stagingBuffer, nullptr);
vkFreeMemory(device, stagingBufferMemory, nullptr);
}
運行程序確認三角形繪制正常。它是可見的,但其頂點數據現在是從高性能的顯存中加載。當我們開始渲染更復雜的幾何圖形時,這個技術是非常重要。
Conclusion
需要了解的是,在真實的生產環境中的應用程序裏,不建議為每個緩沖區調用vkAllocateMemory分配內存。內存分配的最大數量受到maxMemoryAllocationCount物理設備所限,及時在像NVIDIA GTX1080這樣的高端硬件上,也只能提供4096的大小。同一時間,為大量對象分配內存的正確方法是創建一個自定義分配器,通過使用我們在許多函數中用到的偏移量offset,將一個大塊的可分配內存區域劃分為多個可分配內存塊,提供緩沖區使用。
也可以自己實現一個靈活的內存分配器,或者使用GOUOpen提供的VulkanMemoryAllocator庫。然而,對於本教程,我們可以做到為每個資源使用單獨的分配,因為我們不會觸達任何資源限制條件。
項目代碼 GitHub 地址。
Vulkan Tutorial 21 Staging buffer