天天看點

Vulkan入門(12)-暫存緩沖和索引緩沖.md參考資料簡述一. 傳輸隊列二. 暫存緩沖區三. 索引緩沖區四. 繪制指令概述五. 小結

文章目錄

  • 參考資料
  • 簡述
  • 一. 傳輸隊列
  • 二. 暫存緩沖區
    • 2.1 VkBufferUsageFlagBits
    • 2.2 VkMemoryPropertyFlags
    • 2.3 緩沖區拷貝函數
      • 2.2.1 vkCmdCopyBuffer 拷貝緩沖區
    • 2.3 緩沖區拷貝
  • 三. 索引緩沖區
    • 3.1 建立索引緩沖區
    • 3.2 使用頂點緩沖
  • 四. 繪制指令概述
    • 4.1 非索引繪圖指令
      • 4.1.1 vkCmdDraw
      • 4.1.2 vkCmdDrawIndirect
      • 4.1.3 vkCmdDrawIndirectCount、vkCmdDrawIndirectCountKHR、vkCmdDrawIndirectCountAMD
    • 4.2 索引繪圖指令
      • 4.2.1 vkCmdDrawIndexed
      • 4.2.2 vkCmdDrawIndexedIndirect
      • 4.2.3 vkCmdDrawIndexedIndirectCount、vkCmdDrawIndexedIndirectKHR、vkCmdDrawIndexedIndirectAMD
  • 五. 小結

參考資料

  1. [Vulkan coordinate system] http://vulkano.rs/guide/vertex-input
  2. [The new Vulkan Coordinate System] https://www.douban.com/note/700990025/

簡述

雖然現在我們建立的頂點緩沖區工作正常,但是從CPU通路它的記憶體類型可能不是圖形顯示卡本身讀取的最佳記憶體類型,最理想的記憶體具有VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT位标志,并且通常不可由專用圖形顯示卡上的CPU通路。而且我們上一篇最後實作的随滑鼠移動的功能也沒有考慮同步,簡單一點,考慮讀寫分離。

現在建立兩個頂點緩沖區:CPU可通路記憶體中的一個暫存緩沖區用于從頂點數組上傳資料,最終頂點緩沖區位于裝置本地記憶體中。然後我們将使用一個緩沖區複制指令将資料從暫存緩沖區移動到實際的頂點緩沖區。簡單來說就是暫存緩沖區用于cpu寫入,頂點緩沖區用于GPU讀取資料。

一. 傳輸隊列

buffer copy指令需要支援傳輸操作的隊列族,使用VK_QUEUE_TRANSFER_BIT表示。不過任何具有VK_QUEUE_GRAPHICS_BIT或VK_QUEUE_COMPUTE_BIT功能的隊列家族都已經隐式支援VK_QUEUE_TRANSFER_BIT操作。在這些情況下,不需要實作在queueFlags中顯式地列出它。

但可以嘗試使用專門用于傳輸操作的不同隊列族, 可以如下操作:

  1. 修改QueueFamilyIndices和findQueueFamilies來顯式地查找具有VK_QUEUE_TRANSFER位的隊列族,而不是VK_QUEUE_GRAPHICS_BIT位
  2. 修改createLogicalDevice以請求傳輸隊列的句柄
  3. 為傳輸隊列系列上送出的指令緩沖區建立第二個指令池
  4. 修改資源的共享模式為VK_SHARING_MODE_CONCURRENT,并指定圖形和傳輸隊列族
  5. 送出傳輸指令,如vkCmdCopyBuffer到傳輸隊列,而不是圖形隊列

二. 暫存緩沖區

因為我們要建立多個VkBuffer,是以最好把共有的部分抽出,以避免代碼累贅:

// 傳入必要參數
    void createBuffer(VkDeviceSize size, VkBufferUsageFlags usage,
            VkMemoryPropertyFlags properties, VkBuffer& buffer,
            VkDeviceMemory& bufferMemory, VkSharingMode mode) {
        VkBufferCreateInfo bufferInfo = {};
        bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
        bufferInfo.size = size;
        bufferInfo.usage = usage;
        bufferInfo.sharingMode = mode;

        if (vkCreateBuffer(device, &bufferInfo, nullptr, &buffer) != VK_SUCCESS) {
            throw std::runtime_error("failed to create vertex 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 vertex buffer memory!");
        }
        vkBindBufferMemory(device, buffer, bufferMemory, 0);
    }

    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, VK_SHARING_MODE_EXCLUSIVE);
        void* data;
        vkMapMemory(device, stagingBufferMemory, 0, bufferSize, 0, &data);
        memcpy(data, vertices.data(), (size_t) bufferSize);
        vkUnmapMemory(device, stagingBufferMemory);

        // 注意這裡的MEMORY_PROPERTY是VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT!
        createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_DST_BIT |
                VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
                VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, vertexBuffer,
                // 資源隻能由單個隊列族獨占
                vertexBufferMemory, VK_SHARING_MODE_EXCLUSIVE);
    }
           

因為我們需要使用傳輸隊列,是以注意stagingBuffer的usage是用的VK_BUFFER_USAGE_TRANSFER_SRC_BIT,而vertexBuffer現在用的是VK_BUFFER_USAGE_TRANSFER_DST_BIT!

vertexBuffer現在從裝置本地的記憶體類型配置設定,這意味着我們不能使用vkMapMemory。但是,我們可以将資料從stagingBuffer複制到vertexBuffer。我們必須通過指定stagingBuffer的傳輸源标志和vertexBuffer的傳輸目标标志以及頂點緩沖區使用标志來表明我們打算這樣做。

2.1 VkBufferUsageFlagBits

typedef enum VkBufferUsageFlagBits {
    VK_BUFFER_USAGE_TRANSFER_SRC_BIT = 0x00000001,
    VK_BUFFER_USAGE_TRANSFER_DST_BIT = 0x00000002,
    VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT = 0x00000004,
    VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT = 0x00000008,
    VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT = 0x00000010,
    VK_BUFFER_USAGE_STORAGE_BUFFER_BIT = 0x00000020,
    VK_BUFFER_USAGE_INDEX_BUFFER_BIT = 0x00000040,
    VK_BUFFER_USAGE_VERTEX_BUFFER_BIT = 0x00000080,
    VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT = 0x00000100,
    VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT = 0x00020000,
    VK_BUFFER_USAGE_TRANSFORM_FEEDBACK_BUFFER_BIT_EXT = 0x00000800,
    VK_BUFFER_USAGE_TRANSFORM_FEEDBACK_COUNTER_BUFFER_BIT_EXT = 0x00001000,
    VK_BUFFER_USAGE_CONDITIONAL_RENDERING_BIT_EXT = 0x00000200,
    VK_BUFFER_USAGE_RAY_TRACING_BIT_NV = 0x00000400,
    VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT_EXT = VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT,
    VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT_KHR = VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT,
    VK_BUFFER_USAGE_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF
} VkBufferUsageFlagBits;
           

VkBufferUsageFlagBits設定的位可以指定緩沖區的使用行為:

  1. VK_BUFFER_USAGE_TRANSFER_SRC_BIT指定緩沖區可以用作傳輸指令的源(請參閱VK_PIPELINE_STAGE_TRANSFER_BIT的定義)。
  2. VK_BUFFER_USAGE_TRANSFER_DST_BIT指定緩沖區可以用作傳輸指令的目的地。
  3. VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT緩沖區可用于建立一個VkBufferView,該視圖适合占用VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER類型的VkDescriptorSet槽位。
  4. VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT指定該緩沖區可以用來建立一個VkBufferView,該視圖适合于占用VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER類型的VkDescriptorSet槽位。
  5. VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT緩沖區可以用于VkDescriptorBufferInfo中,該緩沖區适合于占用VkDescriptorSet類型的VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC槽位。
  6. VK_BUFFER_USAGE_STORAGE_BUFFER_BIT指定該緩沖區可用于VkDescriptorBufferInfo中,該緩沖區适合于占用VkDescriptorSet類型的VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC槽位。
  7. VK_BUFFER_USAGE_INDEX_BUFFER_BIT指定該緩沖區适合作為buffer參數傳遞給vkCmdBindIndexBuffer。
  8. VK_BUFFER_USAGE_VERTEX_BUFFER_BIT指定緩沖區适合作為pBuffers數組的元素傳遞給vkCmdBindVertexBuffers。
  9. VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT緩沖區适合作為buffer參數傳遞給vkCmdDrawIndirect、vkCmdDrawIndexedIndirect、vkCmdDrawMeshTasksIndirectNV、vkCmdDrawMeshTasksIndirectCountNV或vkCmdDispatchIndirect。它也适合作為VkIndirectCommandsTokenNVX的緩沖區成員,或VkCmdProcessCommandsInfoNVX的sequencesCountBuffer或sequencesIndexBuffer成員傳遞
  10. VK_BUFFER_USAGE_CONDITIONAL_RENDERING_BIT_EXT指定緩沖區适合作為buffer參數傳遞給vkCmdBeginConditionalRenderingEXT。
  11. VK_BUFFER_USAGE_TRANSFORM_FEEDBACK_BUFFER_BIT_EXT指定該緩沖區适合使用for binding作為vkCmdBindTransformFeedbackBuffersEXT的轉換回報緩沖區。
  12. VK_BUFFER_USAGE_TRANSFORM_FEEDBACK_COUNTER_BUFFER_BIT_EXT指定該緩沖區适合與vkCmdBeginTransformFeedbackEXT和vkCmdEndTransformFeedbackEXT一起用作計數器緩沖區。
  13. VK_BUFFER_USAGE_RAY_TRACING_BIT_NV指定緩沖區适用于vkCmdTraceRaysNV和vkCmdBuildAccelerationStructureNV。
  14. VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT指定緩沖區可以通過vkGetBufferDeviceAddress來檢索緩沖區裝置位址,并使用該位址從着色器通路緩沖區的記憶體。

2.2 VkMemoryPropertyFlags

typedef enum VkMemoryPropertyFlagBits {
    VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT = 0x00000001,
    VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT = 0x00000002,
    VK_MEMORY_PROPERTY_HOST_COHERENT_BIT = 0x00000004,
    VK_MEMORY_PROPERTY_HOST_CACHED_BIT = 0x00000008,
    VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT = 0x00000010,
    VK_MEMORY_PROPERTY_PROTECTED_BIT = 0x00000020,
    VK_MEMORY_PROPERTY_DEVICE_COHERENT_BIT_AMD = 0x00000040,
    VK_MEMORY_PROPERTY_DEVICE_UNCACHED_BIT_AMD = 0x00000080,
    VK_MEMORY_PROPERTY_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF
} VkMemoryPropertyFlagBits;
           
  1. VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT: 指定使用這種類型配置設定的記憶體對于裝置通路是最有效的。當且僅當記憶體類型屬于設定了VK_MEMORY_HEAP_DEVICE_LOCAL_BIT的堆時,才會設定此屬性。
  2. VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT: 指定使用這種類型配置設定的記憶體可以通過vkMapMemory映射給主機通路。
  3. VK_MEMORY_PROPERTY_HOST_COHERENT_BIT: 指定主機緩存管理指令vkFlushMappedMemoryRanges和vkInvalidateMappedMemoryRanges分别用于重新整理主機對裝置的寫操作,或者使裝置的寫操作對主機可見。
  4. VK_MEMORY_PROPERTY_HOST_CACHED_BIT: 指定用這種類型配置設定的記憶體緩存在主機上。主機記憶體對非緩存記憶體的通路比對緩存記憶體的通路慢,但是非緩存記憶體總是與主機一緻的。
  5. VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT: 指定記憶體類型僅允許裝置通路記憶體。記憶體類型不能同時設定VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT和VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT。另外,對象的後備記憶體可以由在惰性配置設定記憶體中指定的lazy實作提供。
  6. VK_MEMORY_PROPERTY_PROTECTED_BIT: 指定記憶體類型僅允許裝置通路記憶體,并允許受保護的隊列操作通路記憶體。記憶體類型不能設定VK_MEMORY_PROPERTY_PROTECTED_BIT和任何VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT、VK_MEMORY_PROPERTY_HOST_COHERENT_BIT或VK_MEMORY_PROPERTY_HOST_CACHED_BIT。
  7. VK_MEMORY_PROPERTY_DEVICE_COHERENT_BIT_AMD: 指定對這種記憶體類型配置設定的裝置通路将自動變為可用和可見的。
  8. VK_MEMORY_PROPERTY_DEVICE_UNCACHHED_BIT_AMD: 指定用這種類型配置設定的記憶體不會緩存到裝置上。非緩存裝置記憶體總是裝置一緻的。

2.3 緩沖區拷貝函數

記憶體傳輸操作使用指令緩沖區執行,就像繪制指令一樣。是以,首先配置設定一個臨時的指令緩沖區。您可能希望為這些短期緩沖區建立一個單獨的指令池,因為實作可能能夠應用記憶體配置設定優化。在這種情況下,您應該在生成指令池期間使用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;
    // 隻使用一次指令緩沖區,并等待函數傳回,直到複制操作完成執行
    // 是以使用VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT标志
    beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
    vkBeginCommandBuffer(commandBuffer, &beginInfo);

    // 緩沖拷貝指令
    VkBufferCopy copyRegion = {};
    copyRegion.srcOffset = 0; // Optional
    copyRegion.dstOffset = 0; // Optional
    copyRegion.size = size;
    // 緩沖區的内容使用vkCmdCopyBuffer指令傳輸。
    // 源和目标緩沖區以及要複制的區域數組作為參數。copyRegion由源緩沖區偏移量、目标緩沖區偏移量和大小組成
    vkCmdCopyBuffer(commandBuffer, srcBuffer, dstBuffer, 1, &copyRegion);

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

    vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);
}
           

拷貝緩沖指令的一般流程是:

  1. vkAllocateCommandBuffers 建立指令緩沖,配置設定記憶體
  2. vkBeginCommandBuffer 開始指令記錄
  3. vkCmdCopyBuffer 執行具體指令
  4. vkEndCommandBuffer 結束指令記錄
  5. vkQueueSubmit 将指令送出到管道
  6. vkQueueWaitIdle 等待管道執行指令,也可以通過fence機制
  7. vkFreeCommandBuffers 釋放指令緩沖區

2.2.1 vkCmdCopyBuffer 拷貝緩沖區

在緩沖區對象之間複制資料,調用:vkCmdCopyBuffer

void vkCmdCopyBuffer(
    VkCommandBuffer                             commandBuffer,
    VkBuffer                                    srcBuffer,
    VkBuffer                                    dstBuffer,
    uint32_t                                    regionCount,
    const VkBufferCopy*                         pRegions);
           
  1. commandBuffer是指令将被記錄到的指令緩沖區。
  2. srcBuffer是源緩沖區。
  3. dstBuffer是目标緩沖區。
  4. regionCount是要複制的區域數。
  5. pRegions是一個指向VkBufferCopy結構體數組的指針,該數組指定了要複制的區域。

2.3 緩沖區拷貝

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, VK_SHARING_MODE_EXCLUSIVE);
    void* data;
    vkMapMemory(device, stagingBufferMemory, 0, bufferSize, 0, &data);
    memcpy(data, vertices.data(), (size_t) bufferSize);
    vkUnmapMemory(device, stagingBufferMemory);

    // 注意這裡的MEMORY_PROPERTY是VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT!
    createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_DST_BIT |
            VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
            VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, vertexBuffer,
            // 資源隻能由單個隊列族獨占
            vertexBufferMemory, VK_SHARING_MODE_EXCLUSIVE);

    // 将暫存緩沖區的資料内容拷貝到頂點緩沖區
    copyBuffer(stagingBuffer, vertexBuffer, bufferSize);
    // 銷毀暫存緩沖區,釋放記憶體
    vkDestroyBuffer(device, stagingBuffer, nullptr);
    vkFreeMemory(device, stagingBufferMemory, nullptr);
}
           

在這裡思考一下,為什麼要使用一個暫存緩沖區替換原來的直接使用memcpy呢,而且使用暫存緩沖還額外多了一個建立緩沖區的操作?

因為圖形管道使用頂點資料緩沖區時,如果需要更改頂點資料内容,還需要等待memcpy,如果使用暫存緩沖區,可以将更改頂點資料内容的操作放在另一個線程執行,等到寫完之後,再使用vkCmdCopyBuffer指令拷貝記憶體資料,這樣圖形管道最多等待這個指令拷貝的時間。當然這一點現在看不出來優勢,等我們的頂點資料多而且繪制内容複雜的時候就可以展現出來了。

讓我們更近一步,考慮到每次拷貝都需要執行vkAllocateCommandBuffers配置設定記憶體,不如一開始就請求一塊合适的記憶體區域,畢竟這個函數開銷還是很大的。通過使用我們在許多函數中看到的偏移參數,在許多不同的對象之間分割單個配置設定或回收。可以自己實作也可以使用GPUOpen倡議提供的VulkanMemoryAllocator庫。

三. 索引緩沖區

在真實世界的應用程式中渲染的3D網格經常會在多個三角形之間共享頂點。比如畫一個矩形:

Vulkan入門(12)-暫存緩沖和索引緩沖.md參考資料簡述一. 傳輸隊列二. 暫存緩沖區三. 索引緩沖區四. 繪制指令概述五. 小結

繪制一個矩形需要兩個三角形(基本繪制單元隻有點、線和三角形,是以矩形是兩個三角形之和),這意味着需要有6個頂點的頂點緩沖區。問題是兩個頂點的部分資料重複,會産生50%的備援。在更複雜的網格中,隻會變得更糟,因為頂點會在平均3個三角形中重複使用。解決這個問題的方法是使用索引緩沖區。

索引緩沖區本質上是一個指向頂點緩沖區的指針數組。它允許重新排序頂點資料,并為多個頂點重用現有資料。上面的插圖示範了一個頂點緩沖區包含四個不同的頂點,其索引緩沖區會是什麼樣子的。前三個索引定義了右上角的三角形,後三個索引定義了左下角三角形的頂點(順時鐘)。

3.1 建立索引緩沖區

接下來将修改頂點資料并添加索引資料來繪制一個矩形,像上圖中一樣。修改頂點資料以表示四個角:

const std::vector<Vertex> vertices = {
    {{-0.5f, -0.5f}, {1.0f, 0.0f, 0.0f}},
    {{0.5f, -0.5f}, {0.0f, 1.0f, 0.0f}},
    {{0.5f, 0.5f}, {0.0f, 0.0f, 1.0f}},
    {{-0.5f, 0.5f}, {1.0f, 1.0f, 1.0f}}
};
           

左上角是紅色的,右上方是綠色的,右下角是藍色的,左下角是白色的。現在添加一個新的數組索引來表示索引緩沖區的内容,比對圖中的索引來繪制右上三角形和左下三角形。

const std::vector<uint16_t> indices = {
    0, 1, 2, 2, 3, 0
};
           

可以使用uint16_t或uint32_t作為索引緩沖區,這取決于頂點中條目的數量。我們可以堅持uint16_t現在,因為我們使用少于65535唯一頂點。

就像頂點資料一樣,索引需要上傳到VkBuffer中,GPU才能通路它們。定義兩個新的類成員來儲存索引緩沖區的資源:

VkBuffer indexBuffer;
VkDeviceMemory indexBufferMemory;

void initVulkan() {
    ...
    createVertexBuffer();
    createIndexBuffer();
    ...
}

void createIndexBuffer() {
    VkDeviceSize bufferSize = sizeof(indices[0]) * indices.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, VK_SHARING_MODE_EXCLUSIVE);
    void* data;
    vkMapMemory(device, stagingBufferMemory, 0, bufferSize, 0, &data);
    memcpy(data, indices.data(), (size_t) bufferSize);
    vkUnmapMemory(device, stagingBufferMemory);

    createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_DST_BIT
            | VK_BUFFER_USAGE_INDEX_BUFFER_BIT,
            VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, indexBuffer,
            // 資源隻能由單個隊列族獨占
            indexBufferMemory, VK_SHARING_MODE_EXCLUSIVE);
    // 将暫存緩沖區的資料内容拷貝到頂點緩沖區
    copyBuffer(stagingBuffer, indexBuffer, bufferSize);
    // 銷毀暫存緩沖區,釋放記憶體
    vkDestroyBuffer(device, stagingBuffer, nullptr);
    vkFreeMemory(device, stagingBufferMemory, nullptr);
}
           

可以看到 createIndexBuffer 幾乎和 createVertexBuffer 一樣,隻有bufferSize和VkBufferUsageFlags不同而已,畢竟都是隻是緩沖區。

索引緩沖同樣也需要顯示銷毀:

void cleanup() {
    cleanupSwapChain();
    vkDestroyBuffer(device, indexBuffer, nullptr);
    vkFreeMemory(device, indexBufferMemory, nullptr);
    vkDestroyBuffer(device, vertexBuffer, nullptr);
    vkFreeMemory(device, vertexBufferMemory, nullptr);
    ...
}
           

3.2 使用頂點緩沖

使用索引緩沖區繪制涉及createCommandBuffers的兩個更改。我們首先需要綁定索引緩沖區,就像我們對頂點緩沖區所做的那樣。但是索引緩沖區隻能有一個。而且,不可能對每個頂點屬性使用不同的索引,是以即使隻有一個屬性發生變化,仍然需要完全複制頂點資料。

索引緩沖區與vkCmdBindIndexBuffer綁定,vkCmdBindIndexBuffer包含索引緩沖區、其中的位元組偏移量和索引資料類型作為參數。如前所述,可能的類型是VK_INDEX_TYPE_UINT16和VK_INDEX_TYPE_UINT32。

僅僅綁定索引緩沖區還不能改變任何東西,我們還需要更改繪圖指令來告訴Vulkan使用索引緩沖區。移除vkCmdDraw,并用vkCmdDrawIndexed替換:

VkBuffer vertexBuffers[] = {vertexBuffer};
        VkDeviceSize offsets[] = {0};
        vkCmdBindVertexBuffers(commandBuffers[i], 0, 1, vertexBuffers, offsets);
        // VK_INDEX_TYPE_UINT16 是因為我們索引用的就是uint16_t
        vkCmdBindIndexBuffer(commandBuffers[i], indexBuffer, 0, VK_INDEX_TYPE_UINT16);
        // 使用vkCmdDrawIndexed替換vkCmdDraw
        // vkCmdDraw(commandBuffers[i], static_cast<uint32_t>(vertices.size()), 1, 0, 0);
        vkCmdDrawIndexed(commandBuffers[i], static_cast<uint32_t>(indices.size()), 1, 0, 0, 0);
        vkCmdEndRenderPass(commandBuffers[i]);
           

對vkCmdDrawIndexed函數的調用非常類似于vkCmdDraw。前兩個參數指定索引的數量和執行個體的數量。我們沒有使用執行個體,是以隻指定一個執行個體。索引的數量表示将被傳遞到頂點緩沖區的頂點的數量。下一個參數指定到索引緩沖區的偏移量,使用值1将導緻顯示卡從第二個索引開始讀取。倒數第二個參數指定要添加到索引緩沖區中的索引的偏移量。最後一個參數指定了執行個體化的偏移量。

四. 繪制指令概述

繪制指令大緻分為兩類:非索引繪圖指令和索引繪圖指令。

4.1 非索引繪圖指令

非索引繪圖指令為頂點着色器提供一個連續的vertexIndex。順序索引是由裝置自動生成的,這些指令有:

  1. vkCmdDraw
  2. vkCmdDrawIndirect
  3. vkCmdDrawIndirectCount
  4. vkCmdDrawIndirectCountKHR
  5. vkCmdDrawIndirectCountAMD

4.1.1 vkCmdDraw

vkCmdDraw可以記錄一個非索引的繪制,其原型如下:

void vkCmdDraw(
    VkCommandBuffer                             commandBuffer,
    uint32_t                                    vertexCount,
    uint32_t                                    instanceCount,
    uint32_t                                    firstVertex,
    uint32_t                                    firstInstance);
           
  1. commandBuffer是指令記錄到的指令緩沖區
  2. vertexCount是要繪制的頂點數
  3. instanceCount是要繪制的執行個體數量
  4. firstVertex是繪制的第一個頂點的索引
  5. firstInstance是繪制的第一個執行個體的執行個體ID

執行該指令時,将使用目前基本體拓撲和頂點計數連續頂點索引(第一個頂點索引值等于第一個頂點)組裝基本體。原語繪制執行個體數量為instanceCount,instanceIndex從firstInstance開始,每個執行個體依次遞增。組裝原語的執行要綁定到圖形管道。

4.1.2 vkCmdDrawIndirect

vkCmdDrawIndirect用于記錄非索引的間接繪制,其原型如下:

void vkCmdDrawIndirect(
    VkCommandBuffer                             commandBuffer,
    VkBuffer                                    buffer,
    VkDeviceSize                                offset,
    uint32_t                                    drawCount,
    uint32_t                                    stride);
           
  1. commandBuffer是記錄指令的指令緩沖區
  2. buffer是包含繪圖參數的緩沖區
  3. offset是參數開始的緩沖區中的位元組偏移量
  4. drawCount是要執行的繪制數,可以為零
  5. stride是連續繪圖參數集之間的位元組步幅

vkCmdDrawIndirect的行為與vkCmdDraw類似,不同的是參數是在執行過程中由裝置從緩沖區讀取的。drawCount繪制由指令執行,參數從緩沖區的偏移量開始,每次繪制時按步長位元組遞增。每次繪制的參數都編碼在一個VkDrawIndirectCommand結構數組中。如果drawCount小于或等于1,則忽略stride。

4.1.3 vkCmdDrawIndirectCount、vkCmdDrawIndirectCountKHR、vkCmdDrawIndirectCountAMD

記錄來自緩沖區的draw調用計數的非索引繪制調用,可以使用vkCmdDrawIndirectCount,vkCmdDrawIndirectCountKHR或者vkCmdDrawIndirectCountAMD, 這三個指令幾乎等效:

void vkCmdDrawIndirectCount(
    VkCommandBuffer                             commandBuffer,
    VkBuffer                                    buffer,
    VkDeviceSize                                offset,
    VkBuffer                                    countBuffer,
    VkDeviceSize                                countBufferOffset,
    uint32_t                                    maxDrawCount,
    uint32_t                                    stride);

void vkCmdDrawIndirectCountKHR(
    VkCommandBuffer                             commandBuffer,
    VkBuffer                                    buffer,
    VkDeviceSize                                offset,
    VkBuffer                                    countBuffer,
    VkDeviceSize                                countBufferOffset,
    uint32_t                                    maxDrawCount,
    uint32_t                                    stride);

void vkCmdDrawIndirectCountAMD(
    VkCommandBuffer                             commandBuffer,
    VkBuffer                                    buffer,
    VkDeviceSize                                offset,
    VkBuffer                                    countBuffer,
    VkDeviceSize                                countBufferOffset,
    uint32_t                                    maxDrawCount,
    uint32_t                                    stride);
           
  1. commandBuffer是記錄指令的指令緩沖區
  2. buffer是包含繪圖參數的緩沖區
  3. offset是參數開始的緩沖區中的位元組偏移量
  4. countBuffer是包含繪圖計數的緩沖區
  5. countBufferOffset是開始繪制計數的位元組偏移到countBuffer中
  6. maxDrawCount指定将執行的最大繪制數。實際執行的繪制調用數是countBuffer和maxDrawCount中指定的最小計數
  7. stride是連續繪圖參數集之間的位元組步幅

