Unity's static batching: why does it shoot itself in the foot?

I also second the support for NativeArrays.
Needed to post a comment (better than a like)! :smile:

Fair enough, I suspect NativeArray is the way to go then.

Well, this is why I asked if StaticBatchingUtility is in fact the exact same pipeline used for build-time batching. Thing is, I’m not quite convinced of the realism of “once registered, it will always be used”. This will hold true in trivial scenarios where I’ve consciously registered this delegate to test this exact scenario.

But in a realistic scenario, (speaking here with the experience of semi-large projects with a long history), I or someone on my team may have added a plugin that may (unbeknown to me, or even undocumented) register its own delegate. Or we may need different delegates for different scenarios and have forgotten about an older one. All of these may register their own delegate, at certain times, in a certain order, under certain conditions, on certain platforms… What if something registers a delegate for build-time batching, accidentally also affecting the runtime batching of some script written before the custom sorting was a thing? Or vice versa. (How do assembly reloads work, by the way?) In other words, a few years from now, I foresee myself having a couple of really bad days trying to figure out why the performance of everything is suddenly on fire. :wink:

But on a more abstract level, I feel that if some data is only relevant within the context of a particular operation, it should also be local to that operation (i.e. passed). Otherwise you’re just creating unnecessary state that’s asking to get messed up. Or put another way: if I’m always going to (re)set the delegate before I call StaticBatchingUtility.Combine, just to be safe, they might as well be one call. I for one much prefer very explicitly passing a sorting method for some specific use case, rather than something “helpfully” changing that sorting method across my entire project.

So I would rather propose the following:

// Always uses default sorting.
public static void Combine(GameObject staticBatchRoot);
// Uses delegateMethod or default sorting if delegateMethod is null.
public static void Combine(GameObject staticBatchRoot, CustomSortingDelegate delegateMethod);

Which brings us to build-time batching, which I feel is the primary motivation for setting the delegate in the way currently proposed, correct? To answer your question: yes, control would definitely need to be more flexible than a per-project basis. In my case, almost all problems I sought to improve with the formerly broken batching were highly specific to a particular scene. Setting a single sorting method across the entire project would render the whole thing useless and possibly detrimental.

I think the proper way to go about this would be to add a hook in the build process (don’t know the exact API) where I am given a scene and can (optionally) return a sorting method.

Much appreciated!

Thanks for all the feedback so far. I’ve made some changes (but I’ve not yet made a new public build)

The important information:

  • I’ve replaced the Span with a NativeArray, so it’s much easier to sort the data

  • I’ve changed how you “register” the custom sorting, which i will go into more detail about, below

The script API’s now take an optional extra param for the sorting, eg:

public static void Combine(GameObject staticBatchRoot, CustomSortingDelegate delegateMethod);

The automatic batching that happens at build time is now hooked into our C# code that dispatches the Static Batching (UnityCsReference/Editor/Mono/PostprocessScene.cs at master · Unity-Technologies/UnityCsReference · GitHub)

I’ve modified that code to ask if you want to use custom sorting for a given scene, and if you return a sorting delegate, the static batching will use it.

So usage is:

StaticBatchingUtility.RegisterCustomSortingDelegate(MyQueryFunc);

StaticBatchingUtility.CustomSortingDelegate MyQueryFunc(SceneManagement.Scene scene)
{
    if (scene.path == "myspecialscene")
        return MySortingFunc;
    return null;
}

void MySortingFunc(NativeArray<BatchingData> data)
{
    data.Sort(MyCompareFn);
}

Note that there is still some global state to manage here. (registering MyQueryFunc)
I’m not really sure how to completely get around that (more ideas welcome!) :slight_smile:

It feels like, if we need the global state, then perhaps it needs to be a list of “query funcs”…

Let me know your thoughts.

(Out of context, but it would be great if we could use MemoryExtensions.Sort, i.e. sort spans!)

