@vgpu/render 0.0.5 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +94 -5
  2. package/dist/frame.d.ts +19 -0
  3. package/dist/frame.d.ts.map +1 -0
  4. package/dist/frame.js +50 -0
  5. package/dist/frame.js.map +1 -0
  6. package/dist/index.d.ts +11 -3
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +5 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/perf/frame-time-measure.d.ts +22 -0
  11. package/dist/perf/frame-time-measure.d.ts.map +1 -0
  12. package/dist/perf/frame-time-measure.js +91 -0
  13. package/dist/perf/frame-time-measure.js.map +1 -0
  14. package/dist/perf/gpu-frame-time.d.ts +49 -0
  15. package/dist/perf/gpu-frame-time.d.ts.map +1 -0
  16. package/dist/perf/gpu-frame-time.js +50 -0
  17. package/dist/perf/gpu-frame-time.js.map +1 -0
  18. package/dist/perf/index.d.ts +5 -0
  19. package/dist/perf/index.d.ts.map +1 -0
  20. package/dist/perf/index.js +3 -0
  21. package/dist/perf/index.js.map +1 -0
  22. package/dist/perf/pixel-diff.d.ts +27 -0
  23. package/dist/perf/pixel-diff.d.ts.map +1 -0
  24. package/dist/perf/pixel-diff.js +40 -0
  25. package/dist/perf/pixel-diff.js.map +1 -0
  26. package/dist/pipeline-descriptor.d.ts +35 -0
  27. package/dist/pipeline-descriptor.d.ts.map +1 -0
  28. package/dist/pipeline-descriptor.js +44 -0
  29. package/dist/pipeline-descriptor.js.map +1 -0
  30. package/dist/pipeline.d.ts +24 -14
  31. package/dist/pipeline.d.ts.map +1 -1
  32. package/dist/pipeline.js +58 -7
  33. package/dist/pipeline.js.map +1 -1
  34. package/dist/render-bundle.d.ts +24 -0
  35. package/dist/render-bundle.d.ts.map +1 -0
  36. package/dist/render-bundle.js +39 -0
  37. package/dist/render-bundle.js.map +1 -0
  38. package/dist/render-pass.d.ts +16 -1
  39. package/dist/render-pass.d.ts.map +1 -1
  40. package/dist/render-pass.js +44 -7
  41. package/dist/render-pass.js.map +1 -1
  42. package/dist/storage-buffer.d.ts +81 -0
  43. package/dist/storage-buffer.d.ts.map +1 -0
  44. package/dist/storage-buffer.js +91 -0
  45. package/dist/storage-buffer.js.map +1 -0
  46. package/dist/uniform.d.ts +55 -0
  47. package/dist/uniform.d.ts.map +1 -0
  48. package/dist/uniform.js +64 -0
  49. package/dist/uniform.js.map +1 -0
  50. package/package.json +8 -3
  51. package/src/Frame.docs.md +79 -0
  52. package/src/RenderPass.docs.md +6 -3
  53. package/src/createRenderPipeline.docs.md +113 -19
  54. package/src/perf/perf.docs.md +45 -0
  55. package/src/rapid-renderer.docs.md +15 -1
  56. package/src/render-bundle.docs.md +46 -0
  57. package/src/render-target/render-target-canvas.docs.md +15 -0
  58. package/src/storage-buffer.docs.md +102 -0
  59. package/src/uniform.docs.md +70 -0
