操作系統:Windows8.1
顯卡:Nivida GTX965M
開發工具:Visual Studio 2017
Introduction
在Vulkan中,緩沖區是內存的一塊區域,該區域用於向顯卡提供預要讀取的任意數據。它們可以用來存儲頂點數據,也可以用於其他目的。與之前創建的Vulkan對象不同的是,緩沖區自己不會分配內存空間。前幾個章節了解到,Vulkan API使開發者控制所有的實現,內存管理是其中一個非常重要的環節。
Buffer creation
添加新的函數createVertexBuffer,並在initVulkan函數中的createCommandBuffers函數之前調用。
void initVulkan() { createInstance(); setupDebugCallback(); createSurface(); pickPhysicalDevice(); createLogicalDevice(); createSwapChain(); createImageViews(); createRenderPass(); createGraphicsPipeline(); createFramebuffers(); createCommandPool(); createVertexBuffer(); createCommandBuffers(); createSemaphores(); } ... void createVertexBuffer() { }
創建緩沖區需要填充VkBufferCreateInfo結構體。
VkBufferCreateInfo bufferInfo = {}; bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; bufferInfo.size = sizeof(vertices[0]) * vertices.size();
結構體的第一個字段size指定緩沖區字節大小。計算緩沖區每個頂點數據的字節大小可以直接使用sizeof。
bufferInfo.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT;
第二個字段usage,表示緩沖區的數據將如何使用。可以使用位操作指定多個使用目的。我們的案例將會使用一個頂點緩沖區,我們將會在未來的章節使用其他的用法。
bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
就像交換鏈中的圖像一樣,緩沖區也可以由特定的隊列簇占有或者多個同時共享。在我們的案例中緩沖區將會被用於圖形隊列,所以我們堅持使用獨占訪問模式exclusive mode。
flags參數用於配置稀疏內存緩沖區,現在關於flags的設置是無關緊要的,所以我們默認填0.
我們使用vkCreateBuffer函數創建緩沖區。定義一個類成員vertexBuffer存儲緩沖區句柄。
VkBuffer vertexBuffer; ... void createVertexBuffer() { VkBufferCreateInfo bufferInfo = {}; bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; bufferInfo.size = sizeof(vertices[0]) * vertices.size(); bufferInfo.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT; bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; if (vkCreateBuffer(device, &bufferInfo, nullptr, &vertexBuffer) != VK_SUCCESS) { throw std::runtime_error("failed to create vertex buffer!"); } }
緩沖區在程序退出之前為渲染命令rendering command提供支持,並且不依賴交換鏈,我們在cleanup函數中清理。
void cleanup() { cleanupSwapChain(); vkDestroyBuffer(device, vertexBuffer, nullptr); ... }
Memory requirements
雖然緩沖區創建完成了,但是實際上並沒有分配任何可用內存。給緩沖區分配內存的第一步是vkGetBufferMemoryRequirements函數查詢內存需求。
VkMemoryRequirements memRequirements;
vkGetBufferMemoryRequirements(device, vertexBuffer, &memRequirements);
VkMemoryRequirements結構體有三個字段:
- size: 需要的內存字節大小,可能與bufferInfo.size大小不一致。
- alignment: 緩沖區的內存分配區域開始的字節偏移量,它取決於bufferInfo.usage和bufferInfo.flags。
- memoryTypeBits: 適用於緩沖區的存儲器類型的位字段。
顯卡可以分配不同類型的內存。每種類型的內存根據所允許的操作和特性均不相同。我們需要結合緩沖區與應用程序實際的需要找到正確的內存類型使用。現在添加一個新的函數完成此邏輯findMemoryType。
uint32_t findMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties) {
}
首先需要通過vkGetPhysicalDeviceMemoryProperties函數遍歷有效的內存類型。
VkPhysicalDeviceMemoryProperties memProperties;
vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memProperties);
VkPhysicalDeviceMemoryProperties結構體有兩個數組,一個是memoryTypes,另一個是memoryHeaps。內存堆是比較特別的內存資源,類似VRAM內存以及在VRAM消耗盡時進行 swap space 中的RAM。在堆中存在不同類型的內存。現在我們專注內存類型本身,而不是堆的來源。但是可以想到會影響到性能。
我們首先為緩沖區找到合適的內存類型:
for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) { if (typeFilter & (1 << i)) { return i; } } throw std::runtime_error("failed to find suitable memory type!");
typeFilter參數將以位的形式代表適合的內存類型。這意味着通過簡單的迭代內存屬性集合,並根據需要的類型與每個內存屬性的類型進行AND操作,判斷是否為1。
然而,不僅僅對vertex buffer頂點緩沖區的內存類型感興趣。還需要將頂點數據寫入內存。memoryTypes數組是由VkMemoryType結構體組成的,它描述了堆以及每個內存類型的相關屬性。屬性定義了內存的特殊功能,就像內存映射功能,使我們可以從CPU向它寫入數據。此屬性由VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT定義,但是我們還需要使用VK_MEMORY_PROPERTY_HOST_CHOERENT_BIT屬性。當我們進行內存映射的時候會看到它們。
我們修改loop循環,並使用這些屬性作為內存篩選條件:
for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) { if ((typeFilter & (1 << i)) && (memProperties.memoryTypes[i].propertyFlags & properties) == properties) { return i; } }
在將來我們可能不止一個所需屬性,所以我們應該檢查按位AND的結果是否為零,而不是直接等於期望的屬性位字段。如果有一個內存類型適合我們的緩沖區,它也具有需要的所有屬性,那么我們就返回它的索引,否則我們拋出一個異常信息。
Memory allocation
我們現在決定了正確的內存類型,所以我們可以通過VkMemoryAllocateInfo結構體分配內存。
VkMemoryAllocateInfo allocInfo = {}; allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; allocInfo.allocationSize = memRequirements.size; allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
內存分配簡單的指定大小和類型參數,這兩個參數是從之前為頂點緩沖區設置的內存需求結構體和所需屬性帶過來的。創建一個類成員,存儲使用vkAllocateMemory函數分配的內存句柄。
VkBuffer vertexBuffer; VkDeviceMemory vertexBufferMemory; ... if (vkAllocateMemory(device, &allocInfo, nullptr, &vertexBufferMemory) != VK_SUCCESS) { throw std::runtime_error("failed to allocate vertex buffer memory!"); }
如果內存分配成功,我們使用vkBindBufferMemory函數將內存關聯到緩沖區:
vkBindBufferMemory(device, vertexBuffer, vertexBufferMemory, 0);
前三個參數已經不言自明了,第四個參數是內存區域的偏移量。因為這個內存被專門為頂點緩沖區分配,偏移量設置為0。如果偏移量non-zero,那么需要通過memRequirements.alignment整除。
當然,就像在C++動態分配內存一樣,所分配的內存需要在某個節點釋放。當緩沖區不再使用時,綁定到緩沖區對象的內存獲取會被釋放,所以讓我們在緩沖區被銷毀后釋放它們:
void cleanup() { cleanupSwapChain(); vkDestroyBuffer(device, vertexBuffer, nullptr); vkFreeMemory(device, vertexBufferMemory, nullptr);
Filling the vertex buffer
現在將頂點數據Copy到緩沖區。使用vkMapMemory將緩沖區內存映射(mapping the buffer memory)到CPU可訪問的內存中完成。
void* data; vkMapMemory(device, vertexBufferMemory, 0, bufferInfo.size, 0, &data);
該功能允許我們訪問由偏移量和大小指定的內存資源的區域。在這里offset和size分別是0和bufferInfo.size,還可以指定特殊值VK_WHOLE_SIZE來映射所有內存。第二個到最后一個參數可以用於指定標志位,但是當前版本的API還沒有可用的參數。它必須設置為0。
void* data; vkMapMemory(device, vertexBufferMemory, 0, bufferInfo.size, 0, &data); memcpy(data, vertices.data(), (size_t) bufferInfo.size); vkUnmapMemory(device, vertexBufferMemory);
可以簡單的通過memcpy將頂點數據拷貝到映射內存中,並使用vkUnmapMemory取消映射。不幸的是,驅動程序是不會立即將數據復制到緩沖區中,比如緩存的原因。也可能嘗試映射的內存對於寫緩沖區操作不可見。處理該類問題有兩種方法:
- 使用主機一致的內存堆空間,用VK_MEMORY_PROPERTY_HOST_COHERENT_BIT指定
- 當完成寫入內存映射操作后,調用vkFlushMappedMemoryRanges函數,當讀取映射內存之前,調用vkInvalidateMappedMemoryRanges函數
我們使用第一個方式,它確保了映射的內存總是與實際分配的內存一致。需要了解的是,這種方式與明確flushing操作相比,可能對性能有一點減損。但是我們在下一章會了解為什么無關緊要。
Binding the vertex buffer
現在討論渲染期間綁定緩沖區操作。我們將會擴展createCommandBuffers函數。
vkCmdBindPipeline(commandBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline); VkBuffer vertexBuffers[] = {vertexBuffer}; VkDeviceSize offsets[] = {0}; vkCmdBindVertexBuffers(commandBuffers[i], 0, 1, vertexBuffers, offsets); vkCmdDraw(commandBuffers[i], static_cast<uint32_t>(vertices.size()), 1, 0, 0);
vkCmdBindVertexBuffers函數用於綁定頂點緩沖區,就像之前的設置一樣,除了命令緩沖區之外,前兩個參數指定了我們要為其指定的頂點緩沖區的偏移量和數量。最后兩個參數指定了將要綁定的頂點緩沖區的數組及開始讀取數據的起始偏移量。最后調用vkCmdDraw函數傳遞緩沖區中頂點的數量,而不是硬編碼3。
現在運行程序可以看到正確的三角形繪制:

嘗試修改上面頂點的顏色為白色white,修改vertices數組如下:
const std::vector<Vertex> vertices = { {{0.0f, -0.5f}, {1.0f, 1.0f, 1.0f}}, {{0.5f, 0.5f}, {0.0f, 1.0f, 0.0f}}, {{-0.5f, 0.5f}, {0.0f, 0.0f, 1.0f}} };
再次運行程序看到如下圖:

在下一章節中,我們將會介紹將頂點數據復制到頂點緩沖區的不同方式,從而實現更好的性能,但需要更多的工作。
項目代碼 GitHub 地址。
