/** * WASM particle-simulation backend (G4): advances the whole particle buffer for * a `ComputeParticleEntity` in one `particle_step` call, instead of the JS * per-particle `updateCPU` loop. This is the CPU fallback path — it runs exactly * when there is no GPU — so the machines that most need it get the batched * kernel. The JS `updateCPU` remains the permanent fallback when WASM cannot * instantiate. * * ## f32, and its own differential oracle * * The particle buffer is a `Float32Array` (matches the WGSL compute shader), so * the kernel (`crates/vectojs-core-rs/src/particle.rs`) commits to **f32** and is * NOT bit-comparable to the f64 transform core. Its oracle is * {@link particleStepReferenceF32} below — a JS f32 reference that rounds every * intermediate to f32 (`Math.fround`) in the same op order and uses * `sqrt(dx*dx+dy*dy)` (not `Math.hypot`, which is correctly-rounded f64). With * those two rules the reference is bit-identical to the kernel, since f32 * add/sub/mul/div/sqrt of f32 operands round once whether done in f32 or * f64-then-`fround`. (The shipped `updateCPU` stays f64 and differs by <1 ULP * per step — the accepted CPU-vs-GPU-class divergence the survey documents.) * * ## SoA transpose * * The render/GPU buffer is AoS stride-8. The backend transposes position/ * velocity/origin/life into per-field f32 arrays ({@link gather}), runs the * kernel, and scatters position/velocity/life back ({@link scatter}); origin and * `size` never change during the sim. */ /** Per-field SoA views over the kernel's linear memory. Position/velocity/life * are read back each frame; origin is upload-once. */ export interface ParticleView { px: Float32Array; py: Float32Array; vx: Float32Array; vy: Float32Array; ox: Float32Array; oy: Float32Array; life: Float32Array; } /** Scalar simulation parameters + the current explosion impulse, passed to * `particle_step`. Mirrors `updateCPU`'s arguments. */ export interface ParticleStepParams { dt: number; mouseX: number; mouseY: number; width: number; height: number; springK: number; damping: number; bounceDamping: number; maxVelocity: number; explosion: { x: number; y: number; force: number; } | null; } export declare class ParticleBackend { private readonly ex; private cap; private view; constructor(instance: WebAssembly.Instance); /** The resident SoA views, valid until the next capacity growth. */ particleView(): ParticleView; /** * Size (and grow, if needed) capacity for `count` particles. Call BEFORE * writing into {@link particleView} — a growth detaches the previous views. */ ensure(count: number): void; /** * Re-create the typed-array views if another backend's allocation grew the * shared linear memory and detached them (see {@link viewsStale}). Call after * {@link ensure} and before {@link gather}/{@link scatter}. */ revalidateViews(): void; /** * Advance `count` particles one step in place. Returns `true` when at least * one live particle is still moving or off-origin beyond epsilon (the fused * `hasPendingAnimations` flag), so the caller need not re-scan the buffer. * * Returns `null` when the kernel REJECTED the call — `count` beyond the * capacity {@link ensure} allocated, or no `particle_init` yet. Nothing was * written, so the caller must NOT {@link scatter} (that would write the * gathered pre-step values back and freeze the simulation) and should fall * back to the JS `updateCPU` path for this frame. See {@link lastStatus}. */ step(count: number, p: ParticleStepParams): boolean | null; /** * Status of the most recent {@link step} — `WASM_STATUS.OK` unless the kernel * declined it. Mirrors {@link TransformBackend.lastStatus}. */ lastStatus: number; /** Transpose the AoS stride-8 buffer into the SoA views (position/velocity/ * life every frame; origin upload-once when `withOrigin`). */ gather(data: Float32Array, count: number, withOrigin: boolean): void; /** Scatter the mutated position/velocity/life back into the AoS buffer. */ scatter(data: Float32Array, count: number): void; private refreshViews; } /** Instantiate synchronously (Node/tests/worker). Returns `null` on failure so * callers fall back to the JS `updateCPU`. */ export declare function instantiateSync(bytes: BufferSource): ParticleBackend | null; /** Instantiate asynchronously (browser main thread). Returns `null` on any * failure so the caller keeps using the JS `updateCPU`. */ export declare function instantiateAsync(bytes: BufferSource): Promise; /** Anything the particle core can be loaded from. */ export type ParticleModuleSource = BufferSource | string | URL | Response | Promise; /** Instantiate from a URL/Response with streaming compilation when available, * falling back to fetch → arrayBuffer → instantiate. Returns `null` on any * failure so the caller keeps the JS path. */ export declare function instantiateStreaming(source: string | URL | Response | Promise): Promise; /** * JS f32 reference for `particle_step`, operating on the same {@link ParticleView} * SoA arrays in place and returning the fused pending-animation flag. This is the * kernel's differential oracle: every intermediate is rounded to f32 with * `Math.fround` in the SAME op order as `particle.rs`, and distance uses * `sqrt(dx*dx+dy*dy)` (NOT `Math.hypot`). It is therefore bit-identical to the * Rust kernel — the differential test asserts exact equality. (This is NOT the * shipped fallback; `ComputeParticleEntity.updateCPU` stays f64.) */ export declare function particleStepReferenceF32(view: ParticleView, count: number, p: ParticleStepParams): boolean;