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

Vulkan 暫存緩衝區

頂點緩衝區現在已經可以正常工作,但相比於顯卡內部讀取數據,單純從CPU訪問內存數據的方式性能不是最佳的。最佳的方式是採用VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT標誌位,通常來說用在專用的圖形卡,CPU是無法訪問的。在本章節我們創建兩個頂點緩衝區。一個緩衝區提供給CPU-HOST內存訪問使用,用於從頂點數組中提交數據,另一個頂點緩衝區用於設備local內存。我們將會使用緩衝區拷貝的命令將數據從暫存緩衝區拷貝到實際的圖形卡內存中。
在這裏插入圖片描述

一、傳輸隊列

緩衝區拷貝的命令需要隊列簇支持傳輸操作,可以通過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。

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);
}

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

總結

需要了解的是,在真實的生產環境中的應用程序裏,不建議爲每個緩衝區調用vkAllocateMemory分配內存。內存分配的最大數量受到maxMemoryAllocationCount物理設備所限,即使像NVIDIA GTX1080這樣的高端硬件上,也只能提供4096的大小。同一時間,爲大量對象分配內存的正確方法是創建一個自定義分配器,通過使用我們在許多函數中用到的偏移量offset,將一個大塊的可分配內存區域劃分爲多個可分配內存塊,提供緩衝區使用。

也可以自己實現一個靈活的內存分配器,或者使用GOUOpen提供的VulkanMemoryAllocator庫。然而,對於本教程,我們可以做到爲每個資源使用單獨的分配,因爲我們不會觸達任何資源限制條件。

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