If you turned on GPU Resident Drawer and saw this error in the console, you’re in the right place.
A BatchDrawCommand is using the pass [PassName] from the shader [ShaderName]
which does not define a DOTS_INSTANCING_ON variant.
“Something’s wrong, but I have no idea what” — that’s exactly where you are. To use GPU Resident Drawer with custom shaders, there’s one mandatory hurdle: DOTS Instancing support.
The name might suggest you need the Entities package, but that’s a common misconception. DOTS Instancing is a shader-level mechanism for passing per-instance data, and it works perfectly with standard GameObject-based projects.
Table of Contents
- What Is DOTS Instancing?
- Adding DOTS Instancing to a Custom Shader
- Step 1: Required pragma Declarations
- Step 2: Declare the Property Block
- Step 3: SRP Batcher Compatibility
- Step 4: Propagate Instance ID
- Project Settings You Must Verify
- Verifying Compatibility
- Common Pitfalls
What Is DOTS Instancing?
The two approaches are easy to confuse, but the fundamental difference is where the data lives.
With traditional GPU instancing (UNITY_INSTANCING_BUFFER_START), the CPU packs per-instance data (transforms, material properties, etc.) into a Constant Buffer every frame and uploads it to the GPU. There’s a hard limit of 1,023 instances per batch (Unity’s internal cap — this can be lower depending on the number of properties per instance within the 64KB cbuffer limit), and the data transfer itself is CPU overhead.
DOTS Instancing works differently. Instance data resides in a GraphicsBuffer (ByteAddressBuffer) in GPU memory, and the shader only receives a single 32-bit metadata value that tells it where to read in that buffer. The CPU only needs to upload data when it actually changes — no per-frame re-upload required.
| Traditional GPU Instancing | DOTS Instancing | |
|---|---|---|
| Instance data location | CPU → Constant Buffer (per frame) | GPU-resident GraphicsBuffer |
| What gets sent to the shader | Full property array | One 32-bit metadata value |
| Batch size limit | ~500 (64KB cbuffer limit) | Thousands, effectively unlimited |
| SRP Batcher compatible | No | Yes (simultaneously) |
| Declaration macro | UNITY_INSTANCING_BUFFER_START |
UNITY_DOTS_INSTANCING_START |
GPU Resident Drawer internally uses the BatchRendererGroup (BRG) API, and BRG only supports DOTS Instancing. Shaders written for traditional GPU instancing simply won’t work with BRG.
One more thing to keep in mind: DOTS Instancing shaders must also be SRP Batcher compatible. You need to satisfy the SRP Batcher requirement (UnityPerMaterial cbuffer declaration) at the same time. The pattern that satisfies both simultaneously is the core of this post.
Adding DOTS Instancing to a Custom Shader
Step 1: Required pragma Declarations
Every Pass must include these three lines.
#pragma exclude_renderers gles gles3 glcore // Exclude platforms without SM 4.5
#pragma target 4.5 // Minimum shader model for DOTS Instancing
#pragma multi_compile _ DOTS_INSTANCING_ON // Generate DOTS Instancing variant
Adding #pragma multi_compile_instancing alongside generates traditional GPU instancing variants too. Supporting both paths in one shader is the standard approach.
#pragma multi_compile_instancing
#pragma instancing_options renderinglayer
#pragma multi_compile _ DOTS_INSTANCING_ON
Important: These pragmas must be added to every Pass in the shader. If you add them to the Forward pass but forget ShadowCaster or DepthOnly, those passes will throw errors. The error message specifies the pass name, so it’s easy to identify.
Step 2: Declare the DOTS Instancing Property Block
Declare the property block inside an #ifdef UNITY_DOTS_INSTANCING_ENABLED guard.
#ifdef UNITY_DOTS_INSTANCING_ENABLED
UNITY_DOTS_INSTANCING_START(UserPropertyMetadata)
UNITY_DOTS_INSTANCED_PROP(float4, _BaseColor)
UNITY_DOTS_INSTANCED_PROP(float, _Smoothness)
UNITY_DOTS_INSTANCING_END(UserPropertyMetadata)
#endif
Three block names are available:
– BuiltinPropertyMetadata — Unity internal use only
– MaterialPropertyMetadata — used by URP/HDRP built-in shaders
– UserPropertyMetadata — use this for custom shaders
Using UserPropertyMetadata avoids any naming conflicts with Unity’s internal shader code.
There are three accessor macro variants; in practice, WITH_DEFAULT is the safe default. BRG reserves the first 64 bytes of the buffer as zero, so if a property hasn’t been set, it returns 0. Getting black from _BaseColor is much easier to diagnose than garbage values.
| Macro | Behavior | When to Use |
|---|---|---|
UNITY_ACCESS_DOTS_INSTANCED_PROP |
Read instance data directly | When data is guaranteed to exist |
UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_DEFAULT |
Returns 0 if no data | Most cases — use this as default |
UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_CUSTOM_DEFAULT |
Returns specified value if no data | When you need a non-zero default |
Step 3: Maintain SRP Batcher Compatibility + Unify Access
This is the most important part. To support both the DOTS path and the non-DOTS path (SRP Batcher), declare both the UnityPerMaterial cbuffer and the DOTS block simultaneously, then use the #define trick to unify the shader body.
// cbuffer for SRP Batcher — required for SRP Batcher compatibility
CBUFFER_START(UnityPerMaterial)
float4 _BaseMap_ST;
float4 _BaseColor;
float _Smoothness;
CBUFFER_END
// In DOTS Instancing path, override to read from GPU buffer instead of cbuffer
#ifdef UNITY_DOTS_INSTANCING_ENABLED
UNITY_DOTS_INSTANCING_START(UserPropertyMetadata)
UNITY_DOTS_INSTANCED_PROP(float4, _BaseColor)
UNITY_DOTS_INSTANCED_PROP(float, _Smoothness)
UNITY_DOTS_INSTANCING_END(UserPropertyMetadata)
// Leave shader body as-is; the preprocessor swaps the read path
#define _BaseColor UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_DEFAULT(float4, _BaseColor)
#define _Smoothness UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_DEFAULT(float, _Smoothness)
#endif
Thanks to the #define overrides, the vertex/fragment shader body can use _BaseColor as-is. The preprocessor automatically switches between cbuffer reads and GPU buffer reads depending on which path is being compiled.
Step 4: Propagate Instance ID
Add these macros to your structs and functions.
struct Attributes
{
float4 positionOS : POSITION;
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID // Add instance ID to vertex input
};
struct Varyings
{
float4 positionCS : SV_POSITION;
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID // Required for DOTS property access in fragment
};
Varyings Vert(Attributes input)
{
Varyings output;
UNITY_SETUP_INSTANCE_ID(input); // Initialize unity_InstanceID
UNITY_TRANSFER_INSTANCE_ID(input, output); // Pass to fragment
// ...
}
half4 Frag(Varyings input) : SV_Target
{
UNITY_SETUP_INSTANCE_ID(input); // Required before reading DOTS properties in fragment
return _BaseColor; // Reads from DOTS buffer or cbuffer via #define
}
Complete Minimal URP Unlit Shader
Here’s a fully working shader combining everything above.
Shader "Custom/UnlitDotsInstanced"
{
Properties
{
_BaseMap ("Base Texture", 2D) = "white" {}
_BaseColor ("Base Color", Color) = (1, 1, 1, 1)
}
SubShader
{
Tags { "RenderPipeline"="UniversalPipeline" "Queue"="Geometry" }
Pass
{
Name "Forward"
Tags { "LightMode"="UniversalForward" }
HLSLPROGRAM
#pragma exclude_renderers gles gles3 glcore
#pragma target 4.5
#pragma vertex Vert
#pragma fragment Frag
#pragma multi_compile_instancing
#pragma instancing_options renderinglayer
#pragma multi_compile _ DOTS_INSTANCING_ON
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
struct Attributes
{
float4 positionOS : POSITION;
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Varyings
{
float4 positionCS : SV_POSITION;
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
CBUFFER_START(UnityPerMaterial)
float4 _BaseMap_ST;
float4 _BaseColor;
CBUFFER_END
#ifdef UNITY_DOTS_INSTANCING_ENABLED
UNITY_DOTS_INSTANCING_START(UserPropertyMetadata)
UNITY_DOTS_INSTANCED_PROP(float4, _BaseColor)
UNITY_DOTS_INSTANCING_END(UserPropertyMetadata)
#define _BaseColor UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_DEFAULT(float4, _BaseColor)
#endif
TEXTURE2D(_BaseMap);
SAMPLER(sampler_BaseMap);
Varyings Vert(Attributes input)
{
Varyings output;
UNITY_SETUP_INSTANCE_ID(input);
UNITY_TRANSFER_INSTANCE_ID(input, output);
output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
output.uv = TRANSFORM_TEX(input.uv, _BaseMap);
return output;
}
half4 Frag(Varyings input) : SV_Target
{
UNITY_SETUP_INSTANCE_ID(input);
half4 tex = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv);
return tex * _BaseColor;
}
ENDHLSL
}
}
}
Project Settings You Must Verify
If everything works in the Editor but breaks in a build, this setting is almost certainly the culprit.
Edit > Project Settings > Graphics > Shader Stripping > BatchRendererGroup Variants
If this is set to Strip All or Strip Unused, all DOTS_INSTANCING_ON variants will be removed at build time. This is the classic pattern where everything looks fine in the Editor but fails in a build.
Set it to Keep All.
Verifying Compatibility
Checking Batching with Frame Debugger
Open Window > Analysis > Frame Debugger and look at the draw call list. Objects correctly processed by GPU Resident Drawer will have their draw calls grouped under “Hybrid Batch Group”. If your custom shader objects aren’t there, they’ve been excluded from BRG processing.
Checking GPU Occlusion Culling with Rendering Debugger
Window > Analysis > Rendering Debugger > GPU Resident Drawer tab
Enabling the Occlusion Test Overlay draws an overlay in the Scene/Game view showing which objects are subject to GPU occlusion culling. If your custom shader objects don’t appear in this overlay, they’ve been excluded from BRG.
SRP Batcher Compatibility in the Inspector
Select a Material in the Inspector and check the bottom of the panel for “SRP Batcher: compatible” or “SRP Batcher: not compatible”. DOTS Instancing shaders must simultaneously satisfy SRP Batcher requirements, so “not compatible” means you need to review your UnityPerMaterial cbuffer declaration.
Common Pitfalls
These are patterns I’ve repeatedly seen — both in my own work with custom shaders and while helping client projects adopt GPU Resident Drawer.
Adding pragmas to only some passes
A common mistake is adding the pragmas to the Forward pass but missing ShadowCaster and DepthOnly. The error message specifies the pass name, so check that first.
Using MaterialPropertyBlock
The moment you call renderer.SetPropertyBlock(...) from a script, that Renderer is entirely excluded from GPU Resident Drawer — even changing a single property has this effect. If you need per-instance properties, you must pass them through the DOTS Instancing buffer.
Using ShaderGraph
ShaderGraph’s Sprite Lit / Sprite Unlit targets don’t generate DOTS_INSTANCING_ON variants. You must use URP Lit or URP Unlit targets.
Declaring properties with UNITY_INSTANCING_BUFFER_START
#pragma multi_compile_instancing and #pragma multi_compile _ DOTS_INSTANCING_ON can coexist in the same shader. The problem isn’t the pragmas — it’s declaring instancing properties with UNITY_INSTANCING_BUFFER_START. This breaks the UnityPerMaterial cbuffer structure and eliminates SRP Batcher compatibility. Property declarations must use the DOTS approach (UNITY_DOTS_INSTANCING_START) exclusively.
Using Light Probe Proxy Volumes
Objects using LPPV are excluded from GPU Resident Drawer. I covered the reason in detail in the previous post, GPU Resident Drawer Deep Dive.
Closing
I remember spending a frustrating amount of time debugging GPU Resident Drawer compatibility on what seemed like a simple custom shader. The structure itself is straightforward: pragma declarations, property block, #define overrides — once you internalize this flow, retrofitting existing shaders is much faster than you’d expect.
That said, if your project has a heavy dependency on MaterialPropertyBlock, that needs to be addressed first. Getting the full performance benefit of GPU Resident Drawer requires understanding the complete flow of passing instance data through the DOTS Instancing buffer.
If you want to understand how GPU Resident Drawer works under the hood, I’d recommend reading the previous post, GPU Resident Drawer Deep Dive, first.
← Previous: GPU Resident Drawer Internals — How Unity 6 Cuts Draw Calls
Further Reading
Resources I personally verified while writing this post.
-
DOTS Instancing shaders in URP — Unity 6 Manual
The official page covering the full DOTS Instancing concept and its relationship with BRG. Background material for the “What Is It” section. -
Declare DOTS Instancing properties in a custom shader — Unity 6 Manual
Where I verified the three block name options (BuiltinPropertyMetadata/MaterialPropertyMetadata/UserPropertyMetadata) and their differences. -
Access DOTS Instancing properties in a custom shader — Unity 6 Manual
The page covering behavioral differences between the threeUNITY_ACCESS_DOTS_INSTANCED_PROPmacro variants, and whyWITH_DEFAULTis the safe choice. -
Support DOTS Instancing in a custom shader in URP — Unity 6 Manual
Covers how to satisfy both SRP Batcher and DOTS Instancing in URP. The official basis for the#defineoverride pattern. -
Best practice for DOTS Instancing shaders in URP — Unity 6 Manual
Covers Shader Stripping settings, MaterialPropertyBlock limitations, and other “breaks only in builds” cases. -
Enable the GPU Resident Drawer in URP — Unity 6 Manual
GPU Resident Drawer activation requirements and excluded objects (LPPV, etc.). -
Writing custom shaders for the BatchRendererGroup API — Unity 6 Manual
For understanding how shaders work at the BRG level. Good companion reading with the previous post if you’re interested in GPU Resident Drawer internals.