@richardkettlewell
Can we have this custom sorting function in 2022 LTS, especially given the new pricing changes for Unity 6 (2023 LTS)

Found a solution → Do the following with a editor script:
Sort the to be batched objects in a array of new parents based on position.
Call Staticbatchingutility.combine on every single parent

WAY better batching results, decreased my drawcalls 30x fold

Word of warning: (unless you’re using the 2023 fix, haven’t used that version yet) this won’t fundamentally fix anything. Depending on your scene contents, it may improve batching, just by virtue of somewhat grouping spatially related objects. But the core issue is inside Staticbatchingutility.Combine, which this approach still uses. Within each group, the ordering will still be randomized, leading to lots of context switches for reflection probes and real time shadow distance boundaries. You’ll minimize those issues by using smaller spatial buckets, but that may start counteracting the optimization.

Did this ever make it into an official build?
A year later and I suspect not since I’ve not heard anything. So is there to be any progress with it?
I guess if it hasn’t happened by now, then there is no way its getting back ported to 2022 which is a huge shame.

Based on the apparent lack of action from Unity (despite the best efforts of its engineers) I’m looking back at this option and i’m glad to see you’ve kept up with development.

So i’m wondering how reliable is this system? I mean at a fundamental level does the concept work, even if you feel we aren’t able to get the best batching potentially possible due to it being a workaround? Would you be happy to use it in a live project?

My main quandary and likely due to not understanding exactly how the Unity batching system works at runtime, is due to the statement that you disable static batching flag before Unity gets a chance to act on it. I understand the reason for that, makes perfect sense, what i’m unsure about is how does Unity perform the rendering of static batches at runtime if the static batching flag is disabled?

Thinking as I type this, I guess you are disabling the project global ‘static batching’, but per gameObject static batching flag is maintained? Perhaps that is enough for Unity to realize that each meshfilter is pointing to a combine mesh and deal with it appropriately?

I guess what I was worried about is if the Static Batching flag was disable during build, at runtime how did Unity know to use the (I assume) special case batch rendering system? This is as I mentioned, assuming that Unity needs a special render system to deal with rendering static batches.

Here’s a weird thing I just noticed. (Unity 2022.3.14 )

Whenever I use static batching (Unity’s) in BiRP (Forward or Deferred) the static batched objects are being rendered in a vague back-to-front order! It could just be random and only appears back-to-front, but it is definitely not front-to-back. This is still true no matter if I change Camera.opaqueSortMode to try and force front-to-back.

If I disable static batching, then I get the expected front-to-back rendering in both Forward and deferred. As a note I also disabled GPU instancing on all materials in the scene too.

Is this normal behavior? I guess I’ve never actually bothered to check though I now realize we’ve been seeing this behavior in all the demos in this thread. It just never occurred to me it was happening as I was more focused on the poor batching counts.

So can anyone confirm this for me, I mean it wouldn’t be the first time I’ve broken Unity and not realized, so maybe its something I’ve done. If this is expected behavior, can any explain why? I get that with batching you maybe can’t expect a perfect front-to-back ordering, but this seems wildly non-optimal.

I’m not even sure its due to the renderer wanting to only render from a single batch group at a time, as I swear I’ve seen it switch back and forth between batches before.

Starting to think that perhaps there might be even further opportunity to gain performance with some better batch rendering algorithm. Probably some combination of more focused batching algorithm written with the batch rendering algorithm in mind to maximize front-to-back rendering, minimal batch swapping to gain as much benefit from the depth buffer as possible.

I’ve ran out of time today, but will be performing the same tests in a URP version of my project to check what results I get there. I’ll post back if anything is different.

BTW - I’ve been playing around with @Error-md1 (StressLevelZero) CustomStaticBatching project ( very cool BTW). One thing that piqued my interest was the option/suggestion to place non-vertices into a secondary vertex stream. It suggests its better for Tiled based GPU’s, but maybe also in general - anyone care to share some light on that?

