Vulkan教程 - 15 索引缓冲

2024-08-21 19:58
文章标签 15 教程 索引 缓冲 vulkan

本文主要是介绍Vulkan教程 - 15 索引缓冲,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

        顶点缓冲已经能正常工作了,但是让我们能够从CPU访问的内存类型可能对显卡本身读取来说不是最优的。最好的内存会有VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT标记,且通常在专用显卡上不可以用CPU访问。本章我们创建两个顶点缓冲,一个位于CPU可访问内存中的临时缓冲来上传来自顶点数组的数据,一个设备本地内存中的最终的顶点缓冲。我们使用缓冲复制命令来移动数据,从临时缓冲移动到实际顶点缓冲中。

        缓冲复制命令要求队列族支持转移操作,用VK_QUEUE_TRANSFER_BIT标记。一个好消息是,任意队列族,有VK_QUEUE_GRAPHICS_BIT或者VK_QUEUE_COMPUTE_BIT能力的话,其实已经隐式支持VK_QUEUE_TRANSFER_BIT操作了。这些情况下,实现并不要显式罗列到queueFlags中。

        如果你喜欢挑战自己,那么你仍然可以尝试使用一个不同的专门用于转移操作的队列族。它会要求你做以下修改:

        修改QueueFamilyIndices和findQueueFamilies以显式查找有VK_QUEUE_TRANSFER位的队列族,但不是VK_QUEUE_GRAPHICS_BIT;

        修改createLogicalDevice来获取转移队列句柄;

        为已经提交到转移队列族的命令缓冲创建一个次命令池;

        修改资源的sharingMode为VK_SHARING_MODE_CONCURRENT,并同时指定图形和转移队列族;

        提交任何转移命令如vkCmdCopyBuffer(本章我们也是用这个)到转移队列而不是图形队列。

        是有一些工作量,但是它会教你很多东西,就是关于资源如何在不同队列族间共享的内容。

        因为我们要创建多重缓冲,将缓冲创建移动到助手方法中是个不错的想法。创建一个新的方法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);
}

        确保添加了缓冲大小,内存属性和使用方法等参数以便我们用该方法创建多个不同类型的缓冲。最后两个参数是输出变量,以便向其写入句柄。

        现在可以从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);
}

        运行下程序,确保顶点缓冲没有问题。

        我们现在打算修改createVertexBuffer,以仅仅使用一个可见缓冲作为临时缓冲,并使用设备本地的一个作为实际顶点缓冲。

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用于映射和拷贝顶点数据。本章中我们将会使用两个新的缓冲用法标记:

        VK_BUFFER_USAGE_TRANSFER_SRC_BIT:在内存转移操作中,缓冲可以用作源地址;

        VK_BUFFER_USAGE_TRANSFER_DST_BIT:在内存转移操作中,缓冲可以用作目的地。

        vertexBuffer现在从设备本地类型的内存中分配,一般表示我们无法使用vkMapMemory了。但是,我们可以从stagingBuffer中拷贝数据到vertexBuffer。我们必须通过指定stagingBuffer的转移源标记,vertexBuffer的转移目的地标记,以及顶点缓冲用法标记,来表示我们想要那么做。

        我们现在打算写一个方法来从一个缓冲拷贝内容到另一个:

void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) {}

        内存转移操作通过命令缓冲执行,就和绘制命令一样。因此我们必须首先分配一个临时命令缓冲。你可能希望能为这些短暂存在的缓冲创建一个单独的命令池,因为实现可能会应用于内存分配优化。在这种情况下,你应该在命令池生成过程中使用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标记这里并不是必需的,因为我们只是打算使用一次命令缓冲,然后从方法中用返回来等待,直到复制操作已经完成。告诉驱动我们使用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命令进行转移。它接收源和目的缓冲作为参数,以及一个要拷贝的区域数组。区域在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使用一个栅栏,或者简单地用vkQueueWaitIdle等待转移队列变空闲。栅栏会允许你同时计划多个转移,并等待所有都完成,而不是一次只能执行一个。这样也给驱动更多机会优化。

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

        运行程序确保能看到原来熟悉的三角形。现在可能还看不到我们的改进,但是现在顶点数据是从高性能内存中加载的。当我们渲染更复杂几何对象的时候会有影响。

        要注意的是,真实的程序中不应该对每个缓冲调用vkAllocateMemory。内存分配数量最大值由物理设备maxMemoryAllocationCount限制,可能在高端显卡如1080上也仅有4096而已。对大量对象分配内存的正确方法是创建一个自定义的分配器,将多个不同物体的一个分配操作使用offset参数进行切分。

        你要渲染在真实程序中的3D网格常常会在多个三角形中共享顶点。就是很简单的东西如绘制一个矩形就会发生这种事情:

        绘制一个矩形需要两个三角形,意味着我们需要一个有6个顶点的顶点缓冲。问题是,两个顶点的数据需要重复,导致50%的冗余。对于更复杂的网格表现会更糟,解决办法就是使用索引缓冲。

        索引缓冲实际上是一组指向顶点缓冲的指针。它允许你记录顶点数据,对多个顶点重用已有的数据。上面的插图表明了矩形的索引缓冲看起来会是什么样子,如果我们有一个顶点缓冲包含了所有四个不同顶点的话。第一组三个顶点定义了右上三角形,后面三个顶点定义了左下的三角形。

        本章我们要修改顶点数据,添加索引数据来绘制矩形。修改顶点数据来表示四个角:

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

        左上角是红色,右上角是绿色,右下角是蓝色,左下角是白色。我们添加一个新的数组indices来表示索引缓冲的内容。它应该和插图中绘制右上和左下三角形的索引匹配:

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

        索引缓冲可以使用uint16_t或者uint32_t,这取决于vertices中记录的个数。我们还是用uint16_t,因为我们使用的互不相同的顶点少于65535。

        就和顶点数据一样,索引需要加载到VkBuffer以便GPU能访问。定义两个新的类成员来存储索引缓冲资源:

VkCommandPool commandPool;
VkBuffer vertexBuffer;
VkDeviceMemory vertexBufferMemory;
VkBuffer indexBuffer;
VkDeviceMemory indexBufferMemory;

        我们将要添加的createIndexBuffer方法就和createVertexBuffer基本一样:

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);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);copyBuffer(stagingBuffer, indexBuffer, bufferSize);vkDestroyBuffer(device, stagingBuffer, nullptr);vkFreeMemory(device, stagingBufferMemory, nullptr);
}

        该方法在initVulkan的createVertexBuffer后调用。

        只有两处不同。一处是bufferSize现在等于索引个数乘以索引类型大小,大小就是uint16_t或者uint32_t。indexBuffer用法应该是VK_BUFFER_USAGE_INDEX_BUFFER_BIT而不是VK_BUFFER_USAGE_VERTEX_BUFFER_BIT了。除此之外,处理都是一样的。我们创建一个临时缓冲以便向其拷贝索引内容,然后将它拷贝到最终设备本地索引缓冲中。

        索引缓冲应该在程序结尾清理掉,就和顶点缓冲一样:

cleanupSwapChain();vkDestroyBuffer(device, indexBuffer, nullptr);
vkFreeMemory(device, indexBufferMemory, nullptr);vkDestroyBuffer(device, vertexBuffer, nullptr);
vkFreeMemory(device, vertexBufferMemory, nullptr);

        绘制的时候使用索引缓冲涉及到对createCommandBuffers的两处修改。我们首先需要绑定索引缓冲,就和我们之前对顶点缓冲做的工作一样。不同之处是你只能有一个索引缓冲。很不幸,不能为每个顶点属性使用不同索引,所以我们还是完全复制顶点数据,即使它就有一个属性不同。

vkCmdBindVertexBuffers(commandBuffers[i], 0, 1, vertexBuffers, offsets);
vkCmdBindIndexBuffer(commandBuffers[i], indexBuffer, 0, VK_INDEX_TYPE_UINT16);

        索引缓冲用vkCmdBindIndexBuffer绑定,参数有索引缓冲,字节偏移量,索引数据类型。

        只是绑定索引缓冲并不会改变什么,我们还要修改绘制命令,告诉Vulkan使用索引缓冲。删除vkCmdDraw,替换为vkCmdDrawIndexed:

vkCmdDrawIndexed(commandBuffers[i], static_cast<uint32_t>(indices.size()), 1, 0, 0, 0);

        该方法的调用和vkCmdDraw类似。头两个参数指定了索引个数和实例个数。我们不用实例,所以就是1。索引个数表示将要传递到顶点缓冲上的顶点的个数。下一个参数指定索引缓冲偏置,使用1会导致显卡开始从第二个索引读取。倒数第二个参数指定了在索引缓冲中添加索引的时候的偏移量。最后的参数指定了实例的偏置,这里我们不用。

        现在运行程序看到如下的矩形:

        你现在知道如何通过顶点缓冲重用顶点来节省内存了,这在将来加载复杂3D模型的时候尤其重要。

        之前的章节已经提到,你应该分配多个资源,如同来自单个内存分配的缓冲那样,但实际上还要多进一步。驱动开发者建议你也要存储多个缓冲到单个VkBuffer并在类似vkCmdBindVertexBuffers的命令中使用偏置,就和顶点和索引缓冲一样。其优势是你的数据会更方便缓存,因为它们更接近在一起。甚至可以对多个资源重用相同块的内存,如果它们不是相同的渲染操作中使用,当然也要保证它们的数据是刷新过的。这就是混叠,一些Vulkan方法有明确的标记来让你指定想要这么做。

