"Editor vsync hides physics cost: time the step directly"
▸ 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:
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.
- 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