@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,269 @@
1
+ // prepareAdaptiveTexture — lift `aval<ITexture>` to
2
+ // `AdaptiveResource<GPUTexture>`.
3
+ //
4
+ // Behaviour matches `prepareAdaptiveBuffer`: on `compute`, if the
5
+ // source is `gpu` return the user's handle; if `host`, ensure we
6
+ // own a GPUTexture of the right size/format and upload via the
7
+ // queue (writeTexture for raw, copyExternalImageToTexture for
8
+ // external sources).
9
+ //
10
+ // Reuse: a host source with the same dimensions + format reuses
11
+ // the existing texture; mismatch triggers a fresh allocation.
12
+ //
13
+ // Mip-map generation is not implemented in this first cut —
14
+ // `generateMips: true` on an external source allocates the mip
15
+ // levels but only mip 0 is uploaded. (Mip generation requires a
16
+ // compute pass; that lands in a follow-up.)
17
+
18
+ import {
19
+ AdaptiveResource,
20
+ tryAcquire,
21
+ tryRelease,
22
+ type ITexture,
23
+ type ExternalTextureSource,
24
+ type RawTextureSource,
25
+ } from "../core/index.js";
26
+ import {
27
+ type AdaptiveToken,
28
+ type aval,
29
+ } from "@aardworx/wombat.adaptive";
30
+ import { TextureUsage } from "./webgpuFlags.js";
31
+
32
+ export interface PrepareAdaptiveTextureOptions {
33
+ /** Extra usage flags. `COPY_DST` and `TEXTURE_BINDING` are added automatically. */
34
+ readonly usage?: GPUTextureUsageFlags;
35
+ /** Optional debug label. */
36
+ readonly label?: string;
37
+ }
38
+
39
+ interface OwnedDesc {
40
+ width: number;
41
+ height: number;
42
+ depthOrArrayLayers: number;
43
+ format: GPUTextureFormat;
44
+ mipLevelCount: number;
45
+ }
46
+
47
+ function descsEqual(a: OwnedDesc, b: OwnedDesc): boolean {
48
+ return (
49
+ a.width === b.width
50
+ && a.height === b.height
51
+ && a.depthOrArrayLayers === b.depthOrArrayLayers
52
+ && a.format === b.format
53
+ && a.mipLevelCount === b.mipLevelCount
54
+ );
55
+ }
56
+
57
+ function descFor(src: RawTextureSource | ExternalTextureSource): OwnedDesc {
58
+ if (src.kind === "raw") {
59
+ return {
60
+ width: src.width,
61
+ height: src.height,
62
+ depthOrArrayLayers: src.depthOrArrayLayers ?? 1,
63
+ format: src.format,
64
+ mipLevelCount: src.mipLevelCount ?? 1,
65
+ };
66
+ }
67
+ // external
68
+ const s = src.source;
69
+ let width = 0;
70
+ let height = 0;
71
+ if (s instanceof ImageData) {
72
+ width = s.width; height = s.height;
73
+ } else if ("naturalWidth" in s && typeof s.naturalWidth === "number") {
74
+ // HTMLImageElement
75
+ width = s.naturalWidth || s.width;
76
+ height = s.naturalHeight || s.height;
77
+ } else if ("videoWidth" in s && typeof s.videoWidth === "number") {
78
+ // HTMLVideoElement
79
+ width = s.videoWidth; height = s.videoHeight;
80
+ } else {
81
+ // ImageBitmap, HTMLCanvasElement, OffscreenCanvas — all have width/height
82
+ width = (s as { width: number }).width;
83
+ height = (s as { height: number }).height;
84
+ }
85
+ const generateMips = src.generateMips === true;
86
+ return {
87
+ width, height, depthOrArrayLayers: 1,
88
+ format: src.format ?? "rgba8unorm",
89
+ mipLevelCount: generateMips ? mipCount(width, height) : 1,
90
+ };
91
+ }
92
+
93
+ function mipCount(w: number, h: number): number {
94
+ return Math.floor(Math.log2(Math.max(w, h))) + 1;
95
+ }
96
+
97
+ class AdaptiveTexture extends AdaptiveResource<GPUTexture> {
98
+ private _owned: GPUTexture | undefined = undefined;
99
+ private _ownedDesc: OwnedDesc | undefined = undefined;
100
+
101
+ constructor(
102
+ private readonly device: GPUDevice,
103
+ private readonly source: aval<ITexture>,
104
+ private readonly opts: PrepareAdaptiveTextureOptions,
105
+ ) {
106
+ super();
107
+ }
108
+
109
+ protected create(): void { tryAcquire(this.source); }
110
+
111
+ protected destroy(): void {
112
+ if (this._owned !== undefined) {
113
+ this._owned.destroy();
114
+ this._owned = undefined;
115
+ this._ownedDesc = undefined;
116
+ }
117
+ tryRelease(this.source);
118
+ }
119
+
120
+ override compute(token: AdaptiveToken): GPUTexture {
121
+ const src = this.source.getValue(token);
122
+ if (src.kind === "gpu") {
123
+ if (this._owned !== undefined) {
124
+ this._owned.destroy();
125
+ this._owned = undefined;
126
+ this._ownedDesc = undefined;
127
+ }
128
+ return src.texture;
129
+ }
130
+ const desc = descFor(src.source);
131
+ const usage =
132
+ (this.opts.usage ?? 0)
133
+ | TextureUsage.COPY_DST
134
+ | TextureUsage.TEXTURE_BINDING
135
+ | (src.source.kind === "external" ? TextureUsage.RENDER_ATTACHMENT : 0);
136
+ if (this._owned === undefined || this._ownedDesc === undefined || !descsEqual(this._ownedDesc, desc)) {
137
+ if (this._owned !== undefined) this._owned.destroy();
138
+ const tdesc: GPUTextureDescriptor = {
139
+ size: { width: desc.width, height: desc.height, depthOrArrayLayers: desc.depthOrArrayLayers },
140
+ format: desc.format,
141
+ mipLevelCount: desc.mipLevelCount,
142
+ usage,
143
+ ...(this.opts.label !== undefined ? { label: this.opts.label } : {}),
144
+ };
145
+ this._owned = this.device.createTexture(tdesc);
146
+ this._ownedDesc = desc;
147
+ }
148
+ if (src.source.kind === "raw") {
149
+ uploadRaw(this.device, this._owned, src.source);
150
+ } else {
151
+ uploadExternal(this.device, this._owned, src.source);
152
+ }
153
+ return this._owned;
154
+ }
155
+ }
156
+
157
+ function uploadRaw(device: GPUDevice, tex: GPUTexture, src: RawTextureSource): void {
158
+ const bytes = src.data instanceof ArrayBuffer ? new Uint8Array(src.data) : new Uint8Array(src.data.buffer, src.data.byteOffset, src.data.byteLength);
159
+ const block = blockInfoFor(src.format);
160
+ const blocksPerRow = Math.ceil(src.width / block.width);
161
+ const rowsOfBlocks = Math.ceil(src.height / block.height);
162
+ device.queue.writeTexture(
163
+ { texture: tex },
164
+ bytes as unknown as GPUAllowSharedBufferSource,
165
+ { bytesPerRow: blocksPerRow * block.bytesPerBlock, rowsPerImage: rowsOfBlocks },
166
+ { width: src.width, height: src.height, depthOrArrayLayers: src.depthOrArrayLayers ?? 1 },
167
+ );
168
+ }
169
+
170
+ function uploadExternal(device: GPUDevice, tex: GPUTexture, src: ExternalTextureSource): void {
171
+ device.queue.copyExternalImageToTexture(
172
+ { source: src.source as GPUImageCopyExternalImageSource },
173
+ { texture: tex },
174
+ { width: (src.source as { width: number }).width, height: (src.source as { height: number }).height, depthOrArrayLayers: 1 },
175
+ );
176
+ }
177
+
178
+ interface BlockInfo {
179
+ readonly width: number; // pixels per block in x
180
+ readonly height: number; // pixels per block in y
181
+ readonly bytesPerBlock: number;
182
+ }
183
+
184
+ /**
185
+ * Block dimensions + size for a WebGPU texture format. Linear
186
+ * (uncompressed) formats report 1×1 blocks. Block-compressed
187
+ * formats (BC*, ETC*, ASTC*, EAC) use their native block size so
188
+ * `writeTexture`'s bytesPerRow / rowsPerImage land on the right
189
+ * block grid.
190
+ */
191
+ function blockInfoFor(format: GPUTextureFormat): BlockInfo {
192
+ switch (format) {
193
+ // 8-bit
194
+ case "r8unorm": case "r8snorm": case "r8uint": case "r8sint":
195
+ return { width: 1, height: 1, bytesPerBlock: 1 };
196
+ // 16-bit
197
+ case "rg8unorm": case "rg8snorm": case "rg8uint": case "rg8sint":
198
+ case "r16uint": case "r16sint": case "r16float":
199
+ return { width: 1, height: 1, bytesPerBlock: 2 };
200
+ // 32-bit
201
+ case "rgba8unorm": case "rgba8unorm-srgb": case "rgba8snorm":
202
+ case "rgba8uint": case "rgba8sint":
203
+ case "bgra8unorm": case "bgra8unorm-srgb":
204
+ case "rgb10a2unorm": case "rgb10a2uint":
205
+ case "rg11b10ufloat": case "rgb9e5ufloat":
206
+ case "rg16uint": case "rg16sint": case "rg16float":
207
+ case "r32uint": case "r32sint": case "r32float":
208
+ return { width: 1, height: 1, bytesPerBlock: 4 };
209
+ // 64-bit
210
+ case "rgba16uint": case "rgba16sint": case "rgba16float":
211
+ case "rg32uint": case "rg32sint": case "rg32float":
212
+ return { width: 1, height: 1, bytesPerBlock: 8 };
213
+ // 128-bit
214
+ case "rgba32uint": case "rgba32sint": case "rgba32float":
215
+ return { width: 1, height: 1, bytesPerBlock: 16 };
216
+
217
+ // ---- Block-compressed formats ----
218
+ // BC1/2/3/4/5 (DXT/RGTC) — 4×4 blocks
219
+ case "bc1-rgba-unorm": case "bc1-rgba-unorm-srgb":
220
+ case "bc4-r-unorm": case "bc4-r-snorm":
221
+ return { width: 4, height: 4, bytesPerBlock: 8 };
222
+ case "bc2-rgba-unorm": case "bc2-rgba-unorm-srgb":
223
+ case "bc3-rgba-unorm": case "bc3-rgba-unorm-srgb":
224
+ case "bc5-rg-unorm": case "bc5-rg-snorm":
225
+ case "bc6h-rgb-ufloat": case "bc6h-rgb-float":
226
+ case "bc7-rgba-unorm": case "bc7-rgba-unorm-srgb":
227
+ return { width: 4, height: 4, bytesPerBlock: 16 };
228
+
229
+ // ETC2 — 4×4 blocks
230
+ case "etc2-rgb8unorm": case "etc2-rgb8unorm-srgb":
231
+ case "etc2-rgb8a1unorm": case "etc2-rgb8a1unorm-srgb":
232
+ case "eac-r11unorm": case "eac-r11snorm":
233
+ return { width: 4, height: 4, bytesPerBlock: 8 };
234
+ case "etc2-rgba8unorm": case "etc2-rgba8unorm-srgb":
235
+ case "eac-rg11unorm": case "eac-rg11snorm":
236
+ return { width: 4, height: 4, bytesPerBlock: 16 };
237
+
238
+ // ASTC — variable block size, all 16 bytes/block.
239
+ case "astc-4x4-unorm": case "astc-4x4-unorm-srgb": return { width: 4, height: 4, bytesPerBlock: 16 };
240
+ case "astc-5x4-unorm": case "astc-5x4-unorm-srgb": return { width: 5, height: 4, bytesPerBlock: 16 };
241
+ case "astc-5x5-unorm": case "astc-5x5-unorm-srgb": return { width: 5, height: 5, bytesPerBlock: 16 };
242
+ case "astc-6x5-unorm": case "astc-6x5-unorm-srgb": return { width: 6, height: 5, bytesPerBlock: 16 };
243
+ case "astc-6x6-unorm": case "astc-6x6-unorm-srgb": return { width: 6, height: 6, bytesPerBlock: 16 };
244
+ case "astc-8x5-unorm": case "astc-8x5-unorm-srgb": return { width: 8, height: 5, bytesPerBlock: 16 };
245
+ case "astc-8x6-unorm": case "astc-8x6-unorm-srgb": return { width: 8, height: 6, bytesPerBlock: 16 };
246
+ case "astc-8x8-unorm": case "astc-8x8-unorm-srgb": return { width: 8, height: 8, bytesPerBlock: 16 };
247
+ case "astc-10x5-unorm": case "astc-10x5-unorm-srgb": return { width: 10, height: 5, bytesPerBlock: 16 };
248
+ case "astc-10x6-unorm": case "astc-10x6-unorm-srgb": return { width: 10, height: 6, bytesPerBlock: 16 };
249
+ case "astc-10x8-unorm": case "astc-10x8-unorm-srgb": return { width: 10, height: 8, bytesPerBlock: 16 };
250
+ case "astc-10x10-unorm": case "astc-10x10-unorm-srgb": return { width: 10, height: 10, bytesPerBlock: 16 };
251
+ case "astc-12x10-unorm": case "astc-12x10-unorm-srgb": return { width: 12, height: 10, bytesPerBlock: 16 };
252
+ case "astc-12x12-unorm": case "astc-12x12-unorm-srgb": return { width: 12, height: 12, bytesPerBlock: 16 };
253
+
254
+ default:
255
+ throw new Error(`blockInfoFor: unsupported format ${format}`);
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Wrap an `aval<ITexture>` as a ref-counted, adaptively-uploaded
261
+ * `AdaptiveResource<GPUTexture>`.
262
+ */
263
+ export function prepareAdaptiveTexture(
264
+ device: GPUDevice,
265
+ source: aval<ITexture>,
266
+ opts: PrepareAdaptiveTextureOptions = {},
267
+ ): AdaptiveResource<GPUTexture> {
268
+ return new AdaptiveTexture(device, source, opts);
269
+ }
@@ -0,0 +1,163 @@
1
+ // allocateFramebuffer — `aval<{width,height}>` → `AdaptiveResource<IFramebuffer>`.
2
+ //
3
+ // The signature defines names + formats; the size aval drives
4
+ // allocation. Each color attachment gets its own GPUTexture sized
5
+ // to the current `width × height`; depth/stencil similarly. On
6
+ // size change, all textures are torn down and reallocated.
7
+ //
8
+ // This is the building block for `runtime.renderTo(scene, size)`:
9
+ // - The render task acquires the framebuffer's resource;
10
+ // - Each frame, evaluates the FBO via its own AdaptiveResource
11
+ // pipeline, getting current views;
12
+ // - Encodes a render pass into those views;
13
+ // - Exposes the chosen color attachment as an
14
+ // `aval<ITexture>` so downstream samplers see live data.
15
+
16
+ import { AdaptiveResource, type IFramebuffer } from "../core/index.js";
17
+ import { HashMap, type AdaptiveToken, type aval } from "@aardworx/wombat.adaptive";
18
+ import { TextureUsage } from "./webgpuFlags.js";
19
+
20
+ export interface FramebufferSize {
21
+ readonly width: number;
22
+ readonly height: number;
23
+ }
24
+
25
+ export interface AllocateFramebufferOptions {
26
+ /** Extra usage flags ORed into every attachment. `RENDER_ATTACHMENT` is added automatically. */
27
+ readonly extraUsage?: GPUTextureUsageFlags;
28
+ /** Optional debug-label prefix; attachment name is appended. */
29
+ readonly labelPrefix?: string;
30
+ }
31
+
32
+ class AdaptiveFramebuffer extends AdaptiveResource<IFramebuffer> {
33
+ private _ownedColors = new Map<string, GPUTexture>();
34
+ private _ownedResolves = new Map<string, GPUTexture>();
35
+ private _ownedDepth: GPUTexture | undefined;
36
+ private _lastSize: FramebufferSize | undefined;
37
+
38
+ constructor(
39
+ private readonly device: GPUDevice,
40
+ private readonly signature: import("../core/index.js").FramebufferSignature,
41
+ private readonly size: aval<FramebufferSize>,
42
+ private readonly opts: AllocateFramebufferOptions,
43
+ ) {
44
+ super();
45
+ }
46
+
47
+ protected create(): void {}
48
+
49
+ protected destroy(): void {
50
+ for (const t of this._ownedColors.values()) t.destroy();
51
+ this._ownedColors.clear();
52
+ for (const t of this._ownedResolves.values()) t.destroy();
53
+ this._ownedResolves.clear();
54
+ if (this._ownedDepth !== undefined) {
55
+ this._ownedDepth.destroy();
56
+ this._ownedDepth = undefined;
57
+ }
58
+ this._lastSize = undefined;
59
+ }
60
+
61
+ override compute(token: AdaptiveToken): IFramebuffer {
62
+ const size = this.size.getValue(token);
63
+ if (
64
+ this._lastSize === undefined
65
+ || this._lastSize.width !== size.width
66
+ || this._lastSize.height !== size.height
67
+ ) {
68
+ this.reallocate(size);
69
+ this._lastSize = size;
70
+ }
71
+ const msaa = this.signature.sampleCount > 1;
72
+ let colorViews = HashMap.empty<string, GPUTextureView>();
73
+ let colorTextures = HashMap.empty<string, GPUTexture>();
74
+ let resolveViews = HashMap.empty<string, GPUTextureView>();
75
+ for (const [name, tex] of this._ownedColors.entries()) {
76
+ colorViews = colorViews.add(name, tex.createView());
77
+ if (msaa) {
78
+ const resolve = this._ownedResolves.get(name);
79
+ if (resolve === undefined) {
80
+ throw new Error(`framebuffer: missing resolve target for "${name}"`);
81
+ }
82
+ resolveViews = resolveViews.add(name, resolve.createView());
83
+ colorTextures = colorTextures.add(name, resolve);
84
+ } else {
85
+ colorTextures = colorTextures.add(name, tex);
86
+ }
87
+ }
88
+ return {
89
+ signature: this.signature,
90
+ colors: colorViews,
91
+ colorTextures,
92
+ ...(msaa ? { resolveColors: resolveViews } : {}),
93
+ ...(this._ownedDepth ? { depthStencil: this._ownedDepth.createView(), depthStencilTexture: this._ownedDepth } : {}),
94
+ width: size.width,
95
+ height: size.height,
96
+ };
97
+ }
98
+
99
+ private reallocate(size: FramebufferSize): void {
100
+ // Destroy old.
101
+ for (const t of this._ownedColors.values()) t.destroy();
102
+ this._ownedColors.clear();
103
+ for (const t of this._ownedResolves.values()) t.destroy();
104
+ this._ownedResolves.clear();
105
+ if (this._ownedDepth !== undefined) {
106
+ this._ownedDepth.destroy();
107
+ this._ownedDepth = undefined;
108
+ }
109
+ const sampleCount = this.signature.sampleCount;
110
+ const msaa = sampleCount > 1;
111
+ // Multisample textures are not directly sampleable; they only
112
+ // serve as render attachments, with a separate single-sample
113
+ // resolve target carrying TEXTURE_BINDING.
114
+ const colorUsage = msaa
115
+ ? TextureUsage.RENDER_ATTACHMENT
116
+ : TextureUsage.RENDER_ATTACHMENT
117
+ | TextureUsage.TEXTURE_BINDING
118
+ | (this.opts.extraUsage ?? 0);
119
+ const resolveUsage = TextureUsage.RENDER_ATTACHMENT
120
+ | TextureUsage.TEXTURE_BINDING
121
+ | (this.opts.extraUsage ?? 0);
122
+ const labelPrefix = this.opts.labelPrefix ?? "fbo";
123
+
124
+ for (const [name, format] of this.signature.colors) {
125
+ const tex = this.device.createTexture({
126
+ size: { width: size.width, height: size.height, depthOrArrayLayers: 1 },
127
+ format,
128
+ usage: colorUsage,
129
+ sampleCount,
130
+ label: msaa ? `${labelPrefix}.${name}.msaa` : `${labelPrefix}.${name}`,
131
+ });
132
+ this._ownedColors.set(name, tex);
133
+ if (msaa) {
134
+ const resolve = this.device.createTexture({
135
+ size: { width: size.width, height: size.height, depthOrArrayLayers: 1 },
136
+ format,
137
+ usage: resolveUsage,
138
+ sampleCount: 1,
139
+ label: `${labelPrefix}.${name}.resolve`,
140
+ });
141
+ this._ownedResolves.set(name, resolve);
142
+ }
143
+ }
144
+ if (this.signature.depthStencil !== undefined) {
145
+ this._ownedDepth = this.device.createTexture({
146
+ size: { width: size.width, height: size.height, depthOrArrayLayers: 1 },
147
+ format: this.signature.depthStencil.format,
148
+ usage: TextureUsage.RENDER_ATTACHMENT,
149
+ sampleCount,
150
+ label: `${labelPrefix}.depthStencil`,
151
+ });
152
+ }
153
+ }
154
+ }
155
+
156
+ export function allocateFramebuffer(
157
+ device: GPUDevice,
158
+ signature: import("../core/index.js").FramebufferSignature,
159
+ size: aval<FramebufferSize>,
160
+ opts: AllocateFramebufferOptions = {},
161
+ ): AdaptiveResource<IFramebuffer> {
162
+ return new AdaptiveFramebuffer(device, signature, size, opts);
163
+ }
@@ -0,0 +1,36 @@
1
+ // Helpers for constructing a `FramebufferSignature` from plain
2
+ // records. Pure value construction — no GPU calls — but lives in
3
+ // the resources package because it pairs with `allocateFramebuffer`.
4
+
5
+ import { HashMap } from "@aardworx/wombat.adaptive";
6
+ import type {
7
+ DepthStencilAttachmentSignature,
8
+ FramebufferSignature,
9
+ } from "../core/index.js";
10
+
11
+ export interface FramebufferSignatureSpec {
12
+ readonly colors: Record<string, GPUTextureFormat>;
13
+ readonly depthStencil?: { readonly format: GPUTextureFormat };
14
+ readonly sampleCount?: number;
15
+ }
16
+
17
+ export function createFramebufferSignature(
18
+ spec: FramebufferSignatureSpec,
19
+ ): FramebufferSignature {
20
+ let colors = HashMap.empty<string, GPUTextureFormat>();
21
+ for (const [name, format] of Object.entries(spec.colors)) {
22
+ colors = colors.add(name, format);
23
+ }
24
+ const sig: FramebufferSignature = {
25
+ colors,
26
+ sampleCount: spec.sampleCount ?? 1,
27
+ ...(spec.depthStencil ? { depthStencil: depthStencilFor(spec.depthStencil.format) } : {}),
28
+ };
29
+ return sig;
30
+ }
31
+
32
+ function depthStencilFor(format: GPUTextureFormat): DepthStencilAttachmentSignature {
33
+ const hasDepth = /^depth/.test(format) || format === "depth24plus-stencil8" || format === "depth32float-stencil8";
34
+ const hasStencil = /stencil/.test(format);
35
+ return { format, hasDepth, hasStencil };
36
+ }
@@ -0,0 +1,82 @@
1
+ // Public API of @aardworx/wombat.rendering-resources.
2
+ //
3
+ // Layer 2 — resource-creator functions on `GPUDevice`. Each
4
+ // `prepare*` function lifts a user-provided `aval<I…>` source
5
+ // type into a ref-counted `AdaptiveResource<GPU…>` that:
6
+ // - Allocates the GPU handle on first compute / on host
7
+ // mismatch.
8
+ // - Uploads on host-side change, reuses the existing handle
9
+ // when shape-compatible.
10
+ // - Frees the handle on last `release()`.
11
+
12
+ export {
13
+ prepareAdaptiveBuffer,
14
+ type PrepareAdaptiveBufferOptions,
15
+ } from "./adaptiveBuffer.js";
16
+
17
+ export {
18
+ prepareAdaptiveTexture,
19
+ type PrepareAdaptiveTextureOptions,
20
+ } from "./adaptiveTexture.js";
21
+
22
+ export {
23
+ prepareAdaptiveSampler,
24
+ } from "./adaptiveSampler.js";
25
+
26
+ export {
27
+ prepareUniformBuffer,
28
+ type PrepareUniformBufferOptions,
29
+ } from "./uniformBuffer.js";
30
+
31
+ export {
32
+ compileRenderPipeline,
33
+ type CompileRenderPipelineDescription,
34
+ } from "./renderPipeline.js";
35
+
36
+ export {
37
+ installShaderDiagnostics,
38
+ type ShaderDiagnosticsOptions,
39
+ } from "./shaderDiagnostics.js";
40
+
41
+ export {
42
+ decodeLine,
43
+ decodePosition,
44
+ type DecodedPosition,
45
+ } from "./sourceMapDecoder.js";
46
+
47
+ export {
48
+ generateMips,
49
+ type GenerateMipsOptions,
50
+ } from "./mipGen.js";
51
+
52
+ export {
53
+ prepareRenderObject,
54
+ PreparedRenderObject,
55
+ type PrepareRenderObjectOptions,
56
+ } from "./preparedRenderObject.js";
57
+
58
+ export {
59
+ prepareComputeShader,
60
+ PreparedComputeShader,
61
+ ComputeInputBinding,
62
+ type PrepareComputeShaderOptions,
63
+ type DispatchSize,
64
+ } from "./preparedComputeShader.js";
65
+
66
+ export {
67
+ createFramebufferSignature,
68
+ type FramebufferSignatureSpec,
69
+ } from "./framebufferSignature.js";
70
+
71
+ export {
72
+ allocateFramebuffer,
73
+ type AllocateFramebufferOptions,
74
+ type FramebufferSize,
75
+ } from "./framebuffer.js";
76
+
77
+ export {
78
+ BufferUsage,
79
+ ColorWrite,
80
+ ShaderStage,
81
+ TextureUsage,
82
+ } from "./webgpuFlags.js";
@@ -0,0 +1,148 @@
1
+ // Mip-map generation via compute pass.
2
+ //
3
+ // Standard 2× downsample shader: each invocation reads a 2×2 block
4
+ // of source-mip texels and writes the average to the destination
5
+ // mip. Iterated mip(N) → mip(N+1) for each pair of adjacent levels.
6
+ //
7
+ // `generateMips(device, encoder, texture)` records the dispatches
8
+ // into the encoder; the user is responsible for submission.
9
+ // Pipelines + bind groups are cached per (device, format) so
10
+ // repeat calls share the GPU work.
11
+
12
+ const WGSL_MIP_GEN_TEMPLATE = (storageFormat: string) => `
13
+ @group(0) @binding(0) var src: texture_2d<f32>;
14
+ @group(0) @binding(1) var dst: texture_storage_2d<${storageFormat}, write>;
15
+
16
+ @compute @workgroup_size(8, 8, 1)
17
+ fn main(@builtin(global_invocation_id) id: vec3<u32>) {
18
+ let dstSize: vec2<u32> = textureDimensions(dst);
19
+ if (id.x >= dstSize.x || id.y >= dstSize.y) { return; }
20
+ let srcSize: vec2<u32> = textureDimensions(src);
21
+ let i: vec2<i32> = vec2<i32>(i32(id.x) * 2, i32(id.y) * 2);
22
+ let i2: vec2<i32> = vec2<i32>(min(i.x + 1, i32(srcSize.x) - 1), min(i.y + 1, i32(srcSize.y) - 1));
23
+ let a: vec4<f32> = textureLoad(src, vec2<i32>(i.x, i.y), 0);
24
+ let b: vec4<f32> = textureLoad(src, vec2<i32>(i2.x, i.y), 0);
25
+ let c: vec4<f32> = textureLoad(src, vec2<i32>(i.x, i2.y), 0);
26
+ let d: vec4<f32> = textureLoad(src, vec2<i32>(i2.x, i2.y), 0);
27
+ textureStore(dst, vec2<i32>(i32(id.x), i32(id.y)), (a + b + c + d) * 0.25);
28
+ }
29
+ `;
30
+
31
+ interface MipPipelineCache {
32
+ pipelines: Map<GPUTextureFormat, { pipeline: GPUComputePipeline; layout: GPUBindGroupLayout }>;
33
+ }
34
+
35
+ const caches = new WeakMap<GPUDevice, MipPipelineCache>();
36
+
37
+ function pipelineFor(device: GPUDevice, format: GPUTextureFormat): { pipeline: GPUComputePipeline; layout: GPUBindGroupLayout } {
38
+ let cache = caches.get(device);
39
+ if (cache === undefined) {
40
+ cache = { pipelines: new Map() };
41
+ caches.set(device, cache);
42
+ }
43
+ const existing = cache.pipelines.get(format);
44
+ if (existing !== undefined) return existing;
45
+
46
+ const storageFormat = storageFormatFor(format);
47
+ const module = device.createShaderModule({ code: WGSL_MIP_GEN_TEMPLATE(storageFormat), label: `mipgen-${format}` });
48
+ const layout = device.createBindGroupLayout({
49
+ entries: [
50
+ { binding: 0, visibility: 0x4, texture: { sampleType: "float" } },
51
+ { binding: 1, visibility: 0x4, storageTexture: { access: "write-only", format: storageFormat as GPUTextureFormat } },
52
+ ],
53
+ });
54
+ const pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: [layout] });
55
+ const pipeline = device.createComputePipeline({
56
+ layout: pipelineLayout,
57
+ compute: { module, entryPoint: "main" },
58
+ label: `mipgen-${format}`,
59
+ });
60
+ const entry = { pipeline, layout };
61
+ cache.pipelines.set(format, entry);
62
+ return entry;
63
+ }
64
+
65
+ /**
66
+ * WebGPU storage-texture formats are a constrained subset; we map
67
+ * common sampled formats to the closest writable storage format.
68
+ * For BGRA / sRGB / non-storage formats we fall back to
69
+ * `rgba8unorm` and accept the colour-space caveat (callers using
70
+ * sRGB textures should generate their mips in linear).
71
+ */
72
+ function storageFormatFor(format: GPUTextureFormat): GPUTextureFormat {
73
+ switch (format) {
74
+ case "rgba8unorm":
75
+ case "rgba8snorm":
76
+ case "rgba16float":
77
+ case "rgba32float":
78
+ case "r32float":
79
+ case "rg32float":
80
+ case "r32uint":
81
+ case "rg32uint":
82
+ case "rgba32uint":
83
+ return format;
84
+ default:
85
+ return "rgba8unorm";
86
+ }
87
+ }
88
+
89
+ export interface GenerateMipsOptions {
90
+ /** Generate mips starting from this base level. Default 0. */
91
+ readonly baseMipLevel?: number;
92
+ /** Generate up to this many additional mips beyond the base. Default: down to 1×1. */
93
+ readonly mipLevelCount?: number;
94
+ /** Optional debug label. */
95
+ readonly label?: string;
96
+ }
97
+
98
+ /**
99
+ * Records compute dispatches that downsample `texture`'s mip
100
+ * chain. The texture must have been created with
101
+ * `STORAGE_BINDING | TEXTURE_BINDING` and the appropriate
102
+ * `mipLevelCount`. Each level is computed from the level above
103
+ * via a 2×2 box filter.
104
+ */
105
+ export function generateMips(
106
+ device: GPUDevice,
107
+ encoder: GPUCommandEncoder,
108
+ texture: GPUTexture,
109
+ opts: GenerateMipsOptions = {},
110
+ ): void {
111
+ const baseLevel = opts.baseMipLevel ?? 0;
112
+ const totalMips = texture.mipLevelCount;
113
+ const lastLevel = opts.mipLevelCount !== undefined
114
+ ? Math.min(totalMips - 1, baseLevel + opts.mipLevelCount)
115
+ : totalMips - 1;
116
+ if (lastLevel <= baseLevel) return;
117
+
118
+ const { pipeline, layout } = pipelineFor(device, texture.format);
119
+ const pass = encoder.beginComputePass(opts.label !== undefined ? { label: opts.label } : {});
120
+ pass.setPipeline(pipeline);
121
+
122
+ for (let mip = baseLevel; mip < lastLevel; mip++) {
123
+ const dstWidth = Math.max(1, texture.width >> (mip + 1));
124
+ const dstHeight = Math.max(1, texture.height >> (mip + 1));
125
+ const srcView = texture.createView({
126
+ baseMipLevel: mip, mipLevelCount: 1,
127
+ baseArrayLayer: 0, arrayLayerCount: 1,
128
+ dimension: "2d",
129
+ });
130
+ const dstView = texture.createView({
131
+ baseMipLevel: mip + 1, mipLevelCount: 1,
132
+ baseArrayLayer: 0, arrayLayerCount: 1,
133
+ dimension: "2d",
134
+ });
135
+ const bg = device.createBindGroup({
136
+ layout,
137
+ entries: [
138
+ { binding: 0, resource: srcView },
139
+ { binding: 1, resource: dstView },
140
+ ],
141
+ });
142
+ pass.setBindGroup(0, bg);
143
+ const workX = Math.ceil(dstWidth / 8);
144
+ const workY = Math.ceil(dstHeight / 8);
145
+ pass.dispatchWorkgroups(workX, workY, 1);
146
+ }
147
+ pass.end();
148
+ }