Version: 2022.1
Creating draw commands
The Rendering Statistics window

DOTS Instancing shaders

To render large instance counts efficiently, BRG uses a new shaderA program that runs on the GPU. More info
See in Glossary
instancing mode called DOTS Instancing. Every shader that BRG uses must support DOTS Instancing. In traditional instanced shaders, the shader is passed an array for each instanced property in a constant or uniform buffer, such that each element in each array contains the property value for a single instance in the draw. In DOTS Instanced shaders, Unity passes one 32-bit integer to the shader for each DOTS Instanced property. This 32-bit integer is called a metadata value. This integer can represent anything you want, but typically it represents an offset in the buffer from where the shader loads property data for the instance that the shader is rendering.

DOTS Instancing has many advantages compared to traditional instancing, including the following:

  • The instance data is stored in a GraphicsBuffer and remains persistent on the GPU, which means that Unity doesn’t need to set it up again each time it renders the instance. Setting up data only when an instance actually changes can significantly improve performance in cases where instance data changes rarely or not at all. This is much more efficient than traditional instancing, which requires an engine to set up all data for every instance every frame.
  • The process for setting up instance data is separate from setting up the draw call. This makes draw call setup lightweight and efficient. BRG makes this possible with a special fast path of the SRP Batcher that only does a minimal amount of work for each draw call. The responsibility for this work moves to you and gives you more control over what to render in each draw call.
  • The size of a draw call is no longer limited by how much instance data can fit in a constant or uniform buffer. This makes it possible for BRG to render larger instance counts with a single draw call.
    Note: The number of instance indices still limits the draw call size, since each index still requires some data. However, an index consumes far less memory than a full set of instanced properties which means many more instances can fit inside a constant or uniform buffer. For example, each index requires 16 byes so if the memory limit for a buffer on a particular platform is 64kb, 4096 indices can fit in the buffer.
  • If every instance uses the same value for a given property, it is possible to have all instances load the value from the same place in memory. This saves memory and the number of GPU cycles spent duplicating the value for each instance.

Supporting DOTS Instancing

To support DOTS Instancing, a shader needs to do the following:

  • Use shader model 4.5 or newer. Specify #pragma target 4.5 or higher.
  • Support the DOTS_INSTANCING_ON keyword. Declare this with #pragma multi_compile _ DOTS_INSTANCING_ON.
  • Declare at least one block of DOTS Instanced properties each of which has least one property. For more information, see Declaring DOTS Instanced properties.

Note: Shader Graphs and shaders that Unity provides in URP and HDRP support DOTS Instancing.

Declaring DOTS Instanced properties

To load instance data, such as transform matrices, the shader needs to define DOTS Instanced properties. Below is an example of a simple DOTS Instanced property block:

UNITY_DOTS_INSTANCING_START(MaterialPropertyMetadata)
    UNITY_DOTS_INSTANCED_PROP(float4, Color)
UNITY_DOTS_INSTANCING_END(MaterialPropertyMetadata)

To mark the beginning and end of the property block, use the UNITY_DOTS_INSTANCING_START and UNITY_DOTS_INSTANCING_END macros followed by the name of the block. The example uses the name MaterialPropertyMetadata. There are three allowed block names:

  • BuiltinPropertyMetadata
  • MaterialPropertyMetadata
  • UserPropertyMetadata

The shader can declare one of each, so a DOTS Instanced shader can have between zero and three of such blocks. Unity-defined shader code doesn’t use UserPropertyMetadata so this name is guaranteed to be free for you to use. URP and HDRP define BuiltinPropertyMetadata for every shader they provide and define MaterialPropertyMetadata for most of them too, so it’s best practice to use UserPropertyMetadata. Your custom shaders can use all three possible names, even all at once.

The block can contain any number of DOTS Instanced property definitions formatted like:

UNITY_DOTS_INSTANCED_PROP(PropertyType, PropertyName)

PropertyType can be any HLSL built-in type (like uint, float4, float4x4, or int2x4) except a bool vector, and PropertyName is the name of the DOTS Instanced property. DOTS Instanced properties are completely separate from regular material properties, and you can give them the same name as another regular material property. This is possible because the UNITY_DOTS_INSTANCED_PROP macro generates special constant names which Unity recognizes that don’t conflict with other property names. Shaders that Unity provides give DOTS Instanced properties the same names as regular material properties, but you don’t need to follow this convention.

