@plasius/gpu-renderer 0.2.55 → 0.2.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1654,14 +1654,6 @@ function textureSample(binding, textures, label, extra = {}) {
1654
1654
  }
1655
1655
  function cloneMaterialExtensions(material, textures) {
1656
1656
  const extensions = { ...material.extensions ?? {} };
1657
- if (material.workflow === "unlit") {
1658
- const unlitKey = "KHR_materials_unlit";
1659
- const unlitValue = extensions[unlitKey] ?? extensions.unlit ?? {};
1660
- if (typeof unlitValue !== "object" || unlitValue === null || Array.isArray(unlitValue)) {
1661
- throw new TypeError(`Canonical material extension ${unlitKey} must be an object.`);
1662
- }
1663
- extensions[unlitKey] = { ...unlitValue, enabled: true };
1664
- }
1665
1657
  for (const [bindingName, extensionName, textureKey] of EXTENSION_TEXTURE_BINDINGS) {
1666
1658
  const binding = material.textures?.[bindingName];
1667
1659
  if (!binding) {
@@ -1686,7 +1678,10 @@ function mapMaterial(material, materialRefId, textures) {
1686
1678
  if (material.alphaMode === "mask") {
1687
1679
  throw new Error("Canonical alphaMode mask is unsupported by WavefrontMeshInput.");
1688
1680
  }
1689
- if (material.workflow !== "metallic-roughness" && material.workflow !== "unlit") {
1681
+ if (material.workflow === "unlit" || Object.hasOwn(material.extensions ?? {}, "KHR_materials_unlit") || Object.hasOwn(material.extensions ?? {}, "unlit")) {
1682
+ throw new Error("Canonical unlit materials are unsupported by WavefrontMeshInput; retain an unlit-capable rendering path.");
1683
+ }
1684
+ if (material.workflow !== "metallic-roughness") {
1690
1685
  throw new Error(`Unsupported canonical material workflow: ${material.workflow}.`);
1691
1686
  }
1692
1687
  const mapped = {
@@ -4063,948 +4058,1289 @@ function boundsCentroid(bounds) {
4063
4058
  ];
4064
4059
  }
4065
4060
 
4066
- // src/wavefront-sampling.js
4067
- function buildOrthonormalBasis(normal) {
4068
- const tangentFallback = Math.abs(normal[1]) < 0.999 ? [0, 1, 0] : [1, 0, 0];
4069
- const tangent = normalize(cross2(tangentFallback, normal), [1, 0, 0]);
4070
- const bitangent = normalize(cross2(normal, tangent), [0, 0, 1]);
4071
- return { tangent, bitangent };
4072
- }
4073
- function localToWorld(local, normal) {
4074
- const basis = buildOrthonormalBasis(normal);
4075
- return normalize(
4076
- add(
4077
- add(scale(basis.tangent, local[0]), scale(basis.bitangent, local[1])),
4078
- scale(normal, local[2])
4079
- ),
4080
- normal
4081
- );
4082
- }
4083
- function radicalInverseVdc(bits) {
4084
- let value = bits >>> 0;
4085
- value = (value << 16 | value >>> 16) >>> 0;
4086
- value = ((value & 1431655765) << 1 | (value & 2863311530) >>> 1) >>> 0;
4087
- value = ((value & 858993459) << 2 | (value & 3435973836) >>> 2) >>> 0;
4088
- value = ((value & 252645135) << 4 | (value & 4042322160) >>> 4) >>> 0;
4089
- value = ((value & 16711935) << 8 | (value & 4278255360) >>> 8) >>> 0;
4090
- return value * 23283064365386963e-26;
4091
- }
4092
- function hammersley(index, count) {
4093
- return [index / Math.max(count, 1), radicalInverseVdc(index)];
4094
- }
4095
- function importanceSampleGgx(sample, roughness, normal) {
4096
- const alpha = Math.max(roughness * roughness, 1e-4);
4097
- const phi = 2 * Math.PI * sample[0];
4098
- const cosTheta = Math.sqrt((1 - sample[1]) / (1 + (alpha * alpha - 1) * sample[1]));
4099
- const sinTheta = Math.sqrt(Math.max(0, 1 - cosTheta * cosTheta));
4100
- const halfVector = localToWorld(
4101
- [Math.cos(phi) * sinTheta, Math.sin(phi) * sinTheta, cosTheta],
4102
- normal
4103
- );
4104
- return normalize(halfVector, normal);
4105
- }
4106
- function geometrySchlickGgx(nDotV, roughness) {
4107
- const k = (roughness + 1) * (roughness + 1) / 8;
4108
- return nDotV / Math.max(nDotV * (1 - k) + k, 1e-6);
4109
- }
4110
- function geometrySmith(nDotV, nDotL, roughness) {
4111
- return geometrySchlickGgx(nDotV, roughness) * geometrySchlickGgx(nDotL, roughness);
4061
+ // src/wavefront-runtime-support.js
4062
+ var WEBGPU_DEFAULT_MAX_STORAGE_BUFFER_BINDING_SIZE = 128 * 1024 * 1024;
4063
+ var WEBGPU_DEFAULT_MAX_BUFFER_SIZE = 256 * 1024 * 1024;
4064
+ function readRequestedDeviceLimit(source, name) {
4065
+ const rawValue = source?.[name];
4066
+ if (rawValue === void 0 || rawValue === null) {
4067
+ return 0;
4068
+ }
4069
+ const value = Number(rawValue);
4070
+ if (!Number.isSafeInteger(value) || value < 0) {
4071
+ throw new Error(`${name} must be a non-negative safe integer.`);
4072
+ }
4073
+ return value;
4112
4074
  }
4113
- function integrateBrdfSample(nDotV, roughness, sampleCount) {
4114
- const viewDirection = [Math.sqrt(Math.max(0, 1 - nDotV * nDotV)), 0, nDotV];
4115
- const normal = [0, 0, 1];
4116
- let scaleTerm = 0;
4117
- let biasTerm = 0;
4118
- for (let index = 0; index < sampleCount; index += 1) {
4119
- const xi = hammersley(index, sampleCount);
4120
- const halfVector = importanceSampleGgx(xi, roughness, normal);
4121
- const vDotH = Math.max(dot(viewDirection, halfVector), 0);
4122
- const lightDirection = normalize(
4123
- subtract(scale(halfVector, 2 * vDotH), viewDirection),
4124
- normal
4075
+ function assertAdapterSupportsRequiredLimit(adapter, name, requiredValue) {
4076
+ const availableValue = Number(adapter?.limits?.[name]);
4077
+ if (!Number.isFinite(availableValue)) {
4078
+ throw new Error(
4079
+ `Wavefront scene buffers require ${name}>=${requiredValue} bytes, but this adapter does not expose that limit.`
4080
+ );
4081
+ }
4082
+ if (availableValue < requiredValue) {
4083
+ throw new Error(
4084
+ `Wavefront scene buffers require ${name}>=${requiredValue} bytes, but this adapter exposes ${availableValue} bytes.`
4125
4085
  );
4126
- const nDotL = Math.max(lightDirection[2], 0);
4127
- const nDotH = Math.max(halfVector[2], 0);
4128
- if (nDotL <= 0 || nDotH <= 0 || vDotH <= 0) {
4129
- continue;
4130
- }
4131
- const geometry = geometrySmith(nDotV, nDotL, roughness);
4132
- const visibility = geometry * vDotH / Math.max(nDotH * nDotV, 1e-6);
4133
- const fresnel2 = (1 - vDotH) ** 5;
4134
- scaleTerm += (1 - fresnel2) * visibility;
4135
- biasTerm += fresnel2 * visibility;
4136
4086
  }
4137
- return [scaleTerm / sampleCount, biasTerm / sampleCount];
4138
4087
  }
4139
-
4140
- // src/wavefront-gpu-resources.js
4141
- function clampTileSizeForDevice(config, device) {
4142
- const limit = Number(device?.limits?.maxStorageBufferBindingSize);
4143
- if (!Number.isFinite(limit) || limit <= 0) {
4144
- return config.tileSize;
4088
+ function getGpuUsageConstants() {
4089
+ if (typeof GPUBufferUsage === "undefined" || typeof GPUTextureUsage === "undefined" || typeof GPUShaderStage === "undefined") {
4090
+ throw new Error("WebGPU runtime unavailable. Required GPU constants are missing.");
4145
4091
  }
4146
- const maxPixelsByRay = Math.floor(limit / RAY_RECORD_BYTES);
4147
- const maxPixelsByHit = Math.floor(limit / HIT_RECORD_BYTES);
4148
- const maxPixelsByPathVertex = Math.floor(
4149
- limit / ((config.maxDepth + 1) * PATH_VERTEX_RECORD_BYTES)
4150
- );
4151
- const maxPixels = Math.max(
4152
- 256,
4153
- Math.min(maxPixelsByRay, maxPixelsByHit, maxPixelsByPathVertex)
4154
- );
4155
- if (config.tilePixelCapacity <= maxPixels) {
4156
- return config.tileSize;
4092
+ if (typeof GPUBufferUsage.INDIRECT !== "number") {
4093
+ throw new Error("WebGPU runtime unavailable. GPUBufferUsage.INDIRECT is missing.");
4157
4094
  }
4158
- return Math.max(16, Math.floor(Math.sqrt(maxPixels)));
4159
- }
4160
- function createBuffer(device, usage, size, label) {
4161
- return device.createBuffer({
4162
- label,
4163
- size: Math.max(4, size),
4164
- usage
4165
- });
4166
- }
4167
- function alignTo(value, alignment) {
4168
- const resolvedAlignment = Math.max(1, alignment);
4169
- return Math.ceil(value / resolvedAlignment) * resolvedAlignment;
4095
+ return {
4096
+ buffer: GPUBufferUsage,
4097
+ texture: GPUTextureUsage,
4098
+ shader: GPUShaderStage,
4099
+ map: typeof GPUMapMode === "undefined" ? null : GPUMapMode
4100
+ };
4170
4101
  }
4171
- function float32ToFloat16Bits(value) {
4172
- const floatView = new Float32Array(1);
4173
- const intView = new Uint32Array(floatView.buffer);
4174
- floatView[0] = Number.isFinite(value) ? value : 0;
4175
- const x = intView[0];
4176
- const sign = x >> 16 & 32768;
4177
- let mantissa = x & 8388607;
4178
- let exponent = x >> 23 & 255;
4179
- if (exponent === 255) {
4180
- return sign | (mantissa ? 32256 : 31744);
4102
+ function resolveCanvas3(canvasOrSelector, documentRef = globalThis.document) {
4103
+ if (typeof canvasOrSelector === "string") {
4104
+ const resolved = documentRef?.querySelector?.(canvasOrSelector);
4105
+ if (!resolved) {
4106
+ throw new Error(`Unable to find canvas for selector: ${canvasOrSelector}`);
4107
+ }
4108
+ return resolved;
4181
4109
  }
4182
- exponent = exponent - 127 + 15;
4183
- if (exponent >= 31) {
4184
- return sign | 31744;
4110
+ if (canvasOrSelector?.getContext) {
4111
+ return canvasOrSelector;
4185
4112
  }
4186
- if (exponent <= 0) {
4187
- if (exponent < -10) {
4188
- return sign;
4189
- }
4190
- mantissa = (mantissa | 8388608) >> 1 - exponent;
4191
- return sign | mantissa + 4096 >> 13;
4113
+ const fallback = documentRef?.querySelector?.("canvas[data-plasius-wavefront-path-tracing]");
4114
+ if (!fallback) {
4115
+ throw new Error("A canvas is required for WebGPU wavefront path tracing.");
4192
4116
  }
4193
- return sign | exponent << 10 | mantissa + 4096 >> 13;
4117
+ return fallback;
4194
4118
  }
4195
- function environmentMapIntegerScale(data) {
4196
- if (data instanceof Uint8Array) {
4197
- return 1 / 255;
4119
+ async function getPipelineDiagnostics(shaderModule) {
4120
+ if (typeof shaderModule?.getCompilationInfo !== "function") {
4121
+ return "";
4198
4122
  }
4199
- if (data instanceof Uint16Array) {
4200
- return 1 / 65535;
4123
+ try {
4124
+ const info = await shaderModule.getCompilationInfo();
4125
+ const messages = info.messages ?? [];
4126
+ if (messages.length === 0) {
4127
+ return "";
4128
+ }
4129
+ return messages.map((message) => {
4130
+ const line = message.lineNum ?? "?";
4131
+ const column = message.linePos ?? "?";
4132
+ return `line ${line}:${column} ${message.message}`;
4133
+ }).join("\n");
4134
+ } catch {
4135
+ return "";
4201
4136
  }
4202
- return 1;
4203
4137
  }
4204
- function environmentMapHasSamplingData(environmentMap) {
4205
- if (!environmentMap || !environmentMap.data) {
4206
- return false;
4138
+ async function createComputePipeline(device, shaderModule, layout, entryPoint, label) {
4139
+ const descriptor = {
4140
+ label,
4141
+ layout,
4142
+ compute: {
4143
+ module: shaderModule,
4144
+ entryPoint
4145
+ }
4146
+ };
4147
+ try {
4148
+ if (typeof device.createComputePipelineAsync === "function") {
4149
+ return await device.createComputePipelineAsync(descriptor);
4150
+ }
4151
+ return device.createComputePipeline(descriptor);
4152
+ } catch (error) {
4153
+ const diagnostics = await getPipelineDiagnostics(shaderModule);
4154
+ const suffix = diagnostics ? `
4155
+ ${diagnostics}` : "";
4156
+ throw new Error(`WGSL compilation failed for ${label}: ${error.message}${suffix}`, {
4157
+ cause: error
4158
+ });
4207
4159
  }
4208
- const width = Math.max(1, environmentMap.width ?? 1);
4209
- const height = Math.max(1, environmentMap.height ?? 1);
4210
- return environmentMap.data.length >= width * height * 4;
4211
4160
  }
4212
- function createRgba8TextureUpload(source) {
4213
- const width = Math.max(1, Math.trunc(source.width));
4214
- const height = Math.max(1, Math.trunc(source.height));
4215
- const bytesPerRow = alignTo(width * 4, 256);
4216
- const bytes = new Uint8Array(bytesPerRow * height);
4217
- const data = source.data instanceof Uint8Array ? source.data : new Uint8Array(source.data);
4218
- for (let y = 0; y < height; y += 1) {
4219
- const sourceOffset = y * width * 4;
4220
- const targetOffset = y * bytesPerRow;
4221
- bytes.set(data.subarray(sourceOffset, sourceOffset + width * 4), targetOffset);
4161
+ async function assertShaderModuleCompiles(shaderModule, label) {
4162
+ if (typeof shaderModule?.getCompilationInfo !== "function") {
4163
+ return;
4222
4164
  }
4223
- return Object.freeze({
4224
- bytes,
4225
- bytesPerRow,
4226
- width,
4227
- height
4228
- });
4229
- }
4230
- function readEnvironmentMapComponent(data, index, fallback, integerScale = 1) {
4231
- if (!data || index >= data.length) {
4232
- return fallback;
4165
+ const info = await shaderModule.getCompilationInfo();
4166
+ const messages = Array.isArray(info?.messages) ? info.messages : [];
4167
+ const errors = messages.filter((message) => message?.type === "error");
4168
+ if (errors.length <= 0) {
4169
+ return;
4233
4170
  }
4234
- const value = Number(data[index]);
4235
- return Number.isFinite(value) ? Math.max(0, value) * integerScale : fallback;
4171
+ const diagnostics = errors.map((message) => {
4172
+ const line = Number.isFinite(message.lineNum) ? message.lineNum : "?";
4173
+ const column = Number.isFinite(message.linePos) ? message.linePos : "?";
4174
+ return `line ${line}:${column} ${message.message}`;
4175
+ }).join("\n");
4176
+ throw new Error(`WGSL compilation preflight failed for ${label}:
4177
+ ${diagnostics}`);
4236
4178
  }
4237
- function createBrdfLutUploadBytes(size = DEFAULT_BRDF_LUT_SIZE, sampleCount = DEFAULT_BRDF_LUT_SAMPLE_COUNT) {
4238
- const cacheKey = `${Math.max(1, Math.trunc(size))}:${Math.max(1, Math.trunc(sampleCount))}`;
4239
- const cached = BRDF_LUT_UPLOAD_CACHE.get(cacheKey);
4240
- if (cached) {
4241
- return cached;
4242
- }
4243
- const width = Math.max(1, Math.trunc(size));
4244
- const height = Math.max(1, Math.trunc(size));
4245
- const rowBytes = width * 8;
4246
- const bytesPerRow = alignTo(rowBytes, 256);
4247
- const bytes = new Uint8Array(bytesPerRow * height);
4248
- const view = new DataView(bytes.buffer);
4249
- for (let y = 0; y < height; y += 1) {
4250
- const roughness = (y + 0.5) / height;
4251
- for (let x = 0; x < width; x += 1) {
4252
- const nDotV = Math.max((x + 0.5) / width, 1e-4);
4253
- const [scaleTerm, biasTerm] = integrateBrdfSample(nDotV, roughness, sampleCount);
4254
- const offset = y * bytesPerRow + x * 8;
4255
- view.setUint16(offset, float32ToFloat16Bits(scaleTerm), true);
4256
- view.setUint16(offset + 2, float32ToFloat16Bits(biasTerm), true);
4257
- view.setUint16(offset + 4, float32ToFloat16Bits(0), true);
4258
- view.setUint16(offset + 6, float32ToFloat16Bits(1), true);
4259
- }
4260
- }
4261
- const upload = Object.freeze({ bytes, bytesPerRow, width, height });
4262
- BRDF_LUT_UPLOAD_CACHE.set(cacheKey, upload);
4263
- return upload;
4264
- }
4265
- function createLinearEnvironmentPixels(environmentMap, fallbackColor) {
4266
- const width = Math.max(1, environmentMap.width);
4267
- const height = Math.max(1, environmentMap.height);
4268
- const pixels = new Float32Array(width * height * 4);
4269
- const data = environmentMap.data;
4270
- const integerScale = environmentMapIntegerScale(data);
4271
- for (let index = 0; index < width * height; index += 1) {
4272
- const sourceOffset = index * 4;
4273
- const targetOffset = index * 4;
4274
- pixels[targetOffset] = readEnvironmentMapComponent(data, sourceOffset, fallbackColor[0], integerScale);
4275
- pixels[targetOffset + 1] = readEnvironmentMapComponent(data, sourceOffset + 1, fallbackColor[1], integerScale);
4276
- pixels[targetOffset + 2] = readEnvironmentMapComponent(data, sourceOffset + 2, fallbackColor[2], integerScale);
4277
- pixels[targetOffset + 3] = readEnvironmentMapComponent(data, sourceOffset + 3, fallbackColor[3] ?? 1, integerScale);
4179
+ async function createRenderPipeline(device, descriptor) {
4180
+ if (typeof device.createRenderPipelineAsync === "function") {
4181
+ return device.createRenderPipelineAsync(descriptor);
4278
4182
  }
4279
- return pixels;
4280
- }
4281
- function environmentUvToDirection(u, v, rotationRadians = 0) {
4282
- const angle = (u - rotationRadians / (2 * Math.PI) - 0.5) * 2 * Math.PI;
4283
- const theta = v * Math.PI;
4284
- const sinTheta = Math.sin(theta);
4285
- return [
4286
- Math.cos(angle) * sinTheta,
4287
- Math.cos(theta),
4288
- Math.sin(angle) * sinTheta
4289
- ];
4183
+ return device.createRenderPipeline(descriptor);
4290
4184
  }
4291
- function sampleEnvironmentPixelsBilinear(pixels, width, height, u, v) {
4292
- const wrappedU = (u % 1 + 1) % 1;
4293
- const clampedV = clamp2(v, 0, 1);
4294
- const x = wrappedU * width - 0.5;
4295
- const y = clampedV * height - 0.5;
4296
- const x0 = (Math.floor(x) % width + width) % width;
4297
- const y0 = clamp2(Math.floor(y), 0, height - 1);
4298
- const x1 = (x0 + 1) % width;
4299
- const y1 = clamp2(y0 + 1, 0, height - 1);
4300
- const tx = x - Math.floor(x);
4301
- const ty = y - Math.floor(y);
4302
- const read = (px, py) => {
4303
- const offset = (py * width + px) * 4;
4304
- return [pixels[offset], pixels[offset + 1], pixels[offset + 2], pixels[offset + 3]];
4185
+ function createWavefrontDeviceDescriptor(adapter, options = {}, memory = null) {
4186
+ const descriptor = { ...options.deviceDescriptor ?? {} };
4187
+ const descriptorRequiredLimits = { ...descriptor.requiredLimits ?? {} };
4188
+ const requiredLimits = {
4189
+ ...descriptorRequiredLimits,
4190
+ ...options.requiredLimits ?? {}
4305
4191
  };
4306
- const a = read(x0, y0);
4307
- const b = read(x1, y0);
4308
- const c = read(x0, y1);
4309
- const d = read(x1, y1);
4310
- const mixPair = (first, second, factor) => first * (1 - factor) + second * factor;
4311
- return [
4312
- mixPair(mixPair(a[0], b[0], tx), mixPair(c[0], d[0], tx), ty),
4313
- mixPair(mixPair(a[1], b[1], tx), mixPair(c[1], d[1], tx), ty),
4314
- mixPair(mixPair(a[2], b[2], tx), mixPair(c[2], d[2], tx), ty),
4315
- mixPair(mixPair(a[3], b[3], tx), mixPair(c[3], d[3], tx), ty)
4316
- ];
4317
- }
4318
- function directionToEnvironmentUv(direction, rotationRadians = 0) {
4319
- const unitDirection = normalize(direction, [0, 1, 0]);
4320
- const rotationTurns = rotationRadians / (2 * Math.PI);
4321
- const u = ((Math.atan2(unitDirection[2], unitDirection[0]) / (2 * Math.PI) + 0.5 + rotationTurns) % 1 + 1) % 1;
4322
- const v = Math.acos(clamp2(unitDirection[1], -1, 1)) / Math.PI;
4323
- return [u, clamp2(v, 0, 1)];
4324
- }
4325
- function sampleEnvironmentRadiance(pixels, width, height, direction, rotationRadians = 0) {
4326
- const [u, v] = directionToEnvironmentUv(direction, rotationRadians);
4327
- return sampleEnvironmentPixelsBilinear(pixels, width, height, u, v);
4328
- }
4329
- function createFloat16RgbaUploadFromLevels(levels) {
4330
- return levels.map((level) => {
4331
- const rowBytes = level.width * 8;
4332
- const bytesPerRow = alignTo(rowBytes, 256);
4333
- const bytes = new Uint8Array(bytesPerRow * level.height);
4334
- const view = new DataView(bytes.buffer);
4335
- for (let y = 0; y < level.height; y += 1) {
4336
- for (let x = 0; x < level.width; x += 1) {
4337
- const sourceOffset = (y * level.width + x) * 4;
4338
- const targetOffset = y * bytesPerRow + x * 8;
4339
- view.setUint16(targetOffset, float32ToFloat16Bits(level.data[sourceOffset]), true);
4340
- view.setUint16(targetOffset + 2, float32ToFloat16Bits(level.data[sourceOffset + 1]), true);
4341
- view.setUint16(targetOffset + 4, float32ToFloat16Bits(level.data[sourceOffset + 2]), true);
4342
- view.setUint16(targetOffset + 6, float32ToFloat16Bits(level.data[sourceOffset + 3]), true);
4343
- }
4192
+ const traceStorageBindings = TRACE_STORAGE_BUFFER_BINDINGS;
4193
+ const traceTextureBindings = TRACE_SAMPLED_TEXTURE_BINDINGS + (options.instances !== void 0 ? 1 : 0);
4194
+ const exposedStorageBufferLimit = Number(adapter?.limits?.maxStorageBuffersPerShaderStage);
4195
+ if (Number.isFinite(exposedStorageBufferLimit)) {
4196
+ if (exposedStorageBufferLimit < traceStorageBindings) {
4197
+ throw new Error(
4198
+ `Wavefront mesh tracing requires maxStorageBuffersPerShaderStage>=${traceStorageBindings}, but this adapter exposes ${exposedStorageBufferLimit}.`
4199
+ );
4344
4200
  }
4345
- return Object.freeze({ bytes, bytesPerRow, width: level.width, height: level.height });
4346
- });
4347
- }
4348
- function createPrefilteredEnvironmentLevels(environmentMap, fallbackColor) {
4349
- const sourcePixels = createLinearEnvironmentPixels(environmentMap, fallbackColor);
4350
- const sourceWidth = Math.max(1, environmentMap.width);
4351
- const sourceHeight = Math.max(1, environmentMap.height);
4352
- const mipLevelCount = Math.max(1, Math.floor(Math.log2(Math.max(sourceWidth, sourceHeight))) + 1);
4353
- const levels = [
4354
- Object.freeze({
4355
- width: sourceWidth,
4356
- height: sourceHeight,
4357
- data: sourcePixels
4358
- })
4359
- ];
4360
- for (let mipLevel = 1; mipLevel < mipLevelCount; mipLevel += 1) {
4361
- const width = Math.max(1, sourceWidth >> mipLevel);
4362
- const height = Math.max(1, sourceHeight >> mipLevel);
4363
- const roughness = mipLevelCount <= 1 ? 0 : mipLevel / (mipLevelCount - 1);
4364
- const data = new Float32Array(width * height * 4);
4365
- const sampleCount = roughness < 0.25 ? 64 : roughness < 0.6 ? 96 : 128;
4366
- for (let y = 0; y < height; y += 1) {
4367
- for (let x = 0; x < width; x += 1) {
4368
- const direction = environmentUvToDirection((x + 0.5) / width, (y + 0.5) / height, environmentMap.rotationRadians);
4369
- const normal = normalize(direction, [0, 1, 0]);
4370
- const viewDirection = normal;
4371
- let totalWeight = 0;
4372
- const accum = [0, 0, 0];
4373
- for (let sampleIndex = 0; sampleIndex < sampleCount; sampleIndex += 1) {
4374
- const xi = hammersley(sampleIndex, sampleCount);
4375
- const halfVector = importanceSampleGgx(xi, roughness, normal);
4376
- const viewDotHalf = Math.max(dot(viewDirection, halfVector), 0);
4377
- const lightDirection = normalize(
4378
- subtract(scale(halfVector, 2 * viewDotHalf), viewDirection),
4379
- normal
4380
- );
4381
- const nDotL = Math.max(dot(normal, lightDirection), 0);
4382
- if (nDotL <= 1e-6) {
4383
- continue;
4384
- }
4385
- const radiance = sampleEnvironmentRadiance(
4386
- sourcePixels,
4387
- sourceWidth,
4388
- sourceHeight,
4389
- lightDirection,
4390
- environmentMap.rotationRadians
4391
- );
4392
- accum[0] += radiance[0] * nDotL;
4393
- accum[1] += radiance[1] * nDotL;
4394
- accum[2] += radiance[2] * nDotL;
4395
- totalWeight += nDotL;
4396
- }
4397
- const offset = (y * width + x) * 4;
4398
- data[offset] = accum[0] / Math.max(totalWeight, 1e-6);
4399
- data[offset + 1] = accum[1] / Math.max(totalWeight, 1e-6);
4400
- data[offset + 2] = accum[2] / Math.max(totalWeight, 1e-6);
4401
- data[offset + 3] = 1;
4402
- }
4201
+ requiredLimits.maxStorageBuffersPerShaderStage = Math.max(
4202
+ Number(requiredLimits.maxStorageBuffersPerShaderStage ?? 0),
4203
+ traceStorageBindings
4204
+ );
4205
+ }
4206
+ const exposedSampledTextureLimit = Number(adapter?.limits?.maxSampledTexturesPerShaderStage);
4207
+ if (Number.isFinite(exposedSampledTextureLimit)) {
4208
+ if (exposedSampledTextureLimit < traceTextureBindings) {
4209
+ throw new Error(
4210
+ `Wavefront material tracing requires maxSampledTexturesPerShaderStage>=${traceTextureBindings}, but this adapter exposes ${exposedSampledTextureLimit}.`
4211
+ );
4403
4212
  }
4404
- levels.push(Object.freeze({ width, height, data }));
4213
+ requiredLimits.maxSampledTexturesPerShaderStage = Math.max(
4214
+ Number(requiredLimits.maxSampledTexturesPerShaderStage ?? 0),
4215
+ traceTextureBindings
4216
+ );
4405
4217
  }
4406
- return Object.freeze({
4407
- levels,
4408
- mipLevelCount,
4409
- width: sourceWidth,
4410
- height: sourceHeight
4411
- });
4412
- }
4413
- function createEnvironmentSamplingTables(environmentMap, fallbackColor) {
4414
- if (!environmentMapHasSamplingData(environmentMap)) {
4415
- return Object.freeze({
4416
- width: 1,
4417
- height: 1,
4418
- pdf: new Float32Array([1]),
4419
- marginalCdf: new Float32Array([1]),
4420
- conditionalCdf: new Float32Array([1]),
4421
- hasImportanceData: false
4422
- });
4218
+ const sceneStorageBufferBindingBytes = Number(
4219
+ memory?.sceneStorageBufferBindingBytes ?? 0
4220
+ );
4221
+ const requestedStorageBufferBindingSize = Math.max(
4222
+ readRequestedDeviceLimit(descriptorRequiredLimits, "maxStorageBufferBindingSize"),
4223
+ readRequestedDeviceLimit(options.requiredLimits, "maxStorageBufferBindingSize"),
4224
+ sceneStorageBufferBindingBytes > WEBGPU_DEFAULT_MAX_STORAGE_BUFFER_BINDING_SIZE ? sceneStorageBufferBindingBytes : 0
4225
+ );
4226
+ if (requestedStorageBufferBindingSize > 0) {
4227
+ assertAdapterSupportsRequiredLimit(
4228
+ adapter,
4229
+ "maxStorageBufferBindingSize",
4230
+ requestedStorageBufferBindingSize
4231
+ );
4232
+ requiredLimits.maxStorageBufferBindingSize = requestedStorageBufferBindingSize;
4423
4233
  }
4424
- const pixels = createLinearEnvironmentPixels(environmentMap, fallbackColor);
4425
- const width = Math.max(1, environmentMap.width);
4426
- const height = Math.max(1, environmentMap.height);
4427
- const pdf = new Float32Array(width * height);
4428
- const marginalCdf = new Float32Array(height);
4429
- const conditionalCdf = new Float32Array(width * height);
4430
- const rowSums = new Float32Array(height);
4431
- let totalWeight = 0;
4432
- for (let y = 0; y < height; y += 1) {
4433
- const theta = (y + 0.5) / height * Math.PI;
4434
- const sinTheta = Math.max(Math.sin(theta), 1e-4);
4435
- let rowWeight = 0;
4436
- for (let x = 0; x < width; x += 1) {
4437
- const offset = (y * width + x) * 4;
4438
- const luminance = pixels[offset] * 0.2126 + pixels[offset + 1] * 0.7152 + pixels[offset + 2] * 0.0722;
4439
- const weight = Math.max(luminance * sinTheta, 1e-6);
4440
- pdf[y * width + x] = weight;
4441
- rowWeight += weight;
4442
- conditionalCdf[y * width + x] = rowWeight;
4443
- }
4444
- rowSums[y] = rowWeight;
4445
- totalWeight += rowWeight;
4446
- if (rowWeight > 0) {
4447
- for (let x = 0; x < width; x += 1) {
4448
- conditionalCdf[y * width + x] /= rowWeight;
4449
- }
4450
- } else {
4451
- for (let x = 0; x < width; x += 1) {
4452
- conditionalCdf[y * width + x] = (x + 1) / width;
4453
- }
4454
- }
4455
- marginalCdf[y] = totalWeight;
4234
+ const requestedMaxBufferSize = Math.max(
4235
+ readRequestedDeviceLimit(descriptorRequiredLimits, "maxBufferSize"),
4236
+ readRequestedDeviceLimit(options.requiredLimits, "maxBufferSize"),
4237
+ sceneStorageBufferBindingBytes > WEBGPU_DEFAULT_MAX_BUFFER_SIZE ? sceneStorageBufferBindingBytes : 0,
4238
+ requestedStorageBufferBindingSize > WEBGPU_DEFAULT_MAX_BUFFER_SIZE ? requestedStorageBufferBindingSize : 0
4239
+ );
4240
+ if (requestedMaxBufferSize > 0) {
4241
+ assertAdapterSupportsRequiredLimit(adapter, "maxBufferSize", requestedMaxBufferSize);
4242
+ requiredLimits.maxBufferSize = requestedMaxBufferSize;
4456
4243
  }
4457
- for (let y = 0; y < height; y += 1) {
4458
- marginalCdf[y] /= Math.max(totalWeight, 1e-6);
4244
+ const requiredFeatures = Array.from(descriptor.requiredFeatures ?? []);
4245
+ const adapterFeatures = adapter?.features;
4246
+ const timestampQuerySupported = typeof adapterFeatures?.has === "function" ? adapterFeatures.has("timestamp-query") : adapterFeatures && typeof adapterFeatures[Symbol.iterator] === "function" ? Array.from(adapterFeatures).includes("timestamp-query") : false;
4247
+ if (options.gpuTimestamps !== false && timestampQuerySupported && !requiredFeatures.includes("timestamp-query")) {
4248
+ requiredFeatures.push("timestamp-query");
4459
4249
  }
4460
- for (let index = 0; index < pdf.length; index += 1) {
4461
- pdf[index] /= Math.max(totalWeight, 1e-6);
4250
+ if (requiredFeatures.length > 0) {
4251
+ descriptor.requiredFeatures = requiredFeatures;
4462
4252
  }
4463
- return Object.freeze({
4464
- width,
4465
- height,
4466
- pdf,
4467
- marginalCdf,
4468
- conditionalCdf,
4469
- hasImportanceData: true
4470
- });
4471
- }
4472
- function createEnvironmentMapResource(device, constants, environmentMap, fallbackColor) {
4473
- if (environmentMap.view) {
4474
- return Object.freeze({
4475
- view: environmentMap.view,
4476
- sampler: environmentMap.sampler ?? device.createSampler({
4477
- label: "plasius.wavefront.environmentMapSampler",
4478
- addressModeU: "repeat",
4479
- addressModeV: "clamp-to-edge",
4480
- magFilter: "linear",
4481
- minFilter: "linear",
4482
- mipmapFilter: "linear"
4483
- }),
4484
- texture: null,
4485
- ownsTexture: false,
4486
- width: Math.max(1, environmentMap.width),
4487
- height: Math.max(1, environmentMap.height),
4488
- mipLevelCount: Math.max(1, environmentMap.mipLevelCount ?? 1)
4489
- });
4253
+ if (Object.keys(requiredLimits).length > 0) {
4254
+ descriptor.requiredLimits = {
4255
+ ...descriptor.requiredLimits ?? {},
4256
+ ...requiredLimits
4257
+ };
4490
4258
  }
4491
- if (environmentMap.texture && typeof environmentMap.texture.createView === "function") {
4492
- return Object.freeze({
4493
- view: environmentMap.texture.createView(),
4494
- sampler: environmentMap.sampler ?? device.createSampler({
4495
- label: "plasius.wavefront.environmentMapSampler",
4496
- addressModeU: "repeat",
4497
- addressModeV: "clamp-to-edge",
4498
- magFilter: "linear",
4499
- minFilter: "linear",
4500
- mipmapFilter: "linear"
4501
- }),
4502
- texture: environmentMap.texture,
4503
- ownsTexture: false,
4504
- width: Math.max(1, environmentMap.width),
4505
- height: Math.max(1, environmentMap.height),
4506
- mipLevelCount: Math.max(1, environmentMap.mipLevelCount ?? 1)
4507
- });
4259
+ return Object.keys(descriptor).length > 0 ? descriptor : void 0;
4260
+ }
4261
+ function readGpuLimit(adapter, device, name) {
4262
+ const adapterValue = Number(adapter?.limits?.[name]);
4263
+ if (Number.isFinite(adapterValue)) {
4264
+ return adapterValue;
4508
4265
  }
4509
- const prefiltered = createPrefilteredEnvironmentLevels(environmentMap, fallbackColor);
4510
- const uploads = createFloat16RgbaUploadFromLevels(prefiltered.levels);
4511
- const texture = device.createTexture({
4512
- label: environmentMap.enabled ? "plasius.wavefront.environmentMap" : "plasius.wavefront.environmentMapFallback",
4513
- size: { width: prefiltered.width, height: prefiltered.height },
4514
- format: "rgba16float",
4515
- mipLevelCount: prefiltered.mipLevelCount,
4516
- usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
4517
- });
4518
- uploads.forEach((upload, mipLevel) => {
4519
- device.queue.writeTexture(
4520
- { texture, mipLevel },
4521
- upload.bytes,
4522
- { bytesPerRow: upload.bytesPerRow, rowsPerImage: upload.height },
4523
- { width: upload.width, height: upload.height, depthOrArrayLayers: 1 }
4524
- );
4525
- });
4526
- return Object.freeze({
4527
- view: texture.createView(),
4528
- sampler: environmentMap.sampler ?? device.createSampler({
4529
- label: "plasius.wavefront.environmentMapSampler",
4530
- addressModeU: "repeat",
4531
- addressModeV: "clamp-to-edge",
4532
- magFilter: "linear",
4533
- minFilter: "linear",
4534
- mipmapFilter: "linear"
4535
- }),
4536
- texture,
4537
- ownsTexture: true,
4538
- width: prefiltered.width,
4539
- height: prefiltered.height,
4540
- mipLevelCount: prefiltered.mipLevelCount
4541
- });
4266
+ const deviceValue = Number(device?.limits?.[name]);
4267
+ return Number.isFinite(deviceValue) ? deviceValue : null;
4542
4268
  }
4543
- function createEnvironmentSamplingTextureResource(device, constants, environmentMap, fallbackColor) {
4544
- const tables = createEnvironmentSamplingTables(environmentMap, fallbackColor);
4545
- const rowBytes = tables.width * 8;
4546
- const bytesPerRow = alignTo(rowBytes, 256);
4547
- const bytes = new Uint8Array(bytesPerRow * tables.height);
4548
- const view = new DataView(bytes.buffer);
4549
- for (let y = 0; y < tables.height; y += 1) {
4550
- for (let x = 0; x < tables.width; x += 1) {
4551
- const probability = tables.pdf[y * tables.width + x];
4552
- const conditional = tables.conditionalCdf[y * tables.width + x];
4553
- const marginal = tables.marginalCdf[y];
4554
- const offset = y * bytesPerRow + x * 8;
4555
- view.setUint16(offset, float32ToFloat16Bits(probability), true);
4556
- view.setUint16(offset + 2, float32ToFloat16Bits(conditional), true);
4557
- view.setUint16(offset + 4, float32ToFloat16Bits(marginal), true);
4558
- view.setUint16(offset + 6, float32ToFloat16Bits(1), true);
4559
- }
4269
+ function createAdapterInfoSnapshot(adapter) {
4270
+ const info = adapter?.info;
4271
+ if (!info || typeof info !== "object") {
4272
+ return null;
4560
4273
  }
4561
- const texture = device.createTexture({
4562
- label: "plasius.wavefront.environmentSampling",
4563
- size: { width: tables.width, height: tables.height },
4564
- format: "rgba16float",
4565
- usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
4566
- });
4567
- device.queue.writeTexture(
4568
- { texture },
4569
- bytes,
4570
- { bytesPerRow, rowsPerImage: tables.height },
4571
- { width: tables.width, height: tables.height, depthOrArrayLayers: 1 }
4572
- );
4573
4274
  return Object.freeze({
4574
- view: texture.createView(),
4575
- texture,
4576
- ownsTexture: true,
4577
- hasImportanceData: tables.hasImportanceData
4275
+ vendor: typeof info.vendor === "string" ? info.vendor : "",
4276
+ architecture: typeof info.architecture === "string" ? info.architecture : "",
4277
+ device: typeof info.device === "string" ? info.device : "",
4278
+ description: typeof info.description === "string" ? info.description : ""
4578
4279
  });
4579
4280
  }
4580
- function createBrdfLutResource(device, constants, size = DEFAULT_BRDF_LUT_SIZE) {
4581
- const upload = createBrdfLutUploadBytes(size);
4582
- const texture = device.createTexture({
4583
- label: "plasius.wavefront.brdfLut",
4584
- size: { width: upload.width, height: upload.height },
4585
- format: "rgba16float",
4586
- usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
4587
- });
4588
- device.queue.writeTexture(
4589
- { texture },
4590
- upload.bytes,
4591
- { bytesPerRow: upload.bytesPerRow, rowsPerImage: upload.height },
4592
- { width: upload.width, height: upload.height, depthOrArrayLayers: 1 }
4593
- );
4281
+ function createGpuAdapterParallelismDiagnostics(adapter, device) {
4594
4282
  return Object.freeze({
4595
- view: texture.createView(),
4596
- sampler: device.createSampler({
4597
- label: "plasius.wavefront.brdfLutSampler",
4598
- addressModeU: "clamp-to-edge",
4599
- addressModeV: "clamp-to-edge",
4600
- magFilter: "linear",
4601
- minFilter: "linear"
4283
+ physicalCoreCount: null,
4284
+ physicalCoreCountAvailable: false,
4285
+ physicalCoreCountUnavailableReason: "WebGPU does not expose physical GPU core counts.",
4286
+ adapterInfo: createAdapterInfoSnapshot(adapter),
4287
+ adapterLimits: Object.freeze({
4288
+ maxComputeInvocationsPerWorkgroup: readGpuLimit(adapter, device, "maxComputeInvocationsPerWorkgroup"),
4289
+ maxComputeWorkgroupSizeX: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeX"),
4290
+ maxComputeWorkgroupSizeY: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeY"),
4291
+ maxComputeWorkgroupSizeZ: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeZ"),
4292
+ maxComputeWorkgroupsPerDimension: readGpuLimit(adapter, device, "maxComputeWorkgroupsPerDimension"),
4293
+ maxStorageBuffersPerShaderStage: readGpuLimit(adapter, device, "maxStorageBuffersPerShaderStage"),
4294
+ maxStorageBufferBindingSize: readGpuLimit(adapter, device, "maxStorageBufferBindingSize"),
4295
+ maxBufferSize: readGpuLimit(adapter, device, "maxBufferSize")
4602
4296
  }),
4603
- texture,
4604
- ownsTexture: true,
4605
- width: upload.width,
4606
- height: upload.height
4297
+ configuredWorkgroupSize: WORKGROUP_SIZE
4607
4298
  });
4608
4299
  }
4609
- function createMediumTextureResource(device, constants, mediums) {
4610
- const normalized = Array.isArray(mediums) && mediums.length > 0 ? mediums : [{ id: 0 }];
4611
- const width = Math.max(
4300
+ function createEnvironmentMapSnapshot(environmentMap) {
4301
+ return Object.freeze({
4302
+ enabled: environmentMap.enabled,
4303
+ width: environmentMap.width,
4304
+ height: environmentMap.height,
4305
+ mipLevelCount: environmentMap.mipLevelCount ?? 1,
4306
+ projection: environmentMap.projection,
4307
+ intensity: environmentMap.intensity,
4308
+ rotationRadians: environmentMap.rotationRadians,
4309
+ ambientStrength: environmentMap.ambientStrength,
4310
+ hasImportanceData: environmentMap.hasImportanceData === true
4311
+ });
4312
+ }
4313
+ function nowMs3() {
4314
+ if (typeof performance !== "undefined" && typeof performance.now === "function") {
4315
+ return performance.now();
4316
+ }
4317
+ return Date.now();
4318
+ }
4319
+ function estimateAccelerationBuildWaitFactor(config) {
4320
+ if (config?.gpuAccelerationBuildRequired !== true) {
4321
+ return 1;
4322
+ }
4323
+ const bvhSortStageCount = Array.isArray(config?.bvhSortStages) ? config.bvhSortStages.length : 0;
4324
+ const bvhBuildLevelCount = Array.isArray(config?.bvhBuildLevels) ? config.bvhBuildLevels.length : 0;
4325
+ const accelerationStageCount = 2 + bvhSortStageCount + bvhBuildLevelCount;
4326
+ return Math.max(1, 1 + accelerationStageCount / 96);
4327
+ }
4328
+ function estimateSubmittedGpuWorkTiming(config, tileCount, overrideTimeoutMs = null, options = {}) {
4329
+ if (Number.isFinite(overrideTimeoutMs)) {
4330
+ const overrideMs = Math.max(1, Math.trunc(Number(overrideTimeoutMs)));
4331
+ return Object.freeze({
4332
+ timeoutMs: overrideMs,
4333
+ maxWaitMs: overrideMs
4334
+ });
4335
+ }
4336
+ const samplesPerPixel = Math.max(
4612
4337
  1,
4613
- normalized.reduce((maximum, medium) => Math.max(maximum, medium.id ?? 0), 0) + 1
4338
+ Number(config?.renderedSamplesPerPixel ?? config?.samplesPerPixel ?? 1)
4614
4339
  );
4615
- const level = {
4616
- width,
4617
- height: MEDIUM_TABLE_ROWS,
4618
- data: new Float32Array(width * MEDIUM_TABLE_ROWS * 4)
4619
- };
4620
- for (const medium of normalized) {
4621
- const mediumId = Math.max(0, Math.trunc(Number(medium.id) || 0));
4622
- const absorptionOffset = mediumId * 4;
4623
- level.data[absorptionOffset] = Math.max(0, medium.absorption?.[0] ?? 0);
4624
- level.data[absorptionOffset + 1] = Math.max(0, medium.absorption?.[1] ?? 0);
4625
- level.data[absorptionOffset + 2] = Math.max(0, medium.absorption?.[2] ?? 0);
4626
- level.data[absorptionOffset + 3] = Math.max(0, medium.phaseModel ?? 0);
4627
- const scatteringOffset = (width + mediumId) * 4;
4628
- level.data[scatteringOffset] = Math.max(0, medium.scattering?.[0] ?? 0);
4629
- level.data[scatteringOffset + 1] = Math.max(0, medium.scattering?.[1] ?? 0);
4630
- level.data[scatteringOffset + 2] = Math.max(0, medium.scattering?.[2] ?? 0);
4631
- level.data[scatteringOffset + 3] = Math.max(0, medium.density ?? 0);
4632
- }
4633
- const upload = createFloat16RgbaUploadFromLevels([level])[0];
4634
- const texture = device.createTexture({
4635
- label: "plasius.wavefront.mediumTable",
4636
- size: { width, height: MEDIUM_TABLE_ROWS },
4637
- format: "rgba16float",
4638
- usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
4639
- });
4640
- device.queue.writeTexture(
4641
- { texture },
4642
- upload.bytes,
4643
- { bytesPerRow: upload.bytesPerRow, rowsPerImage: upload.height },
4644
- { width, height: MEDIUM_TABLE_ROWS, depthOrArrayLayers: 1 }
4340
+ const maxDepth = Math.max(1, Number(config?.maxDepth ?? 1));
4341
+ const deferredResolvePasses = config?.deferredPathResolve ? 1 : 0;
4342
+ const denoisePasses = config?.denoise ? samplesPerPixel < 4 ? 2 : 1 : 0;
4343
+ const tiles = Math.max(1, Number(tileCount ?? 1));
4344
+ const estimatedPasses = tiles * (samplesPerPixel * (maxDepth + 1 + deferredResolvePasses) + denoisePasses + 1);
4345
+ const triangleCount = Math.max(0, Number(config?.triangleCount ?? 0));
4346
+ const geometryFactor = Math.max(1, triangleCount / 131072);
4347
+ const includeAccelerationBuild = options.includeAccelerationBuild === true;
4348
+ const accelerationFactor = includeAccelerationBuild ? estimateAccelerationBuildWaitFactor(config) : 1;
4349
+ const estimatedWindowMs = Math.round(
4350
+ (GPU_SUBMITTED_WORK_TIMEOUT_MS + estimatedPasses * 5) * geometryFactor * accelerationFactor
4351
+ );
4352
+ const timeoutMs = Math.min(
4353
+ GPU_MAX_SUBMITTED_WORK_TIMEOUT_MS,
4354
+ Math.max(GPU_SUBMITTED_WORK_TIMEOUT_MS, estimatedWindowMs)
4355
+ );
4356
+ const maxWaitMultiplier = includeAccelerationBuild ? 3 : 2;
4357
+ const maxWaitMs = Math.min(
4358
+ GPU_MAX_SUBMITTED_WORK_DEADLINE_MS,
4359
+ Math.max(timeoutMs, estimatedWindowMs * maxWaitMultiplier)
4645
4360
  );
4646
4361
  return Object.freeze({
4647
- texture,
4648
- view: texture.createView(),
4649
- ownsTexture: true,
4650
- count: normalized.length,
4651
- width
4362
+ timeoutMs,
4363
+ maxWaitMs
4652
4364
  });
4653
4365
  }
4654
- function mediumTablesEqual(left, right) {
4655
- const leftMediums = Array.isArray(left) ? left : [];
4656
- const rightMediums = Array.isArray(right) ? right : [];
4657
- if (leftMediums.length !== rightMediums.length) {
4658
- return false;
4366
+
4367
+ // src/wavefront-gpu-synchronization.js
4368
+ async function waitForSubmittedGpuWork(device, options = {}) {
4369
+ if (typeof device.queue.onSubmittedWorkDone !== "function") {
4370
+ return true;
4659
4371
  }
4660
- for (let index = 0; index < leftMediums.length; index += 1) {
4661
- const leftMedium = leftMediums[index];
4662
- const rightMedium = rightMediums[index];
4663
- if ((leftMedium?.id ?? 0) !== (rightMedium?.id ?? 0)) {
4664
- return false;
4665
- }
4666
- if ((leftMedium?.phaseModel ?? 0) !== (rightMedium?.phaseModel ?? 0)) {
4667
- return false;
4372
+ const timeoutMs = Math.max(
4373
+ 1,
4374
+ Number.isFinite(options.timeoutMs) ? Number(options.timeoutMs) : GPU_SUBMITTED_WORK_TIMEOUT_MS
4375
+ );
4376
+ const maxWaitMs = Math.max(
4377
+ timeoutMs,
4378
+ Number.isFinite(options.maxWaitMs) ? Number(options.maxWaitMs) : timeoutMs
4379
+ );
4380
+ const allowTimeout = options.allowTimeout !== false;
4381
+ const completionPromise = device.queue.onSubmittedWorkDone().then(
4382
+ () => ({ status: "done" }),
4383
+ (error) => {
4384
+ throw error;
4668
4385
  }
4669
- if ((leftMedium?.density ?? 0) !== (rightMedium?.density ?? 0)) {
4386
+ );
4387
+ const lossPromise = typeof device.lost?.then === "function" ? device.lost.then((info) => {
4388
+ throw new Error(
4389
+ `WebGPU device lost while waiting for submitted work (${info?.reason ?? "unknown"}).`
4390
+ );
4391
+ }) : null;
4392
+ const startedAtMs = nowMs3();
4393
+ while (true) {
4394
+ const elapsedMs = Math.max(0, nowMs3() - startedAtMs);
4395
+ const remainingMs = Math.max(0, maxWaitMs - elapsedMs);
4396
+ if (remainingMs <= 0) {
4397
+ if (!allowTimeout) {
4398
+ throw new Error(`Timed out after ${Math.round(maxWaitMs)} ms waiting for submitted GPU work.`);
4399
+ }
4400
+ console.warn(
4401
+ `[plasius.wavefront] Submitted GPU work did not report completion within ${Math.round(maxWaitMs)} ms; continuing.`
4402
+ );
4670
4403
  return false;
4671
4404
  }
4672
- for (let component = 0; component < 3; component += 1) {
4673
- if ((leftMedium?.absorption?.[component] ?? 0) !== (rightMedium?.absorption?.[component] ?? 0)) {
4674
- return false;
4405
+ const waitWindowMs = Math.max(1, Math.min(timeoutMs, remainingMs));
4406
+ let timeoutHandle = null;
4407
+ let resolveTimeoutPromise = null;
4408
+ let timeoutSettled = false;
4409
+ const settleTimeoutPromise = (value) => {
4410
+ if (timeoutSettled) {
4411
+ return;
4675
4412
  }
4676
- if ((leftMedium?.scattering?.[component] ?? 0) !== (rightMedium?.scattering?.[component] ?? 0)) {
4677
- return false;
4413
+ timeoutSettled = true;
4414
+ resolveTimeoutPromise?.(value);
4415
+ };
4416
+ const timeoutPromise = new Promise((resolve) => {
4417
+ resolveTimeoutPromise = resolve;
4418
+ timeoutHandle = setTimeout(
4419
+ () => settleTimeoutPromise({ status: "timeout" }),
4420
+ waitWindowMs
4421
+ );
4422
+ });
4423
+ let result;
4424
+ try {
4425
+ result = await Promise.race(
4426
+ [completionPromise, timeoutPromise, lossPromise].filter(Boolean)
4427
+ );
4428
+ } finally {
4429
+ if (timeoutHandle !== null) {
4430
+ clearTimeout(timeoutHandle);
4431
+ settleTimeoutPromise({ status: "cancelled" });
4432
+ }
4433
+ }
4434
+ if (result?.status === "done") {
4435
+ return true;
4436
+ }
4437
+ if (result?.status !== "timeout") {
4438
+ return true;
4439
+ }
4440
+ }
4441
+ }
4442
+
4443
+ // src/wavefront-upload.js
4444
+ async function uploadWavefrontBuffer(device, buffer, data, { chunkBytes = 8 * 1024 * 1024 } = {}) {
4445
+ if (!Number.isSafeInteger(chunkBytes) || chunkBytes < 4 || chunkBytes % 4 !== 0) throw new Error("GPU upload chunk size must be a positive multiple of four.");
4446
+ if (data.byteLength <= chunkBytes) {
4447
+ device.queue.writeBuffer(buffer, 0, data);
4448
+ return;
4449
+ }
4450
+ for (let offset = 0; offset < data.byteLength; offset += chunkBytes) {
4451
+ device.queue.writeBuffer(buffer, offset, data, offset, Math.min(chunkBytes, data.byteLength - offset));
4452
+ await waitForSubmittedGpuWork(device, { timeoutMs: 1e4, maxWaitMs: 1e4, allowTimeout: false });
4453
+ }
4454
+ }
4455
+
4456
+ // src/gpu-allocation-budget.js
4457
+ var formats = Object.freeze({
4458
+ r8unorm: 1,
4459
+ r8snorm: 1,
4460
+ r8uint: 1,
4461
+ r8sint: 1,
4462
+ rg8unorm: 2,
4463
+ rg8snorm: 2,
4464
+ rg8uint: 2,
4465
+ rg8sint: 2,
4466
+ r16float: 2,
4467
+ r16uint: 2,
4468
+ r16sint: 2,
4469
+ rgba8unorm: 4,
4470
+ "rgba8unorm-srgb": 4,
4471
+ bgra8unorm: 4,
4472
+ "bgra8unorm-srgb": 4,
4473
+ rgba8snorm: 4,
4474
+ rgba8uint: 4,
4475
+ rgba8sint: 4,
4476
+ rg16float: 4,
4477
+ rg16uint: 4,
4478
+ rg16sint: 4,
4479
+ r32float: 4,
4480
+ r32uint: 4,
4481
+ r32sint: 4,
4482
+ rgb10a2unorm: 4,
4483
+ rg11b10ufloat: 4,
4484
+ rgba16float: 8,
4485
+ rgba16uint: 8,
4486
+ rgba16sint: 8,
4487
+ rg32float: 8,
4488
+ rg32uint: 8,
4489
+ rg32sint: 8,
4490
+ rgba32float: 16,
4491
+ rgba32uint: 16,
4492
+ rgba32sint: 16
4493
+ });
4494
+ function integer(value, name, minimum = 1) {
4495
+ if (!Number.isSafeInteger(value) || value < minimum) throw new Error(`${name} must be a safe integer >= ${minimum}.`);
4496
+ return value;
4497
+ }
4498
+ function within(value, limit, name) {
4499
+ if (Number.isFinite(limit) && value > limit) throw new Error(`GPU ${name} limit exceeded: requested ${value}, available ${limit}.`);
4500
+ }
4501
+ function textureBytes(descriptor, limits) {
4502
+ const size = descriptor.size;
4503
+ const array = Array.isArray(size) || ArrayBuffer.isView(size);
4504
+ const width = integer(array ? size[0] : size?.width, "Texture width");
4505
+ const height = integer((array ? size[1] : size?.height) ?? 1, "Texture height");
4506
+ const layers = integer((array ? size[2] : size?.depthOrArrayLayers) ?? 1, "Texture depth/layers");
4507
+ const dimension2 = descriptor.dimension ?? "2d";
4508
+ if (!["1d", "2d", "3d"].includes(dimension2)) throw new Error("Unsupported GPU texture dimension.");
4509
+ const limit = limits[dimension2 === "3d" ? "maxTextureDimension3D" : dimension2 === "1d" ? "maxTextureDimension1D" : "maxTextureDimension2D"];
4510
+ within(Math.max(width, height, dimension2 === "3d" ? layers : 1), limit, "texture dimension");
4511
+ if (dimension2 !== "3d") within(layers, limits.maxTextureArrayLayers, "texture layers");
4512
+ const bpp = formats[descriptor.format];
4513
+ if (!bpp) throw new Error(`GPU memory accounting does not support texture format ${descriptor.format}.`);
4514
+ const levels = integer(descriptor.mipLevelCount ?? 1, "Mip count");
4515
+ const samples = integer(descriptor.sampleCount ?? 1, "Sample count");
4516
+ if (levels > Math.floor(Math.log2(Math.max(width, height, dimension2 === "3d" ? layers : 1))) + 1) throw new Error("Invalid texture mip count.");
4517
+ let bytes = 0;
4518
+ for (let level = 0; level < levels; level++) bytes += Math.max(1, Math.floor(width / 2 ** level)) * Math.max(1, Math.floor(height / 2 ** level)) * (dimension2 === "3d" ? Math.max(1, Math.floor(layers / 2 ** level)) : layers) * bpp * samples;
4519
+ return integer(bytes, "Texture allocation");
4520
+ }
4521
+ function createGpuAllocationTracker(rawDevice, { budgetBytes, storageUsage = globalThis.GPUBufferUsage?.STORAGE ?? 128 } = {}) {
4522
+ if (budgetBytes !== void 0) integer(budgetBytes, "GPU memory budget");
4523
+ const records = /* @__PURE__ */ new Map();
4524
+ let allocatedBytes = 0, peakAllocatedBytes = 0;
4525
+ function allocate(kind, descriptor, bytes, create) {
4526
+ integer(bytes, "GPU allocation", 0);
4527
+ const total = integer(allocatedBytes + bytes, "GPU allocation total", 0);
4528
+ if (budgetBytes !== void 0 && total > budgetBytes) throw new Error(`GPU memory budget exceeded: ${total} requested bytes, budget ${budgetBytes}. Reduce quality or scene resources.`);
4529
+ const resource = create(descriptor), originalDestroy = resource.destroy.bind(resource);
4530
+ const release = () => {
4531
+ if (!records.has(resource)) return;
4532
+ originalDestroy();
4533
+ records.delete(resource);
4534
+ allocatedBytes -= bytes;
4535
+ };
4536
+ try {
4537
+ Object.defineProperty(resource, "destroy", { value: release, configurable: true });
4538
+ } catch (error) {
4539
+ originalDestroy();
4540
+ throw error;
4541
+ }
4542
+ records.set(resource, { kind, label: descriptor.label ?? "", bytes, release });
4543
+ allocatedBytes = total;
4544
+ peakAllocatedBytes = Math.max(total, peakAllocatedBytes);
4545
+ return resource;
4546
+ }
4547
+ const destroy = () => {
4548
+ for (const record of [...records.values()]) record.release();
4549
+ };
4550
+ const methods = {
4551
+ createBuffer(descriptor) {
4552
+ const bytes = integer(descriptor.size, "Buffer size", 0);
4553
+ within(bytes, rawDevice.limits?.maxBufferSize, "buffer size");
4554
+ if ((descriptor.usage ?? 0) & storageUsage) within(bytes, rawDevice.limits?.maxStorageBufferBindingSize, "storage binding size");
4555
+ return allocate("buffer", descriptor, bytes, (d) => rawDevice.createBuffer(d));
4556
+ },
4557
+ createTexture(descriptor) {
4558
+ return allocate("texture", descriptor, textureBytes(descriptor, rawDevice.limits ?? {}), (d) => rawDevice.createTexture(d));
4559
+ },
4560
+ createQuerySet(descriptor) {
4561
+ return allocate("query-set", descriptor, integer(descriptor.count, "Query count") * 8, (d) => rawDevice.createQuerySet(d));
4562
+ },
4563
+ destroy() {
4564
+ destroy();
4565
+ rawDevice.destroy();
4566
+ }
4567
+ };
4568
+ const device = new Proxy(rawDevice, { get(target, key) {
4569
+ if (Object.hasOwn(methods, key)) return methods[key];
4570
+ const value = Reflect.get(target, key, target);
4571
+ return typeof value === "function" ? value.bind(target) : value;
4572
+ } });
4573
+ return Object.freeze({ device, destroy, snapshot: () => Object.freeze({
4574
+ accounting: "requested-resource-bytes",
4575
+ budgetBytes: budgetBytes ?? null,
4576
+ allocatedBytes,
4577
+ peakAllocatedBytes,
4578
+ resources: Object.freeze([...records.values()].map(({ kind, label, bytes }) => Object.freeze({ kind, label, bytes })))
4579
+ }) });
4580
+ }
4581
+ function openGpuAllocationErrorScopes(device) {
4582
+ if (typeof device.pushErrorScope !== "function" || typeof device.popErrorScope !== "function") return async () => {
4583
+ };
4584
+ let count = 0;
4585
+ try {
4586
+ device.pushErrorScope("out-of-memory");
4587
+ count++;
4588
+ device.pushErrorScope("validation");
4589
+ count++;
4590
+ } catch (error) {
4591
+ while (count-- > 0) Promise.resolve(device.popErrorScope()).catch(() => {
4592
+ });
4593
+ throw error;
4594
+ }
4595
+ return async () => {
4596
+ const completions = [];
4597
+ while (count > 0) {
4598
+ count--;
4599
+ try {
4600
+ completions.push(Promise.resolve(device.popErrorScope()));
4601
+ } catch (error) {
4602
+ completions.push(Promise.reject(error));
4678
4603
  }
4679
4604
  }
4680
- }
4681
- return true;
4605
+ if (!completions.length) return;
4606
+ let timer;
4607
+ try {
4608
+ const errors = await Promise.race([Promise.all(completions), new Promise((_, reject) => {
4609
+ timer = setTimeout(() => reject(new Error("GPU allocation validation timed out.")), 1e4);
4610
+ })]);
4611
+ const error = errors.find(Boolean);
4612
+ if (error) throw new Error(`GPU allocation failed: ${error.message ?? String(error)}`);
4613
+ } finally {
4614
+ clearTimeout(timer);
4615
+ }
4616
+ };
4682
4617
  }
4683
- function createAtlasTextureResource(device, constants, atlas, label) {
4684
- const upload = createRgba8TextureUpload(atlas);
4685
- const texture = device.createTexture({
4686
- label,
4687
- size: { width: upload.width, height: upload.height },
4688
- format: "rgba8unorm",
4689
- usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
4690
- });
4691
- device.queue.writeTexture(
4692
- { texture },
4693
- upload.bytes,
4694
- { bytesPerRow: upload.bytesPerRow, rowsPerImage: upload.height },
4695
- { width: upload.width, height: upload.height, depthOrArrayLayers: 1 }
4618
+
4619
+ // src/wavefront-sampling.js
4620
+ function buildOrthonormalBasis(normal) {
4621
+ const tangentFallback = Math.abs(normal[1]) < 0.999 ? [0, 1, 0] : [1, 0, 0];
4622
+ const tangent = normalize(cross2(tangentFallback, normal), [1, 0, 0]);
4623
+ const bitangent = normalize(cross2(normal, tangent), [0, 0, 1]);
4624
+ return { tangent, bitangent };
4625
+ }
4626
+ function localToWorld(local, normal) {
4627
+ const basis = buildOrthonormalBasis(normal);
4628
+ return normalize(
4629
+ add(
4630
+ add(scale(basis.tangent, local[0]), scale(basis.bitangent, local[1])),
4631
+ scale(normal, local[2])
4632
+ ),
4633
+ normal
4696
4634
  );
4697
- return Object.freeze({
4698
- texture,
4699
- view: texture.createView(),
4700
- ownsTexture: true
4701
- });
4702
4635
  }
4703
-
4704
- // src/wavefront-runtime-support.js
4705
- var WEBGPU_DEFAULT_MAX_STORAGE_BUFFER_BINDING_SIZE = 128 * 1024 * 1024;
4706
- var WEBGPU_DEFAULT_MAX_BUFFER_SIZE = 256 * 1024 * 1024;
4707
- function readRequestedDeviceLimit(source, name) {
4708
- const rawValue = source?.[name];
4709
- if (rawValue === void 0 || rawValue === null) {
4710
- return 0;
4711
- }
4712
- const value = Number(rawValue);
4713
- if (!Number.isSafeInteger(value) || value < 0) {
4714
- throw new Error(`${name} must be a non-negative safe integer.`);
4715
- }
4716
- return value;
4636
+ function radicalInverseVdc(bits) {
4637
+ let value = bits >>> 0;
4638
+ value = (value << 16 | value >>> 16) >>> 0;
4639
+ value = ((value & 1431655765) << 1 | (value & 2863311530) >>> 1) >>> 0;
4640
+ value = ((value & 858993459) << 2 | (value & 3435973836) >>> 2) >>> 0;
4641
+ value = ((value & 252645135) << 4 | (value & 4042322160) >>> 4) >>> 0;
4642
+ value = ((value & 16711935) << 8 | (value & 4278255360) >>> 8) >>> 0;
4643
+ return value * 23283064365386963e-26;
4717
4644
  }
4718
- function assertAdapterSupportsRequiredLimit(adapter, name, requiredValue) {
4719
- const availableValue = Number(adapter?.limits?.[name]);
4720
- if (!Number.isFinite(availableValue)) {
4721
- throw new Error(
4722
- `Wavefront scene buffers require ${name}>=${requiredValue} bytes, but this adapter does not expose that limit.`
4723
- );
4724
- }
4725
- if (availableValue < requiredValue) {
4726
- throw new Error(
4727
- `Wavefront scene buffers require ${name}>=${requiredValue} bytes, but this adapter exposes ${availableValue} bytes.`
4645
+ function hammersley(index, count) {
4646
+ return [index / Math.max(count, 1), radicalInverseVdc(index)];
4647
+ }
4648
+ function importanceSampleGgx(sample, roughness, normal) {
4649
+ const alpha = Math.max(roughness * roughness, 1e-4);
4650
+ const phi = 2 * Math.PI * sample[0];
4651
+ const cosTheta = Math.sqrt((1 - sample[1]) / (1 + (alpha * alpha - 1) * sample[1]));
4652
+ const sinTheta = Math.sqrt(Math.max(0, 1 - cosTheta * cosTheta));
4653
+ const halfVector = localToWorld(
4654
+ [Math.cos(phi) * sinTheta, Math.sin(phi) * sinTheta, cosTheta],
4655
+ normal
4656
+ );
4657
+ return normalize(halfVector, normal);
4658
+ }
4659
+ function geometrySchlickGgx(nDotV, roughness) {
4660
+ const k = (roughness + 1) * (roughness + 1) / 8;
4661
+ return nDotV / Math.max(nDotV * (1 - k) + k, 1e-6);
4662
+ }
4663
+ function geometrySmith(nDotV, nDotL, roughness) {
4664
+ return geometrySchlickGgx(nDotV, roughness) * geometrySchlickGgx(nDotL, roughness);
4665
+ }
4666
+ function integrateBrdfSample(nDotV, roughness, sampleCount) {
4667
+ const viewDirection = [Math.sqrt(Math.max(0, 1 - nDotV * nDotV)), 0, nDotV];
4668
+ const normal = [0, 0, 1];
4669
+ let scaleTerm = 0;
4670
+ let biasTerm = 0;
4671
+ for (let index = 0; index < sampleCount; index += 1) {
4672
+ const xi = hammersley(index, sampleCount);
4673
+ const halfVector = importanceSampleGgx(xi, roughness, normal);
4674
+ const vDotH = Math.max(dot(viewDirection, halfVector), 0);
4675
+ const lightDirection = normalize(
4676
+ subtract(scale(halfVector, 2 * vDotH), viewDirection),
4677
+ normal
4728
4678
  );
4679
+ const nDotL = Math.max(lightDirection[2], 0);
4680
+ const nDotH = Math.max(halfVector[2], 0);
4681
+ if (nDotL <= 0 || nDotH <= 0 || vDotH <= 0) {
4682
+ continue;
4683
+ }
4684
+ const geometry = geometrySmith(nDotV, nDotL, roughness);
4685
+ const visibility = geometry * vDotH / Math.max(nDotH * nDotV, 1e-6);
4686
+ const fresnel2 = (1 - vDotH) ** 5;
4687
+ scaleTerm += (1 - fresnel2) * visibility;
4688
+ biasTerm += fresnel2 * visibility;
4729
4689
  }
4690
+ return [scaleTerm / sampleCount, biasTerm / sampleCount];
4730
4691
  }
4731
- function getGpuUsageConstants() {
4732
- if (typeof GPUBufferUsage === "undefined" || typeof GPUTextureUsage === "undefined" || typeof GPUShaderStage === "undefined") {
4733
- throw new Error("WebGPU runtime unavailable. Required GPU constants are missing.");
4692
+
4693
+ // src/wavefront-gpu-resources.js
4694
+ function clampTileSizeForDevice(config, device) {
4695
+ const limit = Number(device?.limits?.maxStorageBufferBindingSize);
4696
+ if (!Number.isFinite(limit) || limit <= 0) {
4697
+ return config.tileSize;
4734
4698
  }
4735
- if (typeof GPUBufferUsage.INDIRECT !== "number") {
4736
- throw new Error("WebGPU runtime unavailable. GPUBufferUsage.INDIRECT is missing.");
4699
+ const maxPixelsByRay = Math.floor(limit / RAY_RECORD_BYTES);
4700
+ const maxPixelsByHit = Math.floor(limit / HIT_RECORD_BYTES);
4701
+ const maxPixelsByPathVertex = Math.floor(
4702
+ limit / ((config.maxDepth + 1) * PATH_VERTEX_RECORD_BYTES)
4703
+ );
4704
+ const maxPixels = Math.max(
4705
+ 256,
4706
+ Math.min(maxPixelsByRay, maxPixelsByHit, maxPixelsByPathVertex)
4707
+ );
4708
+ if (config.tilePixelCapacity <= maxPixels) {
4709
+ return config.tileSize;
4737
4710
  }
4738
- return {
4739
- buffer: GPUBufferUsage,
4740
- texture: GPUTextureUsage,
4741
- shader: GPUShaderStage,
4742
- map: typeof GPUMapMode === "undefined" ? null : GPUMapMode
4743
- };
4711
+ return Math.max(16, Math.floor(Math.sqrt(maxPixels)));
4744
4712
  }
4745
- function resolveCanvas3(canvasOrSelector, documentRef = globalThis.document) {
4746
- if (typeof canvasOrSelector === "string") {
4747
- const resolved = documentRef?.querySelector?.(canvasOrSelector);
4748
- if (!resolved) {
4749
- throw new Error(`Unable to find canvas for selector: ${canvasOrSelector}`);
4750
- }
4751
- return resolved;
4713
+ function createBuffer(device, usage, size, label) {
4714
+ return device.createBuffer({
4715
+ label,
4716
+ size: Math.max(4, size),
4717
+ usage
4718
+ });
4719
+ }
4720
+ function alignTo(value, alignment) {
4721
+ const resolvedAlignment = Math.max(1, alignment);
4722
+ return Math.ceil(value / resolvedAlignment) * resolvedAlignment;
4723
+ }
4724
+ function float32ToFloat16Bits(value) {
4725
+ const floatView = new Float32Array(1);
4726
+ const intView = new Uint32Array(floatView.buffer);
4727
+ floatView[0] = Number.isFinite(value) ? value : 0;
4728
+ const x = intView[0];
4729
+ const sign = x >> 16 & 32768;
4730
+ let mantissa = x & 8388607;
4731
+ let exponent = x >> 23 & 255;
4732
+ if (exponent === 255) {
4733
+ return sign | (mantissa ? 32256 : 31744);
4752
4734
  }
4753
- if (canvasOrSelector?.getContext) {
4754
- return canvasOrSelector;
4735
+ exponent = exponent - 127 + 15;
4736
+ if (exponent >= 31) {
4737
+ return sign | 31744;
4755
4738
  }
4756
- const fallback = documentRef?.querySelector?.("canvas[data-plasius-wavefront-path-tracing]");
4757
- if (!fallback) {
4758
- throw new Error("A canvas is required for WebGPU wavefront path tracing.");
4739
+ if (exponent <= 0) {
4740
+ if (exponent < -10) {
4741
+ return sign;
4742
+ }
4743
+ mantissa = (mantissa | 8388608) >> 1 - exponent;
4744
+ return sign | mantissa + 4096 >> 13;
4759
4745
  }
4760
- return fallback;
4746
+ return sign | exponent << 10 | mantissa + 4096 >> 13;
4761
4747
  }
4762
- async function getPipelineDiagnostics(shaderModule) {
4763
- if (typeof shaderModule?.getCompilationInfo !== "function") {
4764
- return "";
4748
+ function environmentMapIntegerScale(data) {
4749
+ if (data instanceof Uint8Array) {
4750
+ return 1 / 255;
4765
4751
  }
4766
- try {
4767
- const info = await shaderModule.getCompilationInfo();
4768
- const messages = info.messages ?? [];
4769
- if (messages.length === 0) {
4770
- return "";
4771
- }
4772
- return messages.map((message) => {
4773
- const line = message.lineNum ?? "?";
4774
- const column = message.linePos ?? "?";
4775
- return `line ${line}:${column} ${message.message}`;
4776
- }).join("\n");
4777
- } catch {
4778
- return "";
4752
+ if (data instanceof Uint16Array) {
4753
+ return 1 / 65535;
4779
4754
  }
4755
+ return 1;
4780
4756
  }
4781
- async function createComputePipeline(device, shaderModule, layout, entryPoint, label) {
4782
- const descriptor = {
4783
- label,
4784
- layout,
4785
- compute: {
4786
- module: shaderModule,
4787
- entryPoint
4788
- }
4789
- };
4790
- try {
4791
- if (typeof device.createComputePipelineAsync === "function") {
4792
- return await device.createComputePipelineAsync(descriptor);
4793
- }
4794
- return device.createComputePipeline(descriptor);
4795
- } catch (error) {
4796
- const diagnostics = await getPipelineDiagnostics(shaderModule);
4797
- const suffix = diagnostics ? `
4798
- ${diagnostics}` : "";
4799
- throw new Error(`WGSL compilation failed for ${label}: ${error.message}${suffix}`, {
4800
- cause: error
4801
- });
4757
+ function environmentMapHasSamplingData(environmentMap) {
4758
+ if (!environmentMap || !environmentMap.data) {
4759
+ return false;
4760
+ }
4761
+ const width = Math.max(1, environmentMap.width ?? 1);
4762
+ const height = Math.max(1, environmentMap.height ?? 1);
4763
+ return environmentMap.data.length >= width * height * 4;
4764
+ }
4765
+ function createRgba8TextureUpload(source) {
4766
+ const width = Math.max(1, Math.trunc(source.width));
4767
+ const height = Math.max(1, Math.trunc(source.height));
4768
+ const bytesPerRow = alignTo(width * 4, 256);
4769
+ const bytes = new Uint8Array(bytesPerRow * height);
4770
+ const data = source.data instanceof Uint8Array ? source.data : new Uint8Array(source.data);
4771
+ for (let y = 0; y < height; y += 1) {
4772
+ const sourceOffset = y * width * 4;
4773
+ const targetOffset = y * bytesPerRow;
4774
+ bytes.set(data.subarray(sourceOffset, sourceOffset + width * 4), targetOffset);
4775
+ }
4776
+ return Object.freeze({
4777
+ bytes,
4778
+ bytesPerRow,
4779
+ width,
4780
+ height
4781
+ });
4782
+ }
4783
+ function readEnvironmentMapComponent(data, index, fallback, integerScale = 1) {
4784
+ if (!data || index >= data.length) {
4785
+ return fallback;
4802
4786
  }
4787
+ const value = Number(data[index]);
4788
+ return Number.isFinite(value) ? Math.max(0, value) * integerScale : fallback;
4803
4789
  }
4804
- async function assertShaderModuleCompiles(shaderModule, label) {
4805
- if (typeof shaderModule?.getCompilationInfo !== "function") {
4806
- return;
4790
+ function createBrdfLutUploadBytes(size = DEFAULT_BRDF_LUT_SIZE, sampleCount = DEFAULT_BRDF_LUT_SAMPLE_COUNT) {
4791
+ const cacheKey = `${Math.max(1, Math.trunc(size))}:${Math.max(1, Math.trunc(sampleCount))}`;
4792
+ const cached = BRDF_LUT_UPLOAD_CACHE.get(cacheKey);
4793
+ if (cached) {
4794
+ return cached;
4807
4795
  }
4808
- const info = await shaderModule.getCompilationInfo();
4809
- const messages = Array.isArray(info?.messages) ? info.messages : [];
4810
- const errors = messages.filter((message) => message?.type === "error");
4811
- if (errors.length <= 0) {
4812
- return;
4796
+ const width = Math.max(1, Math.trunc(size));
4797
+ const height = Math.max(1, Math.trunc(size));
4798
+ const rowBytes = width * 8;
4799
+ const bytesPerRow = alignTo(rowBytes, 256);
4800
+ const bytes = new Uint8Array(bytesPerRow * height);
4801
+ const view = new DataView(bytes.buffer);
4802
+ for (let y = 0; y < height; y += 1) {
4803
+ const roughness = (y + 0.5) / height;
4804
+ for (let x = 0; x < width; x += 1) {
4805
+ const nDotV = Math.max((x + 0.5) / width, 1e-4);
4806
+ const [scaleTerm, biasTerm] = integrateBrdfSample(nDotV, roughness, sampleCount);
4807
+ const offset = y * bytesPerRow + x * 8;
4808
+ view.setUint16(offset, float32ToFloat16Bits(scaleTerm), true);
4809
+ view.setUint16(offset + 2, float32ToFloat16Bits(biasTerm), true);
4810
+ view.setUint16(offset + 4, float32ToFloat16Bits(0), true);
4811
+ view.setUint16(offset + 6, float32ToFloat16Bits(1), true);
4812
+ }
4813
4813
  }
4814
- const diagnostics = errors.map((message) => {
4815
- const line = Number.isFinite(message.lineNum) ? message.lineNum : "?";
4816
- const column = Number.isFinite(message.linePos) ? message.linePos : "?";
4817
- return `line ${line}:${column} ${message.message}`;
4818
- }).join("\n");
4819
- throw new Error(`WGSL compilation preflight failed for ${label}:
4820
- ${diagnostics}`);
4814
+ const upload = Object.freeze({ bytes, bytesPerRow, width, height });
4815
+ BRDF_LUT_UPLOAD_CACHE.set(cacheKey, upload);
4816
+ return upload;
4821
4817
  }
4822
- async function createRenderPipeline(device, descriptor) {
4823
- if (typeof device.createRenderPipelineAsync === "function") {
4824
- return device.createRenderPipelineAsync(descriptor);
4818
+ function createLinearEnvironmentPixels(environmentMap, fallbackColor) {
4819
+ const width = Math.max(1, environmentMap.width);
4820
+ const height = Math.max(1, environmentMap.height);
4821
+ const pixels = new Float32Array(width * height * 4);
4822
+ const data = environmentMap.data;
4823
+ const integerScale = environmentMapIntegerScale(data);
4824
+ for (let index = 0; index < width * height; index += 1) {
4825
+ const sourceOffset = index * 4;
4826
+ const targetOffset = index * 4;
4827
+ pixels[targetOffset] = readEnvironmentMapComponent(data, sourceOffset, fallbackColor[0], integerScale);
4828
+ pixels[targetOffset + 1] = readEnvironmentMapComponent(data, sourceOffset + 1, fallbackColor[1], integerScale);
4829
+ pixels[targetOffset + 2] = readEnvironmentMapComponent(data, sourceOffset + 2, fallbackColor[2], integerScale);
4830
+ pixels[targetOffset + 3] = readEnvironmentMapComponent(data, sourceOffset + 3, fallbackColor[3] ?? 1, integerScale);
4825
4831
  }
4826
- return device.createRenderPipeline(descriptor);
4832
+ return pixels;
4827
4833
  }
4828
- function createWavefrontDeviceDescriptor(adapter, options = {}, memory = null) {
4829
- const descriptor = { ...options.deviceDescriptor ?? {} };
4830
- const descriptorRequiredLimits = { ...descriptor.requiredLimits ?? {} };
4831
- const requiredLimits = {
4832
- ...descriptorRequiredLimits,
4833
- ...options.requiredLimits ?? {}
4834
+ function environmentUvToDirection(u, v, rotationRadians = 0) {
4835
+ const angle = (u - rotationRadians / (2 * Math.PI) - 0.5) * 2 * Math.PI;
4836
+ const theta = v * Math.PI;
4837
+ const sinTheta = Math.sin(theta);
4838
+ return [
4839
+ Math.cos(angle) * sinTheta,
4840
+ Math.cos(theta),
4841
+ Math.sin(angle) * sinTheta
4842
+ ];
4843
+ }
4844
+ function sampleEnvironmentPixelsBilinear(pixels, width, height, u, v) {
4845
+ const wrappedU = (u % 1 + 1) % 1;
4846
+ const clampedV = clamp2(v, 0, 1);
4847
+ const x = wrappedU * width - 0.5;
4848
+ const y = clampedV * height - 0.5;
4849
+ const x0 = (Math.floor(x) % width + width) % width;
4850
+ const y0 = clamp2(Math.floor(y), 0, height - 1);
4851
+ const x1 = (x0 + 1) % width;
4852
+ const y1 = clamp2(y0 + 1, 0, height - 1);
4853
+ const tx = x - Math.floor(x);
4854
+ const ty = y - Math.floor(y);
4855
+ const read = (px, py) => {
4856
+ const offset = (py * width + px) * 4;
4857
+ return [pixels[offset], pixels[offset + 1], pixels[offset + 2], pixels[offset + 3]];
4834
4858
  };
4835
- const exposedStorageBufferLimit = Number(adapter?.limits?.maxStorageBuffersPerShaderStage);
4836
- if (Number.isFinite(exposedStorageBufferLimit)) {
4837
- if (exposedStorageBufferLimit < TRACE_STORAGE_BUFFER_BINDINGS) {
4838
- throw new Error(
4839
- `Wavefront mesh tracing requires maxStorageBuffersPerShaderStage>=${TRACE_STORAGE_BUFFER_BINDINGS}, but this adapter exposes ${exposedStorageBufferLimit}.`
4840
- );
4859
+ const a = read(x0, y0);
4860
+ const b = read(x1, y0);
4861
+ const c = read(x0, y1);
4862
+ const d = read(x1, y1);
4863
+ const mixPair = (first, second, factor) => first * (1 - factor) + second * factor;
4864
+ return [
4865
+ mixPair(mixPair(a[0], b[0], tx), mixPair(c[0], d[0], tx), ty),
4866
+ mixPair(mixPair(a[1], b[1], tx), mixPair(c[1], d[1], tx), ty),
4867
+ mixPair(mixPair(a[2], b[2], tx), mixPair(c[2], d[2], tx), ty),
4868
+ mixPair(mixPair(a[3], b[3], tx), mixPair(c[3], d[3], tx), ty)
4869
+ ];
4870
+ }
4871
+ function directionToEnvironmentUv(direction, rotationRadians = 0) {
4872
+ const unitDirection = normalize(direction, [0, 1, 0]);
4873
+ const rotationTurns = rotationRadians / (2 * Math.PI);
4874
+ const u = ((Math.atan2(unitDirection[2], unitDirection[0]) / (2 * Math.PI) + 0.5 + rotationTurns) % 1 + 1) % 1;
4875
+ const v = Math.acos(clamp2(unitDirection[1], -1, 1)) / Math.PI;
4876
+ return [u, clamp2(v, 0, 1)];
4877
+ }
4878
+ function sampleEnvironmentRadiance(pixels, width, height, direction, rotationRadians = 0) {
4879
+ const [u, v] = directionToEnvironmentUv(direction, rotationRadians);
4880
+ return sampleEnvironmentPixelsBilinear(pixels, width, height, u, v);
4881
+ }
4882
+ function createFloat16RgbaUploadFromLevels(levels) {
4883
+ return levels.map((level) => {
4884
+ const rowBytes = level.width * 8;
4885
+ const bytesPerRow = alignTo(rowBytes, 256);
4886
+ const bytes = new Uint8Array(bytesPerRow * level.height);
4887
+ const view = new DataView(bytes.buffer);
4888
+ for (let y = 0; y < level.height; y += 1) {
4889
+ for (let x = 0; x < level.width; x += 1) {
4890
+ const sourceOffset = (y * level.width + x) * 4;
4891
+ const targetOffset = y * bytesPerRow + x * 8;
4892
+ view.setUint16(targetOffset, float32ToFloat16Bits(level.data[sourceOffset]), true);
4893
+ view.setUint16(targetOffset + 2, float32ToFloat16Bits(level.data[sourceOffset + 1]), true);
4894
+ view.setUint16(targetOffset + 4, float32ToFloat16Bits(level.data[sourceOffset + 2]), true);
4895
+ view.setUint16(targetOffset + 6, float32ToFloat16Bits(level.data[sourceOffset + 3]), true);
4896
+ }
4841
4897
  }
4842
- requiredLimits.maxStorageBuffersPerShaderStage = Math.max(
4843
- Number(requiredLimits.maxStorageBuffersPerShaderStage ?? 0),
4844
- TRACE_STORAGE_BUFFER_BINDINGS
4845
- );
4846
- }
4847
- const exposedSampledTextureLimit = Number(adapter?.limits?.maxSampledTexturesPerShaderStage);
4848
- if (Number.isFinite(exposedSampledTextureLimit)) {
4849
- if (exposedSampledTextureLimit < TRACE_SAMPLED_TEXTURE_BINDINGS) {
4850
- throw new Error(
4851
- `Wavefront material tracing requires maxSampledTexturesPerShaderStage>=${TRACE_SAMPLED_TEXTURE_BINDINGS}, but this adapter exposes ${exposedSampledTextureLimit}.`
4852
- );
4898
+ return Object.freeze({ bytes, bytesPerRow, width: level.width, height: level.height });
4899
+ });
4900
+ }
4901
+ function createPrefilteredEnvironmentLevels(environmentMap, fallbackColor) {
4902
+ const sourcePixels = createLinearEnvironmentPixels(environmentMap, fallbackColor);
4903
+ const sourceWidth = Math.max(1, environmentMap.width);
4904
+ const sourceHeight = Math.max(1, environmentMap.height);
4905
+ const mipLevelCount = Math.max(1, Math.floor(Math.log2(Math.max(sourceWidth, sourceHeight))) + 1);
4906
+ const levels = [
4907
+ Object.freeze({
4908
+ width: sourceWidth,
4909
+ height: sourceHeight,
4910
+ data: sourcePixels
4911
+ })
4912
+ ];
4913
+ for (let mipLevel = 1; mipLevel < mipLevelCount; mipLevel += 1) {
4914
+ const width = Math.max(1, sourceWidth >> mipLevel);
4915
+ const height = Math.max(1, sourceHeight >> mipLevel);
4916
+ const roughness = mipLevelCount <= 1 ? 0 : mipLevel / (mipLevelCount - 1);
4917
+ const data = new Float32Array(width * height * 4);
4918
+ const sampleCount = roughness < 0.25 ? 64 : roughness < 0.6 ? 96 : 128;
4919
+ for (let y = 0; y < height; y += 1) {
4920
+ for (let x = 0; x < width; x += 1) {
4921
+ const direction = environmentUvToDirection((x + 0.5) / width, (y + 0.5) / height, environmentMap.rotationRadians);
4922
+ const normal = normalize(direction, [0, 1, 0]);
4923
+ const viewDirection = normal;
4924
+ let totalWeight = 0;
4925
+ const accum = [0, 0, 0];
4926
+ for (let sampleIndex = 0; sampleIndex < sampleCount; sampleIndex += 1) {
4927
+ const xi = hammersley(sampleIndex, sampleCount);
4928
+ const halfVector = importanceSampleGgx(xi, roughness, normal);
4929
+ const viewDotHalf = Math.max(dot(viewDirection, halfVector), 0);
4930
+ const lightDirection = normalize(
4931
+ subtract(scale(halfVector, 2 * viewDotHalf), viewDirection),
4932
+ normal
4933
+ );
4934
+ const nDotL = Math.max(dot(normal, lightDirection), 0);
4935
+ if (nDotL <= 1e-6) {
4936
+ continue;
4937
+ }
4938
+ const radiance = sampleEnvironmentRadiance(
4939
+ sourcePixels,
4940
+ sourceWidth,
4941
+ sourceHeight,
4942
+ lightDirection,
4943
+ environmentMap.rotationRadians
4944
+ );
4945
+ accum[0] += radiance[0] * nDotL;
4946
+ accum[1] += radiance[1] * nDotL;
4947
+ accum[2] += radiance[2] * nDotL;
4948
+ totalWeight += nDotL;
4949
+ }
4950
+ const offset = (y * width + x) * 4;
4951
+ data[offset] = accum[0] / Math.max(totalWeight, 1e-6);
4952
+ data[offset + 1] = accum[1] / Math.max(totalWeight, 1e-6);
4953
+ data[offset + 2] = accum[2] / Math.max(totalWeight, 1e-6);
4954
+ data[offset + 3] = 1;
4955
+ }
4853
4956
  }
4854
- requiredLimits.maxSampledTexturesPerShaderStage = Math.max(
4855
- Number(requiredLimits.maxSampledTexturesPerShaderStage ?? 0),
4856
- TRACE_SAMPLED_TEXTURE_BINDINGS
4857
- );
4858
- }
4859
- const sceneStorageBufferBindingBytes = Number(
4860
- memory?.sceneStorageBufferBindingBytes ?? 0
4861
- );
4862
- const requestedStorageBufferBindingSize = Math.max(
4863
- readRequestedDeviceLimit(descriptorRequiredLimits, "maxStorageBufferBindingSize"),
4864
- readRequestedDeviceLimit(options.requiredLimits, "maxStorageBufferBindingSize"),
4865
- sceneStorageBufferBindingBytes > WEBGPU_DEFAULT_MAX_STORAGE_BUFFER_BINDING_SIZE ? sceneStorageBufferBindingBytes : 0
4866
- );
4867
- if (requestedStorageBufferBindingSize > 0) {
4868
- assertAdapterSupportsRequiredLimit(
4869
- adapter,
4870
- "maxStorageBufferBindingSize",
4871
- requestedStorageBufferBindingSize
4872
- );
4873
- requiredLimits.maxStorageBufferBindingSize = requestedStorageBufferBindingSize;
4874
- }
4875
- const requestedMaxBufferSize = Math.max(
4876
- readRequestedDeviceLimit(descriptorRequiredLimits, "maxBufferSize"),
4877
- readRequestedDeviceLimit(options.requiredLimits, "maxBufferSize"),
4878
- sceneStorageBufferBindingBytes > WEBGPU_DEFAULT_MAX_BUFFER_SIZE ? sceneStorageBufferBindingBytes : 0,
4879
- requestedStorageBufferBindingSize > WEBGPU_DEFAULT_MAX_BUFFER_SIZE ? requestedStorageBufferBindingSize : 0
4880
- );
4881
- if (requestedMaxBufferSize > 0) {
4882
- assertAdapterSupportsRequiredLimit(adapter, "maxBufferSize", requestedMaxBufferSize);
4883
- requiredLimits.maxBufferSize = requestedMaxBufferSize;
4957
+ levels.push(Object.freeze({ width, height, data }));
4884
4958
  }
4885
- const requiredFeatures = Array.from(descriptor.requiredFeatures ?? []);
4886
- const adapterFeatures = adapter?.features;
4887
- const timestampQuerySupported = typeof adapterFeatures?.has === "function" ? adapterFeatures.has("timestamp-query") : adapterFeatures && typeof adapterFeatures[Symbol.iterator] === "function" ? Array.from(adapterFeatures).includes("timestamp-query") : false;
4888
- if (options.gpuTimestamps !== false && timestampQuerySupported && !requiredFeatures.includes("timestamp-query")) {
4889
- requiredFeatures.push("timestamp-query");
4959
+ return Object.freeze({
4960
+ levels,
4961
+ mipLevelCount,
4962
+ width: sourceWidth,
4963
+ height: sourceHeight
4964
+ });
4965
+ }
4966
+ function createEnvironmentSamplingTables(environmentMap, fallbackColor) {
4967
+ if (!environmentMapHasSamplingData(environmentMap)) {
4968
+ return Object.freeze({
4969
+ width: 1,
4970
+ height: 1,
4971
+ pdf: new Float32Array([1]),
4972
+ marginalCdf: new Float32Array([1]),
4973
+ conditionalCdf: new Float32Array([1]),
4974
+ hasImportanceData: false
4975
+ });
4890
4976
  }
4891
- if (requiredFeatures.length > 0) {
4892
- descriptor.requiredFeatures = requiredFeatures;
4977
+ const pixels = createLinearEnvironmentPixels(environmentMap, fallbackColor);
4978
+ const width = Math.max(1, environmentMap.width);
4979
+ const height = Math.max(1, environmentMap.height);
4980
+ const pdf = new Float32Array(width * height);
4981
+ const marginalCdf = new Float32Array(height);
4982
+ const conditionalCdf = new Float32Array(width * height);
4983
+ const rowSums = new Float32Array(height);
4984
+ let totalWeight = 0;
4985
+ for (let y = 0; y < height; y += 1) {
4986
+ const theta = (y + 0.5) / height * Math.PI;
4987
+ const sinTheta = Math.max(Math.sin(theta), 1e-4);
4988
+ let rowWeight = 0;
4989
+ for (let x = 0; x < width; x += 1) {
4990
+ const offset = (y * width + x) * 4;
4991
+ const luminance = pixels[offset] * 0.2126 + pixels[offset + 1] * 0.7152 + pixels[offset + 2] * 0.0722;
4992
+ const weight = Math.max(luminance * sinTheta, 1e-6);
4993
+ pdf[y * width + x] = weight;
4994
+ rowWeight += weight;
4995
+ conditionalCdf[y * width + x] = rowWeight;
4996
+ }
4997
+ rowSums[y] = rowWeight;
4998
+ totalWeight += rowWeight;
4999
+ if (rowWeight > 0) {
5000
+ for (let x = 0; x < width; x += 1) {
5001
+ conditionalCdf[y * width + x] /= rowWeight;
5002
+ }
5003
+ } else {
5004
+ for (let x = 0; x < width; x += 1) {
5005
+ conditionalCdf[y * width + x] = (x + 1) / width;
5006
+ }
5007
+ }
5008
+ marginalCdf[y] = totalWeight;
4893
5009
  }
4894
- if (Object.keys(requiredLimits).length > 0) {
4895
- descriptor.requiredLimits = {
4896
- ...descriptor.requiredLimits ?? {},
4897
- ...requiredLimits
4898
- };
5010
+ for (let y = 0; y < height; y += 1) {
5011
+ marginalCdf[y] /= Math.max(totalWeight, 1e-6);
4899
5012
  }
4900
- return Object.keys(descriptor).length > 0 ? descriptor : void 0;
5013
+ for (let index = 0; index < pdf.length; index += 1) {
5014
+ pdf[index] /= Math.max(totalWeight, 1e-6);
5015
+ }
5016
+ return Object.freeze({
5017
+ width,
5018
+ height,
5019
+ pdf,
5020
+ marginalCdf,
5021
+ conditionalCdf,
5022
+ hasImportanceData: true
5023
+ });
4901
5024
  }
4902
- function readGpuLimit(adapter, device, name) {
4903
- const adapterValue = Number(adapter?.limits?.[name]);
4904
- if (Number.isFinite(adapterValue)) {
4905
- return adapterValue;
5025
+ function createEnvironmentMapResource(device, constants, environmentMap, fallbackColor) {
5026
+ if (environmentMap.view) {
5027
+ return Object.freeze({
5028
+ view: environmentMap.view,
5029
+ sampler: environmentMap.sampler ?? device.createSampler({
5030
+ label: "plasius.wavefront.environmentMapSampler",
5031
+ addressModeU: "repeat",
5032
+ addressModeV: "clamp-to-edge",
5033
+ magFilter: "linear",
5034
+ minFilter: "linear",
5035
+ mipmapFilter: "linear"
5036
+ }),
5037
+ texture: null,
5038
+ ownsTexture: false,
5039
+ width: Math.max(1, environmentMap.width),
5040
+ height: Math.max(1, environmentMap.height),
5041
+ mipLevelCount: Math.max(1, environmentMap.mipLevelCount ?? 1)
5042
+ });
4906
5043
  }
4907
- const deviceValue = Number(device?.limits?.[name]);
4908
- return Number.isFinite(deviceValue) ? deviceValue : null;
5044
+ if (environmentMap.texture && typeof environmentMap.texture.createView === "function") {
5045
+ return Object.freeze({
5046
+ view: environmentMap.texture.createView(),
5047
+ sampler: environmentMap.sampler ?? device.createSampler({
5048
+ label: "plasius.wavefront.environmentMapSampler",
5049
+ addressModeU: "repeat",
5050
+ addressModeV: "clamp-to-edge",
5051
+ magFilter: "linear",
5052
+ minFilter: "linear",
5053
+ mipmapFilter: "linear"
5054
+ }),
5055
+ texture: environmentMap.texture,
5056
+ ownsTexture: false,
5057
+ width: Math.max(1, environmentMap.width),
5058
+ height: Math.max(1, environmentMap.height),
5059
+ mipLevelCount: Math.max(1, environmentMap.mipLevelCount ?? 1)
5060
+ });
5061
+ }
5062
+ const prefiltered = createPrefilteredEnvironmentLevels(environmentMap, fallbackColor);
5063
+ const uploads = createFloat16RgbaUploadFromLevels(prefiltered.levels);
5064
+ const texture = device.createTexture({
5065
+ label: environmentMap.enabled ? "plasius.wavefront.environmentMap" : "plasius.wavefront.environmentMapFallback",
5066
+ size: { width: prefiltered.width, height: prefiltered.height },
5067
+ format: "rgba16float",
5068
+ mipLevelCount: prefiltered.mipLevelCount,
5069
+ usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
5070
+ });
5071
+ uploads.forEach((upload, mipLevel) => {
5072
+ device.queue.writeTexture(
5073
+ { texture, mipLevel },
5074
+ upload.bytes,
5075
+ { bytesPerRow: upload.bytesPerRow, rowsPerImage: upload.height },
5076
+ { width: upload.width, height: upload.height, depthOrArrayLayers: 1 }
5077
+ );
5078
+ });
5079
+ return Object.freeze({
5080
+ view: texture.createView(),
5081
+ sampler: environmentMap.sampler ?? device.createSampler({
5082
+ label: "plasius.wavefront.environmentMapSampler",
5083
+ addressModeU: "repeat",
5084
+ addressModeV: "clamp-to-edge",
5085
+ magFilter: "linear",
5086
+ minFilter: "linear",
5087
+ mipmapFilter: "linear"
5088
+ }),
5089
+ texture,
5090
+ ownsTexture: true,
5091
+ width: prefiltered.width,
5092
+ height: prefiltered.height,
5093
+ mipLevelCount: prefiltered.mipLevelCount
5094
+ });
4909
5095
  }
4910
- function createAdapterInfoSnapshot(adapter) {
4911
- const info = adapter?.info;
4912
- if (!info || typeof info !== "object") {
4913
- return null;
5096
+ function createEnvironmentSamplingTextureResource(device, constants, environmentMap, fallbackColor) {
5097
+ const tables = createEnvironmentSamplingTables(environmentMap, fallbackColor);
5098
+ const rowBytes = tables.width * 8;
5099
+ const bytesPerRow = alignTo(rowBytes, 256);
5100
+ const bytes = new Uint8Array(bytesPerRow * tables.height);
5101
+ const view = new DataView(bytes.buffer);
5102
+ for (let y = 0; y < tables.height; y += 1) {
5103
+ for (let x = 0; x < tables.width; x += 1) {
5104
+ const probability = tables.pdf[y * tables.width + x];
5105
+ const conditional = tables.conditionalCdf[y * tables.width + x];
5106
+ const marginal = tables.marginalCdf[y];
5107
+ const offset = y * bytesPerRow + x * 8;
5108
+ view.setUint16(offset, float32ToFloat16Bits(probability), true);
5109
+ view.setUint16(offset + 2, float32ToFloat16Bits(conditional), true);
5110
+ view.setUint16(offset + 4, float32ToFloat16Bits(marginal), true);
5111
+ view.setUint16(offset + 6, float32ToFloat16Bits(1), true);
5112
+ }
4914
5113
  }
5114
+ const texture = device.createTexture({
5115
+ label: "plasius.wavefront.environmentSampling",
5116
+ size: { width: tables.width, height: tables.height },
5117
+ format: "rgba16float",
5118
+ usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
5119
+ });
5120
+ device.queue.writeTexture(
5121
+ { texture },
5122
+ bytes,
5123
+ { bytesPerRow, rowsPerImage: tables.height },
5124
+ { width: tables.width, height: tables.height, depthOrArrayLayers: 1 }
5125
+ );
4915
5126
  return Object.freeze({
4916
- vendor: typeof info.vendor === "string" ? info.vendor : "",
4917
- architecture: typeof info.architecture === "string" ? info.architecture : "",
4918
- device: typeof info.device === "string" ? info.device : "",
4919
- description: typeof info.description === "string" ? info.description : ""
5127
+ view: texture.createView(),
5128
+ texture,
5129
+ ownsTexture: true,
5130
+ hasImportanceData: tables.hasImportanceData
4920
5131
  });
4921
5132
  }
4922
- function createGpuAdapterParallelismDiagnostics(adapter, device) {
5133
+ function createBrdfLutResource(device, constants, size = DEFAULT_BRDF_LUT_SIZE) {
5134
+ const upload = createBrdfLutUploadBytes(size);
5135
+ const texture = device.createTexture({
5136
+ label: "plasius.wavefront.brdfLut",
5137
+ size: { width: upload.width, height: upload.height },
5138
+ format: "rgba16float",
5139
+ usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
5140
+ });
5141
+ device.queue.writeTexture(
5142
+ { texture },
5143
+ upload.bytes,
5144
+ { bytesPerRow: upload.bytesPerRow, rowsPerImage: upload.height },
5145
+ { width: upload.width, height: upload.height, depthOrArrayLayers: 1 }
5146
+ );
4923
5147
  return Object.freeze({
4924
- physicalCoreCount: null,
4925
- physicalCoreCountAvailable: false,
4926
- physicalCoreCountUnavailableReason: "WebGPU does not expose physical GPU core counts.",
4927
- adapterInfo: createAdapterInfoSnapshot(adapter),
4928
- adapterLimits: Object.freeze({
4929
- maxComputeInvocationsPerWorkgroup: readGpuLimit(adapter, device, "maxComputeInvocationsPerWorkgroup"),
4930
- maxComputeWorkgroupSizeX: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeX"),
4931
- maxComputeWorkgroupSizeY: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeY"),
4932
- maxComputeWorkgroupSizeZ: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeZ"),
4933
- maxComputeWorkgroupsPerDimension: readGpuLimit(adapter, device, "maxComputeWorkgroupsPerDimension"),
4934
- maxStorageBuffersPerShaderStage: readGpuLimit(adapter, device, "maxStorageBuffersPerShaderStage"),
4935
- maxStorageBufferBindingSize: readGpuLimit(adapter, device, "maxStorageBufferBindingSize"),
4936
- maxBufferSize: readGpuLimit(adapter, device, "maxBufferSize")
5148
+ view: texture.createView(),
5149
+ sampler: device.createSampler({
5150
+ label: "plasius.wavefront.brdfLutSampler",
5151
+ addressModeU: "clamp-to-edge",
5152
+ addressModeV: "clamp-to-edge",
5153
+ magFilter: "linear",
5154
+ minFilter: "linear"
4937
5155
  }),
4938
- configuredWorkgroupSize: WORKGROUP_SIZE
5156
+ texture,
5157
+ ownsTexture: true,
5158
+ width: upload.width,
5159
+ height: upload.height
4939
5160
  });
4940
5161
  }
4941
- function createEnvironmentMapSnapshot(environmentMap) {
4942
- return Object.freeze({
4943
- enabled: environmentMap.enabled,
4944
- width: environmentMap.width,
4945
- height: environmentMap.height,
4946
- mipLevelCount: environmentMap.mipLevelCount ?? 1,
4947
- projection: environmentMap.projection,
4948
- intensity: environmentMap.intensity,
4949
- rotationRadians: environmentMap.rotationRadians,
4950
- ambientStrength: environmentMap.ambientStrength,
4951
- hasImportanceData: environmentMap.hasImportanceData === true
5162
+ function createMediumTextureResource(device, constants, mediums) {
5163
+ const normalized = Array.isArray(mediums) && mediums.length > 0 ? mediums : [{ id: 0 }];
5164
+ const width = Math.max(
5165
+ 1,
5166
+ normalized.reduce((maximum, medium) => Math.max(maximum, medium.id ?? 0), 0) + 1
5167
+ );
5168
+ const level = {
5169
+ width,
5170
+ height: MEDIUM_TABLE_ROWS,
5171
+ data: new Float32Array(width * MEDIUM_TABLE_ROWS * 4)
5172
+ };
5173
+ for (const medium of normalized) {
5174
+ const mediumId = Math.max(0, Math.trunc(Number(medium.id) || 0));
5175
+ const absorptionOffset = mediumId * 4;
5176
+ level.data[absorptionOffset] = Math.max(0, medium.absorption?.[0] ?? 0);
5177
+ level.data[absorptionOffset + 1] = Math.max(0, medium.absorption?.[1] ?? 0);
5178
+ level.data[absorptionOffset + 2] = Math.max(0, medium.absorption?.[2] ?? 0);
5179
+ level.data[absorptionOffset + 3] = Math.max(0, medium.phaseModel ?? 0);
5180
+ const scatteringOffset = (width + mediumId) * 4;
5181
+ level.data[scatteringOffset] = Math.max(0, medium.scattering?.[0] ?? 0);
5182
+ level.data[scatteringOffset + 1] = Math.max(0, medium.scattering?.[1] ?? 0);
5183
+ level.data[scatteringOffset + 2] = Math.max(0, medium.scattering?.[2] ?? 0);
5184
+ level.data[scatteringOffset + 3] = Math.max(0, medium.density ?? 0);
5185
+ }
5186
+ const upload = createFloat16RgbaUploadFromLevels([level])[0];
5187
+ const texture = device.createTexture({
5188
+ label: "plasius.wavefront.mediumTable",
5189
+ size: { width, height: MEDIUM_TABLE_ROWS },
5190
+ format: "rgba16float",
5191
+ usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
5192
+ });
5193
+ device.queue.writeTexture(
5194
+ { texture },
5195
+ upload.bytes,
5196
+ { bytesPerRow: upload.bytesPerRow, rowsPerImage: upload.height },
5197
+ { width, height: MEDIUM_TABLE_ROWS, depthOrArrayLayers: 1 }
5198
+ );
5199
+ return Object.freeze({
5200
+ texture,
5201
+ view: texture.createView(),
5202
+ ownsTexture: true,
5203
+ count: normalized.length,
5204
+ width
4952
5205
  });
4953
5206
  }
4954
- function nowMs3() {
4955
- if (typeof performance !== "undefined" && typeof performance.now === "function") {
4956
- return performance.now();
5207
+ function mediumTablesEqual(left, right) {
5208
+ const leftMediums = Array.isArray(left) ? left : [];
5209
+ const rightMediums = Array.isArray(right) ? right : [];
5210
+ if (leftMediums.length !== rightMediums.length) {
5211
+ return false;
4957
5212
  }
4958
- return Date.now();
4959
- }
4960
- function estimateAccelerationBuildWaitFactor(config) {
4961
- if (config?.gpuAccelerationBuildRequired !== true) {
4962
- return 1;
5213
+ for (let index = 0; index < leftMediums.length; index += 1) {
5214
+ const leftMedium = leftMediums[index];
5215
+ const rightMedium = rightMediums[index];
5216
+ if ((leftMedium?.id ?? 0) !== (rightMedium?.id ?? 0)) {
5217
+ return false;
5218
+ }
5219
+ if ((leftMedium?.phaseModel ?? 0) !== (rightMedium?.phaseModel ?? 0)) {
5220
+ return false;
5221
+ }
5222
+ if ((leftMedium?.density ?? 0) !== (rightMedium?.density ?? 0)) {
5223
+ return false;
5224
+ }
5225
+ for (let component = 0; component < 3; component += 1) {
5226
+ if ((leftMedium?.absorption?.[component] ?? 0) !== (rightMedium?.absorption?.[component] ?? 0)) {
5227
+ return false;
5228
+ }
5229
+ if ((leftMedium?.scattering?.[component] ?? 0) !== (rightMedium?.scattering?.[component] ?? 0)) {
5230
+ return false;
5231
+ }
5232
+ }
4963
5233
  }
4964
- const bvhSortStageCount = Array.isArray(config?.bvhSortStages) ? config.bvhSortStages.length : 0;
4965
- const bvhBuildLevelCount = Array.isArray(config?.bvhBuildLevels) ? config.bvhBuildLevels.length : 0;
4966
- const accelerationStageCount = 2 + bvhSortStageCount + bvhBuildLevelCount;
4967
- return Math.max(1, 1 + accelerationStageCount / 96);
5234
+ return true;
4968
5235
  }
4969
- function estimateSubmittedGpuWorkTiming(config, tileCount, overrideTimeoutMs = null, options = {}) {
4970
- if (Number.isFinite(overrideTimeoutMs)) {
4971
- const overrideMs = Math.max(1, Math.trunc(Number(overrideTimeoutMs)));
4972
- return Object.freeze({
4973
- timeoutMs: overrideMs,
4974
- maxWaitMs: overrideMs
4975
- });
4976
- }
4977
- const samplesPerPixel = Math.max(
4978
- 1,
4979
- Number(config?.renderedSamplesPerPixel ?? config?.samplesPerPixel ?? 1)
4980
- );
4981
- const maxDepth = Math.max(1, Number(config?.maxDepth ?? 1));
4982
- const deferredResolvePasses = config?.deferredPathResolve ? 1 : 0;
4983
- const denoisePasses = config?.denoise ? samplesPerPixel < 4 ? 2 : 1 : 0;
4984
- const tiles = Math.max(1, Number(tileCount ?? 1));
4985
- const estimatedPasses = tiles * (samplesPerPixel * (maxDepth + 1 + deferredResolvePasses) + denoisePasses + 1);
4986
- const triangleCount = Math.max(0, Number(config?.triangleCount ?? 0));
4987
- const geometryFactor = Math.max(1, triangleCount / 131072);
4988
- const includeAccelerationBuild = options.includeAccelerationBuild === true;
4989
- const accelerationFactor = includeAccelerationBuild ? estimateAccelerationBuildWaitFactor(config) : 1;
4990
- const estimatedWindowMs = Math.round(
4991
- (GPU_SUBMITTED_WORK_TIMEOUT_MS + estimatedPasses * 5) * geometryFactor * accelerationFactor
4992
- );
4993
- const timeoutMs = Math.min(
4994
- GPU_MAX_SUBMITTED_WORK_TIMEOUT_MS,
4995
- Math.max(GPU_SUBMITTED_WORK_TIMEOUT_MS, estimatedWindowMs)
4996
- );
4997
- const maxWaitMultiplier = includeAccelerationBuild ? 3 : 2;
4998
- const maxWaitMs = Math.min(
4999
- GPU_MAX_SUBMITTED_WORK_DEADLINE_MS,
5000
- Math.max(timeoutMs, estimatedWindowMs * maxWaitMultiplier)
5236
+ function createAtlasTextureResource(device, constants, atlas, label) {
5237
+ const upload = createRgba8TextureUpload(atlas);
5238
+ const texture = device.createTexture({
5239
+ label,
5240
+ size: { width: upload.width, height: upload.height },
5241
+ format: "rgba8unorm",
5242
+ usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
5243
+ });
5244
+ device.queue.writeTexture(
5245
+ { texture },
5246
+ upload.bytes,
5247
+ { bytesPerRow: upload.bytesPerRow, rowsPerImage: upload.height },
5248
+ { width: upload.width, height: upload.height, depthOrArrayLayers: 1 }
5001
5249
  );
5002
5250
  return Object.freeze({
5003
- timeoutMs,
5004
- maxWaitMs
5251
+ texture,
5252
+ view: texture.createView(),
5253
+ ownsTexture: true
5005
5254
  });
5006
5255
  }
5007
5256
 
5257
+ // src/wavefront-instance-shader.js
5258
+ var WAVEFRONT_INSTANCE_SHADER_WGSL = `
5259
+ struct MeshInstance {
5260
+ world: mat4x4<f32>,
5261
+ inverse: mat4x4<f32>,
5262
+ objectId: u32,
5263
+ root: u32,
5264
+ handedness: f32,
5265
+ _pad: u32,
5266
+ };
5267
+ @group(0) @binding(47) var meshInstances: texture_2d<u32>;
5268
+ fn read_mesh_instance(index: u32) -> MeshInstance {
5269
+ let y = i32(index);
5270
+ let metadata = textureLoad(meshInstances, vec2<i32>(8, y), 0);
5271
+ return MeshInstance(
5272
+ mat4x4<f32>(bitcast<vec4<f32>>(textureLoad(meshInstances, vec2<i32>(0, y), 0)),
5273
+ bitcast<vec4<f32>>(textureLoad(meshInstances, vec2<i32>(1, y), 0)),
5274
+ bitcast<vec4<f32>>(textureLoad(meshInstances, vec2<i32>(2, y), 0)),
5275
+ bitcast<vec4<f32>>(textureLoad(meshInstances, vec2<i32>(3, y), 0))),
5276
+ mat4x4<f32>(bitcast<vec4<f32>>(textureLoad(meshInstances, vec2<i32>(4, y), 0)),
5277
+ bitcast<vec4<f32>>(textureLoad(meshInstances, vec2<i32>(5, y), 0)),
5278
+ bitcast<vec4<f32>>(textureLoad(meshInstances, vec2<i32>(6, y), 0)),
5279
+ bitcast<vec4<f32>>(textureLoad(meshInstances, vec2<i32>(7, y), 0))),
5280
+ metadata.x, metadata.y, bitcast<f32>(metadata.z), 0u);
5281
+ }
5282
+
5283
+ fn instance_normal(instance: MeshInstance, normal: vec3<f32>) -> vec3<f32> {
5284
+ let normalMatrix = transpose(mat3x3<f32>(instance.inverse[0].xyz, instance.inverse[1].xyz, instance.inverse[2].xyz));
5285
+ return safe_normalize(normalMatrix * normal, vec3<f32>(0.0, 1.0, 0.0));
5286
+ }
5287
+
5288
+ fn triangle_for_candidate(candidate: Candidate) -> TriangleRecord {
5289
+ var triangle = triangles[candidate.triangleIndex];
5290
+ let instance = read_mesh_instance(candidate.instanceIndex);
5291
+ triangle.v0 = instance.world * vec4<f32>(triangle.v0.xyz, 1.0);
5292
+ triangle.v1 = instance.world * vec4<f32>(triangle.v1.xyz, 1.0);
5293
+ triangle.v2 = instance.world * vec4<f32>(triangle.v2.xyz, 1.0);
5294
+ triangle.meshId = instance.objectId;
5295
+ return triangle;
5296
+ }
5297
+
5298
+ fn intersect_bvh(ray: RayRecord, initialNearest: f32) -> Candidate {
5299
+ var nearest = initialNearest;
5300
+ var best = no_candidate();
5301
+ var stack = array<u32, 64>();
5302
+ var stackSize = 1u;
5303
+ stack[0] = 0u;
5304
+ loop {
5305
+ if (stackSize == 0u) { break; }
5306
+ stackSize -= 1u;
5307
+ let node = bvhNodes[stack[stackSize]];
5308
+ if (!intersect_bounds(ray, node.boundsMin.xyz, node.boundsMax.xyz, nearest)) { continue; }
5309
+ if (node.triangleCount == 0u) {
5310
+ stack[stackSize] = node.childOrFirst;
5311
+ stack[stackSize + 1u] = node.rightChild;
5312
+ stackSize += 2u;
5313
+ } else {
5314
+ let instanceIndex = node.childOrFirst;
5315
+ let instance = read_mesh_instance(instanceIndex);
5316
+ var localRay = ray;
5317
+ localRay.origin = instance.inverse * vec4<f32>(ray.origin.xyz, 1.0);
5318
+ // Keep this direction unnormalized so t remains the world-space ray parameter.
5319
+ localRay.direction = instance.inverse * vec4<f32>(ray.direction.xyz, 0.0);
5320
+ var candidate = intersect_geometry_bvh(localRay, nearest, instance.root);
5321
+ if (candidate.hit == 1u && candidate.distance < nearest) {
5322
+ candidate.geometricNormal = instance_normal(instance, candidate.geometricNormal);
5323
+ candidate.shadingNormal = repair_shading_normal(candidate.geometricNormal,
5324
+ instance_normal(instance, candidate.shadingNormal));
5325
+ if (instance.handedness < 0.0) { candidate.frontFace = 1u - candidate.frontFace; }
5326
+ candidate.instanceIndex = instanceIndex;
5327
+ nearest = candidate.distance;
5328
+ best = candidate;
5329
+ }
5330
+ }
5331
+ }
5332
+ return best;
5333
+ }
5334
+ `;
5335
+ var WAVEFRONT_UNINSTANCED_SHADER_WGSL = `
5336
+ fn triangle_for_candidate(candidate: Candidate) -> TriangleRecord {
5337
+ return triangles[candidate.triangleIndex];
5338
+ }
5339
+ fn intersect_bvh(ray: RayRecord, initialNearest: f32) -> Candidate {
5340
+ return intersect_geometry_bvh(ray, initialNearest, 0u);
5341
+ }
5342
+ `;
5343
+
5008
5344
  // src/wavefront-shader-bvh.js
5009
5345
  var WAVEFRONT_SHADER_BVH_WGSL = `
5010
5346
  fn default_mesh_range() -> MeshRange {
@@ -5467,6 +5803,7 @@ fn no_candidate() -> Candidate {
5467
5803
  1u,
5468
5804
  0xffffffffu,
5469
5805
  0xffffffffu,
5806
+ 0xffffffffu,
5470
5807
  0u,
5471
5808
  0u
5472
5809
  );
@@ -5493,6 +5830,7 @@ fn surface_candidate(
5493
5830
  uv,
5494
5831
  frontFace,
5495
5832
  triangleIndex,
5833
+ 0xffffffffu,
5496
5834
  primitiveId,
5497
5835
  materialRefId,
5498
5836
  mediumRefId
@@ -5554,7 +5892,7 @@ fn intersect_triangle(ray: RayRecord, triangle: TriangleRecord, triangleIndex: u
5554
5892
  );
5555
5893
  }
5556
5894
 
5557
- fn intersect_bvh(ray: RayRecord, initialNearest: f32) -> Candidate {
5895
+ fn intersect_geometry_bvh(ray: RayRecord, initialNearest: f32, root: u32) -> Candidate {
5558
5896
  var nearest = initialNearest;
5559
5897
  var best = no_candidate();
5560
5898
  if (config.bvhNodeCount == 0u || config.triangleCount == 0u) {
@@ -5563,7 +5901,7 @@ fn intersect_bvh(ray: RayRecord, initialNearest: f32) -> Candidate {
5563
5901
 
5564
5902
  var stack = array<u32, 64>();
5565
5903
  var stackSize = 1u;
5566
- stack[0] = 0u;
5904
+ stack[0] = root;
5567
5905
 
5568
5906
  loop {
5569
5907
  if (stackSize == 0u) {
@@ -5854,7 +6192,7 @@ fn intersectActiveQueue(@builtin(global_invocation_id) globalId: vec3<u32>) {
5854
6192
  if (meshCandidate.hit == 1u && meshCandidate.distance < nearest) {
5855
6193
  nearest = meshCandidate.distance;
5856
6194
  candidate = meshCandidate;
5857
- hitTriangle = triangles[meshCandidate.triangleIndex];
6195
+ hitTriangle = triangle_for_candidate(meshCandidate);
5858
6196
  }
5859
6197
 
5860
6198
  if (candidate.hit == 0u) {
@@ -7008,6 +7346,7 @@ struct Candidate {
7008
7346
  uv: vec2<f32>,
7009
7347
  frontFace: u32,
7010
7348
  triangleIndex: u32,
7349
+ instanceIndex: u32,
7011
7350
  primitiveId: u32,
7012
7351
  materialRefId: u32,
7013
7352
  mediumRefId: u32,
@@ -8605,6 +8944,7 @@ var WAVEFRONT_COMPUTE_WGSL = [
8605
8944
  WAVEFRONT_SHADER_MATERIALS_WGSL,
8606
8945
  WAVEFRONT_SHADER_LIGHTING_WGSL,
8607
8946
  WAVEFRONT_SHADER_BVH_WGSL,
8947
+ WAVEFRONT_UNINSTANCED_SHADER_WGSL,
8608
8948
  WAVEFRONT_SHADER_KERNELS_WGSL
8609
8949
  ].join("\n");
8610
8950
  function createWavefrontPathTracingComputeShaderSource(options = {}) {
@@ -8616,7 +8956,7 @@ function createWavefrontPathTracingComputeShaderSource(options = {}) {
8616
8956
  if (workgroupSize !== WAVEFRONT_COMPUTE_WORKGROUP_SIZE) {
8617
8957
  throw new Error(`wavefront mesh compute currently requires workgroupSize=${WAVEFRONT_COMPUTE_WORKGROUP_SIZE}`);
8618
8958
  }
8619
- return WAVEFRONT_COMPUTE_WGSL;
8959
+ return options.instanced === true ? WAVEFRONT_COMPUTE_WGSL.replace(WAVEFRONT_UNINSTANCED_SHADER_WGSL, WAVEFRONT_INSTANCE_SHADER_WGSL) : WAVEFRONT_COMPUTE_WGSL;
8620
8960
  }
8621
8961
 
8622
8962
  // src/wavefront-progressive-shader.js
@@ -9620,7 +9960,7 @@ function chooseSplitAxis(triangles) {
9620
9960
  }
9621
9961
  return extent[1] >= extent[2] ? 1 : 2;
9622
9962
  }
9623
- function buildBvh(triangles, maxLeafTriangles = 4) {
9963
+ function buildWavefrontBoundsBvh(triangles, maxLeafTriangles = 4) {
9624
9964
  if (triangles.length === 0) {
9625
9965
  return Object.freeze({ nodes: Object.freeze([]), triangles: Object.freeze([]) });
9626
9966
  }
@@ -9672,7 +10012,7 @@ function createWavefrontMeshAcceleration(meshes = [], gpuMaterialSource = null)
9672
10012
  const source = Array.isArray(meshes) ? meshes : [meshes];
9673
10013
  const resolvedMaterialSource = gpuMaterialSource ?? createWavefrontGpuMaterialSource(source);
9674
10014
  const triangles = createMeshTriangleRecords(source, resolvedMaterialSource);
9675
- return buildBvh(triangles);
10015
+ return buildWavefrontBoundsBvh(triangles);
9676
10016
  }
9677
10017
  function estimateMeshSourceShape(meshes) {
9678
10018
  const source = Array.isArray(meshes) ? meshes : [];
@@ -10490,6 +10830,145 @@ function dispatchWavefrontGpuAccelerationBuild({
10490
10830
  return true;
10491
10831
  }
10492
10832
 
10833
+ // src/wavefront-instances.js
10834
+ var WAVEFRONT_INSTANCE_BYTES = 144;
10835
+ var freeze = Object.freeze;
10836
+ var uint = (value) => Number.isInteger(value) && value >= 0 && value < 4294967295;
10837
+ function affineTransform(value) {
10838
+ if (!Array.isArray(value) && !ArrayBuffer.isView(value) || value.length !== 16 || !Array.from(value).every((n) => Number.isFinite(n) && Math.abs(n) <= 1e12) || value[3] !== 0 || value[7] !== 0 || value[11] !== 0 || value[15] !== 1) {
10839
+ throw new Error("Instance transform must be a finite affine column-major matrix.");
10840
+ }
10841
+ const m = Array.from(value, Math.fround);
10842
+ const [a, b, c, , d, e, f, , g, h, i] = m;
10843
+ const determinant = a * (e * i - f * h) - d * (b * i - c * h) + g * (b * f - c * e);
10844
+ if (!Number.isFinite(determinant) || Math.abs(determinant) < 1e-12) {
10845
+ throw new Error("Instance transform must be invertible.");
10846
+ }
10847
+ const inv = [
10848
+ (e * i - f * h) / determinant,
10849
+ (c * h - b * i) / determinant,
10850
+ (b * f - c * e) / determinant,
10851
+ 0,
10852
+ (f * g - d * i) / determinant,
10853
+ (a * i - c * g) / determinant,
10854
+ (c * d - a * f) / determinant,
10855
+ 0,
10856
+ (d * h - e * g) / determinant,
10857
+ (b * g - a * h) / determinant,
10858
+ (a * e - b * d) / determinant,
10859
+ 0,
10860
+ 0,
10861
+ 0,
10862
+ 0,
10863
+ 1
10864
+ ];
10865
+ for (let r = 0; r < 3; r++) inv[12 + r] = -(inv[r] * m[12] + inv[4 + r] * m[13] + inv[8 + r] * m[14]);
10866
+ if (!inv.every((n) => Number.isFinite(n) && Math.abs(n) <= 1e12)) throw new Error("Instance inverse transform is out of range.");
10867
+ return { transform: freeze(m), inverse: freeze(inv.map((n) => n === 0 ? 0 : n)), handedness: Math.sign(determinant) };
10868
+ }
10869
+ function transformInstancePoint(m, p) {
10870
+ return [0, 1, 2].map((r) => m[r] * p[0] + m[4 + r] * p[1] + m[8 + r] * p[2] + m[12 + r]);
10871
+ }
10872
+ function meshBounds(mesh) {
10873
+ const localMin = [Infinity, Infinity, Infinity], localMax = [-Infinity, -Infinity, -Infinity];
10874
+ for (let i = 0; i < mesh.positions.length; i += 3) for (let a = 0; a < 3; a++) {
10875
+ localMin[a] = Math.min(localMin[a], Math.fround(mesh.positions[i + a]));
10876
+ localMax[a] = Math.max(localMax[a], Math.fround(mesh.positions[i + a]));
10877
+ }
10878
+ return { min: localMin, max: localMax };
10879
+ }
10880
+ function boundsOf(local, transform) {
10881
+ const localMin = local.min, localMax = local.max;
10882
+ const min = [Infinity, Infinity, Infinity], max = [-Infinity, -Infinity, -Infinity];
10883
+ for (let corner = 0; corner < 8; corner++) {
10884
+ const p = transformInstancePoint(transform, [0, 1, 2].map((a) => corner & 1 << a ? localMax[a] : localMin[a]));
10885
+ for (let a = 0; a < 3; a++) {
10886
+ min[a] = Math.min(min[a], p[a]);
10887
+ max[a] = Math.max(max[a], p[a]);
10888
+ }
10889
+ }
10890
+ if (![...min, ...max].every(Number.isFinite)) throw new Error("Instance mesh has invalid bounds.");
10891
+ return freeze({ min: freeze(min), max: freeze(max) });
10892
+ }
10893
+ function prepareWavefrontInstances(meshes, instances) {
10894
+ if (!Array.isArray(instances) || instances.length === 0) throw new Error("Instances must be a nonempty array.");
10895
+ const byId = /* @__PURE__ */ new Map(), localBounds = /* @__PURE__ */ new Map();
10896
+ for (const mesh of meshes) {
10897
+ if (!uint(mesh.id) || byId.has(mesh.id)) throw new Error("Instanced mesh IDs must be unique unsigned integers.");
10898
+ if (mesh.materialKind === MATERIAL_EMISSIVE || emissionPower(mesh.emission ?? [0, 0, 0, 1]) > 1e-4) {
10899
+ throw new Error("Instanced emissive meshes require per-instance light sampling, which is not supported yet.");
10900
+ }
10901
+ byId.set(mesh.id, mesh);
10902
+ localBounds.set(mesh.id, meshBounds(mesh));
10903
+ }
10904
+ const ids = /* @__PURE__ */ new Set(), used = /* @__PURE__ */ new Set();
10905
+ const records = instances.map((instance) => {
10906
+ if (!instance || !uint(instance.id) || ids.has(instance.id)) throw new Error("Instance IDs must be unique unsigned integers.");
10907
+ const mesh = byId.get(instance.meshId);
10908
+ if (!mesh) throw new Error("Instance references an unknown mesh.");
10909
+ ids.add(instance.id);
10910
+ used.add(mesh.id);
10911
+ const affine = affineTransform(instance.transform);
10912
+ const bounds = boundsOf(localBounds.get(mesh.id), affine.transform);
10913
+ return freeze({
10914
+ ...affine,
10915
+ id: instance.id,
10916
+ meshId: mesh.id,
10917
+ bounds,
10918
+ centroid: freeze(bounds.min.map((v, a) => (v + bounds.max[a]) / 2))
10919
+ });
10920
+ });
10921
+ if (used.size !== meshes.length) throw new Error("Every instanced mesh must have at least one instance.");
10922
+ return freeze({ records: freeze(records), geometryCount: meshes.length });
10923
+ }
10924
+ function buildWavefrontInstances(meshes, plan, materialSource) {
10925
+ const tlas = buildWavefrontBoundsBvh(plan.records, 1);
10926
+ const nodes = tlas.nodes.map((node) => {
10927
+ const pad = node.bounds.min.map((v, a) => Math.max(1, Math.abs(v), Math.abs(node.bounds.max[a])) * 1e-6);
10928
+ return freeze({ ...node, bounds: freeze({
10929
+ min: freeze(node.bounds.min.map((v, a) => v - pad[a])),
10930
+ max: freeze(node.bounds.max.map((v, a) => v + pad[a]))
10931
+ }) });
10932
+ }), triangles = [], roots = /* @__PURE__ */ new Map();
10933
+ for (const [meshIndex, mesh] of meshes.entries()) {
10934
+ const blas = createWavefrontMeshAcceleration([mesh], materialSource);
10935
+ const nodeOffset = nodes.length, triangleOffset = triangles.length;
10936
+ roots.set(mesh.id, nodeOffset);
10937
+ for (const node of blas.nodes) nodes.push(freeze({
10938
+ ...node,
10939
+ firstTriangle: node.firstTriangle + (node.triangleCount > 0 ? triangleOffset : nodeOffset),
10940
+ leftChild: node.leftChild + nodeOffset,
10941
+ rightChild: node.rightChild + nodeOffset
10942
+ }));
10943
+ for (const triangle of blas.triangles) triangles.push(freeze({
10944
+ ...triangle,
10945
+ triangleId: triangle.triangleId + triangleOffset,
10946
+ materialSlot: meshIndex
10947
+ }));
10948
+ }
10949
+ const records = tlas.triangles.map((record) => freeze({ ...record, root: roots.get(record.meshId) }));
10950
+ const buffer = new ArrayBuffer(records.length * WAVEFRONT_INSTANCE_BYTES);
10951
+ const floats = new Float32Array(buffer), uints = new Uint32Array(buffer);
10952
+ records.forEach((record, index) => {
10953
+ const offset = index * 36;
10954
+ floats.set(record.transform, offset);
10955
+ floats.set(record.inverse, offset + 16);
10956
+ uints[offset + 32] = record.id;
10957
+ uints[offset + 33] = record.root;
10958
+ floats[offset + 34] = record.handedness;
10959
+ });
10960
+ return freeze({
10961
+ records: freeze(records),
10962
+ buffer,
10963
+ instanceCount: records.length,
10964
+ geometryCount: meshes.length,
10965
+ instanceBytes: buffer.byteLength,
10966
+ tlasNodeCount: tlas.nodes.length,
10967
+ blasNodeCount: nodes.length - tlas.nodes.length,
10968
+ acceleration: freeze({ nodes: freeze(nodes), triangles: freeze(triangles) })
10969
+ });
10970
+ }
10971
+
10493
10972
  // src/wavefront-config.js
10494
10973
  var WEBGPU_DEFAULT_MIN_UNIFORM_BUFFER_OFFSET_ALIGNMENT = 256;
10495
10974
  function allocatedRecordBytes(count, recordBytes) {
@@ -10763,6 +11242,7 @@ function estimateWavefrontPathTracingMemory(options = {}) {
10763
11242
  const progressiveBytes = options.progressive === true ? width * height * 16 + 16 : 0;
10764
11243
  const pathVertexBytes = tilePixelCapacity * (maxDepth + 1) * PATH_VERTEX_RECORD_BYTES;
10765
11244
  const sceneObjectBytes = sceneObjectCapacity * SCENE_OBJECT_RECORD_BYTES;
11245
+ const instanceBytes = readNonNegativeInteger2("instanceBytes", options.instanceBytes, 0);
10766
11246
  const triangleBytes = allocatedRecordBytes(triangleCapacity, TRIANGLE_RECORD_BYTES);
10767
11247
  const materialTableBytes = 0;
10768
11248
  const bvhNodeBytes = bvhNodeCapacity * BVH_NODE_RECORD_BYTES;
@@ -10818,6 +11298,7 @@ function estimateWavefrontPathTracingMemory(options = {}) {
10818
11298
  pathVertexBytes,
10819
11299
  sceneObjectBytes,
10820
11300
  triangleBytes,
11301
+ instanceBytes,
10821
11302
  materialTableBytes,
10822
11303
  bvhNodeBytes,
10823
11304
  bvhCombinedBytes,
@@ -10847,6 +11328,7 @@ function estimateWavefrontPathTracingMemoryForConfig(config, limits = {}) {
10847
11328
  maxDepth: config.maxDepth,
10848
11329
  sceneObjectCapacity: config.sceneObjectCapacity,
10849
11330
  triangleCapacity: config.triangleCapacity,
11331
+ instanceBytes: config.instancing?.instanceBytes ?? 0,
10850
11332
  bvhNodeCapacity: config.bvhNodeCapacity,
10851
11333
  bvhLeafSortCapacity: config.bvhLeafSortCapacity,
10852
11334
  emissiveTriangleCapacity: config.emissiveTriangleCapacity,
@@ -10865,6 +11347,7 @@ function estimateWavefrontPathTracingMemoryForConfig(config, limits = {}) {
10865
11347
  function createWavefrontPathTracingComputeConfig(options = {}) {
10866
11348
  assertAnalyticDisplayQualityPolicy(options);
10867
11349
  const accelerationBuildMode = resolveAccelerationBuildMode(options);
11350
+ if (options.instances !== void 0 && accelerationBuildMode === "gpu") throw new Error("Instance BLAS construction currently requires cpu-upload acceleration.");
10868
11351
  const canvas = options.canvas;
10869
11352
  const width = readPositiveInteger2("width", options.width, getCanvasDimension(canvas, "width", DEFAULT_WIDTH));
10870
11353
  const height = readPositiveInteger2("height", options.height, getCanvasDimension(canvas, "height", DEFAULT_HEIGHT));
@@ -10895,9 +11378,16 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
10895
11378
  );
10896
11379
  const meshes = normalizeMeshes(options);
10897
11380
  const meshSourceShape = estimateMeshSourceShape(meshes);
11381
+ if (options.memoryBudgetBytes !== void 0) {
11382
+ if (!Number.isSafeInteger(options.memoryBudgetBytes) || options.memoryBudgetBytes <= 0) throw new Error("GPU memory budget must be a positive safe integer.");
11383
+ const geometryMinimumBytes = meshSourceShape.triangleCount * TRIANGLE_RECORD_BYTES + (options.instances?.length ?? 0) * 144;
11384
+ if (!Number.isSafeInteger(geometryMinimumBytes) || geometryMinimumBytes > options.memoryBudgetBytes) throw new Error("Unique geometry exceeds the GPU memory budget before acceleration construction.");
11385
+ }
11386
+ const instancePlan = options.instances === void 0 ? null : prepareWavefrontInstances(meshes, options.instances);
10898
11387
  const gpuMaterialSource = meshes.length > 0 ? createWavefrontGpuMaterialSource(meshes) : createWavefrontGpuMaterialSource([]);
10899
11388
  const gpuMeshSource = meshes.length > 0 ? createWavefrontGpuMeshSource(meshes, gpuMaterialSource) : createWavefrontGpuMeshSource([]);
10900
- const meshAcceleration = accelerationBuildMode === "cpu-upload" ? createWavefrontMeshAcceleration(meshes, gpuMaterialSource) : Object.freeze({ nodes: Object.freeze([]), triangles: Object.freeze([]) });
11389
+ const instancing = instancePlan ? buildWavefrontInstances(meshes, instancePlan, gpuMaterialSource) : null;
11390
+ const meshAcceleration = instancing?.acceleration ?? (accelerationBuildMode === "cpu-upload" ? createWavefrontMeshAcceleration(meshes, gpuMaterialSource) : Object.freeze({ nodes: Object.freeze([]), triangles: Object.freeze([]) }));
10901
11391
  const emissiveTriangleIndices = createWavefrontEmissiveTriangleIndexSource(
10902
11392
  meshes,
10903
11393
  options.emissiveTriangleCapacity
@@ -10975,6 +11465,7 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
10975
11465
  gpuAccelerationBuildRequired: accelerationBuildMode === "gpu" && triangleCount > 0,
10976
11466
  gpuMeshSource,
10977
11467
  gpuMaterialSource,
11468
+ instancing,
10978
11469
  meshAcceleration,
10979
11470
  emissiveTriangleIndices,
10980
11471
  emissiveTriangleCount: emissiveTriangleIndices.count,
@@ -11013,6 +11504,7 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
11013
11504
  tilePixelCapacity,
11014
11505
  maxDepth,
11015
11506
  sceneObjectCapacity,
11507
+ instanceBytes: instancing?.instanceBytes ?? 0,
11016
11508
  triangleCapacity,
11017
11509
  bvhNodeCapacity,
11018
11510
  bvhLeafSortCapacity,
@@ -11046,6 +11538,7 @@ function createWavefrontTraceBindGroup({
11046
11538
  counterBuffer,
11047
11539
  outputView,
11048
11540
  triangleBuffer,
11541
+ instanceView,
11049
11542
  bvhNodeBuffer,
11050
11543
  radianceView,
11051
11544
  environmentPortalBuffer,
@@ -11103,7 +11596,8 @@ function createWavefrontTraceBindGroup({
11103
11596
  { binding: 41, resource: extensionAtlasResources.specularColor.view },
11104
11597
  { binding: 42, resource: extensionAtlasResources.iridescence.view },
11105
11598
  { binding: 43, resource: extensionAtlasResources.iridescenceThickness.view },
11106
- { binding: 44, resource: extensionAtlasResources.anisotropy.view }
11599
+ { binding: 44, resource: extensionAtlasResources.anisotropy.view },
11600
+ ...instanceView ? [{ binding: 47, resource: instanceView }] : []
11107
11601
  ]
11108
11602
  });
11109
11603
  }
@@ -11201,115 +11695,39 @@ function createWavefrontDenoiseBindGroups({
11201
11695
  inputView: radianceView,
11202
11696
  targetView: denoiseScratchView,
11203
11697
  label: "plasius.wavefront.bind.denoise.radianceToScratch"
11204
- }),
11205
- resolve: createWavefrontDenoiseResolveBindGroup({
11206
- device,
11207
- denoiseResolveBindGroupLayout,
11208
- configBuffer,
11209
- inputView: denoiseScratchView,
11210
- targetView: outputView,
11211
- label: "plasius.wavefront.bind.denoise.scratchToOutput"
11212
- }),
11213
- directResolve: createWavefrontDenoiseResolveBindGroup({
11214
- device,
11215
- denoiseResolveBindGroupLayout,
11216
- configBuffer,
11217
- inputView: radianceView,
11218
- targetView: outputView,
11219
- label: "plasius.wavefront.bind.denoise.radianceToOutput"
11220
- })
11221
- });
11222
- }
11223
- function createWavefrontPresentBindGroup({
11224
- device,
11225
- presentBindGroupLayout,
11226
- outputView,
11227
- sampler
11228
- }) {
11229
- return device.createBindGroup({
11230
- label: "plasius.wavefront.presentBindGroup",
11231
- layout: presentBindGroupLayout,
11232
- entries: [
11233
- { binding: 0, resource: outputView },
11234
- { binding: 1, resource: sampler }
11235
- ]
11236
- });
11237
- }
11238
-
11239
- // src/wavefront-gpu-synchronization.js
11240
- async function waitForSubmittedGpuWork(device, options = {}) {
11241
- if (typeof device.queue.onSubmittedWorkDone !== "function") {
11242
- return true;
11243
- }
11244
- const timeoutMs = Math.max(
11245
- 1,
11246
- Number.isFinite(options.timeoutMs) ? Number(options.timeoutMs) : GPU_SUBMITTED_WORK_TIMEOUT_MS
11247
- );
11248
- const maxWaitMs = Math.max(
11249
- timeoutMs,
11250
- Number.isFinite(options.maxWaitMs) ? Number(options.maxWaitMs) : timeoutMs
11251
- );
11252
- const allowTimeout = options.allowTimeout !== false;
11253
- const completionPromise = device.queue.onSubmittedWorkDone().then(
11254
- () => ({ status: "done" }),
11255
- (error) => {
11256
- throw error;
11257
- }
11258
- );
11259
- const lossPromise = typeof device.lost?.then === "function" ? device.lost.then((info) => {
11260
- throw new Error(
11261
- `WebGPU device lost while waiting for submitted work (${info?.reason ?? "unknown"}).`
11262
- );
11263
- }) : null;
11264
- const startedAtMs = nowMs3();
11265
- while (true) {
11266
- const elapsedMs = Math.max(0, nowMs3() - startedAtMs);
11267
- const remainingMs = Math.max(0, maxWaitMs - elapsedMs);
11268
- if (remainingMs <= 0) {
11269
- if (!allowTimeout) {
11270
- throw new Error(`Timed out after ${Math.round(maxWaitMs)} ms waiting for submitted GPU work.`);
11271
- }
11272
- console.warn(
11273
- `[plasius.wavefront] Submitted GPU work did not report completion within ${Math.round(maxWaitMs)} ms; continuing.`
11274
- );
11275
- return false;
11276
- }
11277
- const waitWindowMs = Math.max(1, Math.min(timeoutMs, remainingMs));
11278
- let timeoutHandle = null;
11279
- let resolveTimeoutPromise = null;
11280
- let timeoutSettled = false;
11281
- const settleTimeoutPromise = (value) => {
11282
- if (timeoutSettled) {
11283
- return;
11284
- }
11285
- timeoutSettled = true;
11286
- resolveTimeoutPromise?.(value);
11287
- };
11288
- const timeoutPromise = new Promise((resolve) => {
11289
- resolveTimeoutPromise = resolve;
11290
- timeoutHandle = setTimeout(
11291
- () => settleTimeoutPromise({ status: "timeout" }),
11292
- waitWindowMs
11293
- );
11294
- });
11295
- let result;
11296
- try {
11297
- result = await Promise.race(
11298
- [completionPromise, timeoutPromise, lossPromise].filter(Boolean)
11299
- );
11300
- } finally {
11301
- if (timeoutHandle !== null) {
11302
- clearTimeout(timeoutHandle);
11303
- settleTimeoutPromise({ status: "cancelled" });
11304
- }
11305
- }
11306
- if (result?.status === "done") {
11307
- return true;
11308
- }
11309
- if (result?.status !== "timeout") {
11310
- return true;
11311
- }
11312
- }
11698
+ }),
11699
+ resolve: createWavefrontDenoiseResolveBindGroup({
11700
+ device,
11701
+ denoiseResolveBindGroupLayout,
11702
+ configBuffer,
11703
+ inputView: denoiseScratchView,
11704
+ targetView: outputView,
11705
+ label: "plasius.wavefront.bind.denoise.scratchToOutput"
11706
+ }),
11707
+ directResolve: createWavefrontDenoiseResolveBindGroup({
11708
+ device,
11709
+ denoiseResolveBindGroupLayout,
11710
+ configBuffer,
11711
+ inputView: radianceView,
11712
+ targetView: outputView,
11713
+ label: "plasius.wavefront.bind.denoise.radianceToOutput"
11714
+ })
11715
+ });
11716
+ }
11717
+ function createWavefrontPresentBindGroup({
11718
+ device,
11719
+ presentBindGroupLayout,
11720
+ outputView,
11721
+ sampler
11722
+ }) {
11723
+ return device.createBindGroup({
11724
+ label: "plasius.wavefront.presentBindGroup",
11725
+ layout: presentBindGroupLayout,
11726
+ entries: [
11727
+ { binding: 0, resource: outputView },
11728
+ { binding: 1, resource: sampler }
11729
+ ]
11730
+ });
11313
11731
  }
11314
11732
 
11315
11733
  // src/wavefront-frame-telemetry.js
@@ -12136,7 +12554,7 @@ function dispatchWavefrontFrameAwaitingGpu({
12136
12554
  }
12137
12555
 
12138
12556
  // src/wavefront-pipelines.js
12139
- function createWavefrontBindGroupLayouts(device, constants) {
12557
+ function createWavefrontBindGroupLayouts(device, constants, instanced = false) {
12140
12558
  const trace = device.createBindGroupLayout({
12141
12559
  label: "plasius.wavefront.traceBindGroupLayout",
12142
12560
  entries: [
@@ -12188,7 +12606,8 @@ function createWavefrontBindGroupLayouts(device, constants) {
12188
12606
  { binding: 41, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
12189
12607
  { binding: 42, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
12190
12608
  { binding: 43, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
12191
- { binding: 44, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } }
12609
+ { binding: 44, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
12610
+ ...instanced ? [{ binding: 47, visibility: constants.shader.COMPUTE, texture: { sampleType: "uint" } }] : []
12192
12611
  ]
12193
12612
  });
12194
12613
  const acceleration = device.createBindGroupLayout({
@@ -12383,12 +12802,12 @@ async function createWavefrontPresentPipeline(device, presentBindGroupLayout, fo
12383
12802
  primitive: { topology: "triangle-list" }
12384
12803
  });
12385
12804
  }
12386
- async function createWavefrontPipelineResources({ device, constants, format }) {
12387
- const bindGroupLayouts = createWavefrontBindGroupLayouts(device, constants);
12805
+ async function createWavefrontPipelineResources({ device, constants, format, instanced = false }) {
12806
+ const bindGroupLayouts = createWavefrontBindGroupLayouts(device, constants, instanced);
12388
12807
  const pipelineLayouts = createWavefrontPipelineLayouts(device, bindGroupLayouts);
12389
12808
  const computeShader = device.createShaderModule({
12390
12809
  label: "plasius.wavefront.computeShader",
12391
- code: WAVEFRONT_COMPUTE_WGSL
12810
+ code: createWavefrontPathTracingComputeShaderSource({ instanced })
12392
12811
  });
12393
12812
  await assertShaderModuleCompiles(computeShader, "plasius.wavefront.computeShader");
12394
12813
  return Object.freeze({
@@ -12821,890 +13240,911 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
12821
13240
  if (!adapter) {
12822
13241
  throw new Error("Unable to acquire a WebGPU adapter for wavefront path tracing.");
12823
13242
  }
12824
- const device = await adapter.requestDevice(
13243
+ const rawDevice = await adapter.requestDevice(
12825
13244
  createWavefrontDeviceDescriptor(adapter, options, initialConfig.memory)
12826
13245
  );
12827
- const gpuAdapterParallelism = createGpuAdapterParallelismDiagnostics(adapter, device);
12828
- const context = canvas.getContext("webgpu");
12829
- if (!context || typeof context.configure !== "function") {
12830
- throw new Error("Canvas WebGPU context does not support configure().");
12831
- }
12832
- const format = options.format ?? (typeof navigatorRef.gpu.getPreferredCanvasFormat === "function" ? navigatorRef.gpu.getPreferredCanvasFormat() : "bgra8unorm");
12833
- let config = initialConfig;
12834
- const safeTileSize = clampTileSizeForDevice(config, device);
12835
- if (safeTileSize !== config.tileSize) {
12836
- config = createWavefrontPathTracingComputeConfig({
12837
- ...options,
12838
- canvas,
12839
- width: config.width,
12840
- height: config.height,
12841
- tileSize: safeTileSize,
12842
- tilePixelCapacity: Math.min(
12843
- config.tilePixelCapacity,
12844
- safeTileSize * safeTileSize
12845
- )
12846
- });
12847
- }
12848
- config = Object.freeze({
12849
- ...config,
12850
- memory: estimateWavefrontPathTracingMemoryForConfig(config, device.limits)
12851
- });
12852
- canvas.width = config.width;
12853
- canvas.height = config.height;
12854
- context.configure({
12855
- device,
12856
- format,
12857
- alphaMode: options.alpha === true ? "premultiplied" : "opaque"
12858
- });
12859
- const bufferUsage = constants.buffer.STORAGE | constants.buffer.COPY_DST;
12860
- const memory = config.memory;
12861
- const activeQueue = createBuffer(device, bufferUsage, memory.queueBytes, "plasius.wavefront.activeQueue");
12862
- const nextQueue = createBuffer(device, bufferUsage, memory.queueBytes, "plasius.wavefront.nextQueue");
12863
- const hitBuffer = createBuffer(device, bufferUsage, memory.hitBytes, "plasius.wavefront.hitBuffer");
12864
- const accumulationBuffer = createBuffer(
12865
- device,
12866
- bufferUsage,
12867
- memory.accumulationBytes,
12868
- "plasius.wavefront.accumulation"
12869
- );
12870
- const pathVertexBuffer = createBuffer(
12871
- device,
12872
- bufferUsage,
12873
- memory.pathVertexBytes,
12874
- "plasius.wavefront.pathVertices"
12875
- );
12876
- const sceneObjectBuffer = createBuffer(
12877
- device,
12878
- constants.buffer.STORAGE | constants.buffer.COPY_DST,
12879
- memory.sceneObjectBytes,
12880
- "plasius.wavefront.sceneObjects"
12881
- );
12882
- const triangleBuffer = createBuffer(
12883
- device,
12884
- constants.buffer.STORAGE | constants.buffer.COPY_DST,
12885
- memory.triangleBytes,
12886
- "plasius.wavefront.triangles"
12887
- );
12888
- const bvhNodeBuffer = createBuffer(
12889
- device,
12890
- constants.buffer.STORAGE | constants.buffer.COPY_DST,
12891
- memory.bvhCombinedBytes,
12892
- "plasius.wavefront.bvhNodes"
12893
- );
12894
- const meshVertexBuffer = createBuffer(
12895
- device,
12896
- constants.buffer.STORAGE | constants.buffer.COPY_DST,
12897
- memory.meshVertexBytes,
12898
- "plasius.wavefront.meshVertices"
12899
- );
12900
- const meshIndexBuffer = createBuffer(
12901
- device,
12902
- constants.buffer.STORAGE | constants.buffer.COPY_DST,
12903
- memory.meshIndexBytes,
12904
- "plasius.wavefront.meshIndices"
12905
- );
12906
- const meshRangeBuffer = createBuffer(
12907
- device,
12908
- constants.buffer.STORAGE | constants.buffer.COPY_DST,
12909
- memory.meshRangeBytes,
12910
- "plasius.wavefront.meshRanges"
12911
- );
12912
- const environmentPortalBuffer = createBuffer(
12913
- device,
12914
- constants.buffer.STORAGE | constants.buffer.COPY_DST,
12915
- memory.environmentPortalBytes,
12916
- "plasius.wavefront.environmentPortals"
12917
- );
12918
- const bvhLeafRefBuffer = createBuffer(
12919
- device,
12920
- constants.buffer.STORAGE | constants.buffer.COPY_DST,
12921
- memory.bvhLeafReferenceBytes,
12922
- "plasius.wavefront.bvhLeafRefs"
12923
- );
12924
- const tiles = createTiles(config.width, config.height, config.tileSize);
12925
- const configBufferStride = memory.configBufferStride;
12926
- const outputConfigSlotCount = config.deferredPathResolve ? 0 : tiles.length;
12927
- const frameConfigSlotCount = Math.max(
12928
- 1,
12929
- tiles.length * config.samplesPerPixel + outputConfigSlotCount + (config.denoise || config.progressive ? 1 : 0)
12930
- );
12931
- const configBuffer = createBuffer(
12932
- device,
12933
- constants.buffer.UNIFORM | constants.buffer.COPY_DST,
12934
- memory.frameConfigBytes,
12935
- "plasius.wavefront.frameConfig"
12936
- );
12937
- const bvhBuildConfigBuffer = createBuffer(
12938
- device,
12939
- constants.buffer.UNIFORM | constants.buffer.COPY_DST,
12940
- memory.bvhBuildConfigBytes,
12941
- "plasius.wavefront.bvhBuildConfig"
12942
- );
12943
- const counterBuffer = createBuffer(
12944
- device,
12945
- constants.buffer.STORAGE | constants.buffer.COPY_DST | constants.buffer.COPY_SRC,
12946
- memory.counterBytes,
12947
- "plasius.wavefront.counters"
12948
- );
12949
- const activeDispatchBuffer = createBuffer(
12950
- device,
12951
- constants.buffer.INDIRECT | constants.buffer.COPY_DST,
12952
- memory.indirectDispatchBytes,
12953
- "plasius.wavefront.activeDispatchArgs"
12954
- );
12955
- let packedScene = packWavefrontSceneObjects(config.sceneObjects, config.sceneObjectCapacity);
12956
- device.queue.writeBuffer(sceneObjectBuffer, 0, packedScene.buffer);
12957
- const packedTriangles = packWavefrontTriangles(
12958
- config.meshAcceleration.triangles,
12959
- Math.max(1, config.triangleCapacity)
12960
- );
12961
- const packedBvhNodes = packWavefrontBvhNodes(
12962
- config.meshAcceleration.nodes,
12963
- Math.max(1, config.bvhNodeCapacity + config.emissiveTriangleCapacity)
12964
- );
12965
- const packedBvhNodeUints = new Uint32Array(packedBvhNodes.buffer);
12966
- const packedBvhNodeFloats = new Float32Array(packedBvhNodes.buffer);
12967
- const orderedEmissiveTriangleIndices = resolveOrderedEmissiveTriangleIndices(config);
12968
- const emissiveWeights = orderedEmissiveTriangleIndices.map(
12969
- (triangleIndex) => emissiveTriangleWeight(config.meshAcceleration.triangles[triangleIndex])
12970
- );
12971
- const totalEmissiveWeight = emissiveWeights.reduce((total, weight) => total + weight, 0);
12972
- let cumulativeEmissiveWeight = 0;
12973
- orderedEmissiveTriangleIndices.forEach((triangleIndex, index) => {
12974
- const nodeOffset = (config.bvhNodeCapacity + index) * (BVH_NODE_RECORD_BYTES / 4);
12975
- cumulativeEmissiveWeight += emissiveWeights[index] ?? 0;
12976
- packedBvhNodeFloats[nodeOffset] = cumulativeEmissiveWeight;
12977
- packedBvhNodeFloats[nodeOffset + 1] = emissiveWeights[index] ?? 0;
12978
- packedBvhNodeFloats[nodeOffset + 2] = totalEmissiveWeight;
12979
- packedBvhNodeUints[nodeOffset + 8] = triangleIndex;
12980
- });
12981
- device.queue.writeBuffer(triangleBuffer, 0, packedTriangles.buffer);
12982
- device.queue.writeBuffer(bvhNodeBuffer, 0, packedBvhNodes.buffer);
12983
- device.queue.writeBuffer(meshVertexBuffer, 0, config.gpuMeshSource.vertices.buffer);
12984
- device.queue.writeBuffer(meshIndexBuffer, 0, config.gpuMeshSource.indices.buffer);
12985
- device.queue.writeBuffer(meshRangeBuffer, 0, config.gpuMeshSource.meshes.buffer);
12986
- const packedEnvironmentPortals = packEnvironmentPortals(
12987
- config.environmentPortals,
12988
- Math.max(1, config.environmentPortalCapacity)
12989
- );
12990
- device.queue.writeBuffer(environmentPortalBuffer, 0, packedEnvironmentPortals.buffer);
12991
- const radianceTexture = device.createTexture({
12992
- label: "plasius.wavefront.radiance",
12993
- size: { width: config.width, height: config.height },
12994
- format: "rgba16float",
12995
- usage: constants.texture.STORAGE_BINDING | constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
12996
- });
12997
- const radianceView = radianceTexture.createView();
12998
- const denoiseScratchTexture = device.createTexture({
12999
- label: "plasius.wavefront.denoiseScratch",
13000
- size: { width: config.width, height: config.height },
13001
- format: "rgba16float",
13002
- usage: constants.texture.STORAGE_BINDING | constants.texture.TEXTURE_BINDING
13003
- });
13004
- const denoiseScratchView = denoiseScratchTexture.createView();
13005
- const outputTexture = device.createTexture({
13006
- label: "plasius.wavefront.output",
13007
- size: { width: config.width, height: config.height },
13008
- format: "rgba8unorm",
13009
- usage: constants.texture.STORAGE_BINDING | constants.texture.TEXTURE_BINDING | constants.texture.COPY_SRC
13010
- });
13011
- const outputView = outputTexture.createView();
13012
- const sampler = device.createSampler({
13013
- label: "plasius.wavefront.presentSampler",
13014
- magFilter: "nearest",
13015
- minFilter: "nearest"
13016
- });
13017
- const environmentMapResource = createEnvironmentMapResource(
13018
- device,
13019
- constants,
13020
- config.environmentMap,
13021
- config.environmentColor
13022
- );
13023
- const environmentSamplingResource = createEnvironmentSamplingTextureResource(
13024
- device,
13025
- constants,
13026
- config.environmentMap,
13027
- config.environmentColor
13028
- );
13029
- let mediumTextureResource = createMediumTextureResource(
13030
- device,
13031
- constants,
13032
- config.mediums
13033
- );
13034
- config = Object.freeze({
13035
- ...config,
13036
- environmentMap: Object.freeze({
13037
- ...config.environmentMap,
13038
- width: environmentMapResource.width,
13039
- height: environmentMapResource.height,
13040
- mipLevelCount: environmentMapResource.mipLevelCount,
13041
- hasImportanceData: environmentSamplingResource.hasImportanceData
13042
- })
13043
- });
13044
- const brdfLutResource = createBrdfLutResource(device, constants);
13045
- const baseColorAtlasResource = createAtlasTextureResource(
13046
- device,
13047
- constants,
13048
- config.gpuMaterialSource.baseColorAtlas,
13049
- "plasius.wavefront.materialAtlas.baseColor"
13050
- );
13051
- const metallicRoughnessAtlasResource = createAtlasTextureResource(
13052
- device,
13053
- constants,
13054
- config.gpuMaterialSource.metallicRoughnessAtlas,
13055
- "plasius.wavefront.materialAtlas.metallicRoughness"
13056
- );
13057
- const normalAtlasResource = createAtlasTextureResource(
13058
- device,
13059
- constants,
13060
- config.gpuMaterialSource.normalAtlas,
13061
- "plasius.wavefront.materialAtlas.normal"
13062
- );
13063
- const occlusionAtlasResource = createAtlasTextureResource(
13064
- device,
13065
- constants,
13066
- config.gpuMaterialSource.occlusionAtlas,
13067
- "plasius.wavefront.materialAtlas.occlusion"
13068
- );
13069
- const emissiveAtlasResource = createAtlasTextureResource(
13070
- device,
13071
- constants,
13072
- config.gpuMaterialSource.emissiveAtlas,
13073
- "plasius.wavefront.materialAtlas.emissive"
13074
- );
13075
- const extensionAtlasNames = [
13076
- "clearcoat",
13077
- "clearcoatRoughness",
13078
- "clearcoatNormal",
13079
- "transmission",
13080
- "thickness",
13081
- "sheenColor",
13082
- "sheenRoughness",
13083
- "specular",
13084
- "specularColor",
13085
- "iridescence",
13086
- "iridescenceThickness",
13087
- "anisotropy"
13088
- ];
13089
- const extensionAtlasResources = Object.freeze(
13090
- Object.fromEntries(
13091
- extensionAtlasNames.map((name) => [
13092
- name,
13093
- createAtlasTextureResource(
13246
+ const allocationTracker = createGpuAllocationTracker(rawDevice, { budgetBytes: options.memoryBudgetBytes, storageUsage: constants.buffer.STORAGE });
13247
+ const device = allocationTracker.device;
13248
+ let context, finishAllocationScopes;
13249
+ try {
13250
+ let createTraceBindGroups = function() {
13251
+ return createWavefrontTraceBindGroups({
13252
+ device,
13253
+ traceBindGroupLayout,
13254
+ activeQueue,
13255
+ nextQueue,
13256
+ frameConfigBuffer: configBuffer,
13257
+ hitBuffer,
13258
+ accumulationBuffer,
13259
+ sceneObjectBuffer,
13260
+ counterBuffer,
13261
+ outputView,
13262
+ triangleBuffer,
13263
+ instanceView,
13264
+ bvhNodeBuffer,
13265
+ radianceView,
13266
+ environmentPortalBuffer,
13267
+ environmentMapResource,
13268
+ pathVertexBuffer,
13269
+ baseColorAtlasResource,
13270
+ metallicRoughnessAtlasResource,
13271
+ normalAtlasResource,
13272
+ occlusionAtlasResource,
13273
+ emissiveAtlasResource,
13274
+ materialAtlasSampler,
13275
+ brdfLutResource,
13276
+ environmentSamplingResource,
13277
+ mediumTextureResource,
13278
+ extensionAtlasResources
13279
+ });
13280
+ }, ensureFrameTelemetryResources = function() {
13281
+ if (!frameTelemetryResources) {
13282
+ frameTelemetryResources = createWavefrontFrameTelemetryResources({
13094
13283
  device,
13095
13284
  constants,
13096
- config.gpuMaterialSource.extensionAtlases[name],
13097
- `plasius.wavefront.materialAtlas.${name}`
13098
- )
13099
- ])
13100
- )
13101
- );
13102
- const materialAtlasSampler = device.createSampler({
13103
- label: "plasius.wavefront.materialAtlasSampler",
13104
- addressModeU: "clamp-to-edge",
13105
- addressModeV: "clamp-to-edge",
13106
- magFilter: "linear",
13107
- minFilter: "linear"
13108
- });
13109
- const {
13110
- bindGroupLayouts: {
13111
- trace: traceBindGroupLayout,
13112
- acceleration: accelerationBindGroupLayout,
13113
- denoiseRadiance: denoiseRadianceBindGroupLayout,
13114
- denoiseResolve: denoiseResolveBindGroupLayout,
13115
- present: presentBindGroupLayout
13116
- },
13117
- computePipelines: pipelines,
13118
- presentPipeline
13119
- } = await createWavefrontPipelineResources({ device, constants, format });
13120
- function createTraceBindGroups() {
13121
- return createWavefrontTraceBindGroups({
13122
- device,
13123
- traceBindGroupLayout,
13124
- activeQueue,
13125
- nextQueue,
13126
- frameConfigBuffer: configBuffer,
13127
- hitBuffer,
13128
- accumulationBuffer,
13129
- sceneObjectBuffer,
13130
- counterBuffer,
13131
- outputView,
13132
- triangleBuffer,
13133
- bvhNodeBuffer,
13134
- radianceView,
13135
- environmentPortalBuffer,
13136
- environmentMapResource,
13137
- pathVertexBuffer,
13138
- baseColorAtlasResource,
13139
- metallicRoughnessAtlasResource,
13140
- normalAtlasResource,
13141
- occlusionAtlasResource,
13142
- emissiveAtlasResource,
13143
- materialAtlasSampler,
13144
- brdfLutResource,
13145
- environmentSamplingResource,
13146
- mediumTextureResource,
13147
- extensionAtlasResources
13148
- });
13149
- }
13150
- let bindGroups = createTraceBindGroups();
13151
- const bvhBuildBindGroup = createWavefrontBvhBuildBindGroup({
13152
- device,
13153
- accelerationBindGroupLayout,
13154
- bvhBuildConfigBuffer,
13155
- triangleBuffer,
13156
- bvhNodeBuffer,
13157
- meshVertexBuffer,
13158
- meshIndexBuffer,
13159
- meshRangeBuffer,
13160
- bvhLeafRefBuffer
13161
- });
13162
- const {
13163
- radiance: denoiseRadianceBindGroup,
13164
- resolve: denoiseResolveBindGroup,
13165
- directResolve: denoiseDirectResolveBindGroup
13166
- } = createWavefrontDenoiseBindGroups({
13167
- device,
13168
- denoiseRadianceBindGroupLayout,
13169
- denoiseResolveBindGroupLayout,
13170
- configBuffer,
13171
- radianceView,
13172
- denoiseScratchView,
13173
- outputView
13174
- });
13175
- const presentBindGroup = createWavefrontPresentBindGroup({
13176
- device,
13177
- presentBindGroupLayout,
13178
- outputView,
13179
- sampler
13180
- });
13181
- const progressive = await createWavefrontProgressiveResources({ device, constants, config, configBuffer, radianceTexture, radianceView, outputView });
13182
- let activeFrameTelemetry = null;
13183
- let frameTelemetryResources = null;
13184
- const frameEncoder = createWavefrontFrameEncoder({
13185
- getConfig: () => config,
13186
- progressive,
13187
- getBindGroups: () => bindGroups,
13188
- pipelines,
13189
- counterBuffer,
13190
- activeDispatchBuffer,
13191
- denoiseRadianceBindGroup,
13192
- denoiseResolveBindGroup,
13193
- denoiseDirectResolveBindGroup,
13194
- presentPipeline,
13195
- presentBindGroup,
13196
- context,
13197
- getFrameTelemetry: () => activeFrameTelemetry
13198
- });
13199
- let frame = 0;
13200
- let accelerationBuilt = !config.gpuAccelerationBuildRequired;
13201
- let accelerationBuildCount = 0;
13202
- let activeCameraOptions = options.camera ?? DEFAULT_CAMERA2;
13203
- let lastCompletedFrameTimeMs = null;
13204
- let lastCompletedSamplesPerPixel = Math.max(1, config.samplesPerPixel);
13205
- let lastGpuParallelism = createGpuParallelismDiagnostics(
13206
- gpuAdapterParallelism,
13207
- createGpuParallelismCounters()
13208
- );
13209
- const waitForSubmittedGpuWork2 = (waitOptions = {}) => waitForSubmittedGpuWork(device, waitOptions);
13210
- function ensureFrameTelemetryResources() {
13211
- if (!frameTelemetryResources) {
13212
- frameTelemetryResources = createWavefrontFrameTelemetryResources({
13285
+ maxRayCountRecords: tiles.length * config.samplesPerPixel * config.maxDepth
13286
+ });
13287
+ }
13288
+ return frameTelemetryResources;
13289
+ }, resolveRenderedSamplesPerPixel = function(renderOptions = {}, awaitGPUCompletion = true) {
13290
+ return resolveWavefrontRenderedSamplesPerPixel({
13291
+ config,
13292
+ renderOptions,
13293
+ awaitGPUCompletion,
13294
+ lastCompletedFrameTimeMs,
13295
+ lastCompletedSamplesPerPixel
13296
+ });
13297
+ }, createFrameStats = function({
13298
+ frameIndex,
13299
+ accelerationBuildSubmitted,
13300
+ frameSubmissionCount,
13301
+ parallelismCounters,
13302
+ renderedSamplesPerPixel,
13303
+ targetSamplesPerPixel,
13304
+ frameTimeBudgetMs,
13305
+ budgetConstrained
13306
+ }) {
13307
+ const stats = createWavefrontFrameStats({
13308
+ config,
13309
+ tiles,
13310
+ frame,
13311
+ frameIndex,
13312
+ accelerationBuilt,
13313
+ accelerationBuildCount,
13314
+ accelerationBuildSubmitted,
13315
+ frameSubmissionCount,
13316
+ frameConfigSlotCount,
13317
+ gpuAdapterParallelism,
13318
+ parallelismCounters,
13319
+ renderedSamplesPerPixel,
13320
+ targetSamplesPerPixel,
13321
+ frameTimeBudgetMs,
13322
+ budgetConstrained
13323
+ });
13324
+ lastGpuParallelism = stats.gpuParallelism;
13325
+ return stats.frameStats;
13326
+ }, writeFrameConfigSlot = function(slot, tile, frameIndex, buildRange = {}) {
13327
+ if (slot >= frameConfigSlotCount) {
13328
+ throw new Error("Wavefront frame config slot capacity exceeded.");
13329
+ }
13330
+ const offset = slot * configBufferStride;
13331
+ device.queue.writeBuffer(
13332
+ configBuffer,
13333
+ offset,
13334
+ createConfigPayload(config, tile, frameIndex, buildRange)
13335
+ );
13336
+ return offset;
13337
+ }, createFrameConfigWriter = function(frameIndex) {
13338
+ let slot = 0;
13339
+ return (tile, buildRange = {}) => {
13340
+ const offset = writeFrameConfigSlot(slot, tile, frameIndex, buildRange);
13341
+ slot += 1;
13342
+ return offset;
13343
+ };
13344
+ }, dispatchGpuAccelerationBuild = function(frameIndex, parallelism) {
13345
+ const submitted = dispatchWavefrontGpuAccelerationBuild({
13346
+ config,
13347
+ accelerationBuilt,
13348
+ tiles,
13213
13349
  device,
13214
- constants,
13215
- maxRayCountRecords: tiles.length * config.samplesPerPixel * config.maxDepth
13350
+ bvhBuildConfigBuffer,
13351
+ configBufferStride,
13352
+ bvhBuildBindGroup,
13353
+ pipelines,
13354
+ parallelism,
13355
+ frameIndex
13356
+ });
13357
+ if (submitted) {
13358
+ accelerationBuilt = true;
13359
+ accelerationBuildCount += 1;
13360
+ }
13361
+ return submitted;
13362
+ }, dispatchFrame = function(frameIndex, parallelism, renderedSamplesPerPixel = config.samplesPerPixel) {
13363
+ return dispatchWavefrontFrame({
13364
+ config,
13365
+ tiles,
13366
+ device,
13367
+ frameIndex,
13368
+ parallelism,
13369
+ renderedSamplesPerPixel,
13370
+ frameEncoder,
13371
+ createFrameConfigWriter
13372
+ });
13373
+ }, renderOnce = function(renderOptions = {}, resolvedSamplingPlan = null) {
13374
+ const frameStartTimeMs = nowMs3();
13375
+ frame += 1;
13376
+ const frameIndex = frame + config.frameIndex;
13377
+ const samplingPlan = resolvedSamplingPlan ?? resolveRenderedSamplesPerPixel(renderOptions, false);
13378
+ const parallelismCounters = createGpuParallelismCounters();
13379
+ const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex, parallelismCounters);
13380
+ const frameSubmissionCount = dispatchFrame(
13381
+ frameIndex,
13382
+ parallelismCounters,
13383
+ samplingPlan.renderedSamplesPerPixel
13384
+ );
13385
+ const frameTimeMs = Math.max(0, nowMs3() - frameStartTimeMs);
13386
+ return Object.freeze({
13387
+ ...createFrameStats({
13388
+ frameIndex,
13389
+ accelerationBuildSubmitted,
13390
+ frameSubmissionCount,
13391
+ parallelismCounters,
13392
+ renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel,
13393
+ targetSamplesPerPixel: samplingPlan.targetSamplesPerPixel,
13394
+ frameTimeBudgetMs: samplingPlan.frameTimeBudgetMs,
13395
+ budgetConstrained: samplingPlan.budgetConstrained
13396
+ }),
13397
+ gpuWorkerJobs: createGpuWorkerJobDiagnostics(
13398
+ lastGpuParallelism,
13399
+ frameSubmissionCount + (accelerationBuildSubmitted ? 1 : 0),
13400
+ frameTimeMs,
13401
+ false
13402
+ ),
13403
+ timings: createWavefrontFrameTimingTelemetry({
13404
+ status: "not-requested",
13405
+ source: "cpu-submit",
13406
+ timestampQueryStatus: "not-recorded",
13407
+ totalRenderJobTimeMs: frameTimeMs
13408
+ })
13409
+ });
13410
+ }, dispatchFrameAwaitingGpu = function(frameIndex, parallelism, renderedSamplesPerPixel = config.samplesPerPixel, optionsForFrame = {}) {
13411
+ return dispatchWavefrontFrameAwaitingGpu({
13412
+ config,
13413
+ tiles,
13414
+ device,
13415
+ frameIndex,
13416
+ parallelism,
13417
+ renderedSamplesPerPixel,
13418
+ frameEncoder,
13419
+ writeFrameConfigSlot,
13420
+ optionsForFrame
13421
+ });
13422
+ }, rebuildLiveConfig = function(overrides = {}) {
13423
+ return createWavefrontPathTracingComputeConfig({
13424
+ ...options,
13425
+ canvas,
13426
+ width: config.width,
13427
+ height: config.height,
13428
+ maxDepth: config.maxDepth,
13429
+ tileSize: config.tileSize,
13430
+ samplesPerPixel: config.samplesPerPixel,
13431
+ sceneObjectCapacity: config.sceneObjectCapacity,
13432
+ sceneObjects: packedScene.objects,
13433
+ camera: activeCameraOptions,
13434
+ environmentMap: {
13435
+ ...config.environmentMap
13436
+ },
13437
+ strictPhysicalLowSppLighting: config.strictPhysicalLowSppLighting,
13438
+ featureFlags: options.featureFlags,
13439
+ frameIndex: config.frameIndex,
13440
+ ...overrides
13441
+ });
13442
+ }, rebuildMediumResources = function(nextConfig) {
13443
+ const previousMediumTextureResource = mediumTextureResource;
13444
+ mediumTextureResource = createMediumTextureResource(device, constants, nextConfig.mediums);
13445
+ bindGroups = createTraceBindGroups();
13446
+ if (previousMediumTextureResource?.ownsTexture) {
13447
+ previousMediumTextureResource.texture?.destroy?.();
13448
+ }
13449
+ }, updateSceneObjects = function(sceneObjects) {
13450
+ progressive?.reset();
13451
+ const nextPackedScene = packWavefrontSceneObjects(sceneObjects, config.sceneObjectCapacity);
13452
+ packedScene = nextPackedScene;
13453
+ const nextConfig = rebuildLiveConfig();
13454
+ if (!mediumTablesEqual(config.mediums, nextConfig.mediums)) {
13455
+ rebuildMediumResources(nextConfig);
13456
+ }
13457
+ config = nextConfig;
13458
+ device.queue.writeBuffer(sceneObjectBuffer, 0, packedScene.buffer);
13459
+ return config;
13460
+ }, updateCamera = function(cameraOptions = {}) {
13461
+ progressive?.reset();
13462
+ activeCameraOptions = cameraOptions;
13463
+ config = updateWavefrontCameraConfig(config, cameraOptions);
13464
+ return config;
13465
+ }, getSnapshot = function() {
13466
+ return Object.freeze({
13467
+ frame,
13468
+ accumulatedSamplesPerPixel: progressive?.samples ?? 0,
13469
+ width: config.width,
13470
+ height: config.height,
13471
+ maxDepth: config.maxDepth,
13472
+ tiles: tiles.length,
13473
+ tileSize: config.tileSize,
13474
+ samplesPerPixel: config.samplesPerPixel,
13475
+ maxFramePassesPerSubmission: config.maxFramePassesPerSubmission,
13476
+ sceneObjectCount: config.sceneObjectCount,
13477
+ triangleCount: config.triangleCount,
13478
+ instanceCount: config.instancing?.instanceCount ?? 0,
13479
+ geometryCount: config.instancing?.geometryCount ?? config.gpuMeshSource.meshes.count,
13480
+ emissiveTriangleCount: config.emissiveTriangleCount,
13481
+ environmentPortalCount: config.environmentPortalCount,
13482
+ environmentPortalMode: config.environmentPortalMode,
13483
+ mediumCount: config.mediumCount,
13484
+ environmentMap: createEnvironmentMapSnapshot(config.environmentMap),
13485
+ deferredPathResolve: config.deferredPathResolve,
13486
+ strictPhysicalLowSppLighting: config.strictPhysicalLowSppLighting,
13487
+ transportExperiments: config.transportExperiments,
13488
+ transportExperimentFlags: config.transportExperimentFlags,
13489
+ bvhNodeCount: config.bvhNodeCount,
13490
+ displayQuality: config.displayQuality,
13491
+ accelerationBuildMode: config.accelerationBuildMode,
13492
+ gpuAccelerationBuildRequired: config.gpuAccelerationBuildRequired,
13493
+ accelerationBuilt,
13494
+ accelerationBuildCount,
13495
+ frameConfigSlots: frameConfigSlotCount,
13496
+ gpuParallelism: lastGpuParallelism,
13497
+ memory: config.memory,
13498
+ allocations: allocationTracker.snapshot()
13499
+ });
13500
+ }, destroy = function() {
13501
+ progressive?.destroy();
13502
+ frameTelemetryResources?.destroy?.();
13503
+ activeQueue.destroy?.();
13504
+ nextQueue.destroy?.();
13505
+ hitBuffer.destroy?.();
13506
+ accumulationBuffer.destroy?.();
13507
+ pathVertexBuffer.destroy?.();
13508
+ sceneObjectBuffer.destroy?.();
13509
+ triangleBuffer.destroy?.();
13510
+ instanceTexture?.destroy?.();
13511
+ bvhNodeBuffer.destroy?.();
13512
+ meshVertexBuffer.destroy?.();
13513
+ meshIndexBuffer.destroy?.();
13514
+ meshRangeBuffer.destroy?.();
13515
+ environmentPortalBuffer.destroy?.();
13516
+ bvhLeafRefBuffer.destroy?.();
13517
+ configBuffer.destroy?.();
13518
+ bvhBuildConfigBuffer.destroy?.();
13519
+ counterBuffer.destroy?.();
13520
+ activeDispatchBuffer.destroy?.();
13521
+ radianceTexture.destroy?.();
13522
+ denoiseScratchTexture.destroy?.();
13523
+ outputTexture.destroy?.();
13524
+ if (environmentMapResource.ownsTexture) {
13525
+ environmentMapResource.texture?.destroy?.();
13526
+ }
13527
+ if (environmentSamplingResource.ownsTexture) {
13528
+ environmentSamplingResource.texture?.destroy?.();
13529
+ }
13530
+ if (mediumTextureResource.ownsTexture) {
13531
+ mediumTextureResource.texture?.destroy?.();
13532
+ }
13533
+ brdfLutResource.texture?.destroy?.();
13534
+ if (baseColorAtlasResource.ownsTexture) {
13535
+ baseColorAtlasResource.texture?.destroy?.();
13536
+ }
13537
+ if (metallicRoughnessAtlasResource.ownsTexture) {
13538
+ metallicRoughnessAtlasResource.texture?.destroy?.();
13539
+ }
13540
+ if (normalAtlasResource.ownsTexture) {
13541
+ normalAtlasResource.texture?.destroy?.();
13542
+ }
13543
+ if (occlusionAtlasResource.ownsTexture) {
13544
+ occlusionAtlasResource.texture?.destroy?.();
13545
+ }
13546
+ if (emissiveAtlasResource.ownsTexture) {
13547
+ emissiveAtlasResource.texture?.destroy?.();
13548
+ }
13549
+ Object.values(extensionAtlasResources).forEach((resource) => {
13550
+ if (resource.ownsTexture) {
13551
+ resource.texture?.destroy?.();
13552
+ }
13553
+ });
13554
+ context.unconfigure?.();
13555
+ allocationTracker.destroy();
13556
+ };
13557
+ finishAllocationScopes = openGpuAllocationErrorScopes(rawDevice);
13558
+ const gpuAdapterParallelism = createGpuAdapterParallelismDiagnostics(adapter, device);
13559
+ context = canvas.getContext("webgpu");
13560
+ if (!context || typeof context.configure !== "function") {
13561
+ throw new Error("Canvas WebGPU context does not support configure().");
13562
+ }
13563
+ const format = options.format ?? (typeof navigatorRef.gpu.getPreferredCanvasFormat === "function" ? navigatorRef.gpu.getPreferredCanvasFormat() : "bgra8unorm");
13564
+ let config = initialConfig;
13565
+ const safeTileSize = clampTileSizeForDevice(config, device);
13566
+ if (safeTileSize !== config.tileSize) {
13567
+ config = createWavefrontPathTracingComputeConfig({
13568
+ ...options,
13569
+ canvas,
13570
+ width: config.width,
13571
+ height: config.height,
13572
+ tileSize: safeTileSize,
13573
+ tilePixelCapacity: Math.min(
13574
+ config.tilePixelCapacity,
13575
+ safeTileSize * safeTileSize
13576
+ )
13216
13577
  });
13217
13578
  }
13218
- return frameTelemetryResources;
13219
- }
13220
- function resolveRenderedSamplesPerPixel(renderOptions = {}, awaitGPUCompletion = true) {
13221
- return resolveWavefrontRenderedSamplesPerPixel({
13222
- config,
13223
- renderOptions,
13224
- awaitGPUCompletion,
13225
- lastCompletedFrameTimeMs,
13226
- lastCompletedSamplesPerPixel
13579
+ config = Object.freeze({
13580
+ ...config,
13581
+ memory: estimateWavefrontPathTracingMemoryForConfig(config, device.limits)
13582
+ });
13583
+ canvas.width = config.width;
13584
+ canvas.height = config.height;
13585
+ context.configure({
13586
+ device: rawDevice,
13587
+ format,
13588
+ alphaMode: options.alpha === true ? "premultiplied" : "opaque"
13589
+ });
13590
+ const bufferUsage = constants.buffer.STORAGE | constants.buffer.COPY_DST;
13591
+ const memory = config.memory;
13592
+ let instanceTexture = null, instanceView = null;
13593
+ if (config.instancing) {
13594
+ const count = config.instancing.instanceCount;
13595
+ instanceTexture = device.createTexture({
13596
+ label: "plasius.wavefront.instances",
13597
+ size: { width: 9, height: count },
13598
+ format: "rgba32uint",
13599
+ usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
13600
+ });
13601
+ const upload = new Uint8Array(count * 256), source = new Uint8Array(config.instancing.buffer);
13602
+ for (let i = 0; i < count; i++) upload.set(source.subarray(i * 144, (i + 1) * 144), i * 256);
13603
+ device.queue.writeTexture({ texture: instanceTexture }, upload, { bytesPerRow: 256, rowsPerImage: count }, { width: 9, height: count, depthOrArrayLayers: 1 });
13604
+ instanceView = instanceTexture.createView();
13605
+ }
13606
+ const activeQueue = createBuffer(device, bufferUsage, memory.queueBytes, "plasius.wavefront.activeQueue");
13607
+ const nextQueue = createBuffer(device, bufferUsage, memory.queueBytes, "plasius.wavefront.nextQueue");
13608
+ const hitBuffer = createBuffer(device, bufferUsage, memory.hitBytes, "plasius.wavefront.hitBuffer");
13609
+ const accumulationBuffer = createBuffer(
13610
+ device,
13611
+ bufferUsage,
13612
+ memory.accumulationBytes,
13613
+ "plasius.wavefront.accumulation"
13614
+ );
13615
+ const pathVertexBuffer = createBuffer(
13616
+ device,
13617
+ bufferUsage,
13618
+ memory.pathVertexBytes,
13619
+ "plasius.wavefront.pathVertices"
13620
+ );
13621
+ const sceneObjectBuffer = createBuffer(
13622
+ device,
13623
+ constants.buffer.STORAGE | constants.buffer.COPY_DST,
13624
+ memory.sceneObjectBytes,
13625
+ "plasius.wavefront.sceneObjects"
13626
+ );
13627
+ const triangleBuffer = createBuffer(
13628
+ device,
13629
+ constants.buffer.STORAGE | constants.buffer.COPY_DST,
13630
+ memory.triangleBytes,
13631
+ "plasius.wavefront.triangles"
13632
+ );
13633
+ const bvhNodeBuffer = createBuffer(
13634
+ device,
13635
+ constants.buffer.STORAGE | constants.buffer.COPY_DST,
13636
+ memory.bvhCombinedBytes,
13637
+ "plasius.wavefront.bvhNodes"
13638
+ );
13639
+ const meshVertexBuffer = createBuffer(
13640
+ device,
13641
+ constants.buffer.STORAGE | constants.buffer.COPY_DST,
13642
+ memory.meshVertexBytes,
13643
+ "plasius.wavefront.meshVertices"
13644
+ );
13645
+ const meshIndexBuffer = createBuffer(
13646
+ device,
13647
+ constants.buffer.STORAGE | constants.buffer.COPY_DST,
13648
+ memory.meshIndexBytes,
13649
+ "plasius.wavefront.meshIndices"
13650
+ );
13651
+ const meshRangeBuffer = createBuffer(
13652
+ device,
13653
+ constants.buffer.STORAGE | constants.buffer.COPY_DST,
13654
+ memory.meshRangeBytes,
13655
+ "plasius.wavefront.meshRanges"
13656
+ );
13657
+ const environmentPortalBuffer = createBuffer(
13658
+ device,
13659
+ constants.buffer.STORAGE | constants.buffer.COPY_DST,
13660
+ memory.environmentPortalBytes,
13661
+ "plasius.wavefront.environmentPortals"
13662
+ );
13663
+ const bvhLeafRefBuffer = createBuffer(
13664
+ device,
13665
+ constants.buffer.STORAGE | constants.buffer.COPY_DST,
13666
+ memory.bvhLeafReferenceBytes,
13667
+ "plasius.wavefront.bvhLeafRefs"
13668
+ );
13669
+ const tiles = createTiles(config.width, config.height, config.tileSize);
13670
+ const configBufferStride = memory.configBufferStride;
13671
+ const outputConfigSlotCount = config.deferredPathResolve ? 0 : tiles.length;
13672
+ const frameConfigSlotCount = Math.max(
13673
+ 1,
13674
+ tiles.length * config.samplesPerPixel + outputConfigSlotCount + (config.denoise || config.progressive ? 1 : 0)
13675
+ );
13676
+ const configBuffer = createBuffer(
13677
+ device,
13678
+ constants.buffer.UNIFORM | constants.buffer.COPY_DST,
13679
+ memory.frameConfigBytes,
13680
+ "plasius.wavefront.frameConfig"
13681
+ );
13682
+ const bvhBuildConfigBuffer = createBuffer(
13683
+ device,
13684
+ constants.buffer.UNIFORM | constants.buffer.COPY_DST,
13685
+ memory.bvhBuildConfigBytes,
13686
+ "plasius.wavefront.bvhBuildConfig"
13687
+ );
13688
+ const counterBuffer = createBuffer(
13689
+ device,
13690
+ constants.buffer.STORAGE | constants.buffer.COPY_DST | constants.buffer.COPY_SRC,
13691
+ memory.counterBytes,
13692
+ "plasius.wavefront.counters"
13693
+ );
13694
+ const activeDispatchBuffer = createBuffer(
13695
+ device,
13696
+ constants.buffer.INDIRECT | constants.buffer.COPY_DST,
13697
+ memory.indirectDispatchBytes,
13698
+ "plasius.wavefront.activeDispatchArgs"
13699
+ );
13700
+ let packedScene = packWavefrontSceneObjects(config.sceneObjects, config.sceneObjectCapacity);
13701
+ device.queue.writeBuffer(sceneObjectBuffer, 0, packedScene.buffer);
13702
+ const packedTriangles = packWavefrontTriangles(
13703
+ config.meshAcceleration.triangles,
13704
+ Math.max(1, config.triangleCapacity)
13705
+ );
13706
+ const packedBvhNodes = packWavefrontBvhNodes(
13707
+ config.meshAcceleration.nodes,
13708
+ Math.max(1, config.bvhNodeCapacity + config.emissiveTriangleCapacity)
13709
+ );
13710
+ const packedBvhNodeUints = new Uint32Array(packedBvhNodes.buffer);
13711
+ const packedBvhNodeFloats = new Float32Array(packedBvhNodes.buffer);
13712
+ const orderedEmissiveTriangleIndices = resolveOrderedEmissiveTriangleIndices(config);
13713
+ const emissiveWeights = orderedEmissiveTriangleIndices.map(
13714
+ (triangleIndex) => emissiveTriangleWeight(config.meshAcceleration.triangles[triangleIndex])
13715
+ );
13716
+ const totalEmissiveWeight = emissiveWeights.reduce((total, weight) => total + weight, 0);
13717
+ let cumulativeEmissiveWeight = 0;
13718
+ orderedEmissiveTriangleIndices.forEach((triangleIndex, index) => {
13719
+ const nodeOffset = (config.bvhNodeCapacity + index) * (BVH_NODE_RECORD_BYTES / 4);
13720
+ cumulativeEmissiveWeight += emissiveWeights[index] ?? 0;
13721
+ packedBvhNodeFloats[nodeOffset] = cumulativeEmissiveWeight;
13722
+ packedBvhNodeFloats[nodeOffset + 1] = emissiveWeights[index] ?? 0;
13723
+ packedBvhNodeFloats[nodeOffset + 2] = totalEmissiveWeight;
13724
+ packedBvhNodeUints[nodeOffset + 8] = triangleIndex;
13227
13725
  });
13228
- }
13229
- function createFrameStats({
13230
- frameIndex,
13231
- accelerationBuildSubmitted,
13232
- frameSubmissionCount,
13233
- parallelismCounters,
13234
- renderedSamplesPerPixel,
13235
- targetSamplesPerPixel,
13236
- frameTimeBudgetMs,
13237
- budgetConstrained
13238
- }) {
13239
- const stats = createWavefrontFrameStats({
13240
- config,
13241
- tiles,
13242
- frame,
13243
- frameIndex,
13244
- accelerationBuilt,
13245
- accelerationBuildCount,
13246
- accelerationBuildSubmitted,
13247
- frameSubmissionCount,
13248
- frameConfigSlotCount,
13249
- gpuAdapterParallelism,
13250
- parallelismCounters,
13251
- renderedSamplesPerPixel,
13252
- targetSamplesPerPixel,
13253
- frameTimeBudgetMs,
13254
- budgetConstrained
13726
+ await uploadWavefrontBuffer(device, triangleBuffer, packedTriangles.buffer);
13727
+ await uploadWavefrontBuffer(device, bvhNodeBuffer, packedBvhNodes.buffer);
13728
+ await uploadWavefrontBuffer(device, meshVertexBuffer, config.gpuMeshSource.vertices.buffer);
13729
+ await uploadWavefrontBuffer(device, meshIndexBuffer, config.gpuMeshSource.indices.buffer);
13730
+ await uploadWavefrontBuffer(device, meshRangeBuffer, config.gpuMeshSource.meshes.buffer);
13731
+ const packedEnvironmentPortals = packEnvironmentPortals(
13732
+ config.environmentPortals,
13733
+ Math.max(1, config.environmentPortalCapacity)
13734
+ );
13735
+ device.queue.writeBuffer(environmentPortalBuffer, 0, packedEnvironmentPortals.buffer);
13736
+ const radianceTexture = device.createTexture({
13737
+ label: "plasius.wavefront.radiance",
13738
+ size: { width: config.width, height: config.height },
13739
+ format: "rgba16float",
13740
+ usage: constants.texture.STORAGE_BINDING | constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
13255
13741
  });
13256
- lastGpuParallelism = stats.gpuParallelism;
13257
- return stats.frameStats;
13258
- }
13259
- async function readTerminationMetrics() {
13260
- return readWavefrontTerminationMetrics({
13742
+ const radianceView = radianceTexture.createView();
13743
+ const denoiseScratchTexture = device.createTexture({
13744
+ label: "plasius.wavefront.denoiseScratch",
13745
+ size: { width: config.width, height: config.height },
13746
+ format: "rgba16float",
13747
+ usage: constants.texture.STORAGE_BINDING | constants.texture.TEXTURE_BINDING
13748
+ });
13749
+ const denoiseScratchView = denoiseScratchTexture.createView();
13750
+ const outputTexture = device.createTexture({
13751
+ label: "plasius.wavefront.output",
13752
+ size: { width: config.width, height: config.height },
13753
+ format: "rgba8unorm",
13754
+ usage: constants.texture.STORAGE_BINDING | constants.texture.TEXTURE_BINDING | constants.texture.COPY_SRC
13755
+ });
13756
+ const outputView = outputTexture.createView();
13757
+ const sampler = device.createSampler({
13758
+ label: "plasius.wavefront.presentSampler",
13759
+ magFilter: "nearest",
13760
+ minFilter: "nearest"
13761
+ });
13762
+ const environmentMapResource = createEnvironmentMapResource(
13261
13763
  device,
13262
13764
  constants,
13263
- counterBuffer,
13264
- waitForSubmittedGpuWork: waitForSubmittedGpuWork2
13265
- });
13266
- }
13267
- function writeFrameConfigSlot(slot, tile, frameIndex, buildRange = {}) {
13268
- if (slot >= frameConfigSlotCount) {
13269
- throw new Error("Wavefront frame config slot capacity exceeded.");
13270
- }
13271
- const offset = slot * configBufferStride;
13272
- device.queue.writeBuffer(
13273
- configBuffer,
13274
- offset,
13275
- createConfigPayload(config, tile, frameIndex, buildRange)
13765
+ config.environmentMap,
13766
+ config.environmentColor
13276
13767
  );
13277
- return offset;
13278
- }
13279
- function createFrameConfigWriter(frameIndex) {
13280
- let slot = 0;
13281
- return (tile, buildRange = {}) => {
13282
- const offset = writeFrameConfigSlot(slot, tile, frameIndex, buildRange);
13283
- slot += 1;
13284
- return offset;
13285
- };
13286
- }
13287
- function dispatchGpuAccelerationBuild(frameIndex, parallelism) {
13288
- const submitted = dispatchWavefrontGpuAccelerationBuild({
13289
- config,
13290
- accelerationBuilt,
13291
- tiles,
13768
+ const environmentSamplingResource = createEnvironmentSamplingTextureResource(
13292
13769
  device,
13293
- bvhBuildConfigBuffer,
13294
- configBufferStride,
13295
- bvhBuildBindGroup,
13296
- pipelines,
13297
- parallelism,
13298
- frameIndex
13299
- });
13300
- if (submitted) {
13301
- accelerationBuilt = true;
13302
- accelerationBuildCount += 1;
13303
- }
13304
- return submitted;
13305
- }
13306
- function dispatchFrame(frameIndex, parallelism, renderedSamplesPerPixel = config.samplesPerPixel) {
13307
- return dispatchWavefrontFrame({
13308
- config,
13309
- tiles,
13770
+ constants,
13771
+ config.environmentMap,
13772
+ config.environmentColor
13773
+ );
13774
+ let mediumTextureResource = createMediumTextureResource(
13310
13775
  device,
13311
- frameIndex,
13312
- parallelism,
13313
- renderedSamplesPerPixel,
13314
- frameEncoder,
13315
- createFrameConfigWriter
13316
- });
13317
- }
13318
- function renderOnce(renderOptions = {}, resolvedSamplingPlan = null) {
13319
- const frameStartTimeMs = nowMs3();
13320
- frame += 1;
13321
- const frameIndex = frame + config.frameIndex;
13322
- const samplingPlan = resolvedSamplingPlan ?? resolveRenderedSamplesPerPixel(renderOptions, false);
13323
- const parallelismCounters = createGpuParallelismCounters();
13324
- const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex, parallelismCounters);
13325
- const frameSubmissionCount = dispatchFrame(
13326
- frameIndex,
13327
- parallelismCounters,
13328
- samplingPlan.renderedSamplesPerPixel
13776
+ constants,
13777
+ config.mediums
13329
13778
  );
13330
- const frameTimeMs = Math.max(0, nowMs3() - frameStartTimeMs);
13331
- return Object.freeze({
13332
- ...createFrameStats({
13333
- frameIndex,
13334
- accelerationBuildSubmitted,
13335
- frameSubmissionCount,
13336
- parallelismCounters,
13337
- renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel,
13338
- targetSamplesPerPixel: samplingPlan.targetSamplesPerPixel,
13339
- frameTimeBudgetMs: samplingPlan.frameTimeBudgetMs,
13340
- budgetConstrained: samplingPlan.budgetConstrained
13341
- }),
13342
- gpuWorkerJobs: createGpuWorkerJobDiagnostics(
13343
- lastGpuParallelism,
13344
- frameSubmissionCount + (accelerationBuildSubmitted ? 1 : 0),
13345
- frameTimeMs,
13346
- false
13347
- ),
13348
- timings: createWavefrontFrameTimingTelemetry({
13349
- status: "not-requested",
13350
- source: "cpu-submit",
13351
- timestampQueryStatus: "not-recorded",
13352
- totalRenderJobTimeMs: frameTimeMs
13779
+ config = Object.freeze({
13780
+ ...config,
13781
+ environmentMap: Object.freeze({
13782
+ ...config.environmentMap,
13783
+ width: environmentMapResource.width,
13784
+ height: environmentMapResource.height,
13785
+ mipLevelCount: environmentMapResource.mipLevelCount,
13786
+ hasImportanceData: environmentSamplingResource.hasImportanceData
13353
13787
  })
13354
13788
  });
13355
- }
13356
- function dispatchFrameAwaitingGpu(frameIndex, parallelism, renderedSamplesPerPixel = config.samplesPerPixel, optionsForFrame = {}) {
13357
- return dispatchWavefrontFrameAwaitingGpu({
13358
- config,
13359
- tiles,
13789
+ const brdfLutResource = createBrdfLutResource(device, constants);
13790
+ const baseColorAtlasResource = createAtlasTextureResource(
13360
13791
  device,
13361
- frameIndex,
13362
- parallelism,
13363
- renderedSamplesPerPixel,
13364
- frameEncoder,
13365
- writeFrameConfigSlot,
13366
- optionsForFrame
13367
- });
13368
- }
13369
- async function readOutputProbe(optionsForProbe = {}) {
13370
- return readWavefrontOutputProbe({
13792
+ constants,
13793
+ config.gpuMaterialSource.baseColorAtlas,
13794
+ "plasius.wavefront.materialAtlas.baseColor"
13795
+ );
13796
+ const metallicRoughnessAtlasResource = createAtlasTextureResource(
13371
13797
  device,
13372
13798
  constants,
13373
- config,
13374
- outputTexture,
13375
- waitForSubmittedGpuWork: waitForSubmittedGpuWork2,
13376
- optionsForProbe
13799
+ config.gpuMaterialSource.metallicRoughnessAtlas,
13800
+ "plasius.wavefront.materialAtlas.metallicRoughness"
13801
+ );
13802
+ const normalAtlasResource = createAtlasTextureResource(
13803
+ device,
13804
+ constants,
13805
+ config.gpuMaterialSource.normalAtlas,
13806
+ "plasius.wavefront.materialAtlas.normal"
13807
+ );
13808
+ const occlusionAtlasResource = createAtlasTextureResource(
13809
+ device,
13810
+ constants,
13811
+ config.gpuMaterialSource.occlusionAtlas,
13812
+ "plasius.wavefront.materialAtlas.occlusion"
13813
+ );
13814
+ const emissiveAtlasResource = createAtlasTextureResource(
13815
+ device,
13816
+ constants,
13817
+ config.gpuMaterialSource.emissiveAtlas,
13818
+ "plasius.wavefront.materialAtlas.emissive"
13819
+ );
13820
+ const extensionAtlasNames = [
13821
+ "clearcoat",
13822
+ "clearcoatRoughness",
13823
+ "clearcoatNormal",
13824
+ "transmission",
13825
+ "thickness",
13826
+ "sheenColor",
13827
+ "sheenRoughness",
13828
+ "specular",
13829
+ "specularColor",
13830
+ "iridescence",
13831
+ "iridescenceThickness",
13832
+ "anisotropy"
13833
+ ];
13834
+ const extensionAtlasResources = Object.freeze(
13835
+ Object.fromEntries(
13836
+ extensionAtlasNames.map((name) => [
13837
+ name,
13838
+ createAtlasTextureResource(
13839
+ device,
13840
+ constants,
13841
+ config.gpuMaterialSource.extensionAtlases[name],
13842
+ `plasius.wavefront.materialAtlas.${name}`
13843
+ )
13844
+ ])
13845
+ )
13846
+ );
13847
+ const materialAtlasSampler = device.createSampler({
13848
+ label: "plasius.wavefront.materialAtlasSampler",
13849
+ addressModeU: "clamp-to-edge",
13850
+ addressModeV: "clamp-to-edge",
13851
+ magFilter: "linear",
13852
+ minFilter: "linear"
13377
13853
  });
13378
- }
13379
- async function renderFrame(renderOptions = {}) {
13380
- const awaitGPUCompletion = renderOptions.awaitGPUCompletion !== false;
13381
- const samplingPlan = resolveRenderedSamplesPerPixel(renderOptions, awaitGPUCompletion);
13382
- const useThrottledHighSamplePath = awaitGPUCompletion && samplingPlan.renderedSamplesPerPixel >= 8;
13383
- const telemetryRequested = renderOptions.readStats === true;
13384
- const telemetryCanRecord = telemetryRequested && awaitGPUCompletion;
13385
- const telemetryResourcesForFrame = telemetryCanRecord ? ensureFrameTelemetryResources() : null;
13386
- if (telemetryResourcesForFrame?.available) {
13387
- telemetryResourcesForFrame.beginFrame();
13388
- activeFrameTelemetry = telemetryResourcesForFrame;
13389
- }
13390
- const frameStartTimeMs = nowMs3();
13391
- let frameStats;
13392
- try {
13393
- if (useThrottledHighSamplePath) {
13394
- frame += 1;
13395
- const frameIndex = frame + config.frameIndex;
13396
- const parallelismCounters = createGpuParallelismCounters();
13397
- const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex, parallelismCounters);
13398
- let frameSubmissionCount = 0;
13399
- let frameConfigSlot = 0;
13400
- if (accelerationBuildSubmitted) {
13401
- const accelerationWaitOptions = {
13402
- ...estimateSubmittedGpuWorkTiming(
13403
- { ...config, renderedSamplesPerPixel: 1 },
13404
- 1,
13405
- renderOptions.submittedWorkTimeoutMs,
13406
- { includeAccelerationBuild: true }
13407
- ),
13408
- allowTimeout: false
13409
- };
13410
- await waitForSubmittedGpuWork2(accelerationWaitOptions);
13411
- }
13412
- for (let tileIndex = 0; tileIndex < tiles.length; tileIndex += 1) {
13413
- const tileRangeDispatch = dispatchFrameAwaitingGpu(
13414
- frameIndex,
13415
- parallelismCounters,
13416
- samplingPlan.renderedSamplesPerPixel,
13417
- {
13418
- sampleRangeStart: 0,
13419
- sampleRangeEnd: samplingPlan.renderedSamplesPerPixel,
13420
- tileStartIndex: tileIndex,
13421
- tileEndIndex: tileIndex + 1,
13422
- startingSubmissionCount: frameSubmissionCount,
13423
- startingSlot: frameConfigSlot,
13424
- includeDenoise: tileIndex + 1 >= tiles.length,
13425
- includePresent: tileIndex + 1 >= tiles.length
13426
- }
13427
- );
13428
- frameSubmissionCount = tileRangeDispatch.submissionCount;
13429
- frameConfigSlot = tileRangeDispatch.slot;
13430
- const tileWaitOptions = {
13431
- ...estimateSubmittedGpuWorkTiming(
13432
- { ...config, renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel },
13433
- 1,
13434
- renderOptions.submittedWorkTimeoutMs,
13854
+ const {
13855
+ bindGroupLayouts: {
13856
+ trace: traceBindGroupLayout,
13857
+ acceleration: accelerationBindGroupLayout,
13858
+ denoiseRadiance: denoiseRadianceBindGroupLayout,
13859
+ denoiseResolve: denoiseResolveBindGroupLayout,
13860
+ present: presentBindGroupLayout
13861
+ },
13862
+ computePipelines: pipelines,
13863
+ presentPipeline
13864
+ } = await createWavefrontPipelineResources({ device, constants, format, instanced: !!config.instancing });
13865
+ let bindGroups = createTraceBindGroups();
13866
+ const bvhBuildBindGroup = createWavefrontBvhBuildBindGroup({
13867
+ device,
13868
+ accelerationBindGroupLayout,
13869
+ bvhBuildConfigBuffer,
13870
+ triangleBuffer,
13871
+ bvhNodeBuffer,
13872
+ meshVertexBuffer,
13873
+ meshIndexBuffer,
13874
+ meshRangeBuffer,
13875
+ bvhLeafRefBuffer
13876
+ });
13877
+ const {
13878
+ radiance: denoiseRadianceBindGroup,
13879
+ resolve: denoiseResolveBindGroup,
13880
+ directResolve: denoiseDirectResolveBindGroup
13881
+ } = createWavefrontDenoiseBindGroups({
13882
+ device,
13883
+ denoiseRadianceBindGroupLayout,
13884
+ denoiseResolveBindGroupLayout,
13885
+ configBuffer,
13886
+ radianceView,
13887
+ denoiseScratchView,
13888
+ outputView
13889
+ });
13890
+ const presentBindGroup = createWavefrontPresentBindGroup({
13891
+ device,
13892
+ presentBindGroupLayout,
13893
+ outputView,
13894
+ sampler
13895
+ });
13896
+ const progressive = await createWavefrontProgressiveResources({ device, constants, config, configBuffer, radianceTexture, radianceView, outputView });
13897
+ let activeFrameTelemetry = null;
13898
+ let frameTelemetryResources = null;
13899
+ const frameEncoder = createWavefrontFrameEncoder({
13900
+ getConfig: () => config,
13901
+ progressive,
13902
+ getBindGroups: () => bindGroups,
13903
+ pipelines,
13904
+ counterBuffer,
13905
+ activeDispatchBuffer,
13906
+ denoiseRadianceBindGroup,
13907
+ denoiseResolveBindGroup,
13908
+ denoiseDirectResolveBindGroup,
13909
+ presentPipeline,
13910
+ presentBindGroup,
13911
+ context,
13912
+ getFrameTelemetry: () => activeFrameTelemetry
13913
+ });
13914
+ let frame = 0;
13915
+ let accelerationBuilt = !config.gpuAccelerationBuildRequired;
13916
+ let accelerationBuildCount = 0;
13917
+ let activeCameraOptions = options.camera ?? DEFAULT_CAMERA2;
13918
+ let lastCompletedFrameTimeMs = null;
13919
+ let lastCompletedSamplesPerPixel = Math.max(1, config.samplesPerPixel);
13920
+ let lastGpuParallelism = createGpuParallelismDiagnostics(
13921
+ gpuAdapterParallelism,
13922
+ createGpuParallelismCounters()
13923
+ );
13924
+ const waitForSubmittedGpuWork2 = (waitOptions = {}) => waitForSubmittedGpuWork(device, waitOptions);
13925
+ async function readTerminationMetrics() {
13926
+ return readWavefrontTerminationMetrics({
13927
+ device,
13928
+ constants,
13929
+ counterBuffer,
13930
+ waitForSubmittedGpuWork: waitForSubmittedGpuWork2
13931
+ });
13932
+ }
13933
+ async function readOutputProbe(optionsForProbe = {}) {
13934
+ return readWavefrontOutputProbe({
13935
+ device,
13936
+ constants,
13937
+ config,
13938
+ outputTexture,
13939
+ waitForSubmittedGpuWork: waitForSubmittedGpuWork2,
13940
+ optionsForProbe
13941
+ });
13942
+ }
13943
+ async function renderFrame(renderOptions = {}) {
13944
+ const awaitGPUCompletion = renderOptions.awaitGPUCompletion !== false;
13945
+ const samplingPlan = resolveRenderedSamplesPerPixel(renderOptions, awaitGPUCompletion);
13946
+ const useThrottledHighSamplePath = awaitGPUCompletion && samplingPlan.renderedSamplesPerPixel >= 8;
13947
+ const telemetryRequested = renderOptions.readStats === true;
13948
+ const telemetryCanRecord = telemetryRequested && awaitGPUCompletion;
13949
+ const telemetryResourcesForFrame = telemetryCanRecord ? ensureFrameTelemetryResources() : null;
13950
+ if (telemetryResourcesForFrame?.available) {
13951
+ telemetryResourcesForFrame.beginFrame();
13952
+ activeFrameTelemetry = telemetryResourcesForFrame;
13953
+ }
13954
+ const frameStartTimeMs = nowMs3();
13955
+ let frameStats;
13956
+ try {
13957
+ if (useThrottledHighSamplePath) {
13958
+ frame += 1;
13959
+ const frameIndex = frame + config.frameIndex;
13960
+ const parallelismCounters = createGpuParallelismCounters();
13961
+ const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex, parallelismCounters);
13962
+ let frameSubmissionCount = 0;
13963
+ let frameConfigSlot = 0;
13964
+ if (accelerationBuildSubmitted) {
13965
+ const accelerationWaitOptions = {
13966
+ ...estimateSubmittedGpuWorkTiming(
13967
+ { ...config, renderedSamplesPerPixel: 1 },
13968
+ 1,
13969
+ renderOptions.submittedWorkTimeoutMs,
13970
+ { includeAccelerationBuild: true }
13971
+ ),
13972
+ allowTimeout: false
13973
+ };
13974
+ await waitForSubmittedGpuWork2(accelerationWaitOptions);
13975
+ }
13976
+ for (let tileIndex = 0; tileIndex < tiles.length; tileIndex += 1) {
13977
+ const tileRangeDispatch = dispatchFrameAwaitingGpu(
13978
+ frameIndex,
13979
+ parallelismCounters,
13980
+ samplingPlan.renderedSamplesPerPixel,
13435
13981
  {
13436
- includeDenoise: tileIndex + 1 >= tiles.length && config.denoise,
13982
+ sampleRangeStart: 0,
13983
+ sampleRangeEnd: samplingPlan.renderedSamplesPerPixel,
13984
+ tileStartIndex: tileIndex,
13985
+ tileEndIndex: tileIndex + 1,
13986
+ startingSubmissionCount: frameSubmissionCount,
13987
+ startingSlot: frameConfigSlot,
13988
+ includeDenoise: tileIndex + 1 >= tiles.length,
13437
13989
  includePresent: tileIndex + 1 >= tiles.length
13438
13990
  }
13439
- ),
13991
+ );
13992
+ frameSubmissionCount = tileRangeDispatch.submissionCount;
13993
+ frameConfigSlot = tileRangeDispatch.slot;
13994
+ const tileWaitOptions = {
13995
+ ...estimateSubmittedGpuWorkTiming(
13996
+ { ...config, renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel },
13997
+ 1,
13998
+ renderOptions.submittedWorkTimeoutMs,
13999
+ {
14000
+ includeDenoise: tileIndex + 1 >= tiles.length && config.denoise,
14001
+ includePresent: tileIndex + 1 >= tiles.length
14002
+ }
14003
+ ),
14004
+ allowTimeout: false
14005
+ };
14006
+ await waitForSubmittedGpuWork2(tileWaitOptions);
14007
+ }
14008
+ frameStats = createFrameStats({
14009
+ frameIndex,
14010
+ accelerationBuildSubmitted,
14011
+ frameSubmissionCount,
14012
+ parallelismCounters,
14013
+ renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel,
14014
+ targetSamplesPerPixel: samplingPlan.targetSamplesPerPixel,
14015
+ frameTimeBudgetMs: samplingPlan.frameTimeBudgetMs,
14016
+ budgetConstrained: samplingPlan.budgetConstrained
14017
+ });
14018
+ } else {
14019
+ const submittedWorkTiming = estimateSubmittedGpuWorkTiming(
14020
+ { ...config, renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel },
14021
+ tiles.length,
14022
+ renderOptions.submittedWorkTimeoutMs,
14023
+ { includeAccelerationBuild: config.gpuAccelerationBuildRequired && !accelerationBuilt }
14024
+ );
14025
+ const submissionWaitOptions = awaitGPUCompletion ? {
14026
+ timeoutMs: submittedWorkTiming.timeoutMs,
14027
+ maxWaitMs: submittedWorkTiming.maxWaitMs,
13440
14028
  allowTimeout: false
14029
+ } : {
14030
+ timeoutMs: submittedWorkTiming.timeoutMs,
14031
+ maxWaitMs: submittedWorkTiming.maxWaitMs
13441
14032
  };
13442
- await waitForSubmittedGpuWork2(tileWaitOptions);
13443
- }
13444
- frameStats = createFrameStats({
13445
- frameIndex,
13446
- accelerationBuildSubmitted,
13447
- frameSubmissionCount,
13448
- parallelismCounters,
13449
- renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel,
13450
- targetSamplesPerPixel: samplingPlan.targetSamplesPerPixel,
13451
- frameTimeBudgetMs: samplingPlan.frameTimeBudgetMs,
13452
- budgetConstrained: samplingPlan.budgetConstrained
13453
- });
13454
- } else {
13455
- const submittedWorkTiming = estimateSubmittedGpuWorkTiming(
13456
- { ...config, renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel },
13457
- tiles.length,
13458
- renderOptions.submittedWorkTimeoutMs,
13459
- { includeAccelerationBuild: config.gpuAccelerationBuildRequired && !accelerationBuilt }
13460
- );
13461
- const submissionWaitOptions = awaitGPUCompletion ? {
13462
- timeoutMs: submittedWorkTiming.timeoutMs,
13463
- maxWaitMs: submittedWorkTiming.maxWaitMs,
13464
- allowTimeout: false
13465
- } : {
13466
- timeoutMs: submittedWorkTiming.timeoutMs,
13467
- maxWaitMs: submittedWorkTiming.maxWaitMs
13468
- };
13469
- frameStats = renderOnce(renderOptions, samplingPlan);
13470
- if (awaitGPUCompletion) {
13471
- await waitForSubmittedGpuWork2(submissionWaitOptions);
14033
+ frameStats = renderOnce(renderOptions, samplingPlan);
14034
+ if (awaitGPUCompletion) {
14035
+ await waitForSubmittedGpuWork2(submissionWaitOptions);
14036
+ }
13472
14037
  }
14038
+ } finally {
14039
+ activeFrameTelemetry = null;
13473
14040
  }
13474
- } finally {
13475
- activeFrameTelemetry = null;
13476
- }
13477
- const frameTimeMs = Math.max(0, nowMs3() - frameStartTimeMs);
13478
- if (awaitGPUCompletion) {
13479
- lastCompletedFrameTimeMs = frameTimeMs;
13480
- lastCompletedSamplesPerPixel = frameStats.renderedSamplesPerPixel ?? frameStats.samplesPerPixel;
13481
- }
13482
- const expectedPrimaryRays = config.width * config.height * samplingPlan.renderedSamplesPerPixel;
13483
- const expectedRayCounts = tiles.length * samplingPlan.renderedSamplesPerPixel * config.maxDepth;
13484
- const telemetryReadback = telemetryCanRecord ? await telemetryResourcesForFrame.readFrame({
13485
- expectedPrimaryRays,
13486
- expectedRayCounts,
13487
- waitForSubmittedGpuWork: waitForSubmittedGpuWork2
13488
- }) : null;
13489
- const rayCounts = !telemetryRequested ? createWavefrontRayCountTelemetry() : !awaitGPUCompletion ? createWavefrontRayCountTelemetry({
13490
- status: "unavailable",
13491
- expectedPrimaryRays,
13492
- expectedRayCounts,
13493
- reason: "gpu-work-not-awaited"
13494
- }) : telemetryReadback.rayCounts;
13495
- const timings = !telemetryRequested ? createWavefrontFrameTimingTelemetry({
13496
- status: "not-requested",
13497
- source: awaitGPUCompletion ? "queue-completion" : "cpu-submit",
13498
- timestampQueryStatus: "not-recorded",
13499
- totalRenderJobTimeMs: frameTimeMs
13500
- }) : !awaitGPUCompletion ? createWavefrontFrameTimingTelemetry({
13501
- status: "unavailable",
13502
- source: "cpu-submit",
13503
- timestampQueryStatus: "not-recorded",
13504
- totalRenderJobTimeMs: frameTimeMs,
13505
- reason: "gpu-work-not-awaited"
13506
- }) : telemetryReadback.timestampQueryStatus === "available" ? createWavefrontFrameTimingTelemetry({
13507
- status: "available",
13508
- source: "timestamp-query",
13509
- timestampQueryStatus: "available",
13510
- totalGpuTimeMs: telemetryReadback.totalGpuTimeMs,
13511
- totalRenderJobTimeMs: frameTimeMs
13512
- }) : createWavefrontFrameTimingTelemetry({
13513
- status: "fallback",
13514
- source: "queue-completion",
13515
- timestampQueryStatus: telemetryReadback.timestampQueryStatus,
13516
- totalRenderJobTimeMs: frameTimeMs,
13517
- reason: telemetryReadback.reason
13518
- });
13519
- const deviceLossStatus = awaitGPUCompletion ? "not-detected" : typeof device.lost?.then === "function" ? "pending" : "not-exposed";
13520
- frameStats = Object.freeze({
13521
- ...frameStats,
13522
- deviceLossStatus,
13523
- secondaryRays: rayCounts.status === "available" ? rayCounts.secondaryRays : null,
13524
- totalPathSegments: rayCounts.status === "available" ? rayCounts.totalPathSegments : null,
13525
- rayCounts,
13526
- timings,
13527
- telemetryMemoryBytes: telemetryResourcesForFrame?.memoryBytes ?? 0,
13528
- gpuWorkerJobs: createGpuWorkerJobDiagnostics(
13529
- frameStats.gpuParallelism,
13530
- frameStats.commandSubmissions,
13531
- frameTimeMs,
13532
- awaitGPUCompletion
13533
- )
13534
- });
13535
- const terminationMetrics = awaitGPUCompletion && renderOptions.readStats === true ? await readTerminationMetrics() : EMPTY_TERMINATION_METRICS;
13536
- const probe = renderOptions.readOutputProbe === false ? null : await readOutputProbe(renderOptions.probe);
13537
- const maxChannel = probe ? Math.max(...probe.rgba.slice(0, 3)) : 0;
13538
- const completedFrame = Object.freeze({
13539
- ...frameStats,
13540
- outputProbe: probe ? Object.freeze({
13541
- ...probe,
13542
- sampledPixels: 1,
13543
- nonZeroSamples: maxChannel > 0 ? 1 : 0,
13544
- maxChannel
13545
- }) : null,
13546
- bounces: [],
13547
- termination: terminationMetrics.termination,
13548
- terminalRadiance: terminationMetrics.terminalRadiance,
13549
- radianceDiagnostics: terminationMetrics.radianceDiagnostics,
13550
- transportContributions: terminationMetrics.transportContributions,
13551
- queueOverflow: terminationMetrics.queueOverflow
13552
- });
14041
+ const frameTimeMs = Math.max(0, nowMs3() - frameStartTimeMs);
14042
+ if (awaitGPUCompletion) {
14043
+ lastCompletedFrameTimeMs = frameTimeMs;
14044
+ lastCompletedSamplesPerPixel = frameStats.renderedSamplesPerPixel ?? frameStats.samplesPerPixel;
14045
+ }
14046
+ const expectedPrimaryRays = config.width * config.height * samplingPlan.renderedSamplesPerPixel;
14047
+ const expectedRayCounts = tiles.length * samplingPlan.renderedSamplesPerPixel * config.maxDepth;
14048
+ const telemetryReadback = telemetryCanRecord ? await telemetryResourcesForFrame.readFrame({
14049
+ expectedPrimaryRays,
14050
+ expectedRayCounts,
14051
+ waitForSubmittedGpuWork: waitForSubmittedGpuWork2
14052
+ }) : null;
14053
+ const rayCounts = !telemetryRequested ? createWavefrontRayCountTelemetry() : !awaitGPUCompletion ? createWavefrontRayCountTelemetry({
14054
+ status: "unavailable",
14055
+ expectedPrimaryRays,
14056
+ expectedRayCounts,
14057
+ reason: "gpu-work-not-awaited"
14058
+ }) : telemetryReadback.rayCounts;
14059
+ const timings = !telemetryRequested ? createWavefrontFrameTimingTelemetry({
14060
+ status: "not-requested",
14061
+ source: awaitGPUCompletion ? "queue-completion" : "cpu-submit",
14062
+ timestampQueryStatus: "not-recorded",
14063
+ totalRenderJobTimeMs: frameTimeMs
14064
+ }) : !awaitGPUCompletion ? createWavefrontFrameTimingTelemetry({
14065
+ status: "unavailable",
14066
+ source: "cpu-submit",
14067
+ timestampQueryStatus: "not-recorded",
14068
+ totalRenderJobTimeMs: frameTimeMs,
14069
+ reason: "gpu-work-not-awaited"
14070
+ }) : telemetryReadback.timestampQueryStatus === "available" ? createWavefrontFrameTimingTelemetry({
14071
+ status: "available",
14072
+ source: "timestamp-query",
14073
+ timestampQueryStatus: "available",
14074
+ totalGpuTimeMs: telemetryReadback.totalGpuTimeMs,
14075
+ totalRenderJobTimeMs: frameTimeMs
14076
+ }) : createWavefrontFrameTimingTelemetry({
14077
+ status: "fallback",
14078
+ source: "queue-completion",
14079
+ timestampQueryStatus: telemetryReadback.timestampQueryStatus,
14080
+ totalRenderJobTimeMs: frameTimeMs,
14081
+ reason: telemetryReadback.reason
14082
+ });
14083
+ const deviceLossStatus = awaitGPUCompletion ? "not-detected" : typeof device.lost?.then === "function" ? "pending" : "not-exposed";
14084
+ frameStats = Object.freeze({
14085
+ ...frameStats,
14086
+ deviceLossStatus,
14087
+ secondaryRays: rayCounts.status === "available" ? rayCounts.secondaryRays : null,
14088
+ totalPathSegments: rayCounts.status === "available" ? rayCounts.totalPathSegments : null,
14089
+ rayCounts,
14090
+ timings,
14091
+ telemetryMemoryBytes: telemetryResourcesForFrame?.memoryBytes ?? 0,
14092
+ gpuWorkerJobs: createGpuWorkerJobDiagnostics(
14093
+ frameStats.gpuParallelism,
14094
+ frameStats.commandSubmissions,
14095
+ frameTimeMs,
14096
+ awaitGPUCompletion
14097
+ )
14098
+ });
14099
+ const terminationMetrics = awaitGPUCompletion && renderOptions.readStats === true ? await readTerminationMetrics() : EMPTY_TERMINATION_METRICS;
14100
+ const probe = renderOptions.readOutputProbe === false ? null : await readOutputProbe(renderOptions.probe);
14101
+ const maxChannel = probe ? Math.max(...probe.rgba.slice(0, 3)) : 0;
14102
+ const completedFrame = Object.freeze({
14103
+ ...frameStats,
14104
+ outputProbe: probe ? Object.freeze({
14105
+ ...probe,
14106
+ sampledPixels: 1,
14107
+ nonZeroSamples: maxChannel > 0 ? 1 : 0,
14108
+ maxChannel
14109
+ }) : null,
14110
+ bounces: [],
14111
+ termination: terminationMetrics.termination,
14112
+ terminalRadiance: terminationMetrics.terminalRadiance,
14113
+ radianceDiagnostics: terminationMetrics.radianceDiagnostics,
14114
+ transportContributions: terminationMetrics.transportContributions,
14115
+ queueOverflow: terminationMetrics.queueOverflow
14116
+ });
14117
+ return Object.freeze({
14118
+ ...completedFrame,
14119
+ transportGuardrails: createWavefrontTransportGuardrailSummary(completedFrame)
14120
+ });
14121
+ }
14122
+ await finishAllocationScopes();
13553
14123
  return Object.freeze({
13554
- ...completedFrame,
13555
- transportGuardrails: createWavefrontTransportGuardrailSummary(completedFrame)
13556
- });
13557
- }
13558
- function rebuildLiveConfig(overrides = {}) {
13559
- return createWavefrontPathTracingComputeConfig({
13560
- ...options,
13561
14124
  canvas,
13562
- width: config.width,
13563
- height: config.height,
13564
- maxDepth: config.maxDepth,
13565
- tileSize: config.tileSize,
13566
- samplesPerPixel: config.samplesPerPixel,
13567
- sceneObjectCapacity: config.sceneObjectCapacity,
13568
- sceneObjects: packedScene.objects,
13569
- camera: activeCameraOptions,
13570
- environmentMap: {
13571
- ...config.environmentMap
13572
- },
13573
- strictPhysicalLowSppLighting: config.strictPhysicalLowSppLighting,
13574
- featureFlags: options.featureFlags,
13575
- frameIndex: config.frameIndex,
13576
- ...overrides
14125
+ context,
14126
+ device: rawDevice,
14127
+ format,
14128
+ config,
14129
+ renderOnce,
14130
+ renderFrame,
14131
+ readOutputProbe,
14132
+ updateSceneObjects,
14133
+ updateCamera,
14134
+ resetAccumulation: () => progressive?.reset(),
14135
+ getSnapshot,
14136
+ destroy
13577
14137
  });
13578
- }
13579
- function rebuildMediumResources(nextConfig) {
13580
- const previousMediumTextureResource = mediumTextureResource;
13581
- mediumTextureResource = createMediumTextureResource(device, constants, nextConfig.mediums);
13582
- bindGroups = createTraceBindGroups();
13583
- if (previousMediumTextureResource?.ownsTexture) {
13584
- previousMediumTextureResource.texture?.destroy?.();
13585
- }
13586
- }
13587
- function updateSceneObjects(sceneObjects) {
13588
- progressive?.reset();
13589
- const nextPackedScene = packWavefrontSceneObjects(sceneObjects, config.sceneObjectCapacity);
13590
- packedScene = nextPackedScene;
13591
- const nextConfig = rebuildLiveConfig();
13592
- if (!mediumTablesEqual(config.mediums, nextConfig.mediums)) {
13593
- rebuildMediumResources(nextConfig);
14138
+ } catch (error) {
14139
+ try {
14140
+ await finishAllocationScopes?.();
14141
+ } catch {
13594
14142
  }
13595
- config = nextConfig;
13596
- device.queue.writeBuffer(sceneObjectBuffer, 0, packedScene.buffer);
13597
- return config;
13598
- }
13599
- function updateCamera(cameraOptions = {}) {
13600
- progressive?.reset();
13601
- activeCameraOptions = cameraOptions;
13602
- config = updateWavefrontCameraConfig(config, cameraOptions);
13603
- return config;
13604
- }
13605
- function getSnapshot() {
13606
- return Object.freeze({
13607
- frame,
13608
- accumulatedSamplesPerPixel: progressive?.samples ?? 0,
13609
- width: config.width,
13610
- height: config.height,
13611
- maxDepth: config.maxDepth,
13612
- tiles: tiles.length,
13613
- tileSize: config.tileSize,
13614
- samplesPerPixel: config.samplesPerPixel,
13615
- maxFramePassesPerSubmission: config.maxFramePassesPerSubmission,
13616
- sceneObjectCount: config.sceneObjectCount,
13617
- triangleCount: config.triangleCount,
13618
- emissiveTriangleCount: config.emissiveTriangleCount,
13619
- environmentPortalCount: config.environmentPortalCount,
13620
- environmentPortalMode: config.environmentPortalMode,
13621
- mediumCount: config.mediumCount,
13622
- environmentMap: createEnvironmentMapSnapshot(config.environmentMap),
13623
- deferredPathResolve: config.deferredPathResolve,
13624
- strictPhysicalLowSppLighting: config.strictPhysicalLowSppLighting,
13625
- transportExperiments: config.transportExperiments,
13626
- transportExperimentFlags: config.transportExperimentFlags,
13627
- bvhNodeCount: config.bvhNodeCount,
13628
- displayQuality: config.displayQuality,
13629
- accelerationBuildMode: config.accelerationBuildMode,
13630
- gpuAccelerationBuildRequired: config.gpuAccelerationBuildRequired,
13631
- accelerationBuilt,
13632
- accelerationBuildCount,
13633
- frameConfigSlots: frameConfigSlotCount,
13634
- gpuParallelism: lastGpuParallelism,
13635
- memory: config.memory
13636
- });
13637
- }
13638
- function destroy() {
13639
- progressive?.destroy();
13640
- frameTelemetryResources?.destroy?.();
13641
- activeQueue.destroy?.();
13642
- nextQueue.destroy?.();
13643
- hitBuffer.destroy?.();
13644
- accumulationBuffer.destroy?.();
13645
- pathVertexBuffer.destroy?.();
13646
- sceneObjectBuffer.destroy?.();
13647
- triangleBuffer.destroy?.();
13648
- bvhNodeBuffer.destroy?.();
13649
- meshVertexBuffer.destroy?.();
13650
- meshIndexBuffer.destroy?.();
13651
- meshRangeBuffer.destroy?.();
13652
- environmentPortalBuffer.destroy?.();
13653
- bvhLeafRefBuffer.destroy?.();
13654
- configBuffer.destroy?.();
13655
- bvhBuildConfigBuffer.destroy?.();
13656
- counterBuffer.destroy?.();
13657
- activeDispatchBuffer.destroy?.();
13658
- radianceTexture.destroy?.();
13659
- denoiseScratchTexture.destroy?.();
13660
- outputTexture.destroy?.();
13661
- if (environmentMapResource.ownsTexture) {
13662
- environmentMapResource.texture?.destroy?.();
13663
- }
13664
- if (environmentSamplingResource.ownsTexture) {
13665
- environmentSamplingResource.texture?.destroy?.();
13666
- }
13667
- if (mediumTextureResource.ownsTexture) {
13668
- mediumTextureResource.texture?.destroy?.();
13669
- }
13670
- brdfLutResource.texture?.destroy?.();
13671
- if (baseColorAtlasResource.ownsTexture) {
13672
- baseColorAtlasResource.texture?.destroy?.();
13673
- }
13674
- if (metallicRoughnessAtlasResource.ownsTexture) {
13675
- metallicRoughnessAtlasResource.texture?.destroy?.();
13676
- }
13677
- if (normalAtlasResource.ownsTexture) {
13678
- normalAtlasResource.texture?.destroy?.();
13679
- }
13680
- if (occlusionAtlasResource.ownsTexture) {
13681
- occlusionAtlasResource.texture?.destroy?.();
13682
- }
13683
- if (emissiveAtlasResource.ownsTexture) {
13684
- emissiveAtlasResource.texture?.destroy?.();
13685
- }
13686
- Object.values(extensionAtlasResources).forEach((resource) => {
13687
- if (resource.ownsTexture) {
13688
- resource.texture?.destroy?.();
13689
- }
13690
- });
13691
- context.unconfigure?.();
14143
+ context?.unconfigure?.();
14144
+ allocationTracker.destroy();
14145
+ rawDevice.destroy?.();
14146
+ throw error;
13692
14147
  }
13693
- return Object.freeze({
13694
- canvas,
13695
- context,
13696
- device,
13697
- format,
13698
- config,
13699
- renderOnce,
13700
- renderFrame,
13701
- readOutputProbe,
13702
- updateSceneObjects,
13703
- updateCamera,
13704
- resetAccumulation: () => progressive?.reset(),
13705
- getSnapshot,
13706
- destroy
13707
- });
13708
14148
  }
13709
14149
  async function renderWavefrontPathTracingComputeFrame(options = {}) {
13710
14150
  const renderer = await createWavefrontPathTracingComputeRenderer(options);