Link

统一缓冲区非常适合小型、只读数据。但是,如果您想要着色器中大小未知的数据呢?或者可以写入的数据呢?您可以使用存储缓冲区来实现。存储缓冲区通常比统一缓冲区稍慢,但它们可以大得多得多。如果您想将整个场景塞进一个缓冲区中,则必须使用它们。请务必进行性能分析以了解性能。

使用存储缓冲区,您可以在着色器中使用任何您想要的数据的无大小数组。它们的一个常见用途是存储场景中所有对象的数据。

我们将使用它们来移除对对象矩阵使用推送常量的做法,这将使我们能够在帧开始时批量上传矩阵,然后我们不再需要每次绘制都进行单独的推送常量调用。这也意味着我们将把所有对象矩阵保存在一个数组中,该数组可用于计算着色器中的有趣事物。

创建着色器存储缓冲区

我们将继续在 init_descriptors() 函数中进行操作,因为它是在其中初始化着色器参数的所有缓冲区的地方。在那里,我们将为每帧初始化一个大的存储缓冲区,以保存对象的数据。这是因为我们希望对象仍然是动态的。如果我们有完全静态的对象,我们就不需要每帧一个缓冲区,一个总的缓冲区就足够了。

struct FrameData {
	AllocatedBuffer objectBuffer;
};

struct GPUObjectData{
	glm::mat4 modelMatrix;
};
void VulkanEngine::init_descriptors()
{
	// other code ...
	for (int i = 0; i < FRAME_OVERLAP; i++)
	{
		const int MAX_OBJECTS = 10000;
		_frames[i].objectBuffer = create_buffer(sizeof(GPUObjectData) * MAX_OBJECTS, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, VMA_MEMORY_USAGE_CPU_TO_GPU);

		//other code ....
	}

	// add storage buffers to deletion queues
	for (int i = 0; i < FRAME_OVERLAP; i++)
	{
		vmaDestroyBuffer(_allocator, _frames[i].objectBuffer._buffer, _frames[i].objectBuffer._allocation);

		//other code ....
	}
}

着色器存储缓冲区的创建方式与统一缓冲区相同。它们的工作方式也基本相同,只是具有不同的属性,例如增加的最大大小,以及在着色器中可写。我们将为每帧保留一个包含 10000 个 ObjectData 的数组。这意味着我们可以容纳多达 10000 个对象矩阵,每帧渲染 10000 个对象。这个数字很小,但目前这不是问题。Unreal Engine 在引擎加载更多对象时会根据需要增长其对象缓冲区,但我们没有任何可增长的缓冲区抽象,因此我们预先保留。虽然这里的大小是 10000,但您可以将其增加到您想要的任何大小。存储缓冲区的最大大小非常大,在大多数 GPU 中,它们可以与 VRAM 可以容纳的大小一样大,因此如果您愿意,可以使用 1 亿个矩阵来执行此操作。

我们现在需要将其添加到描述符集中。我们一直将所有内容添加到描述符集编号 0 中,但对于此操作,我们将使用描述符集编号 1。这意味着我们需要另一个描述符集布局,并将其挂钩到管线创建中。

新的描述符集

struct FrameData {
	AllocatedBuffer objectBuffer;
	VkDescriptorSet objectDescriptor;
};
class VulkanEngine {
	VkDescriptorSetLayout _globalSetLayout;
	VkDescriptorSetLayout _objectSetLayout;
}

我们将采用与相机缓冲区类似的方法,即我们将有一个描述符指向一个缓冲区。因为这是一个新的描述符集,所以我们还需要存储其布局以挂钩到管线。

回到 init_descriptors(),我们需要在描述符池上为其保留空间。

	std::vector<VkDescriptorPoolSize> sizes =
	{
		{ VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 10 },
		{ VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 10 },
		{ VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 10 }
	};

在下面一点,我们将初始化集布局,它将仅具有 1 个绑定用于大缓冲区。

	VkDescriptorSetLayoutBinding objectBind = vkinit::descriptorset_layout_binding(VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, VK_SHADER_STAGE_VERTEX_BIT, 0);

	VkDescriptorSetLayoutCreateInfo set2info = {};
	set2info.bindingCount = 1;
	set2info.flags = 0;
	set2info.pNext = nullptr;
	set2info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
	set2info.pBindings = &objectBind;

	vkCreateDescriptorSetLayout(_device, &set2info, nullptr, &_objectSetLayout);

与另一个集相同,我们为新集创建一个布局,该布局将指向 1 个存储缓冲区。

