@tinypoly/gpu-renderer 0.2.5 → 0.3.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 (101) hide show
  1. package/ARCHITECTURE.md +48 -10
  2. package/README.md +2 -0
  3. package/dist/accumulator.d.ts.map +1 -1
  4. package/dist/accumulator.js +1 -0
  5. package/dist/accumulator.js.map +1 -1
  6. package/dist/denoise.js +2 -2
  7. package/dist/denoise.js.map +1 -1
  8. package/dist/device.d.ts.map +1 -1
  9. package/dist/device.js +9 -0
  10. package/dist/device.js.map +1 -1
  11. package/dist/frameLayout.d.ts.map +1 -1
  12. package/dist/frameLayout.js +5 -0
  13. package/dist/frameLayout.js.map +1 -1
  14. package/dist/geometry.d.ts +48 -8
  15. package/dist/geometry.d.ts.map +1 -1
  16. package/dist/geometry.js +409 -134
  17. package/dist/geometry.js.map +1 -1
  18. package/dist/grass.d.ts.map +1 -1
  19. package/dist/grass.js +2 -1
  20. package/dist/grass.js.map +1 -1
  21. package/dist/imageResolve.js +2 -2
  22. package/dist/imageResolve.js.map +1 -1
  23. package/dist/index.d.ts +1 -0
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +1 -0
  26. package/dist/index.js.map +1 -1
  27. package/dist/instancing.d.ts +100 -0
  28. package/dist/instancing.d.ts.map +1 -0
  29. package/dist/instancing.js +286 -0
  30. package/dist/instancing.js.map +1 -0
  31. package/dist/lights.d.ts.map +1 -1
  32. package/dist/lights.js +8 -5
  33. package/dist/lights.js.map +1 -1
  34. package/dist/memoryProbe.d.ts +76 -0
  35. package/dist/memoryProbe.d.ts.map +1 -0
  36. package/dist/memoryProbe.js +197 -0
  37. package/dist/memoryProbe.js.map +1 -0
  38. package/dist/oidnPass.d.ts +6 -0
  39. package/dist/oidnPass.d.ts.map +1 -1
  40. package/dist/oidnPass.js +18 -1
  41. package/dist/oidnPass.js.map +1 -1
  42. package/dist/pathTracePass.d.ts +15 -2
  43. package/dist/pathTracePass.d.ts.map +1 -1
  44. package/dist/pathTracePass.js +38 -16
  45. package/dist/pathTracePass.js.map +1 -1
  46. package/dist/renderer.d.ts +13 -0
  47. package/dist/renderer.d.ts.map +1 -1
  48. package/dist/renderer.js +248 -41
  49. package/dist/renderer.js.map +1 -1
  50. package/dist/sceneUpload.d.ts +26 -8
  51. package/dist/sceneUpload.d.ts.map +1 -1
  52. package/dist/sceneUpload.js +97 -27
  53. package/dist/sceneUpload.js.map +1 -1
  54. package/dist/shaderModule.d.ts.map +1 -1
  55. package/dist/shaderModule.js +2 -1
  56. package/dist/shaderModule.js.map +1 -1
  57. package/dist/skyBake.d.ts.map +1 -1
  58. package/dist/skyBake.js +5 -3
  59. package/dist/skyBake.js.map +1 -1
  60. package/dist/texturePack.d.ts +43 -3
  61. package/dist/texturePack.d.ts.map +1 -1
  62. package/dist/texturePack.js +209 -37
  63. package/dist/texturePack.js.map +1 -1
  64. package/dist/wgsl/bvh.wgsl.d.ts +5 -1
  65. package/dist/wgsl/bvh.wgsl.d.ts.map +1 -1
  66. package/dist/wgsl/bvh.wgsl.js +105 -15
  67. package/dist/wgsl/bvh.wgsl.js.map +1 -1
  68. package/dist/wgsl/common.wgsl.d.ts.map +1 -1
  69. package/dist/wgsl/common.wgsl.js +92 -11
  70. package/dist/wgsl/common.wgsl.js.map +1 -1
  71. package/dist/wgsl/surface.wgsl.d.ts +1 -1
  72. package/dist/wgsl/surface.wgsl.d.ts.map +1 -1
  73. package/dist/wgsl/surface.wgsl.js +24 -14
  74. package/dist/wgsl/surface.wgsl.js.map +1 -1
  75. package/dist/wgsl/texture.wgsl.d.ts +1 -1
  76. package/dist/wgsl/texture.wgsl.d.ts.map +1 -1
  77. package/dist/wgsl/texture.wgsl.js +11 -0
  78. package/dist/wgsl/texture.wgsl.js.map +1 -1
  79. package/package.json +1 -1
  80. package/src/accumulator.ts +1 -0
  81. package/src/denoise.ts +2 -2
  82. package/src/device.ts +12 -0
  83. package/src/frameLayout.ts +5 -0
  84. package/src/geometry.ts +527 -133
  85. package/src/grass.ts +2 -1
  86. package/src/imageResolve.ts +2 -2
  87. package/src/index.ts +1 -0
  88. package/src/instancing.ts +415 -0
  89. package/src/lights.ts +9 -5
  90. package/src/memoryProbe.ts +275 -0
  91. package/src/oidnPass.ts +21 -1
  92. package/src/pathTracePass.ts +42 -18
  93. package/src/renderer.ts +308 -47
  94. package/src/sceneUpload.ts +139 -36
  95. package/src/shaderModule.ts +2 -1
  96. package/src/skyBake.ts +5 -3
  97. package/src/texturePack.ts +274 -43
  98. package/src/wgsl/bvh.wgsl.ts +105 -15
  99. package/src/wgsl/common.wgsl.ts +92 -11
  100. package/src/wgsl/surface.wgsl.ts +24 -14
  101. package/src/wgsl/texture.wgsl.ts +11 -0
