@plasius/gpu-renderer 0.2.56 → 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
@@ -4058,948 +4058,1289 @@ function boundsCentroid(bounds) {
4058
4058
  ];
4059
4059
  }
4060
4060
 
4061
- // src/wavefront-sampling.js
4062
- function buildOrthonormalBasis(normal) {
4063
- const tangentFallback = Math.abs(normal[1]) < 0.999 ? [0, 1, 0] : [1, 0, 0];
4064
- const tangent = normalize(cross2(tangentFallback, normal), [1, 0, 0]);
4065
- const bitangent = normalize(cross2(normal, tangent), [0, 0, 1]);
4066
- return { tangent, bitangent };
4067
- }
4068
- function localToWorld(local, normal) {
4069
- const basis = buildOrthonormalBasis(normal);
4070
- return normalize(
4071
- add(
4072
- add(scale(basis.tangent, local[0]), scale(basis.bitangent, local[1])),
4073
- scale(normal, local[2])
4074
- ),
4075
- normal
4076
- );
4077
- }
4078
- function radicalInverseVdc(bits) {
4079
- let value = bits >>> 0;
4080
- value = (value << 16 | value >>> 16) >>> 0;
4081
- value = ((value & 1431655765) << 1 | (value & 2863311530) >>> 1) >>> 0;
4082
- value = ((value & 858993459) << 2 | (value & 3435973836) >>> 2) >>> 0;
4083
- value = ((value & 252645135) << 4 | (value & 4042322160) >>> 4) >>> 0;
4084
- value = ((value & 16711935) << 8 | (value & 4278255360) >>> 8) >>> 0;
4085
- return value * 23283064365386963e-26;
4086
- }
4087
- function hammersley(index, count) {
4088
- return [index / Math.max(count, 1), radicalInverseVdc(index)];
4089
- }
4090
- function importanceSampleGgx(sample, roughness, normal) {
4091
- const alpha = Math.max(roughness * roughness, 1e-4);
4092
- const phi = 2 * Math.PI * sample[0];
4093
- const cosTheta = Math.sqrt((1 - sample[1]) / (1 + (alpha * alpha - 1) * sample[1]));
4094
- const sinTheta = Math.sqrt(Math.max(0, 1 - cosTheta * cosTheta));
4095
- const halfVector = localToWorld(
4096
- [Math.cos(phi) * sinTheta, Math.sin(phi) * sinTheta, cosTheta],
4097
- normal
4098
- );
4099
- return normalize(halfVector, normal);
4100
- }
4101
- function geometrySchlickGgx(nDotV, roughness) {
4102
- const k = (roughness + 1) * (roughness + 1) / 8;
4103
- return nDotV / Math.max(nDotV * (1 - k) + k, 1e-6);
4104
- }
4105
- function geometrySmith(nDotV, nDotL, roughness) {
4106
- 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;
4107
4074
  }
