BatchRendererGroup sample: High frame rate even on a budget GLES device


BatchRendererGroup sample
High frame rate even on budget GLES mobile

Hi everyone!

We just released a blog post about our new low level graphics API BatchRendererGroup.We also released a sample project showing good performance even on a budget mobile phone.

Feel free to discuss it in this forum thread!

Blog Post: https://blog.unity.com/engine-platform/batchrenderergroup-sample-high-frame-rate-on-budget-devices
Sample project: GitHub - Unity-Technologies/brg-shooter: Unity blog-post sample showing BatchRendererGroup and Burst/JobSystem. Focus is high performance even on budget mobile devices. Unity 2022.3.5 or above required

12 Likes

Hi

You wrote about Vulkan Metal Dx and Gles 3.0 but what about Gles 3.1-3.2 do it support SSBO instead of UBO?

BatchRendererGroup with GLES is always using UBO, whatever 3.0 or greater

3 Likes

Amazing article and tool!

Do you have any idea of how it compares in terms of performance to the latest built-in SRP batcher?

Any restrictions on using BatchRendererGroup in BiRP?

SRP Batcher and BatchRendererGroup are two different things. The former is a generic and automatic option trying to minimize the amount of GPU setup between the draw calls. You don’t have to worry about anything, but it’s not magic: if you have 1000 cubes to render, you will end up with 1000 drawcalls.

BatchRendererGroup is a low level API to do explicit GPU instanced draw. It requires more effort (you have to manage GPU memory yourself, and also generate the draw commands ). In dedicated situations such as the shooter sample, where you have plenty of similar objects to render with some custom properties per object, BRG is way faster than SRP Batcher regarding CPU.

BatchRendererGroup is a new tool for dedicated situations. It’s not a replacement for SRP Batcher. In the shooter sample, SRP Batcher is used to render the main ship, enemy spheres and missiles. BRG is used to render the huge amount of CPU animated cubes.

As SRP Batcher, BatchRendererGroup requires SRP. So it’s not compatible with BiRP.

1 Like

Hi. Any plan to port the project to entities graphics to battle test entities graphic performance? It seems like entities graphic can’t achieve the same high performance compares to fully manual written brg that u show.

nice to see ready to use examples!

any way to optimize this part? (just increased background object count to test)

*ah ok, can get rid of that, if i dont need to move the objects.
with 16 million static quads, its then here

1 Like

entities.graphics is also driving BatchRendererGroup.

As entities.graphics can render any generic ECS scene, it has to do a lot more preparation work than this BRG shooter demo, each frame. Like:

  • react to any ECS chunks change
  • manage (alloc/free) into a GPU memory pool if anything change ( entity spaw, component change )
  • do frustum culling for all entities
  • gather all visible objects and bin them into several batches (depending on shader, material, or some feature flag like transparency
  • and finally, generate BatchRendererGroup draw commands

It’s expected entities.graphics to take more CPU time than just driving BRG. As a result it’s more versatile and generic.

3 Likes

This part is the GfxBuffer.SetData. Basically it’s the data upload from system memory to GPU memory.

As you noticed, if your data doesn’t change ( like everything static ), then you can avoid the upload and run even faster

2 Likes

Does BRG support lighting per-instance? I know in the past it did not.

Great article, by the way! Love deep dives like this.

You mean having several point lights in a scene (dynamic or static) affecting each instance?

Regarding dynamic lighting, we only support HDRP in deferred mode, or URP in forward+ mode.

Regarding static lighting (GI) both lightmaps or SH are supported. Everything is handled for you if you’re using entities.graphics package. But If you want to drive BRG directly, you need to do manage some data yourself. For lightmaps, you have to handle a buffer of lightmap index & scale per instance. For SH you can handle a buffer of SHCoefficients per instance.

You can also have a look at entities.graphics package source code. It contains a lot of interesting code to drive BRG (including lightmaps and SH)

2 Likes

Fantastic! I know BRG is meant to be super low level so it’s not meant to have all features, but glad to hear dynamic lighting is included. Very excited to try this out!

I understand entities.graphics is much more versatile and generic solution but still it shouldn’t takes so much time like over 2ms at mobile platform which is not acceptable. Currently why entities.graphics takes so much CPU time is caused by it’s not using burst ISystem. I understand there’s couple of graphics api does not burst compatible yet but I think official can split them out as ISystem to burst all the burst compatible items and managed items can’t burst goes to SystemBase. I believe by doing so can further reduce the time cost significantly today. But still official should try to make those graphics api burst compatible until entities.graphics able to fully adopt burst ISystem. Currently entities.graphics seems like it’s zero burst ISystem adoption.

Another huge performance issue is main thread stalling caused by low level graphics api i.e. Vulkan, OpenGLES3 implementation is still on main thread and cause entities.graphics systems stuck on main thread and slow down other module like dots physics significantly. Official will need to improve all the low level graphics api supported by entities.graphics off the main thread. I hope these 2 huge tasks official can start work on them asap and hopefully can ship them soon.

Thank you for the article! You’ve compared BRG to Graphics.DrawMeshInstanced, but what about Graphics.DrawMeshInstancedIndirect?

Currently I’m using Graphics.DrawMeshInstancedIndirect like this:

// Per Instance properties.
public struct InstancedMeshProperties
    {
        public float4x4 ObjectToWorld;
        public float4x4 WorldToObject;
        public Vector4 Color;

        public InstancedMeshProperties(float4x4 trs, Color32 color)
        {
            ObjectToWorld = trs;
            WorldToObject = math.inverse(trs);
            Color = new Vector4(color.r, color.g, color.b, color.a) / 255f;
        }

        public static int Size()
        {
            return sizeof(float) * 4 * 4 + sizeof(float) * 4 * 4 + sizeof(float) * 4;
        }
    }

// Job to write data to gpu.
  [BurstCompile]
    public struct WriteToGPU : IJob
    {
        [ReadOnly] public NativeList<Instance> Instances;
        [ReadOnly] public NativeArray<InstancedMeshProperties> MeshProperties;

        [WriteOnly] public NativeArray<InstancedMeshProperties> GPUBuffer;

        public void Execute()
        {
            for (int i = 0; i < Instances.Lenght; i++)
            {
                   GPUBuffer[i] = MeshProperties[Instances[i].ID];
            }
        }
    }


// Create buffer for max visible instances.
_buffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured,
                    GraphicsBuffer.UsageFlags.LockBufferForWrite, _maxInstancesVisibleInRuntime,
                    InstancedMeshProperties.Size());

// Write to GPU with LockBufferForWrite mechanism to prevent SetData.
new WriteToGPU()
{
    Instances = instances,
    MeshProperties = _instancedMeshProperties,
    GPUBuffer = _buffer.LockBufferForWrite<InstancedMeshProperties>(0, _maxInstancesVisibleInRuntime)
}.Schedule(culling);

// Unlock buffer and draw.
_buffer.UnlockBufferAfterWrite<InstancedMeshProperties>(instancesCount);
Graphics.DrawMeshInstancedIndirect(_mesh, 0, _material, Bounds, _argsBuffer, camera: cameraValue,castShadows: ShadowCastingMode.Off, lightProbeUsage: LightProbeUsage.Off);

//In shader update matrices and color.
void vertInstancingSetup()
{
    #ifndef SHADERGRAPH_PREVIEW
    #if UNITY_ANY_INSTANCING_ENABLED
    unity_ObjectToWorld = mul(unity_ObjectToWorld, _Properties[unity_InstanceID].ObjectToWorld);
    unity_WorldToObject = mul(unity_WorldToObject, _Properties[unity_InstanceID].WorldToObject);
    #endif
    #endif
}

void GetInstancedColor_float(out half4 result)
{
    result = half4(0,0,0,0);
    #ifndef SHADERGRAPH_PREVIEW
    #if UNITY_ANY_INSTANCING_ENABLED
    result = _Properties[unity_InstanceID].Color;
    #endif
    #else
    result = half4(1,1,1,1);
    #endif
}