我们不应忘记将此新布局添加到删除队列中

	_mainDeletionQueue.push_function([&]() {
		// other code ....
		vkDestroyDescriptorSetLayout(_device, _objectSetLayout, nullptr);
	}

现在我们需要创建描述符集以指向缓冲区。

for (int i = 0; i < FRAME_OVERLAP; i++)
{
		//allocation for the other descriptor and other code....

		//allocate the descriptor set that will point to object buffer
		VkDescriptorSetAllocateInfo objectSetAlloc = {};
		objectSetAlloc.pNext = nullptr;
		objectSetAlloc.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
		objectSetAlloc.descriptorPool = _descriptorPool;
		objectSetAlloc.descriptorSetCount = 1;
		objectSetAlloc.pSetLayouts = &_objectSetLayout;

		vkAllocateDescriptorSets(_device, &objectSetAlloc, &_frames[i].objectDescriptor);


		VkDescriptorBufferInfo cameraInfo;
		cameraInfo.buffer = _frames[i].cameraBuffer._buffer;
		cameraInfo.offset = 0;
		cameraInfo.range = sizeof(GPUCameraData);

		VkDescriptorBufferInfo sceneInfo;
		sceneInfo.buffer = _sceneParameterBuffer._buffer;
		sceneInfo.offset = 0;
		sceneInfo.range = sizeof(GPUSceneData);

		VkDescriptorBufferInfo objectBufferInfo;
		objectBufferInfo.buffer = _frames[i].objectBuffer._buffer;
		objectBufferInfo.offset = 0;
		objectBufferInfo.range = sizeof(GPUObjectData) * MAX_OBJECTS;


		VkWriteDescriptorSet cameraWrite = vkinit::write_descriptor_buffer(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, _frames[i].globalDescriptor,&cameraInfo,0);

		VkWriteDescriptorSet sceneWrite = vkinit::write_descriptor_buffer(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, _frames[i].globalDescriptor, &sceneInfo, 1);

		VkWriteDescriptorSet objectWrite = vkinit::write_descriptor_buffer(VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, _frames[i].objectDescriptor, &objectBufferInfo, 0);

		VkWriteDescriptorSet setWrites[] = { cameraWrite,sceneWrite,objectWrite };

		vkUpdateDescriptorSets(_device, 3, setWrites, 0, nullptr);
}

我们需要另一个 DescriptorBufferInfo 和另一个 WriteDescriptorSet。请注意,在这里,我们正在使用 1 个 vkUpdateDescriptorSets() 调用来更新 2 个不同的描述符集。这样做是完全有效的。现在缓冲区已初始化并且具有指向它的描述符,我们需要将其添加到着色器中。

我们将修改 tri_mesh.vert 着色器,以从 SSBO 而不是从推送常量读取对象数据。我们将仍然保留推送常量,但它不会被使用。

#version 460
layout (location = 0) in vec3 vPosition;
layout (location = 1) in vec3 vNormal;
layout (location = 2) in vec3 vColor;

layout (location = 0) out vec3 outColor;

layout(set = 0, binding = 0) uniform  CameraBuffer{
	mat4 view;
	mat4 proj;
	mat4 viewproj;
} cameraData;

struct ObjectData{
	mat4 model;
};

//all object matrices
layout(std140,set = 1, binding = 0) readonly buffer ObjectBuffer{

	ObjectData objects[];
} objectBuffer;

//push constants block
layout( push_constant ) uniform constants
{
 vec4 data;
 mat4 render_matrix;
} PushConstants;

void main()
{
	mat4 modelMatrix = objectBuffer.objects[gl_BaseInstance].model;
	mat4 transformMatrix = (cameraData.viewproj * modelMatrix);
	gl_Position = transformMatrix * vec4(vPosition, 1.0f);
	outColor = vColor;
}

我们将 GLSL 版本更改为 460,因为我们希望能够使用 gl_BaseInstance 来索引到变换数组中。

并在 init_vulkan() 中启用 shader draw parameters 功能

void VulkanEngine::init_vulkan(){
	// initialize vulkan instance and surface ...
	
    	// create the final vulkan device
    	vkb::DeviceBuilder deviceBuilder{physicalDevice};
    	VkPhysicalDeviceShaderDrawParametersFeatures shader_draw_parameters_features = {};
    	shader_draw_parameters_features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_DRAW_PARAMETERS_FEATURES;
    	shader_draw_parameters_features.pNext = nullptr;
    	shader_draw_parameters_features.shaderDrawParameters = VK_TRUE;
    	vkb::Device vkbDevice = deviceBuilder.add_pNext(&shader_draw_parameters_features).build().value();

	// other code ...
}

请注意我们声明 ObjectBuffer 的方式

layout(std140,set = 1, binding = 0) readonly buffer ObjectBuffer{

	ObjectData objects[];
} objectBuffer;

我们需要 std140 布局描述,以使数组与 cpp 中数组的工作方式相匹配。该 std140 强制执行有关内存如何布局及其对齐方式的一些规则。该集现在是 1,绑定是 0,表示它是一个新的描述符集槽。

在声明它时,我们还使用 readonly buffer 而不是 uniform。着色器存储缓冲区可以读取或写入,因此我们需要让 Vulkan 知道。它们也使用 buffer 而不是 uniform 定义。

内部的数组也是无大小的。您只能在存储缓冲区中使用无大小的数组。这将使着色器可以缩放到我们拥有的任何缓冲区大小。

另一件事是我们访问正确对象矩阵的方式。我们不再使用推送常量,而是这样做

mat4 modelMatrix = objectBuffer.objects[gl_BaseInstance].model;

我们正在使用 gl_BaseInstance 来访问对象缓冲区。这是因为 Vulkan 在其正常绘制调用中的工作方式。Vulkan 中的所有绘制命令都请求“第一个实例”和“实例计数”。我们没有进行实例化渲染,因此实例计数始终为 1。但是我们仍然可以更改“第一个实例”参数,并通过这种方式获得 gl_BaseInstance,作为一个我们可以用于着色器中任何用途的整数。这为我们提供了一种简单的方法,无需设置推送常量或描述符即可向着色器发送单个整数。

我们现在需要将描述符布局挂钩到管线。

init_pipelines() 上,我们在创建管线布局时将其添加到描述符列表中

VkDescriptorSetLayout setLayouts[] = { _globalSetLayout, _objectSetLayout };

mesh_pipeline_layout_info.setLayoutCount = 2;
mesh_pipeline_layout_info.pSetLayouts = setLayouts;

我们现在已经设置好了管线,所以最后一件事是写入缓冲区。

写入着色器存储缓冲区

draw_objects() 上,我们将通过将渲染对象的渲染矩阵复制到缓冲区中来写入缓冲区。这在渲染循环之前进行,与其他的内存写入操作一起。

void* objectData;
vmaMapMemory(_allocator, get_current_frame().objectBuffer._allocation, &objectData);

GPUObjectData* objectSSBO = (GPUObjectData*)objectData;

for (int i = 0; i < count; i++)
{
	RenderObject& object = first[i];
	objectSSBO[i].modelMatrix = object.transformMatrix;
}

vmaUnmapMemory(_allocator, get_current_frame().objectBuffer._allocation);

我们没有在这里使用 memcpy,而是使用了不同的技巧。可以从映射缓冲区到另一种类型的 void* 进行类型转换,并正常写入。这将完全正常工作,并且可以更轻松地将复杂类型写入缓冲区。

缓冲区现在已填充,因此我们现在需要在绘制命令中绑定描述符集并使用 firstIndex 参数来访问着色器中的对象数据。

if (object.material != lastMaterial) {

	vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, object.material->pipeline);
	lastMaterial = object.material;

			//camera data descriptor
	uint32_t uniform_offset = pad_uniform_buffer_size(sizeof(GPUSceneData)) * frameIndex;
	vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, object.material->pipelineLayout, 0, 1, &get_current_frame().globalDescriptor, 1, &uniform_offset);

	//object data descriptor
	vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, object.material->pipelineLayout, 1, 1, &get_current_frame().objectDescriptor, 0, nullptr);
}

//more code ....

//we can now draw
vkCmdDraw(cmd, object.mesh->_vertices.size(), 1,0 , i);

我们正在 vkCmdDraw() 调用中使用循环中的索引来将实例索引发送到着色器。

现在我们已经实现了多种不同类型的缓冲区,以及在不同描述符集上的缓冲区。

本教程的最后一步是纹理,这将在下一章中介绍。但在进入那里之前,我强烈建议您尝试使用代码库做一些事情。

现在,我们每帧有一个描述符集用于 Set 0(相机和场景缓冲区)。尝试重构它,使其仅使用 1 个描述符集和 1 个缓冲区用于相机和场景缓冲区,将所有帧的结构体都打包到同一个统一缓冲区中,然后使用动态偏移。

或者,尝试创建另一个 SSBO,其中包含类似 ObjectColor 的内容,以在每个对象的基础上使用,并尝试使用它通过修改着色器以不同的方式为对象着色。

下一步:内存传输