Internally, Unity provides the shader with a 32-bit integer metadata value for every DOTS Instanced property the shader declares. Unity sets the metadata value when your code makes a BatchRendererGroup.AddBatch call to create the batch associated with the draw. The metadata value defaults to 0 if Unity doesn’t set it. The shader also has access to ByteAddressBuffer unity_DOTSInstanceData which Unity sets to the GraphicsBuffer you pass as an argument to BatchRendererGroup.AddBatch. This buffer is typically where the shader loads the instance data from. Multiple batches can share a single GraphicsBuffer, but it is also possible for each batch to use its own separate GraphicsBuffer for unity_DOTSInstanceData.

Note: Unity doesn’t provide any DOTS Instanced data automatically. It’s your responsibility to make sure that the unity_DOTSInstanceData buffer of each batch contains the correct data. Instance data must include many properties that are Unity normally provides for GameObjectsThe fundamental object in Unity scenes, which can represent characters, props, scenery, cameras, waypoints, and more. A GameObject’s functionality is defined by the Components attached to it. More info
See in Glossary
, such as transform matrices, light probeLight probes store information about how light passes through space in your scene. A collection of light probes arranged within a given space can improve lighting on moving objects and static LOD scenery within that space. More info
See in Glossary
coefficients, and lightmapA pre-rendered texture that contains the effects of light sources on static objects in the scene. Lightmaps are overlaid on top of scene geometry to create the effect of lighting. More info
See in Glossary
texture coordinates.

Accessing DOTS Instanced properties

To access DOTS Instanced properties, your shader can use one of the access macros that Unity provides. The access macros assume that instance data in unity_DOTSInstanceData uses the following layout:

  • The 31 least significant bits of the metadata value contain the byte address of the first instance in the batch within the unity_DOTSInstanceData buffer.
  • If the most significant bit of the metadata value is 0, every instance uses the value from instance index zero. This means each instance loads directly from the byte address in the metadata value. In this case, the buffer only needs to store a single value, instead of one value per instance.
  • If the most significant bit of the metadata value is 1, the address should contain an array where you can find the value for instance index instanceID using AddressOfInstance0 + sizeof(PropertyType) * instanceID. In this case, you should ensure that every rendered instance index has valid data in buffer. Otherwise, out-of-bounds access and undefined behavior can occur.

You can also set the the metadata value directly which is useful if you want to use a custom data source that doesn’t use the above layout, such as a texture.

Unity provides the following access macros:

Access macro Description
UNITY_ACCESS_DOTS_INSTANCED_PROP(PropertyType, PropertyName) Returns the value loaded from unity_DOTSInstanceData using the layout described above. Shaders that Unity provides use this version for DOTS Instanced built-in properties that don’t have a default value to fall back on.
UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_DEFAULT(PropertyType, PropertyName) Returns the same as UNITY_ACCESS_DOTS_INSTANCED_PROP, except if the most significant bit of the metadata value is zero, it returns a default value. The default value is the value of the regular material property with the same name as the DOTS Instanced property, which is why Shaders that Unity provides use the convention where DOTS Instanced properties have the same name as regular material properties. When using the default value, the access macro doesn’t access unity_DOTSInstanceData at all. Shaders that Unity provides use this access macro for DOTS Instanced material properties, so the loads fall back to the value set on the material.
UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_CUSTOM_DEFAULT(PropertyType, PropertyName, DefaultValue) Returns the same as UNITY_ACCESS_DOTS_INSTANCED_PROP unless the most significant bit of the metadata value is zero, in which case this macroreturns DefaultValue instead, and doesn’t access unity_DOTSInstanceData.
UNITY_DOTS_INSTANCED_METADATA_NAME(PropertyType, PropertyName) Returns the metadata value directly without accessing anything. This is useful for custom instance data loading schemes.

For an example of how to use these macros, see Access macro example.

Alongside the access macros, Unity provides shader functions that load the values of constants directly from the draw command data. Shaders that Unity provides use these functions.

Unity provides the following shader functions:

Shader function Description
LoadDOTSInstancedData_RenderingLayer Returns the renderingLayerMask for the draw command.
LoadDOTSInstancedData_MotionVectorsParams Returns the motion vector generation mode for the draw command. This is formatted as a float4, which is what Unity shaders expect.
LoadDOTSInstancedData_WorldTransformParams Returns whether to draw the instance with a flipped triangle winding. See FlipWinding.
LoadDOTSInstancedData_LightData Returns whether the sceneA Scene contains the environments and menus of your game. Think of each unique Scene file as a unique level. In each Scene, you place your environments, obstacles, and decorations, essentially designing and building your game in pieces. More info
See in Glossary
’s main Directional Light is active for the instance. The main light can be deactivated for multiple reasons, for example if the light already included in light maps.
LoadDOTSInstancedData_LODFade Returns the 8 bit crossfade value you set if the LODCrossFade flag is set. If the flag is not set, the return value is undefined.

