verve.gl multi-instance scenes

Guide: WebGL · Advanced WebGL · live: two scenes, one page.

Two or more ctx.glScene(...) islands can share a single page, each with its own camera, scene, picking, and animation (P7). Every .build() call emits its own <verve-island data-name="GlScene" data-vid="…"> marker; the vid is auto-assigned per island on the page (core/island.zig), so two scenes always get two distinct vids.

The blocker P7 fixed

The bridge instantiates one GlScene wasm chunk per page (cached by name) and calls hydrate(props, len, root_id) once per <verve-island data-name="GlScene">, each with a distinct root_id (the island's vid). Before P7 the chunk's module statics were singletons, so every instance rendered the same shared state — a second scene on the page would stomp the first. (Per-canvas GL resource state — buffers, textures, shaders — already lived per-canvas in the JS bridge; only the wasm-side scene state was shared.)

Per-instance state

Every per-instance static lives in an Inst struct (client/islands/GlScene.zig):

const MAX_INSTANCES = 4;
var instances: [MAX_INSTANCES]Inst = .{Inst{}} ** MAX_INSTANCES;

instances is keyed by vid via findSlot/allocSlotallocSlot claims a free slot on first hydrate for that vid and resets it to a fresh Inst{}. Up to MAX_INSTANCES = 4 GlScene islands can be co-resident on one page; a fifth hydrate finds the pool exhausted and is ignored.

Routing — glscene_select

export fn glscene_select(root_id: u32) void

A chunk export that sets a current: ?*Inst pointer. The bridge calls it before every frame / event / asset-callback / restore dispatch, deriving the vid from the canvas's enclosing <verve-island data-vid> (or, for in-flight async gl_load callbacks, the vid captured at hydrate time). Every other export keeps its original signature and operates on current, so the generic gl-driving contract in the bridge is unchanged — the guarded select is a no-op for chunks without a glscene_select export (e.g. GlDemo, non-GL islands).

glscene_unmount(root_id) reclaims a slot on canvas disconnect and decrements the live-instance count so the asset region can re-reset on the next fresh population.

Asset region sharing

The 4 MB page-scoped bump allocator (client/asset_region.zig, region_capacity = 4 * 1024 * 1024) resets only when the first instance of a fresh population hydrates (live_count transitions 0 → 1) — while one or more instances are live they share the region; a full unmount→remount of every instance re-resets it. N co-resident scenes share the same 4 MB budget; raise region_capacity if a page's scenes together need more, though alloc degrades gracefully on exhaustion rather than trapping.

Co-locating a different stateful chunk (e.g. GlDemo) alongside GlScene on the same page is a separate, still-unsolved case: different chunks still overlap at wasm data base 0x1000 — that cross-chunk memory-partition problem is out of scope for P7, which only covers multiple instances of the same chunk.

Demo (/gl-multi)

Two independent GlScene islands side by side in a 2-column grid:

const scene_a = ctx.glScene(.{ .src = "/gl/demo.vmesh", .env = "/gl/studio.venv" })
    .camera(.{ .distance = 4, .pitch = 0.3, .yaw = 0.6 })
    .light(.{ .dir = .{ -0.4, -0.7, -0.6 }, .intensity = 3.0 })
    .autoRotate(0.5)
    .build();

const scene_b = ctx.glScene(.{ .src = "/gl/shadow.vmesh", .env = "/gl/studio.venv" })
    .camera(.{ .distance = 9, .pitch = 0.55, .yaw = -0.5 })
    .light(.{ .dir = .{ -0.45, -0.82, -0.35 }, .intensity = 3.2 })
    .autoRotate(-0.35)
    .build();

scene_a (demo.vmesh) auto-rotates at 0.5 rad/s; scene_b (shadow.vmesh, a cube on a floor) auto-rotates at -0.35 rad/s — opposite directions, so the two instances are visibly independent at a glance. Both share the same studio.venv environment. No bespoke controls: drag either canvas to orbit it independently of the other. Under the hood each panel is its own Inst slot selected by its own vid — proof that the two islands never cross-talk.

See also: orthographic projection, whose perspective/orthographic comparison is itself two co-resident GlScene instances.