vgpu5 symbolsView source ↗

Symbols in this topic

Draw

Target-agnostic renderable shader unit created by draw(gpu). It reflects WGSL bindings, caches pipelines per target format/depth/sample count, and supports geometries, explicit vertex counts, instancing, and raw group claims.

Import

TypeScript
1
import type { DepthOptions, Draw, DrawOptions, DrawCallOptions, DrawLayoutOptions, GeometryLike, StencilFaceOptions, StencilOptions } from "vgpu";

Signature

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
import type { ShaderSource, StorageBuffer, Target, TargetSignature } from "vgpu";
 
type SetBag = Record<string, unknown>;
 
type BlendPreset = "alpha" | "additive" | "premultiplied";
interface BlendComponentOptions { readonly src: GPUBlendFactor; readonly dst: GPUBlendFactor; readonly op?: GPUBlendOperation; }
interface BlendOptions { readonly color: BlendComponentOptions; readonly alpha?: BlendComponentOptions; }
 
interface DepthOptions {
  readonly write?: boolean;
  readonly compare?: GPUCompareFunction;
  readonly bias?: number;
  readonly biasSlopeScale?: number;
  readonly biasClamp?: number;
}
 
interface StencilFaceOptions {
  readonly compare?: GPUCompareFunction;
  readonly fail?: GPUStencilOperation;
  readonly depthFail?: GPUStencilOperation;
  readonly pass?: GPUStencilOperation;
}
 
interface StencilOptions {
  readonly front?: StencilFaceOptions;
  readonly back?: StencilFaceOptions;
  readonly readMask?: number;
  readonly writeMask?: number;
  readonly ref?: number;
}
 
interface DrawOptions {
  readonly shader: string | ShaderSource;
  readonly geometry?: GeometryLike;
  readonly set?: SetBag;
  readonly label?: string;
  readonly targets?: readonly Target[];
  readonly instances?: number;
  readonly vertices?: number;
  readonly firstInstance?: number;
  readonly blend?: BlendPreset | BlendOptions;
  readonly blendConstant?: readonly [number, number, number, number];
  readonly writeMask?: readonly ("r" | "g" | "b" | "a")[];
  readonly colors?: readonly ({ readonly blend?: BlendPreset | BlendOptions; readonly writeMask?: readonly ("r" | "g" | "b" | "a")[] } | null)[];
  readonly cull?: "none" | "front" | "back";
  readonly frontFace?: "ccw" | "cw";
  readonly unclippedDepth?: boolean;
  readonly depth?: false | DepthOptions;
  readonly stencil?: StencilOptions;
  readonly multisample?: { readonly alphaToCoverage?: boolean; readonly mask?: number };
  readonly constants?: Readonly<Record<string, number | boolean>>;
  readonly entry?: { readonly vertex?: string; readonly fragment?: string };
}
 
interface DrawCallOptions {
  readonly target?: Target;
  readonly offsets?: readonly number[] | Partial<Record<number, readonly number[]>>;
  readonly instances?: number;
  readonly vertices?: number;
  readonly indices?: number;
  readonly firstVertex?: number;
  readonly firstIndex?: number;
  readonly baseVertex?: number;
  readonly firstInstance?: number;
  readonly indirect?: StorageBuffer | { readonly buffer: StorageBuffer; readonly offset?: number };
}
 
interface DrawLayoutOptions { readonly dynamicOffsets?: boolean; }
 
interface GeometryLike {
  readonly vertexCount?: number;
  readonly indexCount?: number;
  readonly instanceCount?: number;
  readonly vertexBuffers?: readonly GPUBuffer[];
  readonly indexBuffer?: GPUBuffer;
  readonly indexFormat?: GPUIndexFormat;
  readonly vertexBufferLayouts?: readonly GPUVertexBufferLayout[];
  readonly topology?: GPUPrimitiveTopology;
  readonly stripIndexFormat?: GPUIndexFormat;
  readonly firstVertex?: number;
  readonly firstIndex?: number;
  readonly baseVertex?: number;
}
 
interface Draw {
  readonly gpu: GPURenderPipeline | undefined;
  readonly targets: readonly Target[] | undefined;
  set(values: SetBag): this;
  group(n: number, bindGroup: GPUBindGroup): this;
  layout(n: number, opts?: DrawLayoutOptions): GPUBindGroupLayout;
  draw(target?: Target | DrawCallOptions): void;
  compile(target?: Target | TargetSignature): Promise<this>;
  compileSync(target?: Target | TargetSignature): this;
}

