@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.
- package/package.json +67 -0
- package/src/commands/clear.ts +66 -0
- package/src/commands/index.ts +11 -0
- package/src/commands/render.ts +106 -0
- package/src/core/acquire.ts +22 -0
- package/src/core/adaptiveResource.ts +88 -0
- package/src/core/buffer.ts +45 -0
- package/src/core/bufferView.ts +26 -0
- package/src/core/clear.ts +16 -0
- package/src/core/command.ts +50 -0
- package/src/core/drawCall.ts +22 -0
- package/src/core/framebuffer.ts +37 -0
- package/src/core/framebufferSignature.ts +19 -0
- package/src/core/index.ts +116 -0
- package/src/core/pipelineState.ts +64 -0
- package/src/core/renderContext.ts +48 -0
- package/src/core/renderObject.ts +57 -0
- package/src/core/renderTask.ts +17 -0
- package/src/core/renderTree.ts +29 -0
- package/src/core/sampler.ts +12 -0
- package/src/core/shader.ts +28 -0
- package/src/core/texture.ts +63 -0
- package/src/index.ts +18 -0
- package/src/resources/adaptiveBuffer.ts +135 -0
- package/src/resources/adaptiveSampler.ts +82 -0
- package/src/resources/adaptiveTexture.ts +269 -0
- package/src/resources/framebuffer.ts +163 -0
- package/src/resources/framebufferSignature.ts +36 -0
- package/src/resources/index.ts +82 -0
- package/src/resources/mipGen.ts +148 -0
- package/src/resources/preparedComputeShader.ts +426 -0
- package/src/resources/preparedRenderObject.ts +498 -0
- package/src/resources/renderPipeline.ts +148 -0
- package/src/resources/shaderDiagnostics.ts +52 -0
- package/src/resources/sourceMapDecoder.ts +133 -0
- package/src/resources/uniformBuffer.ts +170 -0
- package/src/resources/webgpuFlags.ts +43 -0
- package/src/runtime/copy.ts +13 -0
- package/src/runtime/index.ts +24 -0
- package/src/runtime/renderTask.ts +137 -0
- package/src/runtime/renderTo.ts +154 -0
- package/src/runtime/runtime.ts +113 -0
- package/src/runtime/scenePass.ts +273 -0
- package/src/window/canvas.ts +252 -0
- package/src/window/index.ts +14 -0
- 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
|
+
}
|