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

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

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

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

void createVertexBuffer() {
VkDeviceSize bufferSize = sizeof(vertices[]) * 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, , bufferSize, , &data);
memcpy(data, vertices.data(), (size_t) bufferSize);
vkUnmapMemory(device, vertexBufferMemory);
}

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

Using a staging buffer


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

void createVertexBuffer() {
VkDeviceSize bufferSize = sizeof(vertices[]) * 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, , bufferSize, , &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);
}

我们使用stagingBuffer来划分stagingBufferMemory缓冲区用来映射、拷贝顶点数据。在本章节我们使用两个新的缓冲区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 = ; 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 = ; // Optional
copyRegion.dstOffset = ; // Optional
copyRegion.size = size;
vkCmdCopyBuffer(commandBuffer, srcBuffer, dstBuffer, , &copyRegion);

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

vkEndCommandBuffer(commandBuffer);

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

VkSubmitInfo submitInfo = {};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submitInfo.commandBufferCount = ;
submitInfo.pCommandBuffers = &commandBuffer; vkQueueSubmit(graphicsQueue, , &submitInfo, VK_NULL_HANDLE);
vkQueueWaitIdle(graphicsQueue);

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

vkFreeCommandBuffers(device, commandPool, , &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 地址。

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. 被低估的选手 - JavaFx

    被低估的选手 - JavaFx 1.MFC(Visual C++) 个人不是很喜欢这个框架,太多系统定义的东西,就像无底洞,学都学不完,这个东西需要你有比较强的记忆力,并且能融会贯通里面很多预定义的功 ...

  2. C++ 元编程 —— 让编译器帮你写程序

    目录 1 C++ 中的元编程 1.1 什么是元编程 1.2 元编程在 C++ 中的位置 1.3 C++ 元编程的历史 2 元编程的语言支持 2.1 C++ 中的模板类型 2.2 C++ 中的模板参数 ...

  3. 【Android Widget】2.ImageView

    1.属性详解 1.1 ScaleType属性详解 ImageView的Scaletype决定了图片在View上显示时的样子,如进行何种比例的缩放,及显示图片的整体还是部分,等等. 设置的方式包括: 1 ...

  4. Haoop基本操作

    一.HDFS的常用操作 (一).HDFS文件的权限 与Linux文件权限类似 r: read; w:write; x:execute,权限x对于文件忽略,对于文件夹表示是否允许访问其内容. 如果Lin ...

  5. MyEclipse9.0破解

    http://www.cnblogs.com/liulanglang/archive/2011/11/15/2249702.html

  6. 广义后缀树(GST)算法的简介

    导言 最近软件安全课上,讲病毒特征码的提取时,老师讲了一下GST算法.这里就做个小总结. 简介 基本信息  广义后缀树的英文为Generalized Suffix Tree,简称GST. 算法目的   ...

  7. 如何选择版本控制系统 ---为什么选择Git版本控制系统

    版本控制系统 "代码"作为软件研发的核心产物,在整个开发周期都在递增,不断合入新需求以及解决bug的新patch,这就需要有一款系统,能够存储.追踪文件的修改历史,记录多个版本的开 ...

  8. Swift 中 String 取下标及性能问题

    Swift 中 String 取下标及性能问题 取下标 String String 用 String.Index 取下标(subscript)得到 Character,String.Index 要从 ...

  9. 关于MySQL数据库的一些操作

    启动:net start MySQL 关闭:net stop MySQL (也可以用quit:) 登录到MySQL:mysql -u root -p -u : 所要登录的用户名; -p : 告诉服务器 ...

  10. 【注意事项】APP左右横滑设计

    移动端屏幕越来越大,但用户对内容量的要求也水涨船高.如何在有限的屏幕内透出更多的内容,是设计师们研究的重点. 常用的内容拓展设计有:Y 方向 List 滑动.Z 方向 3D Touch .入口式内容折 ...