import { ViewContainer, ViewContainerOptions, RenderPipe, ExtensionType, Renderer, InstructionSet, Application } from 'pixi.js'; import { PylinkaProject, System, ParamDef, SystemBundle } from '@pylinka/graph'; import { K as KnobStore, a as KnobBus } from '../knobs-CVFP2Gxg.js'; import { C as CompiledAtlasOptions, M as MaskTable, a as CompiledMaskOptions } from '../mask-DvK7Ulmr.js'; import '../atlas-D83bp0cn.js'; /** * The GPU simulation boundary (REQUIREMENTS.md §7, §11.5, §13). This is the * seam between the pixi-facing render integration and the GPU backends. Both * compiled backends implement it (see backends.ts): 'webgpu' runs the §13 * compute kernels on the host renderer's device; 'webgl2' runs the compiled * transform-feedback step in the host's GL context. * * Per-frame split: `prepare(dt)` runs on the app tick (CPU scheduling + * uniform staging, §13.11 steps 1–4); `simulate()` + `draw()` run inside the * pixi render pass via PylinkaRenderPipe.execute (§13.11 step 5). simulate() * is a no-op unless a prepare() is pending, so extra renders never * double-integrate. */ /** A 2×3 affine (pixi worldTransform: a, b, c, d, tx, ty). */ interface Affine { a: number; b: number; c: number; d: number; tx: number; ty: number; } interface SimStats { aliveCount: number; overflowCount: number; gpuMs: number | null; } /** * Per-system GPU simulation, owned by a ParticleView. All methods run inside * the host renderer's frame. Steady-state methods allocate nothing (§7.4). */ interface SimBackend { /** Advance the CPU scheduler and stage uniforms for this frame. */ prepare(dtSeconds: number): void; /** Move where new particles are born (view-local px). */ setEmitter(x: number, y: number): void; /** Queue extra spawns for the next frame. */ spawnBurst(count: number): void; /** Emit + update GPU work for the pending prepare (§13.11 step 5). */ simulate(): void; /** Instanced draw into the host's open pass, using the view's transform. */ draw(worldTransform: Affine): void; /** * Re-read an edited project (§7.5): value edits are zero-recompile; a graph * hash change swaps pipelines + resets the pool. False = needs re-create. */ apply(project: PylinkaProject): boolean; /** Reset the pool + scheduler. */ restart(): void; readonly stats: SimStats; destroy(): void; } /** Everything a backend factory needs to build a SimBackend for one system. */ interface SimBackendDeps { backend: 'webgpu' | 'webgl2'; /** 'webgpu' → GPUDevice; 'webgl2' → WebGL2RenderingContext. Opaque here. */ device: unknown; /** The host pixi Renderer — draw() records into its pass / restores state. */ renderer: unknown; system: System; params: ParamDef[]; /** Project-wide knob store shared across systems (KnobBus fan-out). */ knobs: KnobStore; seed?: number; /** resolved texture/atlas for this system (single sprite or animated sheet); * the backend resolves it to sprite + anim. Absent → the soft-disc sprite. */ atlas?: CompiledAtlasOptions; /** rasterised emission mask: opaque texels of a painted image become spawn * positions, replacing the graph's analytic shape. Absent → the shape. */ mask?: MaskTable; /** * The SimBackend of this system's sub-emitter PARENT: this system's * particles are born from that one's, at the event its `output.deathBurst` * names. The backends read each other's GPU buffers directly, so a parent * must be built — and stepped — before its children. */ subParent?: SimBackend; } type SimBackendFactory = (deps: SimBackendDeps) => SimBackend; /** Register a GPU backend factory (the built-ins self-register on import). */ declare function registerSimBackend(kind: 'webgpu' | 'webgl2', factory: SimBackendFactory): void; declare function getSimBackendFactory(kind: 'webgpu' | 'webgl2'): SimBackendFactory | undefined; /** * ParticleView — the pixi v8 renderable (REQUIREMENTS.md §7.3, docs/SPIKE-RESULTS * "S2"). A ViewContainer subclass, so it lives in the scene graph and gets * z-interleaving, culling, and transforms for free. Its `renderPipeId` routes it * to PylinkaRenderPipe; its `worldTransform` is read there to build the render * transform. Add it to a STATIC layer and drive the emitter via the uniform — * never reparent it onto a moving sprite (§7.3 anti-pattern). */ declare class ParticleView extends ViewContainer { readonly renderPipeId = "pylinka"; batched: boolean; /** GPU simulation for this system; undefined until a backend is registered. */ sim: SimBackend | undefined; constructor(options?: ViewContainerOptions); protected updateBounds(): void; } /** * PylinkaRenderPipe — the custom render pass (docs/SPIKE-RESULTS "S2", §13.8, * §13.11). Registered for both WebGL and WebGPU renderers; `execute()` runs the * per-frame emit/update dispatch and the single instanced draw, using the view's * world transform. The GPU work is delegated to the view's SimBackend, which is * gated on the M1.0 spike — until one is registered, execute is a no-op. * * Unlike the Neutrino reference (which batches CPU quads), pylinka does its own * instanced draw straight from the sim buffers — no batcher. */ declare class PylinkaRenderPipe implements RenderPipe { static extension: { readonly type: readonly [ExtensionType.WebGLPipes, ExtensionType.WebGPUPipes]; readonly name: "pylinka"; }; /** Held for the GPU implementation (command encoder / GL context). */ private readonly renderer; constructor(renderer: Renderer); /** Build phase: enqueue this view as an instruction so execute() runs for it. * A non-batched (direct-draw) renderable MUST break pixi's open batch first — * otherwise batchable siblings on either side merge into one batch that is * emitted AFTER this instruction and paints over the particles, regardless of * true scene order (and across the whole RenderGroup). Mirrors pixi's own * MeshPipe.addRenderable for direct-draw meshes. */ addRenderable(view: ParticleView, instructionSet: InstructionSet): void; /** Values are uniform-driven, so a reused instruction needs no rebuild. */ updateRenderable(): void; destroyRenderable(view: ParticleView): void; /** Reuse the instruction set across frames (structure is stable per graph hash). */ validateRenderable(): boolean; /** Draw phase (InstructionPipe.execute): dispatch + instanced draw. */ execute(view: ParticleView): void; } /** * Application plugin (docs/SPIKE-RESULTS "S2"). Runs after the pixi Application * is created, resolves the backend from its renderer, and stashes it on * `app.pylinka` for the runtime to use. */ declare class PylinkaApplicationPlugin { static extension: { readonly type: ExtensionType.Application; readonly name: "pylinka"; }; static init(this: Application): void; static destroy(this: Application): void; } declare function registerPylinka(): void; declare function unregisterPylinka(): void; /** * BackendProvider (REQUIREMENTS.md §7.2). The backend follows the host renderer: * a WebGPU renderer shares its device; a WebGL renderer runs the compiled * WebGL2 transform-feedback backend in the same GL context (so scene * interleaving works). Device is left opaque here — it is handed to the * SimBackend factory. */ interface ResolvedBackend { kind: 'webgpu' | 'webgl2'; /** GPUDevice (webgpu) or WebGL2RenderingContext (webgl2); opaque to core. */ device: unknown; } declare function resolveBackend(renderer: Renderer): ResolvedBackend; /** Register both built-in factories. Runs on import of '@pylinka/core/pixi'. */ declare function registerCompiledBackends(): void; /** * Texture inputs for the pixi runtime (§7.6). A dev in a pixi app already has * images — let them hand a URL, a data-URL, a DOM image, or a pixi Texture * straight to `createPylinka` instead of reaching into the sim's private * `uploadSprite`. Single sprite = a 1×1 grid; an animated sequence = a uniform * sprite sheet (cols/rows) with fps/play/pick. Resolved to the compiled * backends' `CompiledAtlasOptions` (image + grid + anim). */ /** A pixi Texture / TextureSource — we read its underlying `resource`. */ interface PixiTextureLike { source?: { resource?: TexImageSource; }; resource?: TexImageSource; } /** Where a texture's pixels come from. */ type TextureImage = string | TexImageSource | PixiTextureLike; /** A texture/atlas the runtime should render particles with. Same grid + anim * knobs as the compiled backends, but the image may be a URL/data-URL or a * pixi Texture (resolved to a DOM image source before upload). */ interface TextureInput extends Omit { image: TextureImage; } /** Load a URL / data-URL into an HTMLImageElement (CORS-safe for WebGL upload). */ declare function loadImage(src: string): Promise; /** Coerce any TextureImage to a DOM TexImageSource the backends can upload. */ declare function toTexImageSource(src: TextureImage): Promise; /** Resolve a TextureInput into the compiled backends' atlas options. */ declare function resolveTexture(input: TextureInput): Promise; /** * The §11.5 runtime API: createPylinka / createParticleSystem. Each enabled * system becomes a ParticleView (a pixi renderable — add `view` to a STATIC * layer, §7.3) driven by a SimBackend on the host renderer's device/context. * Knobs fan out project-wide through one shared KnobStore; `update(dt)` runs * the CPU side (§13.11 steps 1–4) and the GPU work happens when pixi renders * the views (PylinkaRenderPipe.execute). */ interface CreateOptions { /** The host pixi Renderer (app.renderer). Backend + device derive from it. */ renderer: unknown; /** seconds; enables deterministic fixed-step mode (e.g. 1/60) */ fixedStep?: number; /** dt clamp, default 0.05 */ maxDt?: number; /** deterministic base seed (capture mode) */ seed?: number; onDeviceLost?: () => void; /** Texture for every system — a single sprite or an animated sheet (§7.6). * Accepts a URL/data-URL, a DOM image, or a pixi Texture. */ texture?: TextureInput; /** Per-system textures keyed by System.name — overrides `texture`. */ textures?: Record; /** * Emit only inside a painted area: opaque texels of the image become spawn * positions, replacing the graph's analytic spawn shape. The mask is centred * on the emitter and moves with it. */ emissionMask?: CompiledMaskOptions; /** Per-system emission masks keyed by System.name — overrides `emissionMask`. */ emissionMasks?: Record; /** * Sub-emitter links, `child system name` → `parent system name`: the child's * particles are born from the parent's, at the event the child's * `output.deathBurst` names. Parents are built and stepped before their * children, since the two share GPU buffers. * * An editor project carries these under `subEmitters` keyed by system ID; * they are read from there when this option is absent. */ subEmitters?: Record; /** * Where relative texture paths in the project resolve from. A project * exported with external assets (a `.pylinka.zip`, or the minimal JSON) * holds `assets/flame.png` instead of a data URI; set this to the URL the * project JSON was served from (or its folder) so those load next to it. * Absent, relative paths resolve against the page, like any ``. */ assetBase?: string | URL; } /** Resolve a project-relative asset path against `assetBase`; data/absolute URLs pass through. */ declare function resolveAssetUrl(src: string, assetBase: string | URL | undefined): string; interface ParticleSystemView { /** pixi Container (a ParticleView) — add to a STATIC layer (§7.3). */ readonly view: ParticleView; readonly params: KnobBus; /** Advance the CPU side; no-op work happens at pixi render time. */ update(dtSeconds: number): void; setEmitterPosition(x: number, y: number): void; /** Sample `target.getGlobalPosition()` into the emitter each update (§7.3). */ follow(target: unknown): void; unfollow(): void; spawnBurst(count: number): void; restart(): void; /** Re-read an edited project (zero-recompile for value edits, §7.5). */ apply(project: PylinkaProject): boolean; readonly stats: SimStats; destroy(): void; } interface PylinkaRuntime { /** keyed by System.name */ readonly systems: Record; /** project-wide knob fan-out */ readonly params: KnobBus; /** once per rAF tick; clamps / fixed-steps internally */ update(dtSeconds: number): void; destroy(): void; } /** Build one system (§11.5). The bundle's system runs regardless of `enabled`. */ declare function createParticleSystem(bundle: SystemBundle, opts: CreateOptions): Promise; /** Build every enabled system of a project (§11.5). */ declare function createPylinka(project: PylinkaProject, opts: CreateOptions): Promise; export { type Affine, CompiledMaskOptions, type CreateOptions, MaskTable, type ParticleSystemView, ParticleView, PylinkaApplicationPlugin, PylinkaRenderPipe, type PylinkaRuntime, type ResolvedBackend, type SimBackend, type SimBackendDeps, type SimBackendFactory, type SimStats, type TextureImage, type TextureInput, createParticleSystem, createPylinka, getSimBackendFactory, loadImage, registerCompiledBackends, registerPylinka, registerSimBackend, resolveAssetUrl, resolveBackend, resolveTexture, toTexImageSource, unregisterPylinka };