@@ -0,0 +1,55 @@
1
+ import { Buffer, type Device } from "@vgpu/core";
2
+ export interface UniformOptions {
3
+ /** Byte size of the uniform buffer. Must be a positive number. */
4
+ readonly size: number;
5
+ /** Optional debug label forwarded to the buffer, bind group layout, and bind group. */
6
+ readonly label?: string;
7
+ /**
8
+ * Shader stages that read binding 0. Defaults to `VERTEX | FRAGMENT`.
9
+ * Ignored when {@link UniformOptions.bindGroupLayout} is provided.
10
+ */
11
+ readonly visibility?: GPUShaderStageFlags;
12
+ /**
13
+ * Reuse a pipeline-owned bind group layout instead of creating one.
14
+ * The layout's binding 0 must be a uniform buffer compatible with `size`.
15
+ */
16
+ readonly bindGroupLayout?: GPUBindGroupLayout;
17
+ }
18
+ /**
19
+ * A single stable uniform buffer for one render pass, rewritten per frame.
20
+ *
21
+ * @remarks
22
+ * Unlike {@link UniformPool} (a dynamic-offset ring allocator built for many
23
+ * per-draw uniforms), `Uniform` owns exactly one uniform buffer at binding 0 with
24
+ * a fixed (non-dynamic) offset. The caller decides when to {@link Uniform.write}
25
+ * — typically gating on real changes — and the bind group never moves. This fits
26
+ * the "one camera/globals buffer per pass" case where a dynamic-offset binding
27
+ * would needlessly mark the layout `hasDynamicOffset` and re-upload every frame.
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * const uniform = new Uniform(device, { size: 64, label: "globals" });
32
+ * // per frame, only when inputs changed:
33
+ * uniform.write(globalsBytes);
34
+ * pass.setBindGroup(0, uniform.bindGroup);
35
+ * ```
36
+ */
37
+ export declare class Uniform {
38
+ readonly device: Device;
39
+ readonly size: number;
40
+ readonly buffer: Buffer;
41
+ readonly bindGroupLayout: GPUBindGroupLayout;
42
+ readonly bindGroup: GPUBindGroup;
43
+ private destroyed;
44
+ constructor(device: Device, opts: UniformOptions);
45
+ /** The underlying uniform `GPUBuffer`. */
46
+ get gpu(): GPUBuffer;
47
+ /**
48
+ * Upload `data` to the buffer via `queue.writeBuffer`. No dynamic offset; the
49
+ * bind group is unchanged. Call only when the uniform contents actually change.
50
+ */
51
+ write(data: BufferSource, offset?: number): void;
52
+ destroy(): void;
53
+ dispose(): void;
54
+ }
55
+ //# sourceMappingURL=uniform.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"uniform.d.ts","sourceRoot":"","sources":["../src/uniform.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,MAAM,EAA0C,KAAK,MAAM,EAAE,MAAM,YAAY,CAAC;AAE/F,MAAM,WAAW,cAAc;IAC7B,kEAAkE;IAClE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,uFAAuF;IACvF,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,mBAAmB,CAAC;IAC1C;;;OAGG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,kBAAkB,CAAC;CAC/C;AAID;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,OAAO;IAON,QAAQ,CAAC,MAAM,EAAE,MAAM;IANnC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,eAAe,EAAE,kBAAkB,CAAC;IAC7C,QAAQ,CAAC,SAAS,EAAE,YAAY,CAAC;IACjC,OAAO,CAAC,SAAS,CAAS;gBAEL,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc;IAczD,0CAA0C;IAC1C,IAAI,GAAG,IAAI,SAAS,CAEnB;IAED;;;OAGG;IACH,KAAK,CAAC,IAAI,EAAE,YAAY,EAAE,MAAM,SAAI,GAAG,IAAI;IAI3C,OAAO,IAAI,IAAI;IAMf,OAAO,IAAI,IAAI;CAGhB"}
@@ -0,0 +1,64 @@
1
+ import { bind, createBindGroup, createBindGroupLayout } from "@vgpu/core";
2
+ const defaultVisibility = ((globalThis.GPUShaderStage?.VERTEX ?? 1) | (globalThis.GPUShaderStage?.FRAGMENT ?? 2));
3
+ /**
4
+ * A single stable uniform buffer for one render pass, rewritten per frame.
5
+ *
6
+ * @remarks
7
+ * Unlike {@link UniformPool} (a dynamic-offset ring allocator built for many
8
+ * per-draw uniforms), `Uniform` owns exactly one uniform buffer at binding 0 with
9
+ * a fixed (non-dynamic) offset. The caller decides when to {@link Uniform.write}
10
+ * — typically gating on real changes — and the bind group never moves. This fits
11
+ * the "one camera/globals buffer per pass" case where a dynamic-offset binding
12
+ * would needlessly mark the layout `hasDynamicOffset` and re-upload every frame.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * const uniform = new Uniform(device, { size: 64, label: "globals" });
17
+ * // per frame, only when inputs changed:
18
+ * uniform.write(globalsBytes);
19
+ * pass.setBindGroup(0, uniform.bindGroup);
20
+ * ```
21
+ */
22
+ export class Uniform {
23
+ device;
24
+ size;
25
+ buffer;
26
+ bindGroupLayout;
27
+ bindGroup;
28
+ destroyed = false;
29
+ constructor(device, opts) {
30
+ this.device = device;
31
+ this.size = opts.size;
32
+ this.buffer = device.createBuffer({ size: opts.size, usage: ["uniform", "copy_dst"], label: opts.label });
33
+ this.bindGroupLayout = opts.bindGroupLayout ?? createBindGroupLayout(device, {
34
+ label: opts.label ? `${opts.label}.bgl` : undefined,
35
+ entries: [bind.uniform(0, opts.visibility ?? defaultVisibility, { minBindingSize: opts.size })],
36
+ });
37
+ this.bindGroup = createBindGroup(device, {
38
+ label: opts.label ? `${opts.label}.bg` : undefined,
39
+ layout: this.bindGroupLayout,
40
+ entries: [bind.resource(0, this.buffer)],
41
+ });
42
+ }
43
+ /** The underlying uniform `GPUBuffer`. */
44
+ get gpu() {
45
+ return this.buffer.gpu;
46
+ }
47
+ /**
48
+ * Upload `data` to the buffer via `queue.writeBuffer`. No dynamic offset; the
49
+ * bind group is unchanged. Call only when the uniform contents actually change.
50
+ */
51
+ write(data, offset = 0) {
52
+ this.buffer.write(data, offset);
53
+ }
54
+ destroy() {
55
+ if (this.destroyed)
56
+ return;
57
+ this.destroyed = true;
58
+ this.buffer.destroy();
59
+ }
60
+ dispose() {
61
+ this.destroy();
62
+ }
63
+ }
64
+ //# sourceMappingURL=uniform.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"uniform.js","sourceRoot":"","sources":["../src/uniform.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAU,eAAe,EAAE,qBAAqB,EAAe,MAAM,YAAY,CAAC;AAmB/F,MAAM,iBAAiB,GAAG,CAAC,CAAC,UAAU,CAAC,cAAc,EAAE,MAAM,IAAI,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,cAAc,EAAE,QAAQ,IAAI,CAAC,CAAC,CAAwB,CAAC;AAEzI;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,OAAO,OAAO;IAOG;IANZ,IAAI,CAAS;IACb,MAAM,CAAS;IACf,eAAe,CAAqB;IACpC,SAAS,CAAe;IACzB,SAAS,GAAG,KAAK,CAAC;IAE1B,YAAqB,MAAc,EAAE,IAAoB;QAApC,WAAM,GAAN,MAAM,CAAQ;QACjC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACtB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,YAAY,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,SAAS,EAAE,UAAU,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;QAC1G,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC,eAAe,IAAI,qBAAqB,CAAC,MAAM,EAAE;YAC3E,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,SAAS;YACnD,OAAO,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,EAAE,IAAI,CAAC,UAAU,IAAI,iBAAiB,EAAE,EAAE,cAAc,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;SAChG,CAAC,CAAC;QACH,IAAI,CAAC,SAAS,GAAG,eAAe,CAAC,MAAM,EAAE;YACvC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC,SAAS;YAClD,MAAM,EAAE,IAAI,CAAC,eAAe;YAC5B,OAAO,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;SACzC,CAAC,CAAC;IACL,CAAC;IAED,0CAA0C;IAC1C,IAAI,GAAG;QACL,OAAO,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC;IACzB,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,IAAkB,EAAE,MAAM,GAAG,CAAC;QAClC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAClC,CAAC;IAED,OAAO;QACL,IAAI,IAAI,CAAC,SAAS;YAAE,OAAO;QAC3B,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QACtB,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;IACxB,CAAC;IAED,OAAO;QACL,IAAI,CAAC,OAAO,EAAE,CAAC;IACjB,CAAC;CACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vgpu/render",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "Render pipeline + render pass abstractions for vgpu.",
5
5
  "keywords": [
6
6
  "webgpu",
@@ -47,6 +47,10 @@
47
47
  "./passes": {
48
48
  "types": "./dist/passes/index.d.ts",
49
49
  "import": "./dist/passes/index.js"
50
+ },
51
+ "./perf": {
52
+ "types": "./dist/perf/index.d.ts",
53
+ "import": "./dist/perf/index.js"
50
54
  }
51
55
  },
