Version:

AZSL, Binding Rules For Unbounded Arrays

AZSL supports declaration of unbounded arrays inside Shader Resource Group (SRG) definitions (ShaderResourceGroup), but there are limitations on how many and what kind of unbounded arrays can be declared. Amazon Shading Language Compiler (AZSLc) verifies that those rules are not violated.

Unbounded array rules and limitations

The following table summarizes the rules and limitations for binding unbounded array for the platforms: DX12, Vulkan, and Metal. Each platform uses the --use-spaces and --unique-idx command line arguments for AZSLc.

In the following table, the symbols, t[], u[], b[] and s[] refer to the type of resource for the unbounded array that is being declared.

  • t – for shader resource views (SRV)
  • s – for samplers
  • u – for unordered access views (UAV)
  • b – for constant buffer views (CBV)
--use-spaces--unique-idxPlatformResults
OFFOFFOnly the last SRG can contain one last unbounded array for each:
t[], u[], b[], s[]
OFFONOnly the last SRG can contain one, and only one, last unbounded array across all resource types:
Only one of t[], u[], b[], s[]
ONOFFDX12Each SRG can contain one last unbounded array for each:
t[], u[], b[], s[]
ONONVulkan
Metal
Each SRG can contain one, and only one, last unbounded array across all resource types:
Only one of t[], u[], b[], s[]

Things to know

To understand the examples on this page, review the following information:

  • In HLSL, a register space is a slot that connects shader data from the CPU to the GPU. For comparison, a register space is similar to a descriptor set in Vulkan.
  • When compiling shader code with the --use-spaces option, AZSLc assigns a unique register space per SRG.
  • When compiling shader code with the --unique-idx option, AZSLc emits all resource descriptors into numerical sequences, regardless of the descriptor type. Vulkan and Metal platforms require this.
  • Unbounded arrays take over the whole range of register indices from the point they are declared. This means you must declare any resources including bounded arrays before declaring an unbounded array. Any resource declared after an unbounded array will cause an error.

Examples

All the examples shown here assume that AZSLc compiles the shader code with the --use-spaces option set to ON. So, assume that each SRG is assigned to its own register space.

Example 1: Register assignments

This example demonstrates how index bindings are assigned to register spaces. It contains one SRG and no unbounded arrays.

    ShaderResourceGroupSemantic slot1
    {
        FrequencyId = 1;
    };
    
    struct MyStruct
    {
        float3 m_a;
        float3 m_b;
    };
    
    //For DX12, --unique-idx is OFF, so the expected register assignment is as follows:
    // The ConstantBuffer reserved for SRG1 binds to b0 register
    ShaderResourceGroup SRG1 : slot1
    {
        Texture2D<float4>        m_texSRV1;      // Binds to t0 register.
        Texture2D<float4>        m_texSRV2;      // Binds to t1 register.
        RWTexture2D<float4>      m_texUAV1;      // Binds to u0 register.
    	RWTexture2D<float4>      m_texUAV2;      // Binds to u1 register.
        Sampler                  m_sampler1;     // Binds to s0 register.
        Sampler                  m_sampler2;     // Binds to s1 register.
        ConstantBuffer<MyStruct> m_cb1;          // Binds to b1 register.
        ConstantBuffer<MyStruct> m_cb2;          // Binds to b2 register.
    };

Error case 1.1

This example demonstrates how compiling an unbounded array (Texture2D<float4> m_texSRV_unbounded[]) can result in a register assignment conflict.
This example fails to compile because m_texSRV_unbounded[] takes over the whole range of register indices from the point its declared. The unbounded array, m_texSRV_unbounded[], is the first texture shader resource view (SRV), so it takes over the whole range of register indices that’s allocated for SRVs, from t0 to tN. Texture SRVs that are declared afterwards don’t have a register index to bind to.

    ShaderResourceGroupSemantic slot1
    {
        FrequencyId = 1;
    };
    
    struct MyStruct
    {
        float3 m_a;
        float3 m_b;
    };
    
    //For DX12, --unique-idx is OFF, so the expected register assignment is as follows
    ShaderResourceGroup SRG1 : slot1
    {
        Texture2D<float4>        m_texSRV_unbounded[]; // Binds to t0 register, and takes over t1+.
        Texture2D<float4>        m_texSRV1;      // ERROR. There's no tN available to bind this resource to.
        Texture2D<float4>        m_texSRV2;      // ERROR. There's no tN available to bind this resource to.
        RWTexture2D<float4>      m_texUAV1;      // Binds to u0 register.
    	RWTexture2D<float4>      m_texUAV2;      // Binds to u1 register.
        Sampler                  m_sampler1;     // Binds to s0 register.
        Sampler                  m_sampler2;     // Binds to s1 register.
        ConstantBuffer<MyStruct> m_cb1; // Binds to b1 register. (The ConstantBuffer reserved for SRG1 binds to b0 register).
        ConstantBuffer<MyStruct> m_cb2; // Binds to b2 register.
    };