Parameters

ParamTypeRequiredDefaultNotes
opts.shaderstring | ShaderSourceWGSL string or loader-produced ShaderSource. Must contain compatible vertex/fragment entry points; default names are vs_main and fs_main if reflection does not find them.
opts.geometryGeometryLikeundefinedSupplies vertex/index buffers and layouts. Omit for generated vertex-index drawing.
opts.setRecord<string, unknown>undefinedInitial .set() call.
opts.labelstring"draw"Debug/error label.
opts.targetsreadonly Target[]undefinedSynchronous pre-warm sugar for the listed target signatures. In browser load paths, prefer await draw.compile(target).
opts.instancesnumber1Default instance count. Integer >= 0; per-call instances overrides.
opts.verticesnumber3 for non-indexed, unless geometry.vertexCount existsDefault non-indexed vertex count. Ignored by indexed geometries. Integer >= 0.
opts.firstInstancenumber0Default first instance. Integer >= 0; per-call firstInstance overrides.
opts.blend"alpha" | "additive" | "premultiplied" | BlendOptionsundefinedConstructor-only blend state applied uniformly to every color target. Presets resolve at construction; explicit components use src/dst and optional op ("add" default). Omitted alpha copies color.
opts.blendConstantreadonly [number, number, number, number]whatever the pass holds — (0, 0, 0, 0) at pass startScales "constant"/"one-minus-constant" blend factors. Use it to fade or crossfade a whole layer per draw without touching per-vertex alpha. Encoder state, not pipeline state (see Notes). Components must be finite; values outside [0, 1] are legal. When omitted, no setBlendConstant is emitted for this draw, so constant factors read the current pass value: the (0, 0, 0, 0) default at the start of the pass, or the value an earlier draw in the same pass set, which persists until the next set. At least one color target's effective blend (colors[i].blend when that target has one, else the top-level blend) must use a constant factor.
opts.writeMaskreadonly ("r" | "g" | "b" | "a")[]all channelsConstructor-only color channel mask applied uniformly to every color target. Omit to write RGBA; [] writes no channels; ["r","g","b"] skips alpha.
opts.colorsreadonly ({ blend?, writeMask? } | null)[]undefinedPer-attachment blend/writeMask overrides for MRT — the deferred-shading case, where one draw writes a G-buffer (target(gpu, { colors: [...] })) and each attachment needs different state. One entry per color attachment, aligned by index. Inheritance is per field: null entries and omitted fields fall back to the top-level blend/writeMask; { writeMask: [] } leaves that attachment untouched.
opts.cull"none" | "front" | "back""none"Skips rasterizing triangles that face away from the chosen side. "back": culls faces pointing away from the viewer; on a closed geometry they are never visible, so the GPU skips roughly half the fragment work. "front": culls faces toward the viewer; used when rendering shadow maps to reduce peter panning. When omitted, no triangles are culled — required for open geometry such as foliage cards.
opts.frontFace"ccw" | "cw""ccw"Winding order that counts as front-facing — the reference cull works against. Set "cw" for geometry authored clockwise, or for draws with a negative (mirrored) scale, which flips the on-screen winding. When omitted, counter-clockwise triangles are front.
opts.unclippedDepthbooleanfalseDisables depth clipping so geometry outside [near, far] rasterizes instead of vanishing. Use it for shadow-map pancaking: casters behind the light's near plane flatten onto it instead of being clipped out of the map. Requires the "depth-clip-control" device feature. When omitted or false, standard clipping applies.
opts.depthfalse | DepthOptions{ write: true, compare: "less-equal" }Depth test/write state for targets with a depth attachment; fields are in the DepthOptions table below. false disables depth testing for overlays and gizmos that must draw over the scene regardless of distance. When omitted, nearer fragments win and coplanar re-draws still pass; ignored when the target has no depth.
opts.stencilStencilOptionsWebGPU pass-through defaultsMasks draws to marked screen regions using the stencil aspect of the depth attachment — portals, mirrors, object outlines, masked UI. Requires a target depth format with a stencil aspect (depth: "depth24plus-stencil8"). Fields are in the StencilOptions table below.
opts.multisample{ alphaToCoverage?, mask? }{ alphaToCoverage: false, mask: 0xFFFFFFFF }MSAA state. alphaToCoverage: turns fragment alpha into a per-sample coverage mask, so alpha-tested foliage antialiases in any draw order — no blending, no transparency sorting; requires an msaa: true target. mask: bitmask of samples the draw may write — a niche debugging tool most draws never set. Only the low sampleCount bits matter; higher bits are legal and ignored.
opts.constantsReadonly<Record<string, number | boolean>>the WGSL defaultsValues for WGSL override constants, fixed at pipeline creation. Use them to specialize one shader — quality tiers, feature toggles, workgroup-size tuning — without string-templating the WGSL. Key by override name, or by the decimal string of N when the declaration has @id(N) (the name is not usable then). Booleans become 1/0; every override declared without a default must be provided.
opts.entry{ vertex?: string; fragment?: string }first entry point of each stageSelects which @vertex/@fragment functions to compile when one WGSL module declares several — variants of one technique sharing helpers, such as depth-only and shaded passes from the same source. Names must exist in the shader with the matching stage. Omitted fields keep the first entry point of that stage.
draw.set.valuesRecord<string, unknown>Values keyed by WGSL binding variable name. JS objects/numbers are packed; resources are bound by identity.
draw.group.nnumberBind group index to claim for manual bind-group binding (group(n, bindGroup)).
draw.group.bindGroupGPUBindGroupMust be compatible with draw.layout(n) or draw.layout(n, { dynamicOffsets: true }).
draw.layout.nnumberReflected bind group index.
draw.layout.opts.dynamicOffsetsbooleanfalseWhen true, returns/reuses a layout whose buffer entries have hasDynamicOffset: true and clears cached pipelines.
draw.draw.targetTarget | DrawCallOptions{}One-shot draw options. Pass a bare target for the common case, or an options bag when setting counts or offsets.
opts.targetTargetRequired at runtime when an options bag is used. Use a Surface or an offscreen Target.
opts.offsetsreadonly number[] | Partial<Record<number, readonly number[]>>Reflected/claimed fallback offsetsDynamic offsets for claimed/dynamic groups. Array applies to every group; object keys by group.
opts.instancesnumberDrawOptions.instances ?? geometry.instanceCount ?? 1Per-call instance count; integer >= 0.
opts.verticesnumbergeometry.vertexCount ?? DrawOptions.vertices ?? 3Per-call non-indexed vertex count; indexed geometries use geometry.indexCount.
opts.firstVertexnumber0Non-indexed first vertex; indexed geometries use firstIndex/baseVertex 0.
opts.firstInstancenumberDrawOptions.firstInstance ?? 0Per-call first instance.
opts.indirectStorageBuffer | { buffer, offset? }undefinedGPU-driven draw: the GPU reads the draw arguments from the buffer at byte offset (default 0) instead of CPU-side counts. Use it when a culling compute pass decides what to draw — the arguments are written on the GPU and the CPU never round-trips. Create the buffer with storage(gpu, bytes, { indirect: true }); argument layouts are in Notes.

