pelican-engine 0.0.0-stage → 0.3.1

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 (192) hide show
  1. package/AGENTS.md +147 -0
  2. package/LICENSE +22 -0
  3. package/README.md +129 -2
  4. package/distribution.json +197 -0
  5. package/docs/AI-WORKFLOW.md +517 -0
  6. package/docs/ARCHITECTURE.md +268 -0
  7. package/docs/CAPABILITIES.md +264 -0
  8. package/docs/DECISIONS.md +107 -0
  9. package/docs/M3-USAGE.md +192 -0
  10. package/docs/PELICAN-MIGRATION.md +38 -0
  11. package/docs/RENDER-BACKENDS.md +50 -0
  12. package/docs/ROADMAP.md +239 -0
  13. package/docs/THIRD-PARTY-NOTICES.md +151 -0
  14. package/docs/api/REFERENCE.md +1435 -0
  15. package/docs/api/index.json +1139 -0
  16. package/docs/npm/DISTRIBUTION.md +130 -0
  17. package/docs/npm/GAME-AUTHOR.md +147 -0
  18. package/engine.manifest.json +15 -0
  19. package/lib/assets/audio.js +67 -0
  20. package/lib/assets/errors.js +36 -0
  21. package/lib/assets/glb.js +277 -0
  22. package/lib/assets/index.js +53 -0
  23. package/lib/assets/jpeg.js +199 -0
  24. package/lib/assets/model.js +558 -0
  25. package/lib/assets/optimize.js +95 -0
  26. package/lib/assets/texture.js +187 -0
  27. package/lib/compiler/distribution.js +17 -0
  28. package/lib/compiler/extract.js +425 -0
  29. package/lib/compiler/graph.js +366 -0
  30. package/lib/compiler/index.js +574 -0
  31. package/lib/compiler/normalize.js +1194 -0
  32. package/lib/compiler/source-map.js +25 -0
  33. package/lib/compiler/types.js +2 -0
  34. package/lib/compiler/version.js +93 -0
  35. package/lib/experiments/simulation/index.js +2 -0
  36. package/lib/experiments/simulation/math.js +41 -0
  37. package/lib/experiments/simulation/probe.js +215 -0
  38. package/lib/experiments/simulation/scenes.js +192 -0
  39. package/lib/experiments/simulation/solver.js +346 -0
  40. package/lib/experiments/simulation/types.js +1 -0
  41. package/lib/experiments/world/adapters.js +30 -0
  42. package/lib/experiments/world/geometry.js +180 -0
  43. package/lib/experiments/world/index.js +2 -0
  44. package/lib/experiments/world/probe.js +288 -0
  45. package/lib/experiments/world/types.js +1 -0
  46. package/lib/experiments/world/world.js +599 -0
  47. package/lib/navigation/index.js +157 -0
  48. package/lib/physics/index.js +909 -0
  49. package/lib/protocol/index.js +92 -0
  50. package/lib/renderer/acceleration.js +609 -0
  51. package/lib/renderer/browser.js +2015 -0
  52. package/lib/renderer/bsdf.js +260 -0
  53. package/lib/renderer/checkpoint.js +88 -0
  54. package/lib/renderer/compute-executor.js +1034 -0
  55. package/lib/renderer/compute-texture-assets.js +49 -0
  56. package/lib/renderer/compute-view-contract.js +1 -0
  57. package/lib/renderer/denoiser.js +361 -0
  58. package/lib/renderer/dielectric.js +360 -0
  59. package/lib/renderer/embedded-image-pixels.js +368 -0
  60. package/lib/renderer/emissive-lights.js +67 -0
  61. package/lib/renderer/environment-importance.js +55 -0
  62. package/lib/renderer/environment.js +346 -0
  63. package/lib/renderer/gameplay.js +464 -0
  64. package/lib/renderer/geometry.js +103 -0
  65. package/lib/renderer/graph-expressions.js +485 -0
  66. package/lib/renderer/graph-tsl.js +141 -0
  67. package/lib/renderer/graph.js +214 -0
  68. package/lib/renderer/managed-texture-sampling.js +29 -0
  69. package/lib/renderer/material-graphs.js +297 -0
  70. package/lib/renderer/medium.js +348 -0
  71. package/lib/renderer/models.js +1053 -0
  72. package/lib/renderer/motion-shader.js +40 -0
  73. package/lib/renderer/path-shader.js +196 -0
  74. package/lib/renderer/pathtrace.js +324 -0
  75. package/lib/renderer/postprocess-contract.js +1 -0
  76. package/lib/renderer/postprocess.js +494 -0
  77. package/lib/renderer/render-buffers.js +132 -0
  78. package/lib/renderer/render-textures.js +155 -0
  79. package/lib/renderer/startup.js +50 -0
  80. package/lib/renderer/surface-capture.js +199 -0
  81. package/lib/renderer/surface-gpu.js +270 -0
  82. package/lib/renderer/surface-motion.js +84 -0
  83. package/lib/renderer/surface-ray-shader.js +91 -0
  84. package/lib/renderer/surface-shader.js +88 -0
  85. package/lib/renderer/surface-tracer.js +1720 -0
  86. package/lib/renderer/surface.js +1425 -0
  87. package/lib/renderer/temporal-filter.js +512 -0
  88. package/lib/renderer/texture-pixels.js +207 -0
  89. package/lib/renderer/traversal-nodes.js +54 -0
  90. package/lib/renderer/ui.js +446 -0
  91. package/lib/renderer/vertex-bounds.js +127 -0
  92. package/lib/renderer/vertex-contract.js +1 -0
  93. package/lib/renderer/vertex-graphs.js +197 -0
  94. package/lib/renderer/volume-shader.js +50 -0
  95. package/lib/runtime/compute-contract.js +1 -0
  96. package/lib/runtime/compute-service.js +674 -0
  97. package/lib/runtime/contracts.js +1 -0
  98. package/lib/runtime/error.js +37 -0
  99. package/lib/runtime/events.js +189 -0
  100. package/lib/runtime/game-services.js +772 -0
  101. package/lib/runtime/host.js +839 -0
  102. package/lib/runtime/material-parameters.js +145 -0
  103. package/lib/runtime/plugins.js +1 -0
  104. package/lib/runtime/postprocess-contract.js +1 -0
  105. package/lib/runtime/postprocess.js +189 -0
  106. package/lib/runtime/prefabs.js +13 -0
  107. package/lib/runtime/random.js +88 -0
  108. package/lib/runtime/render-buffer-contract.js +1 -0
  109. package/lib/runtime/render-buffers.js +243 -0
  110. package/lib/runtime/render-capabilities.js +17 -0
  111. package/lib/runtime/render-settings.js +249 -0
  112. package/lib/runtime/render-texture-contract.js +1 -0
  113. package/lib/runtime/render-textures.js +271 -0
  114. package/lib/runtime/resources.js +648 -0
  115. package/lib/runtime/scheduler.js +765 -0
  116. package/lib/runtime/storage.js +169 -0
  117. package/lib/runtime/world.js +594 -0
  118. package/lib/schema/index.js +24 -0
  119. package/lib/sdk/graph-values.js +16 -0
  120. package/lib/sdk/index.d.ts +800 -0
  121. package/lib/sdk/index.js +82 -0
  122. package/lib/tools/api-index.js +113 -0
  123. package/lib/tools/browser-session.js +89 -0
  124. package/lib/tools/cli.js +513 -0
  125. package/lib/tools/compress-dist.js +59 -0
  126. package/lib/tools/control-client.js +153 -0
  127. package/lib/tools/control-server.js +633 -0
  128. package/lib/tools/diagnostic-code.js +3 -0
  129. package/lib/tools/doctor.js +496 -0
  130. package/lib/tools/npm/browser-cli.js +6 -0
  131. package/lib/tools/npm/entry.js +40 -0
  132. package/lib/tools/npm/layout.js +195 -0
  133. package/lib/tools/setup.js +153 -0
  134. package/licenses/@dimforge/rapier2d-compat/LICENSE +201 -0
  135. package/licenses/@dimforge/rapier3d-compat/LICENSE +201 -0
  136. package/licenses/@gltf-transform/core/LICENSE.md +21 -0
  137. package/licenses/@gltf-transform/extensions/LICENSE.md +21 -0
  138. package/licenses/@gltf-transform/functions/LICENSE.md +21 -0
  139. package/licenses/@img/colour/LICENSE.md +82 -0
  140. package/licenses/@img/sharp-win32-x64/LICENSE +191 -0
  141. package/licenses/@oxc-project/types/LICENSE +22 -0
  142. package/licenses/@rolldown/pluginutils/LICENSE +21 -0
  143. package/licenses/@tweenjs/tween.js/LICENSE +23 -0
  144. package/licenses/@types/ndarray/LICENSE +21 -0
  145. package/licenses/@types/node/LICENSE +21 -0
  146. package/licenses/@types/stats.js/LICENSE +21 -0
  147. package/licenses/@types/three/@dimforge/rapier3d-compat/LICENSE +201 -0
  148. package/licenses/@types/three/LICENSE +21 -0
  149. package/licenses/@types/webxr/LICENSE +21 -0
  150. package/licenses/@webgpu/types/LICENSE +26 -0
  151. package/licenses/THIRD-PARTY-NOTICES.md +139 -0
  152. package/licenses/cwise-compiler/LICENSE +22 -0
  153. package/licenses/detect-libc/LICENSE +201 -0
  154. package/licenses/esbuild/LICENSE.md +21 -0
  155. package/licenses/fast-png/LICENSE +21 -0
  156. package/licenses/fdir/LICENSE +7 -0
  157. package/licenses/fflate/LICENSE +21 -0
  158. package/licenses/iobuffer/LICENSE +22 -0
  159. package/licenses/iota-array/LICENSE +22 -0
  160. package/licenses/is-buffer/LICENSE +21 -0
  161. package/licenses/ktx-parse/LICENSE +21 -0
  162. package/licenses/lightningcss/LICENSE +373 -0
  163. package/licenses/lightningcss-win32-x64-msvc/LICENSE +373 -0
  164. package/licenses/meshoptimizer/LICENSE.md +21 -0
  165. package/licenses/nanoid/LICENSE +20 -0
  166. package/licenses/ndarray/LICENSE +22 -0
  167. package/licenses/ndarray-lanczos/LICENSE +21 -0
  168. package/licenses/ndarray-ops/LICENSE +22 -0
  169. package/licenses/ndarray-pixels/LICENSE +21 -0
  170. package/licenses/ngraph.path/LICENSE +21 -0
  171. package/licenses/picocolors/LICENSE +15 -0
  172. package/licenses/picomatch/LICENSE +21 -0
  173. package/licenses/playwright/LICENSE +202 -0
  174. package/licenses/playwright/NOTICE +5 -0
  175. package/licenses/playwright-core/LICENSE +202 -0
  176. package/licenses/playwright-core/NOTICE +5 -0
  177. package/licenses/postcss/LICENSE +20 -0
  178. package/licenses/prettier/LICENSE +7 -0
  179. package/licenses/property-graph/LICENSE +21 -0
  180. package/licenses/rolldown/LICENSE +25 -0
  181. package/licenses/semver/LICENSE +15 -0
  182. package/licenses/sharp/LICENSE +191 -0
  183. package/licenses/source-map-js/LICENSE +28 -0
  184. package/licenses/three/LICENSE +21 -0
  185. package/licenses/tinyglobby/LICENSE +21 -0
  186. package/licenses/tsx/LICENSE +21 -0
  187. package/licenses/typescript/LICENSE.txt +55 -0
  188. package/licenses/undici-types/LICENSE +21 -0
  189. package/licenses/uniq/LICENSE +22 -0
  190. package/licenses/vite/LICENSE.md +2084 -0
  191. package/package.json +49 -4
  192. package/web/index.html +34 -0
@@ -0,0 +1,1720 @@
1
+ import * as THREE from "three/webgpu";
2
+ import { createSurfaceCapture } from "./surface-capture.js";
3
+ import { createSurfaceAcceleration, } from "./acceleration.js";
4
+ import { packSurfaceGeometry, packSurfaceResources, packSurfaceMaterialTable, } from "./surface-gpu.js";
5
+ import { SURFACE_SHADER } from "./surface-shader.js";
6
+ import { SURFACE_RAY_SHADER } from "./surface-ray-shader.js";
7
+ import { BSDF_SHADER } from "./bsdf.js";
8
+ import { DIELECTRIC_SHADER } from "./dielectric.js";
9
+ import { VOLUME_SHADER } from "./volume-shader.js";
10
+ import { PATH_SHADER, PRIMARY_SHADER, TRACE_PRESENT_SHADER, TRACE_LINEAR_PRESENT_SHADER, TRACE_PARAMETER_FLOATS, TRACE_PARAMETER_BYTES, } from "./path-shader.js";
11
+ import { createSurfaceDenoiser } from "./denoiser.js";
12
+ import { createSurfaceTextureCache, } from "./surface.js";
13
+ import { createMediumCatalog } from "./medium.js";
14
+ import { createEnvironmentCapture } from "./environment.js";
15
+ import { appendEmissiveLights } from "./emissive-lights.js";
16
+ import { buildEnvironmentImportance } from "./environment-importance.js";
17
+ import { packTraversalData } from "./traversal-nodes.js";
18
+ import { packPreviousSurfacePositions } from "./surface-motion.js";
19
+ import { MOTION_SHADER } from "./motion-shader.js";
20
+ import { compileSurfaceGraphs, DEFAULT_SURFACE_GRAPHS, graphEmissiveProposal, } from "./material-graphs.js";
21
+ import { withGraphGPUDeadline } from "./graph.js";
22
+ import { createTemporalFilter, } from "./temporal-filter.js";
23
+ const contextOwners = new WeakMap();
24
+ const storageMin = {
25
+ triangles: 288,
26
+ materials: 128,
27
+ textures: 96,
28
+ texels: 16,
29
+ nodes: 32,
30
+ lights: 48,
31
+ environmentImportance: 16,
32
+ pixels: 16,
33
+ average: 16,
34
+ normalDepth: 16,
35
+ motion: 16,
36
+ albedo: 16,
37
+ };
38
+ const FAULT_DEBUG_BYTES = (13 + 8 * 8) * 16;
39
+ function bytes(data) {
40
+ const copy = new Uint8Array(data.byteLength);
41
+ copy.set(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
42
+ return copy;
43
+ }
44
+ function equal(a, b) {
45
+ return (!!a &&
46
+ a.length === b.length &&
47
+ a.every((value, index) => value === b[index]));
48
+ }
49
+ function halton(index, base) {
50
+ let value = 0, fraction = 1;
51
+ while (index > 0) {
52
+ fraction /= base;
53
+ value += fraction * (index % base);
54
+ index = Math.floor(index / base);
55
+ }
56
+ return value - 0.5;
57
+ }
58
+ async function shader(device, code, label) {
59
+ const module = device.createShaderModule({ code, label });
60
+ const info = await module.getCompilationInfo();
61
+ const errors = info.messages.filter((message) => message.type === "error");
62
+ if (errors.length)
63
+ throw new Error("TC_TRACE_SHADER: " +
64
+ label +
65
+ ": " +
66
+ errors.map((message) => message.message).join("\n"));
67
+ return module;
68
+ }
69
+ /** Private, real WebGPU image renderer; opaque material path currently. */
70
+ export class SurfaceTracer {
71
+ device;
72
+ canvas;
73
+ width;
74
+ height;
75
+ rasterRenderer;
76
+ onFault;
77
+ captureFaults;
78
+ capture = createSurfaceCapture();
79
+ acceleration = createSurfaceAcceleration();
80
+ buffers = new Map();
81
+ frame;
82
+ textures = [];
83
+ materialData;
84
+ surfaceShader = SURFACE_SHADER;
85
+ graphRows = new Float32Array();
86
+ graphMaterials = 0;
87
+ graphParameterRows = 0;
88
+ fragmentBuffers = new Map();
89
+ fragmentGpuCopies = 0;
90
+ fragmentGpuCopyBytes = 0;
91
+ textureGpuCopies = 0;
92
+ textureGpuCopyBytes = 0;
93
+ materialGeneration = 0;
94
+ lightData;
95
+ lightingSignature = "";
96
+ uniform;
97
+ presentUniform;
98
+ primary;
99
+ depth;
100
+ context;
101
+ compute;
102
+ opaqueCompute;
103
+ volumeCompute;
104
+ computeModule;
105
+ motionCompute;
106
+ motionGroup;
107
+ motionActive = false;
108
+ geometryData;
109
+ capturedSurface;
110
+ previousSurface;
111
+ previousViewProjection = new THREE.Matrix4();
112
+ previousCameraOrigin = new THREE.Vector3();
113
+ previousCameraValid = false;
114
+ motionPasses = 0;
115
+ lastMotionComputed = false;
116
+ motionMatches = { matched: 0, missing: 0 };
117
+ temporal;
118
+ temporalOutput;
119
+ temporalHistoryReset = true;
120
+ temporalResetReasons = new Set(["initial"]);
121
+ lastTemporalReset = true;
122
+ lastTemporalProgressive = false;
123
+ lastTemporalReasons = [];
124
+ temporalResets = 0;
125
+ mediumCatalog;
126
+ mediumIds = new Map();
127
+ mediumFaces = [];
128
+ mediumSignature = "";
129
+ mediumRevision = 0;
130
+ initialMedia = [];
131
+ initialMediaKey = "";
132
+ lightBufferDirty = true;
133
+ mediumParams = [0, 0, 0, 0];
134
+ raster;
135
+ present;
136
+ linearPresent;
137
+ linearPresentPending;
138
+ denoiser;
139
+ computeGroup;
140
+ rasterGroup;
141
+ disposed = false;
142
+ lost = false;
143
+ reset = true;
144
+ serial = 0;
145
+ samples = 0;
146
+ resets = 0;
147
+ cameraKey = "";
148
+ settingsKey = "";
149
+ environment = [0, 0, 0];
150
+ uploadBytes = 0;
151
+ lastCaptureMs = 0;
152
+ lastUploadMs = 0;
153
+ lastSubmitMs = 0;
154
+ output;
155
+ contentGeneration = 0;
156
+ environmentTexture = null;
157
+ timestamps;
158
+ timingResolve;
159
+ timingStart = 2;
160
+ environmentCapture;
161
+ environmentCache = createSurfaceTextureCache();
162
+ environmentSource = null;
163
+ environmentRevision = "";
164
+ environmentImage;
165
+ environmentIndex = -1;
166
+ environmentProposal;
167
+ environmentIntensity = 1;
168
+ environmentRotation = new THREE.Matrix4();
169
+ environmentState = "";
170
+ emissiveCount = 0;
171
+ lightTechniques = 0;
172
+ mediumFaultCheck = Promise.resolve();
173
+ mediumFaultBusy = false;
174
+ mediumFault;
175
+ mediumFaults = 0;
176
+ mediumDebug;
177
+ faultTarget = -1;
178
+ faultSample = 0;
179
+ constructor(device, canvas, width, height, rasterRenderer, onFault, captureFaults = false) {
180
+ this.device = device;
181
+ this.canvas = canvas;
182
+ this.width = width;
183
+ this.height = height;
184
+ this.rasterRenderer = rasterRenderer;
185
+ this.onFault = onFault;
186
+ this.captureFaults = captureFaults;
187
+ if (!Number.isSafeInteger(width) ||
188
+ !Number.isSafeInteger(height) ||
189
+ width < 1 ||
190
+ height < 1 ||
191
+ width > device.limits.maxTextureDimension2D ||
192
+ height > device.limits.maxTextureDimension2D)
193
+ throw new Error("TC_TRACE_SIZE: Invalid device output dimensions.");
194
+ if (width * height * 64 + 16 + (captureFaults ? FAULT_DEBUG_BYTES : 0) >
195
+ device.limits.maxStorageBufferBindingSize)
196
+ throw new Error("TC_TRACE_PIXEL_BUDGET: Output storage exceeds the active adapter limit.");
197
+ canvas.width = width;
198
+ canvas.height = height;
199
+ const context = canvas.getContext("webgpu");
200
+ if (!context)
201
+ throw new Error("TC_GPU_CONTEXT");
202
+ this.context = context;
203
+ context.configure({
204
+ device,
205
+ format: navigator.gpu.getPreferredCanvasFormat(),
206
+ alphaMode: "opaque",
207
+ });
208
+ contextOwners.set(canvas, this);
209
+ this.uniform = device.createBuffer({
210
+ size: TRACE_PARAMETER_BYTES,
211
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
212
+ });
213
+ this.presentUniform = device.createBuffer({
214
+ size: 16,
215
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
216
+ });
217
+ this.primary = device.createTexture({
218
+ size: [width, height],
219
+ format: "rgba32float",
220
+ usage: GPUTextureUsage.RENDER_ATTACHMENT |
221
+ GPUTextureUsage.TEXTURE_BINDING |
222
+ GPUTextureUsage.COPY_SRC,
223
+ });
224
+ this.depth = device.createTexture({
225
+ size: [width, height],
226
+ format: "depth24plus",
227
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
228
+ });
229
+ if (device.features.has("timestamp-query")) {
230
+ this.timestamps = device.createQuerySet({ type: "timestamp", count: 6 });
231
+ this.timingResolve = device.createBuffer({
232
+ size: 256,
233
+ usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC,
234
+ });
235
+ }
236
+ if (rasterRenderer)
237
+ this.environmentCapture = createEnvironmentCapture(rasterRenderer);
238
+ this.ensure("pixels", width * height * 64 + 16 + (captureFaults ? FAULT_DEBUG_BYTES : 0));
239
+ for (const key of ["average", "normalDepth", "albedo"])
240
+ this.ensure(key, width * height * 16);
241
+ void device.lost.then(() => {
242
+ this.lost = true;
243
+ });
244
+ }
245
+ static async create(device, canvas, width, height, rasterRenderer, onFault, captureFaults = false, signal) {
246
+ if (signal?.aborted)
247
+ throw new Error("TC_TRACE_CANCELLED: Surface initialization was cancelled.");
248
+ const result = new SurfaceTracer(device, canvas, width, height, rasterRenderer, onFault, captureFaults);
249
+ const cancel = () => result.dispose();
250
+ signal?.addEventListener("abort", cancel, { once: true });
251
+ try {
252
+ await result.initialize();
253
+ result.active();
254
+ return result;
255
+ }
256
+ catch (error) {
257
+ result.dispose();
258
+ throw error;
259
+ }
260
+ finally {
261
+ signal?.removeEventListener("abort", cancel);
262
+ }
263
+ }
264
+ async initialize() {
265
+ await this.createSurfacePipelines(SURFACE_SHADER);
266
+ const presentModule = await shader(this.device, TRACE_PRESENT_SHADER, "surface radiance display");
267
+ this.active();
268
+ this.present = await this.device.createRenderPipelineAsync({
269
+ layout: "auto",
270
+ vertex: { module: presentModule, entryPoint: "vertex" },
271
+ fragment: {
272
+ module: presentModule,
273
+ entryPoint: "fragment",
274
+ targets: [{ format: navigator.gpu.getPreferredCanvasFormat() }],
275
+ },
276
+ primitive: { topology: "triangle-list" },
277
+ });
278
+ this.active();
279
+ const denoiser = await createSurfaceDenoiser(this.device);
280
+ if (this.disposed || this.lost) {
281
+ denoiser.dispose();
282
+ this.active();
283
+ }
284
+ this.denoiser = denoiser;
285
+ }
286
+ async createSurfacePipelines(surfaceShader) {
287
+ const generation = this.contentGeneration;
288
+ const pipelines = await withGraphGPUDeadline(this.device, "TC_TRACE_GRAPH", async (active) => {
289
+ active();
290
+ this.active();
291
+ const computeModule = await shader(this.device, surfaceShader +
292
+ SURFACE_RAY_SHADER +
293
+ BSDF_SHADER +
294
+ DIELECTRIC_SHADER +
295
+ VOLUME_SHADER +
296
+ PATH_SHADER, "surface path integrator");
297
+ active();
298
+ this.active();
299
+ const compute = await this.device.createComputePipelineAsync({
300
+ layout: "auto",
301
+ compute: {
302
+ module: computeModule,
303
+ entryPoint: "pathMain",
304
+ constants: {
305
+ traceVolumeEnabled: 0,
306
+ surfacePhysicalEnabled: 0,
307
+ surfaceOrderedTraversal: 0,
308
+ traceCaptureFault: this.captureFaults ? 1 : 0,
309
+ },
310
+ },
311
+ });
312
+ active();
313
+ this.active();
314
+ const primaryModule = await shader(this.device, surfaceShader + PRIMARY_SHADER, "raster surface visibility");
315
+ active();
316
+ this.active();
317
+ const raster = await this.device.createRenderPipelineAsync({
318
+ layout: "auto",
319
+ vertex: { module: primaryModule, entryPoint: "primaryVertex" },
320
+ fragment: {
321
+ module: primaryModule,
322
+ entryPoint: "primaryFragment",
323
+ targets: [{ format: "rgba32float" }],
324
+ },
325
+ primitive: { topology: "triangle-list", cullMode: "none" },
326
+ depthStencil: {
327
+ format: "depth24plus",
328
+ depthWriteEnabled: true,
329
+ depthCompare: "less",
330
+ },
331
+ });
332
+ active();
333
+ this.active();
334
+ return { compute, computeModule, raster };
335
+ });
336
+ this.active();
337
+ if (generation !== this.contentGeneration)
338
+ throw Error("TC_TRACE_CAPTURE_STALE: Content changed during graph pipeline setup.");
339
+ const { compute, computeModule, raster } = pipelines;
340
+ this.compute = compute;
341
+ this.opaqueCompute = compute;
342
+ this.computeModule = computeModule;
343
+ this.raster = raster;
344
+ this.volumeCompute = undefined;
345
+ this.motionCompute = undefined;
346
+ this.computeGroup = undefined;
347
+ this.rasterGroup = undefined;
348
+ this.motionGroup = undefined;
349
+ this.surfaceShader = surfaceShader;
350
+ }
351
+ active() {
352
+ if (this.disposed)
353
+ throw new Error("TC_TRACE_DISPOSED");
354
+ if (this.lost)
355
+ throw new Error("TC_GPU_DEVICE_LOST: Recreate renderer resources on a new device.");
356
+ if (this.mediumFault)
357
+ throw this.mediumFault;
358
+ }
359
+ ensure(key, required) {
360
+ const size = Math.max(storageMin[key], Math.ceil(required / 256) * 256);
361
+ if (size >
362
+ Math.min(this.device.limits.maxStorageBufferBindingSize, this.device.limits.maxBufferSize))
363
+ throw new Error("TC_TRACE_BUFFER_BUDGET: " +
364
+ key +
365
+ " exceeds actual adapter storage binding limit.");
366
+ const previous = this.buffers.get(key);
367
+ if (previous && previous.bytes >= size)
368
+ return previous.buffer;
369
+ const buffer = this.device.createBuffer({
370
+ label: "pelican " + key,
371
+ size,
372
+ usage: GPUBufferUsage.STORAGE |
373
+ GPUBufferUsage.COPY_DST |
374
+ GPUBufferUsage.COPY_SRC,
375
+ });
376
+ this.buffers.set(key, { buffer, bytes: size });
377
+ if (key === "materials")
378
+ this.materialGeneration++;
379
+ previous?.buffer.destroy();
380
+ this.computeGroup = undefined;
381
+ this.rasterGroup = undefined;
382
+ this.motionGroup = undefined;
383
+ return buffer;
384
+ }
385
+ put(key, data) {
386
+ const buffer = this.ensure(key, data.byteLength);
387
+ if (data.byteLength) {
388
+ this.device.queue.writeBuffer(buffer, 0, bytes(data));
389
+ if (key === "materials")
390
+ this.materialGeneration++;
391
+ this.uploadBytes += data.byteLength;
392
+ }
393
+ return buffer;
394
+ }
395
+ prepareFragmentBuffers(plan, source, materialBytes) {
396
+ if (!plan.length)
397
+ return [];
398
+ if (!source)
399
+ throw Error((plan.some((copy) => copy.texture)
400
+ ? "TC_RENDER_TEXTURE_CAPABILITY"
401
+ : "TC_RENDER_BUFFER_CAPABILITY") +
402
+ ": Native fragment slots require a managed compute GPU source.");
403
+ const limit = Math.min(this.device.limits.maxStorageBufferBindingSize, this.device.limits.maxBufferSize);
404
+ if (!Number.isSafeInteger(materialBytes) ||
405
+ Math.ceil(materialBytes / 256) * 256 > limit)
406
+ throw Error("TC_RENDER_BUFFER_LIMIT: Material table exceeds the device buffer limits.");
407
+ let end = 0;
408
+ // Resolve and validate every slot before encoding any copies or overwriting the table.
409
+ return plan.map((copy) => {
410
+ const { descriptor, count, byteOffset, type } = copy;
411
+ if (!Number.isSafeInteger(count) ||
412
+ count < 1 ||
413
+ count > 0xffffffff ||
414
+ descriptor.values.length !== count ||
415
+ descriptor.type !== type ||
416
+ (copy.texture &&
417
+ (type !== "vec4" ||
418
+ !Number.isSafeInteger(copy.texture.width) ||
419
+ !Number.isSafeInteger(copy.texture.height) ||
420
+ copy.texture.width < 1 ||
421
+ copy.texture.height < 1 ||
422
+ copy.texture.width * copy.texture.height !== count ||
423
+ descriptor.texture?.width !== copy.texture.width ||
424
+ descriptor.texture?.height !== copy.texture.height)) ||
425
+ !descriptor.key ||
426
+ !Number.isSafeInteger(descriptor.revision) ||
427
+ descriptor.revision < 0 ||
428
+ !Number.isSafeInteger(byteOffset) ||
429
+ byteOffset % 128 !== 0 ||
430
+ byteOffset < end ||
431
+ byteOffset + Math.ceil(count / 8) * 128 > materialBytes)
432
+ throw Error((copy.texture
433
+ ? "TC_RENDER_TEXTURE_RESOURCE"
434
+ : "TC_RENDER_BUFFER_RESOURCE") +
435
+ ": Invalid native fragment copy plan for " +
436
+ copy.material +
437
+ ":" +
438
+ copy.name);
439
+ end = byteOffset + Math.ceil(count / 8) * 128;
440
+ const view = source.resident(descriptor);
441
+ if (view.type !== type ||
442
+ view.count !== count ||
443
+ view.revision !== descriptor.revision ||
444
+ !(view.buffer.usage & GPUBufferUsage.COPY_SRC) ||
445
+ view.buffer.size < count * 16 ||
446
+ view.buffer.mapState === "mapped" ||
447
+ view.buffer.mapState === "pending" ||
448
+ view.buffer === this.buffers.get("materials")?.buffer)
449
+ throw Error((copy.texture
450
+ ? "TC_RENDER_TEXTURE_RESOURCE"
451
+ : "TC_RENDER_BUFFER_RESOURCE") +
452
+ ": Mismatched resident fragment slot " +
453
+ copy.material +
454
+ ":" +
455
+ copy.name);
456
+ return { ...copy, view };
457
+ });
458
+ }
459
+ copyFragmentBuffers(plan) {
460
+ const destination = this.buffers.get("materials");
461
+ if (plan.length &&
462
+ (!destination || !(destination.buffer.usage & GPUBufferUsage.COPY_DST)))
463
+ throw Error("TC_RENDER_BUFFER_RESOURCE: Missing native fragment material destination.");
464
+ const next = new Map();
465
+ const copies = [];
466
+ let changed = this.fragmentBuffers.size !== plan.length;
467
+ for (const { view, ...copy } of plan) {
468
+ if (view.buffer === destination.buffer ||
469
+ copy.byteOffset + copy.count * 16 > destination.bytes)
470
+ throw Error("TC_RENDER_BUFFER_RESOURCE: Invalid native fragment copy destination.");
471
+ const previous = this.fragmentBuffers.get(copy.byteOffset);
472
+ const contentChanged = !previous ||
473
+ previous.key !== copy.descriptor.key ||
474
+ previous.revision !== copy.descriptor.revision ||
475
+ previous.source !== view.buffer ||
476
+ previous.count !== copy.count ||
477
+ previous.type !== copy.type ||
478
+ previous.material !== copy.material ||
479
+ previous.name !== copy.name ||
480
+ previous.texture?.width !== copy.texture?.width ||
481
+ previous.texture?.height !== copy.texture?.height;
482
+ changed ||= contentChanged;
483
+ const state = {
484
+ ...copy,
485
+ key: copy.descriptor.key,
486
+ revision: copy.descriptor.revision,
487
+ source: view.buffer,
488
+ destination: destination.buffer,
489
+ generation: this.materialGeneration,
490
+ };
491
+ next.set(copy.byteOffset, state);
492
+ if (contentChanged ||
493
+ previous.destination !== state.destination ||
494
+ previous.generation !== state.generation)
495
+ copies.push(state);
496
+ }
497
+ if (copies.length) {
498
+ const encoder = this.device.createCommandEncoder({
499
+ label: "Pelican native fragment buffer copies",
500
+ });
501
+ for (const copy of copies)
502
+ encoder.copyBufferToBuffer(copy.source, 0, copy.destination, copy.byteOffset, copy.count * 16);
503
+ this.device.queue.submit([encoder.finish()]);
504
+ const buffers = copies.filter((copy) => !copy.texture), textures = copies.filter((copy) => copy.texture);
505
+ this.fragmentGpuCopies += buffers.length;
506
+ this.fragmentGpuCopyBytes += buffers.reduce((sum, copy) => sum + copy.count * 16, 0);
507
+ this.textureGpuCopies += textures.length;
508
+ this.textureGpuCopyBytes += textures.reduce((sum, copy) => sum + copy.count * 16, 0);
509
+ }
510
+ const hasTextures = [
511
+ ...this.fragmentBuffers.values(),
512
+ ...next.values(),
513
+ ].some((copy) => copy.texture);
514
+ this.fragmentBuffers = next;
515
+ if (changed) {
516
+ this.invalidateHistory(hasTextures ? "fragment-textures" : "fragment-buffers");
517
+ this.reset = true;
518
+ }
519
+ }
520
+ clearFragmentBuffers() {
521
+ this.fragmentBuffers.clear();
522
+ this.graphRows = new Float32Array();
523
+ this.graphMaterials = 0;
524
+ this.graphParameterRows = 0;
525
+ this.fragmentGpuCopies = 0;
526
+ this.fragmentGpuCopyBytes = 0;
527
+ this.textureGpuCopies = 0;
528
+ this.textureGpuCopyBytes = 0;
529
+ this.materialGeneration = 0;
530
+ }
531
+ get(key) {
532
+ return this.buffers.get(key)?.buffer ?? this.ensure(key, storageMin[key]);
533
+ }
534
+ invalidateHistory(reason) {
535
+ this.temporalHistoryReset = true;
536
+ this.temporalResetReasons.add(reason);
537
+ }
538
+ invalidateTemporal(reason = "mode") {
539
+ this.invalidateHistory(reason);
540
+ this.temporal?.reset();
541
+ this.temporalOutput = undefined;
542
+ this.lastMotionComputed = false;
543
+ this.previousCameraValid = false;
544
+ this.reset = true;
545
+ }
546
+ setFaultTarget(pixel, sample = 0) {
547
+ if (!this.captureFaults ||
548
+ !Number.isSafeInteger(pixel) ||
549
+ pixel < -1 ||
550
+ pixel >= this.width * this.height ||
551
+ !Number.isSafeInteger(sample) ||
552
+ sample < 0 ||
553
+ sample >= 64)
554
+ throw new Error("TC_TRACE_FAULT_TARGET: Select a valid pixel/sample in the development renderer.");
555
+ this.faultTarget = pixel;
556
+ this.faultSample = sample;
557
+ }
558
+ clearContent() {
559
+ this.mediumFault = undefined;
560
+ this.active();
561
+ this.capture.clear();
562
+ this.acceleration.dispose();
563
+ this.acceleration = createSurfaceAcceleration();
564
+ this.frame = undefined;
565
+ this.samples = 0;
566
+ this.mediumFault = undefined;
567
+ this.mediumFaults = 0;
568
+ this.mediumDebug = undefined;
569
+ this.device.queue.writeBuffer(this.get("pixels"), 0, new Uint32Array(4));
570
+ if (this.captureFaults)
571
+ this.device.queue.writeBuffer(this.get("pixels"), this.width * this.height * 64 + 16, new Uint32Array(FAULT_DEBUG_BYTES / 4));
572
+ this.textures = [];
573
+ this.materialData = undefined;
574
+ this.clearFragmentBuffers();
575
+ this.invalidateHistory("content");
576
+ this.temporal?.reset();
577
+ this.temporalOutput = undefined;
578
+ this.geometryData = undefined;
579
+ this.capturedSurface = undefined;
580
+ this.previousSurface = undefined;
581
+ this.previousCameraValid = false;
582
+ this.motionActive = false;
583
+ this.lastMotionComputed = false;
584
+ this.motionMatches = { matched: 0, missing: 0 };
585
+ this.motionGroup = undefined;
586
+ this.lightData = undefined;
587
+ this.lightingSignature = "";
588
+ this.mediumCatalog?.dispose();
589
+ this.mediumCatalog = undefined;
590
+ this.mediumIds.clear();
591
+ this.mediumFaces = [];
592
+ this.mediumSignature = "";
593
+ this.mediumRevision++;
594
+ this.initialMedia = [];
595
+ this.initialMediaKey = "";
596
+ this.mediumParams = [0, 0, 0, 0];
597
+ this.lightBufferDirty = true;
598
+ this.environmentCapture?.clear();
599
+ this.environmentCache.clear();
600
+ this.environmentImage?.dispose();
601
+ this.environmentImage = undefined;
602
+ this.environmentSource = null;
603
+ this.environmentRevision = "";
604
+ this.environmentIndex = -1;
605
+ this.environmentProposal = undefined;
606
+ this.buffers.get("environmentImportance")?.buffer.destroy();
607
+ this.buffers.delete("environmentImportance");
608
+ this.environmentState = "";
609
+ for (const key of [
610
+ "triangles",
611
+ "materials",
612
+ "textures",
613
+ "texels",
614
+ "nodes",
615
+ "lights",
616
+ ]) {
617
+ this.buffers.get(key)?.buffer.destroy();
618
+ this.buffers.delete(key);
619
+ }
620
+ this.computeGroup = undefined;
621
+ this.rasterGroup = undefined;
622
+ this.output = undefined;
623
+ this.contentGeneration++;
624
+ this.reset = true;
625
+ }
626
+ async update(scene, knownGraphs = new Set(), motionEnabled = false, graphs = new Map(), computeSource) {
627
+ this.active();
628
+ const generation = this.contentGeneration;
629
+ if (scene.fog)
630
+ throw new Error("TC_TRACE_FOG_STAGE: Fog transport is not integrated yet.");
631
+ if (scene.background && !(scene.background instanceof THREE.Color))
632
+ throw new Error("TC_TRACE_BACKGROUND_STAGE: Textured background needs explicit directional sampling.");
633
+ const source = scene.environment, revision = source
634
+ ? [
635
+ source.version,
636
+ source.source.version,
637
+ source
638
+ .pmremVersion ?? 0,
639
+ source.mapping,
640
+ ].join(":")
641
+ : "";
642
+ if (source !== this.environmentSource ||
643
+ revision !== this.environmentRevision) {
644
+ if (source && !this.environmentCapture)
645
+ throw new Error("TC_TRACE_ENVIRONMENT_RENDERER: An initialized raster renderer is required for environment capture.");
646
+ const radiance = source
647
+ ? await this.environmentCapture.capture(source)
648
+ : null;
649
+ if (this.disposed || generation !== this.contentGeneration)
650
+ throw new Error("TC_TRACE_CAPTURE_STALE: Surface content owner changed during environment capture.");
651
+ if (source !== scene.environment)
652
+ throw new Error("TC_TRACE_ENVIRONMENT_STALE: Content environment changed during capture.");
653
+ this.environmentImage?.dispose();
654
+ this.environmentCache.clear();
655
+ this.environmentImage = undefined;
656
+ if (radiance) {
657
+ this.environmentProposal = buildEnvironmentImportance(radiance.data, radiance.width, radiance.height);
658
+ this.put("environmentImportance", this.environmentProposal.table);
659
+ const texture = new THREE.DataTexture(radiance.data, radiance.width, radiance.height, THREE.RGBAFormat, THREE.FloatType);
660
+ texture.colorSpace = THREE.LinearSRGBColorSpace;
661
+ texture.flipY = false;
662
+ texture.wrapS = THREE.RepeatWrapping;
663
+ texture.wrapT = THREE.ClampToEdgeWrapping;
664
+ texture.magFilter = THREE.LinearFilter;
665
+ texture.minFilter = THREE.LinearFilter;
666
+ texture.generateMipmaps = false;
667
+ this.environmentImage = texture;
668
+ }
669
+ this.environmentSource = source;
670
+ this.environmentRevision = revision;
671
+ this.reset = true;
672
+ }
673
+ const start = performance.now(), captured = this.capture.capture(scene, knownGraphs, graphs);
674
+ this.environmentIndex = -1;
675
+ if (this.environmentImage) {
676
+ const diagnostics = [];
677
+ const environment = this.environmentCache.getSnapshot(this.environmentImage, undefined, diagnostics);
678
+ this.environmentIndex = captured.surface.textures.length;
679
+ captured.surface = Object.freeze({
680
+ ...captured.surface,
681
+ textures: Object.freeze([...captured.surface.textures, environment]),
682
+ diagnostics: Object.freeze([
683
+ ...captured.surface.diagnostics.filter((d) => d.code !== "SURFACE_SCENE_EFFECTS_NOT_CAPTURED"),
684
+ ...diagnostics,
685
+ ]),
686
+ });
687
+ }
688
+ this.environmentIntensity = scene.environmentIntensity;
689
+ this.environmentRotation
690
+ .makeRotationFromEuler(scene.environmentRotation)
691
+ .transpose();
692
+ const environmentState = JSON.stringify([
693
+ this.environmentIndex,
694
+ this.environmentIntensity,
695
+ ...this.environmentRotation.elements,
696
+ ]);
697
+ if (environmentState !== this.environmentState) {
698
+ this.reset = true;
699
+ this.invalidateHistory("environment");
700
+ }
701
+ this.environmentState = environmentState;
702
+ this.lastCaptureMs = performance.now() - start;
703
+ const materialLimit = Math.floor(Math.min(this.device.limits.maxStorageBufferBindingSize, this.device.limits.maxBufferSize) / 256) * 256;
704
+ const graphProgram = compileSurfaceGraphs(captured.surface, graphs, materialLimit);
705
+ if (graphProgram.bufferCopies.length && !computeSource)
706
+ throw Error((graphProgram.bufferCopies.some((copy) => copy.texture)
707
+ ? "TC_RENDER_TEXTURE_CAPABILITY"
708
+ : "TC_RENDER_BUFFER_CAPABILITY") +
709
+ ": Native fragment slots require a managed compute GPU source.");
710
+ const graphSource = SURFACE_SHADER.replace(DEFAULT_SURFACE_GRAPHS, graphProgram.source);
711
+ if (graphSource !== this.surfaceShader) {
712
+ await this.createSurfacePipelines(graphSource);
713
+ this.invalidateHistory("material-graphs");
714
+ this.reset = true;
715
+ }
716
+ if (!equal(this.graphRows, graphProgram.rows)) {
717
+ this.invalidateHistory("graph-parameters");
718
+ this.reset = true;
719
+ }
720
+ this.graphRows = graphProgram.rows;
721
+ this.graphMaterials = graphProgram.materials;
722
+ this.graphParameterRows =
723
+ graphProgram.rows.length / 32 -
724
+ graphProgram.bufferCopies.reduce((sum, copy) => sum + Math.ceil(copy.count / 8), 0);
725
+ await this.updateMedium(captured.surface);
726
+ if (motionEnabled && !this.motionCompute) {
727
+ const pipeline = await withGraphGPUDeadline(this.device, "TC_TRACE_GRAPH", async (active) => {
728
+ active();
729
+ const module = await shader(this.device, this.surfaceShader + SURFACE_RAY_SHADER + MOTION_SHADER, "primary motion reprojection");
730
+ active();
731
+ const pipeline = await this.device.createComputePipelineAsync({
732
+ label: "primary motion reprojection",
733
+ layout: "auto",
734
+ compute: {
735
+ module,
736
+ entryPoint: "motionMain",
737
+ constants: {
738
+ surfacePhysicalEnabled: 1,
739
+ surfaceOrderedTraversal: 0,
740
+ },
741
+ },
742
+ });
743
+ active();
744
+ return pipeline;
745
+ });
746
+ this.active();
747
+ if (generation !== this.contentGeneration)
748
+ throw Error("TC_TRACE_CAPTURE_STALE: Content changed during graph motion pipeline setup.");
749
+ this.motionCompute = pipeline;
750
+ }
751
+ if (motionEnabled && !this.temporal)
752
+ this.temporal = await createTemporalFilter(this.device);
753
+ if (this.disposed || generation !== this.contentGeneration)
754
+ throw new Error("TC_TRACE_CAPTURE_STALE: Content changed during volume pipeline setup.");
755
+ const fragmentPlan = this.prepareFragmentBuffers(graphProgram.bufferCopies, computeSource, captured.surface.materials.length * 128 + graphProgram.rows.byteLength);
756
+ const uploadStart = performance.now();
757
+ this.motionActive = motionEnabled;
758
+ if (motionEnabled)
759
+ this.ensure("motion", this.width * this.height * 16);
760
+ this.uploadBytes = 0;
761
+ const frame = this.acceleration.update(captured.surface), textureChanged = this.textures.length !== captured.surface.textures.length ||
762
+ this.textures.some((texture, index) => texture !== captured.surface.textures[index]);
763
+ if (textureChanged || !this.materialData) {
764
+ const resources = packSurfaceResources(captured.surface);
765
+ const combined = new Float32Array(resources.materials.length + this.graphRows.length);
766
+ combined.set(resources.materials);
767
+ combined.set(this.graphRows, resources.materials.length);
768
+ this.put("materials", combined);
769
+ this.put("textures", resources.textures);
770
+ this.put("texels", resources.texels);
771
+ this.materialData = combined;
772
+ this.textures = captured.surface.textures;
773
+ this.invalidateHistory("textures");
774
+ this.reset = true;
775
+ }
776
+ else {
777
+ const packedMaterials = packSurfaceMaterialTable(captured.surface);
778
+ const materials = new Float32Array(packedMaterials.length + this.graphRows.length);
779
+ materials.set(packedMaterials);
780
+ materials.set(this.graphRows, packedMaterials.length);
781
+ if (!equal(this.materialData, materials)) {
782
+ this.put("materials", materials);
783
+ this.materialData = materials;
784
+ this.invalidateHistory("materials");
785
+ this.reset = true;
786
+ }
787
+ }
788
+ this.copyFragmentBuffers(fragmentPlan);
789
+ if (!this.frame || frame.changes.kind !== "none") {
790
+ const geometry = packSurfaceGeometry(captured.surface, frame.orderedTriangles, this.mediumIds), traversalNodes = packTraversalData(frame.nodes, geometry);
791
+ this.geometryData = geometry;
792
+ if (!motionEnabled)
793
+ this.put("triangles", geometry);
794
+ const old = this.buffers.get("nodes"), buffer = this.ensure("nodes", traversalNodes.byteLength);
795
+ if (!old || old.buffer !== buffer || frame.changes.kind === "rebuild")
796
+ this.put("nodes", traversalNodes);
797
+ else
798
+ for (const range of frame.changes.nodeRanges) {
799
+ this.device.queue.writeBuffer(buffer, range.offset * 4, bytes(traversalNodes.subarray(range.offset, range.offset + range.count)));
800
+ this.uploadBytes += range.count * 4;
801
+ }
802
+ if (old?.buffer === buffer && frame.changes.kind !== "rebuild") {
803
+ const tail = traversalNodes.subarray(frame.nodes.length);
804
+ this.device.queue.writeBuffer(buffer, frame.nodes.byteLength, bytes(tail));
805
+ this.uploadBytes += tail.byteLength;
806
+ }
807
+ this.reset = true;
808
+ }
809
+ if (motionEnabled) {
810
+ const motion = packPreviousSurfacePositions(this.previousSurface, captured.surface, frame.orderedTriangles);
811
+ this.motionMatches = { matched: motion.matched, missing: motion.missing };
812
+ const geometry = this.geometryData;
813
+ const combined = new Float32Array(geometry.length + motion.data.length);
814
+ combined.set(geometry);
815
+ combined.set(motion.data, geometry.length);
816
+ this.put("triangles", combined);
817
+ }
818
+ const lighting = appendEmissiveLights(graphEmissiveProposal(captured.surface, graphs), frame.orderedTriangles, captured.lights);
819
+ this.emissiveCount = lighting.emitters;
820
+ let explicit = 0;
821
+ for (let index = 3; index < captured.lights.length; index += 12)
822
+ if (captured.lights[index] < 2)
823
+ explicit++;
824
+ this.lightTechniques =
825
+ explicit +
826
+ (this.emissiveCount ? 1 : 0) +
827
+ (this.environmentIndex >= 0 ? 1 : 0);
828
+ if (!equal(this.lightData, lighting.data)) {
829
+ this.lightData = lighting.data;
830
+ this.lightBufferDirty = true;
831
+ this.reset = true;
832
+ }
833
+ if (lighting.signature !== this.lightingSignature) {
834
+ this.lightingSignature = lighting.signature;
835
+ this.invalidateHistory("lights");
836
+ }
837
+ if (this.environment.some((value, index) => value !== captured.environment[index])) {
838
+ this.reset = true;
839
+ this.invalidateHistory("background");
840
+ }
841
+ this.environment = captured.environment;
842
+ this.environmentTexture = captured.environmentTexture;
843
+ this.frame = frame;
844
+ this.capturedSurface = captured.surface;
845
+ this.lastUploadMs = performance.now() - uploadStart;
846
+ return this.inspect();
847
+ }
848
+ async updateMedium(surface) {
849
+ const generation = this.contentGeneration;
850
+ const materials = new Map(surface.materials.map((material) => [material.uuid, material]));
851
+ const keys = new Set(surface.triangles
852
+ .filter((triangle) => (materials.get(triangle.materialUuid)?.transmission ?? 0) > 0)
853
+ .map((triangle) => JSON.stringify([triangle.objectUuid, triangle.instanceId])));
854
+ const faces = surface.triangles.filter((triangle) => keys.has(JSON.stringify([triangle.objectUuid, triangle.instanceId])));
855
+ const signature = JSON.stringify([...new Set(faces.map((face) => face.materialUuid))].map((id) => {
856
+ const material = materials.get(id);
857
+ return [
858
+ id,
859
+ material.transmission,
860
+ material.ior,
861
+ material.attenuationColor,
862
+ material.attenuationDistance,
863
+ ];
864
+ }));
865
+ if (signature !== this.mediumSignature ||
866
+ faces.length !== this.mediumFaces.length ||
867
+ faces.some((triangle, index) => triangle !== this.mediumFaces[index])) {
868
+ const candidate = faces.length ? createMediumCatalog(surface) : undefined;
869
+ this.mediumCatalog?.dispose();
870
+ this.mediumCatalog = candidate;
871
+ this.mediumIds = new Map(candidate?.volumes.map((volume) => [volume.key, volume.id]) ?? []);
872
+ this.mediumFaces = faces;
873
+ this.mediumSignature = signature;
874
+ this.mediumRevision++;
875
+ this.initialMediaKey = "";
876
+ this.invalidateHistory("medium");
877
+ this.lightBufferDirty = true;
878
+ this.reset = true;
879
+ }
880
+ if (this.mediumIds.size && !this.volumeCompute) {
881
+ const pipeline = await withGraphGPUDeadline(this.device, "TC_TRACE_GRAPH", async (active) => {
882
+ active();
883
+ const pipeline = await this.device.createComputePipelineAsync({
884
+ label: "closed dielectric volume transport",
885
+ layout: "auto",
886
+ compute: {
887
+ module: this.computeModule,
888
+ entryPoint: "pathMain",
889
+ constants: {
890
+ traceVolumeEnabled: 1,
891
+ surfacePhysicalEnabled: 1,
892
+ surfaceOrderedTraversal: 0,
893
+ traceCaptureFault: this.captureFaults ? 1 : 0,
894
+ },
895
+ },
896
+ });
897
+ active();
898
+ return pipeline;
899
+ });
900
+ this.active();
901
+ if (generation !== this.contentGeneration)
902
+ throw Error("TC_TRACE_CAPTURE_STALE: Content changed during graph volume pipeline setup.");
903
+ this.volumeCompute = pipeline;
904
+ }
905
+ const pipeline = this.mediumIds.size
906
+ ? this.volumeCompute
907
+ : this.opaqueCompute;
908
+ if (pipeline !== this.compute) {
909
+ this.compute = pipeline;
910
+ this.computeGroup = undefined;
911
+ }
912
+ }
913
+ uploadMedium(camera) {
914
+ const position = camera.getWorldPosition(new THREE.Vector3()).toArray(), key = JSON.stringify([this.mediumRevision, ...position]);
915
+ if (key !== this.initialMediaKey) {
916
+ this.initialMedia = this.mediumCatalog?.initial(position) ?? [];
917
+ if (this.initialMedia.length > 64)
918
+ throw new Error("TC_TRACE_MEDIUM_CAPACITY: Camera starts in more than 64 nested media supported by this pipeline.");
919
+ this.initialMediaKey = key;
920
+ this.lightBufferDirty = true;
921
+ }
922
+ const volumes = this.mediumCatalog?.volumes ?? [], base = (this.lightData?.length ?? 0) / 12, initialBase = base + volumes.length;
923
+ this.mediumParams = [
924
+ base,
925
+ this.initialMedia.length,
926
+ initialBase,
927
+ volumes.length,
928
+ ];
929
+ if (this.lightBufferDirty) {
930
+ const data = new Float32Array((initialBase + this.initialMedia.length) * 12);
931
+ if (this.lightData)
932
+ data.set(this.lightData);
933
+ for (const volume of volumes)
934
+ data.set([volume.ior, ...volume.sigma], (base + volume.id - 1) * 12);
935
+ this.initialMedia.forEach((id, index) => {
936
+ data[(initialBase + index) * 12] = id;
937
+ });
938
+ this.put("lights", data);
939
+ this.lightBufferDirty = false;
940
+ }
941
+ }
942
+ groups() {
943
+ if (!this.computeGroup) {
944
+ this.computeGroup = this.device.createBindGroup({
945
+ layout: this.compute.getBindGroupLayout(0),
946
+ entries: [
947
+ ...[
948
+ "triangles",
949
+ "materials",
950
+ "textures",
951
+ "texels",
952
+ "nodes",
953
+ "lights",
954
+ "pixels",
955
+ ].map((key, binding) => ({
956
+ binding,
957
+ resource: { buffer: this.get(key) },
958
+ })),
959
+ { binding: 7, resource: { buffer: this.uniform } },
960
+ { binding: 8, resource: this.primary.createView() },
961
+ {
962
+ binding: 9,
963
+ resource: { buffer: this.get("environmentImportance") },
964
+ },
965
+ ],
966
+ });
967
+ }
968
+ if (!this.rasterGroup) {
969
+ this.rasterGroup = this.device.createBindGroup({
970
+ layout: this.raster.getBindGroupLayout(0),
971
+ entries: [
972
+ ...["triangles", "materials", "textures", "texels"].map((key, binding) => ({
973
+ binding,
974
+ resource: { buffer: this.get(key) },
975
+ })),
976
+ { binding: 7, resource: { buffer: this.uniform } },
977
+ ],
978
+ });
979
+ }
980
+ if (this.motionActive && this.motionCompute && !this.motionGroup) {
981
+ this.motionGroup = this.device.createBindGroup({
982
+ layout: this.motionCompute.getBindGroupLayout(0),
983
+ entries: [
984
+ ...["triangles", "materials", "textures", "texels", "nodes"].map((key, binding) => ({
985
+ binding,
986
+ resource: { buffer: this.get(key) },
987
+ })),
988
+ { binding: 6, resource: { buffer: this.get("motion") } },
989
+ { binding: 7, resource: { buffer: this.uniform } },
990
+ { binding: 8, resource: this.primary.createView() },
991
+ ],
992
+ });
993
+ }
994
+ }
995
+ async prepareLinearPresentation() {
996
+ this.active();
997
+ if (this.linearPresent)
998
+ return;
999
+ if (this.linearPresentPending)
1000
+ return this.linearPresentPending;
1001
+ this.linearPresentPending = withGraphGPUDeadline(this.device, "TC_POSTPROCESS", async (active) => {
1002
+ const module = await shader(this.device, TRACE_LINEAR_PRESENT_SHADER, "linear HDR postprocess handoff");
1003
+ active();
1004
+ this.active();
1005
+ const pipeline = await this.device.createRenderPipelineAsync({
1006
+ layout: "auto",
1007
+ vertex: { module, entryPoint: "vertex" },
1008
+ fragment: {
1009
+ module,
1010
+ entryPoint: "fragment",
1011
+ targets: [
1012
+ { format: "rgba16float" },
1013
+ { format: "rgba16float" },
1014
+ { format: "rgba16float" },
1015
+ ],
1016
+ },
1017
+ primitive: { topology: "triangle-list" },
1018
+ });
1019
+ active();
1020
+ this.active();
1021
+ this.linearPresent = pipeline;
1022
+ }).finally(() => {
1023
+ this.linearPresentPending = undefined;
1024
+ });
1025
+ return this.linearPresentPending;
1026
+ }
1027
+ render(camera, settings, targets) {
1028
+ this.active();
1029
+ if (targets) {
1030
+ if (!this.linearPresent)
1031
+ throw new Error("TC_POSTPROCESS_NOT_READY: Prepare the linear HDR presentation pipeline before rendering.");
1032
+ for (const texture of [targets.color, targets.normal, targets.depth])
1033
+ if (texture.width !== this.width ||
1034
+ texture.height !== this.height ||
1035
+ texture.format !== "rgba16float" ||
1036
+ !(texture.usage & GPUTextureUsage.RENDER_ATTACHMENT))
1037
+ throw new Error("TC_POSTPROCESS_TARGET: Linear HDR targets must match the frame size and rgba16float render-attachment format.");
1038
+ }
1039
+ if (!this.frame)
1040
+ throw new Error("TC_TRACE_SCENE: Capture the current scene before rendering.");
1041
+ const started = performance.now();
1042
+ camera.updateWorldMatrix(true, false);
1043
+ this.uploadMedium(camera);
1044
+ const forward = new THREE.Vector3();
1045
+ camera.getWorldDirection(forward);
1046
+ const spread = Math.tan(THREE.MathUtils.degToRad(camera.fov / 2));
1047
+ const right = new THREE.Vector3()
1048
+ .setFromMatrixColumn(camera.matrixWorld, 0)
1049
+ .multiplyScalar(spread * camera.aspect), up = new THREE.Vector3()
1050
+ .setFromMatrixColumn(camera.matrixWorld, 1)
1051
+ .multiplyScalar(spread);
1052
+ const viewProjection = camera.projectionMatrix
1053
+ .clone()
1054
+ .multiply(camera.matrixWorldInverse), cameraKey = JSON.stringify([
1055
+ ...camera.matrixWorld.elements,
1056
+ ...camera.projectionMatrix.elements,
1057
+ ]), settingsKey = JSON.stringify(settings);
1058
+ if (cameraKey !== this.cameraKey)
1059
+ this.reset = true;
1060
+ if (settingsKey !== this.settingsKey) {
1061
+ this.reset = true;
1062
+ this.invalidateHistory("settings");
1063
+ }
1064
+ this.cameraKey = cameraKey;
1065
+ this.settingsKey = settingsKey;
1066
+ const jitterX = settings.mode === "hybrid" ? halton(this.serial + 1, 2) : 0, jitterY = settings.mode === "hybrid" ? halton(this.serial + 1, 3) : 0;
1067
+ if (settings.mode === "hybrid") {
1068
+ const projection = camera.projectionMatrix.clone();
1069
+ projection.elements[8] += (2 * jitterX) / this.width;
1070
+ projection.elements[9] -= (2 * jitterY) / this.height;
1071
+ viewProjection.copy(projection.multiply(camera.matrixWorldInverse));
1072
+ }
1073
+ if (this.reset) {
1074
+ this.samples = 0;
1075
+ this.resets++;
1076
+ }
1077
+ const params = new Float32Array(TRACE_PARAMETER_FLOATS);
1078
+ params.set([
1079
+ ...camera.position.toArray(),
1080
+ this.lightTechniques,
1081
+ ...forward.toArray(),
1082
+ jitterX,
1083
+ ...right.toArray(),
1084
+ jitterY,
1085
+ ...up.toArray(),
1086
+ this.emissiveCount ? 1 : 0,
1087
+ ...this.environment,
1088
+ 0,
1089
+ this.width,
1090
+ this.height,
1091
+ this.serial++,
1092
+ camera.near,
1093
+ this.frame.metrics.nodes,
1094
+ this.frame.metrics.triangles,
1095
+ (this.lightData?.length ?? 0) / 12,
1096
+ this.reset ? 1 : 0,
1097
+ settings.exposure,
1098
+ settings.samplesPerFrame,
1099
+ settings.maxBounces,
1100
+ camera.far,
1101
+ settings.mode === "hybrid" ? 1 : 0,
1102
+ ["beauty", "albedo", "normal", "depth", "samples"].indexOf(settings.debug),
1103
+ settings.directLighting ? 1 : 0,
1104
+ settings.indirectLighting ? 1 : 0,
1105
+ ...viewProjection.elements,
1106
+ this.environmentIndex,
1107
+ this.environmentIntensity,
1108
+ this.environmentProposal?.width ?? 0,
1109
+ this.environmentProposal?.height ?? 0,
1110
+ this.environmentRotation.elements[0],
1111
+ this.environmentRotation.elements[1],
1112
+ this.environmentRotation.elements[2],
1113
+ this.captureFaults ? this.faultTarget : -1,
1114
+ this.environmentRotation.elements[4],
1115
+ this.environmentRotation.elements[5],
1116
+ this.environmentRotation.elements[6],
1117
+ this.captureFaults ? this.faultSample : 0,
1118
+ this.environmentRotation.elements[8],
1119
+ this.environmentRotation.elements[9],
1120
+ this.environmentRotation.elements[10],
1121
+ 0,
1122
+ ...this.mediumParams,
1123
+ ...this.previousViewProjection.elements,
1124
+ ...this.previousCameraOrigin.toArray(),
1125
+ this.previousCameraValid ? 1 : 0,
1126
+ ]);
1127
+ this.device.queue.writeBuffer(this.uniform, 0, params);
1128
+ this.groups();
1129
+ const encoder = this.device.createCommandEncoder({
1130
+ label: "pelican surface frame",
1131
+ });
1132
+ if (settings.mode === "hybrid") {
1133
+ const pass = encoder.beginRenderPass({
1134
+ label: "raster primary visibility",
1135
+ ...(this.timestamps
1136
+ ? {
1137
+ timestampWrites: {
1138
+ querySet: this.timestamps,
1139
+ beginningOfPassWriteIndex: 0,
1140
+ endOfPassWriteIndex: 1,
1141
+ },
1142
+ }
1143
+ : {}),
1144
+ colorAttachments: [
1145
+ {
1146
+ view: this.primary.createView(),
1147
+ loadOp: "clear",
1148
+ storeOp: "store",
1149
+ clearValue: { r: -1, g: -1, b: 0, a: 0 },
1150
+ },
1151
+ ],
1152
+ depthStencilAttachment: {
1153
+ view: this.depth.createView(),
1154
+ depthLoadOp: "clear",
1155
+ depthStoreOp: "store",
1156
+ depthClearValue: 1,
1157
+ },
1158
+ });
1159
+ pass.setPipeline(this.raster);
1160
+ pass.setBindGroup(0, this.rasterGroup);
1161
+ pass.draw(this.frame.metrics.triangles * 3);
1162
+ pass.end();
1163
+ }
1164
+ const pass = encoder.beginComputePass({
1165
+ label: "surface light transport",
1166
+ ...(this.timestamps
1167
+ ? {
1168
+ timestampWrites: {
1169
+ querySet: this.timestamps,
1170
+ beginningOfPassWriteIndex: 2,
1171
+ endOfPassWriteIndex: 3,
1172
+ },
1173
+ }
1174
+ : {}),
1175
+ });
1176
+ pass.setPipeline(this.compute);
1177
+ pass.setBindGroup(0, this.computeGroup);
1178
+ pass.dispatchWorkgroups(Math.ceil(this.width / 8), Math.ceil(this.height / 8));
1179
+ pass.end();
1180
+ this.lastMotionComputed = this.motionActive && this.reset;
1181
+ if (this.lastMotionComputed) {
1182
+ const motion = encoder.beginComputePass({
1183
+ label: "primary motion reprojection",
1184
+ });
1185
+ motion.setPipeline(this.motionCompute);
1186
+ motion.setBindGroup(0, this.motionGroup);
1187
+ motion.dispatchWorkgroups(Math.ceil(this.width / 8), Math.ceil(this.height / 8));
1188
+ motion.end();
1189
+ this.motionPasses++;
1190
+ }
1191
+ const planeBytes = this.width * this.height * 16;
1192
+ encoder.copyBufferToBuffer(this.get("pixels"), planeBytes + 16, this.get("average"), 0, planeBytes);
1193
+ encoder.copyBufferToBuffer(this.get("pixels"), planeBytes * 2 + 16, this.get("normalDepth"), 0, planeBytes);
1194
+ encoder.copyBufferToBuffer(this.get("pixels"), planeBytes * 3 + 16, this.get("albedo"), 0, planeBytes);
1195
+ this.lastTemporalReset = this.temporalHistoryReset;
1196
+ this.lastTemporalProgressive = !this.reset;
1197
+ this.lastTemporalReasons = [...this.temporalResetReasons];
1198
+ const temporalOutput = this.temporal?.encode(encoder, this.get("average"), this.get("normalDepth"), this.get("albedo"), this.get("motion"), this.width, this.height, {
1199
+ enabled: this.motionActive,
1200
+ reset: this.temporalHistoryReset,
1201
+ progressive: !this.reset,
1202
+ }) ?? this.get("average");
1203
+ this.temporalOutput = this.motionActive ? temporalOutput : undefined;
1204
+ if (this.motionActive && this.temporalHistoryReset)
1205
+ this.temporalResets++;
1206
+ const output = this.denoiser.encode(encoder, temporalOutput, this.get("normalDepth"), this.get("albedo"), this.width, this.height, {
1207
+ enabled: settings.denoise && settings.debug === "beauty",
1208
+ iterations: settings.denoiseIterations,
1209
+ });
1210
+ this.device.queue.writeBuffer(this.presentUniform, 0, new Float32Array([
1211
+ this.width,
1212
+ this.height,
1213
+ settings.exposure,
1214
+ settings.debug === "beauty" ? 0 : 1,
1215
+ ]));
1216
+ const presentationPipeline = targets ? this.linearPresent : this.present;
1217
+ const display = this.device.createBindGroup({
1218
+ layout: presentationPipeline.getBindGroupLayout(0),
1219
+ entries: targets
1220
+ ? [
1221
+ { binding: 0, resource: { buffer: output } },
1222
+ { binding: 1, resource: { buffer: this.get("normalDepth") } },
1223
+ { binding: 2, resource: { buffer: this.presentUniform } },
1224
+ ]
1225
+ : [
1226
+ { binding: 0, resource: { buffer: output } },
1227
+ { binding: 1, resource: { buffer: this.presentUniform } },
1228
+ ],
1229
+ });
1230
+ const present = encoder.beginRenderPass({
1231
+ label: "surface image output",
1232
+ ...(this.timestamps
1233
+ ? {
1234
+ timestampWrites: {
1235
+ querySet: this.timestamps,
1236
+ beginningOfPassWriteIndex: 4,
1237
+ endOfPassWriteIndex: 5,
1238
+ },
1239
+ }
1240
+ : {}),
1241
+ colorAttachments: targets
1242
+ ? [targets.color, targets.normal, targets.depth].map((texture) => ({
1243
+ view: texture.createView(),
1244
+ loadOp: "clear",
1245
+ storeOp: "store",
1246
+ clearValue: { r: 0, g: 0, b: 0, a: 0 },
1247
+ }))
1248
+ : [
1249
+ {
1250
+ view: this.context.getCurrentTexture().createView(),
1251
+ loadOp: "clear",
1252
+ storeOp: "store",
1253
+ clearValue: { r: 0, g: 0, b: 0, a: 1 },
1254
+ },
1255
+ ],
1256
+ });
1257
+ present.setPipeline(presentationPipeline);
1258
+ present.setBindGroup(0, display);
1259
+ present.draw(3);
1260
+ present.end();
1261
+ if (this.timestamps && this.timingResolve)
1262
+ encoder.resolveQuerySet(this.timestamps, 0, 6, this.timingResolve, 0);
1263
+ this.device.queue.submit([encoder.finish()]);
1264
+ this.previousViewProjection.copy(viewProjection);
1265
+ this.previousCameraOrigin.copy(camera.position);
1266
+ this.previousCameraValid = true;
1267
+ this.previousSurface = this.capturedSurface;
1268
+ this.temporalHistoryReset = false;
1269
+ this.temporalResetReasons.clear();
1270
+ this.timingStart = settings.mode === "hybrid" ? 0 : 2;
1271
+ this.output = output;
1272
+ this.scheduleMediumCheck();
1273
+ this.samples += settings.samplesPerFrame;
1274
+ this.reset = false;
1275
+ this.lastSubmitMs = performance.now() - started;
1276
+ return this.lastSubmitMs;
1277
+ }
1278
+ async readPlane(source) {
1279
+ const size = this.width * this.height * 16;
1280
+ const read = this.device.createBuffer({
1281
+ size,
1282
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
1283
+ });
1284
+ try {
1285
+ const encoder = this.device.createCommandEncoder();
1286
+ encoder.copyBufferToBuffer(source, 0, read, 0, size);
1287
+ this.device.queue.submit([encoder.finish()]);
1288
+ await read.mapAsync(GPUMapMode.READ);
1289
+ const result = new Float32Array(read.getMappedRange().slice(0));
1290
+ read.unmap();
1291
+ return result;
1292
+ }
1293
+ finally {
1294
+ read.destroy();
1295
+ }
1296
+ }
1297
+ async readPrimaryStats() {
1298
+ this.active();
1299
+ if (!this.frame)
1300
+ throw new Error("TC_TRACE_PRIMARY_NOT_READY");
1301
+ const serial = this.serial - 1, generation = this.contentGeneration;
1302
+ const rowBytes = Math.ceil((this.width * 16) / 256) * 256;
1303
+ const read = this.device.createBuffer({
1304
+ size: rowBytes * this.height,
1305
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
1306
+ });
1307
+ try {
1308
+ const encoder = this.device.createCommandEncoder();
1309
+ encoder.copyTextureToBuffer({ texture: this.primary }, { buffer: read, bytesPerRow: rowBytes }, [this.width, this.height]);
1310
+ this.device.queue.submit([encoder.finish()]);
1311
+ await read.mapAsync(GPUMapMode.READ);
1312
+ if (generation !== this.contentGeneration || serial !== this.serial - 1)
1313
+ throw new Error("TC_TRACE_READBACK_STALE: Pause before inspecting primary refinement counts.");
1314
+ const mapped = read.getMappedRange();
1315
+ let hits = 0, misses = 0, refinements = 0;
1316
+ for (let y = 0; y < this.height; y++) {
1317
+ const row = new Float32Array(mapped, y * rowBytes, this.width * 4);
1318
+ for (let x = 0; x < this.width; x++) {
1319
+ const id = row[x * 4 + 1];
1320
+ if (!Number.isInteger(id) || id < -2)
1321
+ throw new Error("TC_TRACE_PRIMARY_DATA: Invalid visibility identifier.");
1322
+ if (id === -2)
1323
+ refinements++;
1324
+ else if (id === -1)
1325
+ misses++;
1326
+ else
1327
+ hits++;
1328
+ }
1329
+ }
1330
+ read.unmap();
1331
+ return {
1332
+ pixels: this.width * this.height,
1333
+ hits,
1334
+ misses,
1335
+ refinements,
1336
+ frameSerial: serial,
1337
+ };
1338
+ }
1339
+ finally {
1340
+ read.destroy();
1341
+ }
1342
+ }
1343
+ async readGuides() {
1344
+ this.active();
1345
+ const [normalDepth, albedoRoughness] = await Promise.all([
1346
+ this.readPlane(this.get("normalDepth")),
1347
+ this.readPlane(this.get("albedo")),
1348
+ ]);
1349
+ return { normalDepth, albedoRoughness };
1350
+ }
1351
+ async readTemporal() {
1352
+ this.active();
1353
+ if (!this.temporalOutput)
1354
+ throw new Error("TC_TEMPORAL_NOT_ACTIVE: Enable denoising and render a frame first.");
1355
+ return this.readPlane(this.temporalOutput);
1356
+ }
1357
+ /** Development readback; progressive static frames deliberately do not recompute motion. */
1358
+ async readMotion() {
1359
+ this.active();
1360
+ if (!this.motionActive || !this.lastMotionComputed)
1361
+ throw new Error("TC_MOTION_NOT_CURRENT: Motion is available only after an independent denoised frame, not a progressive accumulation frame.");
1362
+ return this.readPlane(this.get("motion"));
1363
+ }
1364
+ async pixels() {
1365
+ await this.mediumFaultCheck;
1366
+ await this.scheduleMediumCheck();
1367
+ if (this.mediumFault)
1368
+ throw this.mediumFault;
1369
+ const data = await this.readRadiance();
1370
+ let nonBlack = 0, finite = true, sum = 0, maximum = 0;
1371
+ for (let index = 0; index < data.length; index += 4) {
1372
+ const rgb = data.subarray(index, index + 3);
1373
+ if (rgb.some((value) => !Number.isFinite(value)))
1374
+ finite = false;
1375
+ const luminance = rgb[0] * 0.2126 + rgb[1] * 0.7152 + rgb[2] * 0.0722;
1376
+ sum += luminance;
1377
+ if (luminance > 1e-5)
1378
+ nonBlack++;
1379
+ maximum = Math.max(maximum, ...rgb);
1380
+ }
1381
+ return {
1382
+ backend: "webgpu",
1383
+ nonBlack,
1384
+ finite,
1385
+ meanLuminance: sum / (this.width * this.height),
1386
+ maximum,
1387
+ samples: this.samples,
1388
+ frameSerial: this.serial - 1,
1389
+ };
1390
+ }
1391
+ scheduleMediumCheck() {
1392
+ if (this.mediumFaultBusy || this.disposed || this.lost)
1393
+ return this.mediumFaultCheck;
1394
+ this.mediumFaultBusy = true;
1395
+ const generation = this.contentGeneration;
1396
+ this.mediumFaultCheck = (async () => {
1397
+ const read = this.device.createBuffer({
1398
+ size: this.captureFaults ? 16 + FAULT_DEBUG_BYTES : 16,
1399
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
1400
+ });
1401
+ try {
1402
+ const encoder = this.device.createCommandEncoder();
1403
+ encoder.copyBufferToBuffer(this.get("pixels"), 0, read, 0, 16);
1404
+ if (this.captureFaults)
1405
+ encoder.copyBufferToBuffer(this.get("pixels"), this.width * this.height * 64 + 16, read, 16, FAULT_DEBUG_BYTES);
1406
+ this.device.queue.submit([encoder.finish()]);
1407
+ await read.mapAsync(GPUMapMode.READ);
1408
+ const mapped = read.getMappedRange();
1409
+ const values = new Uint32Array(mapped);
1410
+ const faults = values[0], mask = values[1], detail = values[2], pixel = values[3];
1411
+ const debug = this.captureFaults
1412
+ ? Array.from(new Float32Array(mapped).slice(4))
1413
+ : [];
1414
+ read.unmap();
1415
+ if (this.disposed || this.lost || generation !== this.contentGeneration)
1416
+ return;
1417
+ if (faults) {
1418
+ this.mediumFaults = faults;
1419
+ if (debug[23] === 1) {
1420
+ const origin = debug.slice(0, 3), direction = debug.slice(4, 7);
1421
+ let cpuHit;
1422
+ try {
1423
+ cpuHit = this.acceleration.intersect(origin, direction);
1424
+ }
1425
+ catch (error) {
1426
+ cpuHit = { error: String(error) };
1427
+ }
1428
+ const history = debug[48] === debug[19] &&
1429
+ debug[49] === debug[21] &&
1430
+ debug[51] === debug[22]
1431
+ ? Array.from({ length: Math.min(debug[50], 8) }, (_, index) => {
1432
+ const at = 52 + index * 32;
1433
+ return {
1434
+ bounce: index,
1435
+ origin: debug.slice(at, at + 3),
1436
+ triangle: debug[at + 3],
1437
+ direction: debug.slice(at + 4, at + 7),
1438
+ distance: debug[at + 7],
1439
+ mediumDepth: debug[at + 8],
1440
+ medium: debug[at + 9],
1441
+ front: debug[at + 10],
1442
+ boundary: debug[at + 11],
1443
+ position: debug.slice(at + 12, at + 15),
1444
+ roughness: debug[at + 15],
1445
+ scattered: debug.slice(at + 16, at + 19),
1446
+ event: debug[at + 19],
1447
+ geometric: debug.slice(at + 20, at + 23),
1448
+ pdf: debug[at + 23],
1449
+ barycentric: debug.slice(at + 24, at + 27),
1450
+ iorInside: debug[at + 27],
1451
+ normal: debug.slice(at + 28, at + 31),
1452
+ iorOutside: debug[at + 31],
1453
+ };
1454
+ })
1455
+ : null;
1456
+ const source = debug[21] === this.serial - 1
1457
+ ? this.frame?.orderedTriangles[debug[11]]
1458
+ : undefined;
1459
+ const material = source &&
1460
+ this.capturedSurface?.materials.find((material) => material.uuid === source.materialUuid);
1461
+ this.mediumDebug = {
1462
+ origin,
1463
+ direction,
1464
+ bounce: debug[3],
1465
+ medium: debug[7],
1466
+ previousPosition: debug.slice(8, 11),
1467
+ previousHit: debug[11],
1468
+ previousGeometric: debug.slice(12, 15),
1469
+ previousEvent: debug[15],
1470
+ previousBary: debug.slice(16, 19),
1471
+ pixel: debug[19],
1472
+ previousFront: debug[20],
1473
+ frame: debug[21],
1474
+ sample: debug[22],
1475
+ gpuTriangle: [
1476
+ debug.slice(24, 28),
1477
+ debug.slice(28, 32),
1478
+ debug.slice(32, 36),
1479
+ ],
1480
+ physical: debug.slice(36, 40),
1481
+ normalMatrixRecords: debug.slice(40, 48),
1482
+ cpuHit,
1483
+ cpuFrame: this.serial - 1,
1484
+ source: source && {
1485
+ objectUuid: source.objectUuid,
1486
+ geometryUuid: source.geometryUuid,
1487
+ instanceId: source.instanceId,
1488
+ materialUuid: source.materialUuid,
1489
+ materialName: material?.name,
1490
+ },
1491
+ history,
1492
+ historyTruncated: debug[3] >= 8,
1493
+ };
1494
+ }
1495
+ throw new Error("TC_TRACE_MEDIUM_STATE: GPU recorded " +
1496
+ faults +
1497
+ " invalid boundary/order/capacity transitions (mask=" +
1498
+ mask +
1499
+ "; detail=" +
1500
+ detail +
1501
+ ",pixel=" +
1502
+ pixel +
1503
+ "; 1=missing volume, 2=boundary order/duplicate entry, 4=miss inside, 8=capacity)." +
1504
+ (this.mediumDebug
1505
+ ? " debug=" + JSON.stringify(this.mediumDebug)
1506
+ : ""));
1507
+ }
1508
+ }
1509
+ finally {
1510
+ read.destroy();
1511
+ }
1512
+ })()
1513
+ .catch((cause) => {
1514
+ if (this.disposed || this.lost || generation !== this.contentGeneration)
1515
+ return;
1516
+ this.mediumFault =
1517
+ cause instanceof Error ? cause : new Error(String(cause));
1518
+ this.onFault?.(this.mediumFault);
1519
+ })
1520
+ .finally(() => {
1521
+ this.mediumFaultBusy = false;
1522
+ });
1523
+ return this.mediumFaultCheck;
1524
+ }
1525
+ /** Private validation readback. Pixel radiance is not exposed to game code. */
1526
+ async readRadiance(filtered = true) {
1527
+ this.active();
1528
+ const size = this.width * this.height * 16, read = this.device.createBuffer({
1529
+ size,
1530
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
1531
+ });
1532
+ try {
1533
+ const encoder = this.device.createCommandEncoder();
1534
+ encoder.copyBufferToBuffer(filtered ? (this.output ?? this.get("average")) : this.get("average"), 0, read, 0, size);
1535
+ this.device.queue.submit([encoder.finish()]);
1536
+ await read.mapAsync(GPUMapMode.READ);
1537
+ const data = new Float32Array(read.getMappedRange().slice(0));
1538
+ read.unmap();
1539
+ return data;
1540
+ }
1541
+ finally {
1542
+ read.destroy();
1543
+ }
1544
+ }
1545
+ async gpuTimeMs() {
1546
+ this.active();
1547
+ if (!this.timingResolve)
1548
+ return null;
1549
+ const read = this.device.createBuffer({
1550
+ size: 48,
1551
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
1552
+ });
1553
+ try {
1554
+ const encoder = this.device.createCommandEncoder();
1555
+ encoder.copyBufferToBuffer(this.timingResolve, 0, read, 0, 48);
1556
+ this.device.queue.submit([encoder.finish()]);
1557
+ await read.mapAsync(GPUMapMode.READ);
1558
+ const values = new BigUint64Array(read.getMappedRange().slice(0));
1559
+ read.unmap();
1560
+ return {
1561
+ frameMs: Number(values[5] - values[this.timingStart]) / 1e6,
1562
+ transportMs: Number(values[3] - values[2]) / 1e6,
1563
+ presentMs: Number(values[5] - values[4]) / 1e6,
1564
+ primaryMs: this.timingStart === 0 ? Number(values[1] - values[0]) / 1e6 : null,
1565
+ source: "timestamp-query",
1566
+ };
1567
+ }
1568
+ finally {
1569
+ read.destroy();
1570
+ }
1571
+ }
1572
+ queryRay(origin, direction) {
1573
+ this.active();
1574
+ const hit = this.acceleration.intersect(origin, direction);
1575
+ return {
1576
+ backend: "cpu-surface-query",
1577
+ distance: hit?.distance ?? -1,
1578
+ index: hit?.triangleIndex ?? -1,
1579
+ normal: hit?.normal ?? [0, 0, 0],
1580
+ };
1581
+ }
1582
+ inspect() {
1583
+ return {
1584
+ disposed: this.disposed,
1585
+ lost: this.lost,
1586
+ backend: "webgpu",
1587
+ width: this.width,
1588
+ height: this.height,
1589
+ triangleCount: this.frame?.metrics.triangles ?? 0,
1590
+ nodeCount: this.frame?.metrics.nodes ?? 0,
1591
+ samples: this.samples,
1592
+ frameSerial: this.serial - 1,
1593
+ mediumFaults: this.mediumFaults,
1594
+ historyResets: this.resets,
1595
+ contentGeneration: this.contentGeneration,
1596
+ volumeTransport: {
1597
+ volumes: this.mediumIds.size,
1598
+ initialMedia: [...this.initialMedia],
1599
+ catalog: this.mediumCatalog?.inspect(),
1600
+ faults: this.mediumFaults,
1601
+ debug: this.mediumDebug,
1602
+ },
1603
+ acceleration: this.frame && {
1604
+ revision: this.frame.revision,
1605
+ topologyRevision: this.frame.topologyRevision,
1606
+ changes: this.frame.changes,
1607
+ metrics: this.frame.metrics,
1608
+ },
1609
+ capture: this.capture.inspect(),
1610
+ uploadBytes: this.uploadBytes,
1611
+ captureMs: this.lastCaptureMs,
1612
+ uploadMs: this.lastUploadMs,
1613
+ submitMs: this.lastSubmitMs,
1614
+ ownedBytes: [...this.buffers.values()].reduce((sum, entry) => sum + entry.bytes, 0),
1615
+ denoiser: this.denoiser?.inspect(),
1616
+ materialGraphs: {
1617
+ materials: this.graphMaterials,
1618
+ parameterRows: this.graphParameterRows,
1619
+ bufferSlots: {
1620
+ slots: [...this.fragmentBuffers.values()].filter((slot) => !slot.texture).length,
1621
+ gpuCopies: this.fragmentGpuCopies,
1622
+ gpuCopyBytes: this.fragmentGpuCopyBytes,
1623
+ cpuResultUploads: 0,
1624
+ bindings: [...this.fragmentBuffers.values()]
1625
+ .filter((slot) => !slot.texture)
1626
+ .map((slot) => ({
1627
+ material: slot.material,
1628
+ name: slot.name,
1629
+ type: slot.type,
1630
+ count: slot.count,
1631
+ key: slot.key,
1632
+ revision: slot.revision,
1633
+ byteOffset: slot.byteOffset,
1634
+ })),
1635
+ },
1636
+ textureSlots: {
1637
+ slots: [...this.fragmentBuffers.values()].filter((slot) => slot.texture).length,
1638
+ gpuCopies: this.textureGpuCopies,
1639
+ gpuCopyBytes: this.textureGpuCopyBytes,
1640
+ cpuResultUploads: 0,
1641
+ bindings: [...this.fragmentBuffers.values()]
1642
+ .filter((slot) => slot.texture)
1643
+ .map((slot) => ({
1644
+ material: slot.material,
1645
+ name: slot.name,
1646
+ type: slot.type,
1647
+ count: slot.count,
1648
+ key: slot.key,
1649
+ revision: slot.revision,
1650
+ byteOffset: slot.byteOffset,
1651
+ width: slot.texture.width,
1652
+ height: slot.texture.height,
1653
+ })),
1654
+ },
1655
+ },
1656
+ motion: {
1657
+ enabled: this.motionActive,
1658
+ lastComputed: this.lastMotionComputed,
1659
+ passes: this.motionPasses,
1660
+ ...this.motionMatches,
1661
+ },
1662
+ temporal: this.temporal && {
1663
+ ...this.temporal.inspect(),
1664
+ lastReset: this.lastTemporalReset,
1665
+ lastProgressive: this.lastTemporalProgressive,
1666
+ resetReasons: [...this.lastTemporalReasons],
1667
+ historyResets: this.temporalResets,
1668
+ },
1669
+ gpuTiming: this.timestamps
1670
+ ? "timestamp-query; call gpuTimeMs for readback"
1671
+ : "unavailable: adapter timestamp-query not enabled",
1672
+ environment: {
1673
+ index: this.environmentIndex,
1674
+ intensity: this.environmentIntensity,
1675
+ source: this.environmentSource?.uuid ?? null,
1676
+ capture: this.environmentCapture?.inspect(),
1677
+ },
1678
+ limitations: [
1679
+ "Opaque GGX, supported lights/environment MIS and closed dielectric transport are integrated; full physical/lighting quality matrix remains unaccepted.",
1680
+ "CPU surface extraction and BVH refit feed WebGPU traversal. This is not GPU refit or hardware ray tracing.",
1681
+ "Motion reprojection and temporal history filtering are development integrations; complete dynamic/ghosting/physical quality matrices remain unaccepted.",
1682
+ ],
1683
+ memoryScope: "ownedBytes covers tracer storage buffers only. temporal.bytes and denoiser.bytes are additional owned buffers; tracer uniforms, targets, Three resources and CPU caches are separate.",
1684
+ };
1685
+ }
1686
+ dispose() {
1687
+ if (this.disposed)
1688
+ return;
1689
+ this.disposed = true;
1690
+ this.clearFragmentBuffers();
1691
+ this.capture.dispose();
1692
+ this.mediumCatalog?.dispose();
1693
+ this.mediumCatalog = undefined;
1694
+ this.mediumFaces = [];
1695
+ this.mediumIds.clear();
1696
+ this.environmentCapture?.dispose();
1697
+ this.environmentCache.dispose();
1698
+ this.environmentImage?.dispose();
1699
+ this.acceleration.dispose();
1700
+ this.denoiser?.dispose();
1701
+ this.temporal?.dispose();
1702
+ this.temporalOutput = undefined;
1703
+ this.previousSurface = undefined;
1704
+ this.capturedSurface = undefined;
1705
+ this.geometryData = undefined;
1706
+ for (const entry of this.buffers.values())
1707
+ entry.buffer.destroy();
1708
+ this.buffers.clear();
1709
+ this.uniform?.destroy();
1710
+ this.presentUniform?.destroy();
1711
+ this.primary?.destroy();
1712
+ this.depth?.destroy();
1713
+ this.timestamps?.destroy();
1714
+ this.timingResolve?.destroy();
1715
+ if (contextOwners.get(this.canvas) === this) {
1716
+ this.context?.unconfigure();
1717
+ contextOwners.delete(this.canvas);
1718
+ }
1719
+ }
1720
+ }