操作系统:Windows8.1

显卡:Nivida GTX965M

开发工具:Visual Studio 2017


Introduction

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

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中的部分代码(不包括映射)移入该函数。

  1. void createBuffer(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties, VkBuffer& buffer, VkDeviceMemory& bufferMemory) {
  2. VkBufferCreateInfo bufferInfo = {};
  3. bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
  4. bufferInfo.size = size;
  5. bufferInfo.usage = usage;
  6. bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
  7.  
  8. if (vkCreateBuffer(device, &bufferInfo, nullptr, &buffer) != VK_SUCCESS) {
  9. throw std::runtime_error("failed to create buffer!");
  10. }
  11.  
  12. VkMemoryRequirements memRequirements;
  13. vkGetBufferMemoryRequirements(device, buffer, &memRequirements);
  14.  
  15. VkMemoryAllocateInfo allocInfo = {};
  16. allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
  17. allocInfo.allocationSize = memRequirements.size;
  18. allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties);
  19.  
  20. if (vkAllocateMemory(device, &allocInfo, nullptr, &bufferMemory) != VK_SUCCESS) {
  21. throw std::runtime_error("failed to allocate buffer memory!");
  22. }
  23.  
  24. vkBindBufferMemory(device, buffer, bufferMemory, );
  25. }

该函数需要传递缓冲区大小,内存属性和usage最终创建不同类型的缓冲区。最后两个参数保存输出的句柄。

我们可以从createVertexBuffer函数中移除创建缓冲区和分配内存的代码,并使用createBuffer替代:

  1. void createVertexBuffer() {
  2. VkDeviceSize bufferSize = sizeof(vertices[]) * vertices.size();
  3. createBuffer(bufferSize, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, vertexBuffer, vertexBufferMemory);
  4.  
  5. void* data;
  6. vkMapMemory(device, vertexBufferMemory, , bufferSize, , &data);
  7. memcpy(data, vertices.data(), (size_t) bufferSize);
  8. vkUnmapMemory(device, vertexBufferMemory);
  9. }

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

Using a staging buffer


我们现在改变createVertexBuffer函数,仅仅使用host缓冲区作为临时缓冲区,并且使用device缓冲区作为最终的顶点缓冲区。

  1. void createVertexBuffer() {
  2. VkDeviceSize bufferSize = sizeof(vertices[]) * vertices.size();
  3.  
  4. VkBuffer stagingBuffer;
  5. VkDeviceMemory stagingBufferMemory;
  6. createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer, stagingBufferMemory);
  7.  
  8. void* data;
  9. vkMapMemory(device, stagingBufferMemory, , bufferSize, , &data);
  10. memcpy(data, vertices.data(), (size_t) bufferSize);
  11. vkUnmapMemory(device, stagingBufferMemory);
  12.  
  13. createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, vertexBuffer, vertexBufferMemory);
  14. }

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

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

vertexBuffer现在使用device类型作为分配的内存类型,意味着我们不可以使用vkMapMemory内存映射。然而我们可以从stagingBuffervertexBuffer拷贝数据。我们需要指定stagingBuffer的传输源标志位,还要为顶点缓冲区vertexBuffer的usage设置传输目标的标志位。

我们新增函数copyBuffer,用于从一个缓冲区拷贝数据到另一个缓冲区。

  1. void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) {
  2.  
  3. }

使用命令缓冲区执行内存传输的操作命令,就像绘制命令一样。因此我们需要分配一个临时命令缓冲区。或许在这里希望为短期的缓冲区分别创建command pool,那么可以考虑内存分配的优化策略,在command pool生成期间使用VK_COMMAND_POOL_CREATE_TRANSIENT_BIT标志位。

  1. void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) {
  2. VkCommandBufferAllocateInfo allocInfo = {};
  3. allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
  4. allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
  5. allocInfo.commandPool = commandPool;
  6. allocInfo.commandBufferCount = ;
  7.  
  8. VkCommandBuffer commandBuffer;
  9. vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer);
  10. }

