perf: Better realistic collision perf + CI benches (#3842)
This PR basically takes as much allocation out of the realistic collision hot loop as possible. This is done by inlining the vector/matrix/line clipping etc so far less readible.
CI runners will be underwhelming in general, but relative movement is still good.
<img width="720" height="320" alt="edge-floor-realistic-200" src="https://github.com/user-attachments/assets/0ca9a9c3-7e56-4783-ab39-695b43399dcd" />
<img width="720" height="320" alt="sleeping-pile-wake" src="https://github.com/user-attachments/assets/6730b959-70c3-4618-a6a5-46c686412ca6" />
<img width="720" height="320" alt="stack-realistic-100-settle" src="https://github.com/user-attachments/assets/fd7976e7-655e-4ba4-9af5-ded3a480ed12" />
<img width="720" height="320" alt="stack-realistic-300-rest" src="https://github.com/user-attachments/assets/d6e26984-1628-4d18-b428-9fa55d5f2b7f" />
<img width="720" height="320" alt="stack-realistic-300-settle" src="https://github.com/user-attachments/assets/859ee921-1783-4c6d-b7c5-2110ad2c38f1" />
<img width="1240" height="360" alt="summary" src="https://github.com/user-attachments/assets/099f0886-1540-44da-9eed-7373f94a25e7" />
Final report
### Excalibur benchmark
Baseline: `npm:excalibur@latest` (0.32.0) ยท Candidate: `../build/dist/excalibur.js` (0.33.0-alpha.14+816d29e) ยท 2 interleaved run(s), best run shown with the minโmax of run medians ยท ms/frame
| Test | Baseline median | Baseline p95 | Candidate median | Candidate p95 | ฮ median | ฮ p95 | Extras (candidate) | |
|---|---:|---:|---:|---:|---:|---:|---|---|
| actors-1000 | 4.90 <sub>(4.90โ6.15)</sub> | 11.62 | 4.90 <sub>(4.90โ5.50)</sub> | 10.60 | +0.0% | -8.7% | actors=1000 | |
| actors-4000 | 39.05 <sub>(39.05โ39.05)</sub> | 64.41 | 33.35 <sub>(33.35โ40.25)</sub> | 66.93 | -14.6% | +3.9% | actors=4000 | |
| stack-realistic-100-settle | 8.20 <sub>(8.20โ8.30)</sub> | 19.80 | 6.00 <sub>(6.00โ7.05)</sub> | 14.03 | -26.8% | -29.2% | sleepingAtEnd=0, firstFrameAllAsleep=-1 | ๐ |
| stack-realistic-300-settle | ERROR | | 18.10 | | | | page errors: [Fatal] : RangeError: Invalid array length | โ |
| stack-realistic-300-rest | ERROR | | 3.30 | | | | page errors: [Fatal] : RangeError: Invalid array length | โ |
| arcade-1000-bouncing | 28.50 <sub>(28.50โ28.70)</sub> | 45.70 | 23.40 <sub>(23.40โ24.35)</sub> | 40.51 | -17.9% | -11.4% | outside=14 | |
| edge-floor-realistic-200 | 9.70 <sub>(9.70โ10.25)</sub> | 18.10 | 3.80 <sub>(3.80โ3.95)</sub> | 12.80 | -60.8% | -29.3% | sleepingAtEnd=200, fellThrough=0 | ๐ |
| sleeping-pile-wake | 26.40 <sub>(26.40โ27.45)</sub> | 48.70 | 14.70 <sub>(14.70โ14.70)</sub> | 31.70 | -44.3% | -34.9% | sleepingAtEnd=0, spawned=5 | ๐ |
| bunnymark-2000 | 99.20 <sub>(99.20โ99.65)</sub> | 102.30 | 99.20 <sub>(99.20โ100.10)</sub> | 102.03 | -0.0% | -0.3% | bunnies=2000, drawCalls=1 | |
| bunnymark-5000 | 231.80 <sub>(231.80โ232.70)</sub> | 238.51 | 231.90 <sub>(231.90โ233.10)</sub> | 238.55 | +0.0% | +0.0% | bunnies=5000, drawCalls=1 | |
No test is more than 20% slower than the baseline. Results are informational, shared CI runners are noisy.
Also ironed out some correctness issues with stale geometry that would cause simulation instability (already present in v0.32 sadly, see the stack-realistic-300 boxes row). Roughly speaking the solver would blow up impulse on subsequent substeps because the geometry was out of date and it'd try harder and harder to correct.
Additionally there was more savings by skipping sleeping contacts from the solver, we still wake them when necessary but we don't want to apply impulse to things that are asleep. The contact islands now make sure there are now mixed states. Either all awake or all asleep.
Bonus points! Updated and added our legacy benchmarking tool so we can apples to apples compare with older releases yay science!
perf: Better realistic collision perf + CI benches (#3842)
This PR basically takes as much allocation out of the realistic collision hot loop as possible. This is done by inlining the vector/matrix/line clipping etc so far less readible.
CI runners will be underwhelming in general, but relative movement is still good.
<img width="720" height="320" alt="edge-floor-realistic-200" src="https://github.com/user-attachments/assets/0ca9a9c3-7e56-4783-ab39-695b43399dcd" />
<img width="720" height="320" alt="sleeping-pile-wake" src="https://github.com/user-attachments/assets/6730b959-70c3-4618-a6a5-46c686412ca6" />
<img width="720" height="320" alt="stack-realistic-100-settle" src="https://github.com/user-attachments/assets/fd7976e7-655e-4ba4-9af5-ded3a480ed12" />
<img width="720" height="320" alt="stack-realistic-300-rest" src="https://github.com/user-attachments/assets/d6e26984-1628-4d18-b428-9fa55d5f2b7f" />
<img width="720" height="320" alt="stack-realistic-300-settle" src="https://github.com/user-attachments/assets/859ee921-1783-4c6d-b7c5-2110ad2c38f1" />
<img width="1240" height="360" alt="summary" src="https://github.com/user-attachments/assets/099f0886-1540-44da-9eed-7373f94a25e7" />
Final report
### Excalibur benchmark
Baseline: `npm:excalibur@latest` (0.32.0) ยท Candidate: `../build/dist/excalibur.js` (0.33.0-alpha.14+816d29e) ยท 2 interleaved run(s), best run shown with the minโmax of run medians ยท ms/frame
| Test | Baseline median | Baseline p95 | Candidate median | Candidate p95 | ฮ median | ฮ p95 | Extras (candidate) | |
|---|---:|---:|---:|---:|---:|---:|---|---|
| actors-1000 | 4.90 <sub>(4.90โ6.15)</sub> | 11.62 | 4.90 <sub>(4.90โ5.50)</sub> | 10.60 | +0.0% | -8.7% | actors=1000 | |
| actors-4000 | 39.05 <sub>(39.05โ39.05)</sub> | 64.41 | 33.35 <sub>(33.35โ40.25)</sub> | 66.93 | -14.6% | +3.9% | actors=4000 | |
| stack-realistic-100-settle | 8.20 <sub>(8.20โ8.30)</sub> | 19.80 | 6.00 <sub>(6.00โ7.05)</sub> | 14.03 | -26.8% | -29.2% | sleepingAtEnd=0, firstFrameAllAsleep=-1 | ๐ |
| stack-realistic-300-settle | ERROR | | 18.10 | | | | page errors: [Fatal] : RangeError: Invalid array length | โ |
| stack-realistic-300-rest | ERROR | | 3.30 | | | | page errors: [Fatal] : RangeError: Invalid array length | โ |
| arcade-1000-bouncing | 28.50 <sub>(28.50โ28.70)</sub> | 45.70 | 23.40 <sub>(23.40โ24.35)</sub> | 40.51 | -17.9% | -11.4% | outside=14 | |
| edge-floor-realistic-200 | 9.70 <sub>(9.70โ10.25)</sub> | 18.10 | 3.80 <sub>(3.80โ3.95)</sub> | 12.80 | -60.8% | -29.3% | sleepingAtEnd=200, fellThrough=0 | ๐ |
| sleeping-pile-wake | 26.40 <sub>(26.40โ27.45)</sub> | 48.70 | 14.70 <sub>(14.70โ14.70)</sub> | 31.70 | -44.3% | -34.9% | sleepingAtEnd=0, spawned=5 | ๐ |
| bunnymark-2000 | 99.20 <sub>(99.20โ99.65)</sub> | 102.30 | 99.20 <sub>(99.20โ100.10)</sub> | 102.03 | -0.0% | -0.3% | bunnies=2000, drawCalls=1 | |
| bunnymark-5000 | 231.80 <sub>(231.80โ232.70)</sub> | 238.51 | 231.90 <sub>(231.90โ233.10)</sub> | 238.55 | +0.0% | +0.0% | bunnies=5000, drawCalls=1 | |
No test is more than 20% slower than the baseline. Results are informational, shared CI runners are noisy.
Also ironed out some correctness issues with stale geometry that would cause simulation instability (already present in v0.32 sadly, see the stack-realistic-300 boxes row). Roughly speaking the solver would blow up impulse on subsequent substeps because the geometry was out of date and it'd try harder and harder to correct.
Additionally there was more savings by skipping sleeping contacts from the solver, we still wake them when necessary but we don't want to apply impulse to things that are asleep. The contact islands now make sure there are now mixed states. Either all awake or all asleep.
Bonus points! Updated and added our legacy benchmarking tool so we can apples to apples compare with older releases yay science!