๐จ Lesson 2.4: Draw Calls & Batching โ SRP Batcher, GPU Instancing & the GPU Resident Drawer
You can write perfectly garbage-free C# and still miss your frame budget โ because the bottleneck moved to the other side of the frame. Every object on screen has to be described to the graphics card, and each of those instructions costs CPU time on the render thread. Too many of them and the CPU spends the whole frame talking to the GPU instead of running your game. This lesson is about draw calls: what they cost, and the three batching systems โ the SRP Batcher, GPU Instancing, and Unity 6's GPU Resident Drawer โ that collapse thousands of them into a handful.
๐ฏ Learning Objectives
By the end of this lesson, you will be able to:
- Explain what a draw call and a SetPass call are, and why SetPass calls are the expensive part
- Describe how the SRP Batcher speeds up rendering with persistent per-material GPU buffers
- Use GPU Instancing with a
MaterialPropertyBlockto draw many copies of one mesh in a single call - Apply static batching and understand why legacy dynamic batching is mostly gone
- Explain the Unity 6 GPU Resident Drawer and BatchRendererGroup for automatic GPU-driven rendering
- Diagnose batching with the Frame Debugger and the Rendering profiler module
Estimated Time: 70 minutes ยท Prerequisite: Lesson 2.1 (the Rendering profiler module) and URP basics from Unity Intermediate
In This Lesson
What a Draw Call Costs
A draw call is one command from the CPU to the GPU: "draw this mesh, with this state, now." On its own a draw call is cheap. The expensive part is the state change that usually comes with it โ telling the GPU which shader, which textures, which material properties to use before the draw. Unity bundles that state setup into what the Profiler calls a SetPass call.
Every SetPass call means the CPU on the render thread has to bind a shader, upload constants, and swap textures โ real work in the graphics driver. A scene of 2,000 objects each with its own material can mean 2,000 SetPass calls, and that's how a game becomes CPU-bound on the render thread (the exact symptom you learned to spot in Lesson 2.1: main/render threads packed, GPU sitting idle).
๐ Definition โ draw call vs SetPass call
A draw call issues geometry to the GPU. A SetPass call is the more expensive event of changing render state (shader + material properties) before drawing. Many draw calls can share one SetPass if they use the same state. In the Rendering profiler module you watch both โ but SetPass calls are the number that usually predicts your CPU render cost, because state changes are what stall the driver.
๐ก It's a CPU problem, not a GPU one. Counter-intuitively, "too many draw calls" almost never means the GPU can't draw the triangles โ modern GPUs eat millions of triangles for breakfast. It means the CPU can't describe them fast enough. Batching exists to reduce the CPU's talking, not the GPU's drawing.
The Idea of Batching
Batching is any technique that lets the CPU describe many objects with fewer, cheaper instructions โ by grouping objects that share render state so the expensive state setup happens once instead of per object. Unity 6 / URP gives you three cooperating mechanisms, each suited to a different situation:
๐๏ธ SRP Batcher
Speeds up drawing many objects that share the same shader (even with different material values) by keeping their material data resident on the GPU. The default, always-on workhorse.
๐ฅ GPU Instancing
Draws many copies of the same mesh + material in a single call, with per-copy data (position, colour) supplied as an array. Ideal for crowds, foliage, bullets.
๐๏ธ GPU Resident Drawer
Unity 6's automatic, GPU-driven pipeline built on BatchRendererGroup. It moves culling and draw submission onto the GPU for huge static/instanced worlds โ batching you get by ticking a box.
Here is the whole point of the lesson in one picture: the same eight objects submitted three ways. Watch the SetPass count collapse from left to right.
The SRP Batcher
The SRP Batcher is URP's default batching system, and it works differently from old-school batching. It doesn't merge meshes. Instead, it exploits a simple fact: the slow part of a SetPass call is re-uploading material data to the GPU. So the SRP Batcher keeps each material's shader constants in a persistent CBUFFER that lives on the GPU and doesn't get re-uploaded every frame. As long as consecutive objects use the same shader variant, the CPU skips the expensive material re-bind and just points the GPU at the right buffer offset โ a much cheaper per-object cost.
The critical and often-misunderstood detail: the SRP Batcher batches by shader variant, not by material. A hundred objects using a hundred different materials that all share one shader still batch beautifully โ they differ only in the values sitting in that persistent buffer. What breaks the batch is a different shader, because that forces a real state change.
โ ๏ธ What makes a shader SRP-Batcher-compatible
For a shader to qualify, its per-material properties must be declared inside a single CBUFFER named UnityPerMaterial, and per-object properties inside UnityPerDraw. All URP Lit/Unlit shaders and everything Shader Graph produces are already compatible. Hand-written HLSL shaders (Module 5) must follow this layout โ the Inspector's shader panel tells you "SRP Batcher: compatible" or lists why not. If your custom shader isn't compatible, it silently drops out of batching and tanks your SetPass count.
You don't enable the SRP Batcher per object โ it's on by default in the URP asset. Your job is to not break it: keep to compatible shaders, and prefer many materials sharing few shaders over many distinct shaders. This is the single biggest rendering-performance lever in a typical URP project, and it costs you nothing but discipline.
GPU Instancing
When you have lots of the same thing โ 500 identical trees, a swarm of identical bullets, a field of grass โ GPU Instancing beats even the SRP Batcher. It draws every copy of one mesh + one material in a single instanced draw call, handing the GPU an array of per-instance data (each copy's transform, and optionally colour or other properties) that the shader reads to place and tint each instance.
To use it in the traditional way: tick Enable GPU Instancing on the material, ensure every instance shares that mesh and material, and vary per-instance data through a MaterialPropertyBlock (which โ importantly โ does not create a new material instance and so doesn't break instancing). The most direct API is Graphics.RenderMeshInstanced / DrawMeshInstanced, where you supply the transforms yourself:
using UnityEngine;
public class InstancedField : MonoBehaviour
{
[SerializeField] Mesh _mesh;
[SerializeField] Material _material; // "Enable GPU Instancing" ticked
Matrix4x4[] _matrices = new Matrix4x4[1023]; // max 1023 per batch
MaterialPropertyBlock _mpb;
void Start()
{
_mpb = new MaterialPropertyBlock();
var colors = new Vector4[1023];
for (int i = 0; i < _matrices.Length; i++)
{
Vector3 pos = new Vector3(i % 32, 0, i / 32) * 1.5f;
_matrices[i] = Matrix4x4.TRS(pos, Quaternion.identity, Vector3.one);
colors[i] = Random.ColorHSV();
}
_mpb.SetVectorArray("_BaseColor", colors); // per-instance color, one buffer
}
void Update()
{
// One instanced draw for up to 1023 copies โ not 1023 draw calls.
var rp = new RenderParams(_material) { matProps = _mpb };
Graphics.RenderMeshInstanced(rp, _mesh, 0, _matrices);
}
}
โ SRP Batcher vs GPU Instancing โ which when?
SRP Batcher wins for many objects that share a shader but have different meshes (a whole level's props) โ it's automatic and needs no identical geometry. GPU Instancing wins for many objects with the same mesh and material (crowds, foliage, projectiles). They don't stack on the same object โ an object is drawn by one path or the other โ so pick per use case: varied static scenery โ lean on the SRP Batcher; thousands of clones โ instancing.
Static & the Death of Dynamic Batching
Two older systems remain worth knowing:
- Static batching โ for objects marked Static that never move, Unity pre-combines their meshes into a shared buffer at build/load time. They still need the same material to batch the draws, but the geometry is merged once so the CPU submits far fewer, larger draws. It trades memory (the combined mesh) for CPU time, and it's a solid win for immovable level geometry.
- Legacy dynamic batching โ an old system that merged small moving meshes on the CPU every frame. It was always limited (roughly < 300 vertices, same material) and its per-frame CPU merging cost often exceeded the savings. In modern URP with the SRP Batcher and instancing doing a better job, dynamic batching is largely deprecated and off by default โ you'll rarely, if ever, turn it on. Mention it only to recognize it in old projects and tutorials.
๐ The modern hierarchy. Reach for tools in this order: keep shaders SRP-Batcher-compatible (free, broad), mark immovable geometry Static (cheap win), use GPU Instancing for many identical objects, and โ new in Unity 6 โ turn on the GPU Resident Drawer for large instanced/static worlds. Legacy dynamic batching sits at the bottom of the list, usually untouched.
The GPU Resident Drawer
Unity 6 introduces the GPU Resident Drawer, a GPU-driven rendering path that pushes the work of culling and draw submission off the CPU and onto the GPU. Instead of the CPU walking every renderer each frame to decide what's visible and issue draws, the GPU Resident Drawer uploads the scene's instance data once and lets the GPU cull and draw it โ dramatically cutting the CPU render-thread cost for scenes with many static or instanced objects.
It's built on BatchRendererGroup (BRG), the low-level API that also powers Entities Graphics (the system that rendered the 25,600 entities you saw in the Module 4 gold lesson). BRG lets code hand the renderer big batches of instances with GPU-resident data; the GPU Resident Drawer is Unity wiring that API up automatically for your normal MeshRenderer-based scene.
You enable it in the URP Asset โ Rendering โ GPU Resident Drawer (set it to Instanced Drawing), with a companion setting for GPU occlusion culling. Objects must use SRP-Batcher-compatible shaders (there's the SRP Batcher again as the foundation) and be eligible (typically static or non-deforming). Turn it on for a dense scene and watch the Rendering module's CPU render cost fall as the GPU takes over the bookkeeping.
๐ Definition โ GPU-driven rendering
GPU-driven rendering flips the traditional model. Normally the CPU decides what to draw (culling) and issues each draw; the GPU only executes them. In a GPU-driven pipeline, the scene lives in GPU buffers and the GPU itself culls and generates its own draw commands. The CPU's job shrinks to "here's the world, go" โ which is exactly what a CPU-bound-on-rendering game needs. The GPU Resident Drawer is Unity 6's turnkey door into this world; BatchRendererGroup is the door you'd open by hand.
Diagnosing with the Frame Debugger
You can't optimize batching you can't see. Two tools tell you the truth:
- The Rendering profiler module (Lesson 2.1) gives you the running counts: SetPass calls, Draw calls, and Batches. Watch SetPass calls in particular โ if it's in the thousands, you have a batching problem worth chasing.
- The Frame Debugger (Window โ Analysis โ Frame Debugger) is the microscope. Click Enable and it freezes a frame and lists every draw event in order, letting you step through them one at a time and watch the frame build up on screen. For each event it shows what was drawn, with which shader and material, and โ crucially โ why the previous batch broke ("Objects have different materials," "different shader keywords," and so on).
That "why the batch broke" text is gold. It turns batching from guesswork into a checklist: the Frame Debugger names the exact reason two objects didn't batch, you fix that reason (share the shader, use a property block instead of a material variant, mark it static), and you re-check the SetPass count. That loop โ read the count, find the break, fix the cause, re-measure โ is precisely the workflow you'll run in the Lesson 2.5 mini-project on the over-materialed stress scene.
โ ๏ธ Common batch-breakers to look for
- Material variants. Reading
renderer.material(singular) at runtime clones the material, creating a unique instance that breaks batching. Userenderer.sharedMaterialor aMaterialPropertyBlockfor per-object tweaks. - Different shaders. Even visually similar materials on different shaders can't share an SRP batch. Consolidate onto fewer shaders.
- Incompatible custom shaders. A hand-written shader missing the
UnityPerMaterialCBUFFER drops out of the SRP Batcher entirely (see Lesson 5.2). - Multi-pass / different keywords. Different shader keyword combinations are different variants and break the batch.
Hands-on Challenge
๐๏ธ Exercise 1: Watch batches break and heal
Objective: See the SetPass count respond to your choices, using the Frame Debugger.
- Make a scene with ~200 cubes, all using one URP Lit material. Open the Rendering profiler module and note the SetPass count โ it should be low (the SRP Batcher is working).
- In a script, set each cube's colour via
renderer.material.color = ...(the singular, cloning form). Re-check the count โ it jumps, because each cube now has a unique material instance. - Open the Frame Debugger, step through, and read the reason the batches broke.
- Switch to a
MaterialPropertyBlock(renderer.SetPropertyBlock) for the colour instead. Re-check โ the count drops back down.
โ What you should observe
renderer.material.color clones the material per cube, so the SRP Batcher sees 200 different materials โ but since they share a shader they may still batch on the SRP path; the bigger break comes from the cloned instances and any resulting state differences, which the Frame Debugger spells out ("Objects have different materials"). The MaterialPropertyBlock version varies colour without cloning the material, so batching holds and the SetPass count stays low. The takeaway: per-object variation belongs in a property block, never in .material.
๐๏ธ Exercise 2: Instancing a thousand clones
Take the same 1,000-identical-object scene and draw it two ways: once as 1,000 separate GameObjects, once via Graphics.RenderMeshInstanced with the code above. Compare SetPass/draw counts and CPU render time in the Profiler.
โ What you should observe
The 1,000 GameObjects submit far more draw work (even batched, there's per-object CPU overhead), while the instanced version collapses to roughly one SetPass and a handful of instanced draws (batches of up to 1,023). The Rendering module's draw-call count and the render thread's CPU time both drop sharply. This is why crowds, foliage, and projectiles use instancing โ and it's the same mechanism (via BRG) that the GPU Resident Drawer and Entities Graphics use automatically at even larger scale.
๐ฏ Quick Quiz
Question 1: Why do "too many draw calls" usually hurt performance?
Question 2: The SRP Batcher batches by what?
Question 3: You want to draw 800 identical rocks with per-rock colour without breaking batching. What do you use?
Question 4: What is the Unity 6 GPU Resident Drawer's main benefit?
Summary
๐ Key Takeaways
- A draw call issues geometry; a SetPass call changes render state and is the expensive part. Too many SetPass calls makes you CPU-bound on the render thread.
- The SRP Batcher (default, always-on) keeps material data in a persistent GPU CBUFFER and batches by shader variant โ many materials, one shader, still batch. Keep shaders compatible.
- GPU Instancing draws many copies of one mesh+material in a single instanced call; vary per-instance data via a
MaterialPropertyBlock, never by cloning.material. - Static batching pre-merges immovable geometry; legacy dynamic batching is deprecated and off by default.
- The Unity 6 GPU Resident Drawer (on
BatchRendererGroup) moves culling and submission onto the GPU for large static/instanced worlds โ turnkey GPU-driven rendering. - Diagnose with the Rendering profiler module (SetPass count) and the Frame Debugger, which names exactly why each batch broke.
๐ What's Next?
You've now met every pillar of performance work in this module: measuring with the Profiler, hunting memory leaks, killing garbage, and collapsing draw calls. Time to use them together on something deliberately broken. In Lesson 2.5: Profiling & Fixing a Stress Scene, you'll profile a slow scene, find three concrete problems โ per-frame allocations, hundreds of unbatched materials, and an expensive per-frame scene query โ fix each, and re-measure with before/after numbers.
๐ Fewer SetPass calls, faster frames
Share shaders, instance the clones, mark the static geometry static, and let Unity 6's GPU-driven path carry the dense scenes. The Frame Debugger tells you exactly where you're leaving performance on the table.