ECS Physics vs PhysX Test

I set out to do some quick tests for an upcoming Quest 3 project to see how Unity Physics/ECS Graphics compares to PhysX, but I think I’m doing something wrong (I’m new to DOTS/ECS and working through Unity’s implementations)

I created a simple scene with a Monobehaviour that instantiates 1000 rigidbodies with sphere colliders and adds some forces in FixedUpdate() to keep them together. it’s pinned at 72 FPS no matter what I do or how many contacts there are:

I created another scene that instantiates 1000 entities with sphere physics bodies/shapes and moves them with a system in a similar manner to the PhysX test:

This one hovers around 72fps but dips down super low when they bunch up and there are a lot of contacts (around :43s in the video). The profiler is showing BuildPhysicsWorld taking a while:

I’m curious if this is expected or I’m missing something important (seems like the latter)… or maybe this specific setup on Quest 3 is just a bad test for ECS Physics? Any insight to get this running faster or if I’m doing something wrong is much appreciated

  • Unity Entities/Physics/Graphics 1.2.1
  • Burst is enabled, safety checks off
  • has a PhysicsStep with Multi-Threaded on, Enable Contact Solver off (tried both), Synchronize Collision World off
  • spheres collide with themselves and hands, hands are kinematic and only collide with spheres
  • Vulkan

baker for the prefab with the sphere physics body/shape:

using UnityEngine;
using Unity.Entities;

public class UnitConfigAuthoring : MonoBehaviour
{
    public int numUnits;
    public GameObject unitPrefab;
    class Baker : Baker<UnitConfigAuthoring>
    {
        public override void Bake(UnitConfigAuthoring authoring)
        {
            Entity entity = GetEntity(TransformUsageFlags.None);
            AddComponent(entity, new UnitConfig
            {
                numUnits = authoring.numUnits,
                unitPrefab = GetEntity(authoring.unitPrefab, TransformUsageFlags.Dynamic)
            });
        }
    }

}
public struct UnitConfig : IComponentData
{
    public int numUnits;
    public Entity unitPrefab;
}

spawn spheres system:

using Unity.Entities;
using Unity.Burst;
using Unity.Mathematics;
using Unity.Transforms;

[BurstCompile]
[UpdateInGroup(typeof(InitializationSystemGroup), OrderFirst = true)]
public partial struct UnitSpawnerSystem : ISystem
{
    [BurstCompile]
    public void OnCreate(ref SystemState state)
    {
        state.RequireForUpdate<UnitConfig>();
    }
    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        //run once
        state.Enabled = false;

        var config = SystemAPI.GetSingleton<UnitConfig>();

        var rand = new Unity.Mathematics.Random(123);
        for( var i = 0; i < config.numUnits; i++)
        {
            var r = rand.NextFloat();
            var unit = state.EntityManager.Instantiate(config.unitPrefab);
            state.EntityManager.AddComponent<UnitTag>(unit);
            var pos = new float3(0, 1f, 0) + rand.NextFloat3Direction() * r * r * r;
            state.EntityManager.SetComponentData(unit, new LocalTransform
            {
                Position = pos,
                Scale = 0.08f,
                Rotation = quaternion.identity
            });
            state.EntityManager.AddComponent<UnitT>(unit);
            state.EntityManager.SetComponentData(unit, new UnitT
            {
                Value = (float)i / config.numUnits
            });
        }

    }
    public struct UnitTag : IComponentData
    {
       
    }
    public struct UnitT : IComponentData
    {
        public float Value;
    }
}

sphere movement system:

using Unity.Entities;
using Unity.Burst;
using Unity.Physics;
using Unity.Mathematics;
using Unity.Transforms;

[UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
[BurstCompile]
public partial struct UnitMovementSystem : ISystem
{
    [BurstCompile]
    public void OnCreate(ref SystemState state)
    {
        state.RequireForUpdate<UnitConfig>();
    }

    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {

        var time = (float)SystemAPI.Time.ElapsedTime;
        var job = new UnitMovementJob
        {
            deltaTime = SystemAPI.Time.DeltaTime,
            maxDist = 3f,
            constrainDist = 0.6f,
            noiseScale = 1.3f,
            time = time,
            noiseOffset = new float2(time * 0.2f, 0),
            center = new float3(0, 1.2f, 0)
        };
        job.ScheduleParallel();
    }
}

[WithAll(typeof(UnitSpawnerSystem.UnitTag))]
[BurstCompile]
public partial struct UnitMovementJob : IJobEntity
{
    public float deltaTime;
    public float time;
    public float maxDist;
    public float constrainDist;
    public float noiseScale;
    public float2 noiseOffset;
    public float3 center;
    [BurstCompile]
    public void Execute(ref LocalTransform unitTransform, ref PhysicsVelocity unitVelocity, in UnitSpawnerSystem.UnitT unitT)
    {
        var pos = unitTransform.Position;
        var distPos = pos - center;
        var distFromCenter = math.length(distPos);
        if (unitTransform.Position.y < 0)
        {
            unitTransform.Position.y = 0;
            unitVelocity.Linear.y = math.abs(unitVelocity.Linear.y);
        }
        if (distFromCenter > maxDist)
        {
            unitTransform.Position = center + math.normalize(pos - center) * maxDist;
            unitVelocity.Linear = -math.normalize(distPos) * math.min(0.5f, math.length(unitVelocity.Linear));
        } else if (distFromCenter > constrainDist)
        {
            unitVelocity.Linear += -math.normalize(distPos) * deltaTime * 0.4f;
        }
        else
        {
            unitVelocity.Linear += noise.srdnoise(distPos.xz * noiseScale + (noiseOffset + new float2(unitT.Value) * 10f) ) * deltaTime * 0.2f;
        }
    }
}
1 Like

If you zoom into the profiler view you shared, you will see that the fixed step simulation system group decided to step the physics multiple times because you are overrunning the 16ms time consumption, in an attempt to “catch up” which obviously doesn’t work in this case.

The first time around should be enough.
But the following times the systems get bogged down waiting for job completion of the previous systems, such as the contact creation which appears quite long in your case.
That’s why the build physics world system is seemingly taking so long. It’s though not doing anything and just waiting for completion.

Can you zoom into the first physics system group update, which is where the jobs for the one step we would want are launched? There the build physics world should be very fast.
It appears the slowdown here is due to the contact creation jobs which seem to take excessively long. Obviously having only two threads here doesn’t help.

Maybe the way you are adding forces here pushes all the spheres into collision, creating excessive numbers of overlaps.
Is this done in any way differently than for PhysX? Or are you seeing clear behavior differences between the two simulations?
Running them without user interaction would help compare them.

My hunch is that the spheres are penetrating deeper in Unity Physics than in PhysX with the forces you apply, leading to a way larger number of overlaps.
What’s the sphere size btw? We currently have a hard-coded contact tolerance of 0.1m for continuous collision detection, which creates contacts even if objects are more than that distance apart. In high compression cases like this, that could lead to way too many contacts specifically with many small objects.
Maybe try measuring the contact count using an IContactsJob and compare that with PhysX.

appreciate the response! yes I see what you’re saying, BuildPhysicsWorld is super fast:

9855858--1419297--upload_2024-5-26_8-7-20.png
9855858--1419300--ecs-physics-test3.PNG

I’m adding forces each frame in the same manner between the two sims (trying to, using RigidBody.Addforce vs PhysicsVelocity.Linear and the motion appears the same to me). The forces do push them lightly together and create a lot of contacts. The user interaction here actually spreads the bodies apart and stabilizes the framerate, the frame drops only come in the ECS version when the contact count rises. PhysX seems ok with the same high contact count. I can try counting the contacts and comparing the two.

The sphere size might have something to do with it, they are just under that tolerance at 0.08… I wonder if scaling the camera/tracking space up would help? [EDIT: actually I don’t think you can scale the cameraRig in AR on Quest]

Yeah it’s probably that hard-coded contact tolerance then. It likely creates an excessive contact count by increasing the broadphase bounding volumes of the spheres to more than three times their size (0.1 extension in all directions).
This then likely causes a huge number of overlapping bounding volumes. All the overlapping pairs of objects are then admitted to the create contacts phase (the narrow phase) which are these ParallelCreateContactsJobs, which are your bottleneck.
I think this is the problem.

If you want to make a cool experiment, you could modify the Unity Physics source code by modifying the CollisionWorld.CollisionTolerance value to something more reasonable for your use case, like 0.02 or less, and see what that gets you.

Counting the contacts and comparing them between PhysX and Unity Physics could confirm my hypothesis also.

And we had thought of making the collision tolerance modifiable anyways. So this right here would be a good confirmation of the need for this ability.

very cool, you were absolutely right. makes perfect sense and aligns with the profiler… I modified CollsionTolerance to 0.01 and was able to get over 8x improvement over the original test, way better than PhysX. I made a new sim where they’re even smaller (so they fit in the room), a 0.005 tolerance, and made them cubes so I’m not GPU bound (still sphere colliders). this is 4k cubes and it’s pinned at 72fps:

https://www.youtube.com/watch?v=RBZRQ0-sW6I

allowing collision tolerance modification definitely has my vote… I love close up stereo sweet spot VR/AR which makes for small colliders, this will save having to scale the camera rig up or other risky hacks. thank you Daniel!

2 Likes

Love it!
Thanks for trying this and sharing these amazing results.

We are actually looking at performance right now and the collision tolerance was on our short list of things to investigate. Having proof that it’s causing the slow down we had already identified in the create contacts phase is fantastic news.

I’ll transfer all that info into our internal planning system so that we can reprioritize the corresponding changes in the engine.

Regarding to “GPU bound”, you should be fine with no matter what render mesh if you use mesh instancing. If it’s the same mesh for many elements to render, there will be only one copy of them on the GPU and the only difference between the instances will be their transformation. So it should not matter as much.
Not sure exactly how this sort of setup performs on a VR headset though.

for sure! glad this worked out…

and that’s what I was thinking re:instancing, it was strange… I usually use Graphics.RenderMeshIndirect for this sort of thing, this is the first I’m trying out ECS graphics and the SRP batcher. I wanted to focus on physics only for this test, so when I saw the Semaphore.WaitForSignal and Gfx.WaitForGfxCommandsFromMainThread I figured it was GPU bound. when I switched to cubes it ran fine. I’ll head over to the ECS Graphics forum next, my guess is that it’s something Quest specific

thanks again

1 Like

Yeah asking in the graphics forum about this is a good idea.

Another thing: I was wondering about the two worker threads you have in your profiler screenshots.
Since this is done on a Quest 3 I would have expected there to be more threads than two. Obviously if there could be more available, Unity Physics would run way faster since it’s heavily multithreaded.

Edit: just noticed that you shared another screenshot with more worker threads. Did you modify the worker thread count explicitly or was that on another device (PC for example)?

@bzor : for your graphics performance, maybe have a look at the newly released ECS Galaxy Sample .
It features a massive number of render meshes.

@daniel-holz fantastic! it looks amazing… will dig into this tonight :slight_smile:

1 Like