vkCmdDrawIndirectCount的行為與vkCmdDrawIndirectCount類似,隻是在執行期間裝置從緩沖區讀取繪制計數。該指令将從位于countBufferOffset的countBuffer中讀取一個無符号32位整數,并将其用作繪圖計數。

4.2 索引繪圖指令

索引圖形指令從索引緩沖區讀取索引值,并使用此指令計算頂點着色器的vertexIndex值。這些指令有:

  1. vkCmdDrawIndexed
  2. vkCmdDrawIndexedIndirect
  3. vkCmdDrawIndexedIndirectCount
  4. vkCmdDrawIndexedIndirectCountKHR
  5. vkCmdDrawIndexedIndirectCountAMD

4.2.1 vkCmdDrawIndexed

vkCmdDrawIndexed可以記錄一個索引的繪制,其原型如下:

void vkCmdDrawIndexed(
    VkCommandBuffer                             commandBuffer,
    uint32_t                                    indexCount,
    uint32_t                                    instanceCount,
    uint32_t                                    firstIndex,
    int32_t                                     vertexOffset,
    uint32_t                                    firstInstance);
           
  1. commandBuffer是指令記錄到的指令緩沖區
  2. indexCount是要繪制的頂點數
  3. instanceCount是要繪制的執行個體數
  4. firstIndex是索引緩沖區中的基索引
  5. vertexOffset是在索引到頂點緩沖區之前添加到頂點索引的值
  6. firstInstance是要繪制的第一個執行個體的執行個體ID

在執行該指令時,使用目前基元拓撲和indexCount頂點組裝基元,這些頂點的索引是從索引緩沖區檢索的。索引緩沖區被視為一個緊湊封裝的大小無符号整數數組,該整數由vkCmdBindIndexBuffer::indexType形參定義,該形參與該緩沖區綁定。

第一個頂點索引位于綁定索引緩沖區中的firstIndex * indexSize + offset的偏移量,其中offset是由vkCmdBindIndexBuffer指定的偏移量,indexSize是由indexType指定的類型的位元組大小。從索引緩沖區中連續的位置檢索後續的索引值。索引首先與原始的重新開機值比較,然後0擴充到32位(如果indexType是VK_INDEX_TYPE_UINT8_EXT或VK_INDEX_TYPE_UINT16),并添加vertexOffset,然後再作為vertexIndex值提供。

這些原語是用從firstInstance開始的instanceIndex繪制instanceCount次數,并按順序增加每個執行個體。組裝的原語執行應綁定圖形管道。

4.2.2 vkCmdDrawIndexedIndirect

vkCmdDrawIndexedIndirect用于記錄索引的間接繪制,其原型如下:

void vkCmdDrawIndexedIndirect(
    VkCommandBuffer                             commandBuffer,
    VkBuffer                                    buffer,
    VkDeviceSize                                offset,
    uint32_t                                    drawCount,
    uint32_t                                    stride);
           
  1. commandBuffer是記錄指令的指令緩沖區
  2. buffer是包含繪圖參數的緩沖區
  3. offset是參數開始的緩沖區中的位元組偏移量
  4. drawCount是要執行的繪制數,可以為零
  5. stride是連續繪圖參數集之間的位元組步幅

vkCmdDrawIndexedIndirect的行為與vkcmddrawindex類似,不同的是參數是在執行過程中由裝置從緩沖區中讀取的。drawCount繪制由指令執行,參數從緩沖區的偏移量開始,每次繪制時按步長位元組遞增。每次繪制的參數都編碼在vkdrawindexdindirectcommand結構的數組中。如果drawCount小于或等于1,則忽略stride。

4.2.3 vkCmdDrawIndexedIndirectCount、vkCmdDrawIndexedIndirectKHR、vkCmdDrawIndexedIndirectAMD

同樣的,記錄來自緩沖區的draw調用計數的索引繪制調用,可以使用, 這三個指令幾乎等效:

void vkCmdDrawIndexedIndirectCount(
    VkCommandBuffer                             commandBuffer,
    VkBuffer                                    buffer,
    VkDeviceSize                                offset,
    VkBuffer                                    countBuffer,
    VkDeviceSize                                countBufferOffset,
    uint32_t                                    maxDrawCount,
    uint32_t                                    stride);

void vkCmdDrawIndexedIndirectCountKHR(
    VkCommandBuffer                             commandBuffer,
    VkBuffer                                    buffer,
    VkDeviceSize                                offset,
    VkBuffer                                    countBuffer,
    VkDeviceSize                                countBufferOffset,
    uint32_t                                    maxDrawCount,
    uint32_t                                    stride);

