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).