1. 程式人生 > >Vulkan Tutorial 21 Staging buffer

Vulkan Tutorial 21 Staging buffer

optional 頂點數組 輔助 ica 硬件 系統 current ans 使用

操作系統: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顯示的列出。

如果需要挑戰,甚至可以嘗試為不同的隊列簇指定具體的傳輸操作。這部分實現需要對代碼做出如下修改:

  • 修改QueueFamilyIndicesfindQueueFamilies,明確指定隊列簇需要具備VK_QUEUE_TRANSFER
    標誌位,而不是VK_QUEUE_GRAPHICS_BIT
  • 修改createLogicalDevice函數,請求一個傳輸隊列句柄。
  • 創建兩個命令對象池分配命令緩沖區,用於向傳輸隊列簇提交命令。
  • 修改資源的sharingModeVK_SHARING_MODE_CONCURRENT,並指定為graphics和transfer隊列簇。
  • 提交任何傳輸命令,諸如vkCmdCopyBuffer(本章節使用)到傳輸隊列,而不是圖形隊列。

需要一些額外的工作,但是它我們更清楚的了解資源在不同隊列簇如何共享的。

Abstracting buffer creation


考慮到我們在本章節需要創建多個緩沖區,比較理想的是創建輔助函數來完成。新增函數createBuffer

並將createVertexBuffer中的部分代碼(不包括映射)移入該函數。

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內存映射。然而我們可以從stagingBuffervertexBuffer拷貝數據。我們需要指定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, &copyRegion);

緩沖區內容使用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