4108
- function integrateBrdfSample(nDotV, roughness, sampleCount) {
4109
- const viewDirection = [Math.sqrt(Math.max(0, 1 - nDotV * nDotV)), 0, nDotV];
4110
- const normal = [0, 0, 1];
4111
- let scaleTerm = 0;
4112
- let biasTerm = 0;
4113
- for (let index = 0; index < sampleCount; index += 1) {
4114
- const xi = hammersley(index, sampleCount);
4115
- const halfVector = importanceSampleGgx(xi, roughness, normal);
4116
- const vDotH = Math.max(dot(viewDirection, halfVector), 0);
4117
- const lightDirection = normalize(
4118
- subtract(scale(halfVector, 2 * vDotH), viewDirection),
4119
- 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.`
4120
4085
  );
4121
- const nDotL = Math.max(lightDirection[2], 0);
4122
- const nDotH = Math.max(halfVector[2], 0);
4123
- if (nDotL <= 0 || nDotH <= 0 || vDotH <= 0) {
4124
- continue;
4125
- }
4126
- const geometry = geometrySmith(nDotV, nDotL, roughness);
4127
- const visibility = geometry * vDotH / Math.max(nDotH * nDotV, 1e-6);
4128
- const fresnel2 = (1 - vDotH) ** 5;
4129
- scaleTerm += (1 - fresnel2) * visibility;
4130
- biasTerm += fresnel2 * visibility;
4131
4086
  }
4132
- return [scaleTerm / sampleCount, biasTerm / sampleCount];
4133
4087
  }
4134
-
4135
- // src/wavefront-gpu-resources.js
4136
- function clampTileSizeForDevice(config, device) {
4137
- const limit = Number(device?.limits?.maxStorageBufferBindingSize);
4138
- if (!Number.isFinite(limit) || limit <= 0) {
4139
- 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.");
4140
4091
  }
4141
- const maxPixelsByRay = Math.floor(limit / RAY_RECORD_BYTES);
4142
- const maxPixelsByHit = Math.floor(limit / HIT_RECORD_BYTES);
4143
- const maxPixelsByPathVertex = Math.floor(
4144
- limit / ((config.maxDepth + 1) * PATH_VERTEX_RECORD_BYTES)
4145
- );
4146
- const maxPixels = Math.max(
4147
- 256,
4148
- Math.min(maxPixelsByRay, maxPixelsByHit, maxPixelsByPathVertex)
4149
- );
4150
- if (config.tilePixelCapacity <= maxPixels) {
4151
- return config.tileSize;
4092
+ if (typeof GPUBufferUsage.INDIRECT !== "number") {
4093
+ throw new Error("WebGPU runtime unavailable. GPUBufferUsage.INDIRECT is missing.");
4152
4094
  }
4153
- return Math.max(16, Math.floor(Math.sqrt(maxPixels)));
4154
- }
4155
- function createBuffer(device, usage, size, label) {
4156
- return device.createBuffer({
4157
- label,
4158
- size: Math.max(4, size),
4159
- usage
4160
- });
4161
- }
4162
- function alignTo(value, alignment) {
4163
- const resolvedAlignment = Math.max(1, alignment);
4164
- 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
+ };
4165
4101
  }
4166
- function float32ToFloat16Bits(value) {
4167
- const floatView = new Float32Array(1);
4168
- const intView = new Uint32Array(floatView.buffer);
4169
- floatView[0] = Number.isFinite(value) ? value : 0;
4170
- const x = intView[0];
4171
- const sign = x >> 16 & 32768;
4172
- let mantissa = x & 8388607;
4173
- let exponent = x >> 23 & 255;
4174
- if (exponent === 255) {
4175
- 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;
4176
4109
  }
4177
- exponent = exponent - 127 + 15;
4178
- if (exponent >= 31) {
4179
- return sign | 31744;
4110
+ if (canvasOrSelector?.getContext) {
4111
+ return canvasOrSelector;
4180
4112
  }
4181
- if (exponent <= 0) {
4182
- if (exponent < -10) {
4183
- return sign;
4184
- }
4185
- mantissa = (mantissa | 8388608) >> 1 - exponent;
4186
- 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.");
4187
4116
  }
4188
- return sign | exponent << 10 | mantissa + 4096 >> 13;
4117
+ return fallback;
4189
4118
  }
4190
- function environmentMapIntegerScale(data) {
4191
- if (data instanceof Uint8Array) {
4192
- return 1 / 255;
4119
+ async function getPipelineDiagnostics(shaderModule) {
4120
+ if (typeof shaderModule?.getCompilationInfo !== "function") {
4121
+ return "";
4193
4122
  }
4194
- if (data instanceof Uint16Array) {
4195
- 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 "";
4196
4136
  }
4197
- return 1;
4198
4137
  }
4199
- function environmentMapHasSamplingData(environmentMap) {
4200
- if (!environmentMap || !environmentMap.data) {
4201
- 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
+ });
4202
4159
  }
4203
- const width = Math.max(1, environmentMap.width ?? 1);
4204
- const height = Math.max(1, environmentMap.height ?? 1);
4205
- return environmentMap.data.length >= width * height * 4;
4206
4160
  }
4207
- function createRgba8TextureUpload(source) {
4208
- const width = Math.max(1, Math.trunc(source.width));
4209
- const height = Math.max(1, Math.trunc(source.height));
4210
- const bytesPerRow = alignTo(width * 4, 256);
4211
- const bytes = new Uint8Array(bytesPerRow * height);
4212
- const data = source.data instanceof Uint8Array ? source.data : new Uint8Array(source.data);
4213
- for (let y = 0; y < height; y += 1) {
4214
- const sourceOffset = y * width * 4;
4215
- const targetOffset = y * bytesPerRow;
4216
- bytes.set(data.subarray(sourceOffset, sourceOffset + width * 4), targetOffset);
4161
+ async function assertShaderModuleCompiles(shaderModule, label) {
4162
+ if (typeof shaderModule?.getCompilationInfo !== "function") {
4163
+ return;
4217
4164
  }
4218
- return Object.freeze({
4219
- bytes,
4220
- bytesPerRow,
4221
- width,
4222
- height
4223
- });
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;
4170
+ }
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}`);
4224
4178
  }
4225
- function readEnvironmentMapComponent(data, index, fallback, integerScale = 1) {
4226
- if (!data || index >= data.length) {
4227
- return fallback;
4179
+ async function createRenderPipeline(device, descriptor) {
4180
+ if (typeof device.createRenderPipelineAsync === "function") {
4181
+ return device.createRenderPipelineAsync(descriptor);
4228
4182
  }
4229
- const value = Number(data[index]);
4230
- return Number.isFinite(value) ? Math.max(0, value) * integerScale : fallback;
4183
+ return device.createRenderPipeline(descriptor);
4231
4184
  }
4232
- function createBrdfLutUploadBytes(size = DEFAULT_BRDF_LUT_SIZE, sampleCount = DEFAULT_BRDF_LUT_SAMPLE_COUNT) {
4233
- const cacheKey = `${Math.max(1, Math.trunc(size))}:${Math.max(1, Math.trunc(sampleCount))}`;
4234
- const cached = BRDF_LUT_UPLOAD_CACHE.get(cacheKey);
4235
- if (cached) {
4236
- return cached;
4237
- }
4238
- const width = Math.max(1, Math.trunc(size));
4239
- const height = Math.max(1, Math.trunc(size));
4240
- const rowBytes = width * 8;
4241
- const bytesPerRow = alignTo(rowBytes, 256);
4242
- const bytes = new Uint8Array(bytesPerRow * height);
4243
- const view = new DataView(bytes.buffer);
4244
- for (let y = 0; y < height; y += 1) {
4245
- const roughness = (y + 0.5) / height;
4246
- for (let x = 0; x < width; x += 1) {
4247
- const nDotV = Math.max((x + 0.5) / width, 1e-4);
4248
- const [scaleTerm, biasTerm] = integrateBrdfSample(nDotV, roughness, sampleCount);
4249
- const offset = y * bytesPerRow + x * 8;
4250
- view.setUint16(offset, float32ToFloat16Bits(scaleTerm), true);
4251
- view.setUint16(offset + 2, float32ToFloat16Bits(biasTerm), true);
4252
- view.setUint16(offset + 4, float32ToFloat16Bits(0), true);
4253
- view.setUint16(offset + 6, float32ToFloat16Bits(1), true);
4254
- }
4255
- }
4256
- const upload = Object.freeze({ bytes, bytesPerRow, width, height });
4257
- BRDF_LUT_UPLOAD_CACHE.set(cacheKey, upload);
4258
- return upload;
4259
- }
4260
- function createLinearEnvironmentPixels(environmentMap, fallbackColor) {
4261
- const width = Math.max(1, environmentMap.width);
4262
- const height = Math.max(1, environmentMap.height);
4263
- const pixels = new Float32Array(width * height * 4);
4264
- const data = environmentMap.data;
4265
- const integerScale = environmentMapIntegerScale(data);
4266
- for (let index = 0; index < width * height; index += 1) {
4267
- const sourceOffset = index * 4;
4268
- const targetOffset = index * 4;
4269
- pixels[targetOffset] = readEnvironmentMapComponent(data, sourceOffset, fallbackColor[0], integerScale);
4270
- pixels[targetOffset + 1] = readEnvironmentMapComponent(data, sourceOffset + 1, fallbackColor[1], integerScale);
4271
- pixels[targetOffset + 2] = readEnvironmentMapComponent(data, sourceOffset + 2, fallbackColor[2], integerScale);
4272
- pixels[targetOffset + 3] = readEnvironmentMapComponent(data, sourceOffset + 3, fallbackColor[3] ?? 1, integerScale);
4273
- }
4274
- return pixels;
4275
- }
4276
- function environmentUvToDirection(u, v, rotationRadians = 0) {
4277
- const angle = (u - rotationRadians / (2 * Math.PI) - 0.5) * 2 * Math.PI;
4278
- const theta = v * Math.PI;
4279
- const sinTheta = Math.sin(theta);
4280
- return [
4281
- Math.cos(angle) * sinTheta,
4282
- Math.cos(theta),
4283
- Math.sin(angle) * sinTheta
4284
- ];
4285
- }
4286
- function sampleEnvironmentPixelsBilinear(pixels, width, height, u, v) {
4287
- const wrappedU = (u % 1 + 1) % 1;
4288
- const clampedV = clamp2(v, 0, 1);
4289
- const x = wrappedU * width - 0.5;
4290
- const y = clampedV * height - 0.5;
4291
- const x0 = (Math.floor(x) % width + width) % width;
4292
- const y0 = clamp2(Math.floor(y), 0, height - 1);
4293
- const x1 = (x0 + 1) % width;
4294
- const y1 = clamp2(y0 + 1, 0, height - 1);
4295
- const tx = x - Math.floor(x);
4296
- const ty = y - Math.floor(y);
4297
- const read = (px, py) => {
4298
- const offset = (py * width + px) * 4;
4299
- 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 ?? {}
4300
4191
  };
4301
- const a = read(x0, y0);
4302
- const b = read(x1, y0);
4303
- const c = read(x0, y1);
4304
- const d = read(x1, y1);
4305
- const mixPair = (first, second, factor) => first * (1 - factor) + second * factor;
4306
- return [
4307
- mixPair(mixPair(a[0], b[0], tx), mixPair(c[0], d[0], tx), ty),
4308
- mixPair(mixPair(a[1], b[1], tx), mixPair(c[1], d[1], tx), ty),
4309
- mixPair(mixPair(a[2], b[2], tx), mixPair(c[2], d[2], tx), ty),
4310
- mixPair(mixPair(a[3], b[3], tx), mixPair(c[3], d[3], tx), ty)
4311
- ];
4312
- }
4313
- function directionToEnvironmentUv(direction, rotationRadians = 0) {
4314
- const unitDirection = normalize(direction, [0, 1, 0]);
4315
- const rotationTurns = rotationRadians / (2 * Math.PI);
4316
- const u = ((Math.atan2(unitDirection[2], unitDirection[0]) / (2 * Math.PI) + 0.5 + rotationTurns) % 1 + 1) % 1;
4317
- const v = Math.acos(clamp2(unitDirection[1], -1, 1)) / Math.PI;
4318
- return [u, clamp2(v, 0, 1)];
4319
- }
4320
- function sampleEnvironmentRadiance(pixels, width, height, direction, rotationRadians = 0) {
4321
- const [u, v] = directionToEnvironmentUv(direction, rotationRadians);
4322
- return sampleEnvironmentPixelsBilinear(pixels, width, height, u, v);
4323
- }
4324
- function createFloat16RgbaUploadFromLevels(levels) {
4325
- return levels.map((level) => {
4326
- const rowBytes = level.width * 8;
4327
- const bytesPerRow = alignTo(rowBytes, 256);
4328
- const bytes = new Uint8Array(bytesPerRow * level.height);
4329
- const view = new DataView(bytes.buffer);
4330
- for (let y = 0; y < level.height; y += 1) {
4331
- for (let x = 0; x < level.width; x += 1) {
4332
- const sourceOffset = (y * level.width + x) * 4;
4333
- const targetOffset = y * bytesPerRow + x * 8;
4334
- view.setUint16(targetOffset, float32ToFloat16Bits(level.data[sourceOffset]), true);
4335
- view.setUint16(targetOffset + 2, float32ToFloat16Bits(level.data[sourceOffset + 1]), true);
4336
- view.setUint16(targetOffset + 4, float32ToFloat16Bits(level.data[sourceOffset + 2]), true);
4337
- view.setUint16(targetOffset + 6, float32ToFloat16Bits(level.data[sourceOffset + 3]), true);
4338
- }
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
+ );
4339
4200
  }
4340
- return Object.freeze({ bytes, bytesPerRow, width: level.width, height: level.height });
4341
- });
4342
- }
4343
- function createPrefilteredEnvironmentLevels(environmentMap, fallbackColor) {
4344
- const sourcePixels = createLinearEnvironmentPixels(environmentMap, fallbackColor);
4345
- const sourceWidth = Math.max(1, environmentMap.width);
4346
- const sourceHeight = Math.max(1, environmentMap.height);
4347
- const mipLevelCount = Math.max(1, Math.floor(Math.log2(Math.max(sourceWidth, sourceHeight))) + 1);
4348
- const levels = [
4349
- Object.freeze({
4350
- width: sourceWidth,
4351
- height: sourceHeight,
4352
- data: sourcePixels
4353
- })
4354
- ];
4355
- for (let mipLevel = 1; mipLevel < mipLevelCount; mipLevel += 1) {
4356
- const width = Math.max(1, sourceWidth >> mipLevel);
4357
- const height = Math.max(1, sourceHeight >> mipLevel);
4358
- const roughness = mipLevelCount <= 1 ? 0 : mipLevel / (mipLevelCount - 1);
4359
- const data = new Float32Array(width * height * 4);
4360
- const sampleCount = roughness < 0.25 ? 64 : roughness < 0.6 ? 96 : 128;
4361
- for (let y = 0; y < height; y += 1) {
4362
- for (let x = 0; x < width; x += 1) {
4363
- const direction = environmentUvToDirection((x + 0.5) / width, (y + 0.5) / height, environmentMap.rotationRadians);
4364
- const normal = normalize(direction, [0, 1, 0]);
4365
- const viewDirection = normal;
4366
- let totalWeight = 0;
4367
- const accum = [0, 0, 0];
4368
- for (let sampleIndex = 0; sampleIndex < sampleCount; sampleIndex += 1) {
4369
- const xi = hammersley(sampleIndex, sampleCount);
4370
- const halfVector = importanceSampleGgx(xi, roughness, normal);
4371
- const viewDotHalf = Math.max(dot(viewDirection, halfVector), 0);
4372
- const lightDirection = normalize(
4373
- subtract(scale(halfVector, 2 * viewDotHalf), viewDirection),
4374
- normal
4375
- );
4376
- const nDotL = Math.max(dot(normal, lightDirection), 0);
4377
- if (nDotL <= 1e-6) {
4378
- continue;
4379
- }
4380
- const radiance = sampleEnvironmentRadiance(
4381
- sourcePixels,
4382
- sourceWidth,
4383
- sourceHeight,
4384
- lightDirection,
4385
- environmentMap.rotationRadians
4386
- );
4387
- accum[0] += radiance[0] * nDotL;
4388
- accum[1] += radiance[1] * nDotL;
4389
- accum[2] += radiance[2] * nDotL;
4390
- totalWeight += nDotL;
4391
- }
4392
- const offset = (y * width + x) * 4;
4393
- data[offset] = accum[0] / Math.max(totalWeight, 1e-6);
4394
- data[offset + 1] = accum[1] / Math.max(totalWeight, 1e-6);
4395
- data[offset + 2] = accum[2] / Math.max(totalWeight, 1e-6);
4396
- data[offset + 3] = 1;
4397
- }
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
+ );
4398
4212
  }
4399
- levels.push(Object.freeze({ width, height, data }));
4213
+ requiredLimits.maxSampledTexturesPerShaderStage = Math.max(
4214
+ Number(requiredLimits.maxSampledTexturesPerShaderStage ?? 0),
4215
+ traceTextureBindings
4216
+ );
4400
4217
  }
4401
- return Object.freeze({
4402
- levels,
4403
- mipLevelCount,
4404
- width: sourceWidth,
4405
- height: sourceHeight
4406
- });
4407
- }
4408
- function createEnvironmentSamplingTables(environmentMap, fallbackColor) {
4409
- if (!environmentMapHasSamplingData(environmentMap)) {
4410
- return Object.freeze({
4411
- width: 1,
4412
- height: 1,
4413
- pdf: new Float32Array([1]),
4414
- marginalCdf: new Float32Array([1]),
4415
- conditionalCdf: new Float32Array([1]),
4416
- hasImportanceData: false
4417
- });
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;
4418
4233
  }
4419
- const pixels = createLinearEnvironmentPixels(environmentMap, fallbackColor);
4420
- const width = Math.max(1, environmentMap.width);
4421
- const height = Math.max(1, environmentMap.height);
4422
- const pdf = new Float32Array(width * height);
4423
- const marginalCdf = new Float32Array(height);
4424
- const conditionalCdf = new Float32Array(width * height);
4425
- const rowSums = new Float32Array(height);
4426
- let totalWeight = 0;
4427
- for (let y = 0; y < height; y += 1) {
4428
- const theta = (y + 0.5) / height * Math.PI;
4429
- const sinTheta = Math.max(Math.sin(theta), 1e-4);
4430
- let rowWeight = 0;
4431
- for (let x = 0; x < width; x += 1) {
4432
- const offset = (y * width + x) * 4;
4433
- const luminance = pixels[offset] * 0.2126 + pixels[offset + 1] * 0.7152 + pixels[offset + 2] * 0.0722;
4434
- const weight = Math.max(luminance * sinTheta, 1e-6);
4435
- pdf[y * width + x] = weight;
4436
- rowWeight += weight;
4437
- conditionalCdf[y * width + x] = rowWeight;
4438
- }
4439
- rowSums[y] = rowWeight;
4440
- totalWeight += rowWeight;
4441
- if (rowWeight > 0) {
4442
- for (let x = 0; x < width; x += 1) {
4443
- conditionalCdf[y * width + x] /= rowWeight;
4444
- }
4445
- } else {
4446
- for (let x = 0; x < width; x += 1) {
4447
- conditionalCdf[y * width + x] = (x + 1) / width;
4448
- }
4449
- }
4450
- 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;
4451
4243
  }
4452
- for (let y = 0; y < height; y += 1) {
4453
- 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");
4454
4249
  }
4455
- for (let index = 0; index < pdf.length; index += 1) {
4456
- pdf[index] /= Math.max(totalWeight, 1e-6);
4250
+ if (requiredFeatures.length > 0) {
4251
+ descriptor.requiredFeatures = requiredFeatures;
4457
4252
  }
4458
- return Object.freeze({
4459
- width,
4460
- height,
4461
- pdf,
4462
- marginalCdf,
4463
- conditionalCdf,
4464
- hasImportanceData: true
4465
- });
4466
- }
4467
- function createEnvironmentMapResource(device, constants, environmentMap, fallbackColor) {
4468
- if (environmentMap.view) {
4469
- return Object.freeze({
4470
- view: environmentMap.view,
4471
- sampler: environmentMap.sampler ?? device.createSampler({
4472
- label: "plasius.wavefront.environmentMapSampler",
4473
- addressModeU: "repeat",
4474
- addressModeV: "clamp-to-edge",
4475
- magFilter: "linear",
4476
- minFilter: "linear",
4477
- mipmapFilter: "linear"
4478
- }),
4479
- texture: null,
4480
- ownsTexture: false,
4481
- width: Math.max(1, environmentMap.width),
4482
- height: Math.max(1, environmentMap.height),
4483
- mipLevelCount: Math.max(1, environmentMap.mipLevelCount ?? 1)
4484
- });
4253
+ if (Object.keys(requiredLimits).length > 0) {
4254
+ descriptor.requiredLimits = {
4255
+ ...descriptor.requiredLimits ?? {},
4256
+ ...requiredLimits
4257
+ };
4485
4258
  }
4486
- if (environmentMap.texture && typeof environmentMap.texture.createView === "function") {
4487
- return Object.freeze({
4488
- view: environmentMap.texture.createView(),
4489
- sampler: environmentMap.sampler ?? device.createSampler({
4490
- label: "plasius.wavefront.environmentMapSampler",
4491
- addressModeU: "repeat",
4492
- addressModeV: "clamp-to-edge",
4493
- magFilter: "linear",
4494
- minFilter: "linear",
4495
- mipmapFilter: "linear"
4496
- }),
4497
- texture: environmentMap.texture,
4498
- ownsTexture: false,
4499
- width: Math.max(1, environmentMap.width),
4500
- height: Math.max(1, environmentMap.height),
4501
- mipLevelCount: Math.max(1, environmentMap.mipLevelCount ?? 1)
4502
- });
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;
4503
4265
  }
4504
- const prefiltered = createPrefilteredEnvironmentLevels(environmentMap, fallbackColor);
4505
- const uploads = createFloat16RgbaUploadFromLevels(prefiltered.levels);
4506
- const texture = device.createTexture({
4507
- label: environmentMap.enabled ? "plasius.wavefront.environmentMap" : "plasius.wavefront.environmentMapFallback",
4508
- size: { width: prefiltered.width, height: prefiltered.height },
4509
- format: "rgba16float",
4510
- mipLevelCount: prefiltered.mipLevelCount,
4511
- usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
4512
- });
4513
- uploads.forEach((upload, mipLevel) => {
4514
- device.queue.writeTexture(
4515
- { texture, mipLevel },
4516
- upload.bytes,
4517
- { bytesPerRow: upload.bytesPerRow, rowsPerImage: upload.height },
4518
- { width: upload.width, height: upload.height, depthOrArrayLayers: 1 }
4519
- );
4520
- });
4521
- return Object.freeze({
4522
- view: texture.createView(),
4523
- sampler: environmentMap.sampler ?? device.createSampler({
4524
- label: "plasius.wavefront.environmentMapSampler",
4525
- addressModeU: "repeat",
4526
- addressModeV: "clamp-to-edge",
4527
- magFilter: "linear",
4528
- minFilter: "linear",
4529
- mipmapFilter: "linear"
4530
- }),
4531
- texture,
4532
- ownsTexture: true,
4533
- width: prefiltered.width,
4534
- height: prefiltered.height,
4535
- mipLevelCount: prefiltered.mipLevelCount
4536
- });
4266
+ const deviceValue = Number(device?.limits?.[name]);
4267
+ return Number.isFinite(deviceValue) ? deviceValue : null;
4537
4268
  }
4538
- function createEnvironmentSamplingTextureResource(device, constants, environmentMap, fallbackColor) {
4539
- const tables = createEnvironmentSamplingTables(environmentMap, fallbackColor);
4540
- const rowBytes = tables.width * 8;
4541
- const bytesPerRow = alignTo(rowBytes, 256);
4542
- const bytes = new Uint8Array(bytesPerRow * tables.height);
4543
- const view = new DataView(bytes.buffer);
4544
- for (let y = 0; y < tables.height; y += 1) {
4545
- for (let x = 0; x < tables.width; x += 1) {
4546
- const probability = tables.pdf[y * tables.width + x];
4547
- const conditional = tables.conditionalCdf[y * tables.width + x];
4548
- const marginal = tables.marginalCdf[y];
4549
- const offset = y * bytesPerRow + x * 8;
4550
- view.setUint16(offset, float32ToFloat16Bits(probability), true);
4551
- view.setUint16(offset + 2, float32ToFloat16Bits(conditional), true);
4552
- view.setUint16(offset + 4, float32ToFloat16Bits(marginal), true);
4553
- view.setUint16(offset + 6, float32ToFloat16Bits(1), true);
4554
- }
4269
+ function createAdapterInfoSnapshot(adapter) {
4270
+ const info = adapter?.info;
4271
+ if (!info || typeof info !== "object") {
4272
+ return null;
4555
4273
  }
4556
- const texture = device.createTexture({
4557
- label: "plasius.wavefront.environmentSampling",
4558
- size: { width: tables.width, height: tables.height },
4559
- format: "rgba16float",
4560
- usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
4561
- });
4562
- device.queue.writeTexture(
4563
- { texture },
4564
- bytes,
4565
- { bytesPerRow, rowsPerImage: tables.height },
4566
- { width: tables.width, height: tables.height, depthOrArrayLayers: 1 }
4567
- );
4568
4274
  return Object.freeze({
4569
- view: texture.createView(),
4570
- texture,
4571
- ownsTexture: true,
4572
- 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 : ""
4573
4279
  });
4574
4280
  }
4575
- function createBrdfLutResource(device, constants, size = DEFAULT_BRDF_LUT_SIZE) {
4576
- const upload = createBrdfLutUploadBytes(size);
4577
- const texture = device.createTexture({
4578
- label: "plasius.wavefront.brdfLut",
4579
- size: { width: upload.width, height: upload.height },
4580
- format: "rgba16float",
4581
- usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
4582
- });
4583
- device.queue.writeTexture(
4584
- { texture },
4585
- upload.bytes,
4586
- { bytesPerRow: upload.bytesPerRow, rowsPerImage: upload.height },
4587
- { width: upload.width, height: upload.height, depthOrArrayLayers: 1 }
4588
- );
4281
+ function createGpuAdapterParallelismDiagnostics(adapter, device) {
4589
4282
  return Object.freeze({
4590
- view: texture.createView(),
4591
- sampler: device.createSampler({
4592
- label: "plasius.wavefront.brdfLutSampler",
4593
- addressModeU: "clamp-to-edge",
4594
- addressModeV: "clamp-to-edge",
4595
- magFilter: "linear",
4596
- 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")
4597
4296
  }),
4598
- texture,
4599
- ownsTexture: true,
4600
- width: upload.width,
4601
- height: upload.height
4297
+ configuredWorkgroupSize: WORKGROUP_SIZE
4602
4298
  });
4603
4299
  }
4604
- function createMediumTextureResource(device, constants, mediums) {
4605
- const normalized = Array.isArray(mediums) && mediums.length > 0 ? mediums : [{ id: 0 }];
4606
- 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(
4607
4337
  1,
4608
- normalized.reduce((maximum, medium) => Math.max(maximum, medium.id ?? 0), 0) + 1
4338
+ Number(config?.renderedSamplesPerPixel ?? config?.samplesPerPixel ?? 1)
4609
4339
  );
4610
- const level = {
4611
- width,
4612
- height: MEDIUM_TABLE_ROWS,
4613
- data: new Float32Array(width * MEDIUM_TABLE_ROWS * 4)
4614
- };
4615
- for (const medium of normalized) {
4616
- const mediumId = Math.max(0, Math.trunc(Number(medium.id) || 0));
4617
- const absorptionOffset = mediumId * 4;
4618
- level.data[absorptionOffset] = Math.max(0, medium.absorption?.[0] ?? 0);
4619
- level.data[absorptionOffset + 1] = Math.max(0, medium.absorption?.[1] ?? 0);
4620
- level.data[absorptionOffset + 2] = Math.max(0, medium.absorption?.[2] ?? 0);
4621
- level.data[absorptionOffset + 3] = Math.max(0, medium.phaseModel ?? 0);
4622
- const scatteringOffset = (width + mediumId) * 4;
4623
- level.data[scatteringOffset] = Math.max(0, medium.scattering?.[0] ?? 0);
4624
- level.data[scatteringOffset + 1] = Math.max(0, medium.scattering?.[1] ?? 0);
4625
- level.data[scatteringOffset + 2] = Math.max(0, medium.scattering?.[2] ?? 0);
4626
- level.data[scatteringOffset + 3] = Math.max(0, medium.density ?? 0);
4627
- }
4628
- const upload = createFloat16RgbaUploadFromLevels([level])[0];
4629
- const texture = device.createTexture({
4630
- label: "plasius.wavefront.mediumTable",
4631
- size: { width, height: MEDIUM_TABLE_ROWS },
4632
- format: "rgba16float",
4633
- usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
4634
- });
4635
- device.queue.writeTexture(
4636
- { texture },
4637
- upload.bytes,
4638
- { bytesPerRow: upload.bytesPerRow, rowsPerImage: upload.height },
4639
- { 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)
4640
4360
  );
4641
4361
  return Object.freeze({
4642
- texture,
4643
- view: texture.createView(),
4644
- ownsTexture: true,
4645
- count: normalized.length,
4646
- width
4362
+ timeoutMs,
4363
+ maxWaitMs
4647
4364
  });
4648
4365
  }
4649
- function mediumTablesEqual(left, right) {
4650
- const leftMediums = Array.isArray(left) ? left : [];
4651
- const rightMediums = Array.isArray(right) ? right : [];
4652
- if (leftMediums.length !== rightMediums.length) {
4653
- 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;
4654
4371
  }
4655
- for (let index = 0; index < leftMediums.length; index += 1) {
4656
- const leftMedium = leftMediums[index];
4657
- const rightMedium = rightMediums[index];
4658
- if ((leftMedium?.id ?? 0) !== (rightMedium?.id ?? 0)) {
4659
- return false;
4660
- }
4661
- if ((leftMedium?.phaseModel ?? 0) !== (rightMedium?.phaseModel ?? 0)) {
4662
- 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;
4663
4385
  }
4664
- 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
+ );
4665
4403
  return false;
4666
4404
  }
4667
- for (let component = 0; component < 3; component += 1) {
4668
- if ((leftMedium?.absorption?.[component] ?? 0) !== (rightMedium?.absorption?.[component] ?? 0)) {
4669
- 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;
4670
4412
  }
4671
- if ((leftMedium?.scattering?.[component] ?? 0) !== (rightMedium?.scattering?.[component] ?? 0)) {
4672
- 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));
4673
4603
  }
4674
4604
  }
4675
- }
4676
- 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
+ };
4677
4617
  }
4678
- function createAtlasTextureResource(device, constants, atlas, label) {
4679
- const upload = createRgba8TextureUpload(atlas);
4680
- const texture = device.createTexture({
4681
- label,
4682
- size: { width: upload.width, height: upload.height },
4683
- format: "rgba8unorm",
4684
- usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
4685
- });
4686
- device.queue.writeTexture(
4687
- { texture },
4688
- upload.bytes,
4689
- { bytesPerRow: upload.bytesPerRow, rowsPerImage: upload.height },
4690
- { 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
4691
4634
  );
4692
- return Object.freeze({
4693
- texture,
4694
- view: texture.createView(),
4695
- ownsTexture: true
4696
- });
4697
4635
  }
4698
-
4699
- // src/wavefront-runtime-support.js
4700
- var WEBGPU_DEFAULT_MAX_STORAGE_BUFFER_BINDING_SIZE = 128 * 1024 * 1024;
4701
- var WEBGPU_DEFAULT_MAX_BUFFER_SIZE = 256 * 1024 * 1024;
4702
- function readRequestedDeviceLimit(source, name) {
4703
- const rawValue = source?.[name];
4704
- if (rawValue === void 0 || rawValue === null) {
4705
- return 0;
4706
- }
4707
- const value = Number(rawValue);
4708
- if (!Number.isSafeInteger(value) || value < 0) {
4709
- throw new Error(`${name} must be a non-negative safe integer.`);
4710
- }
4711
- 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;
4712
4644
  }
4713
- function assertAdapterSupportsRequiredLimit(adapter, name, requiredValue) {
4714
- const availableValue = Number(adapter?.limits?.[name]);
4715
- if (!Number.isFinite(availableValue)) {
4716
- throw new Error(
4717
- `Wavefront scene buffers require ${name}>=${requiredValue} bytes, but this adapter does not expose that limit.`
4718
- );
4719
- }
4720
- if (availableValue < requiredValue) {
4721
- throw new Error(
4722
- `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
4723
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;
4724
4689
  }
4690
+ return [scaleTerm / sampleCount, biasTerm / sampleCount];
4725
4691
  }
4726
- function getGpuUsageConstants() {
4727
- if (typeof GPUBufferUsage === "undefined" || typeof GPUTextureUsage === "undefined" || typeof GPUShaderStage === "undefined") {
4728
- 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;
4729
4698
  }
4730
- if (typeof GPUBufferUsage.INDIRECT !== "number") {
4731
- 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;
4732
4710
  }
4733
- return {
4734
- buffer: GPUBufferUsage,
4735
- texture: GPUTextureUsage,
4736
- shader: GPUShaderStage,
4737
- map: typeof GPUMapMode === "undefined" ? null : GPUMapMode
4738
- };
4711
+ return Math.max(16, Math.floor(Math.sqrt(maxPixels)));
4739
4712
  }
4740
- function resolveCanvas3(canvasOrSelector, documentRef = globalThis.document) {
4741
- if (typeof canvasOrSelector === "string") {
4742
- const resolved = documentRef?.querySelector?.(canvasOrSelector);
4743
- if (!resolved) {
4744
- throw new Error(`Unable to find canvas for selector: ${canvasOrSelector}`);
4745
- }
4746
- 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);
4747
4734
  }
4748
- if (canvasOrSelector?.getContext) {
4749
- return canvasOrSelector;
4735
+ exponent = exponent - 127 + 15;
4736
+ if (exponent >= 31) {
4737
+ return sign | 31744;
4750
4738
  }
4751
- const fallback = documentRef?.querySelector?.("canvas[data-plasius-wavefront-path-tracing]");
4752
- if (!fallback) {
4753
- 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;
4754
4745
  }
4755
- return fallback;
4746
+ return sign | exponent << 10 | mantissa + 4096 >> 13;
4756
4747
  }
4757
- async function getPipelineDiagnostics(shaderModule) {
4758
- if (typeof shaderModule?.getCompilationInfo !== "function") {
4759
- return "";
4748
+ function environmentMapIntegerScale(data) {
4749
+ if (data instanceof Uint8Array) {
4750
+ return 1 / 255;
4760
4751
  }
4761
- try {
4762
- const info = await shaderModule.getCompilationInfo();
4763
- const messages = info.messages ?? [];
4764
- if (messages.length === 0) {
4765
- return "";
4766
- }
4767
- return messages.map((message) => {
4768
- const line = message.lineNum ?? "?";
4769
- const column = message.linePos ?? "?";
4770
- return `line ${line}:${column} ${message.message}`;
4771
- }).join("\n");
4772
- } catch {
4773
- return "";
4752
+ if (data instanceof Uint16Array) {
4753
+ return 1 / 65535;
4774
4754
  }
4755
+ return 1;
4775
4756
  }
4776
- async function createComputePipeline(device, shaderModule, layout, entryPoint, label) {
4777
- const descriptor = {
4778
- label,
4779
- layout,
4780
- compute: {
4781
- module: shaderModule,
4782
- entryPoint
4783
- }
4784
- };
4785
- try {
4786
- if (typeof device.createComputePipelineAsync === "function") {
4787
- return await device.createComputePipelineAsync(descriptor);
4788
- }
4789
- return device.createComputePipeline(descriptor);
4790
- } catch (error) {
4791
- const diagnostics = await getPipelineDiagnostics(shaderModule);
4792
- const suffix = diagnostics ? `
4793
- ${diagnostics}` : "";
4794
- throw new Error(`WGSL compilation failed for ${label}: ${error.message}${suffix}`, {
4795
- cause: error
4796
- });
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;
4797
4786
  }
4787
+ const value = Number(data[index]);
4788
+ return Number.isFinite(value) ? Math.max(0, value) * integerScale : fallback;
4798
4789
  }
4799
- async function assertShaderModuleCompiles(shaderModule, label) {
4800
- if (typeof shaderModule?.getCompilationInfo !== "function") {
4801
- 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;
4802
4795
  }
4803
- const info = await shaderModule.getCompilationInfo();
4804
- const messages = Array.isArray(info?.messages) ? info.messages : [];
4805
- const errors = messages.filter((message) => message?.type === "error");
4806
- if (errors.length <= 0) {
4807
- 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
+ }
4808
4813
  }
4809
- const diagnostics = errors.map((message) => {
4810
- const line = Number.isFinite(message.lineNum) ? message.lineNum : "?";
4811
- const column = Number.isFinite(message.linePos) ? message.linePos : "?";
4812
- return `line ${line}:${column} ${message.message}`;
4813
- }).join("\n");
4814
- throw new Error(`WGSL compilation preflight failed for ${label}:
4815
- ${diagnostics}`);
4814
+ const upload = Object.freeze({ bytes, bytesPerRow, width, height });
4815
+ BRDF_LUT_UPLOAD_CACHE.set(cacheKey, upload);
4816
+ return upload;
4816
4817
  }
4817
- async function createRenderPipeline(device, descriptor) {
4818
- if (typeof device.createRenderPipelineAsync === "function") {
4819
- 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);
4820
4831
  }
4821
- return device.createRenderPipeline(descriptor);
4832
+ return pixels;
4822
4833
  }
4823
- function createWavefrontDeviceDescriptor(adapter, options = {}, memory = null) {
4824
- const descriptor = { ...options.deviceDescriptor ?? {} };
4825
- const descriptorRequiredLimits = { ...descriptor.requiredLimits ?? {} };
4826
- const requiredLimits = {
4827
- ...descriptorRequiredLimits,
4828
- ...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]];
4829
4858
  };
4830
- const exposedStorageBufferLimit = Number(adapter?.limits?.maxStorageBuffersPerShaderStage);
4831
- if (Number.isFinite(exposedStorageBufferLimit)) {
4832
- if (exposedStorageBufferLimit < TRACE_STORAGE_BUFFER_BINDINGS) {
4833
- throw new Error(
4834
- `Wavefront mesh tracing requires maxStorageBuffersPerShaderStage>=${TRACE_STORAGE_BUFFER_BINDINGS}, but this adapter exposes ${exposedStorageBufferLimit}.`
4835
- );
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
+ }
4836
4897
  }
4837
- requiredLimits.maxStorageBuffersPerShaderStage = Math.max(
4838
- Number(requiredLimits.maxStorageBuffersPerShaderStage ?? 0),
4839
- TRACE_STORAGE_BUFFER_BINDINGS
4840
- );
4841
- }
4842
- const exposedSampledTextureLimit = Number(adapter?.limits?.maxSampledTexturesPerShaderStage);
4843
- if (Number.isFinite(exposedSampledTextureLimit)) {
4844
- if (exposedSampledTextureLimit < TRACE_SAMPLED_TEXTURE_BINDINGS) {
4845
- throw new Error(
4846
- `Wavefront material tracing requires maxSampledTexturesPerShaderStage>=${TRACE_SAMPLED_TEXTURE_BINDINGS}, but this adapter exposes ${exposedSampledTextureLimit}.`
4847
- );
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
+ }
4848
4956
  }
4849
- requiredLimits.maxSampledTexturesPerShaderStage = Math.max(
4850
- Number(requiredLimits.maxSampledTexturesPerShaderStage ?? 0),
4851
- TRACE_SAMPLED_TEXTURE_BINDINGS
4852
- );
4853
- }
4854
- const sceneStorageBufferBindingBytes = Number(
4855
- memory?.sceneStorageBufferBindingBytes ?? 0
4856
- );
4857
- const requestedStorageBufferBindingSize = Math.max(
4858
- readRequestedDeviceLimit(descriptorRequiredLimits, "maxStorageBufferBindingSize"),
4859
- readRequestedDeviceLimit(options.requiredLimits, "maxStorageBufferBindingSize"),
4860
- sceneStorageBufferBindingBytes > WEBGPU_DEFAULT_MAX_STORAGE_BUFFER_BINDING_SIZE ? sceneStorageBufferBindingBytes : 0
4861
- );
4862
- if (requestedStorageBufferBindingSize > 0) {
4863
- assertAdapterSupportsRequiredLimit(
4864
- adapter,
4865
- "maxStorageBufferBindingSize",
4866
- requestedStorageBufferBindingSize
4867
- );
4868
- requiredLimits.maxStorageBufferBindingSize = requestedStorageBufferBindingSize;
4869
- }
4870
- const requestedMaxBufferSize = Math.max(
4871
- readRequestedDeviceLimit(descriptorRequiredLimits, "maxBufferSize"),
4872
- readRequestedDeviceLimit(options.requiredLimits, "maxBufferSize"),
4873
- sceneStorageBufferBindingBytes > WEBGPU_DEFAULT_MAX_BUFFER_SIZE ? sceneStorageBufferBindingBytes : 0,
4874
- requestedStorageBufferBindingSize > WEBGPU_DEFAULT_MAX_BUFFER_SIZE ? requestedStorageBufferBindingSize : 0
4875
- );
4876
- if (requestedMaxBufferSize > 0) {
4877
- assertAdapterSupportsRequiredLimit(adapter, "maxBufferSize", requestedMaxBufferSize);
4878
- requiredLimits.maxBufferSize = requestedMaxBufferSize;
4957
+ levels.push(Object.freeze({ width, height, data }));
4879
4958
  }
4880
- const requiredFeatures = Array.from(descriptor.requiredFeatures ?? []);
4881
- const adapterFeatures = adapter?.features;
4882
- const timestampQuerySupported = typeof adapterFeatures?.has === "function" ? adapterFeatures.has("timestamp-query") : adapterFeatures && typeof adapterFeatures[Symbol.iterator] === "function" ? Array.from(adapterFeatures).includes("timestamp-query") : false;
4883
- if (options.gpuTimestamps !== false && timestampQuerySupported && !requiredFeatures.includes("timestamp-query")) {
4884
- 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
+ });
4885
4976
  }
4886
- if (requiredFeatures.length > 0) {
4887
- 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;
4888
5009
  }
4889
- if (Object.keys(requiredLimits).length > 0) {
4890
- descriptor.requiredLimits = {
4891
- ...descriptor.requiredLimits ?? {},
4892
- ...requiredLimits
4893
- };
5010
+ for (let y = 0; y < height; y += 1) {
5011
+ marginalCdf[y] /= Math.max(totalWeight, 1e-6);
4894
5012
  }
4895
- 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
+ });
4896
5024
  }
4897
- function readGpuLimit(adapter, device, name) {
4898
- const adapterValue = Number(adapter?.limits?.[name]);
4899
- if (Number.isFinite(adapterValue)) {
4900
- 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
+ });
4901
5043
  }
4902
- const deviceValue = Number(device?.limits?.[name]);
4903
- 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
+ });
4904
5095
  }
4905
- function createAdapterInfoSnapshot(adapter) {
4906
- const info = adapter?.info;
4907
- if (!info || typeof info !== "object") {
4908
- 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
+ }
4909
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
+ );
4910
5126
  return Object.freeze({
4911
- vendor: typeof info.vendor === "string" ? info.vendor : "",
4912
- architecture: typeof info.architecture === "string" ? info.architecture : "",
4913
- device: typeof info.device === "string" ? info.device : "",
4914
- description: typeof info.description === "string" ? info.description : ""
5127
+ view: texture.createView(),
5128
+ texture,
5129
+ ownsTexture: true,
5130
+ hasImportanceData: tables.hasImportanceData
4915
5131
  });
4916
5132
  }
4917
- 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
+ );
4918
5147
  return Object.freeze({
4919
- physicalCoreCount: null,
4920
- physicalCoreCountAvailable: false,
4921
- physicalCoreCountUnavailableReason: "WebGPU does not expose physical GPU core counts.",
4922
- adapterInfo: createAdapterInfoSnapshot(adapter),
4923
- adapterLimits: Object.freeze({
4924
- maxComputeInvocationsPerWorkgroup: readGpuLimit(adapter, device, "maxComputeInvocationsPerWorkgroup"),
4925
- maxComputeWorkgroupSizeX: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeX"),
4926
- maxComputeWorkgroupSizeY: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeY"),
4927
- maxComputeWorkgroupSizeZ: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeZ"),
4928
- maxComputeWorkgroupsPerDimension: readGpuLimit(adapter, device, "maxComputeWorkgroupsPerDimension"),
4929
- maxStorageBuffersPerShaderStage: readGpuLimit(adapter, device, "maxStorageBuffersPerShaderStage"),
4930
- maxStorageBufferBindingSize: readGpuLimit(adapter, device, "maxStorageBufferBindingSize"),
4931
- 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"
4932
5155
  }),
4933
- configuredWorkgroupSize: WORKGROUP_SIZE
5156
+ texture,
5157
+ ownsTexture: true,
5158
+ width: upload.width,
5159
+ height: upload.height
4934
5160
  });
4935
5161
  }
4936
- function createEnvironmentMapSnapshot(environmentMap) {
4937
- return Object.freeze({
4938
- enabled: environmentMap.enabled,
4939
- width: environmentMap.width,
4940
- height: environmentMap.height,
4941
- mipLevelCount: environmentMap.mipLevelCount ?? 1,
4942
- projection: environmentMap.projection,
4943
- intensity: environmentMap.intensity,
4944
- rotationRadians: environmentMap.rotationRadians,
4945
- ambientStrength: environmentMap.ambientStrength,
4946
- 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
4947
5205
  });
4948
5206
  }
4949
- function nowMs3() {
4950
- if (typeof performance !== "undefined" && typeof performance.now === "function") {
4951
- 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;
4952
5212
  }
4953
- return Date.now();
4954
- }
4955
- function estimateAccelerationBuildWaitFactor(config) {
4956
- if (config?.gpuAccelerationBuildRequired !== true) {
4957
- 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
+ }
4958
5233
  }
4959
- const bvhSortStageCount = Array.isArray(config?.bvhSortStages) ? config.bvhSortStages.length : 0;
4960
- const bvhBuildLevelCount = Array.isArray(config?.bvhBuildLevels) ? config.bvhBuildLevels.length : 0;
4961
- const accelerationStageCount = 2 + bvhSortStageCount + bvhBuildLevelCount;
4962
- return Math.max(1, 1 + accelerationStageCount / 96);
5234
+ return true;
4963
5235
  }
4964
- function estimateSubmittedGpuWorkTiming(config, tileCount, overrideTimeoutMs = null, options = {}) {
4965
- if (Number.isFinite(overrideTimeoutMs)) {
4966
- const overrideMs = Math.max(1, Math.trunc(Number(overrideTimeoutMs)));
4967
- return Object.freeze({
4968
- timeoutMs: overrideMs,
4969
- maxWaitMs: overrideMs
4970
- });
4971
- }
4972
- const samplesPerPixel = Math.max(
4973
- 1,
4974
- Number(config?.renderedSamplesPerPixel ?? config?.samplesPerPixel ?? 1)
4975
- );
4976
- const maxDepth = Math.max(1, Number(config?.maxDepth ?? 1));
4977
- const deferredResolvePasses = config?.deferredPathResolve ? 1 : 0;
4978
- const denoisePasses = config?.denoise ? samplesPerPixel < 4 ? 2 : 1 : 0;
4979
- const tiles = Math.max(1, Number(tileCount ?? 1));
4980
- const estimatedPasses = tiles * (samplesPerPixel * (maxDepth + 1 + deferredResolvePasses) + denoisePasses + 1);
4981
- const triangleCount = Math.max(0, Number(config?.triangleCount ?? 0));
4982
- const geometryFactor = Math.max(1, triangleCount / 131072);
4983
- const includeAccelerationBuild = options.includeAccelerationBuild === true;
4984
- const accelerationFactor = includeAccelerationBuild ? estimateAccelerationBuildWaitFactor(config) : 1;
4985
- const estimatedWindowMs = Math.round(
4986
- (GPU_SUBMITTED_WORK_TIMEOUT_MS + estimatedPasses * 5) * geometryFactor * accelerationFactor
4987
- );
4988
- const timeoutMs = Math.min(
4989
- GPU_MAX_SUBMITTED_WORK_TIMEOUT_MS,
4990
- Math.max(GPU_SUBMITTED_WORK_TIMEOUT_MS, estimatedWindowMs)
4991
- );
4992
- const maxWaitMultiplier = includeAccelerationBuild ? 3 : 2;
4993
- const maxWaitMs = Math.min(
4994
- GPU_MAX_SUBMITTED_WORK_DEADLINE_MS,
4995
- 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 }
4996
5249
  );
4997
5250
  return Object.freeze({
4998
- timeoutMs,
4999
- maxWaitMs
5251
+ texture,
5252
+ view: texture.createView(),
5253
+ ownsTexture: true
5000
5254
  });
5001
5255
  }
5002
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
+
5003
5344
  // src/wavefront-shader-bvh.js
5004
5345
  var WAVEFRONT_SHADER_BVH_WGSL = `
5005
5346
  fn default_mesh_range() -> MeshRange {
@@ -5462,6 +5803,7 @@ fn no_candidate() -> Candidate {
5462
5803
  1u,
5463
5804
  0xffffffffu,
5464
5805
  0xffffffffu,
5806
+ 0xffffffffu,
5465
5807
  0u,
5466
5808
  0u
5467
5809
  );
@@ -5488,6 +5830,7 @@ fn surface_candidate(
5488
5830
  uv,
5489
5831
  frontFace,
5490
5832
  triangleIndex,
5833
+ 0xffffffffu,
5491
5834
  primitiveId,
5492
5835
  materialRefId,
5493
5836
  mediumRefId
@@ -5549,7 +5892,7 @@ fn intersect_triangle(ray: RayRecord, triangle: TriangleRecord, triangleIndex: u
5549
5892
  );
5550
5893
  }
5551
5894
 
5552
- fn intersect_bvh(ray: RayRecord, initialNearest: f32) -> Candidate {
5895
+ fn intersect_geometry_bvh(ray: RayRecord, initialNearest: f32, root: u32) -> Candidate {
5553
5896
  var nearest = initialNearest;
5554
5897
  var best = no_candidate();
5555
5898
  if (config.bvhNodeCount == 0u || config.triangleCount == 0u) {
@@ -5558,7 +5901,7 @@ fn intersect_bvh(ray: RayRecord, initialNearest: f32) -> Candidate {
5558
5901
 
5559
5902
  var stack = array<u32, 64>();
5560
5903
  var stackSize = 1u;
5561
- stack[0] = 0u;
5904
+ stack[0] = root;
5562
5905
 
5563
5906
  loop {
5564
5907
  if (stackSize == 0u) {
@@ -5849,7 +6192,7 @@ fn intersectActiveQueue(@builtin(global_invocation_id) globalId: vec3<u32>) {
5849
6192
  if (meshCandidate.hit == 1u && meshCandidate.distance < nearest) {
5850
6193
  nearest = meshCandidate.distance;
5851
6194
  candidate = meshCandidate;
5852
- hitTriangle = triangles[meshCandidate.triangleIndex];
6195
+ hitTriangle = triangle_for_candidate(meshCandidate);
5853
6196
  }
5854
6197
 
5855
6198
  if (candidate.hit == 0u) {
@@ -7003,6 +7346,7 @@ struct Candidate {
7003
7346
  uv: vec2<f32>,
7004
7347
  frontFace: u32,
7005
7348
  triangleIndex: u32,
7349
+ instanceIndex: u32,
7006
7350
  primitiveId: u32,
7007
7351
  materialRefId: u32,
7008
7352
  mediumRefId: u32,
@@ -8600,6 +8944,7 @@ var WAVEFRONT_COMPUTE_WGSL = [
8600
8944
  WAVEFRONT_SHADER_MATERIALS_WGSL,
8601
8945
  WAVEFRONT_SHADER_LIGHTING_WGSL,
8602
8946
  WAVEFRONT_SHADER_BVH_WGSL,
8947
+ WAVEFRONT_UNINSTANCED_SHADER_WGSL,
8603
8948
  WAVEFRONT_SHADER_KERNELS_WGSL
8604
8949
  ].join("\n");
8605
8950
  function createWavefrontPathTracingComputeShaderSource(options = {}) {
@@ -8611,7 +8956,7 @@ function createWavefrontPathTracingComputeShaderSource(options = {}) {
8611
8956
  if (workgroupSize !== WAVEFRONT_COMPUTE_WORKGROUP_SIZE) {
8612
8957
  throw new Error(`wavefront mesh compute currently requires workgroupSize=${WAVEFRONT_COMPUTE_WORKGROUP_SIZE}`);
8613
8958
  }
8614
- return WAVEFRONT_COMPUTE_WGSL;
8959
+ return options.instanced === true ? WAVEFRONT_COMPUTE_WGSL.replace(WAVEFRONT_UNINSTANCED_SHADER_WGSL, WAVEFRONT_INSTANCE_SHADER_WGSL) : WAVEFRONT_COMPUTE_WGSL;
8615
8960
  }
8616
8961
 
8617
8962
  // src/wavefront-progressive-shader.js
@@ -9615,7 +9960,7 @@ function chooseSplitAxis(triangles) {
9615
9960
  }
9616
9961
  return extent[1] >= extent[2] ? 1 : 2;
9617
9962
  }
9618
- function buildBvh(triangles, maxLeafTriangles = 4) {
9963
+ function buildWavefrontBoundsBvh(triangles, maxLeafTriangles = 4) {
9619
9964
  if (triangles.length === 0) {
9620
9965
  return Object.freeze({ nodes: Object.freeze([]), triangles: Object.freeze([]) });
9621
9966
  }
@@ -9667,7 +10012,7 @@ function createWavefrontMeshAcceleration(meshes = [], gpuMaterialSource = null)
9667
10012
  const source = Array.isArray(meshes) ? meshes : [meshes];
9668
10013
  const resolvedMaterialSource = gpuMaterialSource ?? createWavefrontGpuMaterialSource(source);
9669
10014
  const triangles = createMeshTriangleRecords(source, resolvedMaterialSource);
9670
- return buildBvh(triangles);
10015
+ return buildWavefrontBoundsBvh(triangles);
9671
10016
  }
9672
10017
  function estimateMeshSourceShape(meshes) {
9673
10018
  const source = Array.isArray(meshes) ? meshes : [];
@@ -10485,6 +10830,145 @@ function dispatchWavefrontGpuAccelerationBuild({
10485
10830
  return true;
10486
10831
  }
10487
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
+
10488
10972
  // src/wavefront-config.js
10489
10973
  var WEBGPU_DEFAULT_MIN_UNIFORM_BUFFER_OFFSET_ALIGNMENT = 256;
10490
10974
  function allocatedRecordBytes(count, recordBytes) {
@@ -10758,6 +11242,7 @@ function estimateWavefrontPathTracingMemory(options = {}) {
10758
11242
  const progressiveBytes = options.progressive === true ? width * height * 16 + 16 : 0;
10759
11243
  const pathVertexBytes = tilePixelCapacity * (maxDepth + 1) * PATH_VERTEX_RECORD_BYTES;
10760
11244
  const sceneObjectBytes = sceneObjectCapacity * SCENE_OBJECT_RECORD_BYTES;
11245
+ const instanceBytes = readNonNegativeInteger2("instanceBytes", options.instanceBytes, 0);
10761
11246
  const triangleBytes = allocatedRecordBytes(triangleCapacity, TRIANGLE_RECORD_BYTES);
10762
11247
  const materialTableBytes = 0;
10763
11248
  const bvhNodeBytes = bvhNodeCapacity * BVH_NODE_RECORD_BYTES;
@@ -10813,6 +11298,7 @@ function estimateWavefrontPathTracingMemory(options = {}) {
10813
11298
  pathVertexBytes,
10814
11299
  sceneObjectBytes,
10815
11300
  triangleBytes,
11301
+ instanceBytes,
10816
11302
  materialTableBytes,
10817
11303
  bvhNodeBytes,
10818
11304
  bvhCombinedBytes,
@@ -10842,6 +11328,7 @@ function estimateWavefrontPathTracingMemoryForConfig(config, limits = {}) {
10842
11328
  maxDepth: config.maxDepth,
10843
11329
  sceneObjectCapacity: config.sceneObjectCapacity,
10844
11330
  triangleCapacity: config.triangleCapacity,
11331
+ instanceBytes: config.instancing?.instanceBytes ?? 0,
10845
11332
  bvhNodeCapacity: config.bvhNodeCapacity,
10846
11333
  bvhLeafSortCapacity: config.bvhLeafSortCapacity,
10847
11334
  emissiveTriangleCapacity: config.emissiveTriangleCapacity,
@@ -10860,6 +11347,7 @@ function estimateWavefrontPathTracingMemoryForConfig(config, limits = {}) {
10860
11347
  function createWavefrontPathTracingComputeConfig(options = {}) {
10861
11348
  assertAnalyticDisplayQualityPolicy(options);
10862
11349
  const accelerationBuildMode = resolveAccelerationBuildMode(options);
11350
+ if (options.instances !== void 0 && accelerationBuildMode === "gpu") throw new Error("Instance BLAS construction currently requires cpu-upload acceleration.");
10863
11351
  const canvas = options.canvas;
10864
11352
  const width = readPositiveInteger2("width", options.width, getCanvasDimension(canvas, "width", DEFAULT_WIDTH));
10865
11353
  const height = readPositiveInteger2("height", options.height, getCanvasDimension(canvas, "height", DEFAULT_HEIGHT));
@@ -10890,9 +11378,16 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
10890
11378
  );
10891
11379
  const meshes = normalizeMeshes(options);
10892
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);
10893
11387
  const gpuMaterialSource = meshes.length > 0 ? createWavefrontGpuMaterialSource(meshes) : createWavefrontGpuMaterialSource([]);
10894
11388
  const gpuMeshSource = meshes.length > 0 ? createWavefrontGpuMeshSource(meshes, gpuMaterialSource) : createWavefrontGpuMeshSource([]);
10895
- 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([]) }));
10896
11391
  const emissiveTriangleIndices = createWavefrontEmissiveTriangleIndexSource(
10897
11392
  meshes,
10898
11393
  options.emissiveTriangleCapacity
@@ -10970,6 +11465,7 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
10970
11465
  gpuAccelerationBuildRequired: accelerationBuildMode === "gpu" && triangleCount > 0,
10971
11466
  gpuMeshSource,
10972
11467
  gpuMaterialSource,
11468
+ instancing,
10973
11469
  meshAcceleration,
10974
11470
  emissiveTriangleIndices,
10975
11471
  emissiveTriangleCount: emissiveTriangleIndices.count,
@@ -11008,6 +11504,7 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
11008
11504
  tilePixelCapacity,
11009
11505
  maxDepth,
11010
11506
  sceneObjectCapacity,
11507
+ instanceBytes: instancing?.instanceBytes ?? 0,
11011
11508
  triangleCapacity,
11012
11509
  bvhNodeCapacity,
11013
11510
  bvhLeafSortCapacity,
@@ -11041,6 +11538,7 @@ function createWavefrontTraceBindGroup({
11041
11538
  counterBuffer,
11042
11539
  outputView,
11043
11540
  triangleBuffer,
11541
+ instanceView,
11044
11542
  bvhNodeBuffer,
11045
11543
  radianceView,
11046
11544
  environmentPortalBuffer,
@@ -11098,7 +11596,8 @@ function createWavefrontTraceBindGroup({
11098
11596
  { binding: 41, resource: extensionAtlasResources.specularColor.view },
11099
11597
  { binding: 42, resource: extensionAtlasResources.iridescence.view },
11100
11598
  { binding: 43, resource: extensionAtlasResources.iridescenceThickness.view },
11101
- { binding: 44, resource: extensionAtlasResources.anisotropy.view }
11599
+ { binding: 44, resource: extensionAtlasResources.anisotropy.view },
11600
+ ...instanceView ? [{ binding: 47, resource: instanceView }] : []
11102
11601
  ]
11103
11602
  });
11104
11603
  }
@@ -11196,115 +11695,39 @@ function createWavefrontDenoiseBindGroups({
11196
11695
  inputView: radianceView,
11197
11696
  targetView: denoiseScratchView,
11198
11697
  label: "plasius.wavefront.bind.denoise.radianceToScratch"
11199
- }),
11200
- resolve: createWavefrontDenoiseResolveBindGroup({
11201
- device,
11202
- denoiseResolveBindGroupLayout,
11203
- configBuffer,
11204
- inputView: denoiseScratchView,
11205
- targetView: outputView,
11206
- label: "plasius.wavefront.bind.denoise.scratchToOutput"
11207
- }),
11208
- directResolve: createWavefrontDenoiseResolveBindGroup({
11209
- device,
11210
- denoiseResolveBindGroupLayout,
11211
- configBuffer,
11212
- inputView: radianceView,
11213
- targetView: outputView,
11214
- label: "plasius.wavefront.bind.denoise.radianceToOutput"
11215
- })
11216
- });
11217
- }
11218
- function createWavefrontPresentBindGroup({
11219
- device,
11220
- presentBindGroupLayout,
11221
- outputView,
11222
- sampler
11223
- }) {
11224
- return device.createBindGroup({
11225
- label: "plasius.wavefront.presentBindGroup",
11226
- layout: presentBindGroupLayout,
11227
- entries: [
11228
- { binding: 0, resource: outputView },
11229
- { binding: 1, resource: sampler }
11230
- ]
11231
- });
11232
- }
11233
-
11234
- // src/wavefront-gpu-synchronization.js
11235
- async function waitForSubmittedGpuWork(device, options = {}) {
11236
- if (typeof device.queue.onSubmittedWorkDone !== "function") {
11237
- return true;
11238
- }
11239
- const timeoutMs = Math.max(
11240
- 1,
11241
- Number.isFinite(options.timeoutMs) ? Number(options.timeoutMs) : GPU_SUBMITTED_WORK_TIMEOUT_MS
11242
- );
11243
- const maxWaitMs = Math.max(
11244
- timeoutMs,
11245
- Number.isFinite(options.maxWaitMs) ? Number(options.maxWaitMs) : timeoutMs
11246
- );
11247
- const allowTimeout = options.allowTimeout !== false;
11248
- const completionPromise = device.queue.onSubmittedWorkDone().then(
11249
- () => ({ status: "done" }),
11250
- (error) => {
11251
- throw error;
11252
- }
11253
- );
11254
- const lossPromise = typeof device.lost?.then === "function" ? device.lost.then((info) => {
11255
- throw new Error(
11256
- `WebGPU device lost while waiting for submitted work (${info?.reason ?? "unknown"}).`
11257
- );
11258
- }) : null;
11259
- const startedAtMs = nowMs3();
11260
- while (true) {
11261
- const elapsedMs = Math.max(0, nowMs3() - startedAtMs);
11262
- const remainingMs = Math.max(0, maxWaitMs - elapsedMs);
11263
- if (remainingMs <= 0) {
11264
- if (!allowTimeout) {
11265
- throw new Error(`Timed out after ${Math.round(maxWaitMs)} ms waiting for submitted GPU work.`);
11266
- }
11267
- console.warn(
11268
- `[plasius.wavefront] Submitted GPU work did not report completion within ${Math.round(maxWaitMs)} ms; continuing.`
11269
- );
11270
- return false;
11271
- }
11272
- const waitWindowMs = Math.max(1, Math.min(timeoutMs, remainingMs));
11273
- let timeoutHandle = null;
11274
- let resolveTimeoutPromise = null;
11275
- let timeoutSettled = false;
11276
- const settleTimeoutPromise = (value) => {
11277
- if (timeoutSettled) {
11278
- return;
11279
- }
11280
- timeoutSettled = true;
11281
- resolveTimeoutPromise?.(value);
11282
- };
11283
- const timeoutPromise = new Promise((resolve) => {
11284
- resolveTimeoutPromise = resolve;
11285
- timeoutHandle = setTimeout(
11286
- () => settleTimeoutPromise({ status: "timeout" }),
11287
- waitWindowMs
11288
- );
11289
- });
11290
- let result;
11291
- try {
11292
- result = await Promise.race(
11293
- [completionPromise, timeoutPromise, lossPromise].filter(Boolean)
11294
- );
11295
- } finally {
11296
- if (timeoutHandle !== null) {
11297
- clearTimeout(timeoutHandle);
11298
- settleTimeoutPromise({ status: "cancelled" });
11299
- }
11300
- }
11301
- if (result?.status === "done") {
11302
- return true;
11303
- }
11304
- if (result?.status !== "timeout") {
11305
- return true;
11306
- }
11307
- }
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
+ });
11308
11731
  }
11309
11732
 
11310
11733
  // src/wavefront-frame-telemetry.js
@@ -12131,7 +12554,7 @@ function dispatchWavefrontFrameAwaitingGpu({
12131
12554
  }
12132
12555
 
12133
12556
  // src/wavefront-pipelines.js
12134
- function createWavefrontBindGroupLayouts(device, constants) {
12557
+ function createWavefrontBindGroupLayouts(device, constants, instanced = false) {
12135
12558
  const trace = device.createBindGroupLayout({
12136
12559
  label: "plasius.wavefront.traceBindGroupLayout",
12137
12560
  entries: [
@@ -12183,7 +12606,8 @@ function createWavefrontBindGroupLayouts(device, constants) {
12183
12606
  { binding: 41, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
12184
12607
  { binding: 42, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
12185
12608
  { binding: 43, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
12186
- { 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" } }] : []
12187
12611
  ]
12188
12612
  });
12189
12613
  const acceleration = device.createBindGroupLayout({
@@ -12378,12 +12802,12 @@ async function createWavefrontPresentPipeline(device, presentBindGroupLayout, fo
12378
12802
  primitive: { topology: "triangle-list" }
12379
12803
  });
12380
12804
  }
12381
- async function createWavefrontPipelineResources({ device, constants, format }) {
12382
- const bindGroupLayouts = createWavefrontBindGroupLayouts(device, constants);
12805
+ async function createWavefrontPipelineResources({ device, constants, format, instanced = false }) {
12806
+ const bindGroupLayouts = createWavefrontBindGroupLayouts(device, constants, instanced);
12383
12807
  const pipelineLayouts = createWavefrontPipelineLayouts(device, bindGroupLayouts);
12384
12808
  const computeShader = device.createShaderModule({
12385
12809
  label: "plasius.wavefront.computeShader",
12386
- code: WAVEFRONT_COMPUTE_WGSL
12810
+ code: createWavefrontPathTracingComputeShaderSource({ instanced })
12387
12811
  });
12388
12812
  await assertShaderModuleCompiles(computeShader, "plasius.wavefront.computeShader");
12389
12813
  return Object.freeze({
@@ -12816,890 +13240,911 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
12816
13240
  if (!adapter) {
12817
13241
  throw new Error("Unable to acquire a WebGPU adapter for wavefront path tracing.");
12818
13242
  }
12819
- const device = await adapter.requestDevice(
13243
+ const rawDevice = await adapter.requestDevice(
12820
13244
  createWavefrontDeviceDescriptor(adapter, options, initialConfig.memory)
12821
13245
  );
12822
- const gpuAdapterParallelism = createGpuAdapterParallelismDiagnostics(adapter, device);
12823
- const context = canvas.getContext("webgpu");
12824
- if (!context || typeof context.configure !== "function") {
12825
- throw new Error("Canvas WebGPU context does not support configure().");
12826
- }
12827
- const format = options.format ?? (typeof navigatorRef.gpu.getPreferredCanvasFormat === "function" ? navigatorRef.gpu.getPreferredCanvasFormat() : "bgra8unorm");
12828
- let config = initialConfig;
12829
- const safeTileSize = clampTileSizeForDevice(config, device);
12830
- if (safeTileSize !== config.tileSize) {
12831
- config = createWavefrontPathTracingComputeConfig({
12832
- ...options,
12833
- canvas,
12834
- width: config.width,
12835
- height: config.height,
12836
- tileSize: safeTileSize,
12837
- tilePixelCapacity: Math.min(
12838
- config.tilePixelCapacity,
12839
- safeTileSize * safeTileSize
12840
- )
12841
- });
12842
- }
12843
- config = Object.freeze({
12844
- ...config,
12845
- memory: estimateWavefrontPathTracingMemoryForConfig(config, device.limits)
12846
- });
12847
- canvas.width = config.width;
12848
- canvas.height = config.height;
12849
- context.configure({
12850
- device,
12851
- format,
12852
- alphaMode: options.alpha === true ? "premultiplied" : "opaque"
12853
- });
12854
- const bufferUsage = constants.buffer.STORAGE | constants.buffer.COPY_DST;
12855
- const memory = config.memory;
12856
- const activeQueue = createBuffer(device, bufferUsage, memory.queueBytes, "plasius.wavefront.activeQueue");
12857
- const nextQueue = createBuffer(device, bufferUsage, memory.queueBytes, "plasius.wavefront.nextQueue");
12858
- const hitBuffer = createBuffer(device, bufferUsage, memory.hitBytes, "plasius.wavefront.hitBuffer");
12859
- const accumulationBuffer = createBuffer(
12860
- device,
12861
- bufferUsage,
12862
- memory.accumulationBytes,
12863
- "plasius.wavefront.accumulation"
12864
- );
12865
- const pathVertexBuffer = createBuffer(
12866
- device,
12867
- bufferUsage,
12868
- memory.pathVertexBytes,
12869
- "plasius.wavefront.pathVertices"
12870
- );
12871
- const sceneObjectBuffer = createBuffer(
12872
- device,
12873
- constants.buffer.STORAGE | constants.buffer.COPY_DST,
12874
- memory.sceneObjectBytes,
12875
- "plasius.wavefront.sceneObjects"
12876
- );
12877
- const triangleBuffer = createBuffer(
12878
- device,
12879
- constants.buffer.STORAGE | constants.buffer.COPY_DST,
12880
- memory.triangleBytes,
12881
- "plasius.wavefront.triangles"
12882
- );
12883
- const bvhNodeBuffer = createBuffer(
12884
- device,
12885
- constants.buffer.STORAGE | constants.buffer.COPY_DST,
12886
- memory.bvhCombinedBytes,
12887
- "plasius.wavefront.bvhNodes"
12888
- );
12889
- const meshVertexBuffer = createBuffer(
12890
- device,
12891
- constants.buffer.STORAGE | constants.buffer.COPY_DST,
12892
- memory.meshVertexBytes,
12893
- "plasius.wavefront.meshVertices"
12894
- );
12895
- const meshIndexBuffer = createBuffer(
12896
- device,
12897
- constants.buffer.STORAGE | constants.buffer.COPY_DST,
12898
- memory.meshIndexBytes,
12899
- "plasius.wavefront.meshIndices"
12900
- );
12901
- const meshRangeBuffer = createBuffer(
12902
- device,
12903
- constants.buffer.STORAGE | constants.buffer.COPY_DST,
12904
- memory.meshRangeBytes,
12905
- "plasius.wavefront.meshRanges"
12906
- );
12907
- const environmentPortalBuffer = createBuffer(
12908
- device,
12909
- constants.buffer.STORAGE | constants.buffer.COPY_DST,
12910
- memory.environmentPortalBytes,
12911
- "plasius.wavefront.environmentPortals"
12912
- );
12913
- const bvhLeafRefBuffer = createBuffer(
12914
- device,
12915
- constants.buffer.STORAGE | constants.buffer.COPY_DST,
12916
- memory.bvhLeafReferenceBytes,
12917
- "plasius.wavefront.bvhLeafRefs"
12918
- );
12919
- const tiles = createTiles(config.width, config.height, config.tileSize);
12920
- const configBufferStride = memory.configBufferStride;
12921
- const outputConfigSlotCount = config.deferredPathResolve ? 0 : tiles.length;
12922
- const frameConfigSlotCount = Math.max(
12923
- 1,
12924
- tiles.length * config.samplesPerPixel + outputConfigSlotCount + (config.denoise || config.progressive ? 1 : 0)
12925
- );
12926
- const configBuffer = createBuffer(
12927
- device,
12928
- constants.buffer.UNIFORM | constants.buffer.COPY_DST,
12929
- memory.frameConfigBytes,
12930
- "plasius.wavefront.frameConfig"
12931
- );
12932
- const bvhBuildConfigBuffer = createBuffer(
12933
- device,
12934
- constants.buffer.UNIFORM | constants.buffer.COPY_DST,
12935
- memory.bvhBuildConfigBytes,
12936
- "plasius.wavefront.bvhBuildConfig"
12937
- );
12938
- const counterBuffer = createBuffer(
12939
- device,
12940
- constants.buffer.STORAGE | constants.buffer.COPY_DST | constants.buffer.COPY_SRC,
12941
- memory.counterBytes,
12942
- "plasius.wavefront.counters"
12943
- );
12944
- const activeDispatchBuffer = createBuffer(
12945
- device,
12946
- constants.buffer.INDIRECT | constants.buffer.COPY_DST,
12947
- memory.indirectDispatchBytes,
12948
- "plasius.wavefront.activeDispatchArgs"
12949
- );
12950
- let packedScene = packWavefrontSceneObjects(config.sceneObjects, config.sceneObjectCapacity);
12951
- device.queue.writeBuffer(sceneObjectBuffer, 0, packedScene.buffer);
12952
- const packedTriangles = packWavefrontTriangles(
12953
- config.meshAcceleration.triangles,
12954
- Math.max(1, config.triangleCapacity)
12955
- );
12956
- const packedBvhNodes = packWavefrontBvhNodes(
12957
- config.meshAcceleration.nodes,
12958
- Math.max(1, config.bvhNodeCapacity + config.emissiveTriangleCapacity)
12959
- );
12960
- const packedBvhNodeUints = new Uint32Array(packedBvhNodes.buffer);
12961
- const packedBvhNodeFloats = new Float32Array(packedBvhNodes.buffer);
12962
- const orderedEmissiveTriangleIndices = resolveOrderedEmissiveTriangleIndices(config);
12963
- const emissiveWeights = orderedEmissiveTriangleIndices.map(
12964
- (triangleIndex) => emissiveTriangleWeight(config.meshAcceleration.triangles[triangleIndex])
12965
- );
12966
- const totalEmissiveWeight = emissiveWeights.reduce((total, weight) => total + weight, 0);
12967
- let cumulativeEmissiveWeight = 0;
12968
- orderedEmissiveTriangleIndices.forEach((triangleIndex, index) => {
12969
- const nodeOffset = (config.bvhNodeCapacity + index) * (BVH_NODE_RECORD_BYTES / 4);
12970
- cumulativeEmissiveWeight += emissiveWeights[index] ?? 0;
12971
- packedBvhNodeFloats[nodeOffset] = cumulativeEmissiveWeight;
12972
- packedBvhNodeFloats[nodeOffset + 1] = emissiveWeights[index] ?? 0;
12973
- packedBvhNodeFloats[nodeOffset + 2] = totalEmissiveWeight;
12974
- packedBvhNodeUints[nodeOffset + 8] = triangleIndex;
12975
- });
12976
- device.queue.writeBuffer(triangleBuffer, 0, packedTriangles.buffer);
12977
- device.queue.writeBuffer(bvhNodeBuffer, 0, packedBvhNodes.buffer);
12978
- device.queue.writeBuffer(meshVertexBuffer, 0, config.gpuMeshSource.vertices.buffer);
12979
- device.queue.writeBuffer(meshIndexBuffer, 0, config.gpuMeshSource.indices.buffer);
12980
- device.queue.writeBuffer(meshRangeBuffer, 0, config.gpuMeshSource.meshes.buffer);
12981
- const packedEnvironmentPortals = packEnvironmentPortals(
12982
- config.environmentPortals,
12983
- Math.max(1, config.environmentPortalCapacity)
12984
- );
12985
- device.queue.writeBuffer(environmentPortalBuffer, 0, packedEnvironmentPortals.buffer);
12986
- const radianceTexture = device.createTexture({
12987
- label: "plasius.wavefront.radiance",
12988
- size: { width: config.width, height: config.height },
12989
- format: "rgba16float",
12990
- usage: constants.texture.STORAGE_BINDING | constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
12991
- });
12992
- const radianceView = radianceTexture.createView();
12993
- const denoiseScratchTexture = device.createTexture({
12994
- label: "plasius.wavefront.denoiseScratch",
12995
- size: { width: config.width, height: config.height },
12996
- format: "rgba16float",
12997
- usage: constants.texture.STORAGE_BINDING | constants.texture.TEXTURE_BINDING
12998
- });
12999
- const denoiseScratchView = denoiseScratchTexture.createView();
13000
- const outputTexture = device.createTexture({
13001
- label: "plasius.wavefront.output",
13002
- size: { width: config.width, height: config.height },
13003
- format: "rgba8unorm",
13004
- usage: constants.texture.STORAGE_BINDING | constants.texture.TEXTURE_BINDING | constants.texture.COPY_SRC
13005
- });
13006
- const outputView = outputTexture.createView();
13007
- const sampler = device.createSampler({
13008
- label: "plasius.wavefront.presentSampler",
13009
- magFilter: "nearest",
13010
- minFilter: "nearest"
13011
- });
13012
- const environmentMapResource = createEnvironmentMapResource(
13013
- device,
13014
- constants,
13015
- config.environmentMap,
13016
- config.environmentColor
13017
- );
13018
- const environmentSamplingResource = createEnvironmentSamplingTextureResource(
13019
- device,
13020
- constants,
13021
- config.environmentMap,
13022
- config.environmentColor
13023
- );
13024
- let mediumTextureResource = createMediumTextureResource(
13025
- device,
13026
- constants,
13027
- config.mediums
13028
- );
13029
- config = Object.freeze({
13030
- ...config,
13031
- environmentMap: Object.freeze({
13032
- ...config.environmentMap,
13033
- width: environmentMapResource.width,
13034
- height: environmentMapResource.height,
13035
- mipLevelCount: environmentMapResource.mipLevelCount,
13036
- hasImportanceData: environmentSamplingResource.hasImportanceData
13037
- })
13038
- });
13039
- const brdfLutResource = createBrdfLutResource(device, constants);
13040
- const baseColorAtlasResource = createAtlasTextureResource(
13041
- device,
13042
- constants,
13043
- config.gpuMaterialSource.baseColorAtlas,
13044
- "plasius.wavefront.materialAtlas.baseColor"
13045
- );
13046
- const metallicRoughnessAtlasResource = createAtlasTextureResource(
13047
- device,
13048
- constants,
13049
- config.gpuMaterialSource.metallicRoughnessAtlas,
13050
- "plasius.wavefront.materialAtlas.metallicRoughness"
13051
- );
13052
- const normalAtlasResource = createAtlasTextureResource(
13053
- device,
13054
- constants,
13055
- config.gpuMaterialSource.normalAtlas,
13056
- "plasius.wavefront.materialAtlas.normal"
13057
- );
13058
- const occlusionAtlasResource = createAtlasTextureResource(
13059
- device,
13060
- constants,
13061
- config.gpuMaterialSource.occlusionAtlas,
13062
- "plasius.wavefront.materialAtlas.occlusion"
13063
- );
13064
- const emissiveAtlasResource = createAtlasTextureResource(
13065
- device,
13066
- constants,
13067
- config.gpuMaterialSource.emissiveAtlas,
13068
- "plasius.wavefront.materialAtlas.emissive"
13069
- );
13070
- const extensionAtlasNames = [
13071
- "clearcoat",
13072
- "clearcoatRoughness",
13073
- "clearcoatNormal",
13074
- "transmission",
13075
- "thickness",
13076
- "sheenColor",
13077
- "sheenRoughness",
13078
- "specular",
13079
- "specularColor",
13080
- "iridescence",
13081
- "iridescenceThickness",
13082
- "anisotropy"
13083
- ];
13084
- const extensionAtlasResources = Object.freeze(
13085
- Object.fromEntries(
13086
- extensionAtlasNames.map((name) => [
13087
- name,
13088
- 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({
13089
13283
  device,
13090
13284
  constants,
13091
- config.gpuMaterialSource.extensionAtlases[name],
13092
- `plasius.wavefront.materialAtlas.${name}`
13093
- )
13094
- ])
13095
- )
13096
- );
13097
- const materialAtlasSampler = device.createSampler({
13098
- label: "plasius.wavefront.materialAtlasSampler",
13099
- addressModeU: "clamp-to-edge",
13100
- addressModeV: "clamp-to-edge",
13101
- magFilter: "linear",
13102
- minFilter: "linear"
13103
- });
13104
- const {
13105
- bindGroupLayouts: {
13106
- trace: traceBindGroupLayout,
13107
- acceleration: accelerationBindGroupLayout,
13108
- denoiseRadiance: denoiseRadianceBindGroupLayout,
13109
- denoiseResolve: denoiseResolveBindGroupLayout,
13110
- present: presentBindGroupLayout
13111
- },
13112
- computePipelines: pipelines,
13113
- presentPipeline
13114
- } = await createWavefrontPipelineResources({ device, constants, format });
13115
- function createTraceBindGroups() {
13116
- return createWavefrontTraceBindGroups({
13117
- device,
13118
- traceBindGroupLayout,
13119
- activeQueue,
13120
- nextQueue,
13121
- frameConfigBuffer: configBuffer,
13122
- hitBuffer,
13123
- accumulationBuffer,
13124
- sceneObjectBuffer,
13125
- counterBuffer,
13126
- outputView,
13127
- triangleBuffer,
13128
- bvhNodeBuffer,
13129
- radianceView,
13130
- environmentPortalBuffer,
13131
- environmentMapResource,
13132
- pathVertexBuffer,
13133
- baseColorAtlasResource,
13134
- metallicRoughnessAtlasResource,
13135
- normalAtlasResource,
13136
- occlusionAtlasResource,
13137
- emissiveAtlasResource,
13138
- materialAtlasSampler,
13139
- brdfLutResource,
13140
- environmentSamplingResource,
13141
- mediumTextureResource,
13142
- extensionAtlasResources
13143
- });
13144
- }
13145
- let bindGroups = createTraceBindGroups();
13146
- const bvhBuildBindGroup = createWavefrontBvhBuildBindGroup({
13147
- device,
13148
- accelerationBindGroupLayout,
13149
- bvhBuildConfigBuffer,
13150
- triangleBuffer,
13151
- bvhNodeBuffer,
13152
- meshVertexBuffer,
13153
- meshIndexBuffer,
13154
- meshRangeBuffer,
13155
- bvhLeafRefBuffer
13156
- });
13157
- const {
13158
- radiance: denoiseRadianceBindGroup,
13159
- resolve: denoiseResolveBindGroup,
13160
- directResolve: denoiseDirectResolveBindGroup
13161
- } = createWavefrontDenoiseBindGroups({
13162
- device,
13163
- denoiseRadianceBindGroupLayout,
13164
- denoiseResolveBindGroupLayout,
13165
- configBuffer,
13166
- radianceView,
13167
- denoiseScratchView,
13168
- outputView
13169
- });
13170
- const presentBindGroup = createWavefrontPresentBindGroup({
13171
- device,
13172
- presentBindGroupLayout,
13173
- outputView,
13174
- sampler
13175
- });
13176
- const progressive = await createWavefrontProgressiveResources({ device, constants, config, configBuffer, radianceTexture, radianceView, outputView });
13177
- let activeFrameTelemetry = null;
13178
- let frameTelemetryResources = null;
13179
- const frameEncoder = createWavefrontFrameEncoder({
13180
- getConfig: () => config,
13181
- progressive,
13182
- getBindGroups: () => bindGroups,
13183
- pipelines,
13184
- counterBuffer,
13185
- activeDispatchBuffer,
13186
- denoiseRadianceBindGroup,
13187
- denoiseResolveBindGroup,
13188
- denoiseDirectResolveBindGroup,
13189
- presentPipeline,
13190
- presentBindGroup,
13191
- context,
13192
- getFrameTelemetry: () => activeFrameTelemetry
13193
- });
13194
- let frame = 0;
13195
- let accelerationBuilt = !config.gpuAccelerationBuildRequired;
13196
- let accelerationBuildCount = 0;
13197
- let activeCameraOptions = options.camera ?? DEFAULT_CAMERA2;
13198
- let lastCompletedFrameTimeMs = null;
13199
- let lastCompletedSamplesPerPixel = Math.max(1, config.samplesPerPixel);
13200
- let lastGpuParallelism = createGpuParallelismDiagnostics(
13201
- gpuAdapterParallelism,
13202
- createGpuParallelismCounters()
13203
- );
13204
- const waitForSubmittedGpuWork2 = (waitOptions = {}) => waitForSubmittedGpuWork(device, waitOptions);
13205
- function ensureFrameTelemetryResources() {
13206
- if (!frameTelemetryResources) {
13207
- 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,
13208
13349
  device,
13209
- constants,
13210
- 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
+ )
13211
13577
  });
13212
13578
  }
13213
- return frameTelemetryResources;
13214
- }
13215
- function resolveRenderedSamplesPerPixel(renderOptions = {}, awaitGPUCompletion = true) {
13216
- return resolveWavefrontRenderedSamplesPerPixel({
13217
- config,
13218
- renderOptions,
13219
- awaitGPUCompletion,
13220
- lastCompletedFrameTimeMs,
13221
- 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;
13222
13725
  });
13223
- }
13224
- function createFrameStats({
13225
- frameIndex,
13226
- accelerationBuildSubmitted,
13227
- frameSubmissionCount,
13228
- parallelismCounters,
13229
- renderedSamplesPerPixel,
13230
- targetSamplesPerPixel,
13231
- frameTimeBudgetMs,
13232
- budgetConstrained
13233
- }) {
13234
- const stats = createWavefrontFrameStats({
13235
- config,
13236
- tiles,
13237
- frame,
13238
- frameIndex,
13239
- accelerationBuilt,
13240
- accelerationBuildCount,
13241
- accelerationBuildSubmitted,
13242
- frameSubmissionCount,
13243
- frameConfigSlotCount,
13244
- gpuAdapterParallelism,
13245
- parallelismCounters,
13246
- renderedSamplesPerPixel,
13247
- targetSamplesPerPixel,
13248
- frameTimeBudgetMs,
13249
- 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
13250
13741
  });
13251
- lastGpuParallelism = stats.gpuParallelism;
13252
- return stats.frameStats;
13253
- }
13254
- async function readTerminationMetrics() {
13255
- 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(
13256
13763
  device,
13257
13764
  constants,
13258
- counterBuffer,
13259
- waitForSubmittedGpuWork: waitForSubmittedGpuWork2
13260
- });
13261
- }
13262
- function writeFrameConfigSlot(slot, tile, frameIndex, buildRange = {}) {
13263
- if (slot >= frameConfigSlotCount) {
13264
- throw new Error("Wavefront frame config slot capacity exceeded.");
13265
- }
13266
- const offset = slot * configBufferStride;
13267
- device.queue.writeBuffer(
13268
- configBuffer,
13269
- offset,
13270
- createConfigPayload(config, tile, frameIndex, buildRange)
13765
+ config.environmentMap,
13766
+ config.environmentColor
13271
13767
  );
13272
- return offset;
13273
- }
13274
- function createFrameConfigWriter(frameIndex) {
13275
- let slot = 0;
13276
- return (tile, buildRange = {}) => {
13277
- const offset = writeFrameConfigSlot(slot, tile, frameIndex, buildRange);
13278
- slot += 1;
13279
- return offset;
13280
- };
13281
- }
13282
- function dispatchGpuAccelerationBuild(frameIndex, parallelism) {
13283
- const submitted = dispatchWavefrontGpuAccelerationBuild({
13284
- config,
13285
- accelerationBuilt,
13286
- tiles,
13768
+ const environmentSamplingResource = createEnvironmentSamplingTextureResource(
13287
13769
  device,
13288
- bvhBuildConfigBuffer,
13289
- configBufferStride,
13290
- bvhBuildBindGroup,
13291
- pipelines,
13292
- parallelism,
13293
- frameIndex
13294
- });
13295
- if (submitted) {
13296
- accelerationBuilt = true;
13297
- accelerationBuildCount += 1;
13298
- }
13299
- return submitted;
13300
- }
13301
- function dispatchFrame(frameIndex, parallelism, renderedSamplesPerPixel = config.samplesPerPixel) {
13302
- return dispatchWavefrontFrame({
13303
- config,
13304
- tiles,
13770
+ constants,
13771
+ config.environmentMap,
13772
+ config.environmentColor
13773
+ );
13774
+ let mediumTextureResource = createMediumTextureResource(
13305
13775
  device,
13306
- frameIndex,
13307
- parallelism,
13308
- renderedSamplesPerPixel,
13309
- frameEncoder,
13310
- createFrameConfigWriter
13311
- });
13312
- }
13313
- function renderOnce(renderOptions = {}, resolvedSamplingPlan = null) {
13314
- const frameStartTimeMs = nowMs3();
13315
- frame += 1;
13316
- const frameIndex = frame + config.frameIndex;
13317
- const samplingPlan = resolvedSamplingPlan ?? resolveRenderedSamplesPerPixel(renderOptions, false);
13318
- const parallelismCounters = createGpuParallelismCounters();
13319
- const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex, parallelismCounters);
13320
- const frameSubmissionCount = dispatchFrame(
13321
- frameIndex,
13322
- parallelismCounters,
13323
- samplingPlan.renderedSamplesPerPixel
13776
+ constants,
13777
+ config.mediums
13324
13778
  );
13325
- const frameTimeMs = Math.max(0, nowMs3() - frameStartTimeMs);
13326
- return Object.freeze({
13327
- ...createFrameStats({
13328
- frameIndex,
13329
- accelerationBuildSubmitted,
13330
- frameSubmissionCount,
13331
- parallelismCounters,
13332
- renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel,
13333
- targetSamplesPerPixel: samplingPlan.targetSamplesPerPixel,
13334
- frameTimeBudgetMs: samplingPlan.frameTimeBudgetMs,
13335
- budgetConstrained: samplingPlan.budgetConstrained
13336
- }),
13337
- gpuWorkerJobs: createGpuWorkerJobDiagnostics(
13338
- lastGpuParallelism,
13339
- frameSubmissionCount + (accelerationBuildSubmitted ? 1 : 0),
13340
- frameTimeMs,
13341
- false
13342
- ),
13343
- timings: createWavefrontFrameTimingTelemetry({
13344
- status: "not-requested",
13345
- source: "cpu-submit",
13346
- timestampQueryStatus: "not-recorded",
13347
- 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
13348
13787
  })
13349
13788
  });
13350
- }
13351
- function dispatchFrameAwaitingGpu(frameIndex, parallelism, renderedSamplesPerPixel = config.samplesPerPixel, optionsForFrame = {}) {
13352
- return dispatchWavefrontFrameAwaitingGpu({
13353
- config,
13354
- tiles,
13789
+ const brdfLutResource = createBrdfLutResource(device, constants);
13790
+ const baseColorAtlasResource = createAtlasTextureResource(
13355
13791
  device,
13356
- frameIndex,
13357
- parallelism,
13358
- renderedSamplesPerPixel,
13359
- frameEncoder,
13360
- writeFrameConfigSlot,
13361
- optionsForFrame
13362
- });
13363
- }
13364
- async function readOutputProbe(optionsForProbe = {}) {
13365
- return readWavefrontOutputProbe({
13792
+ constants,
13793
+ config.gpuMaterialSource.baseColorAtlas,
13794
+ "plasius.wavefront.materialAtlas.baseColor"
13795
+ );
13796
+ const metallicRoughnessAtlasResource = createAtlasTextureResource(
13366
13797
  device,
13367
13798
  constants,
13368
- config,
13369
- outputTexture,
13370
- waitForSubmittedGpuWork: waitForSubmittedGpuWork2,
13371
- 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"
13372
13853
  });
13373
- }
13374
- async function renderFrame(renderOptions = {}) {
13375
- const awaitGPUCompletion = renderOptions.awaitGPUCompletion !== false;
13376
- const samplingPlan = resolveRenderedSamplesPerPixel(renderOptions, awaitGPUCompletion);
13377
- const useThrottledHighSamplePath = awaitGPUCompletion && samplingPlan.renderedSamplesPerPixel >= 8;
13378
- const telemetryRequested = renderOptions.readStats === true;
13379
- const telemetryCanRecord = telemetryRequested && awaitGPUCompletion;
13380
- const telemetryResourcesForFrame = telemetryCanRecord ? ensureFrameTelemetryResources() : null;
13381
- if (telemetryResourcesForFrame?.available) {
13382
- telemetryResourcesForFrame.beginFrame();
13383
- activeFrameTelemetry = telemetryResourcesForFrame;
13384
- }
13385
- const frameStartTimeMs = nowMs3();
13386
- let frameStats;
13387
- try {
13388
- if (useThrottledHighSamplePath) {
13389
- frame += 1;
13390
- const frameIndex = frame + config.frameIndex;
13391
- const parallelismCounters = createGpuParallelismCounters();
13392
- const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex, parallelismCounters);
13393
- let frameSubmissionCount = 0;
13394
- let frameConfigSlot = 0;
13395
- if (accelerationBuildSubmitted) {
13396
- const accelerationWaitOptions = {
13397
- ...estimateSubmittedGpuWorkTiming(
13398
- { ...config, renderedSamplesPerPixel: 1 },
13399
- 1,
13400
- renderOptions.submittedWorkTimeoutMs,
13401
- { includeAccelerationBuild: true }
13402
- ),
13403
- allowTimeout: false
13404
- };
13405
- await waitForSubmittedGpuWork2(accelerationWaitOptions);
13406
- }
13407
- for (let tileIndex = 0; tileIndex < tiles.length; tileIndex += 1) {
13408
- const tileRangeDispatch = dispatchFrameAwaitingGpu(
13409
- frameIndex,
13410
- parallelismCounters,
13411
- samplingPlan.renderedSamplesPerPixel,
13412
- {
13413
- sampleRangeStart: 0,
13414
- sampleRangeEnd: samplingPlan.renderedSamplesPerPixel,
13415
- tileStartIndex: tileIndex,
13416
- tileEndIndex: tileIndex + 1,
13417
- startingSubmissionCount: frameSubmissionCount,
13418
- startingSlot: frameConfigSlot,
13419
- includeDenoise: tileIndex + 1 >= tiles.length,
13420
- includePresent: tileIndex + 1 >= tiles.length
13421
- }
13422
- );
13423
- frameSubmissionCount = tileRangeDispatch.submissionCount;
13424
- frameConfigSlot = tileRangeDispatch.slot;
13425
- const tileWaitOptions = {
13426
- ...estimateSubmittedGpuWorkTiming(
13427
- { ...config, renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel },
13428
- 1,
13429
- 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,
13430
13981
  {
13431
- 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,
13432
13989
  includePresent: tileIndex + 1 >= tiles.length
13433
13990
  }
13434
- ),
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,
13435
14028
  allowTimeout: false
14029
+ } : {
14030
+ timeoutMs: submittedWorkTiming.timeoutMs,
14031
+ maxWaitMs: submittedWorkTiming.maxWaitMs
13436
14032
  };
13437
- await waitForSubmittedGpuWork2(tileWaitOptions);
13438
- }
13439
- frameStats = createFrameStats({
13440
- frameIndex,
13441
- accelerationBuildSubmitted,
13442
- frameSubmissionCount,
13443
- parallelismCounters,
13444
- renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel,
13445
- targetSamplesPerPixel: samplingPlan.targetSamplesPerPixel,
13446
- frameTimeBudgetMs: samplingPlan.frameTimeBudgetMs,
13447
- budgetConstrained: samplingPlan.budgetConstrained
13448
- });
13449
- } else {
13450
- const submittedWorkTiming = estimateSubmittedGpuWorkTiming(
13451
- { ...config, renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel },
13452
- tiles.length,
13453
- renderOptions.submittedWorkTimeoutMs,
13454
- { includeAccelerationBuild: config.gpuAccelerationBuildRequired && !accelerationBuilt }
13455
- );
13456
- const submissionWaitOptions = awaitGPUCompletion ? {
13457
- timeoutMs: submittedWorkTiming.timeoutMs,
13458
- maxWaitMs: submittedWorkTiming.maxWaitMs,
13459
- allowTimeout: false
13460
- } : {
13461
- timeoutMs: submittedWorkTiming.timeoutMs,
13462
- maxWaitMs: submittedWorkTiming.maxWaitMs
13463
- };
13464
- frameStats = renderOnce(renderOptions, samplingPlan);
13465
- if (awaitGPUCompletion) {
13466
- await waitForSubmittedGpuWork2(submissionWaitOptions);
14033
+ frameStats = renderOnce(renderOptions, samplingPlan);
14034
+ if (awaitGPUCompletion) {
14035
+ await waitForSubmittedGpuWork2(submissionWaitOptions);
14036
+ }
13467
14037
  }
14038
+ } finally {
14039
+ activeFrameTelemetry = null;
13468
14040
  }
13469
- } finally {
13470
- activeFrameTelemetry = null;
13471
- }
13472
- const frameTimeMs = Math.max(0, nowMs3() - frameStartTimeMs);
13473
- if (awaitGPUCompletion) {
13474
- lastCompletedFrameTimeMs = frameTimeMs;
13475
- lastCompletedSamplesPerPixel = frameStats.renderedSamplesPerPixel ?? frameStats.samplesPerPixel;
13476
- }
13477
- const expectedPrimaryRays = config.width * config.height * samplingPlan.renderedSamplesPerPixel;
13478
- const expectedRayCounts = tiles.length * samplingPlan.renderedSamplesPerPixel * config.maxDepth;
13479
- const telemetryReadback = telemetryCanRecord ? await telemetryResourcesForFrame.readFrame({
13480
- expectedPrimaryRays,
13481
- expectedRayCounts,
13482
- waitForSubmittedGpuWork: waitForSubmittedGpuWork2
13483
- }) : null;
13484
- const rayCounts = !telemetryRequested ? createWavefrontRayCountTelemetry() : !awaitGPUCompletion ? createWavefrontRayCountTelemetry({
13485
- status: "unavailable",
13486
- expectedPrimaryRays,
13487
- expectedRayCounts,
13488
- reason: "gpu-work-not-awaited"
13489
- }) : telemetryReadback.rayCounts;
13490
- const timings = !telemetryRequested ? createWavefrontFrameTimingTelemetry({
13491
- status: "not-requested",
13492
- source: awaitGPUCompletion ? "queue-completion" : "cpu-submit",
13493
- timestampQueryStatus: "not-recorded",
13494
- totalRenderJobTimeMs: frameTimeMs
13495
- }) : !awaitGPUCompletion ? createWavefrontFrameTimingTelemetry({
13496
- status: "unavailable",
13497
- source: "cpu-submit",
13498
- timestampQueryStatus: "not-recorded",
13499
- totalRenderJobTimeMs: frameTimeMs,
13500
- reason: "gpu-work-not-awaited"
13501
- }) : telemetryReadback.timestampQueryStatus === "available" ? createWavefrontFrameTimingTelemetry({
13502
- status: "available",
13503
- source: "timestamp-query",
13504
- timestampQueryStatus: "available",
13505
- totalGpuTimeMs: telemetryReadback.totalGpuTimeMs,
13506
- totalRenderJobTimeMs: frameTimeMs
13507
- }) : createWavefrontFrameTimingTelemetry({
13508
- status: "fallback",
13509
- source: "queue-completion",
13510
- timestampQueryStatus: telemetryReadback.timestampQueryStatus,
13511
- totalRenderJobTimeMs: frameTimeMs,
13512
- reason: telemetryReadback.reason
13513
- });
13514
- const deviceLossStatus = awaitGPUCompletion ? "not-detected" : typeof device.lost?.then === "function" ? "pending" : "not-exposed";
13515
- frameStats = Object.freeze({
13516
- ...frameStats,
13517
- deviceLossStatus,
13518
- secondaryRays: rayCounts.status === "available" ? rayCounts.secondaryRays : null,
13519
- totalPathSegments: rayCounts.status === "available" ? rayCounts.totalPathSegments : null,
13520
- rayCounts,
13521
- timings,
13522
- telemetryMemoryBytes: telemetryResourcesForFrame?.memoryBytes ?? 0,
13523
- gpuWorkerJobs: createGpuWorkerJobDiagnostics(
13524
- frameStats.gpuParallelism,
13525
- frameStats.commandSubmissions,
13526
- frameTimeMs,
13527
- awaitGPUCompletion
13528
- )
13529
- });
13530
- const terminationMetrics = awaitGPUCompletion && renderOptions.readStats === true ? await readTerminationMetrics() : EMPTY_TERMINATION_METRICS;
13531
- const probe = renderOptions.readOutputProbe === false ? null : await readOutputProbe(renderOptions.probe);
13532
- const maxChannel = probe ? Math.max(...probe.rgba.slice(0, 3)) : 0;
13533
- const completedFrame = Object.freeze({
13534
- ...frameStats,
13535
- outputProbe: probe ? Object.freeze({
13536
- ...probe,
13537
- sampledPixels: 1,
13538
- nonZeroSamples: maxChannel > 0 ? 1 : 0,
13539
- maxChannel
13540
- }) : null,
13541
- bounces: [],
13542
- termination: terminationMetrics.termination,
13543
- terminalRadiance: terminationMetrics.terminalRadiance,
13544
- radianceDiagnostics: terminationMetrics.radianceDiagnostics,
13545
- transportContributions: terminationMetrics.transportContributions,
13546
- queueOverflow: terminationMetrics.queueOverflow
13547
- });
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();
13548
14123
  return Object.freeze({
13549
- ...completedFrame,
13550
- transportGuardrails: createWavefrontTransportGuardrailSummary(completedFrame)
13551
- });
13552
- }
13553
- function rebuildLiveConfig(overrides = {}) {
13554
- return createWavefrontPathTracingComputeConfig({
13555
- ...options,
13556
14124
  canvas,
13557
- width: config.width,
13558
- height: config.height,
13559
- maxDepth: config.maxDepth,
13560
- tileSize: config.tileSize,
13561
- samplesPerPixel: config.samplesPerPixel,
13562
- sceneObjectCapacity: config.sceneObjectCapacity,
13563
- sceneObjects: packedScene.objects,
13564
- camera: activeCameraOptions,
13565
- environmentMap: {
13566
- ...config.environmentMap
13567
- },
13568
- strictPhysicalLowSppLighting: config.strictPhysicalLowSppLighting,
13569
- featureFlags: options.featureFlags,
13570
- frameIndex: config.frameIndex,
13571
- ...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
13572
14137
  });
13573
- }
13574
- function rebuildMediumResources(nextConfig) {
13575
- const previousMediumTextureResource = mediumTextureResource;
13576
- mediumTextureResource = createMediumTextureResource(device, constants, nextConfig.mediums);
13577
- bindGroups = createTraceBindGroups();
13578
- if (previousMediumTextureResource?.ownsTexture) {
13579
- previousMediumTextureResource.texture?.destroy?.();
13580
- }
13581
- }
13582
- function updateSceneObjects(sceneObjects) {
13583
- progressive?.reset();
13584
- const nextPackedScene = packWavefrontSceneObjects(sceneObjects, config.sceneObjectCapacity);
13585
- packedScene = nextPackedScene;
13586
- const nextConfig = rebuildLiveConfig();
13587
- if (!mediumTablesEqual(config.mediums, nextConfig.mediums)) {
13588
- rebuildMediumResources(nextConfig);
14138
+ } catch (error) {
14139
+ try {
14140
+ await finishAllocationScopes?.();
14141
+ } catch {
13589
14142
  }
13590
- config = nextConfig;
13591
- device.queue.writeBuffer(sceneObjectBuffer, 0, packedScene.buffer);
13592
- return config;
13593
- }
13594
- function updateCamera(cameraOptions = {}) {
13595
- progressive?.reset();
13596
- activeCameraOptions = cameraOptions;
13597
- config = updateWavefrontCameraConfig(config, cameraOptions);
13598
- return config;
13599
- }
13600
- function getSnapshot() {
13601
- return Object.freeze({
13602
- frame,
13603
- accumulatedSamplesPerPixel: progressive?.samples ?? 0,
13604
- width: config.width,
13605
- height: config.height,
13606
- maxDepth: config.maxDepth,
13607
- tiles: tiles.length,
13608
- tileSize: config.tileSize,
13609
- samplesPerPixel: config.samplesPerPixel,
13610
- maxFramePassesPerSubmission: config.maxFramePassesPerSubmission,
13611
- sceneObjectCount: config.sceneObjectCount,
13612
- triangleCount: config.triangleCount,
13613
- emissiveTriangleCount: config.emissiveTriangleCount,
13614
- environmentPortalCount: config.environmentPortalCount,
13615
- environmentPortalMode: config.environmentPortalMode,
13616
- mediumCount: config.mediumCount,
13617
- environmentMap: createEnvironmentMapSnapshot(config.environmentMap),
13618
- deferredPathResolve: config.deferredPathResolve,
13619
- strictPhysicalLowSppLighting: config.strictPhysicalLowSppLighting,
13620
- transportExperiments: config.transportExperiments,
13621
- transportExperimentFlags: config.transportExperimentFlags,
13622
- bvhNodeCount: config.bvhNodeCount,
13623
- displayQuality: config.displayQuality,
13624
- accelerationBuildMode: config.accelerationBuildMode,
13625
- gpuAccelerationBuildRequired: config.gpuAccelerationBuildRequired,
13626
- accelerationBuilt,
13627
- accelerationBuildCount,
13628
- frameConfigSlots: frameConfigSlotCount,
13629
- gpuParallelism: lastGpuParallelism,
13630
- memory: config.memory
13631
- });
13632
- }
13633
- function destroy() {
13634
- progressive?.destroy();
13635
- frameTelemetryResources?.destroy?.();
13636
- activeQueue.destroy?.();
13637
- nextQueue.destroy?.();
13638
- hitBuffer.destroy?.();
13639
- accumulationBuffer.destroy?.();
13640
- pathVertexBuffer.destroy?.();
13641
- sceneObjectBuffer.destroy?.();
13642
- triangleBuffer.destroy?.();
13643
- bvhNodeBuffer.destroy?.();
13644
- meshVertexBuffer.destroy?.();
13645
- meshIndexBuffer.destroy?.();
13646
- meshRangeBuffer.destroy?.();
13647
- environmentPortalBuffer.destroy?.();
13648
- bvhLeafRefBuffer.destroy?.();
13649
- configBuffer.destroy?.();
13650
- bvhBuildConfigBuffer.destroy?.();
13651
- counterBuffer.destroy?.();
13652
- activeDispatchBuffer.destroy?.();
13653
- radianceTexture.destroy?.();
13654
- denoiseScratchTexture.destroy?.();
13655
- outputTexture.destroy?.();
13656
- if (environmentMapResource.ownsTexture) {
13657
- environmentMapResource.texture?.destroy?.();
13658
- }
13659
- if (environmentSamplingResource.ownsTexture) {
13660
- environmentSamplingResource.texture?.destroy?.();
13661
- }
13662
- if (mediumTextureResource.ownsTexture) {
13663
- mediumTextureResource.texture?.destroy?.();
13664
- }
13665
- brdfLutResource.texture?.destroy?.();
13666
- if (baseColorAtlasResource.ownsTexture) {
13667
- baseColorAtlasResource.texture?.destroy?.();
13668
- }
13669
- if (metallicRoughnessAtlasResource.ownsTexture) {
13670
- metallicRoughnessAtlasResource.texture?.destroy?.();
13671
- }
13672
- if (normalAtlasResource.ownsTexture) {
13673
- normalAtlasResource.texture?.destroy?.();
13674
- }
13675
- if (occlusionAtlasResource.ownsTexture) {
13676
- occlusionAtlasResource.texture?.destroy?.();
13677
- }
13678
- if (emissiveAtlasResource.ownsTexture) {
13679
- emissiveAtlasResource.texture?.destroy?.();
13680
- }
13681
- Object.values(extensionAtlasResources).forEach((resource) => {
13682
- if (resource.ownsTexture) {
13683
- resource.texture?.destroy?.();
13684
- }
13685
- });
13686
- context.unconfigure?.();
14143
+ context?.unconfigure?.();
14144
+ allocationTracker.destroy();
14145
+ rawDevice.destroy?.();
14146
+ throw error;
13687
14147
  }
13688
- return Object.freeze({
13689
- canvas,
13690
- context,
13691
- device,
13692
- format,
13693
- config,
13694
- renderOnce,
13695
- renderFrame,
13696
- readOutputProbe,
13697
- updateSceneObjects,
13698
- updateCamera,
13699
- resetAccumulation: () => progressive?.reset(),
13700
- getSnapshot,
13701
- destroy
13702
- });
13703
14148
  }
13704
14149
  async function renderWavefrontPathTracingComputeFrame(options = {}) {
13705
14150
  const renderer = await createWavefrontPathTracingComputeRenderer(options);