import { TgpuBindGroupLayout } from 'typegpu'; import { AnyWgslStruct, Infer } from 'typegpu/data'; import { ComputeStep, KitComputePipeline } from '../compute'; import { GpuFragmentParams, KitTexture } from '../contract'; import { d } from '../kit'; /** A value with a lower setting for coarse-pointer (phone/tablet) GPUs. */ export interface DeviceTier { desktop: number; mobile: number; } /** * Pick the coarse-pointer or the desktop value. * * BUILD-TIME CONSTANT BY DESIGN for a resolution: the accumulator length lives in a module-scope * `tgpu.bindGroupLayout`, so it must be decided at import time, not per composition. SSR and the * GPU-free tests resolve to `desktop`. */ export declare function resolveDeviceTier(tier: DeviceTier): number; /** * The square render/accumulator resolution for this device tier. * * PAIR THIS WITH A FIXED `SIZE_REF_RES`. Agent body sizes are authored in texels, so deriving them * from the live (tiered) resolution makes the same preset render a *larger* flock on a phone — * fewer texels across the same canvas means each texel covers more screen. Size math must divide by * {@link SIZE_REF_RES}; only the buffer/texture extents use the tiered value. */ export declare function resolveRenderRes(tier: DeviceTier): number; /** The fixed resolution agent sizes are authored against, decoupling them from {@link resolveRenderRes}. */ export declare const SIZE_REF_RES = 1024; /** * Fixed-point gains for the atomic energy accumulators. * * The resolve's alpha curve is `1 − e^(−1.6·E)` with `E = raw/255`, which reaches 1.0 only * asymptotically. A HARD shape's interior returns coverage exactly 1, so at a gain of 255 its * centre would resolve to `1 − e^(−1.6)` ≈ 0.80 — a solid dot rendering 80% grey. `HARD` * over-drives the scale by 2.5× so those interiors saturate; `SOFT` keeps the true scale because a * Gaussian skirt is *supposed* to top out below opaque. */ export declare const FIXED_POINT_GAINS: { readonly HARD: 637; readonly SOFT: 255; }; /** * The R2 sequence's irrational strides (the 2D "plastic constant" generalization of the golden * ratio): `frac(0.5 + i·α)` gives quasi-uniform coverage for ANY prefix of `i`, so a count slider * can grow or shrink the population without re-seeding and without leaving holes. Positions derived * from the index alone need no storage. */ export declare const R2_ALPHA: readonly [0.7548776662466927, 0.5698402909980532]; /** * The R3 sequence's strides — the same construction one dimension up. Used per FRAME rather than * per agent: stepping a sub-cell offset along R3 dithers a voxel lattice evenly over time, which is * what keeps a density grid from standing in coherent moiré with a shape boundary. */ export declare const R3_ALPHA: readonly [0.8191725133961645, 0.6710436067037893, 0.5497004779019703]; /** Per-frame values every agent shader derives from the renderer's frame params. */ export interface AgentFrame { /** Frame delta clamped to [1ms, 33ms] — one stalled frame must not teleport the simulation. */ dt: number; /** Viewport aspect (guarded against a zero-height canvas). */ aspect: number; pointerX: number; pointerY: number; } /** The subset of the renderer's frame params an agent simulation reads. */ export interface AgentFrameParams { pointer?: { x: number; y: number; }; deltaTime?: number; dimensions?: { width: number; height: number; }; } /** * Normalize the renderer's frame params: dt clamp, aspect, pointer defaults. * * The dt clamp is load-bearing in both directions. The floor stops a division blowing up on a * 0ms frame; the 33ms ceiling means a tab-switch or a long GC pause advances the sim by one slow * frame instead of integrating a one-second step and throwing every agent off-screen. * * `fallbackAspect` is what a shader wants when the canvas has no height yet — 16/9 for a * generator that fills the viewport, 1 for one whose grid is refit to the aspect each frame. */ export declare function readAgentFrame(frameParams: unknown, fallbackAspect?: number): AgentFrame; /** * The numeric prop reader every one of these shaders declared inline (10 copies). * * Why a fallback per call rather than reading the prop's declared default: a dynamic prop (a range * map, a mouse binding) can resolve to a non-number for a frame, and a kernel uniform must never * receive `undefined`. */ export declare function makeCpuValueGetter(getCpuValue: (prop: string) => unknown): (key: string, fallback: number) => number; /** Clamp a raw count prop into `[min, cap]` — the runtime cap, never the prop's declared range. */ export declare function clampAgentCount(raw: number, min: number, cap: number): number; /** Inclusive texel bounds of a splat window, already clipped to the accumulator. */ export declare const SplatWindow: d.WgslStruct<{ x0: d.I32; x1: d.I32; y0: d.I32; y1: d.I32; }>; /** * The splat window for an agent living in WORLD space — x ∈ [0, aspect], y ∈ [0, 1], 1 unit == the * canvas height — splatting into a SQUARE `res × res` accumulator. * * Two things make this worth having in one place. First, the window is sized to the agent's ACTUAL * reach rather than a fixed box, which is a correctness fix and not just an optimization: a fixed * ±16-texel window hard-clipped large `arrow` / `streak` / `glow` shapes into squares. Second, the * radius is PER AXIS. The accumulator is square but the world domain is `aspect` wide, so one * x-texel covers `aspect` times as much world as a y-texel — hence the divide. Getting that wrong * is invisible at 1:1 and stretches every agent on a wide canvas. * * `reach` is the world-space accept radius (shape extent + AA margin). Clipping happens once here, * so the caller's inner loop carries no bounds test, and an off-screen agent clips to an empty * range — which is also the off-target early-out. */ export declare const worldSplatWindow: import('typegpu').TgpuFn<(pos: d.Vec2f, reach: d.F32, aspect: d.F32, res: d.F32) => d.WgslStruct<{ x0: d.I32; x1: d.I32; y0: d.I32; y1: d.I32; }>>; /** * The splat window for an agent already projected to TEXEL coordinates, with an isotropic radius * (the projected-3D consumers, whose render target has square texels so no aspect term survives). * * `last` is the inclusive maximum texel per axis, passed rather than derived because the target * extent can be a per-frame uniform (a target fitted to the frame aspect). */ export declare const texelSplatWindow: import('typegpu').TgpuFn<(center: d.Vec2f, rad: d.I32, last: d.Vec2i) => d.WgslStruct<{ x0: d.I32; x1: d.I32; y0: d.I32; y1: d.I32; }>>; /** * Accumulated energy → coverage alpha: `1 − e^(−k·energy)`. Saturating, so overlapping agents * deepen toward opaque without ever clipping, and empty texels resolve to exactly 0 (fully * transparent gaps, so the field composites over anything). * * WHY THE READ-THEN-ZERO ISN'T HERE. Every consumer's resolve reads its accumulator with * `atomicLoad` and immediately `atomicStore`s 0 — one thread owns each cell and nothing else is in * flight, so that needs no exchange and replaces a separate full-target clear pass. It cannot be * shared as a `tgpu.fn` because WGSL cannot take a storage array as a function parameter, so the * two-line load/store stays in each shader, next to the layout entry it names. */ export declare const energyCoverageAlpha: import('typegpu').TgpuFn<(energy: d.F32, k: d.F32) => d.F32>; /** * Semi-implicit Euler with exponential drag and a speed clamp — the integrator the force-based * consumers share. * * Semi-implicit (velocity updated first, then position from the NEW velocity) is what keeps an * underdamped spring stable at the frame rates a browser actually delivers; explicit Euler gains * energy and eventually explodes. `dragMul` is `exp(−drag·dt)` computed on the CPU, so the decay is * frame-rate independent rather than a per-frame multiply. The clamp is a safety net, not a feel * knob: without it one bad frame's force can launch an agent out of the domain entirely. */ export declare const integrateSemiImplicitEuler: import('typegpu').TgpuFn<(vel: d.Vec3f, force: d.Vec3f, dt: d.F32, dragMul: d.F32, maxSpeed: d.F32) => d.Vec3f>; /** How many threads a step dispatches. */ export type AgentThreads = /** The runtime agent count, passed to `frame({count})`. */ 'agents' /** `maxAgents` — the static bound, for a one-shot init/re-seed over every slot. */ | 'max' /** A per-frame 2D grid, passed to `frame({grid})`. */ | 'grid' /** A fixed size given by `size` (a full render target, a voxel grid). */ | 'fixed'; export interface AgentPipelineSpec { /** A module-scope `tgpu.fn` kernel: `(i)` for 1D dispatch, `(x, y)` for `'grid'` / 2D `'fixed'`. */ kernel: ((i: number) => void) | ((x: number, y: number) => void); threads: AgentThreads; /** Required for `threads: 'fixed'`; its length picks the 1D or 2D dispatch. */ size?: [number] | [number, number]; /** Chain `extraBindGroup` onto this pipeline (a shape family's own SDF resources). */ extra?: boolean; } export interface AgentSystemConfig { /** * The shader's module-scope bind group layout. Buffers are allocated by introspecting it, and * the kernels reference it directly — so its key names are the WGSL identifiers. */ layout: TgpuBindGroupLayout; /** Schema of the layout's `uniform` entry — the per-frame CPU-derived inputs. */ params: TParams; /** The output storage texture the resolve writes and the fragment samples. */ output: { /** Layout key of the `storageTexture` entry. */ key: string; /** Key in the returned `outputs` record (what the fragment reads off `computeOutputs`). */ name: string; size: [number, number]; format: 'rgba16float'; }; /** The static dispatch/loop bound — how many slots the state buffers hold. */ maxAgents: number; /** Effective count ceiling per device tier. The prop's declared range is unchanged. */ countCap?: DeviceTier; /** Layout keys bound late, from another node's output (a child RTT). See `bindExternal`. */ externalKeys?: readonly string[]; /** A second bind group chained onto every pipeline marked `extra: true`. */ extraBindGroup?: unknown; pipelines: Record; /** Per-frame execution order (keys of `pipelines`). */ program: readonly string[]; /** Key of the one-shot step run before the first frame's program, and again on a reseed. */ initStep?: string; } export interface AgentSystem { /** Spread into the compute hook's `outputs`. */ outputs: Record; /** Write the whole per-frame uniform struct (strict layout — a plain struct has no partial patch). */ writeParams: (values: Infer) => void; /** The effective agent-count ceiling for this device. */ readonly countCap: number; /** Clamp a raw count prop into `[min, countCap]`. */ resolveCount: (raw: number, min?: number) => number; /** The created pipelines, by config key — for a shader that needs to dispatch one itself. */ pipelines: Record; /** Bind the late-arriving external resources (call from `bindInputs`). */ bindExternal: (entries: Record) => void; /** * The frame's compute steps, with the one-shot init prepended on the first call (and whenever * `reseed` changes). `null` until every external key is bound. */ frame: (opts?: { count?: number; grid?: [number, number]; reseed?: number; }) => ComputeStep[] | null; } /** * Build the runtime side of an agent simulation: state buffers, the output texture, the uniform, * the bind group, the guarded pipelines, and the per-frame step list. * * Returns `null` when there is no GPU device (SSR, the GPU-free resolve tests) — the caller returns * `null` from its `compute` hook and the fragment falls back. */ export declare function createAgentSystem(params: GpuFragmentParams, config: AgentSystemConfig): AgentSystem | null; //# sourceMappingURL=agentSystem.d.ts.map