D3D12 Root Signatures for different shaders

Started by
16 comments, last by Matias Goldberg 8 years, 8 months ago

How is one supposed to use Root Signatures for rendering lots of different things? I've been reading through the documentation and sample code for DX12, but everything so far is set up for a single application that has one specific rendering purpose.

This page on Root Signatures says "Currently, there is one graphics and one compute root signature per app." Yet the first sentence in the paragraph is "The root signature defines what resources are bound to the graphics pipeline." If a root signature links the command list to the resources required by the shader (textures and cbuffers), then it would seem like I want a root signature per distinct shader. (eg. one that has diffuse/normal textures, a different one that has 3 cbuffers, etc.)

Or am I supposed to instead make an uber root signature that contains the maximum SRVs and CBVs I could ever use?

Advertisement

Now I'm thinking that the presence of the function "SetGraphicsRootSignature" implies that I should create multiple root signatures. That quote must be saying that there can only be one in use at any given time.

You could use multiple root signatures if you want, but it is advised to keep the number of root signatures used in your application to a minimum. It's pretty much a balance of finding the most minimal root signature with which you can render the largest amount of objects. That being said though you want to avoid creating huge root signatures as a catch-all solution as this might push it to use less efficient memory.

Also remember that switching root signatures means you have to rebind all data originally bound to the root signature as this state is not preserved between root signature rebinds, which can introduce some overhead.

I'd experiment with a couple of different solutions and see what works best for you.

I gets all your texture budgets!

You should watch the videos from DX12 presentations from way before the SDK was released.

Basically the root signature is a "wildcard" to do dirty stuff quickly or to do very simple global state changes where it makes sense to put them on the root signature.

But you're strongly encouraged to use and reuse descriptors stored in heaps, bound via tables. You want to use one descriptor per shader (or multiple descriptors per shader, or one descriptor per a group of shaders). Same with the tables.

Edit: So if shader "A" has permanent data that is always bound (e.g. shadow maps, per-pass data like the view matrix or ambient colour) these buffer views (aka descriptors) would go into one table, while more frequently changed data (e.g. diffuse textures) would go into another table, less frequently changed data (e.g. environment maps, texture buffer containing an array of world matrices) would go into its own table, and data that changes per draw (avoid this) should be set to the Root signature directly.

If you can reuse those tables for shader B, that's great. Otherwise add more descriptors.

You can bake the data beforehand (e.g. prepare the descriptor heaps holding the textures used by each material) or do it on the fly (burn heap space for every texture that needs to be rebound, changing the tables during the process)

Aren't tables part of the root signature?

Aren't tables part of the root signature?

Perhaps we should clarify a few confusions:

"Descriptors" are stored in the Heap.
A "Descriptor Table" is nothing more than a range of the heap.
This range of the heap must be set to the root signature.
The Root Signature has its own small heap for 'dirty' things.

In C jargon, we could say it's similar to the following:


struct Descriptor;

Descriptor myTable[256];
myTable[0] = Descriptor( ... );
myTable[1] = Descriptor( ... );
...
SetNumGraphicsRootDescriptorTables( 1 ); //At initialization, tell the Root signature we can have up to 1 table. AFAIK we cannot change it later.
SetGraphicsRootDescriptorTable( 0, &myTable[2], 5 ); //Bind myTable[2] through myTable[8]

This is of course simplified.
Whether you want to use just one table or multiple ones is up to you (probably you will have to use more than just 1 table because of certain restrictions, eg. Samplers must live in their own table). You probably want to have around 4 or 5 to change the Tables based on update frequency (e.g. do not put a texture that will be used by all objects, like a lightmap or a shadow map, in the same table as you will put diffuse textures).
The max amount of tables you can have depends on how you use the limited Root space.

