Is there a way to batch meshes that are drawn with CommandBuffer.DrawMesh?

Hi there, I am trying to draw a great number of meshes on screen with CommandBuffer.DrawMesh, in order to not have to create a GameObject for each mesh (and yes, it has to be directly with the command buffer).

I think Unity has made a great improvement when provided us with lower level accessibility like it is the case of the command buffer. However, drawing meshes that way has one very big pitfall: meshes are not batched anymore, neither statically, nor dynamically. Hence, and due to the scarcity of articles and tutorials out there on how to achieve more advanced endeavors with the command buffer DrawMesh command, I came to ask you experts how one could implement batching in such a case.

Be it high or low level, I am fine learning either. Many thanks for your time.

1 Like

Would like to know the same thing.

It seems silly that it wouldn’t batch the calls together when you are drawing the same mesh with the same material over and over.

I’m also interested in this - I’d like to have more direct control over batching in some cases. It would be super cool if I could call DrawMesh and give it an enumerable of Transforms or something like that. There are probably reasons that it would have to be more sophisticated than that though :\

Even if it is not possible to batch meshes, being able to call multiple times DrawMesh with the same material without having SetPass calls increasing would be awesome, because right now calling DrawMesh on a commandbuffer always triggers a new SetPass

I’m interested in this. Anyone has information about batches with command buffers?

Also interested. But I guess you have to do some kind of manual batching yourself, if that’s even possible.

Bumping this, also very interested. Sorry for the ping but, maybe @Aras would know?

Use CommandBuffer.DrawMeshInstanced if your platform supports it. Should work out better in performance terms where it is available.

1 Like

Any news on this topic? Unity added new SRP Batcher and it works great. But again there is no way to use it with Command Buffers. So, no custom render pipeleline possible.

Command Buffers + Dynamic Batching = No
Command Buffers + SRP Batching = No

It’s really sad that Unity continues to ship half-baked technologies.

3 Likes

Also still interested in this!

EDIT: Did more research and testing.

My conclusions are that:

  • CommandBuffers understandably do not support dynamic batching by design – Graphics.DrawMesh does not either --, so reducing the Batch/DrawCall count is not possible unless you explicitly use DrawMeshInstanced.

  • However, there seems to be no good reason why CommandBuffers need to call SetPass when calling DrawMesh multiple times in a row with the same material, and that seems like a bug.

  • [PerRendererData] or adding proper mesh instancing support to your shader/material does not fix the issue. Even using a single pass material with this setup and no MaterialPropertyBlock will cause multiple SetPass calls to occur.

I’d be tempted to report this as a bug but I’m not 100% confident in my understanding of the graphics pipeline, I’d appreciate if someone chimed in and confirmed my hunch.

When using CommandBuffer/Graphics.DrawMesh, you’re doing rendering outside of the built-in render loop (the one that draws Mesh Renderers, particles, trails, etc). So any of those rendering calls aren’t going to be batched, because Unity doesn’t know anything about them. The only way to do so is to use Monobehaviours with Renderer components, but that defeats the purpose of drawing through script :wink:

DrawMeshInstanced allows you to draw a batch (max 1023) of meshes through an array of matrices, paired with a single mesh, material and MaterialPropertyBlock (properties have to be arrays though). This does mean you have to write your own batching system, in order to pair mesh/material combinations into batches. This all hinges on GPU Instancing, which the shaders need to support, so isn’t dynamic batching per se.

Note that the matrices array has to have a constant size of 1023, failing to do so will crash Unity instantly. The same counts for any arrays you set on the MaterialPropertyBlock. A little undocumented caveat…

Depending on what Unity verison you’re using, you should use BatchRendererGroup. From what I can tell, it’s designed to replace Graphics.DrawMesh/DrawMeshInstanced etc.

From a Monobehaviour, you create a new “BatchRendererGroup” OnEnable/Awake. (Works from [ExecuteAlways] editor scripts too, as long as you pass the correct flag to the sceneViewCullingMask in the constructor)

Then you add “Batches” to the BatchRendererGroup, which will look similar to Graphics.DrawMesh. Eg you pass in a Mesh, Material, Bounds, Lighting flags, etc.

The last thing is a custom culling callback, where you can do culling to determine the visibility of each mesh. Though it can be a bit tricky to get the hang of, it does allow you do use Jobs for the culling which means it can be very efficient.

There’s a couple of examples online of using it, although in other languages. Though google translate was helpful enough for me to figure it out. I don’t have any simple sample code to share though unfortunately.

Thanks for chiming in, but as I mentioned in my prior post I’m aware that Unity can’t possibly do automatic draw call batching for us here. I’m wondering about the SetPass calls, which seem to be useless, because Unity is calling SetPass even when drawing with the same material multiple times in a row. Because the CB knows about that, it could (and I guess should) skip SetPass on all subsequent draw calls that haven’t changed the material. This is, I believe, the behaviour you would get when using Graphics.DrawMesh.

