Skip to content

perf(render): no per-body frame work for large host scenes - #48

Merged
aka-blackboots merged 9 commits into
mainfrom
perf/frame-at-scale
Nov 5, 2026
Merged

aka-blackboots merged 9 commits into
mainfrom
perf/frame-at-scale

Conversation

@aka-blackboots

Copy link
Copy Markdown
Contributor

Summary

Frame cost of large scenes: the kernel no longer does work per body per frame when the host draws many bodies.

  • Camera observed once per renderer frame (OpenGeometry.observeCamera(renderer, camera)): no allocation per draw; a host whose drawables are not kernel bodies (instanced or batched layers) feeds the LOD from its own onBeforeRender.
  • Static display matrices: a body's surface and outline meshes have matrixAutoUpdate = false; install sets their local matrices.
  • One flush per render pass instead of one revision read across the WASM boundary per body per frame. A flush still follows a transform, addChild or removeChild made inside the pass.
  • A drawn body counts as displayed (OpenGeometry.markDisplayed(bodies) for batch stamps). Geometry installs run at the pass end, never inside a render. Bodies dispatch typed geometrychanged and placementchanged.
  • Host-drawn bodies (OpenGeometry.setHostDrawn(bodies, hostDrawn)): three.js skips the body (visible is false) while the kernel keeps its logical visibility for stamps, LOD records, flushes and walks.
  • WorldGraph handle index: node_by_handle is an index lookup with the same results and errors as the scan it replaces.
  • Host groups (OpenGeometry.hostGroup(bodies)): members are validated once; group.drawn() stamps them for the pass in constant time; groups refresh on a temporary target pop and when a member is shown, evaluate only drawn groups, and drop disposed members.

Behaviour changes (owner sign-off needed before merge)

Both are recorded in MIGRATION.md under "Unreleased / Different behaviour":

  1. A body counts as displayed when it is drawn, not only when updateMatrixWorld walks it.
  2. surface and outline meshes are static (matrixAutoUpdate = false). Code that writes their transforms must call updateMatrix().

No version bump: the version is the owner's call at release.

Measured

Host scene: a 12-storey building, 1728x1117 at DPR 2, Apple M2 Max, Chromium, orbit with render on demand.

Change Before After
Host groups (stamps of drawn layers) 47 FPS orbit 116.6–119.9 FPS orbit (screen limit), render CPU about 5 ms
WorldGraph handle index build 8.59 s, first drawing 25.5 s build 7.73 s, first drawing 24.6 s

The rest of the frame work in the host is outside this repository.

Checks

  • npm run check (18 steps) in main/: all pass, on this branch rebased on main at f7a1008.
  • npm run check:full (Playwright on three 0.168 and 0.184): all 20 steps pass, both browser rows included.

Every surface and outline hook observed the camera on every draw, allocating a Vector2 and a 33-value array each time. The observer now returns early when the renderer frame was already seen, reuses one buffer-size vector held in the LOD state, and compares the camera in place. OpenGeometry.observeCamera(renderer, camera) lets a host whose drawables are not kernel bodies, such as instanced or batched layers, feed the LOD from its own onBeforeRender.

Signed-off-by: Vishwajeet Mane <manevishwajeet1@gmail.com>
A forced matrix walk composed both display children of every body on every frame although their local transform only changes when a geometry record is installed. The children now have matrixAutoUpdate off and install updates their local matrices after placing the record origin. The migration guide records the behaviour change for code that writes those meshes' transforms.

Signed-off-by: Vishwajeet Mane <manevishwajeet1@gmail.com>
Every displayed body flushed the graph on its matrix walk, one revision read across the WASM boundary per body per frame. The pass now flushes when it starts, and again only after a transform, addChild or removeChild made inside the pass, so an edit followed by a matrix update in the same task still reaches three.js. The end-of-pass flush for ready shapes is unchanged.

Signed-off-by: Vishwajeet Mane <manevishwajeet1@gmail.com>
A body was displayed only when a host walked it in the frame, so hosts had to walk every body every frame, hidden instance sources included. A body now also stamps itself from its own draw, and OpenGeometry.markDisplayed lets a host batch stamp the bodies it draws. Draw stamps install their geometry at the pass end, never inside a render; a body walked and drawn in one pass is meshed once. The walk stamp stays, so walk-then-sync callers are unchanged. Bodies dispatch typed geometrychanged after an install and placementchanged after a flush that moved them; a throwing listener is reported through the error event and the pass and flush still complete.

Signed-off-by: Vishwajeet Mane <manevishwajeet1@gmail.com>
…e typecheck passes

Signed-off-by: Vishwajeet Mane <manevishwajeet1@gmail.com>
…ay state (K-06)

OpenGeometry.setHostDrawn(bodies, hostDrawn): while true, three.js skips the
body (visible is false) and the kernel keeps a logical visibility for stamps,
LOD records, flushes and walks; giving a body back restores visible exactly
and ignores a disposed body.

Signed-off-by: Vishwajeet Mane <manevishwajeet1@gmail.com>
WorldGraph keeps a (handle, generation) to og_id index, written by commit and
the journal unwind only, so node_by_handle is a lookup with the same result
and errors as the scan it replaces.

Signed-off-by: Vishwajeet Mane <manevishwajeet1@gmail.com>
…pass (K-08)

OpenGeometry.hostGroup(bodies) validates its members once; group.drawn() stamps
them for the pass in constant time, and installs run only for bodies with
pending work; markDisplayed is unchanged.

Signed-off-by: Vishwajeet Mane <manevishwajeet1@gmail.com>
…ember shown, evaluate drawn groups only, and drop disposed members (K-08)

Signed-off-by: Vishwajeet Mane <manevishwajeet1@gmail.com>
@aka-blackboots
aka-blackboots merged commit cdc6895 into main Nov 5, 2026
0 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant