This tutorial shows how to implement bindless resources, a technique that leverages dynamic shader resource indexing feature enabled by the next-gen APIs to significantly improve rendering performance.
In old APIs (Direct3D11, OpenGL/GLES), when an application needed to render multiple objects using different shader resources, it had to run the following loop:
- Bind required resources (textures, constant buffers, etc.)
- Issue draw commands
- Go to step 1 and repeat operations for the next object.
There are multiple rechinques to make this loop more efficient such as instancing (see Tutorial 4), texture arrays (see Tutorial 5), etc. All these methods, however, are very limited.
Next-generation APIs (Direct3D12, Vulkan, Metal) enable a more efficient way: instead of binding new resources every time next object is rendered, all required resources can be bound once, while shaders can dynamically access required resources using the draw call information.
This tutorial is based on Tutorial 5. However, while original tutorial used a texture array object that required all array slices to be identical (same size, format, number of mip levels, etc.), this tutorial binds textures as an array of shader resources. Unlike texture array object, all resources in an array of resources may have completely different parameters. The pixel shader is able to dynamically select the texture to sample using the texture index it receives from the vertex shader (that reads it from the instance data buffer):
#define NUM_TEXTURES 4
Texture2D g_Texture[NUM_TEXTURES];
SamplerState g_Texture_sampler;
struct PSInput
{
float4 Pos : SV_POSITION;
float2 UV : TEX_COORD;
uint TexIndex : TEX_ARRAY_INDEX;
};
struct PSOutput
{
float4 Color : SV_TARGET;
};
void main(in PSInput PSIn,
out PSOutput PSOut)
{
PSOut.Color = g_Texture[NonUniformResourceIndex(PSIn.TexIndex)].Sample(g_Texture_sampler, PSIn.UV);
}
Notice the NonUniformResourceIndex
pseudo-function. It tells the shader compiler that the texture index is
not uniform, e.g. it may be different in every pixel.
Using it is essential,
or the shader may behave incorrectly.
There are no differences in pipeline state initialization for bindless shaders.
Shader resource binding objects are also initialized the same way, with the only difference
that we bind an array of objects using SetArray
method:
m_pBindlessPSO->CreateShaderResourceBinding(&m_BindlessSRB, true);
m_BindlessSRB->GetVariableByName(SHADER_TYPE_PIXEL, "g_Texture")->SetArray(pTexSRVs, 0, NumTextures);
As we discussed earlier, to render all objects, we bind all resources once:
m_pImmediateContext->SetPipelineState(m_pBindlessPSO);
m_pImmediateContext->CommitShaderResources(m_BindlessSRB, RESOURCE_STATE_TRANSITION_MODE_VERIFY);
And then render each object using its geometry properties. Texture index will be fetched from the instance data buffer and passed over to the pixel shader.
for (int i=0; i < NumObjects; ++i)
{
const auto& Geometry = m_Geometries[m_GeometryType[i]];
DrawIndexedAttribs DrawAttrs;
DrawAttrs.IndexType = VT_UINT32;
DrawAttrs.NumIndices = Geometry.NumIndices;
DrawAttrs.FirstIndexLocation = Geometry.FirstIndex;
DrawAttrs.FirstInstanceLocation = static_cast<Uint32>(i);
DrawAttrs.Flags = DRAW_FLAG_VERIFY_ALL | DRAW_FLAG_DYNAMIC_RESOURCE_BUFFERS_INTACT;
m_pImmediateContext->DrawIndexed(DrawAttrs);
}
Notice that we use DRAW_FLAG_DYNAMIC_RESOURCE_BUFFERS_INTACT
flag. This flag informs the engine
that none of the dynamic buffers have been modified since the last draw command, which saves extra work
the engine would have to perform otherwise.