void vkCmdDrawIndexedIndirectCountAMD(
    VkCommandBuffer                             commandBuffer,
    VkBuffer                                    buffer,
    VkDeviceSize                                offset,
    VkBuffer                                    countBuffer,
    VkDeviceSize                                countBufferOffset,
    uint32_t                                    maxDrawCount,
    uint32_t                                    stride);
           
  1. commandBuffer是記錄指令的指令緩沖區
  2. buffer是包含繪圖參數的緩沖區
  3. offset是參數開始的緩沖區中的位元組偏移量
  4. countBuffer是包含繪制計數的緩沖區
  5. countBufferOffset是進入countBuffer的位元組偏移量,在這裡開始繪制計數
  6. maxDrawCount指定将執行的最大繪制數。實際執行的draw調用數是countBuffer和maxDrawCount中指定的最小計數
  7. stride是連續繪圖參數集之間的位元組步幅

vkCmdDrawIndexedIndirectCount的行為與vkCmdDrawIndexedIndirect類似,隻是在執行期間裝置從緩沖區讀取繪制計數。該指令将從位于countBufferOffset的countBuffer中讀取一個無符号32位整數,并将其用作繪圖計數。

五. 小結

在上一篇文章中,我們使用頂點描述符VkVertexInputBindingDescription和VkVertexInputAttributeDescription替換了寫死頂點,并且使用VkBuffer存儲了頂點資料,好處是可随時更改頂點資訊。在本文中,我們又使用了暫存緩沖優化了頂點緩沖每次都需要memcpy的弊端,還介紹了頂點索引,使得我們的程式可以畫出更多的圖形。

使用暫存緩沖是因為圖形管道使用頂點資料緩沖區時,如果需要更改頂點資料内容,還需要等待memcpy,如果使用暫存緩沖區,可以将更改頂點資料内容的操作放在另一個線程執行,等到寫完之後,再使用vkCmdCopyBuffer指令拷貝記憶體資料,這樣圖形管道最多等待這個指令拷貝的時間。當頂點資料多而且繪制内容複雜的時候就可以展現出來了。

而使用頂點索引緩沖是和頂點緩沖幾乎一樣的流程,隻是VkBuffer建立時的VkBufferUsageFlags和size(對應的資料不同嘛)不同。

不過這裡還是很好奇,頂點索引和頂點的關系,比如如果我們頂點坐标不變,頂點索引改成:

// 頂點索引
std::vector<uint16_t> indices = {
    0, 1, 2, 2, 3, 1
};
           

對應的圖形就變成了:

Vulkan入門(12)-暫存緩沖和索引緩沖.md參考資料簡述一. 傳輸隊列二. 暫存緩沖區三. 索引緩沖區四. 繪制指令概述五. 小結

但是當頂點索引改成:

// 頂點索引
std::vector<uint16_t> indices = {
    0, 1, 2, 2, 3, 4
};
           

對應的圖形就變成了:

Vulkan入門(12)-暫存緩沖和索引緩沖.md參考資料簡述一. 傳輸隊列二. 暫存緩沖區三. 索引緩沖區四. 繪制指令概述五. 小結

這個頂點索引和最終圖像的生成到底是什麼個關系呢,參考:https://www.douban.com/note/700990025/

Vulkan中的坐标系使用的右手坐标系,相比OpenGL是用的左手坐标系:

Vulkan入門(12)-暫存緩沖和索引緩沖.md參考資料簡述一. 傳輸隊列二. 暫存緩沖區三. 索引緩沖區四. 繪制指令概述五. 小結

其中原點(0,0,0)在螢幕中央, 是以當我們想畫一個三棱錐可以使用如下頂點及索引:

// 頂點資料
    std::vector<Vertex> vertices = {
        {{-0.25f, -0.01f}, {1.0f, 0.0f, 0.0f}},
        {{0.01f, -0.5f}, {0.0f, 1.0f, 0.0f}},
        {{0.25f, 0.01f}, {0.0f, 0.0f, 1.0f}},
        {{-0.01f, 0.15f}, {1.0f, 1.0f, 1.0f}}
    };
    // 頂點索引
    std::vector<uint16_t> indices = {
        0,1,2,2,3,0,0,1,3,1,2,3
    };
           
Vulkan入門(12)-暫存緩沖和索引緩沖.md參考資料簡述一. 傳輸隊列二. 暫存緩沖區三. 索引緩沖區四. 繪制指令概述五. 小結

接下來,讓我們再接再厲,學習使用資源描述符來加載3D圖形。