pylinka
Menu

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.

EntryWhat runs
@pylinka/core/pixipixi v8 scene integration, backend follows the host renderer (§7.2)
@pylinka/core/gpucreateCompiledParticles, best available compiled backend on a bare canvas
@pylinka/core/webgpucompiled WebGPU compute backend, async create
@pylinka/core/webgl2compiled WebGL2 transform-feedback backend, sync create
@pylinka/core/webglinterpreted 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

FieldTypeNotes
rendererpixi Rendererrequired. The backend and the device or context come from it: WebGPU shares the device, WebGL shares the GL context
fixedStepnumberseconds; enables deterministic fixed-step mode
maxDtnumberdt clamp, default 0.05
seednumberdeterministic base seed (capture mode)
onDeviceLost() => voidWebGPU 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.