DepthOptions — the object form of opts.depth:

FieldTypeRequiredDefaultNotes
writebooleantruefalse keeps testing against the depth buffer without writing it. Use it for blended transparents and decals, which must hide behind opaque geometry but not occlude what draws after them.
compareGPUCompareFunction"less-equal"Comparison a fragment must pass against the stored depth. "greater" plus clearDepth: 0 on the pass gives reversed-Z, which spreads floating-point precision evenly across the view distance.
biasnumber0Constant offset added to each fragment's depth. Must be an integer (WebGPU depthBias is i32). A small positive bias while rendering the shadow map removes shadow acne; a small negative bias lets coplanar decals win the depth test instead of z-fighting.
biasSlopeScalenumber0Extra bias proportional to the polygon's depth slope. Surfaces at glancing angles need more offset than facing ones — pair it with bias to remove acne on sloped ground.
biasClampnumber0 (no clamp)Upper bound on the total bias. Caps runaway slope-scaled bias on near-edge-on triangles, which otherwise detaches shadows from their casters (peter panning).

StencilOptions — the fields of opts.stencil:

FieldTypeRequiredDefaultNotes
frontStencilFaceOptions{ compare: "always", fail/depthFail/pass: "keep" }Test and operations for front-facing triangles; fields are in the StencilFaceOptions table below.
backStencilFaceOptionsmirrors the normalized frontGive it explicitly when the two sides must differ — incrementing on front faces and decrementing on back faces to count volume crossings. With back given and front omitted, the front keeps the WebGPU face defaults.
readMasknumber0xFFFFFFFFBits of the stored stencil value visible to compare. Integer in [0, 0xFFFFFFFF].
writeMasknumber0xFFFFFFFFBits the face operations may change. Integer in [0, 0xFFFFFFFF]. Disjoint masks let several effects share one stencil buffer.
refnumberpass default 0Value compare tests against and "replace" writes. Integer in [0, 0xFFFFFFFF]. Encoder state, not pipeline state (see Notes); an explicit 0 still emits, restoring the pass default after an earlier draw changed it.

StencilFaceOptions — the front / back faces:

FieldTypeRequiredDefaultNotes
compareGPUCompareFunction"always"Comparison between the masked ref and the masked stored value. "equal" draws only inside a previously marked region — the portal or mirror interior.
failGPUStencilOperation"keep"Operation when the stencil comparison fails.
depthFailGPUStencilOperation"keep"Operation when the stencil comparison passes but the depth test fails.
passGPUStencilOperation"keep"Operation when both comparisons pass. "replace" writes ref, marking the region for later draws.

Returns: draw(gpu) returns Draw; set(), group(), and compileSync() return the same Draw; layout() returns a GPUBindGroupLayout; one-shot draw() returns void; compile() returns Promise<this>.

Throws: one VGPU-* code per condition below. Checks marked † resolve against the target signature at compile/draw time; with targets: [...] they surface from draw(gpu) itself, because that option compiles at construction.

  • VGPU-LIMIT-STORAGE-VERTEX / VGPU-LIMIT-STORAGE-FRAGMENT — static storage-buffer use by a selected entry point exceeds the granted stage limit. Request the supported requiredLimits value, or reduce/move the storage data.
  • VGPU-TARGET-REQUIREDdraw.draw() was called without a target and the draw has none to fall back on. Pass a Target, or an options bag with target.
  • VGPU-BLEND-INVALID — unknown blend preset or malformed blend object. Use "alpha", "additive", "premultiplied", or { color: { src, dst, op? }, alpha? }.
  • VGPU-BLEND-CONSTANT-INVALIDblendConstant is not exactly four finite numbers, or no color target's effective blend uses a "constant"/"one-minus-constant" factor (the value could never apply). The effective blend of a target is its colors[i].blend when it has one, else the top-level blend — so a top-level constant factor overridden on every target is still dead, while a constant factor reached only through colors[i].blend is live. Fix the tuple, or add a constant factor to a blend that survives the per-target overrides.
  • VGPU-WRITEMASK-INVALIDwriteMask is not an array, or contains a channel outside "r"/"g"/"b"/"a".
  • VGPU-COLORS-INVALIDcolors is not an array; an entry is neither null nor { blend?, writeMask? }; or † its length differs from the target's color attachment count (both counts are in the message). Give one entry per attachment.
  • VGPU-CULL-INVALIDcull is outside "none"/"front"/"back".
  • VGPU-FRONTFACE-INVALIDfrontFace is outside "ccw"/"cw".
  • VGPU-UNCLIPPED-DEPTH-INVALIDunclippedDepth is not a boolean, or is true on a device whose features lacks "depth-clip-control". Request the feature with init({ requiredFeatures: ["depth-clip-control"] }) on an adapter that supports it.
  • VGPU-DEPTH-INVALID — non-boolean write; unknown compare; non-integer bias; non-finite bias values; a nonzero bias value with a line-*/point-* topology (depth bias is only defined for triangles); or a nonzero biasClamp on a compatibility-mode device. Zero the offending field.
  • VGPU-STENCIL-INVALID — malformed stencil (non-object value, malformed front/back face, unknown compare or fail/depthFail/pass operation, or readMask/writeMask/ref outside integer [0, 0xFFFFFFFF]); or † any stencil state against a depth format without a stencil aspect. Create the target with depth: "depth24plus-stencil8".
  • VGPU-MULTISAMPLE-INVALID — malformed multisample (non-object value, non-boolean alphaToCoverage, or a mask outside integer [0, 0xFFFFFFFF]); or † alphaToCoverage: true against a non-MSAA signature. Create the target with msaa: true.
  • VGPU-CONSTANTS-INVALID — non-object constants; a key that matches no override in the shader (the message lists the available ones); a value that is neither a finite number nor a boolean; or an override declared without a default that constants does not provide. Add constants: { "<nameOrId>": value }.
  • VGPU-ENTRY-INVALID — non-object entry or a non-string vertex/fragment field; a name that matches no entry point; or a name whose entry point has the wrong stage. The message lists the shader's entry points with their stages.
  • VGPU-R1-DRAW-COUNT — a count field is not an integer >= 0. Use 0 only for a deliberate no-op draw.
  • VGPU-INDIRECT-INVALID — at call time: indirect is neither a StorageBuffer nor { buffer, offset? }; the buffer was created without the indirect flag (use storage(gpu, bytes, { indirect: true })); offset is not a non-negative integer multiple of 4; the arguments overrun the buffer (the message shows the byte math); or indirect is combined with vertices/indices/instances/firstVertex/firstIndex/baseVertex/firstInstance — the GPU reads those from the buffer, so drop the CPU-side value.
  • VGPU-R1-BINDING-NEVER-SET — a reflected binding was never provided before drawing. set() the named binding, or claim its group with group(n, bindGroup).
  • VGPU-R1-OWNERSHIP-FLIP — a binding switched between JS-value ownership and resource ownership across set() calls. Keep passing the kind its first set() used.
  • VGPU-R1-BINDING-INCOMPATIBLE-RESOURCE — a set() value does not satisfy the binding; the message names the binding and what it needs.
  • VGPU-SET-TEXTURE-FILTERABILITY — a facade texture format cannot satisfy an ordinarily sampled float binding (detail identifies the format, texture, and paired sampler). Use a filterable format, request float32-filterable, or rewrite to textureLoad.
  • VGPU-R4-GROUP-CLAIMEDset() tried to update a claimed group. Call set() before claiming, or keep updating the group yourself from draw.layout(n).
  • VGPU-R4-GROUP-INCOMPATIBLE — a claimed bind group does not match the draw's layout. Build it from draw.layout(n, { dynamicOffsets? }) before calling group(n, bindGroup).
  • VGPU-R4-GROUP-VALIDATION — WebGPU rejected a claimed group at draw time; delivered asynchronously through gpu.onError. Build the group from draw.layout(n) and pass offsets via the draw call.
  • VGPU-SHADER-SOURCE-INVALID — malformed ShaderSource. Pass WGSL text or a loader-produced { version, wgsl } object.

