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/allocSlot — allocSlot 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) voidA 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.