Error case 1.2

This example demonstrates a similar register assignment conflict, but defines the unbounded array, m_texSRV_unbounded, after m_texSRV1 and before m_texSRV2. Since m_texSRV_unbounded is defined after m_texSRV1, m_texSRV1 can successfully bind to a register. However, since m_texSRV_unbounded is defined before m_texSRV2, it again allocates a whole range of register indices, leaving m_texSRV2 without an available resource to bind to.

    ShaderResourceGroupSemantic slot1
    {
        FrequencyId = 1;
    };
    
    struct MyStruct
    {
        float3 m_a;
        float3 m_b;
    };
    
    //For DX12, --unique-idx is OFF, so the expected register assignment is as follows:
    ShaderResourceGroup SRG1 : slot1
    {
        Texture2D<float4>        m_texSRV1;            // Binds to t0 register.
        Texture2D<float4>        m_texSRV_unbounded[]; // Binds to t1 register, and takes over t2+.
        Texture2D<float4>        m_texSRV2;      // ERROR. There's no tN available to bind this resource to.
        RWTexture2D<float4>      m_texUAV1;      // Binds to u0 register.
    	RWTexture2D<float4>      m_texUAV2;      // Binds to u1 register.
        Sampler                  m_sampler1;     // Binds to s0 register.
        Sampler                  m_sampler2;     // Binds to s1 register.
        ConstantBuffer<MyStruct> m_cb1; // Binds to b1 register. (The ConstantBuffer reserved for SRG1 binds to b0 register).
        ConstantBuffer<MyStruct> m_cb2; // Binds to b2 register.
    };

Solution

This example demonstrates how to resolve the register assignment conflicts in the previous error cases. To resolve this, define the unbounded array last among the group of resources with the same resource type. In this case, define m_texSRV_unbounded after m_texSRV1 and m_texSRV2.

    ShaderResourceGroupSemantic slot1
    {
        FrequencyId = 1;
    };
    
    struct MyStruct
    {
        float3 m_a;
        float3 m_b;
    };
    
    //For DX12, --unique-idx is OFF, so the expected register assignment is as follows
    ShaderResourceGroup SRG1 : slot1
    {
        Texture2D<float4>        m_texSRV1;      // Binds to t0 register.
        Texture2D<float4>        m_texSRV2;      // Binds to t1 register.
        Texture2D<float4>        m_texSRV_unbounded[]; // Binds to t2 register, and takes over t3+.
        RWTexture2D<float4>      m_texUAV1;      // Binds to u0 register.
    	RWTexture2D<float4>      m_texUAV2;      // Binds to u1 register.
        Sampler                  m_sampler1;     // Binds to s0 register.
        Sampler                  m_sampler2;     // Binds to s1 register.
        ConstantBuffer<MyStruct> m_cb1; //Binds to b1 register. (The ConstantBuffer reserved for SRG1 binds to b0 register).
        ConstantBuffer<MyStruct> m_cb2; // Binds to b2 register.
    };

Example 2: Declaring unbounded arrays of different types

This case shows how to declare an unbounded array for each resource type. Always declare each unbounded array after the last non-unbounded resource of the same resource type.

    ShaderResourceGroupSemantic slot1
    {
        FrequencyId = 1;
    };
    
    struct MyStruct
    {
        float3 m_a;
        float3 m_b;
    };
    
    //For DX12, --unique-idx is OFF, so the expected register assignment is as follows
    ShaderResourceGroup SRG1 : slot1
    {
        Texture2D<float4>        m_texSRV1;            // Binds to t0 register.
        Texture2D<float4>        m_texSRV_bounded[3];  // Binds from t1 to t3 registers (inclusive).
        Texture2D<float4>        m_texSRV_unbounded[]; // Binds from t4, and takes over t5+.
        RWTexture2D<float4>      m_texUAV_bounded[5];  // Binds from u0 to u4 registers (inclusive).
    	RWTexture2D<float4>      m_texUAV_unbounded[]; // Binds from u5 register, and taked over u6+.
        Sampler                  m_sampler1_bounded[7]; // Binds from s0 to s6 registers (inclusive).
        Sampler                  m_sampler2_bounded[3]; // Binds from s7 to s9 registers (inclusive).
        Sampler                  m_sampler_unbounded[]; // Binds to s10 register, and takes over s11+.
        ConstantBuffer<MyStruct> m_cb_array[2]; // Binds from b1 to b2 registers (Inclusive). The ConstantBuffer reserved for SRG1 binds to b0 register.
        ConstantBuffer<MyStruct> m_cb2; // Binds to b3 register.
        ConstantBuffer<MyStruct> m_cb_unbounded[]; // Binds to b4 register, and takes over b5+.
    };