Examples

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { init, draw, target } from "vgpu/mock";
 
const gpu = await init();
const colorTarget = target(gpu, { size: [64, 64] });
const tri = draw(gpu, {
  label: "tri",
  targets: [colorTarget],
  shader: `
    @vertex fn vs_main(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
      var p = array<vec2f, 3>(vec2f(-1, -1), vec2f(3, -1), vec2f(-1, 3));
      return vec4f(p[vi], 0, 1);
    }
    @fragment fn fs_main() -> @location(0) vec4f { return vec4f(0, 1, 0, 1); }
  `,
});
 
tri.draw({ target: colorTarget, vertices: 3, instances: 1 });

Backface culling for a closed imported geometry:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { init, draw, target } from "vgpu/mock";
 
const gpu = await init();
const scene = target(gpu, { size: [256, 256], depth: true });
const statue = { vertexCount: 36 }; // closed geometry; vertex data omitted for brevity
const opaque = draw(gpu, {
  shader: `
    @vertex fn vs_main(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
      var p = array<vec2f, 3>(vec2f(-1, -1), vec2f(3, -1), vec2f(-1, 3));
      return vec4f(p[vi % 3u], 0, 1);
    }
    @fragment fn fs_main() -> @location(0) vec4f { return vec4f(0.8, 0.8, 0.7, 1); }
  `,
  geometry: statue,
  cull: "back",    // closed geometry: faces pointing away are never visible
  frontFace: "cw", // the importer produced clockwise triangles
});
opaque.draw(scene);

Culling back faces skips roughly half the fragment work on the closed statue, and frontFace: "cw" keeps the imported clockwise winding — or a negative-scale mirror — counting as front-facing.

Shadow map with depth bias and pancaking:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { init, createMockAdapter, draw, target } from "vgpu/mock";
 
const gpu = await init({
  adapter: createMockAdapter({ features: ["depth-clip-control"] }),
  requiredFeatures: ["depth-clip-control"],
});
const shadowMap = target(gpu, { size: [1024, 1024], depth: true });
const casters = draw(gpu, {
  shader: `
    @vertex fn vs_main(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
      var p = array<vec2f, 3>(vec2f(-1, -1), vec2f(3, -1), vec2f(-1, 3));
      return vec4f(p[vi], 0, 1);
    }
    @fragment fn fs_main() -> @location(0) vec4f { return vec4f(0); }
  `,
  // Nudge stored depth away from the light to stop shadow acne.
  depth: { bias: 2, biasSlopeScale: 2 },
  // Pancake casters behind the light's near plane instead of clipping them away.
  unclippedDepth: true,
});
casters.draw(shadowMap);

The bias pair keeps lit surfaces acne-free, and unclippedDepth flattens casters between the light and its near plane onto the map instead of losing their shadows.

MRT decal into a G-buffer:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { init, draw, target } from "vgpu/mock";
 
const gpu = await init();
// Deferred-shading G-buffer: albedo + world-space normals.
const gbuffer = target(gpu, {
  size: [512, 512],
  colors: [{ format: "rgba8unorm" }, { format: "rgba16float" }],
  depth: true,
});
const decal = draw(gpu, {
  shader: `
    struct Frag { @location(0) albedo: vec4f, @location(1) normal: vec4f }
    @vertex fn vs_main(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
      var p = array<vec2f, 3>(vec2f(-1, -1), vec2f(3, -1), vec2f(-1, 3));
      return vec4f(p[vi], 0, 1);
    }
    @fragment fn fs_main() -> Frag { return Frag(vec4f(0.6, 0.1, 0.1, 0.8), vec4f(0, 1, 0, 0)); }
  `,
  colors: [
    { blend: "alpha" },   // blend the decal into the albedo
    { writeMask: [] },    // leave the normals untouched
  ],
  depth: { write: false }, // the decal sits on existing geometry
});
decal.draw(gbuffer);

One draw blends the decal into gbuffer.colors[0] while { writeMask: [] } leaves the normals exactly as the opaque pass wrote them — the fragment still outputs @location(1), the mask only blocks the write.

Alpha-tested foliage without transparency sorting:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { init, draw, target } from "vgpu/mock";
 
const gpu = await init();
const scene = target(gpu, { size: [256, 256], depth: true, msaa: true });
const foliage = draw(gpu, {
  shader: `
    @vertex fn vs_main(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
      var p = array<vec2f, 3>(vec2f(-1, -1), vec2f(3, -1), vec2f(-1, 3));
      return vec4f(p[vi], 0, 1);
    }
    // In a real scene, alpha comes from the leaf texture.
    @fragment fn fs_main() -> @location(0) vec4f { return vec4f(0.1, 0.5, 0.1, 0.4); }
  `,
  multisample: { alphaToCoverage: true },
});
foliage.draw(scene);

Fragment alpha becomes per-sample coverage, so leaf edges antialias in any draw order — no blending, no transparency sorting.

Stencil-masked portal:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { init, draw, frame, target } from "vgpu/mock";
 
const gpu = await init();
const scene = target(gpu, { size: [256, 256], depth: "depth24plus-stencil8" });
const SHADER = `
  @vertex fn vs_main(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
    var p = array<vec2f, 3>(vec2f(-1, -1), vec2f(3, -1), vec2f(-1, 3));
    return vec4f(p[vi], 0, 1);
  }
  @fragment fn fs_main() -> @location(0) vec4f { return vec4f(0.2, 0.2, 1, 1); }
`;
// Mark the portal's pixels with stencil value 1; write no color, no depth.
const portalMask = draw(gpu, { shader: SHADER, writeMask: [], depth: false, stencil: { front: { pass: "replace" }, ref: 1 } });
// Draw the far world only where the mask matches.
const otherWorld = draw(gpu, { shader: SHADER, stencil: { front: { compare: "equal" }, ref: 1 } });
 
frame(gpu, (currentFrame) => {
  currentFrame.pass({ target: scene }, (pass) => {
    pass.draw(portalMask);
    pass.draw(otherWorld);
  });
});

The first draw marks the portal region in the stencil buffer; the second renders only where the stored value equals ref, inside one pass so the mask survives.

Per-draw layer fade with the blend constant:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { init, draw, target } from "vgpu/mock";
 
