// GPU textures and shaders, reactive (SolidJS) layer: the create* helpers free
// their texture automatically when the reactive owner is disposed. Drive a
// target's uniforms declaratively with ``
// (see TextureProps) - the preferred way, deferred to the next real repaint so
// a fast-changing signal stays paced to actual frames; the prop means "the
// target's params" on every kind (on a draw target, its shared params).
// setTargetParams is the imperative exception: reach for it only when there
// is no `` element to hold a params prop, e.g. a target that only
// feeds another shader as a sampler2D input. The imperative primitives
// (uploadTexture, setTargetParams, destroyTexture, ...) live in the
// `flux:gpu` module.
//
// Sampling is a per-texture property declared at creation: `filter`
// ("linear" default | "nearest") and `wrap` ("clamp" default | "repeat") on
// every create* helper. One state for every consumer - `` display
// and shader sampling both follow it - so a nearest texture upscales with
// hard pixels everywhere (the retro/pixel-art path: render small, display
// big). No mipmaps exist.
//
// Combining several passes is a render-tree job, not a shader one: stack
// `` elements and set their `blendMode` (e.g. `blendMode="plus"` for
// an additive pass over a base pass) instead of writing a pass that samples
// both. WITHIN one pipeline draw, `blend: "add"` accumulates overlapping
// geometry additively and `blend: "multiply"` scales it (both
// order-independent, no sorting); `blend: "alpha"` composites over in
// draw-list order (order-dependent: the app or a scene layer sorts); anything
// else draws with GL blending disabled and overwrites.
//
// The pixel contract. Three facts hold for every texture and target:
//
// - Clip space is y-down. `gl_Position` y = -1 is the top of the target, +1
// the bottom (GL's row 0 is clip y = -1, and Impeller samples row 0 as the
// top). A vertex stage carrying camera-up geometry must negate y, or fold
// the flip into its projection, or it draws upside down: Vulkan's
// convention, not desktop GL's. The fragment path absorbs the same flip
// already, so `vUV` is 0..1 with top-left origin and a fragment-only shader
// never sees it.
// - Color is premultiplied alpha. A target's RGB is expected already
// multiplied by its A - `vec4(rgb * a, a)`, not `vec4(rgb, a)`, which
// composites as opaque. That is what Impeller composites and what
// `` blends; `clearColor` is premultiplied too, so the
// default transparent black needs no thought.
// - Values are non-linear RGBA8, with no color-space concept. Every texture
// and target holds 8-bit RGBA UNORM exactly as written; nothing converts to
// or from linear light. `filter: "linear"` averages and the `blend` modes
// accumulate non-linear values - the usual approximation, stated so
// shaders written today stay correct if a format vocabulary arrives.
import { createEffect, createSignal, getOwner, onCleanup, untrack } from "@solidjs/signals"
import * as gpu from "flux:gpu"
// The create* helpers accept { autoFree: false } to opt out of the
// owner-scoped auto-free, for resources whose lifetime is managed by hand
// (rebuilt on signal changes inside a long-lived component, handed across
// owners, ...). Without the opt-out, each rebuild would stack another
// onCleanup on the component owner: a leak until unmount, then a double-free
// against the by-hand destroys.
// `label` is a free-form debug name (WebGPU's label): surfaced by the dev
// tooling's GPU inventory and engine log messages, never interpreted, kept
// across id-stable resizes.
export type CreateOptions = { autoFree?: boolean; label?: string }
// Sampling options every texture-producing create* helper accepts, applied at
// creation as a property of the texture id (there is no set-sampler-later).
export type SamplerOptions = { filter?: gpu.FilterMode; wrap?: gpu.WrapMode }
export type { FilterMode, WrapMode } from "flux:gpu"
// Pixel format option for the pixel-upload creates (createTexture,
// createMutableTexture), fixed for the id's lifetime like the sampler state.
// "rgba8" (default) or "r8" - see TextureFormat in flux:gpu for the r8
// contract (1 byte/pixel, sampled as `(v, 0, 0, 1)`, any width).
export type TextureFormatOptions = { format?: gpu.TextureFormat }
export type { TextureFormat } from "flux:gpu"
// The branded id types, one per id space (see flux:gpu): plain numbers at
// runtime, distinct types to the checker, so a cross-space slip like
// destroyBuffer(textureId) fails to compile. Exported so apps can annotate
// storage (`let ids: TextureId[]`).
export type { BufferId, DrawId, ProgramId, RenderPipelineId, ShaderStageId, TextureId } from "flux:gpu"
// Re-exported so callers that depend on @solidrt/core -- like @solidrt/components
// -- need not import flux directly: destroyTexture for the manual-cleanup path
// (textures made outside a reactive scope, e.g. after an await, are not
// auto-freed), uploadTexture to push new pixels into a mutable texture, and
// the target-level verbs. setTargetParams writes a target's params on ANY
// target kind - the non-reactive exception described above, so prefer
// `` when a `` element is already in the
// tree. On a single-program target (fragment texture, pipeline target) the
// names validate strictly against its one program; on a draw target they are
// the SHARED params every entry reads (a camera's view-projection: one write
// per camera move instead of one per mesh), applied before each entry's own
// params so an entry naming the same uniform overrides the shared value, and
// a name only some entries' programs declare applies where declared.
// setTargetTextures is its sampler analog: retarget sampler2D inputs without
// recompiling (on a draw target, shared sources every entry reads - an
// environment map, a LUT - bound where an entry's program declares the name
// and its own bindings do not override it). resizeTexture and setTargetSize
// resize in place at a stable id (so `` and sampler bindings
// stay valid); because the id survives, the owner-scoped auto-free
// registered at creation keeps working and no re-registration is needed.
export {
destroyTexture,
endBufferWrite,
resizeTexture,
setTargetParams,
setTargetSize,
setTargetTextures,
uploadTexture,
} from "flux:gpu"
// Pipeline plumbing re-exported raw: setDraw re-renders a pipeline with an
// updated draw range (vertexCount after its buffer gained or lost dynamic
// geometry, firstVertex for a different window of a shared buffer,
// instanceCount for an instanced population; absent keys keep their current
// value, like params); destroyBuffer is the manual
// cleanup path for buffers created outside a reactive scope. renderTarget is
// the explicit render verb for `render: "manual"` targets - targets whose
// pass is state (accumulation, feedback) rather than a pure function of its
// inputs, which the runtime therefore never renders on its own; the app
// steps them, usually from onFrame.
// copyTexture overwrites a manual target with another texture's pixels
// GPU-side (exact, same size): seed a loadOp "load" accumulator, snapshot a
// ping-pong buffer, reset state to a known image.
export { copyTexture, destroyBuffer, renderTarget, setDraw } from "flux:gpu"
export type { BlendMode, CullMode, DrawRange, IndexBinding, IndexFormat, IndexRange, ShaderParams, Topology, VertexAttribute } from "flux:gpu"
// The draw-list verbs, re-exported raw: entries live and die with their draw
// target (see createDrawTarget below), so there is no per-entry lifetime to
// wrap. addDraw adds an entry (appended, or inserted via opts.before) and
// returns its stable DrawId; removeDraw drops one; setDrawParams /
// setDrawTextures / setDrawRange are the per-entry forms of setTargetParams /
// setTargetTextures / setDraw, taking (target, draw, value) with identical
// merge and validation semantics. The per-object hot path is setDrawParams (a
// moved mesh = one call with its new matrix); the per-target one is
// setTargetParams (exported above), which on a draw target writes the SHARED
// params every entry reads. setDrawOrder replaces the whole
// list order with a full permutation of the live ids - the sorting verb
// (opaque front-to-back, transparent back-to-front, re-issued when the
// camera moves).
export { addDraw, removeDraw, setDrawOrder, setDrawParams, setDrawRange, setDrawTextures } from "flux:gpu"
// The device ceilings (max texture/target size, sampler inputs per pass,
// vertex attributes per pipeline), queried once at startup. Creates and binds
// validate against them and throw naming the limit; read these to size within
// the device instead (e.g. clamp a supersampled target to maxTextureSize).
export { limits } from "flux:gpu"
// The raw shading layer, re-exported as-is - no reactive wrapper, the app
// owns these lifetimes. compileShader compiles one stage from complete GLSL
// ES (or with the standard header via { header: true }); linkProgram links a
// vertex and a fragment stage into a program handle; createRenderPipeline
// pairs a program with draw state (vertex layout, topology, blend, depth -
// how it draws) into a pipeline handle that backs any number of
// createShaderTarget calls (and compiles nothing per pipeline or target);
// destroyShader / destroyProgram / destroyRenderPipeline free by id space,
// any order safe against live users. createShaderTexture/createPipelineTexture
// remain the fused conveniences on top.
export {
compileShader,
createRenderPipeline,
destroyProgram,
destroyRenderPipeline,
destroyShader,
linkProgram,
} from "flux:gpu"
/**
* Tags an inline GLSL source, returning it unchanged. Shaders small enough to
* belong beside the code that uses them stay in the file; the tag is what makes
* them legible there, because editors highlight GLSL inside a template literal
* only when a known tag marks it (the name matters - `glsl` is the one the
* grammars look for).
*
* Interpolated values are stringified verbatim, with no GLSL-aware formatting:
* `${2}` splices in the int literal `2`, which will not assign to a float. Pass
* anything that varies as a uniform instead of building it into the source.
*
* Raw semantics, so backslashes reach the compiler as written: the GLSL
* preprocessor continues a line with a trailing `\`, which a cooked template
* would reject as an invalid escape and silently pass through as `undefined`.
*/
export let glsl = String.raw
// captureSnapshot renders a node to pixels and readTexture reads any
// texture's bytes back; both resolve the same { width, height, data } shape.
// A laid-out node captures its layout box; a `d-*` node captures its painted
// box - its own w/h when set, else the nearest laid-out ancestor's box, its
// x/y offset mapped to the texture origin. A capture creates no texture and
// nothing needs freeing; to display or sample the result, upload it with
// createTexture.
//
// This is the one-shot bake path: draw something only the engine can produce
// (shaped text, an SVG, a themed view), capture it and process the pixels on
// the CPU - baking a glyph atlas is the worked example. Not a rendering path:
// a capture rasterizes the subtree offscreen and reads it back to the CPU,
// costing a readback stall and a paint pass of latency every call. Batch
// captures (one paint pass services many), never run them per frame, and do
// not use them to feed live screen content into a shader - for that the
// source has to update in place (another pipeline's target, a camera texture).
export { captureSnapshot, readTexture } from "flux:gpu"
/**
* Uploads raw pixels to an immutable GPU texture and returns its id (use it
* as ``). `data` must be exactly `width * height` pixels
* at the declared format's size (`* 4` bytes for the default "rgba8", `* 1`
* for "r8"); a mismatch throws. For pixels you intend to mutate and
* re-upload, use `createMutableTexture` instead. When called inside a
* reactive scope the texture is freed automatically once that owner is
* disposed; when called outside one (e.g. after an `await`, where the owner
* is no longer current) nothing is registered and you must call
* `destroyTexture` (from flux:gpu) yourself. Pass `{ autoFree: false }` to
* skip the auto-free and own the disposal yourself even inside a reactive
* scope.
*/
export function createTexture(
data: Uint8Array,
width: number,
height: number,
opts?: CreateOptions & SamplerOptions & TextureFormatOptions,
): gpu.TextureId {
let id = gpu.createTexture(data, width, height, opts)
if (opts?.autoFree !== false && getOwner()) onCleanup(() => gpu.destroyTexture(id))
return id
}
/**
* Creates a GPU texture you intend to update over time: seed it with `data`,
* then call `uploadTexture(id, data)` (from flux:gpu) to push new pixels.
* `data` must hold at least `width * height` pixels at the declared format's
* size (`* 4` bytes for the default "rgba8", `* 1` for "r8"; it may hold
* several frames). Like `createTexture`, the texture is freed automatically
* when the reactive owner is disposed (opt out with `{ autoFree: false }`);
* created outside a reactive scope you must call `destroyTexture` (from
* flux:gpu) yourself.
*/
export function createMutableTexture(
data: Uint8Array,
width: number,
height: number,
opts?: CreateOptions & SamplerOptions & TextureFormatOptions,
): gpu.TextureId {
let id = gpu.createMutableTexture(data, width, height, opts)
if (opts?.autoFree !== false && getOwner()) onCleanup(() => gpu.destroyTexture(id))
return id
}
/**
* Compiles a GLSL ES 3.00 fragment shader and renders it into a texture,
* returning the texture id (usable anywhere a normal texture id is, e.g.
* ``) - hence the name: what comes back is a texture, not a
* shader object. The fragment body may reference `vUV` (0..1, top-left
* origin), `iResolution`, and any uniform it declares (`float`/`int`
* scalars from a number, `vec2`/`vec3`/`vec4`/`mat4` from a flat number
* array); drive their values with ``
* (preferred) or, when there is no `` element for it, imperatively
* with `setTargetParams`. `params` is its own argument - it seeds the same
* live channel those two drive - and takes `null` (or nothing) for a shader
* without uniforms. A time-driven shader declares its own time uniform
* (`uniform float uTime;`) and the app drives it like any other value.
* `opts.textures` binds each declared `uniform sampler2D` to an existing texture id
* (e.g. a camera or decoded image, or another shader/pipeline target) so the
* shader can read it; bound inputs are live dependencies, so the shader
* re-renders whenever a source changes - including a sampled target
* re-rendering, transitively through chains. Frees the
* texture and shader program when the reactive owner is disposed (opt out
* with `{ autoFree: false }`); create outside any reactive scope for
* app-lifetime shaders. For a shader whose source or inputs change
* reactively, use {@link createShaderTextureMemo} instead.
*
* That preamble (`#version 300 es`, precision, `vUV`, `iResolution`,
* `fragColor`) is injected only into sources that do not declare their own
* `#version` line, and declares exactly what the runtime provides - nothing
* app-driven. A source starting with `#version 300 es` compiles exactly
* as written, so a shader carrying its own uniform names - one ported from
* elsewhere - runs unchanged here without dropping to compileShader /
* linkProgram. The built-in vertex stage still supplies `vUV`; declare
* `in vec2 vUV;` yourself to read it. One naming trap: GLSL ES reserves
* `packed` as a keyword, so `vec4 packed = texture(...)` fails with a syntax
* error that does not name the identifier - pick another name.
*/
export function createShaderTexture(
fragmentSrc: string,
width: number,
height: number,
params?: gpu.ShaderParams | null,
opts?: CreateOptions & SamplerOptions & { textures?: Record },
): gpu.TextureId {
let id = gpu.createShaderTexture(fragmentSrc, width, height, params, opts)
if (opts?.autoFree !== false && getOwner()) onCleanup(() => gpu.destroyTexture(id))
return id
}
/**
* Creates a render target over a pipeline from `createRenderPipeline` and
* renders it once, returning the texture id (usable anywhere a normal
* texture id is, e.g. ``; resize with `setTargetSize`, drive
* uniforms with `` or `setTargetParams`). Many targets may
* share one pipeline, and creating a target compiles nothing. The target
* brings the per-target half: size, the concrete vertex `buffer` the
* pipeline's attribute layout describes, the `instanceBuffer` its
* `instanceAttributes` describe (required exactly when it declares any),
* the draw range (`vertexCount` defaults to the rest of the buffer from
* `firstVertex` on, `instanceCount` repeats it as instances told apart by
* `gl_InstanceID` and defaults to one per instance-buffer record; a
* fullscreen pass over an attributeless pipeline is `{ vertexCount: 3 }`
* with a covering-triangle vertex stage), uniforms, and
* `clearColor`. An `indexBuffer` + `indexFormat` pair makes the draw indexed
* (shared vertices stored once), with the range in `firstIndex`/`indexCount`
* spelling - see IndexBinding/IndexRange. Draw state (`attributes`,
* `instanceAttributes`, `topology`, `blend`, `cull`, `depth`, `depthWrite`)
* lives on the pipeline
* and throws here. Frees the target when the reactive owner is disposed (opt
* out with `autoFree: false`); the pipeline is yours and outlives it.
*
* `render: "manual"` makes it a manual target: the runtime never renders it
* (it starts cleared to `clearColor`), only an explicit `renderTarget(id)`
* does, in call order - which is what legalizes feedback state stepped by
* the app. `loadOp: "load"` (manual-only, throws otherwise) keeps the
* previous contents under each draw - single-target accumulation - while
* the default `"clear"` clears to `clearColor` per render; state that must
* read its own pixels (decay, blur, simulation) still ping-pongs across two
* manual targets, and `copyTexture` seeds either shape.
*/
export function createShaderTarget(
pipeline: gpu.RenderPipelineId,
width: number,
height: number,
params?: gpu.ShaderParams | null,
opts?: {
textures?: Record
buffer?: gpu.BufferId
instanceBuffer?: gpu.BufferId
clearColor?: [number, number, number, number]
render?: "auto" | "manual"
loadOp?: "clear" | "load"
} & (gpu.DrawRange | (gpu.IndexBinding & gpu.IndexRange)) &
CreateOptions &
SamplerOptions,
): gpu.TextureId {
let id = gpu.createShaderTarget(pipeline, width, height, params, opts)
if (opts?.autoFree !== false && getOwner()) onCleanup(() => gpu.destroyTexture(id))
return id
}
/**
* Creates a draw target: a render target holding an ordered, MUTABLE list of
* draws, rendered as one pass - clear once (color, and depth when declared),
* then every entry in list order into the same storage. This is the
* multi-pass primitive (N meshes x N pipelines sharing one depth buffer -
* what every 3D API calls a render pass), retained: build the list with
* `addDraw`, prune it with `removeDraw`, and drive per-entry state with
* `setDrawParams` / `setDrawTextures` / `setDrawRange`. `depth: true` gives
* the target the depth storage all entries share (cross-entry occlusion);
* whether an entry tests/writes it stays pipeline state, and a depth-testing
* pipeline into a depthless target throws at `addDraw`.
*
* `params` seeds the target's SHARED params - values every entry reads,
* written once per target instead of once per entry (a camera's
* view-projection is the motivating case: one `setTargetParams` per camera
* move instead of one `setDrawParams` per mesh). Shared values apply before
* each entry's own params, so an entry naming the same uniform overrides
* the shared value; a name only some entries' programs declare is applied
* where declared and skipped elsewhere. They are target state: entry
* add/remove/rebuild cannot lose them. `opts.textures` is the sampler
* analog - shared sources every entry reads (an environment map, a LUT),
* driven later with `setTargetTextures`, same precedence and coverage
* rules.
*
* The render contract is unchanged: the list is input data, so an ordinary
* (`render: "auto"`) draw target re-renders exactly when its entries or
* their inputs change - a static scene costs zero passes, and one render is
* one pass regardless of entry count. `render: "manual"` and `loadOp` work
* as on `createShaderTarget`. Returns the texture id; frees on owner
* disposal (opt out with `autoFree: false`), taking its entries with it - the
* entries' pipelines and buffers are yours and outlive it.
*/
export function createDrawTarget(
width: number,
height: number,
params?: gpu.ShaderParams | null,
opts?: {
depth?: boolean
textures?: Record
clearColor?: [number, number, number, number]
render?: "auto" | "manual"
loadOp?: "clear" | "load"
} & CreateOptions &
SamplerOptions,
): gpu.TextureId {
let id = gpu.createDrawTarget(width, height, params, opts)
if (opts?.autoFree !== false && getOwner()) onCleanup(() => gpu.destroyTexture(id))
return id
}
/** The reactive shader description `createShaderTextureMemo` builds from.
* Sampling (`filter`/`wrap`) is creation-time state, so changing it rebuilds
* at a fresh id, like a fragment-source or sampler-binding change. */
export type ShaderSpec = {
fragmentSrc: string
width: number
height: number
params?: gpu.ShaderParams
textures?: Record
} & SamplerOptions
// Shallow name->value equality for params/textures records; treats undefined
// as the empty record. A param value may be a number or a flat number array
// (typed uniforms), so arrays compare elementwise.
function sameValue(a: number | number[] | undefined, b: number | number[] | undefined): boolean {
if (a === b) return true
if (!Array.isArray(a) || !Array.isArray(b)) return false
return a.length === b.length && a.every((v, i) => v === b[i])
}
function sameRecord(
a: Record | undefined,
b: Record | undefined,
): boolean {
if (a === b) return true
let ka = a ? Object.keys(a) : []
let kb = b ? Object.keys(b) : []
return ka.length === kb.length && ka.every(k => sameValue(a![k], b![k]))
}
/**
* A fragment shader whose spec is reactive: returns an accessor for the
* current texture id (use it as ``) and keeps the GPU
* resource in step with `spec` from then on. Changes that keep the compiled
* program valid mutate in place at a stable id - a size change routes to
* `setTargetSize`, a params change to `setTargetParams` - while a change to
* the fragment source or the sampler bindings rebuilds at a fresh id, updates
* the accessor, and destroys the old id. That destroy is frame-safe (the
* runtime reclaims an id only once the render tree no longer references it),
* so the swap never paints a blank frame. The current id is freed when the
* owning scope is disposed. Data textures need no analog: `uploadTexture` and
* `resizeTexture` already cover their reactive changes id-stably.
*
* `onError` makes a failed rebuild survivable. Without it a shader that does
* not compile throws from inside the effect, where no caller can catch it;
* with it the error is handed to you and the last shader that DID compile
* stays current - id, size, params and accessor all unchanged - so the app
* keeps drawing the previous frame's shader instead of tearing down. That is
* the normal case whenever the source is not known-good: a shader editor, live
* coding, or a dialect ported from elsewhere. The initial compile is not
* covered: it throws at the call site, where an ordinary try/catch works and
* there is no previous shader to fall back to.
*/
export function createShaderTextureMemo(
spec: () => ShaderSpec,
opts?: { onError?: (error: unknown) => void },
): () => gpu.TextureId {
let make = (s: ShaderSpec) =>
gpu.createShaderTexture(s.fragmentSrc, s.width, s.height, s.params, { textures: s.textures, filter: s.filter, wrap: s.wrap })
let current = untrack(spec)
let currentId = make(current)
let [id, setId] = createSignal(currentId)
createEffect(spec, next => {
try {
if (
next.fragmentSrc === current.fragmentSrc &&
sameRecord(next.textures, current.textures) &&
next.filter === current.filter &&
next.wrap === current.wrap
) {
// Program and inputs unchanged: mutate in place, the id stays stable.
if (next.width !== current.width || next.height !== current.height) {
gpu.setTargetSize(currentId, next.width, next.height)
}
if (!sameRecord(next.params, current.params) && next.params) {
gpu.setTargetParams(currentId, next.params)
}
current = next
return
}
// Compile before touching any state: a throw here must leave `current`,
// `currentId` and the accessor all still pointing at the last shader
// that worked, which is what makes onError's keep-last-good real.
let rebuilt = make(next)
let old = currentId
current = next
currentId = rebuilt
setId(rebuilt)
gpu.destroyTexture(old)
} catch (error) {
if (!opts?.onError) throw error
opts.onError(error)
}
})
if (getOwner()) onCleanup(() => gpu.destroyTexture(currentId))
return id
}
// View any TypedArray or ArrayBuffer as a Uint8Array over the same memory,
// without copying, so vertex data can be authored as Float32Array.
function toUint8(data: ArrayBuffer | ArrayBufferView): Uint8Array {
if (data instanceof Uint8Array) return data
if (data instanceof ArrayBuffer) return new Uint8Array(data)
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
}
/**
* Compiles a GLSL ES 3.00 vertex+fragment pipeline and renders it into a
* texture, returning the texture id (usable anywhere a normal texture id is,
* e.g. ``) - named, like `createShaderTexture`, for what comes
* back. Unlike `createShaderTexture` the vertex stage is yours:
* declare `in` attributes matching `opts.attributes` (one interleaved vertex
* in `opts.buffer`, a {@link createBuffer} id) and your own varyings toward
* the fragment stage. Clip space is y-down: `gl_Position` y = -1 is the top
* row of the target and +1 the bottom, so camera-up geometry must negate y
* (or fold the flip into its projection) to display up. Both sources may
* reference `iResolution` and any uniform they declare (`float`/`int`
* scalars from a number, `vec2`/`vec3`/`vec4`/`mat4` from a flat number
* array); drive values with `` or
* `setTargetParams`, exactly like a fragment shader.
* `opts.depth` attaches a private depth buffer (cleared + tested per render);
* `opts.depthWrite: false` (requires depth) keeps the test but stops the
* draw from writing depth. `opts.blend: "add"` makes the draw accumulate
* overlapping geometry additively and `"multiply"` makes it scale (darken)
* what is already there, both order-independent (no sorting) instead of
* overwriting; `"alpha"` composites over in draw-list order (premultiplied
* output, back-to-front is the caller's job). A depth-tested blended pass is
* `{ depth: true, blend: "add", depthWrite: false }` - each option only does
* what it says, neither implies the other. The draw range (`firstVertex`, `vertexCount`, `instanceCount` -
* see DrawRange) defaults to the whole buffer drawn once and can be changed
* later with `setDraw`; `instanceCount` is the standard answer to particles
* and repeated meshes, N copies of the range told apart by `gl_InstanceID`
* in the vertex stage. `opts.instanceAttributes` + `opts.instanceBuffer`
* (declare both or neither) give each instance its own interleaved record -
* real per-instance state instead of `gl_InstanceID` arithmetic - and
* `instanceCount` then defaults to one instance per record. An
* `indexBuffer` + `indexFormat` pair makes the draw
* indexed (shared vertices stored once), with the range in
* `firstIndex`/`indexCount` spelling; `opts.cull` discards one face set by
* winding (counter-clockwise as displayed = front). `opts.render: "manual"` and
* `opts.loadOp` behave exactly as on {@link createShaderTarget}: step the
* target with `renderTarget(id)`, and `loadOp: "load"` (manual-only) keeps
* the previous contents under each draw. Frees the texture and GL program when the reactive
* owner is disposed (opt out with `autoFree: false`); create outside any reactive
* scope for app-lifetime pipelines.
*/
export function createPipelineTexture(
vertexSrc: string,
fragmentSrc: string,
width: number,
height: number,
params?: gpu.ShaderParams | null,
opts?: {
textures?: Record
attributes?: gpu.VertexAttribute[]
buffer?: gpu.BufferId
instanceAttributes?: gpu.VertexAttribute[]
instanceBuffer?: gpu.BufferId
topology?: gpu.Topology
depth?: boolean
depthWrite?: boolean
blend?: gpu.BlendMode
cull?: gpu.CullMode
clearColor?: [number, number, number, number]
render?: "auto" | "manual"
loadOp?: "clear" | "load"
} & (gpu.DrawRange | (gpu.IndexBinding & gpu.IndexRange)) &
CreateOptions &
SamplerOptions,
): gpu.TextureId {
let id = gpu.createPipelineTexture(vertexSrc, fragmentSrc, width, height, params, opts)
if (opts?.autoFree !== false && getOwner()) onCleanup(() => gpu.destroyTexture(id))
return id
}
/**
* Creates a vertex buffer for pipeline attributes from raw data (typically a
* Float32Array laid out to match the pipeline's interleaved attribute list).
* Update it later with {@link writeBuffer}; the buffer's byte size is fixed at
* creation, so reserve room up front for dynamic geometry. Freed automatically
* when the reactive owner is disposed (opt out with `{ autoFree: false }`);
* created outside a reactive scope you must call `destroyBuffer` yourself.
* (Destruction order relative to pipelines does not matter.)
*/
export function createBuffer(data: ArrayBuffer | ArrayBufferView | number, opts?: CreateOptions): gpu.BufferId {
let id = gpu.createBuffer(typeof data === "number" ? data : toUint8(data), opts)
if (opts?.autoFree !== false && getOwner()) onCleanup(() => gpu.destroyBuffer(id))
return id
}
/**
* Opens a zero-copy write into a vertex buffer: returns a Float32Array over
* runtime-owned memory spanning the whole buffer. Write records in place,
* then publish with {@link endBufferWrite} - the bytes move to the GPU with
* no copy on the CPU path, which is the per-frame streaming path (instanced
* sprites, dynamic geometry). Reach other element types through `.buffer`.
*
* Contents are UNSPECIFIED at begin (a recycled block holds what was
* published the time before last): fill everything you publish. One open
* write per buffer at a time. The view is detached at end/destroy - retained
* references become zero-length, so a stale write is inert, never a race.
*/
export function beginBufferWrite(id: gpu.BufferId): Float32Array {
let ab = gpu.beginBufferWrite(id)
return new Float32Array(ab, 0, (ab.byteLength / 4) | 0)
}
/**
* Overwrites part of a vertex buffer at `byteOffset` (default 0). Every
* pipeline drawing from the buffer re-renders with its last-applied params,
* so geometry-only changes reach the screen without a params update.
*/
export function writeBuffer(id: gpu.BufferId, data: ArrayBuffer | ArrayBufferView, byteOffset?: number): void {
gpu.writeBuffer(id, toUint8(data), byteOffset)
}