立即使用命令缓冲过去进行记录:

  1. VkCommandBufferBeginInfo beginInfo = {};
  2. beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
  3. beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
  4.  
  5. vkBeginCommandBuffer(commandBuffer, &beginInfo);

应用于绘制命令缓冲区的VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT标志位在此不必要,因为我们之需要使用一次命令缓冲区,等待该函数返回,直到复制操作完成。告知driver驱动程序使用VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT是一个好的习惯。

  1. VkBufferCopy copyRegion = {};
  2. copyRegion.srcOffset = ; // Optional
  3. copyRegion.dstOffset = ; // Optional
  4. copyRegion.size = size;
  5. vkCmdCopyBuffer(commandBuffer, srcBuffer, dstBuffer, , &copyRegion);

缓冲区内容使用vkCmdCopyBuffer命令传输。它使用source和destination缓冲区及一个缓冲区拷贝的区域作为参数。这个区域被定义在VkBufferCopy结构体中,描述源缓冲区的偏移量,目标缓冲区的偏移量和对应的大小。与vkMapMemory命令不同,这里不可以指定VK_WHOLE_SIZE

  1. vkEndCommandBuffer(commandBuffer);

此命令缓冲区仅包含拷贝命令,因此我们可以在此之后停止记录。现在执行命令缓冲区完成传输:

  1. VkSubmitInfo submitInfo = {};
  2. submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
  3. submitInfo.commandBufferCount = ;
  4. submitInfo.pCommandBuffers = &commandBuffer;
  5.  
  6. vkQueueSubmit(graphicsQueue, , &submitInfo, VK_NULL_HANDLE);
  7. vkQueueWaitIdle(graphicsQueue);

与绘制命令不同的是,这个时候我们不需要等待任何事件。我们只是想立即在缓冲区执行传输命令。这里有同样有两个方式等待传输命令完成。我们可以使用vkWaitForFences等待栅栏fence,或者只是使用vkQueueWaitIdle等待传输队列状态变为idle。一个栅栏允许安排多个连续的传输操作,而不是一次执行一个。这给了驱动程序更多的优化空间。

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

不要忘记清理用于传输命令的命令缓冲区。

我们可以从createVertexBuffer函数中调用copyBuffer,拷贝顶点数据到设备缓冲区中:

  1. createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, vertexBuffer, vertexBufferMemory);
  2.  
  3. copyBuffer(stagingBuffer, vertexBuffer, bufferSize)

当从暂存缓冲区拷贝数据到图形卡设备缓冲区完毕后,我们应该清理它:

  1. ...
  2.  
  3. copyBuffer(stagingBuffer, vertexBuffer, bufferSize);
  4.  
  5. vkDestroyBuffer(device, stagingBuffer, nullptr);
  6. vkFreeMemory(device, stagingBufferMemory, nullptr);
  7. }

运行程序确认三角形绘制正常。性能的提升也许现在不能很好的显现出来,但其顶点数据已经是从高性能的显存中加载。当我们开始渲染更复杂的几何图形时,这个技术是非常重要。

Conclusion


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

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

项目代码 GitHub 地址。

