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

相关文章

golang内存对齐的项目实践

《golang内存对齐的项目实践》本文主要介绍了golang内存对齐的项目实践,内存对齐不仅有助于提高内存访问效率,还确保了与硬件接口的兼容性,是Go语言编程中不可忽视的重要优化手段,下面就来介绍一下... 目录一、结构体中的字段顺序与内存对齐二、内存对齐的原理与规则三、调整结构体字段顺序优化内存对齐四、内

Ubuntu固定虚拟机ip地址的方法教程

《Ubuntu固定虚拟机ip地址的方法教程》本文详细介绍了如何在Ubuntu虚拟机中固定IP地址,包括检查和编辑`/etc/apt/sources.list`文件、更新网络配置文件以及使用Networ... 1、由于虚拟机网络是桥接,所以ip地址会不停地变化,接下来我们就讲述ip如何固定 2、如果apt安

PyCharm 接入 DeepSeek最新完整教程

《PyCharm接入DeepSeek最新完整教程》文章介绍了DeepSeek-V3模型的性能提升以及如何在PyCharm中接入和使用DeepSeek进行代码开发,本文通过图文并茂的形式给大家介绍的... 目录DeepSeek-V3效果演示创建API Key在PyCharm中下载Continue插件配置Con

Deepseek R1模型本地化部署+API接口调用详细教程(释放AI生产力)

《DeepseekR1模型本地化部署+API接口调用详细教程(释放AI生产力)》本文介绍了本地部署DeepSeekR1模型和通过API调用将其集成到VSCode中的过程,作者详细步骤展示了如何下载和... 目录前言一、deepseek R1模型与chatGPT o1系列模型对比二、本地部署步骤1.安装oll

在不同系统间迁移Python程序的方法与教程

《在不同系统间迁移Python程序的方法与教程》本文介绍了几种将Windows上编写的Python程序迁移到Linux服务器上的方法,包括使用虚拟环境和依赖冻结、容器化技术(如Docker)、使用An... 目录使用虚拟环境和依赖冻结1. 创建虚拟环境2. 冻结依赖使用容器化技术(如 docker)1. 创

Spring Boot整合log4j2日志配置的详细教程

《SpringBoot整合log4j2日志配置的详细教程》:本文主要介绍SpringBoot项目中整合Log4j2日志框架的步骤和配置,包括常用日志框架的比较、配置参数介绍、Log4j2配置详解... 目录前言一、常用日志框架二、配置参数介绍1. 日志级别2. 输出形式3. 日志格式3.1 PatternL

MySQL8.2.0安装教程分享

《MySQL8.2.0安装教程分享》这篇文章详细介绍了如何在Windows系统上安装MySQL数据库软件,包括下载、安装、配置和设置环境变量的步骤... 目录mysql的安装图文1.python访问网址2javascript.点击3.进入Downloads向下滑动4.选择Community Server5.

CentOS系统Maven安装教程分享

《CentOS系统Maven安装教程分享》本文介绍了如何在CentOS系统中安装Maven,并提供了一个简单的实际应用案例,安装Maven需要先安装Java和设置环境变量,Maven可以自动管理项目的... 目录准备工作下载并安装Maven常见问题及解决方法实际应用案例总结Maven是一个流行的项目管理工具

本地私有化部署DeepSeek模型的详细教程

《本地私有化部署DeepSeek模型的详细教程》DeepSeek模型是一种强大的语言模型,本地私有化部署可以让用户在自己的环境中安全、高效地使用该模型,避免数据传输到外部带来的安全风险,同时也能根据自... 目录一、引言二、环境准备(一)硬件要求(二)软件要求(三)创建虚拟环境三、安装依赖库四、获取 Dee

MySql9.1.0安装详细教程(最新推荐)

《MySql9.1.0安装详细教程(最新推荐)》MySQL是一个流行的关系型数据库管理系统,支持多线程和多种数据库连接途径,能够处理上千万条记录的大型数据库,本文介绍MySql9.1.0安装详细教程,... 目录mysql介绍:一、下载 Mysql 安装文件二、Mysql 安装教程三、环境配置1.右击此电脑