API
The runtime surface of @pylinka/core. The hot-path methods
(update, set, setEmitterPosition, spawnBurst)
allocate nothing, so you can call them every frame without feeding the collector.
Backends
Every graph compiles to GPU code through @pylinka/compiler: WGSL compute kernels
on WebGPU, one fused transform-feedback step shader on WebGL2. Inline values and knobs live in a
vec4 uniform table, so value edits and knob moves never recompile. Only
structural edits rebuild pipelines, and those reset the pool.
| Entry | What runs |
|---|---|
@pylinka/core/pixi | pixi v8 scene integration, backend follows the host renderer (§7.2) |
@pylinka/core/gpu | createCompiledParticles, best available compiled backend on a bare canvas |
@pylinka/core/webgpu | compiled WebGPU compute backend, async create |
@pylinka/core/webgl2 | compiled WebGL2 transform-feedback backend, sync create |
@pylinka/core/webgl | interpreted WebGL2 engine, and the only one carrying emission masks, animated atlases and sub-emitters |
createPylinka() on a pixi stage
Run a whole project inside a pixi stage; each system becomes a renderable view.
import { registerPylinka, createPylinka } from '@pylinka/core/pixi';
registerPylinka(); // once, before app init
const app = new Application();
await app.init({ preference: 'webgpu' }); // or 'webgl', the backend follows
const fx = await createPylinka(project, { renderer: app.renderer });
app.stage.addChild(fx.systems['sparks'].view); // a STATIC layer (see Core concepts)
app.ticker.add((t) => fx.update(t.deltaMS / 1000));
fx.params.set('windPower', 40); // live, zero recompile CreateOptions
| Field | Type | Notes |
|---|---|---|
renderer | pixi Renderer | required. The backend and the device or context come from it: WebGPU shares the device, WebGL shares the GL context |
fixedStep | number | seconds; enables deterministic fixed-step mode |
maxDt | number | dt clamp, default 0.05 |
seed | number | deterministic base seed (capture mode) |
onDeviceLost | () => void | WebGPU device-loss hook |
PylinkaRuntime
interface PylinkaRuntime {
readonly systems: Record<string, ParticleSystemView>; // keyed by System.name
readonly params: KnobBus; // project-wide fan-out
update(dtSeconds: number): void; // once per rAF tick
destroy(): void;
} ParticleSystemView
A PixiJS container. Add view to a static layer (see Core concepts).
interface ParticleSystemView {
readonly view: Container;
readonly params: KnobBus;
update(dtSeconds: number): void;
setEmitterPosition(x: number, y: number): void;
follow(target: Container): void; // samples getGlobalPosition() each update
unfollow(): void;
spawnBurst(count: number): void;
restart(): void; // pool + scheduler reset
apply(project: PylinkaProject): boolean; // live edit — zero recompile for value edits
readonly stats: { aliveCount: number; overflowCount: number; gpuMs: number | null };
destroy(): void;
}
The stats fields are plain numbers refreshed every 30 frames. Reading them never
triggers a GPU readback, so a debug overlay costs nothing.
KnobBus
interface KnobBus {
set(name: string, x: number, y?: number, z?: number, w?: number): void; // O(1), alloc-free
get(name: string): number; // .x component
}
Pass a second component for a vec2 knob. That is how a cursor drives
field.obstacle or a collide node: write the pointer position into the
knob every frame and the bound ports follow, with no recompile and no graph edit.
fx.params.set('cursor', pointerX, pointerY);
fx.params.set('cursorVel', smoothedVx, smoothedVy); // feeds the obstacle's carry term Single-system helper
When you only need one system, skip the project wrapper:
function createParticleSystem(
bundle: SystemBundle,
opts: CreateOptions,
): Promise<ParticleSystemView> Compiled backends on a bare canvas
Without pixi, drive a canvas directly. The handle is the same on either backend:
import { createCompiledParticles } from '@pylinka/core/gpu';
const fx = await createCompiledParticles(canvas, project); // webgpu, else webgl2
fx.setEmitter(x, y);
requestAnimationFrame(function tick() {
fx.update(1 / 60);
requestAnimationFrame(tick);
});
fx.setKnob('windPower', 40); // value-table write, never recompiles
fx.apply(editedProject); // structural edits rebuild pipelines and reset the pool
Options: backend ('auto' | 'webgpu' | 'webgl2'),
systemName, zoom, sizeScale, seed,
atlas (uniform grid; the cell is picked by output.initTexIndex),
onRecompile. Emission masks, animated atlas sequences and sub-emitters live only
on the interpreted engine, @pylinka/core/webgl.
Context and device loss
Losing the GPU underneath you is routine on phones. A backgrounded tab, a driver reset or another page hogging the GPU all take the context away, and every buffer, program and pipeline dies with it.
On WebGL2, both the interpreted and the compiled backends recover on their own.
They listen for webglcontextlost and call preventDefault() on it,
which is what makes the browser willing to give the context back at all. While it is gone
update() does nothing and contextLost reads true, so a render loop
that keeps ticking costs nothing and throws nothing. When
webglcontextrestored arrives, the effect rebuilds every GPU object and carries your
knob values and emitter position across. Particles alive at the moment of the loss are gone for
good, because their buffers went with the context, so the pool refills from the emitter.
const fx = await createCompiledParticles(canvas, project, {
onContextLost: () => hud.showReconnecting(),
onContextRestored: () => hud.hideReconnecting(),
});
if (fx.contextLost) { /* the effect is paused, nothing to do */ }
On WebGPU the device is shared per canvas, and on the pixi path it belongs to
the renderer, so Pylinka does not go behind your back and replace it. A lost device is detected,
the system stops queueing work rather than piling up validation errors,
contextLost flips, and both onContextLost and the older
onDeviceLost hook fire. Re-acquiring a device and rebuilding is the host's call.