perf(collision): add spatial hash grid for broad-phase collision culling#8289
perf(collision): add spatial hash grid for broad-phase collision culling#8289killerdevildog wants to merge 4 commits into4ian:masterfrom
Conversation
e24bb00 to
7a48dbb
Compare
Replace O(N×M) brute-force pair enumeration in hitBoxesCollisionTest with
a spatial hash grid that only tests nearby object pairs.
Changes:
- Add GDJS/Runtime/spatial-hash-grid.ts: Generic SpatialHashGrid<T> class
that maps items by AABB into fixed-size grid cells. Supports insert and
region query operations. Uses hash-based cell keys (prime multiplication
+ XOR) to avoid Map overhead of string keys. Pools internal arrays to
reduce GC pressure during repeated clear/rebuild cycles.
- Modify GDJS/Runtime/events-tools/objecttools.ts: hitBoxesCollisionTest
now uses SpatialHashGrid when total object count >= 32. For each frame:
1. Computes adaptive cell size (2× average object dimension, min 32px)
2. Inserts all list-2 objects by their AABB
3. For each list-1 object, queries grid for nearby candidates only
4. Runs existing RuntimeObject.collisionTest (bounding circle + SAT)
only on candidate pairs instead of all pairs
Falls back to original twoListsTest for small lists (< 32 objects)
where grid overhead would exceed brute-force cost.
Benchmark results (N objects, all-pairs collision check):
N=50: 0.097ms → 0.021ms (4.7× faster)
N=100: 0.414ms → 0.048ms (8.6× faster)
N=250: 2.696ms → 0.224ms (12.0× faster)
N=500: 11.05ms → 0.800ms (13.8× faster)
N=1000: 43.29ms → 3.417ms (12.7× faster)
At N=1000 the brute-force path consumed ~43ms (exceeding the 16.7ms
frame budget at 60fps). The spatial hash reduces this to ~3.4ms.
7a48dbb to
49bc40d
Compare
|
Would it be possible to have a benchmark that runs The reason for benchmarking Let us know once you have a version that benchmark |
|
@4ian I'll work on it. |
Benchmark Findings: Spatial Hash Collision OptimizationComprehensive benchmarking of |
…head Remove for..in key-counting fast-path that added ~15% overhead at N=2. Instead, flatten once and branch: for small N (<32 total objects) inline the brute-force pair iteration using already-flattened arrays; for large N use spatial hash grid. This eliminates both the key-counting cost and the double-flatten that occurred when calling twoListsTest after hitBoxesCollisionTest had already flattened.
Spatial Hash Collision Optimization — Design NotesWhy Earlier Commits Were Slower at Small NAttempt 1: Flatten + delegate to
|
| N | ops/s | mean (ms) | rme | samples |
|---|---|---|---|---|
| 2 | 2,763,839 | 0.0004 | ±0.84% | 1,381,920 |
| 10 | 1,280,669 | 0.0008 | ±0.46% | 640,335 |
| 50 | 123,929 | 0.0081 | ±2.04% | 61,965 |
| 100 | 33,281 | 0.0300 | ±0.32% | 16,641 |
| 250 | 5,449 | 0.1835 | ±0.26% | 2,725 |
| 500 | 1,369 | 0.7306 | ±0.15% | 685 |
| 1000 | 343 | 2.9160 | ±0.31% | 172 |
Optimization Branch (inlined brute-force + spatial hash, threshold = 32)
| N | ops/s | mean (ms) | rme | samples |
|---|---|---|---|---|
| 2 | 2,651,478 | 0.0004 | ±0.85% | 1,325,739 |
| 10 | 1,248,256 | 0.0008 | ±0.50% | 624,129 |
| 50 | 117,225 | 0.0085 | ±2.01% | 58,613 |
| 100 | 67,776 | 0.0148 | ±1.53% | 33,889 |
| 250 | 25,083 | 0.0399 | ±1.06% | 12,542 |
| 500 | 10,304 | 0.0971 | ±1.36% | 5,152 |
| 1000 | 3,724 | 0.2685 | ±1.25% | 1,863 |
Comparison
| N | master | optimized | speedup | notes |
|---|---|---|---|---|
| 2 | 2,763,839 | 2,651,478 | 0.96x | within run-to-run noise (±0.85%) |
| 10 | 1,280,669 | 1,248,256 | 0.97x | within run-to-run noise (±0.50%) |
| 50 | 123,929 | 117,225 | 0.95x | effectively tied |
| 100 | 33,281 | 67,776 | 2.04x | spatial hash kicks in |
| 250 | 5,449 | 25,083 | 4.60x | |
| 500 | 1,369 | 10,304 | 7.53x | |
| 1000 | 343 | 3,724 | 10.86x |
|
Very good, this benchmark looks much better and I can reproduce this locally. Main request:
The reason for this is so that this could, in another PR/later, be probably adapted to other functions doing similar work like Here are a few AI generated review points that would be good to address:
Once this is cleaned, we can probably go ahead with this. |
- Extract predicate-agnostic twoListsTestWithSpatialHashing so hitBoxesCollisionTest simply delegates with collisionTest predicate. - SpatialHashGrid: add _toCell() helper for consistent negative-coord handling, removing redundant |0 after Math.floor. - SpatialHashGrid: add trimPool(maxSize) to cap unbounded pool growth. - SpatialHashGrid: document hash-collision safety and queryToArray duplicate-result responsibility at class and method level. - Document self-collision pick-flag behaviour in JSDoc.
1. Extract
|
Replace O(N×M) brute-force pair enumeration in
hitBoxesCollisionTestwith a spatial hash grid that only tests nearby object pairs, dramatically reducing collision detection cost for scenes with many objects.Problem
The existing
hitBoxesCollisionTestdelegates totwoListsTest, which tests every object in list 1 against every object in list 2 — O(N×M) complexity. At N=1000 objects this takes ~43ms per collision check, exceeding the entire 16.7ms frame budget at 60fps.Solution
New file:
GDJS/Runtime/spatial-hash-grid.ts— A genericSpatialHashGrid<T>class that maps items by their AABB into fixed-size grid cells. Uses hash-based cell keys (prime multiplication + XOR) and pools internal arrays to minimize GC pressure.Modified:
GDJS/Runtime/events-tools/objecttools.ts—hitBoxesCollisionTestnow builds a spatial hash grid when total object count ≥ 32, inserting list-2 objects by AABB and querying only nearby candidates for each list-1 object. Falls back to the originaltwoListsTestfor small lists where grid overhead is not worthwhile.Key design decisions:
twoListsTest— behavioral parity preservedBenchmark Results
At N=1000, collision detection drops from ~43ms (2.6× over frame budget) to ~3.4ms (well within budget).
Scaling also improves: brute-force N=1000 is 442× slower than N=50; spatial hash N=1000 is only 167× slower than N=50.
Benchmark Code
The benchmark suite used to produce these results is available as
benchmarks.patchin the repo root. Apply and run with:```bash
git apply benchmarks.patch
cd benchmarks && npm install && npx vitest bench --run
```
Testing
benchmarks.patch