Vulkan Tutorial 21 Staging buffer的更多相关文章

  1. Vulkan Tutorial 20 Vertex buffer creation

    操作系统:Windows8.1 显卡:Nivida GTX965M 开发工具:Visual Studio 2017 Introduction 在Vulkan中,缓冲区是内存的一块区域,该区域用于向显卡 ...

  2. Vulkan Tutorial 22 Index buffer

    操作系统:Windows8.1 显卡:Nivida GTX965M 开发工具:Visual Studio 2017 Introduction 在实际产品的运行环境中3D模型的数据往往共享多个三角形之间 ...

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

    [译]Vulkan教程(21)顶点input描述 Vertex input description 顶点input描述 Introduction 入门 In the next few chapters ...

  4. Vulkan Tutorial 25 Images

    操作系统:Windows8.1 显卡:Nivida GTX965M 开发工具:Visual Studio 2017 Introduction 到目前为止,几何图形使用每个顶点颜色进行着色处理,这是一个 ...

  5. [译]Vulkan教程(24)索引buffer

    [译]Vulkan教程(24)索引buffer Index buffer 索引buffer Introduction 入门 The 3D meshes you'll be rendering in a ...

  6. Vulkan Tutorial 23 Descriptor layout and buffer

    操作系统:Windows8.1 显卡:Nivida GTX965M 开发工具:Visual Studio 2017 Introduction 我们现在可以将任意属性传递给每个顶点的顶点着色器使用.但是 ...

  7. Vulkan Tutorial 11 Shader modules

    操作系统:Windows8.1 显卡:Nivida GTX965M 开发工具:Visual Studio 2017 与之前的图像API不同,Vulkan中的着色器代码必须以二进制字节码的格式使用,而不 ...

  8. Vulkan Tutorial 12 Fixed functions

    操作系统:Windows8.1 显卡:Nivida GTX965M 开发工具:Visual Studio 2017 早起的图形API在图形渲染管线的许多阶段提供了默认的状态.在Vulkan中,从vie ...

  9. Vulkan Tutorial 16 Command buffers

    操作系统:Windows8.1 显卡:Nivida GTX965M 开发工具:Visual Studio 2017 诸如绘制和内存操作相关命令,在Vulkan中不是通过函数直接调用的.我们需要在命令缓 ...

随机推荐

  1. synchronized 修饰在 static方法和非static方法的区别

    Java中synchronized用在静态方法和非静态方法上面的区别 在Java中,synchronized是用来表示同步的,我们可以synchronized来修饰一个方法.也可以synchroniz ...

  2. Ultimus BPM 制药与医疗行业应用解决方案

    Ultimus BPM 制药与医疗行业应用解决方案 行业应用需求 制药与医疗行业客户特点有企业总资产高.员工规模大,销售网络往往遍及全国,乃至全球市场:拥有复杂的制药生产或医疗服务组织机构,并均有严格 ...

  3. .net操作压缩文件

    附件:SharpZipLib.zip public class UnZipClass//解压 { /// <summary> /// 解压功能(解压压缩文件到指定目录) /// </ ...

  4. Java操作PDF之iText超入门

    iText是著名的开放项目,是用于生成PDF文档的一个java类库.通过iText不仅可以生成PDF或rtf的文档,而且可以将XML.Html文件转化为PDF文件. http://itextpdf.c ...

  5. TWaver 2D+GIS+3D的试用和在线Demo

    TWaver 2D for HTML5试用下载: http://download.servasoft.com/dl/twaver/sssyuwyeriUR/k/twaver-html5-5.4.7.z ...

  6. mongodb 创建LBS位置索引

    <dependency> <groupId>org.mongodb</groupId> <artifactId>mongo-java-driver< ...

  7. 【charger battery 充電 充電器 電池】過充保護警告訊息 over charging protection,Battery over voltage protection, warning message

    Definition: over charging protection.battery over voltage protection, 是一種 battery 保護機制, 避免 battery 充 ...

  8. qrcode生成二维码插件

    今天我要和大家分享的是利用qrcode来生成二维码. 首先要使用qrcode就需要引用文件,我这边用的是1.7.2版本的jquery加上qrcode <script type="tex ...

  9. java基础之数组常用操作

    常用的对数组进行的操作 1.求数组中最大值,最小值 思路:假设下标为0的元素是最大值,遍历数组,依次跟max进行比较,如果有元素比这个max还大,则把这个值赋给max.最小值同样 public cla ...

  10. Assert与内存泄漏

    以前知道C/C++有assert之后,我想知道assert会不会造成内存泄漏,于是我做了一个测试: #include <iostream> #include <fstream> ...