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

相关文章

全网最全Tomcat完全卸载重装教程小结

《全网最全Tomcat完全卸载重装教程小结》windows系统卸载Tomcat重新通过ZIP方式安装Tomcat,优点是灵活可控,适合开发者自定义配置,手动配置环境变量后,可通过命令行快速启动和管理... 目录一、完全卸载Tomcat1. 停止Tomcat服务2. 通过控制面板卸载3. 手动删除残留文件4.

Python的pandas库基础知识超详细教程

《Python的pandas库基础知识超详细教程》Pandas是Python数据处理核心库,提供Series和DataFrame结构,支持CSV/Excel/SQL等数据源导入及清洗、合并、统计等功能... 目录一、配置环境二、序列和数据表2.1 初始化2.2  获取数值2.3 获取索引2.4 索引取内容2

Python内存管理机制之垃圾回收与引用计数操作全过程

《Python内存管理机制之垃圾回收与引用计数操作全过程》SQLAlchemy是Python中最流行的ORM(对象关系映射)框架之一,它提供了高效且灵活的数据库操作方式,本文将介绍如何使用SQLAlc... 目录安装核心概念连接数据库定义数据模型创建数据库表基本CRUD操作创建数据读取数据更新数据删除数据查

python依赖管理工具UV的安装和使用教程

《python依赖管理工具UV的安装和使用教程》UV是一个用Rust编写的Python包安装和依赖管理工具,比传统工具(如pip)有着更快、更高效的体验,:本文主要介绍python依赖管理工具UV... 目录前言一、命令安装uv二、手动编译安装2.1在archlinux安装uv的依赖工具2.2从github

C#实现SHP文件读取与地图显示的完整教程

《C#实现SHP文件读取与地图显示的完整教程》在地理信息系统(GIS)开发中,SHP文件是一种常见的矢量数据格式,本文将详细介绍如何使用C#读取SHP文件并实现地图显示功能,包括坐标转换、图形渲染、平... 目录概述功能特点核心代码解析1. 文件读取与初始化2. 坐标转换3. 图形绘制4. 地图交互功能缩放

k8s容器放开锁内存限制问题

《k8s容器放开锁内存限制问题》nccl-test容器运行mpirun时因NCCL_BUFFSIZE过大导致OOM,需通过修改docker服务配置文件,将LimitMEMLOCK设为infinity并... 目录问题问题确认放开容器max locked memory限制总结参考:https://Access

SpringBoot集成redisson实现延时队列教程

《SpringBoot集成redisson实现延时队列教程》文章介绍了使用Redisson实现延迟队列的完整步骤,包括依赖导入、Redis配置、工具类封装、业务枚举定义、执行器实现、Bean创建、消费... 目录1、先给项目导入Redisson依赖2、配置redis3、创建 RedissonConfig 配

Redis实现高效内存管理的示例代码

《Redis实现高效内存管理的示例代码》Redis内存管理是其核心功能之一,为了高效地利用内存,Redis采用了多种技术和策略,如优化的数据结构、内存分配策略、内存回收、数据压缩等,下面就来详细的介绍... 目录1. 内存分配策略jemalloc 的使用2. 数据压缩和编码ziplist示例代码3. 优化的

基于C#实现PDF转图片的详细教程

《基于C#实现PDF转图片的详细教程》在数字化办公场景中,PDF文件的可视化处理需求日益增长,本文将围绕Spire.PDFfor.NET这一工具,详解如何通过C#将PDF转换为JPG、PNG等主流图片... 目录引言一、组件部署二、快速入门:PDF 转图片的核心 C# 代码三、分辨率设置 - 清晰度的决定因

深入解析C++ 中std::map内存管理

《深入解析C++中std::map内存管理》文章详解C++std::map内存管理,指出clear()仅删除元素可能不释放底层内存,建议用swap()与空map交换以彻底释放,针对指针类型需手动de... 目录1️、基本清空std::map2️、使用 swap 彻底释放内存3️、map 中存储指针类型的对象