天天看點

Vulkan填坑學習Day20—暫存緩沖區Vulkan 暫存緩沖區

Vulkan 暫存緩沖區

頂點緩沖區現在已經可以正常工作,但相比于顯示卡内部讀取資料,單純從CPU通路記憶體資料的方式性能不是最佳的。最佳的方式是采用VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT标志位,通常來說用在專用的圖形卡,CPU是無法通路的。在本章節我們建立兩個頂點緩沖區。一個緩沖區提供給CPU-HOST記憶體通路使用,用于從頂點數組中送出資料,另一個頂點緩沖區用于裝置local記憶體。我們将會使用緩沖區拷貝的指令将資料從暫存緩沖區拷貝到實際的圖形卡記憶體中。

Vulkan填坑學習Day20—暫存緩沖區Vulkan 暫存緩沖區

一、傳輸隊列

緩沖區拷貝的指令需要隊列簇支援傳輸操作,可以通過VK_QUEUE_TRANSFER_BIT标志位指定。好消息是任何支援VK_QUEUE_GRAPHICS_BIT 或者 VK_QUEUE_COMPUTE_BIT标志位功能的隊列簇都預設支援VK_QUEUE_TRANSFER_BIT操作。這部分的實作不需要在queueFlags顯示的列出。

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

  • 修改QueueFamilyIndices和findQueueFamilies,明确指定隊列簇需要具備VK_QUEUE_TRANSFER标志位,而不是VK_QUEUE_GRAPHICS_BIT。
  • 修改createLogicalDevice函數,請求一個傳輸隊列句柄。
  • 建立兩個指令對象池配置設定指令緩沖區,用于向傳輸隊列簇送出指令。
  • 修改資源的sharingMode為VK_SHARING_MODE_CONCURRENT,并指定為graphics和transfer隊列簇。
  • 送出任何傳輸指令,諸如vkCmdCopyBuffer(本章節使用)到傳輸隊列,而不是圖形隊列。

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

二、建立臨時緩沖區

考慮到我們在本章節需要建立多個緩沖區,比較理想的是建立輔助函數來完成。新增函數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);
}
           

運作程式確定頂點緩沖區仍然正常工作。

三、使用臨時緩沖區

我們現在改變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);
}
           

}

C++

我們使用stagingBuffer來劃分stagingBufferMemory緩沖區用來映射、拷貝頂點資料。在本章節我們使用兩個新的緩沖區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。

此指令緩沖區僅包含拷貝指令,是以我們可以在此之後停止記錄。現在執行指令緩沖區完成傳輸:

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。一個栅欄允許安排多個連續的傳輸操作,而不是一次執行一個。這給了驅動程式更多的優化空間。

不要忘記清理用于傳輸指令的指令緩沖區。

我們可以從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);
}
           

運作程式确認三角形繪制正常。性能的提升也許現在不能很好的顯現出來,但其頂點資料已經是從高性能的顯存中加載。當我們開始渲染更複雜的幾何圖形時,這個技術是非常重要。

總結

需要了解的是,在真實的生産環境中的應用程式裡,不建議為每個緩沖區調用vkAllocateMemory配置設定記憶體。記憶體配置設定的最大數量受到maxMemoryAllocationCount實體裝置所限,即使像NVIDIA GTX1080這樣的高端硬體上,也隻能提供4096的大小。同一時間,為大量對象配置設定記憶體的正确方法是建立一個自定義配置設定器,通過使用我們在許多函數中用到的偏移量offset,将一個大塊的可配置設定記憶體區域劃分為多個可配置設定記憶體塊,提供緩沖區使用。

也可以自己實作一個靈活的記憶體配置設定器,或者使用GOUOpen提供的VulkanMemoryAllocator庫。然而,對于本教程,我們可以做到為每個資源使用單獨的配置設定,因為我們不會觸達任何資源限制條件。