Skip to content

BuiltinAdapter: lazy-populate broadphase on raycast / queryAABB #1458

@obiot

Description

@obiot

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:

  1. `World.update(dt)` bumps a monotonically-increasing tick counter when it rebuilds the broadphase.
  2. `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

  • `adapter.raycast` and `adapter.queryAABB` on `BuiltinAdapter` return the expected results when called immediately after `world.addChild(r)` (no prior `world.update`).
  • Two `world.update(dt)` calls per frame still only rebuild the broadphase once (i.e. the lazy path is correctly cached against the tick counter).
  • Pointer-event hit-testing still works under matter / planck adapters.
  • Existing parity tests still pass (they call `world.update(16)` defensively — that should remain a no-op rather than a double-rebuild).
  • Drop the workaround note from the BuiltinAdapter Quirks wiki page (quirk Dirty Rectangle mechanism #11).

Related

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions