GPU shader canvas
Draw a canvas with a fragment shader instead of paint.ts.For a GPU-drawn visual, declare a canvas with type: "webgpu" and write a fragment
shader instead of a paint.ts. Author it in
TGSL (TypeScript) at canvases/<id>/shader.ts, or raw WGSL at
canvases/<id>/shader.wgsl. Msh compiles it to WGSL (WebGPU) and GLSL ES 3.00
(WebGL2 fallback), so it runs everywhere. Editing it hot-reloads only that canvas.
// ui.ts
Canvas({ id: "viz", type: "webgpu", width: "fill", height: Theme.sizing(20) });// canvases/viz/shader.ts
import { defineShader, vec4f } from "@msh/device-sdk/device/shader";
export default defineShader((f, g) => {
"use gpu";
return vec4f(f.uv.x, f.uv.y, g.time, 1.0);
});defineShader(fn, opts?). The body must begin with the "use gpu" directive.
Arguments
| Arg | Is |
|---|---|
f | Fragment input. f.uv is 0..1 across the canvas, origin top-left. |
g | Per-frame globals (below). |
p | Your declared params, each a live f32 (0..1, modulation-folded). |
Globals (g)
time (seconds, animate with this), resolution, dpr, pointer,
pointerActive, transport (bpm, beat, beatPhase, playing), and the node
palette (accent, background, foreground).
Reading device data
Declare names in opts, read them by name in the body.
opts field | Declared with | Read with |
|---|---|---|
params: [...] | m.addParam | p.name |
channels: [...] | m.channel | channel("name", x) (filtered) or channelTexel("name", i) (exact bin) |
images: [...] | m.texture / ring / array | image("name", uv) → vec4f |
feedback: true | prev(uv) → the previous frame, for trails and glow | |
compute: true | a sibling compute.ts | compute(i) → the compute pass's f32 at index i |
Channel names match the roles: "spectrum", "spectrumPost", "preSpectrum",
"curve". See Data buckets for images, and
Compute pass for a canvas for compute.
export default defineShader(
(f, g, p) => {
"use gpu";
const level = image("spectro", f.uv).x; // a ring bucket
return vec4f(level * p.gain, level * 0.4, g.beatPhase, 1.0);
},
{ params: ["gain"], images: ["spectro"] },
);Math
Import from @msh/device-sdk/device/shader: constructors (vec2f, vec3f, vec4f, f32,
...) and std functions (add, sub, mul, div, dot, cross, normalize,
length, mix, clamp, smoothstep, step, sin, cos, pow, fract, min,
max, abs, ...). You can write a + b on vectors inside a "use gpu" body, but
TypeScript can't model that, so the std functions keep the editor green.
Rules
- Start the body with
"use gpu". - Import only from
@msh/device-sdk/device/shader, nevertypegpu. - The fragment body itself must be valid in both WGSL and GLSL ES 3.00: no storage
buffers, no compute in the fragment, no unbounded loops. The compiler rejects these
up front. To precompute per-frame data on the GPU, run a separate
compute pass (
compute.ts) and read it withcompute(i).
Raw WGSL
Prefer hand-written WGSL? Use canvases/<id>/shader.wgsl with one @fragment entry
taking @location(0) uv: vec2f and returning @location(0) vec4f.
How-to: Write a GPU shader. To run DSP on the GPU instead of visuals, see GPU audio.