Advertisement

Custom Vulkan Render Graph: Proper handling of descriptor sets / set layouts

Started by February 01, 2023 10:05 PM
4 comments, last by skatehumor 1 year, 9 months ago

I'm back with an oddly specific question.

I'm working on implementing a render graph abstraction, largely taking from the advice on the now legendary FrameGraph architecture in the Frostbite engine

https://www.gdcvault.com/play/1024612/FrameGraph-Extensible-Rendering-Architecture-in

I have most of a simple render graph abstraction implemented (e.g. virtualized passes and resources, pass culling, resource lifetime detection for transients, external resource registration, simple and clean API).

I'm currently on the compile / execute phase of the render graph and looking mostly at resource inputs. Is there a decent way to tie descriptor sets and descriptor resources into a render graph?

Would I have to recreate the descriptor sets per frame / every time the graph changes in order to catch changes in the number / order of bindings? Is a render graph only meant to handle less granular resources like depth/color outputs, full screen texture inputs, or global buffers? Maybe I'm thinking about this wrong but looking for some footing on what the best way to tie descriptor sets into a render graph would be.

I guess as an example, if I have a mesh pipeline object for rendering a bunch of meshes, where there is a to-be-bound descriptor set with 4 to-be-bound textures (e.g. albedo, roughness, metal, emissive, etc.), is this something that's best handled outside of a render graph pass description? or is it something I can hook into a render graph in some decent way?

And if a render graph should care about descriptor sets in any such way, is it better to pass in descriptor sets directly into a render pass description as some sort of input, or have descriptor sets dynamically allocate based on render pass inputs? By “better” I mean, is there any sort of industry standard for this?

Much thanks!

skatehumor said:
I'm working on implementing a render graph abstraction

AMD recently did so too. In case you missed it, maybe it helps with your questions: https://github.com/GPUOpen-LibrariesAndSDKs/RenderPipelineShaders

This may be also related, discussing pros and cons of various approaches iirc: https://www.khronos.org/blog/vk-ext-descriptor-buffer

Advertisement

Nice thanks for the links!

As far as I can tell, the AMD RPS sdk doesn't seem to handle descriptors in any meaningful way internally, it seems to leave all binding and layout / set setup to the client. The descriptor buffers extension also seems promising and would heavily simplify any sort of descriptor integration in a render graph since it removes the concept of sets and pools and allows for better reshuffling of descriptors. Also perfect for a more bindless design so I may explore that more but its a relatively new extension it seems.

Ultimately this doesn't give me much insight into the potential process of automating descriptor set bindings within the render graph but maybe that's not something the render graph is meant to do.

Then again if part of specifying a high level render graph setup is to include information about pass input buffers / attachments I can see how it would be useful for the render graph to automatically bind the corresponding descriptors for said inputs when the corresponding passes get executed, hence my confusion.

skatehumor said:
Then again if part of specifying a high level render graph setup is to include information about pass input buffers / attachments I can see how it would be useful for the render graph to automatically bind the corresponding descriptors for said inputs when the corresponding passes get executed, hence my confusion.

My immediate knee-jerk reaction to this is to just go as bindless as possible and keep any relevant resources in large texture arrays or buffers that I can easily do dynamic updates on in some per-frame pre-processing step (before any binds), so there are no concerns about more granular descriptor set binds within any given pass. But maybe someone will slap some sense into this idea.

Okay so I'm gonna leave my (theoretical) solution here for posterity in case anyone's curious as to how I ended up solving this in the future:

Within the render graph I have a static array of 4 descriptor set data objects that houses the descriptor set and the corresponding layout, one for each major binding slot (global, per-pass, per-material, per-object). 4 right now because that's the min spec on max bound descriptor sets on some mobile devices the last time I checked. It's pretty easy to extend this to more descriptor data objects by just turning that into a vector instead of an array.

enum class DescriptorSetType : uint16_t
{
	Global = 0,
	Pass,
	Material,
	Object,
	MaxSets
}
DescriptorData descriptor_datas[(uint16_t)DescriptorSetType::MaxSets];

I have some helpers that can be used to inject the descriptor data definitions / bindings / etc. into the render graph via a condensed format from any external part of the engine that can get a reference to the render graph.

render_graph.inject_descriptor(DescriptorSetType::Global,
{
	{
		.binding = 0,
		.buffer = some_buffer,
		.buffer_offset = 0,
		.buffer_range = sizeof(MyDataType) * MAX_DATA_OBJECTS,
		.type = DescriptorType::StorageBuffer,
		.shader_stages = PipelineShaderStageType::All
	}
});

I then bind the global descriptor set at the beginning of graph execution, followed by the per-pass descriptor set, and then pass a reference to the descriptor datas array into a render graph frame struct that gets passed into the lambdas of each render pass, so they can perform their own descriptor set binding strategy if needed.

render_graph.add_pass(
	graphics_context,
	"general_compute_pass",
	RenderPassFlags::Compute,
	{},
	[](RenderGraph& graph, RGFrameData& frame_data, void* command_buffer)
	{
		uint32_t num_descriptor_datas = frame_data.descriptor_datas_count;
		DescriptorData* descriptor_datas = frame_data.descriptor_datas;
		// ... Do some binding stuff maybe
	}
);

This leaves the whole thing pretty flexible still and lets me easily switch to a more bindless design without having to touch the render graph internals much at all.

This topic is closed to new replies.

Advertisement