Vulkan Tutorial 21 Staging buffer

操作系统:Windows8.1

显卡:Nivida GTX965M

开发工具:Visual Studio 2017


Introduction

顶点缓冲区现在已经可以正常工作,但相比于显卡内部读取数据,单纯从CPU访问内存数据的方式性能不是最佳的。最佳的方式是采用VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT标志位,通常来说用在专用的图形卡,CPU是无法访问的。在本章节我们创建两个顶点缓冲区。一个缓冲区提供给CPU-HOST内存访问使用,用于从顶点数组中提交数据,另一个顶点缓冲区用于设备local内存。我们将会使用缓冲区拷贝的命令将数据从预缓冲区COPY到实际的图形卡内存中。

Transfer queue



缓冲区拷贝的命令需要队列簇支持传输操作,可以通过VK_QUEUE_TRANSFER_BIT标志位指定。好消息是任何支持VK_QUEUE_GRAPHICS_BIT 或者 VK_QUEUE_COMPUTE_BIT标志位功能的队列簇都默认支持VK_QUEUE_TRANSFER_BIT操作。这部分的实现不需要在queueFlags显示的列出。

如果需要挑战,甚至可以尝试为不同的队列簇指定具体的传输操作。这部分实现需要对代码做出如下修改:

  • 修改QueueFamilyIndicesfindQueueFamilies,明确指定队列簇需要具备VK_QUEUE_TRANSFER标志位,而不是VK_QUEUE_GRAPHICS_BIT
  • 修改createLogicalDevice函数,请求一个传输队列句柄。
  • 创建两个命令对象池分配命令缓冲区,用于向传输队列簇提交命令。
  • 修改资源的sharingModeVK_SHARING_MODE_CONCURRENT,并指定为graphics和transfer队列簇。
  • 提交任何传输命令,诸如vkCmdCopyBuffer(本章节使用)到传输队列,而不是图形队列。

需要一些额外的工作,但是它我们更清楚的了解资源在不同队列簇如何共享的。

Abstracting buffer creation



考虑到我们在本章节需要创建多个缓冲区,比较理想的是创建辅助函数来完成。新增函数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);
}

运行程序确保顶点缓冲区仍然正常工作。

Using a staging buffer



我们现在改变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);
}

我们使用stagingBufferMemory划分新的stagingBuffer预缓冲区用来映射、拷贝顶点数据。在本章节我们使用两个新的缓冲区usage标致类型:

  • VK_BUFFER_USAGE_TRANSFER_SRC_BIT:缓冲区可以用于源内存传输操作。
  • VK_BUFFER_USAGE_TRANSFER_DST_BIT:缓冲区可以用于目标内存传输操作。

vertexBuffer现在使用device类型作为分配的内存类型,意味着我们不可以使用vkMapMemory内存映射。然而我们可以从stagingBuffervertexBuffer拷贝数据。我们需要指定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, &copyRegion);

缓冲区内容使用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);
}

运行程序确认三角形绘制正常。它是可见的,但其顶点数据现在是从高性能的显存中加载。当我们开始渲染更复杂的几何图形时,这个技术是非常重要。

Conclusion



需要了解的是,在真实的生产环境中的应用程序里,不建议为每个缓冲区调用vkAllocateMemory分配内存。内存分配的最大数量受到maxMemoryAllocationCount物理设备所限,及时在像NVIDIA GTX1080这样的高端硬件上,也只能提供4096的大小。同一时间,为大量对象分配内存的正确方法是创建一个自定义分配器,通过使用我们在许多函数中用到的偏移量offset,将一个大块的可分配内存区域划分为多个可分配内存块,提供缓冲区使用。

也可以自己实现一个灵活的内存分配器,或者使用GOUOpen提供的VulkanMemoryAllocator库。然而,对于本教程,我们可以做到为每个资源使用单独的分配,因为我们不会触达任何资源限制条件。

项目代码 GitHub 地址。

时间: 2024-12-16 12:22:14

Vulkan Tutorial 21 Staging buffer的相关文章

Vulkan Tutorial 22 Index buffer

操作系统:Windows8.1 显卡:Nivida GTX965M 开发工具:Visual Studio 2017 Introduction 在实际产品的运行环境中3D模型的数据往往共享多个三角形之间的顶点数据.即使绘制一些简单的图形也是如此,比如矩形: 绘制矩形需要两个三角形,通常意味着我们需要6个顶点数据.问题是其中的两个顶点会重复,导致数据会有50%的冗余.如果更复杂的模型,该问题会更加严重,平均每三个三角形就会发生重复顶点使用的情况.解决问题的方法是使用index buffer,即索引缓