This is awesome and reduced the batches in a (very unoptimized) scene by almost 75% (compared to regular static batching in 2021.3 - it’s over 90% less than without batching). I hope you are still working on this, although it already seems to be working great!

Hey, no it did not land, and yes I’d say you’re right that there is no chance for 2022 at this point.

From memory, (apologies if I am misremembering - it’s been a long time now!) I didn’t get enough community feedback about whether it was solving the problem in a way that was acceptable.

To move it forwards, and from re-reading my earlier posts, I think I need to know:

  • is NativeArray the best container to expose to script for sorting?
  • is the registration of a custom sorting function appropriate/flexible enough?
  • i’m not sure about the usefulness of the InstanceID field.. I’m not sure if it’s right to expose that, but, providing access to full objects would have much larger performance implications and is probably unwise.

I can seek out some of these answers internally too, but, I haven’t been in the graphics team here for a lot of years, so this is outside my usual area and thus I can’t necessarily answer them myself :slight_smile:

Thanks for jumping back into the thread.

I guess that’s to be expected, not only are very few developers even aware of the problem, even fewer are aware of this thread and I suspect any other means you have to collect these data points. However this does not mean that this is not still a critical issue.

Personally I followed the discussion about the implementation details, but had neither the time or hands-on knowledge to really offer any input one way or the other. I suspect that many others felt the same. I think the only way I could give input would be to spend time digging into @Error-md1 solution and rewriting it to use the demo build you provided. However whilst I’ve been using their solution, I still haven’t had the time to go through all the code to truly understand every aspect of it.

As such I’m sadly still not in a position to comment on the points you made. Perhaps in a month or so I might have time to dig into a demo build, but did we ever get one that incorporated your proposed delegate solutions?

At this point unless some of the other participants jump back in, I would guess either Unity investigate this fully on their own and implement a solution that has extensive realworld testing, or wait a month or so for me to finish client work and take a look at your demo builds :wink:

Concerns

The only thing that comes to mind is if using the InstanceID is the only viable method, does that mean this can’t be used for runtime? That wouldn’t be great, but i’d rather have the ability to assign a custom sorting at build time than nothing at all! At least for now.

Alternatives

The only alternative that came to mind when originally talking about custom sorting was to use something similar to IComparer interface, but I’ve no idea if that is useful, practical or even what data we can compare if we are already stuck with instanceID?

That might be a bit risky approach though, because having used various techniques for sorting I can already see that I might want to implement a solution where as the developer i’ve added explicit indicators in the scene to batch by, beyond pure maths. For example maybe I add bounding boxes to define that I want all gameObjects within to be batched together, or perhaps I’ve used scene hierarchy to group gameObjects together and want to batch by children of a parent, or heck maybe i’ve added a tag to meshes and want to sub-batch based on those.

Question: What does PlayerSettings.StaticBatching affect?

Does this purely determine whether the CombineMesh code will be executed in the PostprocessScene.cs or does it have wider implications, such as being used by the Unity BiRP renderer to know that meshes are part of a batch?

This is somewhat important to know/understand in terms of being able to use any custom batching method that hooks into PostprocessScene like Error-md1’s solution does.

Is batch Rendering Broken?

I strongly suspect that the sorting algorithm to render batches might also broken. It was likely hidden to us before due to the sorting method of batching being random(instanceID), but now we have greater control over that its become more apparent.

The problem is that when rendering the scene Unity appears to be rendering batches back-to-front (more or less) instead of front-to-back. Unfortunately I just realized I’ve not tested this against the ‘fixed’ Unity sorting in 2023/6.0, only with @Error-md1 solution, so I’m going to have to do more tests to confirm.

In the meantime it would be nice to get any word from Unity graphics Team as to whether static batched meshes have some weird sort order when rendering (BiRP) or if this is by design, or if it should be front-to-back and anything different is not expected.

Yikes, but to me you are THE graphics guy. You were always responsive and pro-active on issues that I had brought up or seen on the forums.