const gpu = await init();
const colorTarget = target(gpu, { size: [128, 128] });
const overlay = draw(gpu, {
  shader: `
    @vertex fn vs_main(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
      var p = array<vec2f, 3>(vec2f(-1, -1), vec2f(3, -1), vec2f(-1, 3));
      return vec4f(p[vi], 0, 1);
    }
    @fragment fn fs_main() -> @location(0) vec4f { return vec4f(1, 0.5, 0, 1); }
  `,
  // Weight the whole layer by the blend constant, not per-vertex alpha.
  blend: { color: { src: "constant", dst: "one-minus-constant" } },
  blendConstant: [0.25, 0.25, 0.25, 0.25], // the layer shows at 25%
});
overlay.draw(colorTarget);

The whole overlay fades with one per-draw value — no per-vertex alpha rewrite, and no extra pipeline, because the constant is encoder state.

Pipeline specialization with constants and entry:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { init, draw, target } from "vgpu/mock";
 
const gpu = await init();
const colorTarget = target(gpu, { size: [128, 128] });
// One module, two fragment variants sharing the vertex stage and helpers.
const SOURCE = `
  override STEPS: u32 = 8;
  @vertex fn vs_main(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
    var p = array<vec2f, 3>(vec2f(-1, -1), vec2f(3, -1), vec2f(-1, 3));
    return vec4f(p[vi], 0, 1);
  }
  @fragment fn fs_shaded() -> @location(0) vec4f { return vec4f(f32(STEPS) / 64.0, 0, 0, 1); }
  @fragment fn fs_flat() -> @location(0) vec4f { return vec4f(0.5, 0.5, 0.5, 1); }
`;
const hero = draw(gpu, { shader: SOURCE, constants: { STEPS: 64 } });        // high quality tier
const backdrop = draw(gpu, { shader: SOURCE, entry: { fragment: "fs_flat" } }); // cheap variant
 
hero.draw(colorTarget);
backdrop.draw(colorTarget);

Both draws compile from the same module: constants specializes the shaded variant at pipeline creation instead of string-templating WGSL, and entry picks the flat fragment for the backdrop.

GPU-driven draw with indirect:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { init, compute, draw, storage, target } from "vgpu/mock";
 
const gpu = await init();
const scene = target(gpu, { size: [256, 256], depth: true });
// drawIndirect arguments: vertexCount, instanceCount, firstVertex, firstInstance.
const args = storage(gpu, 16, { indirect: true });
const cullPass = compute(gpu, `
  @group(0) @binding(0) var<storage, read_write> args: array<u32, 4>;
  @compute @workgroup_size(1) fn cs_main() {
    args = array<u32, 4>(3u, 1u, 0u, 0u); // survivors of the culling test
  }
`);
cullPass.set({ args });
const grass = draw(gpu, {
  shader: `
    @vertex fn vs_main(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
      var p = array<vec2f, 3>(vec2f(-1, -1), vec2f(3, -1), vec2f(-1, 3));
      return vec4f(p[vi], 0, 1);
    }
    @fragment fn fs_main() -> @location(0) vec4f { return vec4f(0, 0.6, 0, 1); }
  `,
});
cullPass.dispatch(1);                          // the GPU decides the counts
grass.draw({ target: scene, indirect: args }); // the CPU never reads them back

The compute pass writes the draw arguments and the draw consumes them on the GPU — the culling result never round-trips through the CPU.

Pipeline pre-warm

draw.compile(target) asynchronously prepares one target signature and resolves to the same draw. draw.compileSync(target) prepares the same signature synchronously; if an async compile for that signature is still pending, the synchronous result wins the race and unblocks later draws. Both methods also accept a target signature object such as { colors: ["bgra8unorm"], depth: "depth24plus", sampleCount: 4 }; colors is required and bare strings are rejected.

Each color/depth/sample-count variant is a different pipeline. A missed variant sync-compiles on first use, which can jank; fire-and-forget pre-warms should always use .catch(...) or gpu.onError/gpu.settled() will not observe the returned promise rejection. targets: [target] is kept as creation-time compileSync() sugar for non-browser hot paths.

