geometry()

Creates an immutable vertex/index layout plus mutable GPU buffers for `draw(gpu)`. The v2 descriptor form accepts named attributes, multiple vertex streams, instancing streams, indices, topology, writes, and slices. Existing scene geometry descriptors such as `box()` continue to work as primitive sugar.

Import

import type { Geometry, GeometryOptions, GeometryBufferOptions, GeometrySliceOptions } from "vgpu";
import { box } from "vgpu/scene";

Signature

interface Gpu {
  geometry(geometry: import("vgpu/scene").SceneGeometry): Geometry;
  geometry(options: GeometryOptions): Geometry;
}

type GeometryData = ArrayBuffer | ArrayBufferView;
type GeometryAttributes = {
  readonly [name: string]: GPUVertexFormat | GeometryAttributeOverride;
};

interface GeometryAttributeOverride {
  readonly format: GPUVertexFormat;
  readonly offset?: number;
  readonly location?: number;
}

interface GeometryBufferOptions {
  readonly attributes: GeometryAttributes;
  readonly data?: GeometryData;
  readonly buffer?: GPUBuffer;
  readonly stride?: number;
  readonly stepMode?: GPUVertexStepMode;
  readonly label?: string;
}

interface GeometryOptions {
  readonly buffers: readonly GeometryBufferOptions[];
  readonly vertexCount?: number;
  readonly instanceCount?: number;
  readonly indices?: Uint16Array | Uint32Array | readonly number[];
  readonly indexBuffer?: GPUBuffer;
  readonly indexFormat?: GPUIndexFormat;
  readonly indexCount?: number;
  readonly topology?: GPUPrimitiveTopology;
  readonly label?: string;
}

interface GeometryBuffer {
  readonly gpu: GPUBuffer;
  readonly stride: number;
  readonly stepMode: GPUVertexStepMode;
  write(data: GeometryData, byteOffset?: number): void;
}

interface GeometrySliceOptions {
  readonly firstIndex?: number;
  readonly indexCount?: number;
  readonly baseVertex?: number;
  readonly firstVertex?: number;
  readonly vertexCount?: number;
  readonly instanceCount?: number;
  readonly label?: string;
}

interface Geometry {
  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 buffers: readonly GeometryBuffer[];
  slice(opts?: GeometrySliceOptions): GeometrySlice;
  write(data: GeometryData, byteOffset?: number): void;
  writeIndices(data: Uint16Array | Uint32Array, byteOffset?: number): void;
  destroy(): void;
}

interface GeometrySlice extends GeometryLike {
  readonly geometry: Geometry;
  readonly firstIndex?: number;
  readonly indexCount?: number;
  readonly baseVertex?: number;
  readonly firstVertex?: number;
  readonly vertexCount?: number;
  readonly instanceCount?: number;
}

interface DrawCallOptions {
  /** Non-indexed count. Precedence: call > slice > geometry > DrawOptions > 3. */
  readonly vertices?: number;
  /** Non-indexed start. Precedence: call > slice > 0. */
  readonly firstVertex?: number;
  /** Indexed count. Precedence: call > slice > geometry. */
  readonly indices?: number;
  /** Indexed start. Precedence: call > slice > 0. */
  readonly firstIndex?: number;
  /** Indexed base vertex. Precedence: call > slice > 0. */
  readonly baseVertex?: number;
}

Parameters

FieldTypeRequiredDefaultNotes
geometrySceneGeometryv1 primitive sugar path. Emits pinned locations: position@location(0), normal@location(1), uv@location(2).
options.buffersreadonly GeometryBufferOptions[]Vertex buffer streams. Maximum 8.
buffer.attributesGeometryAttributesRecord form only. Key is the WGSL vertex input name unless location is specified. Maximum 16 attributes total.
attribute formatGPUVertexFormatShorthand value: { position: "float32x3" }.
attribute.offsetnumbertight-packed orderByte offset within the stream. Integer-like attribute keys are rejected to avoid JavaScript key reordering.
attribute.locationnumbershader name matchExplicit shader location. When present, the record key is only a label.
buffer.dataGeometryDataCreates an owned ["vertex", "copy_dst"] buffer and uploads the initial data. Mutually exclusive with buffer.
buffer.bufferGPUBufferCaller-owned escape hatch. Mutually exclusive with data; not destroyed by geometry.destroy().
buffer.stridenumberroundUp4(sum(format sizes))Explicit stride for padded/interleaved data. Must be valid for WebGPU vertex buffers.
buffer.stepMode"vertex" | "instance""vertex"Instance streams derive geometry.instanceCount from the first instance buffer with data.
options.vertexCountnumberderivedDerived from the first vertex-step buffer with data.
options.instanceCountnumberderivedDraw default after DrawCallOptions.instances and DrawOptions.instances.
options.indicesUint16Array | Uint32Array | readonly number[]Creates an owned ["index", "copy_dst"] index buffer. Uint16Array infers "uint16"; otherwise "uint32".
options.indexBufferGPUBufferCaller-owned index buffer escape hatch. Pair with indexFormat and indexCount.
options.topologyGPUPrimitiveTopology"triangle-list"Pipeline-affecting geometry topology. Strip topologies derive stripIndexFormat from indexFormat.
geometry.slice.optsGeometrySliceOptionsfull rangeFrozen range view sharing buffers and layout identity with the parent geometry.
geometry.write.dataGeometryDataWrites to buffer 0 using queue.writeBuffer. No resize.
geometry.writeIndices.dataUint16Array | Uint32ArrayWrites to an index buffer owned from options.indices. Write caller-owned indexBuffer objects directly. No resize.