[译]Vulkan教程(21)顶点input描述

Vertex input description 顶点input描述 Introduction 入门 In the next few chapters, we're going to replace the hardcoded vertex data in the vertex shader with a vertex buffer in memory. We'll start with the easiest approach of creating a CPU visible buffer

Vulkan Tutorial 19 Vertex input description

操作系统:Windows8.1 显卡:Nivida GTX965M 开发工具:Visual Studio 2017 Introduction 在接下来几个章节中,我们将会使用内存顶点缓冲区来替换之前硬编码到vertex shader中的顶点数据.我们将从最简单的方法开始创建一个CPU可见的缓冲区,并使用memcpy直接将顶点数据直接复制到缓冲区,之后将会使用分段缓冲区将顶点数据赋值到高性能的内存. Vertex shader 首先要修改的是顶点着色器,不再包含顶点数据.顶点着色器接受顶点缓冲区的

Vulkan Tutorial 12 Fixed functions

操作系统:Windows8.1 显卡:Nivida GTX965M 开发工具:Visual Studio 2017 早起的图形API在图形渲染管线的许多阶段提供了默认的状态.在Vulkan中,从viewport的大小到混色函数,需要凡事做到亲历亲为.在本章节中我们会填充有关固有功能操作的所有结构体. Vertex input VkPipelineVertexInputStateCreateInfo结构体描述了顶点数据的格式,该结构体数据传递到vertex shader中.它以两种方式进行描述:

Vulkan Tutorial 29 Loading models

操作系统:Windows8.1 显卡:Nivida GTX965M 开发工具:Visual Studio 2017 Introduction 应用程序现在已经可以渲染纹理3D模型,但是 vertices 顶点和 indices 索引数组中的几何体不是很有趣.在本章节我们扩展程序,从实际的模型文件冲加载顶点和索引数据,并使图形卡实际做一些工作. 许多图形API系列教程中让读者在这样的章节中编写自己的OBJ加载程序.这样做的问题是任何有趣的3D应用程序很快需要某种功能,但是该文件格式不支持,比如骨骼

Vulkan Tutorial 11 Shader modules

操作系统:Windows8.1 显卡:Nivida GTX965M 开发工具:Visual Studio 2017 与之前的图像API不同,Vulkan中的着色器代码必须以二进制字节码的格式使用,而不是像GLSL和HLSL这样具有比较好的可读性的语法.此字节格式成为SPIR-V,它可以与Vulkan和OpenCL一同使用.这是一种可以编写图形和计算着色器的格式,但我们重点介绍本教程中Vulkan图形流水线使用的着色器. 使用二进制字节码格式的优点之一是 使得GPU厂商编写将着色器代码转换为本地代

Vulkan Tutorial 02 编写Vulkan应用程序框架原型

操作系统:Windows8.1 显卡:Nivida GTX965M 开发工具:Visual Studio 2017 General structure 在上一节中,我们创建了一个正确配置.可运行的的Vulkan应用程序,并使用测试代码进行了测试.本节中我们从头开始,使用如下代码构建一个基于GLFW的Vulkan应用程序原型框架的雏形. #include <vulkan/vulkan.h> #include <iostream> #include <stdexcept>

Vulkan Tutorial 05 物理设备与队列簇

操作系统:Windows8.1 显卡:Nivida GTX965M 开发工具:Visual Studio 2017 Selecting a physical device 通过VkInstance初始化Vulkan后,我们需要在系统中查找并选择一个支持我们所需功能的显卡.实际上,我们可以选择任意数量的显卡并同时使用他们,但在本小节中,我们简单的设定选择规则,即将查找到的第一个图形卡作为我们适合的物理设备. 我们添加函数pickPhysicalDevice并在initVulkan函数中调用. vo

Vulkan Tutorial 01 开发环境搭建之Windows

操作系统:Windows8.1 显卡:Nivida GTX965M 开发工具:Visual Studio 2017 相信很多人在开始学习Vulkan开发的起始阶段都会在开发环境的配置上下一些功夫,那么本问将会引导大家快速的完成Vulkan在Windows下的开发环境,并使用几个常用的开发工具库. Vulkan SDK 开发Vulkan应用程序所需的最重要的组件就是SDK.它包括核心头文件.标准的Validation layers及调试工具集.和驱动Loader,如果现在这些关键词不是很明白的话,