这篇关于Vulkan教程 - 15 索引缓冲的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1094137

相关文章

MySQL 强制使用特定索引的操作

《MySQL强制使用特定索引的操作》MySQL可通过FORCEINDEX、USEINDEX等语法强制查询使用特定索引,但优化器可能不采纳,需结合EXPLAIN分析执行计划,避免性能下降,注意版本差异... 目录1. 使用FORCE INDEX语法2. 使用USE INDEX语法3. 使用IGNORE IND

2025版mysql8.0.41 winx64 手动安装详细教程

《2025版mysql8.0.41winx64手动安装详细教程》本文指导Windows系统下MySQL安装配置,包含解压、设置环境变量、my.ini配置、初始化密码获取、服务安装与手动启动等步骤,... 目录一、下载安装包二、配置环境变量三、安装配置四、启动 mysql 服务,修改密码一、下载安装包安装地

电脑提示d3dx11_43.dll缺失怎么办? DLL文件丢失的多种修复教程

《电脑提示d3dx11_43.dll缺失怎么办?DLL文件丢失的多种修复教程》在使用电脑玩游戏或运行某些图形处理软件时,有时会遇到系统提示“d3dx11_43.dll缺失”的错误,下面我们就来分享超... 在计算机使用过程中,我们可能会遇到一些错误提示,其中之一就是缺失某个dll文件。其中,d3dx11_4

Linux下在线安装启动VNC教程

《Linux下在线安装启动VNC教程》本文指导在CentOS7上在线安装VNC,包含安装、配置密码、启动/停止、清理重启步骤及注意事项,强调需安装VNC桌面以避免黑屏,并解决端口冲突和目录权限问题... 目录描述安装VNC安装 VNC 桌面可能遇到的问题总结描js述linux中的VNC就类似于Window

Go语言编译环境设置教程

《Go语言编译环境设置教程》Go语言支持高并发(goroutine)、自动垃圾回收,编译为跨平台二进制文件,云原生兼容且社区活跃,开发便捷,内置测试与vet工具辅助检测错误,依赖模块化管理,提升开发效... 目录Go语言优势下载 Go  配置编译环境配置 GOPROXYIDE 设置(VS Code)一些基本

MySQL逻辑删除与唯一索引冲突解决方案

《MySQL逻辑删除与唯一索引冲突解决方案》本文探讨MySQL逻辑删除与唯一索引冲突问题,提出四种解决方案:复合索引+时间戳、修改唯一字段、历史表、业务层校验,推荐方案1和方案3,适用于不同场景,感兴... 目录问题背景问题复现解决方案解决方案1.复合唯一索引 + 时间戳删除字段解决方案2:删除后修改唯一字

Windows环境下解决Matplotlib中文字体显示问题的详细教程

《Windows环境下解决Matplotlib中文字体显示问题的详细教程》本文详细介绍了在Windows下解决Matplotlib中文显示问题的方法,包括安装字体、更新缓存、配置文件设置及编码調整,并... 目录引言问题分析解决方案详解1. 检查系统已安装字体2. 手动添加中文字体(以SimHei为例)步骤

Java JDK1.8 安装和环境配置教程详解

《JavaJDK1.8安装和环境配置教程详解》文章简要介绍了JDK1.8的安装流程,包括官网下载对应系统版本、安装时选择非系统盘路径、配置JAVA_HOME、CLASSPATH和Path环境变量,... 目录1.下载JDK2.安装JDK3.配置环境变量4.检验JDK官网下载地址:Java Downloads

浅谈mysql的not exists走不走索引

《浅谈mysql的notexists走不走索引》在MySQL中,​NOTEXISTS子句是否使用索引取决于子查询中关联字段是否建立了合适的索引,下面就来介绍一下mysql的notexists走不走索... 在mysql中,​NOT EXISTS子句是否使用索引取决于子查询中关联字段是否建立了合适的索引。以下

PowerShell中15个提升运维效率关键命令实战指南

《PowerShell中15个提升运维效率关键命令实战指南》作为网络安全专业人员的必备技能,PowerShell在系统管理、日志分析、威胁检测和自动化响应方面展现出强大能力,下面我们就来看看15个提升... 目录一、PowerShell在网络安全中的战略价值二、网络安全关键场景命令实战1. 系统安全基线核查