Edit:
The key to performance is in baking the heaps as much as possible so that you just set the ranges of the table and fire rendering, rather than filling the heap data on the fly.
Examples in pseudo code (I'm assuming 2 texture per shader for simplicity in the explanation):
You can bake data like this:


Descriptor myTable[256];

//Once, during initialization:

//Material A, Shader A
myTable[0] = setupDescriptor( diffuseTextureF );
myTable[1] = setupDescriptor( specularTextureG );

//Material B, Shader A
myTable[1] = setupDescriptor( diffuseTextureH );
myTable[2] = setupDescriptor( specularTextureI );

//Material C, Shader B
myTable[3] = setupDescriptor( diffuseTextureJ );
myTable[4] = setupDescriptor( roughnessmapK );

//Every frame:
size_t lastId = -1;
for( i < numObjects )
{
    if( renderable[i]->GetMaterialId() != lastId )
        SetGraphicsRootDescriptorTables( 4, myTable[renderable[i]->GetMaterialId()], 2 );
    drawPrimitiveCmd( renderable[i] );
}


Or you can do set every descriptor on the fly, D3D11/GL style:


//Every frame:
size_t currentTableIdx = 0;
size_t lastId = -1;
for( i < numObjects )
{
    if( renderable[i]->GetMaterialId() != lastId )
    {
        myTable[currentTableIdx] = setupDescriptor( renderable[i]->GetTexture0() );
        myTable[currentTableIdx+1] = setupDescriptor( renderable[i]->GetTexture1() );
        SetGraphicsRootDescriptorTables( 4, myTable[currentTableIdx], 2 );
        currentTableIdx += 2;
    }

    drawPrimitiveCmd( renderable[i] );
}

The first method lets you reuse descriptors if it gets reused even they're not contiguous. The last method only gets to reuse descriptor if renderable and renderable[i-1] used the same resources. Plus it wastes performance setting them up every time they change.
And of course you can reduce the number of calls to SetGraphicsRootDescriptorTables by dynamically indexing the textures in the shader as shown in the samples, which is basically a much better version of D3D11's texture arrays (beware of hardware limits).

This page on Root Signatures says "Currently, there is one graphics and one compute root signature per app." Yet the first sentence in the paragraph is "The root signature defines what resources are bound to the graphics pipeline."


To clarify that, the documentation is worded poorly. You can make more root signatures at your whim, but you can only have a single one bound to a given pipeline at a time.


Generally speaking, you'll have roughly one root signature per "shader signature." e.g., the shaders you use to render meshes may well need a different root signature than the one you use to render debug text. However, since you want to minimize the number of root signatures, most of your mesh-drawing shaders should use the same signature (and hence, the same input vertex format!).

This is true even in older versions of D3D and in OpenGL, mind you. (D3D11 "input layouts" and OpenGL "vertex array objects" are very roughly the primitive precursors to a root signature, and you wanted to minimize the numbers of those in order to maximize draw call throughput in the old APIs.)

Sean Middleditch – Game Systems Engineer – Join my team!


However, since you want to minimize the number of root signatures,

Do you mean go out your way to minimize or just avoid duplicates?

If you mean go out your way could you explain the logic as to why?

-potential energy is easily made kinetic-

The main issue with having too many Root Signatures, is that each time you change the Root Signature, you must bind everything again (RootCBV, RootSRV, Descriptor Tables, etc), since there is no warranty that the next Root signature will match that. And this applies even if the new signature has a RootCBV in the same root slot. This is driver/hardware dependent so you can't rely on the fact that it just work for you (in the case that you don't bind everything again which it does seem to work on some platforms/cases).

Root Slots are not the same as the old slots of DX11 (like constant buffer or texture slot). Is not that you bind a CBV or a SRV to slot 0 and you can rely that is there for good.

A good approach is to have Root Signatures per usage. For example in my case I have 3 root signatures for meshes (one for DepthOnly/shadows, one for GBuffer and one for forward/material pass), so when I am going to render the shadows for the meshes, I just bind one root signature and I never change it for that pass.

There are cases like transparent meshes that are interleaved with particles that require two root signatures, but that is not the common case.

In the end, you shouldn't need more, at most, than a couple of dozens Root Signatures for your engine.

And about the root signature itself, using CBV in the root signature is fine, just as using root constants. In fact for instance data, or things that change per call is better to have it this way, otherwise you would need to modify the tables, but the root signature data is copied to the command list for each draw call, so if you the bigger it is the more expensive it gets. Remember that RootCBV takes 4 DWORDs, and tables only takes one DWORD.

On the GPU side of things, on some platforms, Constants in Root Sgnature can be cheaper than in a Constant Buffer (a CBV in the root or in a table) but it should never be slower. In the end is all about balance.

Note: SRV on the Root only work for buffers, not textures. So the only way to bind textures is using tables.

As a follow up question: Say I have two shaders that use slightly different resources.

Shader A:

2 Textures

1 CBuffer

1 Sampler

Shader B:

1 Texture

1 CBuffer

1 Sampler

Should I use the same root signature for these two shaders? (One that strictly matches Shader A). It seems like it would still work for Shader B, but it would have a place for an additional texture that the shader doesn't actually use.

Would I want to use the same root signature for B, or make one specific to its format?

This topic is closed to new replies.

Advertisement