@aardworx/wombat.rendering 0.1.0

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 (46) hide show
  1. package/package.json +67 -0
  2. package/src/commands/clear.ts +66 -0
  3. package/src/commands/index.ts +11 -0
  4. package/src/commands/render.ts +106 -0
  5. package/src/core/acquire.ts +22 -0
  6. package/src/core/adaptiveResource.ts +88 -0
  7. package/src/core/buffer.ts +45 -0
  8. package/src/core/bufferView.ts +26 -0
  9. package/src/core/clear.ts +16 -0
  10. package/src/core/command.ts +50 -0
  11. package/src/core/drawCall.ts +22 -0
  12. package/src/core/framebuffer.ts +37 -0
  13. package/src/core/framebufferSignature.ts +19 -0
  14. package/src/core/index.ts +116 -0
  15. package/src/core/pipelineState.ts +64 -0
  16. package/src/core/renderContext.ts +48 -0
  17. package/src/core/renderObject.ts +57 -0
  18. package/src/core/renderTask.ts +17 -0
  19. package/src/core/renderTree.ts +29 -0
  20. package/src/core/sampler.ts +12 -0
  21. package/src/core/shader.ts +28 -0
  22. package/src/core/texture.ts +63 -0
  23. package/src/index.ts +18 -0
  24. package/src/resources/adaptiveBuffer.ts +135 -0
  25. package/src/resources/adaptiveSampler.ts +82 -0
  26. package/src/resources/adaptiveTexture.ts +269 -0
  27. package/src/resources/framebuffer.ts +163 -0
  28. package/src/resources/framebufferSignature.ts +36 -0
  29. package/src/resources/index.ts +82 -0
  30. package/src/resources/mipGen.ts +148 -0
  31. package/src/resources/preparedComputeShader.ts +426 -0
  32. package/src/resources/preparedRenderObject.ts +498 -0
  33. package/src/resources/renderPipeline.ts +148 -0
  34. package/src/resources/shaderDiagnostics.ts +52 -0
  35. package/src/resources/sourceMapDecoder.ts +133 -0
  36. package/src/resources/uniformBuffer.ts +170 -0
  37. package/src/resources/webgpuFlags.ts +43 -0
  38. package/src/runtime/copy.ts +13 -0
  39. package/src/runtime/index.ts +24 -0
  40. package/src/runtime/renderTask.ts +137 -0
  41. package/src/runtime/renderTo.ts +154 -0
  42. package/src/runtime/runtime.ts +113 -0
  43. package/src/runtime/scenePass.ts +273 -0
  44. package/src/window/canvas.ts +252 -0
  45. package/src/window/index.ts +14 -0
  46. package/src/window/loop.ts +62 -0