In other words: CommandBuffer.DrawMesh doesn’t benefit from the same basic optimization as Graphics.DrawMesh (calling SetPass only once when drawing the same material multiple times). This is what I believe is a bug. Does that make more sense?

Thanks for the suggestion, I see you also chimed in in another thread, but my question is the same: how does BatchRendererGroup even work with CommandBuffers? It doesn’t seem suited for that purpose at all, it seems to be made for ECS and SRPs.

Ah, I see, I was addressing the OP’s question, and failed to look at the dates. The stats window can be finicky, I believe it only works correctly for the built-in render loop. DrawMeshInstancedIndirect calls don’t affect it at all for example. When using the SRP batcher sometimes it reports a negative amount of drawcalls.

I think if you are calling DrawMesh, you are always including a pass index, so it’s possible this internally amounts to a SetPass call. If the previous pass is the same as the current, this probably isn’t necessary, logically speaking. But for all Unity knows, you are setting a different pass. If this behaviour is different between Graphics and CommandBuffer it could be a bug, or some logical explanation eludes us @richardkettlewell ?

I don’t know if this actually affects performance or not, it could be changing the render state, or it’s simply (re)setting a value over and over again.

According to this 2020 guide, minimizing SetPass calls does have a significant positive performance impact:
https://thegamedev.guru/unity-performance/draw-call-optimization/#tab-con-19

I just tested the same render (about 8k cubes, same material) with Graphics.DrawMesh instead of CommandBuffer.DrawMesh.

Unity 2019.4 LTS.

Graphics.DrawMesh:

  • As expected, only 1 SetPass call
  • Actually dynamically batches?! (only 1 draw call)
  • Actually automatically does frustum culling?
  • Render thread: 0.5ms

CommandBuffer.DrawMesh:

  • As many SetPass calls as draw calls (8k SetPass calls)
  • No dynamic batching (8k draw calls)
  • No frustum culling
  • Render thread: 5.0ms

This makes CommandBuffers a lot less appealing; curious to know whether that is intended behaviour.

1 Like

Graphics API is probably hooked to the same render pipeline entrypoints that handles them as default renderer batches.

Well, Unity kinda expects you to perform these operations on your own (batching and culling) if you’re using CommandBuffers. (That was mentioned in blogs, forum threads and I saw it somewhere in the manual)

CmdBuffers bypass rendering pipeline, and work as extension of it.
Dynamic batching doesn’t work with them afaik.

I’m pretty sure that’s intentional, although I might be wrong, and someone from actual UT should comment on this.

Regarding SetPass (and built-in render pipeline):
It will always affect your performance, so make sure to keep its count low as possible.
(CPU sending textures from RAM to VRAM and then switching rendering context is costly. DrawMeshInstancedIndirect actually uploads its only once to the GPU VRAM, and the switch cost is way lower, but it isn’t supported everywhere. SMv4.5+ required (I think?) for the compute shaders to work properly)

TL;DR: Best bet for the CmdBuffers is DrawMeshInstanced where supported, and a Graphics DrawMesh + Dynamic Batching fallback on the platforms that do not support it. You can use SystemInfo.supportsInstancing to figure it out in runtime.

But that requires <300 vertex meshes though. So it might be wise to just ignore these platforms, or pick single solution.

Frame debugger does show correct information for the built-in pipeline.

There’s a few caveats for the SRP, but I’d say its temporary issues.

I suspected as much, but then in that case I would expect one of the following:

  1. Provide an API for CommandBuffer.DrawMesh that does not automatically call SetPass, like DrawMeshNow
  2. Provide an API for CommandBuffer that applies a MaterialPropertyBlock without calling SetPass

This way, we could manually batch our SetPass calls together and still use MPBs (which is what they’re made for). I don’t mind having full control, but then we need the tools to actually optimize if Unity isn’t doing it for us behind the scenes.

I could use DrawMeshInstanced, but that does not allow me, for example, to use different MaterialPropertyBlocks within one SetPass call; I’d have to use instancing, which is a really different approach that’s not as well supported.

2 Likes

Bumping! Really need API for CommandBuffer.DrawRenderer that does not automatically call SetPass as Ludiq said. I’ve implemented the CSM Scrolling in Unity. However, I have to use CommandBuffer.DrawRenderer to redraw the additonal renderers in cached shadowmap. All the renderers use the same material to draw shadowmap but the SetPass Calls increases unfortunately. Wishing Unity could hear my voice.

2 Likes

I come back to this thread every so often wishing for the same things. Graphics calls are quite slow atm. I really love that Unity opens up a lot more low level stuff than Unreal (afaik), but it’s too bad it isn’t as good as it could be.

Wondering if you have had a chance to test this in 2022 on Unity 2021.2 if that is still the case?

Is there a better way of drawing with command buffers or is Graphics.DrawMesh the better way to go?