Now, let’s review what will happen to all the examples above when using ‘–unique-idx’, which is the case for Vulkan & Metal.

Example 3: Resource assignments with --unique-idx

This example is similar to Example 1: Register assignments, but assumes that AZSLc compiles with the --unique-idx set to ON, which is required by Vulkan and Metal. With the --unique-idx option enabled, the register index always increments, regardless of the resource type.

    ShaderResourceGroupSemantic slot1
    {
        FrequencyId = 1;
    };
    
    struct MyStruct
    {
        float3 m_a;
        float3 m_b;
    };
    
    //For Vulkan & Metal, --unique-idx is ON, so the expected register assignment is as follows:
    // The ConstantBuffer reserved for SRG1 binds to b0 register.
    ShaderResourceGroup SRG1 : slot1
    {
        Texture2D<float4>        m_texSRV1;      // Binds to t1 register.
        Texture2D<float4>        m_texSRV2;      // Binds to t2 register.
        RWTexture2D<float4>      m_texUAV1;      // Binds to u3 register.
    	RWTexture2D<float4>      m_texUAV2;      // Binds to u4 register.
        Sampler                  m_sampler1;     // Binds to s5 register.
        Sampler                  m_sampler2;     // Binds to s6 register.
        ConstantBuffer<MyStruct> m_cb1;          // Binds to b7 register.
        ConstantBuffer<MyStruct> m_cb2;          // Binds to b8 register.
    };

Error case.

This example demonstrates how compiling an unbounded array (Texture2D<float4> m_texSRV_unbounded[]) with the --use-idx option results in a register assignment conflict. This example fails to compile because the unbounded array, m_texSRV_unbounded[], takes over a whole range of register indices, regardless of resource type. m_texSRV_unbounded[] takes over tN, uN, sN, and bN registers, as opposed to the first error case in Example 1 : Register assignments where it only takes over tN registers.

    ShaderResourceGroupSemantic slot1
    {
        FrequencyId = 1;
    };
    
    struct MyStruct
    {
        float3 m_a;
        float3 m_b;
    };
    
    //For Vulkan & Metal, --unique-idx is ON, so the expected register assignment is as follows:
    // The ConstantBuffer reserved for SRG1 binds to b0 register.
    ShaderResourceGroup SRG1 : slot1
    {
        Texture2D<float4>        m_texSRV_unbounded[]; // Binds to t1 register and taked over ALL1+. 
        Texture2D<float4>        m_texSRV1;      // ERROR. There's no tN available to bind this resource to.
        Texture2D<float4>        m_texSRV2;      // ERROR. There's no tN available to bind this resource to.
        RWTexture2D<float4>      m_texUAV1;      // ERROR. There's no uN available to bind this resource to.
    	RWTexture2D<float4>      m_texUAV2;      // ERROR. There's no uN available to bind this resource to.
        Sampler                  m_sampler1;     // ERROR. There's no sN available to bind this resource to.
        Sampler                  m_sampler2;     // ERROR. There's no sN available to bind this resource to.
        ConstantBuffer<MyStruct> m_cb1; // ERROR. There's no bN available to bind this resource to.
        ConstantBuffer<MyStruct> m_cb2; // ERROR. There's no bN available to bind this resource to.
    };

Solution.

This example demonstrates how to resolve the register assignment conflict in the previous error case. To resolve this, define the unbounded array after all resource declarations, regardless of their resource types. In this case, define m_texSRV_unbounded after m_cb2. Alternatively, you can move the declaration of m_texSRV_unbounded into a second SRG declaration, SRG2. The next example demonstrates multiple SRG declarations in detail.

    ShaderResourceGroupSemantic slot1
    {
        FrequencyId = 1;
    };
    
    struct MyStruct
    {
        float3 m_a;
        float3 m_b;
    };
    