@@ -0,0 +1,275 @@
1
+ import { SLOT_CHANNELS } from "./texturePack.js";
2
+
3
+ /**
4
+ * Memory diagnostics: what the renderer holds on the device, by label, and the page's JavaScript heap, logged at each
5
+ * step of a frame. Every buffer and texture the device creates is counted until it is destroyed or collected, so a
6
+ * step that leaves the device fuller than it found it shows up as a growing total between frames.
7
+ */
8
+
9
+ interface Allocation {
10
+ label: string;
11
+ bytes: number;
12
+ }
13
+
14
+ const live = new Map<number, Allocation>();
15
+ let nextId = 0;
16
+ let enabled = false;
17
+
18
+ // A resource dropped without destroy() still holds its memory until it is collected; then it leaves the count.
19
+ const collected = typeof FinalizationRegistry === "undefined"
20
+ ? null
21
+ : new FinalizationRegistry<number>(id => live.delete(id));
22
+
23
+ const MB = 1024 * 1024;
24
+
25
+ /** Turns the memory log on or off. It is off by default. */
26
+ export function setMemoryLogging(on: boolean) {
27
+ enabled = on;
28
+ }
29
+
30
+ /** Whether the memory log is on, so a caller can skip measuring what nobody will read. */
31
+ export function memoryLogging(): boolean {
32
+ return enabled;
33
+ }
34
+
35
+ function track<T extends GPUBuffer | GPUTexture>(resource: T, label: string, bytes: number): T {
36
+ const id = nextId++;
37
+ const destroy = resource.destroy.bind(resource);
38
+
39
+ live.set(id, { label, bytes });
40
+ collected?.register(resource, id);
41
+
42
+ resource.destroy = () => {
43
+ live.delete(id);
44
+ destroy();
45
+ };
46
+
47
+ return resource;
48
+ }
49
+
50
+ /** Bytes per texel of the formats the renderer and a canvas use; anything else counts as four. */
51
+ function texelBytes(format: GPUTextureFormat): number {
52
+ if (format.startsWith("rgba32")) return 16;
53
+ if (format.startsWith("rgba16") || format.startsWith("rg32")) return 8;
54
+ if (format.startsWith("r8") && !format.startsWith("r8g8")) return 1;
55
+
56
+ return 4;
57
+ }
58
+
59
+ /** Counts every buffer and texture the device creates from now on. */
60
+ export function trackDevice(device: GPUDevice) {
61
+ const createBuffer = device.createBuffer.bind(device);
62
+ const createTexture = device.createTexture.bind(device);
63
+
64
+ device.createBuffer = descriptor =>
65
+ track(createBuffer(descriptor), descriptor.label || "unlabeled", descriptor.size);
66
+
67
+ device.createTexture = descriptor => {
68
+ const size = descriptor.size as number[] | GPUExtent3DDict;
69
+
70
+ const [width, height = 1, layers = 1] = Array.isArray(size)
71
+ ? size
72
+ : [size.width, size.height ?? 1, size.depthOrArrayLayers ?? 1];
73
+
74
+ return track(createTexture(descriptor), descriptor.label || "texture",
75
+ width * height * layers * texelBytes(descriptor.format) * (descriptor.mipLevelCount ?? 1));
76
+ };
77
+ }
78
+
79
+ const mb = (bytes: number) => Math.round(bytes / MB * 10) / 10;
80
+
81
+ /** Bytes of every typed array and ArrayBuffer reachable from a value, each counted once, without crossing into DOM. */
82
+ export function retainedBytes(value: unknown): number {
83
+ const seen = new Set<unknown>();
84
+ const buffers = new Set<ArrayBufferLike>();
85
+ const stack = [value];
86
+ let plain = 0;
87
+
88
+ while (stack.length) {
89
+ const item = stack.pop();
90
+
91
+ if (item === null || typeof item !== "object" || seen.has(item)) {
92
+ if (typeof item === "string") plain += item.length * 2;
93
+
94
+ continue;
95
+ }
96
+
97
+ seen.add(item);
98
+
99
+ if (ArrayBuffer.isView(item)) {
100
+ buffers.add(item.buffer);
101
+ continue;
102
+ }
103
+
104
+ if (item instanceof ArrayBuffer) {
105
+ buffers.add(item);
106
+ continue;
107
+ }
108
+
109
+ if (Array.isArray(item)) {
110
+ // A plain array of numbers costs eight bytes an element, which is how a large one goes unnoticed.
111
+ plain += item.length * 8;
112
+ for (const element of item) if (typeof element === "object" || typeof element === "string") stack.push(element);
113
+ continue;
114
+ }
115
+
116
+ if (typeof (item as { nodeType?: unknown }).nodeType === "number") continue;
117
+
118
+ for (const key of Object.keys(item)) stack.push((item as Record<string, unknown>)[key]);
119
+ }
120
+
121
+ let typed = 0;
122
+
123
+ for (const buffer of buffers) typed += buffer.byteLength;
124
+
125
+ return typed + plain;
126
+ }
127
+
128
+ /** What the device holds, in MB, with the largest labels first. */
129
+ export function deviceMemory(): { totalMB: number; byLabel: Record<string, number> } {
130
+ const byLabel = new Map<string, number>();
131
+ let total = 0;
132
+
133
+ for (const { label, bytes } of live.values()) {
134
+ total += bytes;
135
+ byLabel.set(label, (byLabel.get(label) ?? 0) + bytes);
136
+ }
137
+
138
+ const sorted = [...byLabel].sort((a, b) => b[1] - a[1]).slice(0, 12);
139
+
140
+ return { totalMB: mb(total), byLabel: Object.fromEntries(sorted.map(([label, bytes]) => [label, mb(bytes)])) };
141
+ }
142
+
143
+ /** The page's JavaScript heap, where the browser tells (Chrome and Edge only). */
144
+ function heap(): { usedMB: number; totalMB: number; limitMB: number } | null {
145
+ const memory = (performance as { memory?: { usedJSHeapSize: number; totalJSHeapSize: number;
146
+ jsHeapSizeLimit: number } }).memory;
147
+
148
+ return memory
149
+ ? { usedMB: mb(memory.usedJSHeapSize), totalMB: mb(memory.totalJSHeapSize), limitMB: mb(memory.jsHeapSizeLimit) }
150
+ : null;
151
+ }
152
+
153
+ /** Logs one step: the device's total and largest labels, the JavaScript heap, and whatever sizes the step adds. */
154
+ export function logMemory(step: string, details: Record<string, unknown> = {}) {
155
+ if (!enabled) return;
156
+
157
+ const device = deviceMemory();
158
+ const js = heap();
159
+
160
+ // As text, not as an object: a console that folds objects loses them when it is copied.
161
+ const text = Object.entries({ ...details, device: device.byLabel })
162
+ .map(([key, value]) => `\n ${key}: ${typeof value === "object" ? JSON.stringify(value) : String(value)}`)
163
+ .join("");
164
+
165
+ // eslint-disable-next-line no-console -- the log is the point of this module
166
+ console.info(`[gpu-renderer] ${step}: device ${device.totalMB} MB`
167
+ + (js ? `, JS heap ${js.usedMB}/${js.limitMB} MB` : "") + text);
168
+ }
169
+
170
+ interface BreakdownMesh {
171
+ primitive?: string;
172
+ attributes: Record<string, { data: ArrayLike<number> } | undefined>;
173
+ index: ArrayLike<number> | null;
174
+ materials: ({ name?: string; type?: string; values?: Record<string, unknown> } | null | undefined)[];
175
+ }
176
+
177
+ /**
178
+ * Where a snapshot's triangles come from: plain meshes, wireframe materials, or lines, points and sprites, which the
179
+ * packer turns into triangles facing the camera (a line segment becomes two). Lines and wireframes that are not
180
+ * meant to be rendered are the usual reason a scene packs far more than it shows. Also the largest meshes, by the
181
+ * triangles each one packs.
182
+ */
183
+ export function sceneBreakdown(meshes: readonly BreakdownMesh[]) {
184
+ const kinds: Record<string, { meshes: number; packedTriangles: number }> = {};
185
+ const largest: { kind: string; material: string; packedTriangles: number; vertices: number }[] = [];
186
+
187
+ for (const mesh of meshes) {
188
+ const vertices = (mesh.attributes.position?.data.length ?? 0) / 3;
189
+ const count = mesh.index?.length ?? vertices;
190
+ const material = mesh.materials[0];
191
+ const wireframe = !mesh.primitive && mesh.materials.some(entry => entry?.values?.wireframe === true);
192
+ const kind = mesh.primitive ?? (wireframe ? "wireframe" : "mesh");
193
+
194
+ // Two triangles a segment or a point; a strip has one segment fewer than vertices.
195
+ const packed = kind === "lineSegments" ? count : kind === "line" || kind === "lineLoop" ? 2 * count
196
+ : kind === "points" ? 2 * count : count / 3;
197
+
198
+ kinds[kind] ??= { meshes: 0, packedTriangles: 0 };
199
+ kinds[kind].meshes++;
200
+ kinds[kind].packedTriangles += Math.round(packed);
201
+ largest.push({ kind, material: material?.name || material?.type || "?", packedTriangles: Math.round(packed),
202
+ vertices });
203
+ }
204
+
205
+ largest.sort((a, b) => b.packedTriangles - a.packedTriangles);
206
+
207
+ return { kinds, largest: largest.slice(0, 10) };
208
+ }
209
+
210
+ interface UsageTexture {
211
+ id: string;
212
+ width: number;
213
+ height: number;
214
+ data: ArrayLike<number>;
215
+ }
216
+
217
+ interface UsageMaterial {
218
+ maps?: Record<string, string>;
219
+ }
220
+
221
+ /**
222
+ * What each texture costs and how much of it is read: the slots that use it, the channels those slots read, and the
223
+ * bytes a texel would take if only those channels were kept (one, two or four bytes; float textures keep their four
224
+ * floats). The total says what keeping only the channels read would save.
225
+ */
226
+ export function textureUsage(
227
+ textures: readonly UsageTexture[], materials: readonly (UsageMaterial | null | undefined)[]) {
228
+ const slots = new Map<string, Set<string>>();
229
+
230
+ for (const material of new Set(materials)) {
231
+ for (const [slot, id] of Object.entries(material?.maps ?? {})) {
232
+ if (typeof id !== "string") continue;
233
+
234
+ let known = slots.get(id);
235
+
236
+ if (!known) slots.set(id, known = new Set());
237
+
238
+ known.add(slot);
239
+ }
240
+ }
241
+
242
+ let nowBytes = 0, packedBytes = 0;
243
+
244
+ const rows = textures.map(texture => {
245
+ const used = [...slots.get(texture.id) ?? []];
246
+ // A normal map's third component could be rebuilt from the other two, so it counts as two here.
247
+ const channels = new Set(used.flatMap(slot => [...(slot.endsWith("ormalMap") ? "rg" : SLOT_CHANNELS[slot] ?? "rgba")]));
248
+ const float = texture.data instanceof Float32Array;
249
+ const texels = texture.width * texture.height;
250
+ const now = texels * (float ? 16 : 4);
251
+ // Three channels do not pack into fewer than four bytes on a word boundary; one or two do.
252
+ const kept = float ? 16 : channels.size === 0 ? 0 : channels.size <= 2 ? channels.size : 4;
253
+ const packed = texels * kept;
254
+
255
+ nowBytes += now;
256
+ packedBytes += packed;
257
+
258
+ return {
259
+ id: texture.id.length > 12 ? `${texture.id.slice(0, 12)}…` : texture.id,
260
+ fullId: texture.id,
261
+ size: `${texture.width}x${texture.height}`,
262
+ MB: mb(now),
263
+ slots: used.join("+") || "unused",
264
+ channels: [...channels].sort().join("") || "-",
265
+ packedMB: mb(packed),
266
+ };
267
+ });
268
+
269
+ rows.sort((a, b) => b.MB - a.MB);
270
+
271
+ return { totalMB: mb(nowBytes), packedMB: mb(packedBytes), textures: rows };
272
+ }
273
+
274
+ /** Bytes as MB, for the details of a step. */
275
+ export const toMB = mb;
package/src/oidnPass.ts CHANGED
@@ -192,7 +192,7 @@ export class OidnPass {
192
192
  const usage = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST;
193
193
 
194
194
  // Colour, albedo and normal for the network, then the result.
195
- this.buffers = Array.from({ length: 4 }, () => this.device.createBuffer({ size, usage }));
195
+ this.buffers = Array.from({ length: 4 }, () => this.device.createBuffer({ label: "oidn", size, usage }));
196
196
  this.output = this.buffers[3];
197
197
  this.width = width;
198
198
  this.height = height;
@@ -275,6 +275,26 @@ export class OidnPass {
275
275
  return true;
276
276
  }
277
277
 
278
+ /**
279
+ * Lets go of everything sized by an image: the four buffers here and the network's own, its activations and its
280
+ * input and output passes, which together hold a few hundred megabytes for a large image. The network is loaded
281
+ * again by the next run. Call it only once no run is in flight.
282
+ */
283
+ async release() {
284
+ for (const buffer of this.buffers) buffer.destroy();
285
+
286
+ this.buffers = [];
287
+ this.output = null;
288
+ this.width = 0;
289
+ this.height = 0;
290
+
291
+ const byUrl = networks.get(this.device);
292
+
293
+ networks.delete(this.device);
294
+
295
+ for (const unet of byUrl?.values() ?? []) await unet.then(loaded => loaded.dispose(), () => undefined);
296
+ }
297
+
278
298
  destroy() {
279
299
  for (const buffer of [...this.buffers, this.params, this.scale]) buffer.destroy();
280
300
 
@@ -26,28 +26,38 @@ export type FrameUniforms = Record<keyof typeof FRAME_LAYOUT.offsets & string, n
26
26
  */
27
27
  export function pathTraceSource(
28
28
  programs: ShaderPrograms | null = null, subsurface = true, volumes = true, bevel = true,
29
- portals = true, grass = true): string {
29
+ portals = true, grass = true, instances = true): string {
30
30
  // The random walk inside materials and the media inside volumes are compiled in only for a scene that has them: the
31
31
  // kernel is bound by its size, and either would cost every other scene a few percent for code that never runs.
32
32
  return `const SUBSURFACE_WALK: bool = ${subsurface};\nconst VOLUMES: bool = ${volumes};\nconst BEVEL: bool = ${bevel};\n`
33
- + `const PORTALS: bool = ${portals};\nconst GRASS: bool = ${grass};\n` + COMMON_WGSL + RANDOM_WGSL + BVH_WGSL
33
+ + `const PORTALS: bool = ${portals};\nconst GRASS: bool = ${grass};\n` + traversalConstants(instances) + COMMON_WGSL + RANDOM_WGSL + BVH_WGSL
34
34
  + SAMPLING_WGSL + TEXTURE_WGSL + SHADER_SUPPORT_WGSL + programsWgsl(programs) + ENVIRONMENT_WGSL + SURFACE_WGSL
35
35
  + (grass ? GRASS_WGSL : GRASS_STUB_WGSL) + BSDF_WGSL + LIGHTS_WGSL + PATH_TRACE_WGSL;
36
36
  }
37
37
 
38
+ /**
39
+ * Whether the traversal can enter instances, and the stack that takes: an instance stacks its own hierarchy on top of
40
+ * the scene's and the tree over the instances, so the stack doubles. A scene with no repeated geometry keeps the
41
+ * smaller one, which is what the kernel's registers are measured against.
42
+ */
43
+ export function traversalConstants(instances: boolean): string {
44
+ return `const INSTANCES: bool = ${instances};\nconst BVH_STACK: u32 = ${instances ? 64 : 32}u;\n`;
45
+ }
46
+
38
47
  const FRAME_BYTES = FRAME_WORDS * 4;
39
48
 
40
49
  const WORKGROUP = 8;
41
50
 
42
51
  export class PathTracePass {
43
- private pipeline: GPUComputePipeline;
52
+ private pipeline: GPUComputePipeline | null = null;
53
+ /** The kernel being compiled for `programKey`, which every caller asking for the same set waits on. */
54
+ private building: Promise<GPUComputePipeline> | null = null;
44
55
  private readonly uniforms: GPUBuffer;
45
56
  private readonly values = new ArrayBuffer(FRAME_BYTES);
46
57
  private bindGroup: GPUBindGroup | null = null;
47
- private programKey = "|false|false|false|false|false";
58
+ private programKey: string | null = null;
48
59
 
49
60
  constructor(private readonly device: GPUDevice) {
50
- this.pipeline = this.build(null, false, false, false, false, false);
51
61
  this.uniforms = device.createBuffer({ size: FRAME_BYTES, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
52
62
  }
53
63
 
@@ -55,27 +65,41 @@ export class PathTracePass {
55
65
  * Recompiles the kernel when the scene's shaders changed, or whether it has a material that scatters light inside;
56
66
  * the same set keeps the pipeline it has.
57
67
  */
58
- setPrograms(programs: ShaderPrograms | null, subsurface = false, volumes = false, bevel = false,
59
- portals = false, grass = false) {
60
- const key = `${programs?.key ?? ""}|${subsurface}|${volumes}|${bevel}|${portals}|${grass}`;
61
-
62
- if (key === this.programKey) return;
63
-
64
- this.programKey = key;
65
- this.pipeline = this.build(programs, subsurface, volumes, bevel, portals, grass);
66
- this.bindGroup = null;
68
+ async setPrograms(programs: ShaderPrograms | null, subsurface = false, volumes = false, bevel = false,
69
+ portals = false, grass = false, instances = false): Promise<void> {
70
+ const key = `${programs?.key ?? ""}|${subsurface}|${volumes}|${bevel}|${portals}|${grass}|${instances}`;
71
+
72
+ if (key !== this.programKey) {
73
+ this.programKey = key;
74
+ this.pipeline = null;
75
+ this.bindGroup = null;
76
+ this.building = this.build(programs, subsurface, volumes, bevel, portals, grass, instances);
77
+ }
78
+
79
+ const building = this.building as Promise<GPUComputePipeline>;
80
+ const pipeline = await building;
81
+
82
+ // A newer set may have started compiling meanwhile; this one is then already stale.
83
+ if (this.building === building) this.pipeline = pipeline;
67
84
  }
68
85
 
86
+ /**
87
+ * The kernel is large, and on Windows the browser compiles it through the D3D shader compiler, which can take many
88
+ * seconds. Compiled synchronously that blocks the GPU process long enough for its watchdog to kill it, and the device
89
+ * is lost before the first sample; the async path compiles off that thread.
90
+ */
69
91
  private build(programs: ShaderPrograms | null, subsurface: boolean, volumes: boolean,
70
- bevel: boolean, portals: boolean, grass: boolean): GPUComputePipeline {
92
+ bevel: boolean, portals: boolean, grass: boolean, instances: boolean): Promise<GPUComputePipeline> {
71
93
  const module = createShaderModule(this.device, "path trace",
72
- pathTraceSource(programs, subsurface, volumes, bevel, portals, grass));
94
+ pathTraceSource(programs, subsurface, volumes, bevel, portals, grass, instances));
73
95
 
74
- return this.device.createComputePipeline({ layout: "auto", compute: { module, entryPoint: "main" } });
96
+ return this.device.createComputePipelineAsync({ label: "path trace", layout: "auto", compute: { module, entryPoint: "main" } });
75
97
  }
76
98
 
77
99
  /** Rebinds after the scene or the accumulation buffer was replaced. */
78
100
  setBindings(scene: SceneBuffers, accumulation: GPUBuffer) {
101
+ if (!this.pipeline) throw new Error("GPU renderer: the path tracing kernel is not compiled yet.");
102
+
79
103
  this.bindGroup = this.device.createBindGroup({
80
104
  layout: this.pipeline.getBindGroupLayout(0),
81
105
  entries: [
@@ -93,7 +117,7 @@ export class PathTracePass {
93
117
  }
94
118
 
95
119
  encode(encoder: GPUCommandEncoder, frame: FrameUniforms) {
96
- if (!this.bindGroup) throw new Error("GPU renderer: the path tracing pass has no scene bound.");
120
+ if (!this.bindGroup || !this.pipeline) throw new Error("GPU renderer: the path tracing pass has no scene bound.");
97
121
 
98
122
  new Uint32Array(this.values).fill(0);
99
123
  writeStruct(FRAME_LAYOUT, this.values, 0, frame);