/** * @module tui/canvas/gpu/ca-compositor * * The Living Canvas GPU engine. Owns a WebGPU (Dawn) device and runs the CA * kernels over a source texture, producing RGBA frames for the terminal. * * Design: * - GPU is OPTIONAL: `@kmamal/gpu` is an optionalDependency; when it's absent * or no adapter exists, `createCaCompositor` resolves null and callers fall * back to the classic CPU effects. Nothing ever breaks without a GPU. * - Never blocks the render tick: `step()` submits GPU work and resolves the * LATEST completed frame via pipelined readback (3 buffers in flight). The * scene emits whatever frame is ready — GPU latency shows up as one frame * of lag, never as a stalled terminal. * - Live intent: `setIntent` mutates params (family/energy/palette) with zero * reallocation, so the fast lane can steer the visuals mid-response. * - Deterministic: all motion derives from sim time + a fixed seed. No * Math.random in the frame path. */ import { CA_PALETTES, type CaFamilyName } from "./ca-kernels.js"; export type VisualIntent = { family?: CaFamilyName | number; /** 0 = perfectly still (raw image) … 1 = full aliveness. */ energy?: number; palette?: keyof typeof CA_PALETTES | string; /** 0 keep source colors … 1 fully repaint with palette. */ paletteMix?: number; }; export type CaFrame = { data: Uint8Array; width: number; height: number; frameIndex: number; }; type Device = { createShaderModule: (d: { code: string; }) => unknown; createBuffer: (d: { size: number; usage: number; }) => GpuBuffer; createComputePipeline: (d: unknown) => GpuPipeline; createBindGroup: (d: unknown) => unknown; createCommandEncoder: () => GpuEncoder; queue: { writeBuffer: (b: GpuBuffer, offset: number, data: ArrayBufferView) => void; submit: (cmds: unknown[]) => void; }; destroy?: () => void; }; type GpuBuffer = { mapAsync: (mode: number) => Promise; getMappedRange: () => ArrayBuffer; unmap: () => void; destroy?: () => void; }; type GpuPipeline = { getBindGroupLayout: (i: number) => unknown; }; type GpuEncoder = { beginComputePass: () => { setPipeline: (p: unknown) => void; setBindGroup: (i: number, g: unknown) => void; dispatchWorkgroups: (x: number, y: number) => void; end: () => void; }; copyBufferToBuffer: (src: GpuBuffer, so: number, dst: GpuBuffer, o: number, n: number) => void; finish: () => unknown; }; /** Bootstrap the shared Dawn device once; null when GPU is unavailable. */ export declare function getGpuDevice(): Promise; export declare class CaCompositor { private device; readonly width: number; readonly height: number; private params; private palData; private paramsBuf; private palBuf; private fieldA; private fieldB; private srcBuf; private outBuf; private readbacks; private pending; private pipeline; private bindAB; private bindBA; private flip; private frameIndex; private latest; private srcW; private srcH; private disposed; private stepping; private audioTarget; constructor(device: Device, width: number, height: number, opts?: { seed?: number; }); private makeBind; /** Upload a source RGBA frame (any size — the shader cover-samples it). */ setSource(rgba: Uint8Array | Uint8ClampedArray, w: number, h: number): void; /** Steer the visuals live — called by the intent director at any time. */ setIntent(intent: VisualIntent): void; getIntent(): { family: number; energy: number; paletteMix: number; }; setPalette(name: string): void; /** Mic level 0..1 — the canvas breathes with the room. */ setAudioLevel(level: number): void; setReserveBottomPx(px: number): void; /** * Advance one CA step at sim time t and kick a readback. Returns the LATEST * completed frame (usually the previous step) — never blocks on the GPU. */ step(t: number, dt: number): Promise; dispose(): void; } /** * Create a compositor sized for a pixel region, or null when no GPU. The * region is capped so readback+transport stay comfortably at animation rate * (measured: ~1ms compute, ~20-35ms readback+emit at these sizes). */ export declare function createCaCompositor(pxW: number, pxH: number, opts?: { seed?: number; maxDim?: number; }): Promise; export {};