/** * State shared between copies of pex-gpu loaded in the same realm. * * Module-scope caches (the context and commands WeakMaps, the id counters, the * debug counters) are per-copy, so two copies on a page each build their own: a * context created by one is unknown to the other, and the second silently * allocates a duplicate ring allocator and canvas depth texture that the first * one's dispose() can't reach. Holding them in a registry on `globalThis` under * a well-known symbol lets copies find each other instead. * * Two layers, because sharing is only safe between copies that agree on the * shape of what's shared: * * - The root is **frozen forever**: a counter and an object-to-number WeakMap, * which have no shape to disagree on. Ids live here rather than under a * schema because resources cross copies whatever their version — every * is-a-GpuBuffer check is structural — and two id spaces starting at 1 would * collide in the bind group cache. Never rename or repurpose its fields. * - `schemas` partitions the structured state by {@link SCHEMA}. Copies that agree * share it; copies that don't stay isolated and throw on the first crossing * ({@link assertSchema}) instead of reading fields that aren't there. * * Per-realm, like `Symbol.for` itself: workers and iframes get their own * registry, as they do their own GPUDevice. */ const ROOT: unique symbol = Symbol.for("pex-gpu"); const BRAND: unique symbol = Symbol.for("pex-gpu.schema"); /** * Shape version of everything reachable from the `schemas` registry — not just * its six values (`ContextState`, `CommandsState`, `DebugState`, `TimingState`, * `MipmapPipelines`, `ShaderReflection`) but what they hold: `RingAllocator`, * `FrameState`/`ActivePass`, the cached-pipeline records, the debug counters * and timing pools, and `ShaderReflection` down to `StructLayout`, which * `packStruct` writes against. Bump it in the same commit as any change to * those. * * The edits that need it most are the ones that still typecheck on both sides: * a field added to `CommandsState` that the other copy never flushes, or a * `StructLayout` offset convention the other copy packs against. */ export const SCHEMA = 4; type Registry = Map; interface SharedRoot { nextId: number; objectIds: WeakMap; schemas: Map; } const globals = globalThis as unknown as { [ROOT]?: SharedRoot }; // Truthy only when another copy got here first: this module body runs once. const existing = globals[ROOT]; const root: SharedRoot = (globals[ROOT] ??= { nextId: 1, objectIds: new WeakMap(), schemas: new Map(), }); if (existing) { const incompatible = [...existing.schemas.keys()].filter((s) => s !== SCHEMA); console.warn( incompatible.length ? `pex-gpu: incompatible copies loaded (state schema ${SCHEMA} and ${incompatible.join(", ")}). Each keeps its own state and a context cannot cross between them. Deduplicate pex-gpu in your bundler or import map.` : `pex-gpu: multiple copies loaded (state schema ${SCHEMA}). They share state, but the page ships pex-gpu more than once. Deduplicate it in your bundler or import map.`, ); } const registry = root.schemas.getOrInsertComputed( SCHEMA, (): Registry => new Map(), ); /** * Get or create a process-wide singleton, shared with every loaded copy of * pex-gpu that declares the same {@link SCHEMA}. `name` is its key in the * registry and must be unique within the package. */ export const shared = (name: string, factory: () => T): T => registry.getOrInsertComputed(name, factory) as T; /** * Ids for resources and for objects that carry none (GPUSampler, * GPUTextureView, GPURenderBundle). */ export const nextId = (): number => root.nextId++; export const objectId = (object: object): number => root.objectIds.getOrInsertComputed(object, nextId); /** Record the creating copy's schema so other copies can diagnose a mismatch. */ export const brand = (object: object): void => { Object.defineProperty(object, BRAND, { value: SCHEMA }); }; /** * Throw if an object was branded by a copy this one can't share state with. * Unbranded objects pass: they predate branding, and the caller's own "unknown * context" error says more than a guess would. */ export function assertSchema(object: object): void { const schema = (object as { [BRAND]?: number })[BRAND]; if (schema === undefined || schema === SCHEMA) return; throw new Error( `pex-gpu: this context was created by a copy of pex-gpu with state schema ${schema}, but this copy is schema ${SCHEMA} — incompatible versions are loaded on this page`, ); }