Vulkan教程 - 17 描述符与内存对齐

2024-08-21 19:58

本文主要是介绍Vulkan教程 - 17 描述符与内存对齐,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

之前章节的描述符布局描述了描述符可以绑定的类型。本章我们要对每个VkBuffer资源创建一个描述符集合来将它绑定到统一缓冲描述符上。

描述符集合不能够直接创建,必须从一个池中分配,就和命令缓冲一样。同样的,对应也有描述符池。写一个新方法createDescriptorPool来建立它,把它放在初始化Vulkan的创建统一缓冲之后:

createUniformBuffers();
createDescriptorPool();

我们需要描述我们的描述符集合打算包含哪种描述符:

VkDescriptorPoolSize poolSize = {};
poolSize.type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
poolSize.descriptorCount = static_cast<uint32_t>(swapChainImages.size());

我们会为每一帧从这些描述符中分配一个,该池大小会被主VkDescriptorPoolCreateInfo引用:

VkDescriptorPoolCreateInfo poolInfo = {};
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
poolInfo.poolSizeCount = 1;
poolInfo.pPoolSizes = &poolSize;

除了可以获得各个描述符的最大值之外,我们还要指定可以分配的描述符集合的最大值:

poolInfo.maxSets = static_cast<uint32_t>(swapChainImages.size());

该结构体有一个可选标记VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT,和命令池类似,该标记确定了各个描述符集合是否可以被释放。我们不会在创建后再去接触描述符集合,所以我们不用该标记。

添加一个新的类成员来存储描述符池的句柄,调用vkCreateDescriptorPool来创建它。

VkDescriptorPool descriptorPool;
...
if (vkCreateDescriptorPool(device, &poolInfo, nullptr, &descriptorPool) != VK_SUCCESS) {throw std::runtime_error("failed to create descriptor pool!");
}

描述符池应该在交换链重建的时候销毁因为它依赖图像个数:

for (size_t i = 0; i < swapChainImages.size(); i++) {vkDestroyBuffer(device, uniformBuffers[i], nullptr);vkFreeMemory(device, uniformBuffersMemory[i], nullptr);
}vkDestroyDescriptorPool(device, descriptorPool, nullptr);

然后重建交换链的时候进行重建:

createUniformBuffers();
createDescriptorPool();
createCommandBuffers();

现在我们可以分配描述符集合了。添加一个方法createDescriptorSets:

createDescriptorPool();
createDescriptorSets();
createCommandBuffers();

在初始化Vulkan的部分调用如上面所示。重建交换链的时候也要调用,如下:

createDescriptorPool();
createDescriptorSets();
createCommandBuffers();

描述符集合分配通过VkDescriptorSetAllocateInfo结构体描述。你需要指定要分配的描述符池,描述符集合要分配的个数,以及描述符布局:

std::vector<VkDescriptorSetLayout> layouts(swapChainImages.size(), descriptorSetLayout);
VkDescriptorSetAllocateInfo allocInfo = {};
allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
allocInfo.descriptorPool = descriptorPool;
allocInfo.descriptorSetCount = static_cast<uint32_t>(swapChainImages.size());
allocInfo.pSetLayouts = layouts.data();

我们这里会为每个交换链图像创建一个描述符,都使用一样的布局。不幸的是,我们需要所有布局的副本,因为下面一个方法会需要一个数组匹配集合个数。

添加一个类成员来保存描述符集合句柄并用vkAllocateDescriptorSets分配:

VkDescriptorPool descriptorPool;
std::vector<VkDescriptorSet> descriptorSets;
...
descriptorSets.resize(swapChainImages.size());
if (vkAllocateDescriptorSets(device, &allocInfo, descriptorSets.data()) != VK_SUCCESS) {throw std::runtime_error("failed to allocate descriptor sets!");
}

你不需要显式清理描述符集合,因为它们会在描述符池销毁的时候自动释放。vkAllocateDescriptorSets调用会分配描述符集合,每个有一个统一缓冲描述符。

描述符集合已经分配了,但是其中的描述符还需要配置。我们现在需要添加一个循环来产生每个描述符:

for (size_t i = 0; i < swapChainImages.size(); i++) {VkDescriptorBufferInfo bufferInfo = {};bufferInfo.buffer = uniformBuffers[i];bufferInfo.offset = 0;bufferInfo.range = sizeof(UniformBufferObject);
}

引用该缓冲的描述符,和我们的统一缓冲描述符类似,是通过VkDescriptorBufferInfo配置的。该结构体指定了缓冲和它中间的包含描述符所需数据的区域。

如果你覆盖整个缓冲,就像我们这个情况一样,那么范围也可以使用VK_WHOLE_SIZE值。描述符配置使用vkUpdateDescriptorSets方法进行更新,它接收一组VkWriteDescriptorSet结构体作为参数。

VkWriteDescriptorSet descriptorWrite = {};
descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrite.dstSet = descriptorSets[i];
descriptorWrite.dstBinding = 0;
descriptorWrite.dstArrayElement = 0;

最开始两个字段指定要更新和绑定的描述符集合。我们设定统一缓冲绑定索引为0。记住描述符可以是数组,所以我们需要指定想要更新的数组的第一个索引。我们不用数组,所以就设置索引为0。

descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
descriptorWrite.descriptorCount = 1;

我们要再次指定描述符类型。可以在一个数组中一次更新多个描述符,就从索引dstArrayElement处开始。descriptorCount字段描述了想要更新多少数组元素。

descriptorWrite.pBufferInfo = &bufferInfo;
descriptorWrite.pImageInfo = nullptr;  // optional
descriptorWrite.pTexelBufferView = nullptr;  // optional

最后的字段用descriptorCount结构体引用一个数组,该数组才是实际配置描述符的。它依赖于描述符类型,也就是三种之中要用的一个。pBufferInfo字段用于引用缓冲数据的描述符,pImageInfo用于引用图像数据的描述符,pTexelBufferView用于引用缓冲视图的描述符。我们的描述符是基于缓冲的,所以我们用pBufferInfo。

vkUpdateDescriptorSets(device, 1, &descriptorWrite, 0, nullptr);

更新操作用vkUpdateDescriptorSets执行,它接收两种数组作为参数:一组VkWriteDescriptorSet和一组VkCopyDescriptorSet,后者可以用于将描述符进行互相拷贝。

我们现在要更新createCommandBuffers方法,用cmdBindDescriptorSets来为每个交换链图像真正绑定正确的描述符集合到着色器中的描述符。这需要在vkCmdDrawIndexed之前完成:

vkCmdBindDescriptorSets(commandBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS,pipelineLayout, 0, 1, &descriptorSets[i], 0, nullptr);

不像是顶点和索引缓冲,描述符集合对图像管线不是独一无二的。因此我们需要指定是否想要绑定描述符集合到图形或者计算管线。下一个参数就是描述符所基于的布局。接着的三个参数指定了第一个描述符集合索引,要绑定的集合个数以及要绑定的数组。我们之后回来看。最后一个参数指定了一个偏置数组,用于动态描述符。我们以后再看。

你现在运行程序会发现什么都不显示,因为我们在投影矩阵中对Y轴做了反转,现在顶点就是顺时针绘制,而不是逆时针。这就导致后面剔除以阻止几何体绘制。在createGraphicsPipeline方法中修改VkPipelineRasterizationStateCreateInfo结构体中的frontFace来修复该问题:

rasterizer.cullMode = VK_CULL_MODE_BACK_BIT;
rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;

现在运行程序你应该能看到:

矩形已经改变成了正方形,因为投影矩阵现在会纠正宽高比。updateUniformBuffer会处理屏幕大小改变问题,所以我们不用在重建交换链中重建描述符集合。

有一件事情我们一直掩饰到现在,就是C++结构体中的数据到底怎么和着色器中的统一定义相匹配的。看起来很显然,就是二者都用相同的类型:

struct UniformBufferObject {glm::mat4 model;glm::mat4 view;glm::mat4 proj;
};layout(binding = 0) uniform UniformBufferObject {mat4 model;mat4 view;mat4 proj;
} ubo;

但是,这还不是全部原因。例如,修改结构体和着色器代码如下:

struct UniformBufferObject {glm::vec2 foo;glm::mat4 model;glm::mat4 view;glm::mat4 proj;
};layout(binding = 0) uniform UniformBufferObject {vec2 foo;mat4 model;mat4 view;mat4 proj;
} ubo;

重新编译着色器,运行程序,发现好不容易做的彩色正方形消失了!因为我们没有考虑对齐要求。

Vulkan要求你结构体中的数据在内存中以一种特殊方式对齐,例如:

标量必须是N对齐的(如对32位浮点数来说就是4个字节);

vec2必须是2N对齐的(8个字节);

vec3和vec4必须4N对齐(16字节);

内嵌结构体必须由它的成员的基础对齐值来对齐,会多达16的倍数;

mat4矩阵必须要有和vec4一样的对齐值。

我们一开始的着色器有三个mat4字段,已经满足了对齐要求。每个mat4是4*4*4=64字节大小,模型偏置为0,视图偏置为64,投影偏置为128。这些都是16的倍数,所以都工作正常。

新结构体用vec2开始,只占用8字节,因此丢掉了所有偏置。现在模型有个偏置8,视图有个偏置72,投影有个偏置136,没一个是16的倍数的。解决这个问题可以用C++11中的alignas标识符:

struct UniformBufferObject {glm::vec2 foo;alignas(16) glm::mat4 model;glm::mat4 view;glm::mat4 proj;
};

现在运行程序就没问题了。VS中选择17标准,因为14标准会提示std中没有optional。

幸运的是,有一种方法能让你大多数情况下都不用考虑对齐要求。我们可以在包含GLM之前定义GLM_FORCE_DEFAULT_ALIGNED_GENTYPES,它会让GLM使用一种已经满足我们对齐要求的vec2和mat4版本。如果你添加该定义,那么你就可以移除alignas标识符了。

但是不幸的是,这种方法可能会失败,如果你用了嵌入结构体的话。考虑下面的C++代码:

struct Foo {glm::vec2 v;
};struct UniformBufferObject {Foo f1;Foo f2;
};

以及着色器定义:

struct Foo {vec2 v;
};layout(binding = 0) uniform UniformBufferObject {Foo f1;Foo f2;
} ubo;

这种情况下,f2将会有偏置8,但是它却应该有个偏置为16,因为它是嵌入结构体。这时你就必须自己指定对齐了:

struct UniformBufferObject {Foo f1;alignas(16) Foo f2;
};

这些需要注意的地方就是明确对齐的理由之一,这样你就不会被奇怪的对齐错误症状抓个正着:

struct UniformBufferObject {alignas(16) glm::mat4 model;alignas(16) glm::mat4 view;alignas(16) glm::mat4 proj;
};

去掉了foo字段后不要忘记重新编译着色器。

就和一些结构体和方法调用所示,可以同时绑定多个描述符集合。当创建管线布局的时候,你需要为每个描述符集合指定一个描述符布局。着色器就可以像这样来引用特定描述符集合了:

layout(set = 0, binding = 0) uniform UniformBufferObject { ... }

你可以使用该特性将每个对象上都有所变化的描述符,以及被共享的描述符,放到不同的描述符集合。这样你就能避免重新在多个绘制命令中绑定大多数描述符,从而提高了效率。

这篇关于Vulkan教程 - 17 描述符与内存对齐的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

Python pandas库自学超详细教程

《Pythonpandas库自学超详细教程》文章介绍了Pandas库的基本功能、安装方法及核心操作,涵盖数据导入(CSV/Excel等)、数据结构(Series、DataFrame)、数据清洗、转换... 目录一、什么是Pandas库(1)、Pandas 应用(2)、Pandas 功能(3)、数据结构二、安

MySQL 内存使用率常用分析语句

《MySQL内存使用率常用分析语句》用户整理了MySQL内存占用过高的分析方法,涵盖操作系统层确认及数据库层bufferpool、内存模块差值、线程状态、performance_schema性能数据... 目录一、 OS层二、 DB层1. 全局情况2. 内存占js用详情最近连续遇到mysql内存占用过高导致

最新Spring Security的基于内存用户认证方式

《最新SpringSecurity的基于内存用户认证方式》本文讲解SpringSecurity内存认证配置,适用于开发、测试等场景,通过代码创建用户及权限管理,支持密码加密,虽简单但不持久化,生产环... 目录1. 前言2. 因何选择内存认证?3. 基础配置实战❶ 创建Spring Security配置文件

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

java内存泄漏排查过程及解决

《java内存泄漏排查过程及解决》公司某服务内存持续增长,疑似内存泄漏,未触发OOM,排查方法包括检查JVM配置、分析GC执行状态、导出堆内存快照并用IDEAProfiler工具定位大对象及代码... 目录内存泄漏内存问题排查1.查看JVM内存配置2.分析gc是否正常执行3.导出 dump 各种工具分析4.

Go语言编译环境设置教程

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

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