Access macro examples

This section contains examples of the access macros that Unity provides and instructions for how to use these macros to access per-instance data and constant data.

Per-instance

In this example:

  • The metadata value for Color is 0x80001000.
  • The instance index is 5.
  • Data for instance 0 starts at address 0x1000.
  • Data for instance 5 is at address 0x1000 + 5 * sizeof(float4) = 0x1050

Because the most significant bit is already set, the accessor macros don’t load defaults. This means that c0, c1, and c2 will all have the same value, loaded from unity_DOTSInstanceData address 0x1050.

void ExamplePerInstance()
{
    // rawMetadataValue will contain 0x80001000
    uint rawMetadataValue = UNITY_DOTS_INSTANCED_METADATA_NAME(float4, Color);

    float4 c0 = UNITY_ACCESS_DOTS_INSTANCED_PROP(float4, Color);
    float4 c1 = UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_DEFAULT(float4, Color);
    float4 c2 = UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_CUSTOM_DEFAULT(float4, Color, float4(1, 2, 3, 4));
}

Constant

In this example:

  • The metadata value for Color is 0x00001000.
  • The instance index is 5.
  • Data for instance 0 starts at address 0x1000.
  • The most significant bit is not set so data for instance 5 is at the same address as instance 0.

Because the most significant bit is not set, the accessor macros that fall back to defaults don’t access unity_DOTSInstanceData. This means that:

  • c0 will contain the value from unity_DOTSInstanceData address 0x1000.
  • c1 will contain the value of the regular material property Color, and cause a compile error if the Color property doesn’t exist.
  • c2 will contain (1, 2, 3, 4) because that was passed as the explicit default value.
void ExampleConstant()
{
    // rawMetadataValue will contain 0x00001000
    uint rawMetadataValue = UNITY_DOTS_INSTANCED_METADATA_NAME(float4, Color);
    float4 c0 = UNITY_ACCESS_DOTS_INSTANCED_PROP(float4, Color);
    float4 c1 = UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_DEFAULT(float4, Color);
    float4 c2 = UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_CUSTOM_DEFAULT(float4, Color, float4(1, 2, 3, 4));
}

Details

It is best practice to initialize the first 64 bytes of all unity_DOTSInstanceData buffers to zero and leave them unused. This is because the default metadata value that Unity uses for all metadata values not specified during batch creation is zero. Specifically, when a shader loads a zero metadata value from the UNITY_ACCESS_DOTS_INSTANCED_PROPmacro, the shader loads this value from the address zero because the instance index will be disregarded. Ensuring that the first 64 bytes, which is the size of the largest value type (a float4x4 matrix), are zeroes guarantees that such loads predictably return a result of zero. Otherwise, the shader could load something unpredictable, depending on what happens to be located at address zero.

When using DOTS Instancing, Shader Graphs and Shaders that Unity provides use a special convention for transform matrices. To save GPU memory and bandwidth, they store these matrices using only 12 floats instead of the full 16, because four floats are always constant. These shaders expect floats formatted in such a way that the x, y, and z of each column in the matrix are stored in order. In other words, the first three floats are the x, y, and z of the first column, the next three floats are the x, y, and z of the second column, and so on. The matrices don’t store the w element of each column. The transform matrices this affects are:

  • unity_ObjectToWorld
  • unity_WorldToObject
  • unity_MatrixPreviousM
  • unity_MatrixPreviousMI

The following code sample includes a struct that converts regular four-by-four matrices into the 12 floats convention.

struct PackedMatrix
{
    public float c0x;
    public float c0y;
    public float c0z;
    public float c1x;
    public float c1y;
    public float c1z;
    public float c2x;
    public float c2y;
    public float c2z;
    public float c3x;
    public float c3y;
    public float c3z;

    public PackedMatrix(Matrix4x4 m)
    {
        c0x = m.m00;
        c0y = m.m10;
        c0z = m.m20;
        c1x = m.m01;
        c1y = m.m11;
        c1z = m.m21;
        c2x = m.m02;
        c2y = m.m12;
        c2z = m.m22;
        c3x = m.m03;
        c3y = m.m13;
        c3z = m.m23;
    }
}
Creating draw commands
The Rendering Statistics window