Context
In 19.5, `raycast(from, to)` and `queryAABB(rect)` became portable across every adapter — same `RaycastHit` shape, same `queryAABB → Renderable[]` contract. The wiki even pins this in the new capability matrix.
But there's one parity gap that survived 19.5: on `BuiltinAdapter`, the broadphase is only populated inside `World.update(dt)` (`packages/melonjs/src/physics/world.js:285-289`):
```js
update(dt) {
if (this.physic === "builtin" || hasRegisteredEvents() === true) {
this.broadphase.clear();
this.broadphase.insertContainer(this);
}
this.step(dt);
}
```
`raycast` and `queryAABB` walk that broadphase via `world.broadphase.retrieve(...)`. Calling either immediately after `world.addChild(r)` returns nothing because the body hasn't been inserted yet.
Matter and planck don't have this gap — they index on `addBody` directly, so queries work the moment a body is registered.
Repro
```ts
const adapter = new BuiltinAdapter();
const world = new World(0, 0, 800, 600, adapter);
const wall = new Renderable(200, 200, 40, 40);
wall.alwaysUpdate = true;
wall.bodyDef = { type: "static", shapes: [new Rect(0, 0, 40, 40)] };
world.addChild(wall);
adapter.raycast(new Vector2d(0, 220), new Vector2d(400, 220));
// → null (broadphase still empty)
world.update(16);
adapter.raycast(new Vector2d(0, 220), new Vector2d(400, 220));
// → { renderable: wall, point, normal, fraction } (now found)
```
This surfaced when writing the cross-adapter parity tests in #1457 (`packages/matter-adapter/tests/parity.spec.ts` + `packages/planck-adapter/tests/parity.spec.ts`), where every `raycast` / `queryAABB` test has to call `world.update(16)` before querying — a no-op for matter / planck, mandatory for builtin.
Why not just move the broadphase clear/insert into BuiltinAdapter.step?
That breaks pointer events on non-builtin adapters. The `|| hasRegisteredEvents()` branch in `world.update` exists so the broadphase stays populated for pointer hit-testing even when the active physics adapter doesn't need it. Move the populate into builtin's step and pointer events stop working under matter / planck.
Proposed fix
Lazy populate-on-query in `BuiltinAdapter`, guarded by a tick counter so it's a no-op when `world.update` already rebuilt this frame:
- `World.update(dt)` bumps a monotonically-increasing tick counter when it rebuilds the broadphase.
- `BuiltinAdapter.raycast` and `.queryAABB` check the tick counter before walking the broadphase. If the body set has changed since the last rebuild (or if the broadphase has never been populated this frame), rebuild it inline.
Cost:
- Pointer-event path: unchanged.
- `world.update`-driven game loop: unchanged (one rebuild per frame, same as today).
- Standalone `raycast` / `queryAABB` call without a prior `world.update`: one rebuild on the first call, free on subsequent calls within the same frame.
Closes the only remaining cross-adapter parity gap in the portable physics surface.
Acceptance
Related
Context
In 19.5, `raycast(from, to)` and `queryAABB(rect)` became portable across every adapter — same `RaycastHit` shape, same `queryAABB → Renderable[]` contract. The wiki even pins this in the new capability matrix.
But there's one parity gap that survived 19.5: on `BuiltinAdapter`, the broadphase is only populated inside `World.update(dt)` (`packages/melonjs/src/physics/world.js:285-289`):
```js
update(dt) {
if (this.physic === "builtin" || hasRegisteredEvents() === true) {
this.broadphase.clear();
this.broadphase.insertContainer(this);
}
this.step(dt);
}
```
`raycast` and `queryAABB` walk that broadphase via `world.broadphase.retrieve(...)`. Calling either immediately after `world.addChild(r)` returns nothing because the body hasn't been inserted yet.
Matter and planck don't have this gap — they index on `addBody` directly, so queries work the moment a body is registered.
Repro
```ts
const adapter = new BuiltinAdapter();
const world = new World(0, 0, 800, 600, adapter);
const wall = new Renderable(200, 200, 40, 40);
wall.alwaysUpdate = true;
wall.bodyDef = { type: "static", shapes: [new Rect(0, 0, 40, 40)] };
world.addChild(wall);
adapter.raycast(new Vector2d(0, 220), new Vector2d(400, 220));
// → null (broadphase still empty)
world.update(16);
adapter.raycast(new Vector2d(0, 220), new Vector2d(400, 220));
// → { renderable: wall, point, normal, fraction } (now found)
```
This surfaced when writing the cross-adapter parity tests in #1457 (`packages/matter-adapter/tests/parity.spec.ts` + `packages/planck-adapter/tests/parity.spec.ts`), where every `raycast` / `queryAABB` test has to call `world.update(16)` before querying — a no-op for matter / planck, mandatory for builtin.
Why not just move the broadphase clear/insert into BuiltinAdapter.step?
That breaks pointer events on non-builtin adapters. The `|| hasRegisteredEvents()` branch in `world.update` exists so the broadphase stays populated for pointer hit-testing even when the active physics adapter doesn't need it. Move the populate into builtin's step and pointer events stop working under matter / planck.
Proposed fix
Lazy populate-on-query in `BuiltinAdapter`, guarded by a tick counter so it's a no-op when `world.update` already rebuilt this frame:
Cost:
Closes the only remaining cross-adapter parity gap in the portable physics surface.
Acceptance
Related