the symptom, in your words

"Editor vsync hides physics cost: time the step directly"

✓ verified on 26.07.22
lane Making it performposted updated

▸ SYMPTOM

You're profiling a physics-heavy scene in the editor and average fps pins at exactly 60.0 regardless of how many Rigidbodies, colliders, or raycast-wheel vehicles you add. Your performance metrics suggest everything is fine, even when the physics workload should clearly be saturating the CPU.

▸ CAUSE

Editor-embedded play mode is hard-capped at the desktop compositor's vsync (60 Hz). Neither fps_max 0 nor r_frame_sync_enable 0 lifts the cap: it's the OS windowed-present sync on the editor window, not the engine frame-sync. Frame-based fps measurement only tells you "the engine met every 60 Hz deadline," never how much headroom remains or where the real knee is.

▸ FIX

Implement IScenePhysicsEvents on a component and time the physics step directly with a Stopwatch:

snippet
public sealed class PhysicsStepTimer : Component, Component.IScenePhysicsEvents
{
    private readonly Stopwatch _sw = new();
    private readonly List<double> _samples = new();
    private const int WindowSize = 250; // ~5 s at 50 Hz

    void IScenePhysicsEvents.PrePhysicsStep()
    {
        _sw.Restart();
    }

    void IScenePhysicsEvents.PostPhysicsStep()
    {
        _sw.Stop();
        _samples.Add(_sw.Elapsed.TotalMilliseconds);

        if (_samples.Count >= WindowSize)
        {
            _samples.Sort();
            var avg = _samples.Average();
            var p99 = _samples[(int)(_samples.Count * 0.99)];
            Log.Info($"PhysicsStep: avg={avg:F2} ms, p99={p99:F2} ms ({_samples.Count} samples)");
            _samples.Clear();
        }
    }
}

The PrePhysicsStep/PostPhysicsStep callbacks bracket the real 50 Hz solver step. Log avg + worst-1% step milliseconds per window alongside your fps p1: this is your true, vsync-free physics CPU cost.

Budget rule of thumb: each physics step has a 20 ms tick budget. A per-car cost of ~13 us/step (compound-folded Rigidbody) means 16 cars with 128 colliders each can sit at 0.39 ms avg / 0.78 ms p99, under 4% of the tick. Physics part count and collider count are effectively free at roster scale; look for your binding cost elsewhere (world regen, triangle rebuild).

Blind spot: this times the solver, not its consequences

Confirmed on engine 26.07.22: the PrePhysicsStep/PostPhysicsStep bracket times the solver step only. It does not see collision-consequence cost that runs in collision callbacks, OnFixedUpdate, or OnUpdate triggered by a collision that frame. A flat, cheap physics-solve time does NOT exonerate the frame: the expensive work (damage application, mesh rebuilds, VFX spawns) can live entirely downstream of the solve, outside this probe's measurement window. If the solve reads cheap but frames still spike, instrument the callbacks, not the solver.

▸ WHY IT WORKS

Without this technique, you'll either (a) believe your physics is fine because avg fps = 60, or (b) try to optimize systems that aren't actually bound. Frame fps under vsync is a binary "met deadline / didn't" signal: the step timer gives you the continuous cost curve you need to find the real knee before it arrives.

Verified on engine 26.07.22: seen in a real project.
s&box moves fast; an undated fix is a liability. Spot a stale detail?
changelog
  • Added the blind spot: this probe times the solver only, not collision-consequence cost in callbacks/OnFixedUpdate/OnUpdate. Re-verified on engine 26.07.22.
  • Published

Want to know when new guides or fixes drop? Join the community to help build this out. Report gotchas, flag outdated fixes, or just lurk.

Join the Discord