For each visible instance I’m writing to GPU a lot of bytes(InstancedMeshProperties struct), but if I change the previous code to two buffers like described in the article - a persistent buffer with data for all instances and another buffer with visible ids of instances that I’ll update with LockBufferForWrite mechanism - will this be slower than BRG? Am I right that If I set data once for this persistent buffer it will stay in GPU memory and I’ll too have GPU persistency like in BRG?

When a scene is rendered using both SRP batched rendering for some objects and BRG rendering for other objects am I right that transparent objects of BRG batches can only be rendered before or after SRP batched transparent objects - we can’t sort them together in a correct back to front order?

Can we use LockBufferForWrite mechanism with BRG?

Also in your example used single GraphicsBuffer, but don’t we need to use Ring Buffer(array of GraphicsBuffers were every frame we write data to the next buffer) to prevent writing from CPU to buffer that currently used by GPU for rendering? I’m doing this for Graphics.DrawMeshInstancedIndirect in the showed code I just omitted usage of buffers for clarity.

Does BRG support dynamic(additional) lights in URP’s Forward or BRG supports them only in URP’s Forward+?

2 Likes

Using Graphics.DrawMeshInstancedIndirect should be faster than DrawMeshInstanced because it’s up to you to provide per instance data ( So Unity doesn’t have to alloc/upload buffer to copy matrices and any custom MPB data per drawcall ).
Btw both DrawMeshInstancedIndirect or DrawMeshInstanced would need to write your own shader code to fetch your custom data. ( so if you want to use urp/lit or hdrp/lit, you have to fork urp or hdrp and modify shader )

I would say it should run quite similar speed than BRG (because it will basically do the same amount of work). But as you should write your own shader code, you also have to implement any additional feature you would need ( like any flags in https://docs.unity3d.com/ScriptReference/Rendering.BatchFilterSettings.html )

yes

when using BRG transparent + sorted instances, it’s up to you to generate one single DrawCommand per instance. All these single drawcommands will be injected in the frame renderers list, as standard SRP Batcher objects. So they should all be properly distance sorted ( both BRG instances and standard SRP Batcher objects )

yes, BRG is using GraphicsBuffer, whatever how you update the data ( using SetData or LockBufferForWrite )

We use a single buffer because we’re using SetData to update content. SertData will properly handle GPU buffer lifetime and garantee you don’t have issue with GPU currently proceeding the buffer. ( if buffer is already in flight we just do a copy of the data and push a GPU “data copy” command in command buffer )

Using LockBufferForWrite is very low level and you should be aware it could be tricky. Like, you need to handle your own ring buffer as you said. You also have to be sure writing aligned data to avoid any CPU slowdown when writing to GPU memory.

BRG only supports Forward+ for dynamic lights.

5 Likes

Thank you for your answers!

Can you please elaborate about aligned data? It’s that I need my struct to consist of float4( or float2 + float2 for example) so on different platforms everything works correctly?

LockForWrite returned memory will often be write combined memory. So be sure to write this buffer lineary (obviously never “read” from it). Like, do not just write bytes here and there, it will slow down things a lot. Long story short, always write contiguous data in such mapped memory (without holes). If you want more fine grained details, there is an old but really good read about easy mistakes to avoid when dealing with write-combined memory: https://fgiesen.wordpress.com/2013/01/29/write-combining-is-not-your-friend/

1 Like

Thank you!

Currently, I’m using BatchRendererGroup to render a cubes scene in HDRP by using RenderBRG.cs in Unity Graphics repo and get warning “Internal: JobTempAlloc has allocations that are more than the maximum lifespan of 4 frames old - this is not allowed and likely a leak”. It looks like the Native BatchRendererGroup release unmanaged memory too late. Is it right?

EDIT: It seems like a Unity 2022.3.3f1 bug, after upgrade to 2022.3.11f1, the warning disappear.

1 Like

Hi, Can we use BRG to render directly to RenderTexture?
Currently there is No Instancing function on LowLevel rendering to RTs, Command Buffers does not work and need a Camera to work, aswell as having a quite chunky Initial overhead. Currently have to rely on Graphics.DrawMeshNow to draw fastest as possible but it has downtimes of passing data to GPU over and over again which limits its speed at higher numbers of instances.