Jump to content
  • Advertisement
Sign in to follow this  
ZachBethel

DX12 Descriptor binding: DX12 and Vulkan

This topic is 783 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

I've been reading up on how the resource binding methods work in Vulkan and DX12. I'm trying to figure out how to best design an API that abstracts the two with respect to binding descriptors to the pipeline. Naturally, the two API's are similar, but I'm finding that they treat descriptor binding differently in subtle ways.

 

Disclaimer: Skip to the bottom if you have a deep understanding of this already and just care about my specific question.

 

Explanation:

 

In DirectX, you define a "root signature". It can have push constants, inlined descriptors binding points, or descriptor table binding points. It also defines static samples on the signature itself. A descriptor table is a contiguous block of descriptors within a descriptor heap. Binding a table involves specifying the first descriptor in the heap to the pipeline. Tables can hold either UAV/SRV/CBV descriptors or SAMPLER descriptors. You cannot share the two within a single heap--and therefore table. Descriptor tables are also organized into ranges, where each range defines one or more descriptors of a SINGLE type.

 

Root Signature Example:

IC831400.png

 

Descriptor Heap indirection:

descriptors-descriptor-heap-descriptor-t

 

 

In Vulkan, you define a "pipeline layout". It can have push constants and "descriptor set" binding points. You cannot define inlined descriptors binding points. Each descriptor set defines a set of static samplers. A descriptor set is a first class object in Vulkan. It also has one or more ranges of a SINGLE descriptor type.

 

 

 

Descriptor Sets:

vulkan_resbinding_layouts.png

 

 

Now, an interesting pattern I'm seeing is that the two API's provide descriptor versioning functionality for completely different things. In DirectX, you can version descriptors implicitly within the command list using the root descriptor bindings. This allows you to do things like specify a custom offset for a constant buffer view. In Vulkan, they provide an explicit UNIFORM_DYNAMIC descriptor type that allows you to version an offset into the command list. See the image below:

 

vulkan_resbinding_uniforms.png

 

 

Question:

 

Okay, so I'm really just looking for advice on how to organize binding points for an API that wraps these two models.

 

My current tentative approach is to provide an API for creating buffers and images, and then explicit UAV/SRV/CBV/RTV/DSV views into those objects. The resulting view is an opaque, typeless handle on the frontend that can map to descriptors on DirectX 12 or some staging resource in Vulkan for building descriptor sets.

 

I think I want to provide an explicit "ResourceSet" object that defines 1..N ranges of views similar to how both the descriptor set and descriptor table models work. I expect that I would make sampler binding a separate API that does its own thing for the two backends. I would really like to treat these ResourceSet objects similar to constant buffers, except that I'm just writing view handles into it.

 

I need to figure out how to handle versioning of updates to these descriptor sets. In the simplest case, I treat them as fully static. This maps well to both DX12 and Vulkan because I can simply allocate space in a descriptor heap or create a descriptor set, write the descriptors to it, and I'm done.

 

Handling dynamic updates becomes complicated for both API's and this is the crux of where I'm struggling right now.

 

Both APIs let me push constants, so that's not really a problem. However, DirectX allows you to version descriptors directly in the command list, but Vulkan allows you to dynamic offsets into buffers. It seems like this is chiefly for CBVs.

 

So it seems like if I want to do something like have a descriptor set with 3 CBV's, and then do dynamic offsets, I have to explicitly version the entire table in DirectX by allocating some new space in the heap and spilling descriptors to it.

 

On the other hand, since Vulkan doesn't really have the notion of root descriptors, I'd have to create multiple descriptorset objects and version those out if I want to bind a single dynamic UAV.

 

Either way, it seems like the preferred model is to build static descriptor sets but provide some fast path for constant buffers, and that's the direction I think I'm going to head in.

 

Anyway, does this sound like a sane approach? Have you guys find better ways to abstract these two binding models?

 

Side question: How do you version descriptor sets in vulkan? Do you just have to pool descriptor sets for the frame and spill when updates occur?

 

Thanks!

Edited by ZBethel

Share this post


Link to post
Share on other sites
Advertisement

Somehow there are no replies, but I find this topic interesting and followed the link. Maybe this should be moved into the Vulkan forum. As I program mostly in C# right now I would like to see how an API for Vulkan/DX12 would look different. In C# I acquire resources by using using{  and release them with } . The compiler then just stacks up resources. Vulkan/DX12 seem to do more.

Didn't we have the topic data-structures recently? Somehow I feel that one does not need two APIs, but two scene graph compilers.

Share this post


Link to post
Share on other sites

I'm also curious to hear how people are starting to take a crack at this.

Share this post


Link to post
Share on other sites

If I understood the question correctly, you are asking about updating descriptor sets / descriptor tables contents. IMO, by these APIs desing this should be avoided, because when you call vkUpdateDescriptorSets/CreateConstantBufferView/etc. you must ensure that GPU is not using this descriptor (you must use fences in this case). I would create multiple descriptor set copies of the same layout (or reserve more space in descriptor heap in case of D3D12) and update these when they are not used by GPU at the moment. In case of double frame buffering - two copies should be fine.

Share this post


Link to post
Share on other sites

I've been thinking more about this, and I've come to realize some things.

 

I did some investigation into how some real workloads are handling the root signature. I found that a vast majority of what I saw have a structure similar to this:

 

DX12 style binding slots:

 

For bucketed scene draws:

 

0: Some push constants

1: per draw constant buffer

2: per pass constant buffer

3: per material constant buffer

4: A list of SRVs

 

For various post processing jobs:

 

0+ constant buffers

simple table of UAVs

simple table of SRVs

 

I didn't find any use cases where different regions of the same descriptor table were used for different stuff... for the most part is seems a simple list of SRVs / UAVs is enough.

 

I also realized that Vulkan has the strong notion of a render pass, and that UAVs could be factored into render passes as outputs (which are then transitioned to SRVs).

 

To me, it seems like having constant buffer binding slots, a way to bind a list of SRVs to the draw call, and a way to bind a list of UAVs to a render pass is enough to support most scenarios.

 

With regards to list allocation, it seems like descriptor layouts are going to be bounded by the application. Like you said, Witek902, you could just create a free list pool for descriptors and orphan them on update into a recycle queue. Static descriptor sets just get allocated once and held.

 

For DX12, you could model that same technique by allocating fixed size pieces out of a descriptor heap, or use some sort of buddy allocator. With the descriptor heap approach it becomes a bit weirder because it seems the ideal use case scenario is to keep the same heap bound for the whole frame.

 

I also read in Gpu Fast Paths that using dynamic constant buffers eats up 4 registers of the USER-DATA memory devoted to the pipeline layout. Apparently using a push constant to offset into a big table is more performant (I'm not sure how portable this is to platforms like mobile). 

 

Anyway, just some thoughts.

Edited by ZBethel

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

We are the game development community.

Whether you are an indie, hobbyist, AAA developer, or just trying to learn, GameDev.net is the place for you to learn, share, and connect with the games industry. Learn more About Us or sign up!

Sign me up!