Notes

  • Choose blend by use case: omit it for opaque geometry; use "alpha"/"premultiplied" for ordinary composition and "additive" for glow. Reserve explicit equations for special effects. blendConstant is persistent pass state (not pipeline state and not bundle state), so set it when fading or crossfading a layer.
  • For MRT, use colors[i] to inherit or override blend/write masks per attachment. Use cull: "back" on closed geometries, "none" on foliage/cards, and "front" for shadow passes; pair negative scales with frontFace: "cw". Disable depth for overlays and depth writes for transparent/decals. Stencil needs a depth-stencil target; multisample.alphaToCoverage is for alpha-tested foliage with MSAA, not general blending.
  • Use indirect draws when compute produces arguments in storage buffers marked { indirect: true }; this avoids CPU readback and keeps culling GPU-driven.
  • Count precedence is per-call option, then draw option, then geometry/default. instances: 0 and vertices: 0 are valid no-op draws.
  • Blend, write masks, colors, cull, frontFace, unclippedDepth, depth, stencil (all but ref), multisample, constants, and entry are immutable pipeline state, fixed at draw(gpu); draws that differ in any of them compile distinct pipelines. Absent options and their explicit no-op spellings — unclippedDepth: false, multisample: {}, constants: {}, an all-defaults stencil, entry naming the first-of-stage functions — keep byte-identical descriptors and cache keys.
  • WebGPU mapping: cull/frontFace/unclippedDepthGPUPrimitiveState; depthGPUDepthStencilState (writedepthWriteEnabled, comparedepthCompare, the bias family → depthBias/depthBiasSlopeScale/depthBiasClamp); stencil → its stencil members (failfailOp, depthFaildepthFailOp, passpassOp); multisampleGPUMultisampleState, with the sample count always taken from the target's sampleCount; constantsGPUProgrammableStage.constants on both stages.
  • depth: false compiles { depthWriteEnabled: false, depthCompare: "always" } because WebGPU cannot omit depth state when the pass has a depth attachment. stencil merges into the same depth-stencil state; stencil without a depth option keeps the depth defaults.
  • unclippedDepth disables clipping only; fragment depth is still clamped to the viewport [minDepth, maxDepth] range at output.
  • With alphaToCoverage on, WebGPU additionally requires the first color target to be blendable with an alpha channel and forbids a fragment sample_mask output; native validation reports those.
  • WebGPU matches constants keys against the module's override declarations, not per entry point, so one record serves both stages even when an override is referenced by only one of them.
  • entry selection happens at construction: binding visibility, bind group layouts, vertex input layouts (the selected vertex entry's inputs drive geometry attribute matching), and storage-stage limit checks all reflect the chosen variant. Unused declarations keep visibility 0 in reflected layouts, so build claimed bind groups from draw.layout(n) rather than guessing a raw layout.
  • blendConstant and stencil.ref are encoder state: emitted as setBlendConstant/setStencilReference after setPipeline and before the draw, so draws that differ only in them share pipelines. Both are pass state and persist: a value set by one draw stays in effect for every later draw in the same pass that does not set its own. The (0, 0, 0, 0) blend-constant default therefore only holds until the first draw in the pass sets one — pass blendConstant explicitly on any draw in a pass that must not inherit a previous draw's value (stencil.ref behaves the same, and an explicit 0 re-emits). Render bundle encoders cannot set render-pass state, so bundle() rejects such draws with VGPU-BUNDLE-BLEND-CONSTANT/VGPU-BUNDLE-STENCIL-REF; encode them in a frame pass instead.
  • Blend presets: "alpha" uses source alpha over, "premultiplied" uses premultiplied source over, and "additive" uses one-plus-one additive blending for color and alpha. In explicit blends, op defaults to "add" and omitted alpha copies color.
  • indirect argument layouts: a non-indexed geometry (or no geometry) encodes drawIndirect — 4 u32 values, vertexCount, instanceCount, firstVertex, firstInstance (16 bytes); an indexed geometry still sets its index buffer and encodes drawIndexedIndirect — 5 32-bit values, indexCount, instanceCount, firstIndex, baseVertex (signed), firstInstance (20 bytes). Write them from a compute shader (bind the same buffer as storage) or from JS via write(). Indirect draws record fine into bundle(): drawIndirect/drawIndexedIndirect exist on render bundle encoders.
  • A non-zero firstInstance inside the buffered indirect arguments silently turns the draw into a no-op unless the device has the "indirect-first-instance" feature. The value lives on the GPU, so vgpu cannot validate it — request the feature with init({ requiredFeatures: ["indirect-first-instance"] }) when you need it.
  • One-shot draw.draw() has no implicit target and returns void; raw claimed-group validation errors are delivered through gpu.onError, and tests can await gpu.settled().
  • Changing resource identity after a draw is recorded in a Bundle marks that bundle stale; changing JS values in-place does not.
  • See also: Effect, FramePass.draw, Bundle, Surface, Target, SharedUniforms.