Descriptor Resource Sets

Started by
3 comments, last by MJP 6 years, 2 months ago

I wanted to see how others are currently handling descriptor heap updates and management.

I've read a few articles and there tends to be three major strategies :

1 ) You split up descriptor heaps per shader stage ( i.e one for vertex shader , pixel , hull, etc)

2) You have one descriptor heap for an entire pipeline

3) You split up descriptor heaps for update each update frequency (i.e EResourceSet_PerInstance , EResourceSet_PerPass , EResourceSet_PerMaterial, etc)

The benefits of the first two approaches is that it makes it easier to port current code, and descriptor / resource descriptor management and updating tends to be easier to manage, but it seems to be not as efficient.

The benefits of the third approach seems to be that it's the most efficient because you only manage and update objects when they change.

Advertisement

Hi, I know of two different methods that try to stay close to the resource slot binding model (like what is used in DX11).

1.) Replicate DX11 behaviour completely

This uses two descriptor heaps (1 sampler heap + 1 resource heap[CBV,UAV,SRV]) and one root signature for the entire application. You have staging resource heaps which you update from CPU when you call PSSetShaderResources() for example. When a Draw call follows, and there has been a change to the staging heap since the last draw, you issue a copy of the full staging heap to GPU visible heap (implementing descriptor renaming).

2.) Light-weight resource binding model

You have a unique root signature for each "shader-pass" or PSO. When loading shaders, you create the unique root signature to contain tightly packed descriptor tables, so they have only the resources that the shaders in the pass are using. This can be done for DX11-like shaders which declare slot bindings using shader reflection. This way, you have to ensure that when you call Draw, you have to ensure that you bind every descriptor that will be used, previous state will not be kept around. So you don't have to keep a staging descriptor heap, because you will be immediately be filling the GPU-visible heaps with the correct data. This is light-weight because you will not copy the full staging heap around each time you call Draw() and some resources have changed. But this comes with additional responsibility for the developer because now every resource have to be explicitly bound again. However, that is much less copying around than a common, "works for everything" solution like the previous one.

And finally, there can be other methods which could be much better but not easy to play along with the resource slot binding model, which is using the DX12 memory model explicitly by the app. I would certainly prefer the DX12 model when designing a renderer from ground up, though most developers are still more familiar with the old approaches.

I think your terminology is a bit off -- a descriptor heap is a huge area of memory where descriptors can be allocated. You can only have a single combined SRV/CBV/UAV-type descriptor heap bound to the device at a time, and changing this binding is expensive, so you're encouraged to only ever have a single one bound. You can create extra ones as staging areas where you can pre-create SRV's which can later be copied into your main/bound heap. Within a heap, you create descriptor-tables, which get bound to the root signature.

The resource binding model in our engine has 8 "resource list" slots, which each contain an array of SRV's.

In D3D11, each "resource list" is mapped to a contiguous range of t# registers in the shader.
e.g. If a shader has ResList#0 with 4 textures and ResList#1 with 2 textures, the binding system is configured to copy ResList#0 into SRV slots [0,3] and ResList#1 into SRV slots [4,5].

In D3D12, each "resource list" is mapped to a root-descriptor-table parameter. Each shader generates a root-signature where param#0 is a table of CBV's, and param #1,2... are the res-list SRV tables. When submitting a draw-call, we determine if any res-list slots have changed since the previous draw-call (or if the previous draw-call used a different root signature). If so, a descriptor table is allocated for each new res-list within a ring-buffer, the SRV's for that root signature are copied into that new allocation from a non-shader-visible (staging) descriptor heap, and these new tables are set as root parameters.

When creating a texture, it's SRV is pre-created in the non-shader-visible descriptor heap, while the shader-visble descriptor heap is just a ring-buffer of these transient tables.

Long term, you definitely don't want to be constantly copying around descriptors into tables if you want the best possible performance. So yeah, assuming that you mean "descriptor table" instead of "descriptor heap" then #3 sounds like its the closest to that ideal. If you do it that way, then you can basically follow the general practice of constant buffers, and group your descriptors based on update frequency. Ideally you would want to be able to identify descriptor tables that never change, and only build them once.

There's another (crazier) approach which is what we use at work, but it requires at least RESOURCE_BINDING_TIER_2 (so no Kepler or old Intel GPU's). Basically the idea is that you go full "bindless", where instead of having contiguous tables of descriptors, you have 1 great big table and expose that to all of your shaders. Then each shader has access to a big unbounded texture array, and it samples a texture by using an integer index to grab the right texture from the array. Then "binding" can be as simple as filling a constant buffer with a bunch of uint's. There's a bunch of wrinkles in practice, but I assure it works. You can also do some really fun things with this setup once you have it working, since it basically gives your shaders arbitrary access to any texture or buffer, with the ability to use any data structure you want to store handles.

This topic is closed to new replies.

Advertisement