Skip to main content

Benchmarks — Balaur beside Godot, case for case

Balaur 0108981c0 on Apple M1, 8 cores, Darwin 25.6.0, 2026-09-05.

Every case is the scene the reference suite builds, with the same counts, the same 60 Hz tick and the same window: a settle, then 300 timed steps, reported as the median and the 99th percentile of one tick.

step p50 is a whole physics tick: script fixed_update, the solver, and writing every simulated pose back to the scene tree — what the Godot suite calls step_ms. rapier's own step is the solver alone, and the gap between the two is what the engine adds. script seam is what the case's own script cost inside that tick.

Across the 12 physics cases the engine itself costs 1% to 13% of a tick; the rest is rapier, and in the query cases the script's own calls.

Median tick, lower better:

casebodiesBalaurquickest in Godotthat engine's tick, over ours
3d/pyramid379510.43 msBox3D 13.52 ms1.30x
3d/mixed_pile50005.04 msBox3D 9.72 ms1.93x
3d/joint_grid50003.08 msRapier 3D 8.92 ms2.90x
3d/smash50017.99 msBox3D 9.62 ms1.20x
3d/query_storm20005.50 msRapier 3D 5.93 ms1.08x
3d/drop80003.06 msBox3D 11.34 ms3.70x
2d/pyramid100007.10 msRapier 2D 20.03 ms2.82x
2d/mixed_pile50002.86 msBox2D v3 6.71 ms2.35x
2d/joint_grid50002.17 msBox2D v3 4.93 ms2.27x
2d/smash50013.50 msBox2D v3 11.57 ms3.31x
2d/query_storm200012.45 msBox2D v3 4.56 ms0.37x
2d/drop80006.07 msRapier 2D 16.54 ms2.73x

The one case Balaur loses is 2d/query_storm, and the table below says why: nine hundred queries a tick cost 11 ms crossing the script seam and 1 ms inside rapier. A game asking that many questions per tick from script pays for the questions, not for the broad phase.

The Godot columns are 4.7-stable (official) with the addons the reference suite commits, run on this same machine.

3D

pyramid (3d)

3795 bodies. 300 timed steps after 60.

enginestep p50step p99rapier's own stepscript seam
Balaur10.4315.2810.300.002
Box3D13.5233.12
Rapier 3D14.6234.17
Jolt26.92106.63
Godot Physics 3D48.03133.30

mixed_pile (3d)

5000 bodies, 5 static. 300 timed steps after 60.

enginestep p50step p99rapier's own stepscript seam
Balaur5.047.414.870.003
Box3D9.7217.16
Rapier 3D11.2834.73
Jolt13.7037.14
Godot Physics 3D84.90149.55

joint_grid (3d)

5000 bodies, 9950 joints. 300 timed steps after 60.

enginestep p50step p99rapier's own stepscript seam
Balaur3.084.402.820.002
Rapier 3D8.9214.13
Box3D10.2717.44
Godot Physics 3D26.0852.31

Balaur: displacement_max 49.58. Fingerprint cc96e435.

smash (3d)

5001 bodies. 300 timed steps after 60.

enginestep p50step p99rapier's own stepscript seam
Balaur7.9910.687.810.003
Box3D9.6214.37
Jolt12.1229.06
Rapier 3D19.1425.05
Godot Physics 3D99.98201.82

Balaur: non_finite 0, spread 430.5. Fingerprint 8a46de70.

query_storm (3d)

2000 bodies, 8000 static. 300 timed steps after 60.

enginestep p50step p99rapier's own stepscript seam
Balaur5.506.031.124.09
Rapier 3D5.936.80
Box3D6.678.86
Jolt26.4534.11
Godot Physics 3D73.2686.42

Balaur: hits 201175, queries 270000. Fingerprint dc3e3b83.

drop (3d)

8000 bodies, 5 static. 300 timed steps after 60.

enginestep p50step p99rapier's own stepscript seam
Balaur3.064.242.770.002
Box3D11.3441.58
Rapier 3D12.2618.59
Jolt22.1437.79

2D

pyramid (2d)

10000 bodies. 300 timed steps after 60.

enginestep p50step p99rapier's own stepscript seam
Balaur7.108.536.710.002
Rapier 2D20.0328.12
Box2D v321.3352.38
Godot Physics 2D147.26442.54

mixed_pile (2d)

5000 bodies, 3 static. 300 timed steps after 60.

enginestep p50step p99rapier's own stepscript seam
Balaur2.864.692.600.002
Box2D v36.7111.73
Rapier 2D9.2515.28
Godot Physics 2D71.12136.33

joint_grid (2d)

5000 bodies, 9950 joints. 300 timed steps after 60.

enginestep p50step p99rapier's own stepscript seam
Balaur2.172.751.880.002
Box2D v34.938.75
Rapier 2D10.1619.44

Balaur: displacement_max 190.9. Fingerprint f779b873.

smash (2d)

5001 bodies. 300 timed steps after 60.

enginestep p50step p99rapier's own stepscript seam
Balaur3.504.393.290.002
Box2D v311.5728.63
Rapier 2D11.8621.93
Godot Physics 2D55.09104.59

Balaur: non_finite 0, spread 2999. Fingerprint 7c549757.

query_storm (2d)

2000 bodies, 8000 static. 300 timed steps after 60.

enginestep p50step p99rapier's own stepscript seam
Box2D v34.565.49
Rapier 2D6.257.70
Balaur12.4515.571.1411.03
Godot Physics 2D57.2467.48

Balaur: hits 280111, queries 324000. Fingerprint 5ff47d2c.

drop (2d)

8000 bodies, 3 static. 300 timed steps after 60.

enginestep p50step p99rapier's own stepscript seam
Balaur6.079.755.750.003
Rapier 2D16.5431.08
Box2D v322.6646.56
Godot Physics 2D103.34286.81

Nodes

Scene tree

Milliseconds for the whole loop, lower better. Godot's column is its own published run on another machine, so it says which operations are in a different class, not how the two machines compare.

operationBalaur p50Balaur minGodot release
add_children46.1845.8528.19
delete_children_in_order543.27542.129.82
delete_children_reverse463.37461.496.10
delete_children_random513.89512.1219.63
get_node66.9866.3529.60

move_child has no row: the scene tree has no sibling-reorder operation to measure.

Destroying is two orders of magnitude off, and it is the scene tree, not the script: scene::free_subtree unlinks each node from its parent by scanning every sibling, so freeing a flat container of fifty thousand is quadratic. Adding and looking up are within a small factor of Godot's on a faster machine.

What is not the same

  • Balaur's rapier is built without SIMD; the godot-rapier addon's is built with it. Same version, same enhanced-determinism, same threaded solver.

  • The worlds are statistically alike, not bit-identical: the scattered cases draw from each engine's own random stream, so a fingerprint compares two Balaur runs, never Balaur against Godot.

  • The 2D cases are the GDScript's coordinates with y negated, because Godot's 2D plane points down and ours points up. Distances, contacts and gravity are unchanged, and length_unit is 100 as godot-rapier's 2D default is.

  • mixed_pile's convex hull is one fixed size rather than a random one: a hull collider is built from a mesh asset and has no scale.

  • Godot's own suite runs its scene-tree cases on its CI machine, not this one.

Running it

cargo build --release -p balaur_cli --bin balaur
python3 scripts/bench_compare.py --godot-results <benchmarks-repo>/results

One case on its own, or in the editor:

target/release/balaur run examples/benchmark --headless --fixed-tick -- --case=3d/pyramid
target/release/balaur edit examples/benchmark