操作系统:Windows8.1

显卡:Nivida GTX965M

开发工具:Visual Studio 2017

Introduction


在Vulkan中,缓冲区是内存的一块区域,该区域用于向显卡提供预要读取的任意数据。它们可以用来存储顶点数据,也可以用于其他目的。与之前创建的Vulkan对象不同的是,缓冲区自己不会分配内存空间。前几个章节了解到,Vulkan API使开发者控制所有的实现,内存管理是其中一个非常重要的环节。

Buffer creation


添加新的函数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[]) * 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[]) * 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); ...
}

Memory requirements


虽然缓冲区创建完成了,但是实际上并没有分配任何可用内存。给缓冲区分配内存的第一步是vkGetBufferMemoryRequirements函数查询内存需求。

VkMemoryRequirements memRequirements;
vkGetBufferMemoryRequirements(device, vertexBuffer, &memRequirements);

VkMemoryRequirements结构体有三个字段:

  • size: 需要的内存字节大小,可能与bufferInfo.size大小不一致。
  • alignment: 缓冲区的内存分配区域开始的字节偏移量,它取决于bufferInfo.usagebufferInfo.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 = ; i < memProperties.memoryTypeCount; i++) {
if (typeFilter & ( << 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 = ; i < memProperties.memoryTypeCount; i++) {
if ((typeFilter & ( << i)) && (memProperties.memoryTypes[i].propertyFlags & properties) == properties) {
return i;
}
}

在将来我们可能不止一个所需属性,所以我们应该检查按位AND的结果是否为零,而不是直接等于期望的属性位字段。如果有一个内存类型适合我们的缓冲区,它也具有需要的所有属性,那么我们就返回它的索引,否则我们抛出一个异常信息。

Memory allocation


我们现在决定了正确的内存类型,所以我们可以通过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。如果偏移量non-zero,那么需要通过memRequirements.alignment整除。

当然,就像在C++动态分配内存一样,所分配的内存需要在某个节点释放。当缓冲区不再使用时,绑定到缓冲区对象的内存获取会被释放,所以让我们在缓冲区被销毁后释放它们:

void cleanup() {
cleanupSwapChain(); vkDestroyBuffer(device, vertexBuffer, nullptr);
vkFreeMemory(device, vertexBufferMemory, nullptr);

Filling the vertex buffer


现在将顶点数据Copy到缓冲区。使用vkMapMemory将缓冲区内存映射(mapping the buffer memory)到CPU可访问的内存中完成。

void* data;
vkMapMemory(device, vertexBufferMemory, , bufferInfo.size, , &data);

该功能允许我们访问由偏移量和大小指定的内存资源的区域。在这里offset和size分别是0bufferInfo.size,还可以指定特殊值VK_WHOLE_SIZE来映射所有内存。第二个到最后一个参数可以用于指定标志位,但是当前版本的API还没有可用的参数。它必须设置为0

void* data;
vkMapMemory(device, vertexBufferMemory, , bufferInfo.size, , &data);
memcpy(data, vertices.data(), (size_t) bufferInfo.size);
vkUnmapMemory(device, vertexBufferMemory);

可以简单的通过memcpy将顶点数据拷贝到映射内存中,并使用vkUnmapMemory取消映射。不幸的是,驱动程序是不会立即将数据复制到缓冲区中,比如缓存的原因。也可能尝试映射的内存对于写缓冲区操作不可见。处理该类问题有两种方法:

我们使用第一个方式,它确保了映射的内存总是与实际分配的内存一致。需要了解的是,这种方式与明确flushing操作相比,可能对性能有一点减损。但是我们在下一章会了解为什么无关紧要。

Binding the vertex buffer


现在讨论渲染期间绑定缓冲区操作。我们将会扩展createCommandBuffers函数。

vkCmdBindPipeline(commandBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline);

VkBuffer vertexBuffers[] = {vertexBuffer};
VkDeviceSize offsets[] = {};
vkCmdBindVertexBuffers(commandBuffers[i], , , vertexBuffers, offsets); vkCmdDraw(commandBuffers[i], static_cast<uint32_t>(vertices.size()), , , );

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, 1.0f, 0.0f}},
{{-0.5f, 0.5f}, {0.0f, 0.0f, 1.0f}}
};

再次运行程序看到如下图:

在下一章节中,我们将会介绍将顶点数据复制到顶点缓冲区的不同方式,从而实现更好的性能,但需要更多的工作。

项目代码 GitHub地址。

Vulkan Tutorial 20 Vertex buffer creation的更多相关文章

  1. Vulkan Tutorial 22 Index buffer

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

  2. Vulkan Tutorial 19 Vertex input description

    操作系统:Windows8.1 显卡:Nivida GTX965M 开发工具:Visual Studio 2017 Introduction 在接下来几个章节中,我们将会使用内存顶点缓冲区来替换之前硬 ...

  3. Vulkan Tutorial 21 Staging buffer

    操作系统:Windows8.1 显卡:Nivida GTX965M 开发工具:Visual Studio 2017 Introduction 顶点缓冲区现在已经可以正常工作,但相比于显卡内部读取数据, ...

  4. Vulkan SDK之Vertex Buffer

    A vertex buffer is a CPU-visible and GPU-visible buffer that contains the vertex data that describes ...

  5. [译]Vulkan教程(20)重建交换链

    [译]Vulkan教程(20)重建交换链 Swap chain recreation 重建交换链 Introduction 入门 The application we have now success ...

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

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

  7. Vulkan Tutorial 12 Fixed functions

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

  8. CSharpGL(38)带初始数据创建Vertex Buffer Object的情形汇总

    CSharpGL(38)带初始数据创建Vertex Buffer Object的情形汇总 开始 总的来说,OpenGL应用开发者会遇到为如下三种数据创建Vertex Buffer Object的情形: ...

  9. DEVICE DRAW VERTEX BUFFER TOO SMALL

    D3D11 WARNING #356 这个傻warning的意思看起来是说vertex buffer 太小了 描述是这样的: Vertex Buffer at the input vertex slo ...

随机推荐

  1. poj1410计算几何线段相交

    You are to write a program that has to decide whether a given line segment intersects a given rectan ...

  2. O(nlogn)实现LCS与LIS

    序: LIS与LCS分别是求一个序列的最长不下降序列序列与两个序列的最长公共子序列. 朴素法都可以以O(n^2)实现. LCS借助LIS实现O(nlogn)的复杂度,而LIS则是通过二分搜索将复杂度从 ...

  3. 思考题:用Use Case获取需求的方法是否有什么缺陷,还有什么地方需要改进?(提示:是否对所有的应用领域都适用?使用的方便性?.......)

    思考题: 用Use Case获取需求的方法是否有什么缺陷,还有什么地方需要改进?(提示:是否对所有的应用领域都适用?使用的方便性?.......) 简答: 一.用例解释: 在软件工程中,用例是一种在开 ...

  4. 简单jQuery 实现手风琴菜单

    简单jQuery 实现手风琴菜单 css代码 如下: *{ margin: 0; padding: 0; } #accordion{ width: 500px; } #accordion>div ...

  5. gitlab 取消注册功能

    gitlab 默认安装完成以后是允许用户注册,公司内部使用所以准备禁用了注册功能,只允许管理员从后台开通权限: 1.进入"Admin Area" 2.在左边菜单栏最低下点击&quo ...

  6. TP框架 命名空间 与第三方类

    命名空间 相当于虚拟目录 所有类文件都放在虚拟目录 功能:实现自动加载类 TP框架的命名空间要更复杂 内容=> 命名空间中定义和使用 都用\1初始命名空间 相当于 根目录 如:Library文件 ...

  7. Lua学习(4)——函数

    在Lua中函数的调用方式和C语言基本相同,如:print("Hello World")和a = add(x, y).唯一的差别是,如果函数只有一个参数,并且该参数的类型为字符串常量 ...

  8. vue+vux+axios+vuex+vue-router的项目的理解

    本文主要是讲解项目前期的工作,后期考虑再详细说明. 作为一个技术团队如果你们团队选择了上面的技术栈,这说明你们的技术团体对于vue有很熟练的掌握了.在这里我想说明的是前期架构的重要.这里有一遍博客写的 ...

  9. Zepto源码分析-callbacks模块

    // Zepto.js // (c) 2010-2015 Thomas Fuchs // Zepto.js may be freely distributed under the MIT licens ...

  10. 选择排序——Python实现

    选择排序: 选择排序(Selection sort)是一种简单直观的排序算法.它的工作原理如下.首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小( ...