Vulkan填坑學習Day19—創建頂點緩衝區

Vulkan 創建頂點緩衝區

Vulkan 創建頂點緩衝區,在Vulkan中,緩衝區是內存的一塊區域,該區域用於向顯卡提供預要讀取的任意數據。它們可以用來存儲頂點數據,也可以用於其他目的。與之前創建的Vulkan對象不同的是,緩衝區自己不會分配內存空間。前幾個章節瞭解到,Vulkan API使開發者控制所有的實現,內存管理是其中一個非常重要的環節。

一、創建緩衝區

添加新的函數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);

    ...
}

二、內存需求

雖然緩衝區創建完成了,但是實際上並沒有分配任何可用內存。給緩衝區分配內存的第一步是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的結果是否爲零,而不是直接等於期望的屬性位字段。如果有一個內存類型適合我們的緩衝區,它也具有需要的所有屬性,那麼我們就返回它的索引,否則我們拋出一個異常信息。

三、內存分配

我們現在決定了正確的內存類型,所以我們可以通過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);

四、填充頂點緩衝區

現在將頂點數據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操作相比,可能對性能有一點減損。但是我們在下一章會了解爲什麼無關緊要。

五、綁定頂點緩衝區

現在討論渲染期間綁定緩衝區操作。我們將會擴展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, 0.0f, 0.0f}},
	{{-0.5f, 0.5f}, {0.0f, 1.0f, 1.0f}}
};

再次運行程序看到如下圖:
在這裏插入圖片描述

在下一章節中,我們將會介紹將頂點數據複製到頂點緩衝區的不同方式,從而實現更好的性能,但需要更多的工作。

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