// For Vulkan & Metal, --unique-idx is ON, so the expected register assignment is as follows
// The ConstantBuffer reserved for SRG1 binds to b0 register.
    ShaderResourceGroup SRG1 : slot1
    {
        Texture2D<float4>        m_texSRV1;      // Binds to t1 register.
        Texture2D<float4>        m_texSRV2;      // Binds to t2 register.
        RWTexture2D<float4>      m_texUAV1;      // Binds to u3 register.
    	RWTexture2D<float4>      m_texUAV2;      // Binds to u4 register.
        Sampler                  m_sampler1;     // Binds to s5 register.
        Sampler                  m_sampler2;     // Binds to s6 register.
        ConstantBuffer<MyStruct> m_cb1;          // Binds to b7 register.
        ConstantBuffer<MyStruct> m_cb2;          // Binds to b8 register.
        Texture2D<float4>        m_texSRV_unbounded[]; // Binds to t9 register, and takes over ALL10+.
    };

Example 4: Declaring unbounded arrays with --unique-idx

The earlier example, Example 2: Declaring unbounded arrays of different types, demonstrates how to declare an unbounded array for each major resource type: SRV(t), UAV(u), Sampler(s) and CBV(b) inside a single SRG. You cannot do this when compiling with the --use-idx option.
To work around this issue, define additional SRGs per resource type. Since each SRG has their set of register spaces that are defined in a numerical sequence, you can avoid resource assignment conflict by separating resources into different SRGs. As in previous examples, always declare an unbounded array as the last variable inside each SRG.

ShaderResourceGroupSemantic slot1 { FrequencyId = 1; };
ShaderResourceGroupSemantic slot2 { FrequencyId = 2; };
ShaderResourceGroupSemantic slot3 { FrequencyId = 3; };
ShaderResourceGroupSemantic slot4 { FrequencyId = 4; };

struct MyStruct
{
    float3 m_a;
    float3 m_b;
};

// For Vulkan & Metal, --unique-idx is ON, so the expected register assignment is as follows:

// SRG1 binds to register space 0. The ConstantBuffer reserved for SRG1 binds to b0 register. 
ShaderResourceGroup SRG1 : slot1
{
    Texture2D<float4>        m_texSRV1;      // Binds to t1 register.
    Texture2D<float4>        m_texSRV_bounded[3];  // Binds from t2 to t4 register (inclusive).
    Texture2D<float4>        m_texSRV_unbounded[]; // Binds to t5 register, and takes over ALL6+ for register space 0.
    float m_value; // Okay to declare a variable of fundamental type as it is not a resource.
};

// SRG2 binds to register space 1. The ConstantBuffer reserved for SRG2 binds to b0 register. 
ShaderResourceGroup SRG2 : slot2
{
    RWTexture2D<float4>      m_texUAV_bounded[5];  // Binds from u1 to u5 registers (inclusive).
	RWTexture2D<float4>      m_texUAV_unbounded[]; // Binds to u6 register, and takes over ALL7+ for Register Space 1.
    bool m_boolValue;                              // Okay to declare a variable of fundamental type as it is not a resource.
};

// SRG3 binds to register space 2. The ConstantBuffer reserved for SRG3 binds to b0 register.
ShaderResourceGroup SRG3 : slot3
{
    Sampler                  m_sampler1_bounded[7]; // Binds from s1 to s7 registers (inclusive).
    Sampler                  m_sampler2_bounded[3]; // Binds from s8 to s10 (inclusive).
    Sampler                  m_sampler_unbounded[]; // Binds to s11 register, and takes over ALL12+ for Register Space 2.
    float3x3                 m_matrix;              // Okay to declare a variable of fundamental type as it is not a resource.
};

// SRG4 binds to register space 3. The ConstantBuffer reserved for SRG4 binds to b0 register.
ShaderResourceGroup SRG4 : slot4
{
    ConstantBuffer<MyStruct> m_cb_array[2]; // Will be bound to b1 register. (Because The ConstantBuffer reserved for SRG4 will be bound to b0 register)... Until     b2 (inclusive).
    ConstantBuffer<MyStruct> m_cb2; // Will be bound to b3 register.
    ConstantBuffer<MyStruct> m_cb_unbounded[]; // Will be bound to b4 register, and will take over ALL5+ for Register Space 3.
    int m_intValue; // Declaring this variable, of Fundamental type, after an Unbounded Array is ok, as it is not a resource.
};