/** * The four invisible WASM accelerators, and the per-frame report that describes * what each of them actually did. * * Extraction 1 of the `Scene.ts` decomposition * (`forge/decisions/file-decomposition-2026-08.md` §2). `Scene` keeps every * public method and getter it had — `setTransformBackend`, `enableWasmHitTest`, * `accelerators`, and the rest — and each one now delegates here. The public API * is byte-identical; only where the state lives changed. * * ## What this owns, and what it deliberately does not * * It owns the four backend handles, the shared runtime, the resident transform * store, and every field the accelerator report reads. * * It does NOT own three things that sit inside `Scene.ts`'s WASM comment region * but belong to other domains — the domain banners were added first precisely to * make that visible before any code moved (carryctx `DEC-0016`): * * - `_ensureHitGrid` / `_findEntityAtWasm` are the hit-test broad phase * (`HitTester`, extraction 4). They *read* this facade. * - `_tickBatchedDrivers` is the scheduler's batched driver tick * (`RenderScheduler`, extraction 6). It reads the anim backend from here. * - `_computeEntitiesFor` walks the tree for `ComputeParticleEntity` instances * and is consumed by the render walk. Only its cache key — the structure * version — is store state, so the version lives here and the walk stays on * `Scene`. * * ## Why the reporting fields live here rather than with their writers * * `transformReason` is written by the render walk, `animReason` by the driver * tick, `hitReason` by the hit-grid build, `particleReason` by the particle * pass — four domains, four different extractions. They are collected here * anyway because {@link report} is the single reader that has to make them * consistent, and splitting them across four collaborators would leave that one * getter reaching into all four. Each writer sets its own field through a public * property on this object; none of them touches `Scene`'s privates. * * ## The one fact this cannot know * * `particle.available` is true when a WASM particle backend is installed **or** * WebGPU is live, and WebGPU device state belongs to `ContextAndResize` * (extraction 5). So {@link report} takes it as an argument rather than * reaching for it. That is the whole of this facade's dependency on the rest of * `Scene`. */ import type { Entity } from '../Entity'; import { type WasmTransformBackend } from '../../wasm/backend'; import { type CoreModuleSource, type CoreWasmRuntime } from '../../wasm/runtime'; import type { HitTestBackend } from '../../wasm/hit-backend'; import type { AnimBackend } from '../../wasm/anim-backend'; import type { ParticleBackend } from '../../wasm/particle-backend'; /** * Why an accelerator did or did not run on the most recent frame. * * `'active'` is the only value that means the accelerator ran. Everything else * is a distinct decline, kept separate because they call for different actions: * `'not-installed'` means enable it, `'below-gate'` means the workload is too * small to be worth it (working as designed), and `'rejected'` means the kernel * refused its arguments — a fault worth reporting, not a tuning outcome. * * Declared here rather than in `Scene.ts` because this is the class that owns * every field of this type. `Scene.ts` re-exports all three accelerator types, * so `@vectojs/core`'s barrel keeps publishing them unchanged — `AcceleratorReason` * in particular is consumed by `@vectojs/devtools`. */ export type AcceleratorReason = /** Ran on this frame. */ 'active' /** No backend installed; the JS path is the permanent fallback. */ | 'not-installed' /** Installed, but the per-frame gate chose JS (workload below threshold). */ | 'below-gate' /** Installed and gated in, but the kernel rejected the call and wrote nothing. */ | 'rejected' /** Not applicable to this pass (e.g. a non-main renderer, or nothing to do). */ | 'not-applicable'; /** * One accelerator's per-frame status, read from {@link Scene.accelerators}. * * The pair exists because `available` and `activeThisFrame` genuinely differ: * before this shape, `transformBackend`/`animBackend` reported only that a * backend was *installed*, which invites concluding an accelerator is doing work * when its gate never opens. Read `activeThisFrame` for what actually happened * and `reason` for why. */ export interface AcceleratorStatus { /** A backend is installed and could run, gate permitting. */ available: boolean; /** It ran on the most recent frame. */ activeThisFrame: boolean; /** Why it did or did not run. */ reason: AcceleratorReason; /** Which implementation actually did the work on the most recent frame. */ path: string; } /** * Per-frame status of every invisible accelerator, read from * {@link Scene.accelerators}. Each is independent: a scene can compose * transforms in WASM while ticking drivers in JS. */ export interface AcceleratorReport { /** World-matrix composition (`compose_simd`). */ transform: AcceleratorStatus; /** Batched property drivers (`spring_step`/`tween_step`). */ animation: AcceleratorStatus; /** Hit-test broad phase (`hit_build`/`hit_query`) and its gather source. */ hitTest: AcceleratorStatus; /** Particle simulation — WebGPU compute, the WASM CPU kernel, or JS. */ particle: AcceleratorStatus; } export declare class WasmBackendFacade { /** * The main tree's root. * * Held by value rather than as a `Scene` reference: it is assigned exactly * once in `Scene`'s constructor and never reassigned, and taking it directly * is what keeps this class from holding a back-edge to its facade. Only the * main tree is ever composed through the store — overlays render on the JS * path, so the overlay root is not needed here. */ private readonly root; constructor(root: Entity); private _transform; private _mode; private _treeStore; private _slotEntity; private _inputs; private _world; private _structureVersion; private _storeStructureVersion; /** * Consecutive `uploadRuns` rejections. Reset by any success and by * {@link setTransform}; at {@link WASM_UPLOAD_REJECT_LIMIT} the backend mode * flips to `'js'` permanently. */ private _uploadRejections; /** Latch for the permanent-fallback warning, which fires from a per-frame path. */ private hasWarnedUploadFallback; /** * Whether `compute_aabbs` has run against the current frame's world matrices. * The AABB pass is only meaningful after a `compose_*`, so the fused gather * must not read the views before then. */ private _aabbsFresh; /** The installed transform backend, or `null` on the JS path. */ get transform(): WasmTransformBackend | null; /** Which backend composes world matrices for the main render walk. */ get mode(): 'js' | 'wasm'; /** * Whether the render walk should source world matrices from the store this * frame: a backend is installed AND the mode has not fallen back to JS. */ get transformActive(): boolean; /** Store slot -> entity, for the render walk and the fused hit gather. */ get slotEntity(): Entity[]; /** Bumped by every topology change; the store layout's cache key. */ get structureVersion(): number; /** Which structure version the resident store layout was built for. */ get storeStructureVersion(): number; /** Consecutive `uploadRuns` rejections; see {@link WASM_UPLOAD_REJECT_LIMIT}. */ get uploadRejections(): number; /** Invalidate the resident WASM store layout; the next wasm-mode frame * rebuilds it. Called by `Entity.add`/`remove` (topology changes only). */ markStructureChanged(): void; /** * Install (or clear) a WASM transform backend. Passing a backend switches the * main render walk onto it; passing `null` reverts to the JS path. */ setTransform(backend: WasmTransformBackend | null): void; /** * The one WASM instance this Scene's accelerators share. * * Each `enableWasm*` used to instantiate the binary itself, so enabling all * four compiled the same module four times and held four linear memories. The * Rust crate already keeps transform/anim/hit/particle in separate statics, so * one instance serves all of them without aliasing. The compiled module is * cached globally; the instance is per-Scene, which is the isolation that * actually matters. */ private _runtime; /** The shared WASM runtime, if one has been loaded. */ get runtime(): CoreWasmRuntime | null; /** * Install a pre-built runtime, so several Scenes can share one compile while * each keeps its own stores. Pass `null` to detach (backends already installed * keep working; only subsequent `enableWasm*` calls re-load). */ setRuntime(runtime: CoreWasmRuntime | null): void; /** * Load (or reuse) this Scene's shared WASM runtime. * * Returns `null` on any failure — CSP `wasm-unsafe-eval`, a 404, corrupt bytes, * unsupported SIMD — so every caller keeps its JS path. Failure is the default * state here, not an error path. */ ensureRuntime(source: CoreModuleSource): Promise; private _hit; private _anim; private _particle; /** The installed hit-test backend, or `null` for the JS depth-first walk. */ get hit(): HitTestBackend | null; /** Install (or clear) a WASM hit-test backend. */ setHit(backend: HitTestBackend | null): void; /** The installed batched-animation backend, or `null` for the JS tick. */ get anim(): AnimBackend | null; /** Install (or clear) a WASM batched-animation backend. */ setAnim(backend: AnimBackend | null): void; /** The installed particle backend, or `null` for the JS `updateCPU` path. */ get particle(): ParticleBackend | null; /** Install (or clear) a WASM particle backend. */ setParticle(backend: ParticleBackend | null): void; /** * Why the transform accelerator did or did not run on the most recent frame. * Written by the render walk and {@link syncStore}. */ transformReason: AcceleratorReason; /** Why the batched-driver accelerator did or did not run. */ animReason: AcceleratorReason; /** * Why the hit-test accelerator did or did not serve the last pointer query. * The grid is built lazily on demand, not every frame, so this describes the * most recent BUILD. Starts at `'not-installed'` because that is the truth * before a backend exists; the grid build moves it to `'not-applicable'` once * one is installed but nothing has queried yet. */ hitReason: AcceleratorReason; /** Why the particle accelerator did or did not run. */ particleReason: AcceleratorReason; /** Which particle implementation actually simulated the most recent frame. */ particlePath: string; /** * Whether the last grid build sourced its AABBs from the WASM transform store * rather than recomputing them in JS. Diagnostic only — both paths must * produce the same entity for a given point. */ hitFusedGather: boolean; /** Whether the WASM batch path actually ran on the most recent frame. */ animBatchedLastFrame: boolean; /** Frame the hit grid was last built for; `-1` forces a rebuild. */ hitGridFrame: number; /** Whether that build succeeded (did not overflow its item budget). */ hitGridOk: boolean; /** * Per-frame status of every invisible accelerator: whether each is installed, * whether it actually ran on the most recent frame, and why. * * `webgpuActive` is passed in because WebGPU device state belongs to * `ContextAndResize`, not here — see the class comment. */ report(webgpuActive: boolean): AcceleratorReport; /** * Compose the whole main tree's world matrices through the resident WASM store * and return the world-matrix views for the render walk to read. Rebuilds the * store layout (slots + runs) only when the tree structure changed since the * last rebuild; otherwise it just gathers current transforms into the resident * input view and runs the kernel. Returns `null` if there is no backend. */ syncStore(): ReturnType | null; /** * Run the WASM world-AABB pass over the current frame's world matrices, so the * fused hit gather can read AABBs straight out of the store. * * Local bounds are uploaded here rather than in the per-frame transform sync * because `getBounds()` is a virtual call that allocates a rect on most * entities — paying it every frame for a query that may never come would move * cost onto the render path to save it on hover. Returns `false` if any entity * cannot supply bounds through the store, so the caller uses the JS gather. */ ensureAabbs(): boolean; }