Returns: geometry(gpu) returns Geometry; geometry.slice() returns GeometrySlice; write(), writeIndices(), and destroy() return void.

Error codes

CodeWhenFix
VGPU-MESH-LAYOUT-INVALIDInvalid stride/offset/format, both data and buffer, or integer-like attribute key.Use record keys that are names, align offsets/strides, and choose one data source.
VGPU-MESH-LIMIT-EXCEEDEDMore than 8 buffers or 16 attributes.Split draws or reduce streams/attributes.
VGPU-MESH-LOCATION-CONFLICTDuplicate explicit location values.Give each explicit shader location once.
VGPU-MESH-DATA-MISALIGNEDData byte length is not divisible by stride, or index bytes do not match format.Repack data or pass an explicit stride.
VGPU-MESH-RANGE-INVALIDSlice or draw-time range is negative, non-integer, outside parent counts, or uses index ranges on non-indexed geometries.Clamp ranges and use indexed fields only with indexed geometries.
VGPU-MESH-WRITE-RANGEwrite() or writeIndices() would overflow the fixed buffer.Create a larger geometry; writes do not resize buffers.
VGPU-MESH-ATTRIBUTE-UNMATCHEDNamed geometry attribute has no vertex-stage shader input.Rename the attribute or specify location.
VGPU-MESH-INPUT-MISSINGShader declares an uncovered @location input.Add the geometry attribute or remove the shader input.
VGPU-MESH-FORMAT-MISMATCHVertex format base type does not match the WGSL input base type.Use a compatible GPUVertexFormat; width differences are allowed by WebGPU.

Examples

import { init, geometry } from "vgpu/mock";

const gpu = await init();
const positions = new Float32Array([
  -1, -1, 0,
   1, -1, 0,
   0,  1, 0,
]);

const triangle = geometry(gpu, {
  buffers: [{
    data: positions,
    attributes: { position: "float32x3" },
  }],
});
import { init, geometry } from "vgpu/mock";

const gpu = await init();
const ledVertices = new Float32Array(6 * 6);
const ledGeometry = geometry(gpu, {
  label: "triangle-led-front-led-emitters",
  buffers: [{
    data: ledVertices,
    stride: 24,
    attributes: {
      position: "float32x2",
      local: "float32x2",
      led_index: "float32",
    },
  }],
});
import { init, geometry } from "vgpu/mock";

const gpu = await init();
const quadCorners = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]);
const instanceData = new Float32Array(4 * 10);
const particles = geometry(gpu, {
  topology: "triangle-strip",
  buffers: [
    { data: quadCorners, attributes: { corner: "float32x2" } },
    { stepMode: "instance", data: instanceData, attributes: {
      i_pos: "float32x3",
      i_color: { format: "unorm8x4", location: 5 },
    } },
  ],
});
import { init, draw, geometry } from "vgpu/mock";

const gpu = await init();
const vertexData = new Float32Array(3 * 4500);
const allIndices = new Uint32Array(4500);
const gltfGeometry = geometry(gpu, {
  buffers: [{ data: vertexData, attributes: { position: "float32x3" } }],
  indices: allIndices,
});
const hull = gltfGeometry.slice({ firstIndex: 0, indexCount: 3600 });
const glass = gltfGeometry.slice({ firstIndex: 3600, indexCount: 900, label: "glass" });
const pbrWgsl = "@vertex fn vs_main(@location(0) position: vec3f) -> @builtin(position) vec4f { return vec4f(position, 1); }";

draw(gpu, { shader: pbrWgsl, geometry: hull });
draw(gpu, { shader: pbrWgsl, geometry: glass, blend: "alpha" });
import { init, draw, geometry, target } from "vgpu/mock";

const gpu = await init();
const glyphQuads = new Float32Array(4 * 4);
const quadIndices = new Uint16Array([0, 1, 2, 2, 1, 3]);
const text = geometry(gpu, {
  buffers: [{ data: glyphQuads, attributes: { pos: "float32x2", uv: "float32x2" } }],
  indices: quadIndices,
});
const sdfTextWgsl = "@vertex fn vs_main(@location(0) pos: vec2f, @location(1) uv: vec2f) -> @builtin(position) vec4f { return vec4f(pos, 0, 1); }";
const textDraw = draw(gpu, { shader: sdfTextWgsl, geometry: text });
const colorTarget = target(gpu, { size: [640, 480] });

text.write(new Float32Array(4 * 4));
text.writeIndices(new Uint16Array([0, 1, 2, 2, 1, 3]));
textDraw.draw({ target: colorTarget, indices: 6 });
import { init, geometry } from "vgpu/mock";
import { box } from "vgpu/scene";

const gpu = await init();
const cube = geometry(gpu, box({ size: 2 }));

Notes

  • Layout is immutable; data is mutable. Changing formats, strides, topology, index format, or buffer identities requires a new geometry and a new draw.
  • Auto stride is tight-packed and rounded up to 4 bytes. It never guesses padded data; pass stride when a writer emits padding.
  • Slices share parent buffers and the same vertexBufferLayouts array identity so pipelines are shared.
  • Draw-time range overrides use DrawCallOptions.indices, firstIndex, and baseVertex; non-indexed draws use existing vertices and firstVertex.
  • Bundles bake counts and ranges at record time. Dynamic per-frame ranges need direct draws or bundle re-recording.
  • destroy() only destroys buffers owned from data/indices; caller-owned buffer and indexBuffer remain caller-owned.