@@ -0,0 +1,426 @@
1
+ // prepareComputeShader — turn a `ComputeShader` (single compute
2
+ // stage, peer of `Effect`) into an imperative dispatch surface.
3
+ //
4
+ // Mirrors Aardvark.GPGPU's `IComputeShader` + mutable
5
+ // `IComputeInputBinding` pattern: callers build a binding by name,
6
+ // `set` values/buffers/textures/samplers into it imperatively, then
7
+ // `dispatch` to encode + submit (or hand an encoder for batching).
8
+ //
9
+ // Unlike `prepareRenderObject`, none of the inputs are `aval<T>`. The
10
+ // binding owns its own mutable state; uniform writes are byte-poked
11
+ // into a staging `ArrayBuffer` and uploaded on dispatch.
12
+ //
13
+ // Supported binding kinds (from the program interface):
14
+ // - One uniform block per group: `setUniform(name, value)`.
15
+ // - Storage buffers: `setBuffer(name, GPUBuffer)`.
16
+ // - Sampled textures: `setTexture(name, GPUTextureView | GPUTexture)`.
17
+ // - Samplers: `setSampler(name, GPUSampler)`.
18
+ //
19
+ // Storage textures aren't covered here yet — separate gap.
20
+
21
+ import {
22
+ type ComputeShader,
23
+ type ProgramInterface,
24
+ } from "../core/index.js";
25
+ import type { StorageTextureFormat, Type } from "@aardworx/wombat.shader/ir";
26
+ import { ShaderStage } from "./webgpuFlags.js";
27
+ import { installShaderDiagnostics } from "./shaderDiagnostics.js";
28
+ import { makePackedView, writeField, type PackedView } from "./uniformBuffer.js";
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Bind-group-entry descriptions (mirrors PreparedRenderObject's shape, but
32
+ // over plain GPU handles instead of AdaptiveResource<...>).
33
+ // ---------------------------------------------------------------------------
34
+
35
+ type EntryDesc =
36
+ | { kind: "ubuf"; binding: number; uniformBlock: import("../core/index.js").UniformBlockInfo }
37
+ | { kind: "sbuf"; binding: number; name: string; access: "read" | "read_write" }
38
+ | { kind: "tex"; binding: number; name: string; sampleType: GPUTextureSampleType }
39
+ | { kind: "sampler";binding: number; name: string }
40
+ | { kind: "stex"; binding: number; name: string; format: GPUTextureFormat; access: GPUStorageTextureAccess };
41
+
42
+ interface GroupDesc {
43
+ readonly group: number;
44
+ readonly layout: GPUBindGroupLayout;
45
+ readonly entries: readonly EntryDesc[];
46
+ }
47
+
48
+ function storageAccessToGPU(a: "read" | "write" | "read_write"): GPUStorageTextureAccess {
49
+ switch (a) {
50
+ case "read": return "read-only";
51
+ case "write": return "write-only";
52
+ case "read_write": return "read-write";
53
+ }
54
+ }
55
+
56
+ function sampleTypeFor(type: Type): GPUTextureSampleType {
57
+ if (type.kind === "Texture") {
58
+ if (type.comparison === true) return "depth";
59
+ const s = type.sampled;
60
+ if (s.kind === "Int") return s.signed ? "sint" : "uint";
61
+ return "float";
62
+ }
63
+ return "float";
64
+ }
65
+
66
+ function buildGroups(device: GPUDevice, descs: readonly EntryDesc[][]): GroupDesc[] {
67
+ const out: GroupDesc[] = [];
68
+ for (let g = 0; g < descs.length; g++) {
69
+ const entries = (descs[g] ?? []).slice().sort((a, b) => a.binding - b.binding);
70
+ const layoutEntries: GPUBindGroupLayoutEntry[] = entries.map(e => {
71
+ const visibility = ShaderStage.COMPUTE;
72
+ switch (e.kind) {
73
+ case "ubuf": return { binding: e.binding, visibility, buffer: { type: "uniform" } };
74
+ case "sbuf": return {
75
+ binding: e.binding, visibility,
76
+ buffer: { type: e.access === "read_write" ? "storage" : "read-only-storage" },
77
+ };
78
+ case "tex": return { binding: e.binding, visibility, texture: { sampleType: e.sampleType } };
79
+ case "sampler": return { binding: e.binding, visibility, sampler: { type: "filtering" } };
80
+ case "stex": return { binding: e.binding, visibility, storageTexture: { access: e.access, format: e.format } };
81
+ }
82
+ });
83
+ const layout = device.createBindGroupLayout({ entries: layoutEntries });
84
+ out.push({ group: g, layout, entries });
85
+ }
86
+ return out;
87
+ }
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // ComputeInputBinding — mutable, name-keyed state for one shader.
91
+ // ---------------------------------------------------------------------------
92
+
93
+ export interface DispatchSize {
94
+ readonly x: number;
95
+ readonly y: number;
96
+ readonly z: number;
97
+ }
98
+
99
+ interface UboField {
100
+ readonly offset: number;
101
+ readonly type: import("../core/index.js").UniformFieldInfo["type"];
102
+ }
103
+
104
+ interface UboState {
105
+ readonly group: number;
106
+ readonly binding: number;
107
+ readonly view: PackedView;
108
+ readonly fields: ReadonlyMap<string, UboField>;
109
+ buffer: GPUBuffer | undefined;
110
+ dirty: boolean;
111
+ }
112
+
113
+ export class ComputeInputBinding {
114
+ private readonly _ubos = new Map<string, UboState>(); // ubo name → state
115
+ private readonly _ubosByBinding = new Map<string, UboState>(); // "g:b" → state
116
+ private readonly _buffers = new Map<string, GPUBuffer>(); // name → buffer
117
+ private readonly _textures = new Map<string, GPUTextureView>(); // name → view
118
+ private readonly _samplers = new Map<string, GPUSampler>(); // name → sampler
119
+ private readonly _storageTextures = new Map<string, GPUTextureView>(); // name → view (storage)
120
+ /** Names declared by the shader as storage textures (vs sampled). */
121
+ private readonly _storageTextureNames: ReadonlySet<string>;
122
+
123
+ /** @internal — built by `PreparedComputeShader`. */
124
+ constructor(
125
+ private readonly device: GPUDevice,
126
+ private readonly groups: readonly GroupDesc[],
127
+ private readonly iface: ProgramInterface,
128
+ private readonly label: string | undefined,
129
+ ) {
130
+ this._storageTextureNames = new Set(
131
+ iface.samplers.filter(s => s.type.kind === "StorageTexture").map(s => s.name),
132
+ );
133
+ for (const ub of iface.uniformBlocks) {
134
+ const view = makePackedView(ub.size);
135
+ const fields = new Map<string, UboField>(
136
+ ub.fields.map(f => [f.name, { offset: f.offset, type: f.type }]),
137
+ );
138
+ const state: UboState = {
139
+ group: ub.group, binding: ub.slot, view, fields,
140
+ buffer: undefined, dirty: true,
141
+ };
142
+ this._ubos.set(ub.name, state);
143
+ this._ubosByBinding.set(`${ub.group}:${ub.slot}`, state);
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Write a uniform value by name. Searches all uniform blocks; the
149
+ * first hit wins. Throws if the name isn't a uniform field.
150
+ * Accepted types: `number`, `Float32Array`/`Int32Array`/`Uint32Array`,
151
+ * any wombat.base packed value (`{ _data: TypedArray }`).
152
+ */
153
+ setUniform(name: string, value: unknown): this {
154
+ for (const ub of this._ubos.values()) {
155
+ const f = ub.fields.get(name);
156
+ if (f === undefined) continue;
157
+ writeField(ub.view, f.offset, value, f.type);
158
+ ub.dirty = true;
159
+ return this;
160
+ }
161
+ throw new Error(`ComputeInputBinding: no uniform field "${name}"`);
162
+ }
163
+
164
+ setBuffer(name: string, buffer: GPUBuffer): this {
165
+ if (!this.iface.storageBuffers.some(s => s.name === name)) {
166
+ throw new Error(`ComputeInputBinding: no storage buffer "${name}"`);
167
+ }
168
+ this._buffers.set(name, buffer);
169
+ return this;
170
+ }
171
+
172
+ setTexture(name: string, source: GPUTexture | GPUTextureView): this {
173
+ if (!this.iface.textures.some(t => t.name === name)) {
174
+ throw new Error(`ComputeInputBinding: no texture "${name}"`);
175
+ }
176
+ const view = "createView" in source ? source.createView() : source;
177
+ this._textures.set(name, view);
178
+ return this;
179
+ }
180
+
181
+ /**
182
+ * Bind a storage-texture view (write/read/read_write per the
183
+ * shader declaration). `view` should reference a `GPUTexture`
184
+ * created with `STORAGE_BINDING` usage in the format the shader
185
+ * expects.
186
+ */
187
+ setStorageTexture(name: string, source: GPUTexture | GPUTextureView): this {
188
+ if (!this._storageTextureNames.has(name)) {
189
+ throw new Error(`ComputeInputBinding: no storage texture "${name}"`);
190
+ }
191
+ const view = "createView" in source ? source.createView() : source;
192
+ this._storageTextures.set(name, view);
193
+ return this;
194
+ }
195
+
196
+ setSampler(name: string, sampler: GPUSampler): this {
197
+ if (!this.iface.samplers.some(s => s.name === name)) {
198
+ throw new Error(`ComputeInputBinding: no sampler "${name}"`);
199
+ }
200
+ this._samplers.set(name, sampler);
201
+ return this;
202
+ }
203
+
204
+ /** @internal — flush dirty UBOs and build current bind groups. */
205
+ flushAndBuildBindGroups(): GPUBindGroup[] {
206
+ for (const ub of this._ubos.values()) {
207
+ if (ub.buffer === undefined) {
208
+ ub.buffer = this.device.createBuffer({
209
+ size: ub.view.bytes.byteLength,
210
+ usage: 0x40 | 0x8, // UNIFORM | COPY_DST
211
+ ...(this.label !== undefined ? { label: `${this.label}.ubo` } : {}),
212
+ });
213
+ }
214
+ if (ub.dirty) {
215
+ this.device.queue.writeBuffer(ub.buffer, 0, ub.view.bytes);
216
+ ub.dirty = false;
217
+ }
218
+ }
219
+
220
+ const out: GPUBindGroup[] = [];
221
+ for (const g of this.groups) {
222
+ const entries: GPUBindGroupEntry[] = g.entries.map(e => {
223
+ switch (e.kind) {
224
+ case "ubuf": {
225
+ const ub = this._ubosByBinding.get(`${g.group}:${e.binding}`);
226
+ if (!ub || ub.buffer === undefined) {
227
+ throw new Error(`ComputeInputBinding.dispatch: UBO at group=${g.group} binding=${e.binding} not initialised`);
228
+ }
229
+ return { binding: e.binding, resource: { buffer: ub.buffer } };
230
+ }
231
+ case "sbuf": {
232
+ const buf = this._buffers.get(e.name);
233
+ if (buf === undefined) throw new Error(`ComputeInputBinding.dispatch: missing storage buffer "${e.name}"`);
234
+ return { binding: e.binding, resource: { buffer: buf } };
235
+ }
236
+ case "tex": {
237
+ const view = this._textures.get(e.name);
238
+ if (view === undefined) throw new Error(`ComputeInputBinding.dispatch: missing texture "${e.name}"`);
239
+ return { binding: e.binding, resource: view };
240
+ }
241
+ case "sampler": {
242
+ const samp = this._samplers.get(e.name);
243
+ if (samp === undefined) throw new Error(`ComputeInputBinding.dispatch: missing sampler "${e.name}"`);
244
+ return { binding: e.binding, resource: samp };
245
+ }
246
+ case "stex": {
247
+ const view = this._storageTextures.get(e.name);
248
+ if (view === undefined) throw new Error(`ComputeInputBinding.dispatch: missing storage texture "${e.name}"`);
249
+ return { binding: e.binding, resource: view };
250
+ }
251
+ }
252
+ });
253
+ out.push(this.device.createBindGroup({ layout: g.layout, entries }));
254
+ }
255
+ return out;
256
+ }
257
+
258
+ /** Free the binding's owned UBO buffers. Storage buffers / textures / samplers are caller-owned. */
259
+ dispose(): void {
260
+ for (const ub of this._ubos.values()) {
261
+ if (ub.buffer !== undefined) {
262
+ ub.buffer.destroy();
263
+ ub.buffer = undefined;
264
+ }
265
+ ub.dirty = true;
266
+ }
267
+ }
268
+ }
269
+
270
+ // ---------------------------------------------------------------------------
271
+ // PreparedComputeShader
272
+ // ---------------------------------------------------------------------------
273
+
274
+ export interface PrepareComputeShaderOptions {
275
+ readonly label?: string;
276
+ }
277
+
278
+ export class PreparedComputeShader {
279
+ /** @internal */
280
+ constructor(
281
+ private readonly device: GPUDevice,
282
+ public readonly pipeline: GPUComputePipeline,
283
+ public readonly interface_: ProgramInterface,
284
+ private readonly groups: readonly GroupDesc[],
285
+ private readonly label: string | undefined,
286
+ ) {}
287
+
288
+ /** Same shape as Aardvark's `IComputeShader.Interface` — useful for tooling. */
289
+ get programInterface(): ProgramInterface { return this.interface_; }
290
+
291
+ /** Allocate a fresh, empty binding. */
292
+ createInputBinding(): ComputeInputBinding {
293
+ return new ComputeInputBinding(this.device, this.groups, this.interface_, this.label);
294
+ }
295
+
296
+ /**
297
+ * Encode `Bind + SetInput + Dispatch` into the supplied encoder.
298
+ * Use this when batching compute work alongside other GPU commands
299
+ * — caller submits.
300
+ */
301
+ encode(encoder: GPUCommandEncoder, binding: ComputeInputBinding, groups: DispatchSize): void {
302
+ const bindGroups = binding.flushAndBuildBindGroups();
303
+ const pass = encoder.beginComputePass({
304
+ ...(this.label !== undefined ? { label: this.label } : {}),
305
+ });
306
+ pass.setPipeline(this.pipeline);
307
+ for (let i = 0; i < bindGroups.length; i++) pass.setBindGroup(i, bindGroups[i]!);
308
+ pass.dispatchWorkgroups(groups.x, groups.y, groups.z);
309
+ pass.end();
310
+ }
311
+
312
+ /**
313
+ * One-shot dispatch — builds an encoder, encodes, submits, awaits
314
+ * `queue.onSubmittedWorkDone()`. Aardvark's `IComputeShader.Invoke`
315
+ * shape.
316
+ */
317
+ async dispatch(binding: ComputeInputBinding, groups: DispatchSize): Promise<void> {
318
+ const encoder = this.device.createCommandEncoder({
319
+ ...(this.label !== undefined ? { label: `${this.label}.dispatch` } : {}),
320
+ });
321
+ this.encode(encoder, binding, groups);
322
+ this.device.queue.submit([encoder.finish()]);
323
+ await this.device.queue.onSubmittedWorkDone();
324
+ }
325
+ }
326
+
327
+ // ---------------------------------------------------------------------------
328
+ // Compute pipeline cache
329
+ // ---------------------------------------------------------------------------
330
+
331
+ interface ComputePipelineCache {
332
+ pipelines: Map<string, GPUComputePipeline>;
333
+ modules: Map<string, GPUShaderModule>;
334
+ }
335
+ const computeCaches = new WeakMap<GPUDevice, ComputePipelineCache>();
336
+ function cacheFor(device: GPUDevice): ComputePipelineCache {
337
+ let c = computeCaches.get(device);
338
+ if (c === undefined) {
339
+ c = { pipelines: new Map(), modules: new Map() };
340
+ computeCaches.set(device, c);
341
+ }
342
+ return c;
343
+ }
344
+
345
+ // ---------------------------------------------------------------------------
346
+ // prepareComputeShader
347
+ // ---------------------------------------------------------------------------
348
+
349
+ export function prepareComputeShader(
350
+ device: GPUDevice,
351
+ shader: ComputeShader,
352
+ opts: PrepareComputeShaderOptions = {},
353
+ ): PreparedComputeShader {
354
+ const compiled = shader.compile({ target: "wgsl" });
355
+ const csStage = compiled.stages.find(s => s.stage === "compute");
356
+ if (csStage === undefined) {
357
+ throw new Error("prepareComputeShader: ComputeShader compiled without a compute stage");
358
+ }
359
+ const iface = compiled.interface;
360
+
361
+ const maxGroup = Math.max(
362
+ -1,
363
+ ...iface.uniformBlocks.map(b => b.group),
364
+ ...iface.samplers.map(b => b.group),
365
+ ...iface.textures.map(b => b.group),
366
+ ...iface.storageBuffers.map(b => b.group),
367
+ );
368
+ const perGroup: EntryDesc[][] = [];
369
+ for (let g = 0; g <= maxGroup; g++) perGroup.push([]);
370
+
371
+ for (const ub of iface.uniformBlocks) {
372
+ perGroup[ub.group]!.push({ kind: "ubuf", binding: ub.slot, uniformBlock: ub });
373
+ }
374
+ for (const t of iface.textures) {
375
+ perGroup[t.group]!.push({ kind: "tex", binding: t.slot, name: t.name, sampleType: sampleTypeFor(t.type) });
376
+ }
377
+ for (const s of iface.samplers) {
378
+ if (s.type.kind === "StorageTexture") {
379
+ perGroup[s.group]!.push({
380
+ kind: "stex", binding: s.slot, name: s.name,
381
+ format: s.type.format as GPUTextureFormat,
382
+ access: storageAccessToGPU(s.type.access),
383
+ });
384
+ } else {
385
+ perGroup[s.group]!.push({ kind: "sampler", binding: s.slot, name: s.name });
386
+ }
387
+ }
388
+ for (const sb of iface.storageBuffers) {
389
+ perGroup[sb.group]!.push({ kind: "sbuf", binding: sb.slot, name: sb.name, access: sb.access });
390
+ }
391
+
392
+ const groups = buildGroups(device, perGroup);
393
+
394
+ // Compile module + pipeline (cached per device by effect id).
395
+ const cache = cacheFor(device);
396
+ const moduleKey = csStage.source;
397
+ let module = cache.modules.get(moduleKey);
398
+ if (module === undefined) {
399
+ module = device.createShaderModule({
400
+ code: csStage.source,
401
+ ...(opts.label !== undefined ? { label: `${opts.label}.cs` } : {}),
402
+ });
403
+ cache.modules.set(moduleKey, module);
404
+ installShaderDiagnostics(module, csStage.source, {
405
+ ...(opts.label !== undefined ? { label: opts.label } : {}),
406
+ ...(csStage.sourceMap ? { sourceMap: csStage.sourceMap } : {}),
407
+ });
408
+ }
409
+
410
+ const pipelineKey = `${shader.id}|${csStage.entryName}|${groups.length}`;
411
+ let pipeline = cache.pipelines.get(pipelineKey);
412
+ if (pipeline === undefined) {
413
+ const pipelineLayout = device.createPipelineLayout({
414
+ bindGroupLayouts: groups.map(g => g.layout),
415
+ ...(opts.label !== undefined ? { label: `${opts.label}.layout` } : {}),
416
+ });
417
+ pipeline = device.createComputePipeline({
418
+ layout: pipelineLayout,
419
+ compute: { module, entryPoint: csStage.entryName },
420
+ ...(opts.label !== undefined ? { label: opts.label } : {}),
421
+ });
422
+ cache.pipelines.set(pipelineKey, pipeline);
423
+ }
424
+
425
+ return new PreparedComputeShader(device, pipeline, iface, groups, opts.label);
426
+ }