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

相关文章

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

NameNode内存生产配置

Hadoop2.x 系列,配置 NameNode 内存 NameNode 内存默认 2000m ,如果服务器内存 4G , NameNode 内存可以配置 3g 。在 hadoop-env.sh 文件中配置如下。 HADOOP_NAMENODE_OPTS=-Xmx3072m Hadoop3.x 系列,配置 Nam

Makefile简明使用教程

文章目录 规则makefile文件的基本语法:加在命令前的特殊符号:.PHONY伪目标: Makefilev1 直观写法v2 加上中间过程v3 伪目标v4 变量 make 选项-f-n-C Make 是一种流行的构建工具,常用于将源代码转换成可执行文件或者其他形式的输出文件(如库文件、文档等)。Make 可以自动化地执行编译、链接等一系列操作。 规则 makefile文件

SWAP作物生长模型安装教程、数据制备、敏感性分析、气候变化影响、R模型敏感性分析与贝叶斯优化、Fortran源代码分析、气候数据降尺度与变化影响分析

查看原文>>>全流程SWAP农业模型数据制备、敏感性分析及气候变化影响实践技术应用 SWAP模型是由荷兰瓦赫宁根大学开发的先进农作物模型,它综合考虑了土壤-水分-大气以及植被间的相互作用;是一种描述作物生长过程的一种机理性作物生长模型。它不但运用Richard方程,使其能够精确的模拟土壤中水分的运动,而且耦合了WOFOST作物模型使作物的生长描述更为科学。 本文让更多的科研人员和农业工作者

沁恒CH32在MounRiver Studio上环境配置以及使用详细教程

目录 1.  RISC-V简介 2.  CPU架构现状 3.  MounRiver Studio软件下载 4.  MounRiver Studio软件安装 5.  MounRiver Studio软件介绍 6.  创建工程 7.  编译代码 1.  RISC-V简介         RISC就是精简指令集计算机(Reduced Instruction SetCom

前端技术(七)——less 教程

一、less简介 1. less是什么? less是一种动态样式语言,属于css预处理器的范畴,它扩展了CSS语言,增加了变量、Mixin、函数等特性,使CSS 更易维护和扩展LESS 既可以在 客户端 上运行 ,也可以借助Node.js在服务端运行。 less的中文官网:https://lesscss.cn/ 2. less编译工具 koala 官网 http://koala-app.

【Shiro】Shiro 的学习教程(三)之 SpringBoot 集成 Shiro

目录 1、环境准备2、引入 Shiro3、实现认证、退出3.1、使用死数据实现3.2、引入数据库,添加注册功能后端代码前端代码 3.3、MD5、Salt 的认证流程 4.、实现授权4.1、基于角色授权4.2、基于资源授权 5、引入缓存5.1、EhCache 实现缓存5.2、集成 Redis 实现 Shiro 缓存 1、环境准备 新建一个 SpringBoot 工程,引入依赖:

OpenCV结构分析与形状描述符(11)椭圆拟合函数fitEllipse()的使用

操作系统:ubuntu22.04 OpenCV版本:OpenCV4.9 IDE:Visual Studio Code 编程语言:C++11 算法描述 围绕一组2D点拟合一个椭圆。 该函数计算出一个椭圆,该椭圆在最小二乘意义上最好地拟合一组2D点。它返回一个内切椭圆的旋转矩形。使用了由[90]描述的第一个算法。开发者应该注意,由于数据点靠近包含的 Mat 元素的边界,返回的椭圆/旋转矩形数据

JVM内存调优原则及几种JVM内存调优方法

JVM内存调优原则及几种JVM内存调优方法 1、堆大小设置。 2、回收器选择。   1、在对JVM内存调优的时候不能只看操作系统级别Java进程所占用的内存,这个数值不能准确的反应堆内存的真实占用情况,因为GC过后这个值是不会变化的,因此内存调优的时候要更多地使用JDK提供的内存查看工具,比如JConsole和Java VisualVM。   2、对JVM内存的系统级的调优主要的目的是减少

JVM 常见异常及内存诊断

栈内存溢出 栈内存大小设置:-Xss size 默认除了window以外的所有操作系统默认情况大小为 1MB,window 的默认大小依赖于虚拟机内存。 栈帧过多导致栈内存溢出 下述示例代码,由于递归深度没有限制且没有设置出口,每次方法的调用都会产生一个栈帧导致了创建的栈帧过多,而导致内存溢出(StackOverflowError)。 示例代码: 运行结果: 栈帧过大导致栈内存