52
56
  "files": [
@@ -58,14 +62,15 @@
58
62
  ],
59
63
  "dependencies": {
60
64
  "wgpu-matrix": "^3.4.2",
61
- "@vgpu/core": "0.0.5"
65
+ "@vgpu/core": "0.0.7"
62
66
  },
63
67
  "vgpuExportBundleBudgetsGzipBytes": {
64
68
  ".": 49152,
65
69
  "./inspect": 4096,
66
70
  "./utils": 2048,
67
71
  "./edit": 25600,
68
- "./passes": 3584
72
+ "./passes": 3584,
73
+ "./perf": 3072
69
74
  },
70
75
  "scripts": {
71
76
  "build": "tsc -b",
@@ -0,0 +1,79 @@
1
+ # Frame
2
+
3
+ `Frame` owns one `GPUCommandEncoder` for an explicit sequence of commands. Use
4
+ `beginFrame(device)` when one render frame needs multiple render passes, query
5
+ commands, or copies in a user-authored order, then call `submit()` once.
6
+
7
+ VGPU does not infer pass order, bind group layouts, or pipeline layouts. The
8
+ helpers keep the native WebGPU lifecycle visible while removing repeated
9
+ boilerplate around pass begin/end and final submission.
10
+
11
+ ## Native WebGPU baseline
12
+
13
+ Without `Frame`, a multipass render frame is one command encoder, multiple
14
+ passes ended in order, then one finish/submit:
15
+
16
+ ```ts
17
+ const encoder = device.gpu.createCommandEncoder({ label: "hero.frame" });
18
+
19
+ const lightPass = encoder.beginRenderPass(lightPassDescriptor);
20
+ lightPass.executeBundles([lightBundle]);
21
+ lightPass.end();
22
+
23
+ encoder.writeTimestamp(querySet, 0);
24
+ encoder.copyBufferToBuffer(srcBuffer, 0, dstBuffer, 0, byteLength);
25
+
26
+ const compositePass = encoder.beginRenderPass(compositePassDescriptor);
27
+ compositePass.setPipeline(compositePipeline);
28
+ compositePass.setBindGroup(0, compositeBindGroup);
29
+ compositePass.draw(3);
30
+ compositePass.end();
31
+
32
+ device.queue.gpu.submit([encoder.finish()]);
33
+ ```
34
+
35
+ ## With VGPU Frame
36
+
37
+ `Frame` keeps that same lifecycle: one encoder, explicit pass order, finish once,
38
+ submit once. This one-encoder/one-submit contract is intentional for homepage-style
39
+ multipass renderers: setup and warmup should create pipelines, buffers, bind
40
+ groups, and bundles up front; the per-frame path should only encode commands and
41
+ submit the finished command buffer.
42
+
43
+ ```ts
44
+ const frame = beginFrame(device, { label: "hero.frame" });
45
+
46
+ frame.renderPass(lightPassDescriptor, (pass) => {
47
+ pass.executeBundles([lightBundle]);
48
+ });
49
+
50
+ frame.gpu.writeTimestamp(querySet, 0);
51
+ frame.copyBufferToBuffer(srcBuffer, dstBuffer, byteLength);
52
+
53
+ frame.renderPass(compositePassDescriptor, (pass) => {
54
+ pass.setPipeline(compositePipeline);
55
+ pass.setBindGroup(0, compositeBindGroup);
56
+ pass.draw(3);
57
+ });
58
+
59
+ frame.submit();
60
+ ```
61
+
62
+ `frame.renderPass(options, callback)` begins a `RenderPass` wrapper on the
63
+ frame-owned encoder, runs the callback, and ends that pass in a `finally` block.
64
+ Unlike constructing a standalone `RenderPass`, a pass created by `Frame` does
65
+ **not** finish or submit the command encoder when it ends. `frame.submit()` is
66
+ the only submission point.
67
+
68
+ The raw command encoder is available as `frame.gpu` for WebGPU operations that
69
+ are intentionally not wrapped, such as `writeTimestamp()` or `resolveQuerySet()`.
70
+ This is an advanced, semver-protected public escape hatch. Direct raw encoder
71
+ calls follow native WebGPU behavior; VGPU only guards its own helper methods
72
+ after `submit()`.
73
+
74
+ `copyBufferToBuffer(source, destination, size, sourceOffset?, destinationOffset?)`
75
+ is a small typed helper that accepts either core `Buffer` instances or raw
76
+ `GPUBuffer`s. For all other encoder commands, use `frame.gpu` directly.
77
+
78
+ A `Frame` is single-use. Calling VGPU helper methods after `submit()` throws
79
+ `VGPUError` with code `VGPU-FRAME-SUBMITTED`.
@@ -5,9 +5,11 @@ attachments. Construct it with a core `Device` and `RenderPassOptions`, encode
5
5
  render commands, then call `end()` to finish the pass and submit the command
6
6
  buffer through `Device.queue`.
7
7
 
8
- `RenderPassOptions` contains `colorAttachments` and an optional `label`.
9
- `ColorAttachment.view` accepts either a core `Texture` or a raw
10
- `GPUTextureView`; `loadOp`, `storeOp`, and `clearValue` are forwarded to WebGPU.
8
+ `RenderPassOptions` contains `colorAttachments`, optional
9
+ `depthStencilAttachment`, and an optional `label`. `ColorAttachment.view` and
10
+ `DepthStencilAttachment.view` accept either a core `Texture` or a raw
11
+ `GPUTextureView`; load/store operations and clear values are forwarded to
12
+ WebGPU.
11
13
 
12
14
  Commands mirror the WebGPU render pass encoder:
13
15
 
@@ -15,6 +17,7 @@ Commands mirror the WebGPU render pass encoder:
15
17
  - `setBindGroup(index, group, dynamicOffsets?)` binds a raw `GPUBindGroup`.
16
18
  - `setVertexBuffer(slot, buffer, offset?, size?)` binds a core `Buffer` or raw
17
19
  `GPUBuffer` for vertex input.
20
+ - `executeBundles(bundles)` executes setup-time `GPURenderBundle`s in the pass.
18
21
  - `draw({ vertexCount, instanceCount?, firstVertex?, firstInstance? })` issues a
19
22
  non-indexed draw. The numeric WebGPU-style `draw(vertexCount, ...)` call is
20
23
  also accepted.
@@ -1,34 +1,128 @@
1
- # createRenderPipeline
1
+ # createRenderPipeline / createRenderPipelineAsync
2
2
 
3
- Creates a GPU render pipeline from a vgpu `Shader` plus vertex and fragment
4
- entry points. Returns a raw `GPURenderPipeline` with no wrapper.
3
+ Creates a GPU render pipeline from descriptor-like VGPU options. Both helpers
4
+ return a raw `GPURenderPipeline` with no wrapper so the result can be passed
5
+ directly to native WebGPU, `RenderPass.setPipeline()`, or render-bundle recording.
5
6
 
6
- ## Signature
7
+ ## Signatures
7
8
 
8
- `createRenderPipeline(device: Device, opts: RenderPipelineOptions): GPURenderPipeline`
9
+ ```ts
10
+ createRenderPipeline(device: Device, opts: RenderPipelineOptions): GPURenderPipeline
11
+ createRenderPipelineAsync(device: Device, opts: RenderPipelineOptions): Promise<GPURenderPipeline>
12
+ createRenderPipelineFromDescriptor(device: Device, descriptor: GPURenderPipelineDescriptor): GPURenderPipeline
13
+ createRenderPipelineFromDescriptorAsync(device: Device, descriptor: GPURenderPipelineDescriptor, fallback?: RenderPipelineAsyncFallback): Promise<GPURenderPipeline>
14
+ ```
15
+
16
+ `createRenderPipelineAsync` calls `GPUDevice.createRenderPipelineAsync()` when it
17
+ exists. If the implementation does not expose the async API, the default
18
+ compatibility policy is `fallback: "sync"`, which emits a once-only diagnostic and
19
+ calls `createRenderPipeline()` instead. Performance-critical warmup can pass
20
+ `fallback: "throw"` to receive a structured `VGPUError` with code
21
+ `VGPU-RENDER-PIPELINE-ASYNC-UNAVAILABLE` instead of accidentally blocking.
22
+
23
+ ## Raw descriptor entrypoints
24
+
25
+ If you already have a hand-built `GPURenderPipelineDescriptor`, pass it straight
26
+ through — do not reshape it into `RenderPipelineOptions` just to get the
27
+ async→sync fallback:
28
+
29
+ - `createRenderPipelineFromDescriptor(device, descriptor)` forwards the descriptor
30
+ to `GPUDevice.createRenderPipeline()` unchanged.
31
+ - `createRenderPipelineFromDescriptorAsync(device, descriptor, fallback?)` forwards
32
+ it to `GPUDevice.createRenderPipelineAsync()` with the exact same compatibility
33
+ fallback as `createRenderPipelineAsync` (default `"sync"`, or `"throw"` for a
34
+ structured `VGPUError`).
35
+
36
+ The descriptor is forwarded verbatim, so native WebGPU validation and lifecycle
37
+ rules remain the caller's responsibility. These are explicit, separately named
38
+ exports rather than an overload so a `RenderPipelineOptions` caller can never be
39
+ misread as passing a raw descriptor.
40
+
41
+ VGPU does not cache pipelines: one helper call equals one WebGPU device call.
42
+ Keep pipeline caches explicit and owned by the caller.
9
43
 
10
44
  ## Options
11
45
 
12
- - `shader`: the `Shader` whose compiled GPU module will back both stages.
13
- - `vertex.entry`: the vertex shader entry-point name.
14
- - `fragment.entry`: the fragment shader entry-point name.
15
- - `fragment.targets`: the color target formats and blend/write settings.
16
- - `primitive`: optional WebGPU primitive state such as topology or culling.
17
- - `layout`: optional pipeline layout, or `"auto"` to let WebGPU derive one.
46
+ - `shader`: optional shared `Shader` or raw `GPUShaderModule` used by stages that
47
+ do not provide their own module.
48
+ - `vertex`: vertex stage options.
49
+ - `shader` / `module`: optional per-stage `Shader` or raw `GPUShaderModule`.
50
+ - `entry` / `entryPoint`: vertex entry-point name.
51
+ - `buffers`: optional vertex buffer layouts and attributes.
52
+ - `constants`: optional pipeline constants.
53
+ - `fragment`: optional fragment stage options.
54
+ - `shader` / `module`: optional per-stage `Shader` or raw `GPUShaderModule`.
55
+ - `entry` / `entryPoint`: fragment entry-point name.
56
+ - `targets`: color target formats plus blend and write-mask state.
57
+ - `constants`: optional pipeline constants.
58
+ - `primitive`: optional WebGPU primitive state.
59
+ - `depthStencil`: optional depth/stencil state.
60
+ - `multisample`: optional multisample state.
61
+ - `layout`: explicit `GPUPipelineLayout`, or `"auto"`. Defaults to `"auto"`.
18
62
  - `label`: optional debug label forwarded to WebGPU.
63
+ - `fallback`: async-only fallback policy, `"sync"` or `"throw"`.
64
+
65
+ ## Examples
19
66
 
20
- ## Example
67
+ Shared VGPU `Shader` module:
21
68
 
22
69
  ```ts
23
- const pipeline = createRenderPipeline(device, {
70
+ const pipeline = await createRenderPipelineAsync(device, {
71
+ label: "hero.pipeline",
72
+ fallback: "throw",
24
73
  shader,
25
- vertex: { entry: "vs_main" },
26
- fragment: { entry: "fs_main", targets: [{ format: "rgba8unorm" }] },
27
- primitive: { topology: "triangle-list" },
74
+ layout: explicitPipelineLayout,
75
+ vertex: {
76
+ entry: "vs_main",
77
+ buffers: [{
78
+ arrayStride: 16,
79
+ attributes: [{ shaderLocation: 0, offset: 0, format: "float32x4" }],
80
+ }],
81
+ },
82
+ fragment: {
83
+ entry: "fs_main",
84
+ targets: [{
85
+ format,
86
+ blend: {
87
+ color: { operation: "add", srcFactor: "one", dstFactor: "one-minus-src-alpha" },
88
+ alpha: { operation: "add", srcFactor: "one", dstFactor: "one-minus-src-alpha" },
89
+ },
90
+ writeMask: 0xf,
91
+ }],
92
+ },
93
+ primitive: { topology: "triangle-list", cullMode: "back" },
94
+ depthStencil: { format: "depth24plus", depthWriteEnabled: true, depthCompare: "less" },
95
+ multisample: { count: 4 },
28
96
  });
29
97
  ```
30
98
 
31
- ## Notes
99
+ Raw shader modules and per-stage constants:
100
+
101
+ ```ts
102
+ const pipeline = createRenderPipeline(device, {
103
+ layout: "auto",
104
+ vertex: { module: vertexModule, entryPoint: "vs", constants: { scale: 2 } },
105
+ fragment: { module: fragmentModule, entryPoint: "fs", targets: [{ format }] },
106
+ });
107
+ ```
108
+
109
+ Existing raw `GPURenderPipelineDescriptor`, only wanting the async fallback:
110
+
111
+ ```ts
112
+ const descriptor: GPURenderPipelineDescriptor = {
113
+ label: "hero.pipeline",
114
+ layout: pipelineLayout,
115
+ vertex: { module: shaderModule, entryPoint: "vs_main", buffers },
116
+ fragment: { module: shaderModule, entryPoint: "fs_main", targets: [{ format }] },
117
+ primitive: { topology: "triangle-list" },
118
+ };
119
+
120
+ const pipeline = await createRenderPipelineFromDescriptorAsync(device, descriptor, "throw");
121
+ ```
122
+
123
+ ## Raw escape hatch
32
124
 
33
- The returned `GPURenderPipeline` can be used directly with WebGPU APIs or
34
- passed to `RenderPass.setPipeline`.
125
+ `Shader.gpu` is an intentional advanced escape hatch to the underlying
126
+ `GPUShaderModule`. It is part of VGPU's public API surface and should be treated
127
+ as semver-protected, but direct native WebGPU usage remains responsible for
128
+ native WebGPU validation and lifecycle rules.
@@ -0,0 +1,45 @@
1
+ # @vgpu/render/perf
2
+
3
+ Measurement utilities for the optimize loop: confirm a shader change is still correct and actually
4
+ faster. **Tooling only — never call these on a live animation-frame path.** See the `measuring`
5
+ guide (`vgpu docs cat /guides/measuring.docs.md`) for the workflow.
6
+
7
+ ## gpuFrameTime
8
+
9
+ `gpuFrameTime(device, encode, options?)` measures median GPU time per frame for a render routine.
10
+
11
+ The `encode(frame, index)` callback records the frame's passes onto a vgpu `Frame` — the same body
12
+ you run in production. The harness owns warmup, the loop, submit, and timing. It uses GPU timestamp
13
+ queries when the device exposes `timestamp-query`, otherwise wall-clock timing via
14
+ `device.queue.flush()`.
15
+
16
+ ```ts
17
+ import { gpuFrameTime } from "@vgpu/render/perf";
18
+
19
+ const { medianMs, method } = await gpuFrameTime(device, (frame, i) => {
20
+ frame.renderPass(scenePass, (pass) => drawScene(pass, i));
21
+ frame.renderPass(floorPass, (pass) => drawFloor(pass, i));
22
+ });
23
+ ```
24
+
25
+ - `options.frames` (default 120), `options.warmup` (default 30), `options.batch` (wall-clock batch
26
+ size, default 8), `options.forceWallClock`, `options.label`.
27
+ - Returns `GpuFrameTimeResult`: `{ medianMs, meanMs, minMs, p95Ms, samples, method }` where `method`
28
+ is `"timestamp-query"` or `"wall-clock"`.
29
+ - Compare medians before vs after. For absolute numbers use a real-GPU device (`@vgpu/adapter-node`
30
+ Dawn or a browser); software backends give relative signal only.
31
+
32
+ ## pixelDiff
33
+
34
+ `pixelDiff(a, b)` compares two renders byte-for-byte. Pass two `Texture`s (read back via
35
+ `Texture.read()`) or two `Uint8Array`s.
36
+
37
+ ```ts
38
+ import { pixelDiff } from "@vgpu/render/perf";
39
+
40
+ const { maxByte } = await pixelDiff(before, after); // before/after are Texture or Uint8Array
41
+ ```
42
+
43
+ Returns `PixelDiffResult`: `{ maxByte, meanByte, changedBytes, totalBytes, changedFraction }`.
44
+ Interpret `maxByte` (max per-channel delta, 0–255): `0` bit-exact, `1–2` imperceptible
45
+ (driver-rounding floor), more is a real visual change. A length mismatch surfaces as `maxByte = 255`.
@@ -15,4 +15,18 @@ material has uniforms. The renderer does not write camera, transform, light, or
15
15
  material parameters.
16
16
 
17
17
  `clearValue` defaults to opaque black. `depthTarget` is optional. `renderer.gpu`
18
- returns the raw `GPUDevice`. `draw()` resolves after command submission.
18
+ returns the raw `GPUDevice`. Like other `.gpu` fields, this is an advanced,
19
+ semver-protected public escape hatch. `draw()` resolves after command submission.
20
+
21
+ ## Hot-path caveats
22
+
23
+ `RapidRenderer` is optimized for examples and simple draws, not for hiding all
24
+ homepage hot-path work. For performance-critical renderers, keep pipeline,
25
+ buffer, bind-group, and render-bundle creation in setup/warmup code. Similarly,
26
+ `material().writeUniforms()` writes CPU-side uniform data for later upload and
27
+ should be scheduled deliberately.
28
+
29
+ Do not resolve shaders, compile shaders, or rebuild pipelines inside the animation
30
+ frame path. If shader source truly changes dynamically, resolve the shader and
31
+ create the replacement pipeline outside the frame loop, then stage or
32
+ double-buffer the swap so a completed pipeline is installed at a frame boundary.
@@ -0,0 +1,46 @@
1
+ # createRenderBundle
2
+
3
+ `createRenderBundle(device, options)` records a WebGPU render bundle once during
4
+ setup and returns the finished `GPURenderBundle`.
5
+
6
+ Render bundles are reusable draw packets, not mini-passes. They do not own color
7
+ or depth attachments and they do not decide ordering. Execute them from an
8
+ explicit render pass with `pass.executeBundles([bundle])`, alongside the normal
9
+ WebGPU state constraints for bundles and render passes.
10
+
11
+ ```ts
12
+ const lightBundle = createRenderBundle(device, {
13
+ label: "hero.light-sources.bundle",
14
+ colorFormats: [format],
15
+ depthStencilFormat: "depth24plus",
16
+ sampleCount: 1,
17
+ record(bundle) {
18
+ bundle.setPipeline(lightPipeline);
19
+ bundle.setBindGroup(0, lightBindGroup);
20
+ bundle.draw(lightVertexCount);
21
+ },
22
+ });
23
+
24
+ const frame = beginFrame(device);
25
+ frame.renderPass(lightPassDescriptor, (pass) => {
26
+ pass.executeBundles([lightBundle]);
27
+ });
28
+ frame.submit();
29
+ ```
30
+
31
+ Options mirror the required parts of `GPURenderBundleEncoderDescriptor`:
32
+ `colorFormats`, optional `depthStencilFormat`, optional `sampleCount`, and the
33
+ optional depth/stencil read-only flags. The `record` callback receives a
34
+ `RenderBundleRecorder` wrapper with `gpu` escape-hatch access plus convenience
35
+ methods for `setPipeline`, `setBindGroup`, `setVertexBuffer`, and `draw`. The raw
36
+ `bundle.gpu` recorder is an advanced, semver-protected public escape hatch to the
37
+ underlying `GPURenderBundleEncoder`.
38
+
39
+ Create bundles during setup or resize, not in the per-frame hot path. A bundle is
40
+ compatible with the `colorFormats`, `depthStencilFormat`, `sampleCount`,
41
+ `depthReadOnly`, and `stencilReadOnly` values it was recorded with. If resize or
42
+ render-target reconfiguration changes any of those bundle-compatible fields,
43
+ re-record the affected bundles before the next frame.
44
+
45
+ VGPU preserves explicit pipeline, layout, and bind-group control. Bundle helpers
46
+ never infer layouts and do not switch to `layout: "auto"` for performance.
@@ -0,0 +1,15 @@
1
+ # renderTargetForCanvas
2
+
3
+ `renderTargetForCanvas(context, options?)` adapts the current texture from a
4
+ `GPUCanvasContext` into a render target that can be passed to `pass()` and other
5
+ `@vgpu/render/passes` helpers.
6
+
7
+ The returned target resolves its `color` view lazily each time it is read, so it
8
+ tracks `context.getCurrentTexture()` across frames. Use `label` and `clearColor`
9
+ in the options object to configure the generated attachment metadata and default
10
+ clear color.
11
+
12
+ ```ts
13
+ const target = renderTargetForCanvas(canvasContext, { label: "screen" });
14
+ pass({ mesh, material, target });
15
+ ```
@@ -0,0 +1,102 @@
1
+ # StorageBuffer
2
+
3
+ `StorageBuffer` is one stable storage buffer for a single render (or compute)
4
+ pass, rewritten as needed and change-gated by the caller. It is the
5
+ storage-buffer counterpart to `Uniform`: it creates a `storage | copy_dst`
6
+ buffer, a bind group layout with a single storage binding at binding 0, and a
7
+ bind group wired to that buffer. `write()` uploads bytes with
8
+ `queue.writeBuffer` at a fixed offset — there is no dynamic offset and the bind
9
+ group never changes.
10
+
11
+ ## When to use `StorageBuffer` vs `Uniform` vs `UniformPool`
12
+
13
+ Use `StorageBuffer` when the data is too large or too dynamic for a uniform:
14
+ arrays, runtime-sized data, particle/instance state, lookup tables. Storage
15
+ buffers can be much larger than uniforms — up to the adapter's
16
+ `maxStorageBufferBindingSize` (typically 128 MiB) versus a uniform's 64 KiB —
17
+ and the shader can write them with `access: "read-write"`.
18
+
19
+ Use `Uniform` for the common "globals/camera per pass" case: a small, fixed,
20
+ read-only buffer (`var<uniform>`).
21
+
22
+ Use `UniformPool` only when you have **many** small per-draw uniforms. It is a
23
+ dynamic-offset ring allocator: every slot binding is marked
24
+ `hasDynamicOffset: true` and the whole CPU mirror is re-uploaded each frame.
25
+
26
+ ## `access`: read vs read-write
27
+
28
+ `access` controls the bind group layout entry type. Both modes default to
29
+ `FRAGMENT | COMPUTE` visibility:
30
+
31
+ - `"read"` (default) → WGSL `var<storage, read>`, layout type
32
+ `"read-only-storage"`.
33
+ - `"read-write"` → WGSL `var<storage, read_write>`, layout type `"storage"`.
34
+
35
+ > Neither default includes `VERTEX`. Read-write storage is forbidden in the
36
+ > vertex stage outright. Read-only storage is _allowed_ in the vertex stage, but
37
+ > `maxStorageBuffersInVertexStage` is **0** on many adapters (software/CI Vulkan,
38
+ > some mobile GPUs) — a `VERTEX`-visible default silently invalidates the layout
39
+ > there and the draw no-ops. For vertex-stage read-only storage, pass
40
+ > `visibility: VERTEX | …` explicitly **and** raise `maxStorageBuffersInVertexStage`
41
+ > via `requiredLimits` when requesting the device.
42
+
43
+ ## Constructor
44
+
45
+ ```ts
46
+ new StorageBuffer(device, {
47
+ size, // byte size of the storage buffer (required)
48
+ label, // optional debug label for buffer, layout, and bind group
49
+ access, // "read" (default) | "read-write"
50
+ visibility, // GPUShaderStageFlags; defaults by access (see above)
51
+ bindGroupLayout, // optional pipeline-owned layout to reuse instead of creating one
52
+ });
53
+ ```
54
+
55
+ - `size`: byte size of the storage buffer. May be far larger than a uniform.
56
+ - `label`: optional debug label; suffixed `.bgl` / `.bg` for the layout and bind
57
+ group.
58
+ - `access`: `"read"` (default, read-only storage) or `"read-write"` (writable
59
+ storage). Selects the layout entry type and the default visibility.
60
+ - `visibility`: shader stages that access binding 0. Defaults to
61
+ `FRAGMENT | COMPUTE` for both access modes (no `VERTEX` — see the note above).
62
+ Ignored when `bindGroupLayout` is provided.
63
+ - `bindGroupLayout`: reuse a pipeline-owned bind group layout instead of creating
64
+ one. Its binding 0 must be a storage buffer compatible with `size` and
65
+ `access`.
66
+
67
+ ## Members
68
+
69
+ - `storageBuffer.gpu`: the underlying storage `GPUBuffer`.
70
+ - `storageBuffer.buffer`: the VGPU `Buffer` wrapper around `gpu`.
71
+ - `storageBuffer.bindGroup`: the `GPUBindGroup` to bind for the pass.
72
+ - `storageBuffer.bindGroupLayout`: the `GPUBindGroupLayout` (created or reused).
73
+ - `storageBuffer.size`: the buffer byte size.
74
+ - `storageBuffer.access`: the resolved access mode (`"read"` or `"read-write"`).
75
+ - `storageBuffer.write(data, offset = 0)`: uploads `data` via
76
+ `device.queue.writeBuffer(gpu, offset, data)`. No dynamic offset; the bind
77
+ group is unchanged. Call only when the contents actually change.
78
+ - `storageBuffer.destroy()` / `storageBuffer.dispose()`: releases the backing
79
+ buffer once; idempotent.
80
+
81
+ ## Example
82
+
83
+ ```ts
84
+ // A read-only array the fragment shader samples:
85
+ // @group(0) @binding(0) var<storage, read> values: array<f32>;
86
+ const storage = new StorageBuffer(device, { size: 4 * count, label: "values" });
87
+ storage.write(new Float32Array(values));
88
+
89
+ const pipeline = createRenderPipelineFromDescriptor(device, {
90
+ layout: device.gpu.createPipelineLayout({ bindGroupLayouts: [storage.bindGroupLayout] }),
91
+ vertex: { module, entryPoint: "vs_main" },
92
+ fragment: { module, entryPoint: "fs_main", targets: [{ format }] },
93
+ });
94
+
95
+ pass.setPipeline(pipeline);
96
+ pass.setBindGroup(0, storage.bindGroup);
97
+ pass.draw(3);
98
+
99
+ // A compute-written scratch buffer:
100
+ // @group(0) @binding(0) var<storage, read_write> out: array<u32>;
101
+ const scratch = new StorageBuffer(device, { size: 4 * count, access: "read-write" });
102
+ ```