@plasius/gpu-renderer 0.2.55 → 0.2.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1558,14 +1558,6 @@ function textureSample(binding, textures, label, extra = {}) {
1558
1558
  }
1559
1559
  function cloneMaterialExtensions(material, textures) {
1560
1560
  const extensions = { ...material.extensions ?? {} };
1561
- if (material.workflow === "unlit") {
1562
- const unlitKey = "KHR_materials_unlit";
1563
- const unlitValue = extensions[unlitKey] ?? extensions.unlit ?? {};
1564
- if (typeof unlitValue !== "object" || unlitValue === null || Array.isArray(unlitValue)) {
1565
- throw new TypeError(`Canonical material extension ${unlitKey} must be an object.`);
1566
- }
1567
- extensions[unlitKey] = { ...unlitValue, enabled: true };
1568
- }
1569
1561
  for (const [bindingName, extensionName, textureKey] of EXTENSION_TEXTURE_BINDINGS) {
1570
1562
  const binding = material.textures?.[bindingName];
1571
1563
  if (!binding) {
@@ -1590,7 +1582,10 @@ function mapMaterial(material, materialRefId, textures) {
1590
1582
  if (material.alphaMode === "mask") {
1591
1583
  throw new Error("Canonical alphaMode mask is unsupported by WavefrontMeshInput.");
1592
1584
  }
1593
- if (material.workflow !== "metallic-roughness" && material.workflow !== "unlit") {
1585
+ if (material.workflow === "unlit" || Object.hasOwn(material.extensions ?? {}, "KHR_materials_unlit") || Object.hasOwn(material.extensions ?? {}, "unlit")) {
1586
+ throw new Error("Canonical unlit materials are unsupported by WavefrontMeshInput; retain an unlit-capable rendering path.");
1587
+ }
1588
+ if (material.workflow !== "metallic-roughness") {
1594
1589
  throw new Error(`Unsupported canonical material workflow: ${material.workflow}.`);
1595
1590
  }
1596
1591
  const mapped = {
@@ -3971,948 +3966,1289 @@ function boundsCentroid(bounds) {
3971
3966
  ];
3972
3967
  }
3973
3968
 
3974
- // src/wavefront-sampling.js
3975
- function buildOrthonormalBasis(normal) {
3976
- const tangentFallback = Math.abs(normal[1]) < 0.999 ? [0, 1, 0] : [1, 0, 0];
3977
- const tangent = normalize(cross2(tangentFallback, normal), [1, 0, 0]);
3978
- const bitangent = normalize(cross2(normal, tangent), [0, 0, 1]);
3979
- return { tangent, bitangent };
3980
- }
3981
- function localToWorld(local, normal) {
3982
- const basis = buildOrthonormalBasis(normal);
3983
- return normalize(
3984
- add(
3985
- add(scale(basis.tangent, local[0]), scale(basis.bitangent, local[1])),
3986
- scale(normal, local[2])
3987
- ),
3988
- normal
3989
- );
3990
- }
3991
- function radicalInverseVdc(bits) {
3992
- let value = bits >>> 0;
3993
- value = (value << 16 | value >>> 16) >>> 0;
3994
- value = ((value & 1431655765) << 1 | (value & 2863311530) >>> 1) >>> 0;
3995
- value = ((value & 858993459) << 2 | (value & 3435973836) >>> 2) >>> 0;
3996
- value = ((value & 252645135) << 4 | (value & 4042322160) >>> 4) >>> 0;
3997
- value = ((value & 16711935) << 8 | (value & 4278255360) >>> 8) >>> 0;
3998
- return value * 23283064365386963e-26;
3999
- }
4000
- function hammersley(index, count) {
4001
- return [index / Math.max(count, 1), radicalInverseVdc(index)];
4002
- }
4003
- function importanceSampleGgx(sample, roughness, normal) {
4004
- const alpha = Math.max(roughness * roughness, 1e-4);
4005
- const phi = 2 * Math.PI * sample[0];
4006
- const cosTheta = Math.sqrt((1 - sample[1]) / (1 + (alpha * alpha - 1) * sample[1]));
4007
- const sinTheta = Math.sqrt(Math.max(0, 1 - cosTheta * cosTheta));
4008
- const halfVector = localToWorld(
4009
- [Math.cos(phi) * sinTheta, Math.sin(phi) * sinTheta, cosTheta],
4010
- normal
4011
- );
4012
- return normalize(halfVector, normal);
4013
- }
4014
- function geometrySchlickGgx(nDotV, roughness) {
4015
- const k = (roughness + 1) * (roughness + 1) / 8;
4016
- return nDotV / Math.max(nDotV * (1 - k) + k, 1e-6);
4017
- }
4018
- function geometrySmith(nDotV, nDotL, roughness) {
4019
- return geometrySchlickGgx(nDotV, roughness) * geometrySchlickGgx(nDotL, roughness);
3969
+ // src/wavefront-runtime-support.js
3970
+ var WEBGPU_DEFAULT_MAX_STORAGE_BUFFER_BINDING_SIZE = 128 * 1024 * 1024;
3971
+ var WEBGPU_DEFAULT_MAX_BUFFER_SIZE = 256 * 1024 * 1024;
3972
+ function readRequestedDeviceLimit(source, name) {
3973
+ const rawValue = source?.[name];
3974
+ if (rawValue === void 0 || rawValue === null) {
3975
+ return 0;
3976
+ }
3977
+ const value = Number(rawValue);
3978
+ if (!Number.isSafeInteger(value) || value < 0) {
3979
+ throw new Error(`${name} must be a non-negative safe integer.`);
3980
+ }
3981
+ return value;
4020
3982
  }
4021
- function integrateBrdfSample(nDotV, roughness, sampleCount) {
4022
- const viewDirection = [Math.sqrt(Math.max(0, 1 - nDotV * nDotV)), 0, nDotV];
4023
- const normal = [0, 0, 1];
4024
- let scaleTerm = 0;
4025
- let biasTerm = 0;
4026
- for (let index = 0; index < sampleCount; index += 1) {
4027
- const xi = hammersley(index, sampleCount);
4028
- const halfVector = importanceSampleGgx(xi, roughness, normal);
4029
- const vDotH = Math.max(dot(viewDirection, halfVector), 0);
4030
- const lightDirection = normalize(
4031
- subtract(scale(halfVector, 2 * vDotH), viewDirection),
4032
- normal
3983
+ function assertAdapterSupportsRequiredLimit(adapter, name, requiredValue) {
3984
+ const availableValue = Number(adapter?.limits?.[name]);
3985
+ if (!Number.isFinite(availableValue)) {
3986
+ throw new Error(
3987
+ `Wavefront scene buffers require ${name}>=${requiredValue} bytes, but this adapter does not expose that limit.`
3988
+ );
3989
+ }
3990
+ if (availableValue < requiredValue) {
3991
+ throw new Error(
3992
+ `Wavefront scene buffers require ${name}>=${requiredValue} bytes, but this adapter exposes ${availableValue} bytes.`
4033
3993
  );
4034
- const nDotL = Math.max(lightDirection[2], 0);
4035
- const nDotH = Math.max(halfVector[2], 0);
4036
- if (nDotL <= 0 || nDotH <= 0 || vDotH <= 0) {
4037
- continue;
4038
- }
4039
- const geometry = geometrySmith(nDotV, nDotL, roughness);
4040
- const visibility = geometry * vDotH / Math.max(nDotH * nDotV, 1e-6);
4041
- const fresnel2 = (1 - vDotH) ** 5;
4042
- scaleTerm += (1 - fresnel2) * visibility;
4043
- biasTerm += fresnel2 * visibility;
4044
3994
  }
4045
- return [scaleTerm / sampleCount, biasTerm / sampleCount];
4046
3995
  }
4047
-
4048
- // src/wavefront-gpu-resources.js
4049
- function clampTileSizeForDevice(config, device) {
4050
- const limit = Number(device?.limits?.maxStorageBufferBindingSize);
4051
- if (!Number.isFinite(limit) || limit <= 0) {
4052
- return config.tileSize;
3996
+ function getGpuUsageConstants() {
3997
+ if (typeof GPUBufferUsage === "undefined" || typeof GPUTextureUsage === "undefined" || typeof GPUShaderStage === "undefined") {
3998
+ throw new Error("WebGPU runtime unavailable. Required GPU constants are missing.");
4053
3999
  }
4054
- const maxPixelsByRay = Math.floor(limit / RAY_RECORD_BYTES);
4055
- const maxPixelsByHit = Math.floor(limit / HIT_RECORD_BYTES);
4056
- const maxPixelsByPathVertex = Math.floor(
4057
- limit / ((config.maxDepth + 1) * PATH_VERTEX_RECORD_BYTES)
4058
- );
4059
- const maxPixels = Math.max(
4060
- 256,
4061
- Math.min(maxPixelsByRay, maxPixelsByHit, maxPixelsByPathVertex)
4062
- );
4063
- if (config.tilePixelCapacity <= maxPixels) {
4064
- return config.tileSize;
4000
+ if (typeof GPUBufferUsage.INDIRECT !== "number") {
4001
+ throw new Error("WebGPU runtime unavailable. GPUBufferUsage.INDIRECT is missing.");
4065
4002
  }
4066
- return Math.max(16, Math.floor(Math.sqrt(maxPixels)));
4067
- }
4068
- function createBuffer(device, usage, size, label) {
4069
- return device.createBuffer({
4070
- label,
4071
- size: Math.max(4, size),
4072
- usage
4073
- });
4074
- }
4075
- function alignTo(value, alignment) {
4076
- const resolvedAlignment = Math.max(1, alignment);
4077
- return Math.ceil(value / resolvedAlignment) * resolvedAlignment;
4003
+ return {
4004
+ buffer: GPUBufferUsage,
4005
+ texture: GPUTextureUsage,
4006
+ shader: GPUShaderStage,
4007
+ map: typeof GPUMapMode === "undefined" ? null : GPUMapMode
4008
+ };
4078
4009
  }
4079
- function float32ToFloat16Bits(value) {
4080
- const floatView = new Float32Array(1);
4081
- const intView = new Uint32Array(floatView.buffer);
4082
- floatView[0] = Number.isFinite(value) ? value : 0;
4083
- const x = intView[0];
4084
- const sign = x >> 16 & 32768;
4085
- let mantissa = x & 8388607;
4086
- let exponent = x >> 23 & 255;
4087
- if (exponent === 255) {
4088
- return sign | (mantissa ? 32256 : 31744);
4010
+ function resolveCanvas3(canvasOrSelector, documentRef = globalThis.document) {
4011
+ if (typeof canvasOrSelector === "string") {
4012
+ const resolved = documentRef?.querySelector?.(canvasOrSelector);
4013
+ if (!resolved) {
4014
+ throw new Error(`Unable to find canvas for selector: ${canvasOrSelector}`);
4015
+ }
4016
+ return resolved;
4089
4017
  }
4090
- exponent = exponent - 127 + 15;
4091
- if (exponent >= 31) {
4092
- return sign | 31744;
4018
+ if (canvasOrSelector?.getContext) {
4019
+ return canvasOrSelector;
4093
4020
  }
4094
- if (exponent <= 0) {
4095
- if (exponent < -10) {
4096
- return sign;
4097
- }
4098
- mantissa = (mantissa | 8388608) >> 1 - exponent;
4099
- return sign | mantissa + 4096 >> 13;
4021
+ const fallback = documentRef?.querySelector?.("canvas[data-plasius-wavefront-path-tracing]");
4022
+ if (!fallback) {
4023
+ throw new Error("A canvas is required for WebGPU wavefront path tracing.");
4100
4024
  }
4101
- return sign | exponent << 10 | mantissa + 4096 >> 13;
4025
+ return fallback;
4102
4026
  }
4103
- function environmentMapIntegerScale(data) {
4104
- if (data instanceof Uint8Array) {
4105
- return 1 / 255;
4027
+ async function getPipelineDiagnostics(shaderModule) {
4028
+ if (typeof shaderModule?.getCompilationInfo !== "function") {
4029
+ return "";
4106
4030
  }
4107
- if (data instanceof Uint16Array) {
4108
- return 1 / 65535;
4031
+ try {
4032
+ const info = await shaderModule.getCompilationInfo();
4033
+ const messages = info.messages ?? [];
4034
+ if (messages.length === 0) {
4035
+ return "";
4036
+ }
4037
+ return messages.map((message) => {
4038
+ const line = message.lineNum ?? "?";
4039
+ const column = message.linePos ?? "?";
4040
+ return `line ${line}:${column} ${message.message}`;
4041
+ }).join("\n");
4042
+ } catch {
4043
+ return "";
4109
4044
  }
4110
- return 1;
4111
4045
  }
4112
- function environmentMapHasSamplingData(environmentMap) {
4113
- if (!environmentMap || !environmentMap.data) {
4114
- return false;
4046
+ async function createComputePipeline(device, shaderModule, layout, entryPoint, label) {
4047
+ const descriptor = {
4048
+ label,
4049
+ layout,
4050
+ compute: {
4051
+ module: shaderModule,
4052
+ entryPoint
4053
+ }
4054
+ };
4055
+ try {
4056
+ if (typeof device.createComputePipelineAsync === "function") {
4057
+ return await device.createComputePipelineAsync(descriptor);
4058
+ }
4059
+ return device.createComputePipeline(descriptor);
4060
+ } catch (error) {
4061
+ const diagnostics = await getPipelineDiagnostics(shaderModule);
4062
+ const suffix = diagnostics ? `
4063
+ ${diagnostics}` : "";
4064
+ throw new Error(`WGSL compilation failed for ${label}: ${error.message}${suffix}`, {
4065
+ cause: error
4066
+ });
4115
4067
  }
4116
- const width = Math.max(1, environmentMap.width ?? 1);
4117
- const height = Math.max(1, environmentMap.height ?? 1);
4118
- return environmentMap.data.length >= width * height * 4;
4119
4068
  }
4120
- function createRgba8TextureUpload(source) {
4121
- const width = Math.max(1, Math.trunc(source.width));
4122
- const height = Math.max(1, Math.trunc(source.height));
4123
- const bytesPerRow = alignTo(width * 4, 256);
4124
- const bytes = new Uint8Array(bytesPerRow * height);
4125
- const data = source.data instanceof Uint8Array ? source.data : new Uint8Array(source.data);
4126
- for (let y = 0; y < height; y += 1) {
4127
- const sourceOffset = y * width * 4;
4128
- const targetOffset = y * bytesPerRow;
4129
- bytes.set(data.subarray(sourceOffset, sourceOffset + width * 4), targetOffset);
4069
+ async function assertShaderModuleCompiles(shaderModule, label) {
4070
+ if (typeof shaderModule?.getCompilationInfo !== "function") {
4071
+ return;
4130
4072
  }
4131
- return Object.freeze({
4132
- bytes,
4133
- bytesPerRow,
4134
- width,
4135
- height
4136
- });
4137
- }
4138
- function readEnvironmentMapComponent(data, index, fallback, integerScale = 1) {
4139
- if (!data || index >= data.length) {
4140
- return fallback;
4073
+ const info = await shaderModule.getCompilationInfo();
4074
+ const messages = Array.isArray(info?.messages) ? info.messages : [];
4075
+ const errors = messages.filter((message) => message?.type === "error");
4076
+ if (errors.length <= 0) {
4077
+ return;
4141
4078
  }
4142
- const value = Number(data[index]);
4143
- return Number.isFinite(value) ? Math.max(0, value) * integerScale : fallback;
4079
+ const diagnostics = errors.map((message) => {
4080
+ const line = Number.isFinite(message.lineNum) ? message.lineNum : "?";
4081
+ const column = Number.isFinite(message.linePos) ? message.linePos : "?";
4082
+ return `line ${line}:${column} ${message.message}`;
4083
+ }).join("\n");
4084
+ throw new Error(`WGSL compilation preflight failed for ${label}:
4085
+ ${diagnostics}`);
4144
4086
  }
4145
- function createBrdfLutUploadBytes(size = DEFAULT_BRDF_LUT_SIZE, sampleCount = DEFAULT_BRDF_LUT_SAMPLE_COUNT) {
4146
- const cacheKey = `${Math.max(1, Math.trunc(size))}:${Math.max(1, Math.trunc(sampleCount))}`;
4147
- const cached = BRDF_LUT_UPLOAD_CACHE.get(cacheKey);
4148
- if (cached) {
4149
- return cached;
4150
- }
4151
- const width = Math.max(1, Math.trunc(size));
4152
- const height = Math.max(1, Math.trunc(size));
4153
- const rowBytes = width * 8;
4154
- const bytesPerRow = alignTo(rowBytes, 256);
4155
- const bytes = new Uint8Array(bytesPerRow * height);
4156
- const view = new DataView(bytes.buffer);
4157
- for (let y = 0; y < height; y += 1) {
4158
- const roughness = (y + 0.5) / height;
4159
- for (let x = 0; x < width; x += 1) {
4160
- const nDotV = Math.max((x + 0.5) / width, 1e-4);
4161
- const [scaleTerm, biasTerm] = integrateBrdfSample(nDotV, roughness, sampleCount);
4162
- const offset = y * bytesPerRow + x * 8;
4163
- view.setUint16(offset, float32ToFloat16Bits(scaleTerm), true);
4164
- view.setUint16(offset + 2, float32ToFloat16Bits(biasTerm), true);
4165
- view.setUint16(offset + 4, float32ToFloat16Bits(0), true);
4166
- view.setUint16(offset + 6, float32ToFloat16Bits(1), true);
4167
- }
4168
- }
4169
- const upload = Object.freeze({ bytes, bytesPerRow, width, height });
4170
- BRDF_LUT_UPLOAD_CACHE.set(cacheKey, upload);
4171
- return upload;
4172
- }
4173
- function createLinearEnvironmentPixels(environmentMap, fallbackColor) {
4174
- const width = Math.max(1, environmentMap.width);
4175
- const height = Math.max(1, environmentMap.height);
4176
- const pixels = new Float32Array(width * height * 4);
4177
- const data = environmentMap.data;
4178
- const integerScale = environmentMapIntegerScale(data);
4179
- for (let index = 0; index < width * height; index += 1) {
4180
- const sourceOffset = index * 4;
4181
- const targetOffset = index * 4;
4182
- pixels[targetOffset] = readEnvironmentMapComponent(data, sourceOffset, fallbackColor[0], integerScale);
4183
- pixels[targetOffset + 1] = readEnvironmentMapComponent(data, sourceOffset + 1, fallbackColor[1], integerScale);
4184
- pixels[targetOffset + 2] = readEnvironmentMapComponent(data, sourceOffset + 2, fallbackColor[2], integerScale);
4185
- pixels[targetOffset + 3] = readEnvironmentMapComponent(data, sourceOffset + 3, fallbackColor[3] ?? 1, integerScale);
4087
+ async function createRenderPipeline(device, descriptor) {
4088
+ if (typeof device.createRenderPipelineAsync === "function") {
4089
+ return device.createRenderPipelineAsync(descriptor);
4186
4090
  }
4187
- return pixels;
4188
- }
4189
- function environmentUvToDirection(u, v, rotationRadians = 0) {
4190
- const angle = (u - rotationRadians / (2 * Math.PI) - 0.5) * 2 * Math.PI;
4191
- const theta = v * Math.PI;
4192
- const sinTheta = Math.sin(theta);
4193
- return [
4194
- Math.cos(angle) * sinTheta,
4195
- Math.cos(theta),
4196
- Math.sin(angle) * sinTheta
4197
- ];
4091
+ return device.createRenderPipeline(descriptor);
4198
4092
  }
4199
- function sampleEnvironmentPixelsBilinear(pixels, width, height, u, v) {
4200
- const wrappedU = (u % 1 + 1) % 1;
4201
- const clampedV = clamp2(v, 0, 1);
4202
- const x = wrappedU * width - 0.5;
4203
- const y = clampedV * height - 0.5;
4204
- const x0 = (Math.floor(x) % width + width) % width;
4205
- const y0 = clamp2(Math.floor(y), 0, height - 1);
4206
- const x1 = (x0 + 1) % width;
4207
- const y1 = clamp2(y0 + 1, 0, height - 1);
4208
- const tx = x - Math.floor(x);
4209
- const ty = y - Math.floor(y);
4210
- const read = (px, py) => {
4211
- const offset = (py * width + px) * 4;
4212
- return [pixels[offset], pixels[offset + 1], pixels[offset + 2], pixels[offset + 3]];
4093
+ function createWavefrontDeviceDescriptor(adapter, options = {}, memory = null) {
4094
+ const descriptor = { ...options.deviceDescriptor ?? {} };
4095
+ const descriptorRequiredLimits = { ...descriptor.requiredLimits ?? {} };
4096
+ const requiredLimits = {
4097
+ ...descriptorRequiredLimits,
4098
+ ...options.requiredLimits ?? {}
4213
4099
  };
4214
- const a = read(x0, y0);
4215
- const b = read(x1, y0);
4216
- const c = read(x0, y1);
4217
- const d = read(x1, y1);
4218
- const mixPair = (first, second, factor) => first * (1 - factor) + second * factor;
4219
- return [
4220
- mixPair(mixPair(a[0], b[0], tx), mixPair(c[0], d[0], tx), ty),
4221
- mixPair(mixPair(a[1], b[1], tx), mixPair(c[1], d[1], tx), ty),
4222
- mixPair(mixPair(a[2], b[2], tx), mixPair(c[2], d[2], tx), ty),
4223
- mixPair(mixPair(a[3], b[3], tx), mixPair(c[3], d[3], tx), ty)
4224
- ];
4225
- }
4226
- function directionToEnvironmentUv(direction, rotationRadians = 0) {
4227
- const unitDirection = normalize(direction, [0, 1, 0]);
4228
- const rotationTurns = rotationRadians / (2 * Math.PI);
4229
- const u = ((Math.atan2(unitDirection[2], unitDirection[0]) / (2 * Math.PI) + 0.5 + rotationTurns) % 1 + 1) % 1;
4230
- const v = Math.acos(clamp2(unitDirection[1], -1, 1)) / Math.PI;
4231
- return [u, clamp2(v, 0, 1)];
4232
- }
4233
- function sampleEnvironmentRadiance(pixels, width, height, direction, rotationRadians = 0) {
4234
- const [u, v] = directionToEnvironmentUv(direction, rotationRadians);
4235
- return sampleEnvironmentPixelsBilinear(pixels, width, height, u, v);
4236
- }
4237
- function createFloat16RgbaUploadFromLevels(levels) {
4238
- return levels.map((level) => {
4239
- const rowBytes = level.width * 8;
4240
- const bytesPerRow = alignTo(rowBytes, 256);
4241
- const bytes = new Uint8Array(bytesPerRow * level.height);
4242
- const view = new DataView(bytes.buffer);
4243
- for (let y = 0; y < level.height; y += 1) {
4244
- for (let x = 0; x < level.width; x += 1) {
4245
- const sourceOffset = (y * level.width + x) * 4;
4246
- const targetOffset = y * bytesPerRow + x * 8;
4247
- view.setUint16(targetOffset, float32ToFloat16Bits(level.data[sourceOffset]), true);
4248
- view.setUint16(targetOffset + 2, float32ToFloat16Bits(level.data[sourceOffset + 1]), true);
4249
- view.setUint16(targetOffset + 4, float32ToFloat16Bits(level.data[sourceOffset + 2]), true);
4250
- view.setUint16(targetOffset + 6, float32ToFloat16Bits(level.data[sourceOffset + 3]), true);
4251
- }
4100
+ const traceStorageBindings = TRACE_STORAGE_BUFFER_BINDINGS;
4101
+ const traceTextureBindings = TRACE_SAMPLED_TEXTURE_BINDINGS + (options.instances !== void 0 ? 1 : 0);
4102
+ const exposedStorageBufferLimit = Number(adapter?.limits?.maxStorageBuffersPerShaderStage);
4103
+ if (Number.isFinite(exposedStorageBufferLimit)) {
4104
+ if (exposedStorageBufferLimit < traceStorageBindings) {
4105
+ throw new Error(
4106
+ `Wavefront mesh tracing requires maxStorageBuffersPerShaderStage>=${traceStorageBindings}, but this adapter exposes ${exposedStorageBufferLimit}.`
4107
+ );
4252
4108
  }
4253
- return Object.freeze({ bytes, bytesPerRow, width: level.width, height: level.height });
4254
- });
4255
- }
4256
- function createPrefilteredEnvironmentLevels(environmentMap, fallbackColor) {
4257
- const sourcePixels = createLinearEnvironmentPixels(environmentMap, fallbackColor);
4258
- const sourceWidth = Math.max(1, environmentMap.width);
4259
- const sourceHeight = Math.max(1, environmentMap.height);
4260
- const mipLevelCount = Math.max(1, Math.floor(Math.log2(Math.max(sourceWidth, sourceHeight))) + 1);
4261
- const levels = [
4262
- Object.freeze({
4263
- width: sourceWidth,
4264
- height: sourceHeight,
4265
- data: sourcePixels
4266
- })
4267
- ];
4268
- for (let mipLevel = 1; mipLevel < mipLevelCount; mipLevel += 1) {
4269
- const width = Math.max(1, sourceWidth >> mipLevel);
4270
- const height = Math.max(1, sourceHeight >> mipLevel);
4271
- const roughness = mipLevelCount <= 1 ? 0 : mipLevel / (mipLevelCount - 1);
4272
- const data = new Float32Array(width * height * 4);
4273
- const sampleCount = roughness < 0.25 ? 64 : roughness < 0.6 ? 96 : 128;
4274
- for (let y = 0; y < height; y += 1) {
4275
- for (let x = 0; x < width; x += 1) {
4276
- const direction = environmentUvToDirection((x + 0.5) / width, (y + 0.5) / height, environmentMap.rotationRadians);
4277
- const normal = normalize(direction, [0, 1, 0]);
4278
- const viewDirection = normal;
4279
- let totalWeight = 0;
4280
- const accum = [0, 0, 0];
4281
- for (let sampleIndex = 0; sampleIndex < sampleCount; sampleIndex += 1) {
4282
- const xi = hammersley(sampleIndex, sampleCount);
4283
- const halfVector = importanceSampleGgx(xi, roughness, normal);
4284
- const viewDotHalf = Math.max(dot(viewDirection, halfVector), 0);
4285
- const lightDirection = normalize(
4286
- subtract(scale(halfVector, 2 * viewDotHalf), viewDirection),
4287
- normal
4288
- );
4289
- const nDotL = Math.max(dot(normal, lightDirection), 0);
4290
- if (nDotL <= 1e-6) {
4291
- continue;
4292
- }
4293
- const radiance = sampleEnvironmentRadiance(
4294
- sourcePixels,
4295
- sourceWidth,
4296
- sourceHeight,
4297
- lightDirection,
4298
- environmentMap.rotationRadians
4299
- );
4300
- accum[0] += radiance[0] * nDotL;
4301
- accum[1] += radiance[1] * nDotL;
4302
- accum[2] += radiance[2] * nDotL;
4303
- totalWeight += nDotL;
4304
- }
4305
- const offset = (y * width + x) * 4;
4306
- data[offset] = accum[0] / Math.max(totalWeight, 1e-6);
4307
- data[offset + 1] = accum[1] / Math.max(totalWeight, 1e-6);
4308
- data[offset + 2] = accum[2] / Math.max(totalWeight, 1e-6);
4309
- data[offset + 3] = 1;
4310
- }
4109
+ requiredLimits.maxStorageBuffersPerShaderStage = Math.max(
4110
+ Number(requiredLimits.maxStorageBuffersPerShaderStage ?? 0),
4111
+ traceStorageBindings
4112
+ );
4113
+ }
4114
+ const exposedSampledTextureLimit = Number(adapter?.limits?.maxSampledTexturesPerShaderStage);
4115
+ if (Number.isFinite(exposedSampledTextureLimit)) {
4116
+ if (exposedSampledTextureLimit < traceTextureBindings) {
4117
+ throw new Error(
4118
+ `Wavefront material tracing requires maxSampledTexturesPerShaderStage>=${traceTextureBindings}, but this adapter exposes ${exposedSampledTextureLimit}.`
4119
+ );
4311
4120
  }
4312
- levels.push(Object.freeze({ width, height, data }));
4121
+ requiredLimits.maxSampledTexturesPerShaderStage = Math.max(
4122
+ Number(requiredLimits.maxSampledTexturesPerShaderStage ?? 0),
4123
+ traceTextureBindings
4124
+ );
4313
4125
  }
4314
- return Object.freeze({
4315
- levels,
4316
- mipLevelCount,
4317
- width: sourceWidth,
4318
- height: sourceHeight
4319
- });
4320
- }
4321
- function createEnvironmentSamplingTables(environmentMap, fallbackColor) {
4322
- if (!environmentMapHasSamplingData(environmentMap)) {
4323
- return Object.freeze({
4324
- width: 1,
4325
- height: 1,
4326
- pdf: new Float32Array([1]),
4327
- marginalCdf: new Float32Array([1]),
4328
- conditionalCdf: new Float32Array([1]),
4329
- hasImportanceData: false
4330
- });
4126
+ const sceneStorageBufferBindingBytes = Number(
4127
+ memory?.sceneStorageBufferBindingBytes ?? 0
4128
+ );
4129
+ const requestedStorageBufferBindingSize = Math.max(
4130
+ readRequestedDeviceLimit(descriptorRequiredLimits, "maxStorageBufferBindingSize"),
4131
+ readRequestedDeviceLimit(options.requiredLimits, "maxStorageBufferBindingSize"),
4132
+ sceneStorageBufferBindingBytes > WEBGPU_DEFAULT_MAX_STORAGE_BUFFER_BINDING_SIZE ? sceneStorageBufferBindingBytes : 0
4133
+ );
4134
+ if (requestedStorageBufferBindingSize > 0) {
4135
+ assertAdapterSupportsRequiredLimit(
4136
+ adapter,
4137
+ "maxStorageBufferBindingSize",
4138
+ requestedStorageBufferBindingSize
4139
+ );
4140
+ requiredLimits.maxStorageBufferBindingSize = requestedStorageBufferBindingSize;
4331
4141
  }
4332
- const pixels = createLinearEnvironmentPixels(environmentMap, fallbackColor);
4333
- const width = Math.max(1, environmentMap.width);
4334
- const height = Math.max(1, environmentMap.height);
4335
- const pdf = new Float32Array(width * height);
4336
- const marginalCdf = new Float32Array(height);
4337
- const conditionalCdf = new Float32Array(width * height);
4338
- const rowSums = new Float32Array(height);
4339
- let totalWeight = 0;
4340
- for (let y = 0; y < height; y += 1) {
4341
- const theta = (y + 0.5) / height * Math.PI;
4342
- const sinTheta = Math.max(Math.sin(theta), 1e-4);
4343
- let rowWeight = 0;
4344
- for (let x = 0; x < width; x += 1) {
4345
- const offset = (y * width + x) * 4;
4346
- const luminance = pixels[offset] * 0.2126 + pixels[offset + 1] * 0.7152 + pixels[offset + 2] * 0.0722;
4347
- const weight = Math.max(luminance * sinTheta, 1e-6);
4348
- pdf[y * width + x] = weight;
4349
- rowWeight += weight;
4350
- conditionalCdf[y * width + x] = rowWeight;
4351
- }
4352
- rowSums[y] = rowWeight;
4353
- totalWeight += rowWeight;
4354
- if (rowWeight > 0) {
4355
- for (let x = 0; x < width; x += 1) {
4356
- conditionalCdf[y * width + x] /= rowWeight;
4357
- }
4358
- } else {
4359
- for (let x = 0; x < width; x += 1) {
4360
- conditionalCdf[y * width + x] = (x + 1) / width;
4361
- }
4362
- }
4363
- marginalCdf[y] = totalWeight;
4142
+ const requestedMaxBufferSize = Math.max(
4143
+ readRequestedDeviceLimit(descriptorRequiredLimits, "maxBufferSize"),
4144
+ readRequestedDeviceLimit(options.requiredLimits, "maxBufferSize"),
4145
+ sceneStorageBufferBindingBytes > WEBGPU_DEFAULT_MAX_BUFFER_SIZE ? sceneStorageBufferBindingBytes : 0,
4146
+ requestedStorageBufferBindingSize > WEBGPU_DEFAULT_MAX_BUFFER_SIZE ? requestedStorageBufferBindingSize : 0
4147
+ );
4148
+ if (requestedMaxBufferSize > 0) {
4149
+ assertAdapterSupportsRequiredLimit(adapter, "maxBufferSize", requestedMaxBufferSize);
4150
+ requiredLimits.maxBufferSize = requestedMaxBufferSize;
4364
4151
  }
4365
- for (let y = 0; y < height; y += 1) {
4366
- marginalCdf[y] /= Math.max(totalWeight, 1e-6);
4152
+ const requiredFeatures = Array.from(descriptor.requiredFeatures ?? []);
4153
+ const adapterFeatures = adapter?.features;
4154
+ const timestampQuerySupported = typeof adapterFeatures?.has === "function" ? adapterFeatures.has("timestamp-query") : adapterFeatures && typeof adapterFeatures[Symbol.iterator] === "function" ? Array.from(adapterFeatures).includes("timestamp-query") : false;
4155
+ if (options.gpuTimestamps !== false && timestampQuerySupported && !requiredFeatures.includes("timestamp-query")) {
4156
+ requiredFeatures.push("timestamp-query");
4367
4157
  }
4368
- for (let index = 0; index < pdf.length; index += 1) {
4369
- pdf[index] /= Math.max(totalWeight, 1e-6);
4158
+ if (requiredFeatures.length > 0) {
4159
+ descriptor.requiredFeatures = requiredFeatures;
4370
4160
  }
4371
- return Object.freeze({
4372
- width,
4373
- height,
4374
- pdf,
4375
- marginalCdf,
4376
- conditionalCdf,
4377
- hasImportanceData: true
4378
- });
4379
- }
4380
- function createEnvironmentMapResource(device, constants, environmentMap, fallbackColor) {
4381
- if (environmentMap.view) {
4382
- return Object.freeze({
4383
- view: environmentMap.view,
4384
- sampler: environmentMap.sampler ?? device.createSampler({
4385
- label: "plasius.wavefront.environmentMapSampler",
4386
- addressModeU: "repeat",
4387
- addressModeV: "clamp-to-edge",
4388
- magFilter: "linear",
4389
- minFilter: "linear",
4390
- mipmapFilter: "linear"
4391
- }),
4392
- texture: null,
4393
- ownsTexture: false,
4394
- width: Math.max(1, environmentMap.width),
4395
- height: Math.max(1, environmentMap.height),
4396
- mipLevelCount: Math.max(1, environmentMap.mipLevelCount ?? 1)
4397
- });
4161
+ if (Object.keys(requiredLimits).length > 0) {
4162
+ descriptor.requiredLimits = {
4163
+ ...descriptor.requiredLimits ?? {},
4164
+ ...requiredLimits
4165
+ };
4398
4166
  }
4399
- if (environmentMap.texture && typeof environmentMap.texture.createView === "function") {
4400
- return Object.freeze({
4401
- view: environmentMap.texture.createView(),
4402
- sampler: environmentMap.sampler ?? device.createSampler({
4403
- label: "plasius.wavefront.environmentMapSampler",
4404
- addressModeU: "repeat",
4405
- addressModeV: "clamp-to-edge",
4406
- magFilter: "linear",
4407
- minFilter: "linear",
4408
- mipmapFilter: "linear"
4409
- }),
4410
- texture: environmentMap.texture,
4411
- ownsTexture: false,
4412
- width: Math.max(1, environmentMap.width),
4413
- height: Math.max(1, environmentMap.height),
4414
- mipLevelCount: Math.max(1, environmentMap.mipLevelCount ?? 1)
4415
- });
4167
+ return Object.keys(descriptor).length > 0 ? descriptor : void 0;
4168
+ }
4169
+ function readGpuLimit(adapter, device, name) {
4170
+ const adapterValue = Number(adapter?.limits?.[name]);
4171
+ if (Number.isFinite(adapterValue)) {
4172
+ return adapterValue;
4416
4173
  }
4417
- const prefiltered = createPrefilteredEnvironmentLevels(environmentMap, fallbackColor);
4418
- const uploads = createFloat16RgbaUploadFromLevels(prefiltered.levels);
4419
- const texture = device.createTexture({
4420
- label: environmentMap.enabled ? "plasius.wavefront.environmentMap" : "plasius.wavefront.environmentMapFallback",
4421
- size: { width: prefiltered.width, height: prefiltered.height },
4422
- format: "rgba16float",
4423
- mipLevelCount: prefiltered.mipLevelCount,
4424
- usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
4425
- });
4426
- uploads.forEach((upload, mipLevel) => {
4427
- device.queue.writeTexture(
4428
- { texture, mipLevel },
4429
- upload.bytes,
4430
- { bytesPerRow: upload.bytesPerRow, rowsPerImage: upload.height },
4431
- { width: upload.width, height: upload.height, depthOrArrayLayers: 1 }
4432
- );
4433
- });
4434
- return Object.freeze({
4435
- view: texture.createView(),
4436
- sampler: environmentMap.sampler ?? device.createSampler({
4437
- label: "plasius.wavefront.environmentMapSampler",
4438
- addressModeU: "repeat",
4439
- addressModeV: "clamp-to-edge",
4440
- magFilter: "linear",
4441
- minFilter: "linear",
4442
- mipmapFilter: "linear"
4443
- }),
4444
- texture,
4445
- ownsTexture: true,
4446
- width: prefiltered.width,
4447
- height: prefiltered.height,
4448
- mipLevelCount: prefiltered.mipLevelCount
4449
- });
4174
+ const deviceValue = Number(device?.limits?.[name]);
4175
+ return Number.isFinite(deviceValue) ? deviceValue : null;
4450
4176
  }
4451
- function createEnvironmentSamplingTextureResource(device, constants, environmentMap, fallbackColor) {
4452
- const tables = createEnvironmentSamplingTables(environmentMap, fallbackColor);
4453
- const rowBytes = tables.width * 8;
4454
- const bytesPerRow = alignTo(rowBytes, 256);
4455
- const bytes = new Uint8Array(bytesPerRow * tables.height);
4456
- const view = new DataView(bytes.buffer);
4457
- for (let y = 0; y < tables.height; y += 1) {
4458
- for (let x = 0; x < tables.width; x += 1) {
4459
- const probability = tables.pdf[y * tables.width + x];
4460
- const conditional = tables.conditionalCdf[y * tables.width + x];
4461
- const marginal = tables.marginalCdf[y];
4462
- const offset = y * bytesPerRow + x * 8;
4463
- view.setUint16(offset, float32ToFloat16Bits(probability), true);
4464
- view.setUint16(offset + 2, float32ToFloat16Bits(conditional), true);
4465
- view.setUint16(offset + 4, float32ToFloat16Bits(marginal), true);
4466
- view.setUint16(offset + 6, float32ToFloat16Bits(1), true);
4467
- }
4177
+ function createAdapterInfoSnapshot(adapter) {
4178
+ const info = adapter?.info;
4179
+ if (!info || typeof info !== "object") {
4180
+ return null;
4468
4181
  }
4469
- const texture = device.createTexture({
4470
- label: "plasius.wavefront.environmentSampling",
4471
- size: { width: tables.width, height: tables.height },
4472
- format: "rgba16float",
4473
- usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
4474
- });
4475
- device.queue.writeTexture(
4476
- { texture },
4477
- bytes,
4478
- { bytesPerRow, rowsPerImage: tables.height },
4479
- { width: tables.width, height: tables.height, depthOrArrayLayers: 1 }
4480
- );
4481
4182
  return Object.freeze({
4482
- view: texture.createView(),
4483
- texture,
4484
- ownsTexture: true,
4485
- hasImportanceData: tables.hasImportanceData
4183
+ vendor: typeof info.vendor === "string" ? info.vendor : "",
4184
+ architecture: typeof info.architecture === "string" ? info.architecture : "",
4185
+ device: typeof info.device === "string" ? info.device : "",
4186
+ description: typeof info.description === "string" ? info.description : ""
4486
4187
  });
4487
4188
  }
4488
- function createBrdfLutResource(device, constants, size = DEFAULT_BRDF_LUT_SIZE) {
4489
- const upload = createBrdfLutUploadBytes(size);
4490
- const texture = device.createTexture({
4491
- label: "plasius.wavefront.brdfLut",
4492
- size: { width: upload.width, height: upload.height },
4493
- format: "rgba16float",
4494
- usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
4495
- });
4496
- device.queue.writeTexture(
4497
- { texture },
4498
- upload.bytes,
4499
- { bytesPerRow: upload.bytesPerRow, rowsPerImage: upload.height },
4500
- { width: upload.width, height: upload.height, depthOrArrayLayers: 1 }
4501
- );
4189
+ function createGpuAdapterParallelismDiagnostics(adapter, device) {
4502
4190
  return Object.freeze({
4503
- view: texture.createView(),
4504
- sampler: device.createSampler({
4505
- label: "plasius.wavefront.brdfLutSampler",
4506
- addressModeU: "clamp-to-edge",
4507
- addressModeV: "clamp-to-edge",
4508
- magFilter: "linear",
4509
- minFilter: "linear"
4191
+ physicalCoreCount: null,
4192
+ physicalCoreCountAvailable: false,
4193
+ physicalCoreCountUnavailableReason: "WebGPU does not expose physical GPU core counts.",
4194
+ adapterInfo: createAdapterInfoSnapshot(adapter),
4195
+ adapterLimits: Object.freeze({
4196
+ maxComputeInvocationsPerWorkgroup: readGpuLimit(adapter, device, "maxComputeInvocationsPerWorkgroup"),
4197
+ maxComputeWorkgroupSizeX: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeX"),
4198
+ maxComputeWorkgroupSizeY: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeY"),
4199
+ maxComputeWorkgroupSizeZ: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeZ"),
4200
+ maxComputeWorkgroupsPerDimension: readGpuLimit(adapter, device, "maxComputeWorkgroupsPerDimension"),
4201
+ maxStorageBuffersPerShaderStage: readGpuLimit(adapter, device, "maxStorageBuffersPerShaderStage"),
4202
+ maxStorageBufferBindingSize: readGpuLimit(adapter, device, "maxStorageBufferBindingSize"),
4203
+ maxBufferSize: readGpuLimit(adapter, device, "maxBufferSize")
4510
4204
  }),
4511
- texture,
4512
- ownsTexture: true,
4513
- width: upload.width,
4514
- height: upload.height
4205
+ configuredWorkgroupSize: WORKGROUP_SIZE
4515
4206
  });
4516
4207
  }
4517
- function createMediumTextureResource(device, constants, mediums) {
4518
- const normalized = Array.isArray(mediums) && mediums.length > 0 ? mediums : [{ id: 0 }];
4519
- const width = Math.max(
4208
+ function createEnvironmentMapSnapshot(environmentMap) {
4209
+ return Object.freeze({
4210
+ enabled: environmentMap.enabled,
4211
+ width: environmentMap.width,
4212
+ height: environmentMap.height,
4213
+ mipLevelCount: environmentMap.mipLevelCount ?? 1,
4214
+ projection: environmentMap.projection,
4215
+ intensity: environmentMap.intensity,
4216
+ rotationRadians: environmentMap.rotationRadians,
4217
+ ambientStrength: environmentMap.ambientStrength,
4218
+ hasImportanceData: environmentMap.hasImportanceData === true
4219
+ });
4220
+ }
4221
+ function nowMs3() {
4222
+ if (typeof performance !== "undefined" && typeof performance.now === "function") {
4223
+ return performance.now();
4224
+ }
4225
+ return Date.now();
4226
+ }
4227
+ function estimateAccelerationBuildWaitFactor(config) {
4228
+ if (config?.gpuAccelerationBuildRequired !== true) {
4229
+ return 1;
4230
+ }
4231
+ const bvhSortStageCount = Array.isArray(config?.bvhSortStages) ? config.bvhSortStages.length : 0;
4232
+ const bvhBuildLevelCount = Array.isArray(config?.bvhBuildLevels) ? config.bvhBuildLevels.length : 0;
4233
+ const accelerationStageCount = 2 + bvhSortStageCount + bvhBuildLevelCount;
4234
+ return Math.max(1, 1 + accelerationStageCount / 96);
4235
+ }
4236
+ function estimateSubmittedGpuWorkTiming(config, tileCount, overrideTimeoutMs = null, options = {}) {
4237
+ if (Number.isFinite(overrideTimeoutMs)) {
4238
+ const overrideMs = Math.max(1, Math.trunc(Number(overrideTimeoutMs)));
4239
+ return Object.freeze({
4240
+ timeoutMs: overrideMs,
4241
+ maxWaitMs: overrideMs
4242
+ });
4243
+ }
4244
+ const samplesPerPixel = Math.max(
4520
4245
  1,
4521
- normalized.reduce((maximum, medium) => Math.max(maximum, medium.id ?? 0), 0) + 1
4246
+ Number(config?.renderedSamplesPerPixel ?? config?.samplesPerPixel ?? 1)
4522
4247
  );
4523
- const level = {
4524
- width,
4525
- height: MEDIUM_TABLE_ROWS,
4526
- data: new Float32Array(width * MEDIUM_TABLE_ROWS * 4)
4527
- };
4528
- for (const medium of normalized) {
4529
- const mediumId = Math.max(0, Math.trunc(Number(medium.id) || 0));
4530
- const absorptionOffset = mediumId * 4;
4531
- level.data[absorptionOffset] = Math.max(0, medium.absorption?.[0] ?? 0);
4532
- level.data[absorptionOffset + 1] = Math.max(0, medium.absorption?.[1] ?? 0);
4533
- level.data[absorptionOffset + 2] = Math.max(0, medium.absorption?.[2] ?? 0);
4534
- level.data[absorptionOffset + 3] = Math.max(0, medium.phaseModel ?? 0);
4535
- const scatteringOffset = (width + mediumId) * 4;
4536
- level.data[scatteringOffset] = Math.max(0, medium.scattering?.[0] ?? 0);
4537
- level.data[scatteringOffset + 1] = Math.max(0, medium.scattering?.[1] ?? 0);
4538
- level.data[scatteringOffset + 2] = Math.max(0, medium.scattering?.[2] ?? 0);
4539
- level.data[scatteringOffset + 3] = Math.max(0, medium.density ?? 0);
4540
- }
4541
- const upload = createFloat16RgbaUploadFromLevels([level])[0];
4542
- const texture = device.createTexture({
4543
- label: "plasius.wavefront.mediumTable",
4544
- size: { width, height: MEDIUM_TABLE_ROWS },
4545
- format: "rgba16float",
4546
- usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
4547
- });
4548
- device.queue.writeTexture(
4549
- { texture },
4550
- upload.bytes,
4551
- { bytesPerRow: upload.bytesPerRow, rowsPerImage: upload.height },
4552
- { width, height: MEDIUM_TABLE_ROWS, depthOrArrayLayers: 1 }
4248
+ const maxDepth = Math.max(1, Number(config?.maxDepth ?? 1));
4249
+ const deferredResolvePasses = config?.deferredPathResolve ? 1 : 0;
4250
+ const denoisePasses = config?.denoise ? samplesPerPixel < 4 ? 2 : 1 : 0;
4251
+ const tiles = Math.max(1, Number(tileCount ?? 1));
4252
+ const estimatedPasses = tiles * (samplesPerPixel * (maxDepth + 1 + deferredResolvePasses) + denoisePasses + 1);
4253
+ const triangleCount = Math.max(0, Number(config?.triangleCount ?? 0));
4254
+ const geometryFactor = Math.max(1, triangleCount / 131072);
4255
+ const includeAccelerationBuild = options.includeAccelerationBuild === true;
4256
+ const accelerationFactor = includeAccelerationBuild ? estimateAccelerationBuildWaitFactor(config) : 1;
4257
+ const estimatedWindowMs = Math.round(
4258
+ (GPU_SUBMITTED_WORK_TIMEOUT_MS + estimatedPasses * 5) * geometryFactor * accelerationFactor
4259
+ );
4260
+ const timeoutMs = Math.min(
4261
+ GPU_MAX_SUBMITTED_WORK_TIMEOUT_MS,
4262
+ Math.max(GPU_SUBMITTED_WORK_TIMEOUT_MS, estimatedWindowMs)
4263
+ );
4264
+ const maxWaitMultiplier = includeAccelerationBuild ? 3 : 2;
4265
+ const maxWaitMs = Math.min(
4266
+ GPU_MAX_SUBMITTED_WORK_DEADLINE_MS,
4267
+ Math.max(timeoutMs, estimatedWindowMs * maxWaitMultiplier)
4553
4268
  );
4554
4269
  return Object.freeze({
4555
- texture,
4556
- view: texture.createView(),
4557
- ownsTexture: true,
4558
- count: normalized.length,
4559
- width
4270
+ timeoutMs,
4271
+ maxWaitMs
4560
4272
  });
4561
4273
  }
4562
- function mediumTablesEqual(left, right) {
4563
- const leftMediums = Array.isArray(left) ? left : [];
4564
- const rightMediums = Array.isArray(right) ? right : [];
4565
- if (leftMediums.length !== rightMediums.length) {
4566
- return false;
4274
+
4275
+ // src/wavefront-gpu-synchronization.js
4276
+ async function waitForSubmittedGpuWork(device, options = {}) {
4277
+ if (typeof device.queue.onSubmittedWorkDone !== "function") {
4278
+ return true;
4567
4279
  }
4568
- for (let index = 0; index < leftMediums.length; index += 1) {
4569
- const leftMedium = leftMediums[index];
4570
- const rightMedium = rightMediums[index];
4571
- if ((leftMedium?.id ?? 0) !== (rightMedium?.id ?? 0)) {
4572
- return false;
4573
- }
4574
- if ((leftMedium?.phaseModel ?? 0) !== (rightMedium?.phaseModel ?? 0)) {
4575
- return false;
4280
+ const timeoutMs = Math.max(
4281
+ 1,
4282
+ Number.isFinite(options.timeoutMs) ? Number(options.timeoutMs) : GPU_SUBMITTED_WORK_TIMEOUT_MS
4283
+ );
4284
+ const maxWaitMs = Math.max(
4285
+ timeoutMs,
4286
+ Number.isFinite(options.maxWaitMs) ? Number(options.maxWaitMs) : timeoutMs
4287
+ );
4288
+ const allowTimeout = options.allowTimeout !== false;
4289
+ const completionPromise = device.queue.onSubmittedWorkDone().then(
4290
+ () => ({ status: "done" }),
4291
+ (error) => {
4292
+ throw error;
4576
4293
  }
4577
- if ((leftMedium?.density ?? 0) !== (rightMedium?.density ?? 0)) {
4294
+ );
4295
+ const lossPromise = typeof device.lost?.then === "function" ? device.lost.then((info) => {
4296
+ throw new Error(
4297
+ `WebGPU device lost while waiting for submitted work (${info?.reason ?? "unknown"}).`
4298
+ );
4299
+ }) : null;
4300
+ const startedAtMs = nowMs3();
4301
+ while (true) {
4302
+ const elapsedMs = Math.max(0, nowMs3() - startedAtMs);
4303
+ const remainingMs = Math.max(0, maxWaitMs - elapsedMs);
4304
+ if (remainingMs <= 0) {
4305
+ if (!allowTimeout) {
4306
+ throw new Error(`Timed out after ${Math.round(maxWaitMs)} ms waiting for submitted GPU work.`);
4307
+ }
4308
+ console.warn(
4309
+ `[plasius.wavefront] Submitted GPU work did not report completion within ${Math.round(maxWaitMs)} ms; continuing.`
4310
+ );
4578
4311
  return false;
4579
4312
  }
4580
- for (let component = 0; component < 3; component += 1) {
4581
- if ((leftMedium?.absorption?.[component] ?? 0) !== (rightMedium?.absorption?.[component] ?? 0)) {
4582
- return false;
4313
+ const waitWindowMs = Math.max(1, Math.min(timeoutMs, remainingMs));
4314
+ let timeoutHandle = null;
4315
+ let resolveTimeoutPromise = null;
4316
+ let timeoutSettled = false;
4317
+ const settleTimeoutPromise = (value) => {
4318
+ if (timeoutSettled) {
4319
+ return;
4583
4320
  }
4584
- if ((leftMedium?.scattering?.[component] ?? 0) !== (rightMedium?.scattering?.[component] ?? 0)) {
4585
- return false;
4321
+ timeoutSettled = true;
4322
+ resolveTimeoutPromise?.(value);
4323
+ };
4324
+ const timeoutPromise = new Promise((resolve) => {
4325
+ resolveTimeoutPromise = resolve;
4326
+ timeoutHandle = setTimeout(
4327
+ () => settleTimeoutPromise({ status: "timeout" }),
4328
+ waitWindowMs
4329
+ );
4330
+ });
4331
+ let result;
4332
+ try {
4333
+ result = await Promise.race(
4334
+ [completionPromise, timeoutPromise, lossPromise].filter(Boolean)
4335
+ );
4336
+ } finally {
4337
+ if (timeoutHandle !== null) {
4338
+ clearTimeout(timeoutHandle);
4339
+ settleTimeoutPromise({ status: "cancelled" });
4340
+ }
4341
+ }
4342
+ if (result?.status === "done") {
4343
+ return true;
4344
+ }
4345
+ if (result?.status !== "timeout") {
4346
+ return true;
4347
+ }
4348
+ }
4349
+ }
4350
+
4351
+ // src/wavefront-upload.js
4352
+ async function uploadWavefrontBuffer(device, buffer, data, { chunkBytes = 8 * 1024 * 1024 } = {}) {
4353
+ if (!Number.isSafeInteger(chunkBytes) || chunkBytes < 4 || chunkBytes % 4 !== 0) throw new Error("GPU upload chunk size must be a positive multiple of four.");
4354
+ if (data.byteLength <= chunkBytes) {
4355
+ device.queue.writeBuffer(buffer, 0, data);
4356
+ return;
4357
+ }
4358
+ for (let offset = 0; offset < data.byteLength; offset += chunkBytes) {
4359
+ device.queue.writeBuffer(buffer, offset, data, offset, Math.min(chunkBytes, data.byteLength - offset));
4360
+ await waitForSubmittedGpuWork(device, { timeoutMs: 1e4, maxWaitMs: 1e4, allowTimeout: false });
4361
+ }
4362
+ }
4363
+
4364
+ // src/gpu-allocation-budget.js
4365
+ var formats = Object.freeze({
4366
+ r8unorm: 1,
4367
+ r8snorm: 1,
4368
+ r8uint: 1,
4369
+ r8sint: 1,
4370
+ rg8unorm: 2,
4371
+ rg8snorm: 2,
4372
+ rg8uint: 2,
4373
+ rg8sint: 2,
4374
+ r16float: 2,
4375
+ r16uint: 2,
4376
+ r16sint: 2,
4377
+ rgba8unorm: 4,
4378
+ "rgba8unorm-srgb": 4,
4379
+ bgra8unorm: 4,
4380
+ "bgra8unorm-srgb": 4,
4381
+ rgba8snorm: 4,
4382
+ rgba8uint: 4,
4383
+ rgba8sint: 4,
4384
+ rg16float: 4,
4385
+ rg16uint: 4,
4386
+ rg16sint: 4,
4387
+ r32float: 4,
4388
+ r32uint: 4,
4389
+ r32sint: 4,
4390
+ rgb10a2unorm: 4,
4391
+ rg11b10ufloat: 4,
4392
+ rgba16float: 8,
4393
+ rgba16uint: 8,
4394
+ rgba16sint: 8,
4395
+ rg32float: 8,
4396
+ rg32uint: 8,
4397
+ rg32sint: 8,
4398
+ rgba32float: 16,
4399
+ rgba32uint: 16,
4400
+ rgba32sint: 16
4401
+ });
4402
+ function integer(value, name, minimum = 1) {
4403
+ if (!Number.isSafeInteger(value) || value < minimum) throw new Error(`${name} must be a safe integer >= ${minimum}.`);
4404
+ return value;
4405
+ }
4406
+ function within(value, limit, name) {
4407
+ if (Number.isFinite(limit) && value > limit) throw new Error(`GPU ${name} limit exceeded: requested ${value}, available ${limit}.`);
4408
+ }
4409
+ function textureBytes(descriptor, limits) {
4410
+ const size = descriptor.size;
4411
+ const array = Array.isArray(size) || ArrayBuffer.isView(size);
4412
+ const width = integer(array ? size[0] : size?.width, "Texture width");
4413
+ const height = integer((array ? size[1] : size?.height) ?? 1, "Texture height");
4414
+ const layers = integer((array ? size[2] : size?.depthOrArrayLayers) ?? 1, "Texture depth/layers");
4415
+ const dimension2 = descriptor.dimension ?? "2d";
4416
+ if (!["1d", "2d", "3d"].includes(dimension2)) throw new Error("Unsupported GPU texture dimension.");
4417
+ const limit = limits[dimension2 === "3d" ? "maxTextureDimension3D" : dimension2 === "1d" ? "maxTextureDimension1D" : "maxTextureDimension2D"];
4418
+ within(Math.max(width, height, dimension2 === "3d" ? layers : 1), limit, "texture dimension");
4419
+ if (dimension2 !== "3d") within(layers, limits.maxTextureArrayLayers, "texture layers");
4420
+ const bpp = formats[descriptor.format];
4421
+ if (!bpp) throw new Error(`GPU memory accounting does not support texture format ${descriptor.format}.`);
4422
+ const levels = integer(descriptor.mipLevelCount ?? 1, "Mip count");
4423
+ const samples = integer(descriptor.sampleCount ?? 1, "Sample count");
4424
+ if (levels > Math.floor(Math.log2(Math.max(width, height, dimension2 === "3d" ? layers : 1))) + 1) throw new Error("Invalid texture mip count.");
4425
+ let bytes = 0;
4426
+ 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;
4427
+ return integer(bytes, "Texture allocation");
4428
+ }
4429
+ function createGpuAllocationTracker(rawDevice, { budgetBytes, storageUsage = globalThis.GPUBufferUsage?.STORAGE ?? 128 } = {}) {
4430
+ if (budgetBytes !== void 0) integer(budgetBytes, "GPU memory budget");
4431
+ const records = /* @__PURE__ */ new Map();
4432
+ let allocatedBytes = 0, peakAllocatedBytes = 0;
4433
+ function allocate(kind, descriptor, bytes, create) {
4434
+ integer(bytes, "GPU allocation", 0);
4435
+ const total = integer(allocatedBytes + bytes, "GPU allocation total", 0);
4436
+ if (budgetBytes !== void 0 && total > budgetBytes) throw new Error(`GPU memory budget exceeded: ${total} requested bytes, budget ${budgetBytes}. Reduce quality or scene resources.`);
4437
+ const resource = create(descriptor), originalDestroy = resource.destroy.bind(resource);
4438
+ const release = () => {
4439
+ if (!records.has(resource)) return;
4440
+ originalDestroy();
4441
+ records.delete(resource);
4442
+ allocatedBytes -= bytes;
4443
+ };
4444
+ try {
4445
+ Object.defineProperty(resource, "destroy", { value: release, configurable: true });
4446
+ } catch (error) {
4447
+ originalDestroy();
4448
+ throw error;
4449
+ }
4450
+ records.set(resource, { kind, label: descriptor.label ?? "", bytes, release });
4451
+ allocatedBytes = total;
4452
+ peakAllocatedBytes = Math.max(total, peakAllocatedBytes);
4453
+ return resource;
4454
+ }
4455
+ const destroy = () => {
4456
+ for (const record of [...records.values()]) record.release();
4457
+ };
4458
+ const methods = {
4459
+ createBuffer(descriptor) {
4460
+ const bytes = integer(descriptor.size, "Buffer size", 0);
4461
+ within(bytes, rawDevice.limits?.maxBufferSize, "buffer size");
4462
+ if ((descriptor.usage ?? 0) & storageUsage) within(bytes, rawDevice.limits?.maxStorageBufferBindingSize, "storage binding size");
4463
+ return allocate("buffer", descriptor, bytes, (d) => rawDevice.createBuffer(d));
4464
+ },
4465
+ createTexture(descriptor) {
4466
+ return allocate("texture", descriptor, textureBytes(descriptor, rawDevice.limits ?? {}), (d) => rawDevice.createTexture(d));
4467
+ },
4468
+ createQuerySet(descriptor) {
4469
+ return allocate("query-set", descriptor, integer(descriptor.count, "Query count") * 8, (d) => rawDevice.createQuerySet(d));
4470
+ },
4471
+ destroy() {
4472
+ destroy();
4473
+ rawDevice.destroy();
4474
+ }
4475
+ };
4476
+ const device = new Proxy(rawDevice, { get(target, key) {
4477
+ if (Object.hasOwn(methods, key)) return methods[key];
4478
+ const value = Reflect.get(target, key, target);
4479
+ return typeof value === "function" ? value.bind(target) : value;
4480
+ } });
4481
+ return Object.freeze({ device, destroy, snapshot: () => Object.freeze({
4482
+ accounting: "requested-resource-bytes",
4483
+ budgetBytes: budgetBytes ?? null,
4484
+ allocatedBytes,
4485
+ peakAllocatedBytes,
4486
+ resources: Object.freeze([...records.values()].map(({ kind, label, bytes }) => Object.freeze({ kind, label, bytes })))
4487
+ }) });
4488
+ }
4489
+ function openGpuAllocationErrorScopes(device) {
4490
+ if (typeof device.pushErrorScope !== "function" || typeof device.popErrorScope !== "function") return async () => {
4491
+ };
4492
+ let count = 0;
4493
+ try {
4494
+ device.pushErrorScope("out-of-memory");
4495
+ count++;
4496
+ device.pushErrorScope("validation");
4497
+ count++;
4498
+ } catch (error) {
4499
+ while (count-- > 0) Promise.resolve(device.popErrorScope()).catch(() => {
4500
+ });
4501
+ throw error;
4502
+ }
4503
+ return async () => {
4504
+ const completions = [];
4505
+ while (count > 0) {
4506
+ count--;
4507
+ try {
4508
+ completions.push(Promise.resolve(device.popErrorScope()));
4509
+ } catch (error) {
4510
+ completions.push(Promise.reject(error));
4586
4511
  }
4587
4512
  }
4588
- }
4589
- return true;
4513
+ if (!completions.length) return;
4514
+ let timer;
4515
+ try {
4516
+ const errors = await Promise.race([Promise.all(completions), new Promise((_, reject) => {
4517
+ timer = setTimeout(() => reject(new Error("GPU allocation validation timed out.")), 1e4);
4518
+ })]);
4519
+ const error = errors.find(Boolean);
4520
+ if (error) throw new Error(`GPU allocation failed: ${error.message ?? String(error)}`);
4521
+ } finally {
4522
+ clearTimeout(timer);
4523
+ }
4524
+ };
4590
4525
  }
4591
- function createAtlasTextureResource(device, constants, atlas, label) {
4592
- const upload = createRgba8TextureUpload(atlas);
4593
- const texture = device.createTexture({
4594
- label,
4595
- size: { width: upload.width, height: upload.height },
4596
- format: "rgba8unorm",
4597
- usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
4598
- });
4599
- device.queue.writeTexture(
4600
- { texture },
4601
- upload.bytes,
4602
- { bytesPerRow: upload.bytesPerRow, rowsPerImage: upload.height },
4603
- { width: upload.width, height: upload.height, depthOrArrayLayers: 1 }
4526
+
4527
+ // src/wavefront-sampling.js
4528
+ function buildOrthonormalBasis(normal) {
4529
+ const tangentFallback = Math.abs(normal[1]) < 0.999 ? [0, 1, 0] : [1, 0, 0];
4530
+ const tangent = normalize(cross2(tangentFallback, normal), [1, 0, 0]);
4531
+ const bitangent = normalize(cross2(normal, tangent), [0, 0, 1]);
4532
+ return { tangent, bitangent };
4533
+ }
4534
+ function localToWorld(local, normal) {
4535
+ const basis = buildOrthonormalBasis(normal);
4536
+ return normalize(
4537
+ add(
4538
+ add(scale(basis.tangent, local[0]), scale(basis.bitangent, local[1])),
4539
+ scale(normal, local[2])
4540
+ ),
4541
+ normal
4604
4542
  );
4605
- return Object.freeze({
4606
- texture,
4607
- view: texture.createView(),
4608
- ownsTexture: true
4609
- });
4610
4543
  }
4611
-
4612
- // src/wavefront-runtime-support.js
4613
- var WEBGPU_DEFAULT_MAX_STORAGE_BUFFER_BINDING_SIZE = 128 * 1024 * 1024;
4614
- var WEBGPU_DEFAULT_MAX_BUFFER_SIZE = 256 * 1024 * 1024;
4615
- function readRequestedDeviceLimit(source, name) {
4616
- const rawValue = source?.[name];
4617
- if (rawValue === void 0 || rawValue === null) {
4618
- return 0;
4619
- }
4620
- const value = Number(rawValue);
4621
- if (!Number.isSafeInteger(value) || value < 0) {
4622
- throw new Error(`${name} must be a non-negative safe integer.`);
4623
- }
4624
- return value;
4544
+ function radicalInverseVdc(bits) {
4545
+ let value = bits >>> 0;
4546
+ value = (value << 16 | value >>> 16) >>> 0;
4547
+ value = ((value & 1431655765) << 1 | (value & 2863311530) >>> 1) >>> 0;
4548
+ value = ((value & 858993459) << 2 | (value & 3435973836) >>> 2) >>> 0;
4549
+ value = ((value & 252645135) << 4 | (value & 4042322160) >>> 4) >>> 0;
4550
+ value = ((value & 16711935) << 8 | (value & 4278255360) >>> 8) >>> 0;
4551
+ return value * 23283064365386963e-26;
4625
4552
  }
4626
- function assertAdapterSupportsRequiredLimit(adapter, name, requiredValue) {
4627
- const availableValue = Number(adapter?.limits?.[name]);
4628
- if (!Number.isFinite(availableValue)) {
4629
- throw new Error(
4630
- `Wavefront scene buffers require ${name}>=${requiredValue} bytes, but this adapter does not expose that limit.`
4631
- );
4632
- }
4633
- if (availableValue < requiredValue) {
4634
- throw new Error(
4635
- `Wavefront scene buffers require ${name}>=${requiredValue} bytes, but this adapter exposes ${availableValue} bytes.`
4553
+ function hammersley(index, count) {
4554
+ return [index / Math.max(count, 1), radicalInverseVdc(index)];
4555
+ }
4556
+ function importanceSampleGgx(sample, roughness, normal) {
4557
+ const alpha = Math.max(roughness * roughness, 1e-4);
4558
+ const phi = 2 * Math.PI * sample[0];
4559
+ const cosTheta = Math.sqrt((1 - sample[1]) / (1 + (alpha * alpha - 1) * sample[1]));
4560
+ const sinTheta = Math.sqrt(Math.max(0, 1 - cosTheta * cosTheta));
4561
+ const halfVector = localToWorld(
4562
+ [Math.cos(phi) * sinTheta, Math.sin(phi) * sinTheta, cosTheta],
4563
+ normal
4564
+ );
4565
+ return normalize(halfVector, normal);
4566
+ }
4567
+ function geometrySchlickGgx(nDotV, roughness) {
4568
+ const k = (roughness + 1) * (roughness + 1) / 8;
4569
+ return nDotV / Math.max(nDotV * (1 - k) + k, 1e-6);
4570
+ }
4571
+ function geometrySmith(nDotV, nDotL, roughness) {
4572
+ return geometrySchlickGgx(nDotV, roughness) * geometrySchlickGgx(nDotL, roughness);
4573
+ }
4574
+ function integrateBrdfSample(nDotV, roughness, sampleCount) {
4575
+ const viewDirection = [Math.sqrt(Math.max(0, 1 - nDotV * nDotV)), 0, nDotV];
4576
+ const normal = [0, 0, 1];
4577
+ let scaleTerm = 0;
4578
+ let biasTerm = 0;
4579
+ for (let index = 0; index < sampleCount; index += 1) {
4580
+ const xi = hammersley(index, sampleCount);
4581
+ const halfVector = importanceSampleGgx(xi, roughness, normal);
4582
+ const vDotH = Math.max(dot(viewDirection, halfVector), 0);
4583
+ const lightDirection = normalize(
4584
+ subtract(scale(halfVector, 2 * vDotH), viewDirection),
4585
+ normal
4636
4586
  );
4587
+ const nDotL = Math.max(lightDirection[2], 0);
4588
+ const nDotH = Math.max(halfVector[2], 0);
4589
+ if (nDotL <= 0 || nDotH <= 0 || vDotH <= 0) {
4590
+ continue;
4591
+ }
4592
+ const geometry = geometrySmith(nDotV, nDotL, roughness);
4593
+ const visibility = geometry * vDotH / Math.max(nDotH * nDotV, 1e-6);
4594
+ const fresnel2 = (1 - vDotH) ** 5;
4595
+ scaleTerm += (1 - fresnel2) * visibility;
4596
+ biasTerm += fresnel2 * visibility;
4637
4597
  }
4598
+ return [scaleTerm / sampleCount, biasTerm / sampleCount];
4638
4599
  }
4639
- function getGpuUsageConstants() {
4640
- if (typeof GPUBufferUsage === "undefined" || typeof GPUTextureUsage === "undefined" || typeof GPUShaderStage === "undefined") {
4641
- throw new Error("WebGPU runtime unavailable. Required GPU constants are missing.");
4600
+
4601
+ // src/wavefront-gpu-resources.js
4602
+ function clampTileSizeForDevice(config, device) {
4603
+ const limit = Number(device?.limits?.maxStorageBufferBindingSize);
4604
+ if (!Number.isFinite(limit) || limit <= 0) {
4605
+ return config.tileSize;
4642
4606
  }
4643
- if (typeof GPUBufferUsage.INDIRECT !== "number") {
4644
- throw new Error("WebGPU runtime unavailable. GPUBufferUsage.INDIRECT is missing.");
4607
+ const maxPixelsByRay = Math.floor(limit / RAY_RECORD_BYTES);
4608
+ const maxPixelsByHit = Math.floor(limit / HIT_RECORD_BYTES);
4609
+ const maxPixelsByPathVertex = Math.floor(
4610
+ limit / ((config.maxDepth + 1) * PATH_VERTEX_RECORD_BYTES)
4611
+ );
4612
+ const maxPixels = Math.max(
4613
+ 256,
4614
+ Math.min(maxPixelsByRay, maxPixelsByHit, maxPixelsByPathVertex)
4615
+ );
4616
+ if (config.tilePixelCapacity <= maxPixels) {
4617
+ return config.tileSize;
4645
4618
  }
4646
- return {
4647
- buffer: GPUBufferUsage,
4648
- texture: GPUTextureUsage,
4649
- shader: GPUShaderStage,
4650
- map: typeof GPUMapMode === "undefined" ? null : GPUMapMode
4651
- };
4619
+ return Math.max(16, Math.floor(Math.sqrt(maxPixels)));
4652
4620
  }
4653
- function resolveCanvas3(canvasOrSelector, documentRef = globalThis.document) {
4654
- if (typeof canvasOrSelector === "string") {
4655
- const resolved = documentRef?.querySelector?.(canvasOrSelector);
4656
- if (!resolved) {
4657
- throw new Error(`Unable to find canvas for selector: ${canvasOrSelector}`);
4658
- }
4659
- return resolved;
4621
+ function createBuffer(device, usage, size, label) {
4622
+ return device.createBuffer({
4623
+ label,
4624
+ size: Math.max(4, size),
4625
+ usage
4626
+ });
4627
+ }
4628
+ function alignTo(value, alignment) {
4629
+ const resolvedAlignment = Math.max(1, alignment);
4630
+ return Math.ceil(value / resolvedAlignment) * resolvedAlignment;
4631
+ }
4632
+ function float32ToFloat16Bits(value) {
4633
+ const floatView = new Float32Array(1);
4634
+ const intView = new Uint32Array(floatView.buffer);
4635
+ floatView[0] = Number.isFinite(value) ? value : 0;
4636
+ const x = intView[0];
4637
+ const sign = x >> 16 & 32768;
4638
+ let mantissa = x & 8388607;
4639
+ let exponent = x >> 23 & 255;
4640
+ if (exponent === 255) {
4641
+ return sign | (mantissa ? 32256 : 31744);
4660
4642
  }
4661
- if (canvasOrSelector?.getContext) {
4662
- return canvasOrSelector;
4643
+ exponent = exponent - 127 + 15;
4644
+ if (exponent >= 31) {
4645
+ return sign | 31744;
4663
4646
  }
4664
- const fallback = documentRef?.querySelector?.("canvas[data-plasius-wavefront-path-tracing]");
4665
- if (!fallback) {
4666
- throw new Error("A canvas is required for WebGPU wavefront path tracing.");
4647
+ if (exponent <= 0) {
4648
+ if (exponent < -10) {
4649
+ return sign;
4650
+ }
4651
+ mantissa = (mantissa | 8388608) >> 1 - exponent;
4652
+ return sign | mantissa + 4096 >> 13;
4667
4653
  }
4668
- return fallback;
4654
+ return sign | exponent << 10 | mantissa + 4096 >> 13;
4669
4655
  }
4670
- async function getPipelineDiagnostics(shaderModule) {
4671
- if (typeof shaderModule?.getCompilationInfo !== "function") {
4672
- return "";
4656
+ function environmentMapIntegerScale(data) {
4657
+ if (data instanceof Uint8Array) {
4658
+ return 1 / 255;
4673
4659
  }
4674
- try {
4675
- const info = await shaderModule.getCompilationInfo();
4676
- const messages = info.messages ?? [];
4677
- if (messages.length === 0) {
4678
- return "";
4679
- }
4680
- return messages.map((message) => {
4681
- const line = message.lineNum ?? "?";
4682
- const column = message.linePos ?? "?";
4683
- return `line ${line}:${column} ${message.message}`;
4684
- }).join("\n");
4685
- } catch {
4686
- return "";
4660
+ if (data instanceof Uint16Array) {
4661
+ return 1 / 65535;
4687
4662
  }
4663
+ return 1;
4688
4664
  }
4689
- async function createComputePipeline(device, shaderModule, layout, entryPoint, label) {
4690
- const descriptor = {
4691
- label,
4692
- layout,
4693
- compute: {
4694
- module: shaderModule,
4695
- entryPoint
4696
- }
4697
- };
4698
- try {
4699
- if (typeof device.createComputePipelineAsync === "function") {
4700
- return await device.createComputePipelineAsync(descriptor);
4701
- }
4702
- return device.createComputePipeline(descriptor);
4703
- } catch (error) {
4704
- const diagnostics = await getPipelineDiagnostics(shaderModule);
4705
- const suffix = diagnostics ? `
4706
- ${diagnostics}` : "";
4707
- throw new Error(`WGSL compilation failed for ${label}: ${error.message}${suffix}`, {
4708
- cause: error
4709
- });
4665
+ function environmentMapHasSamplingData(environmentMap) {
4666
+ if (!environmentMap || !environmentMap.data) {
4667
+ return false;
4668
+ }
4669
+ const width = Math.max(1, environmentMap.width ?? 1);
4670
+ const height = Math.max(1, environmentMap.height ?? 1);
4671
+ return environmentMap.data.length >= width * height * 4;
4672
+ }
4673
+ function createRgba8TextureUpload(source) {
4674
+ const width = Math.max(1, Math.trunc(source.width));
4675
+ const height = Math.max(1, Math.trunc(source.height));
4676
+ const bytesPerRow = alignTo(width * 4, 256);
4677
+ const bytes = new Uint8Array(bytesPerRow * height);
4678
+ const data = source.data instanceof Uint8Array ? source.data : new Uint8Array(source.data);
4679
+ for (let y = 0; y < height; y += 1) {
4680
+ const sourceOffset = y * width * 4;
4681
+ const targetOffset = y * bytesPerRow;
4682
+ bytes.set(data.subarray(sourceOffset, sourceOffset + width * 4), targetOffset);
4683
+ }
4684
+ return Object.freeze({
4685
+ bytes,
4686
+ bytesPerRow,
4687
+ width,
4688
+ height
4689
+ });
4690
+ }
4691
+ function readEnvironmentMapComponent(data, index, fallback, integerScale = 1) {
4692
+ if (!data || index >= data.length) {
4693
+ return fallback;
4710
4694
  }
4695
+ const value = Number(data[index]);
4696
+ return Number.isFinite(value) ? Math.max(0, value) * integerScale : fallback;
4711
4697
  }
4712
- async function assertShaderModuleCompiles(shaderModule, label) {
4713
- if (typeof shaderModule?.getCompilationInfo !== "function") {
4714
- return;
4698
+ function createBrdfLutUploadBytes(size = DEFAULT_BRDF_LUT_SIZE, sampleCount = DEFAULT_BRDF_LUT_SAMPLE_COUNT) {
4699
+ const cacheKey = `${Math.max(1, Math.trunc(size))}:${Math.max(1, Math.trunc(sampleCount))}`;
4700
+ const cached = BRDF_LUT_UPLOAD_CACHE.get(cacheKey);
4701
+ if (cached) {
4702
+ return cached;
4715
4703
  }
4716
- const info = await shaderModule.getCompilationInfo();
4717
- const messages = Array.isArray(info?.messages) ? info.messages : [];
4718
- const errors = messages.filter((message) => message?.type === "error");
4719
- if (errors.length <= 0) {
4720
- return;
4704
+ const width = Math.max(1, Math.trunc(size));
4705
+ const height = Math.max(1, Math.trunc(size));
4706
+ const rowBytes = width * 8;
4707
+ const bytesPerRow = alignTo(rowBytes, 256);
4708
+ const bytes = new Uint8Array(bytesPerRow * height);
4709
+ const view = new DataView(bytes.buffer);
4710
+ for (let y = 0; y < height; y += 1) {
4711
+ const roughness = (y + 0.5) / height;
4712
+ for (let x = 0; x < width; x += 1) {
4713
+ const nDotV = Math.max((x + 0.5) / width, 1e-4);
4714
+ const [scaleTerm, biasTerm] = integrateBrdfSample(nDotV, roughness, sampleCount);
4715
+ const offset = y * bytesPerRow + x * 8;
4716
+ view.setUint16(offset, float32ToFloat16Bits(scaleTerm), true);
4717
+ view.setUint16(offset + 2, float32ToFloat16Bits(biasTerm), true);
4718
+ view.setUint16(offset + 4, float32ToFloat16Bits(0), true);
4719
+ view.setUint16(offset + 6, float32ToFloat16Bits(1), true);
4720
+ }
4721
4721
  }
4722
- const diagnostics = errors.map((message) => {
4723
- const line = Number.isFinite(message.lineNum) ? message.lineNum : "?";
4724
- const column = Number.isFinite(message.linePos) ? message.linePos : "?";
4725
- return `line ${line}:${column} ${message.message}`;
4726
- }).join("\n");
4727
- throw new Error(`WGSL compilation preflight failed for ${label}:
4728
- ${diagnostics}`);
4722
+ const upload = Object.freeze({ bytes, bytesPerRow, width, height });
4723
+ BRDF_LUT_UPLOAD_CACHE.set(cacheKey, upload);
4724
+ return upload;
4729
4725
  }
4730
- async function createRenderPipeline(device, descriptor) {
4731
- if (typeof device.createRenderPipelineAsync === "function") {
4732
- return device.createRenderPipelineAsync(descriptor);
4726
+ function createLinearEnvironmentPixels(environmentMap, fallbackColor) {
4727
+ const width = Math.max(1, environmentMap.width);
4728
+ const height = Math.max(1, environmentMap.height);
4729
+ const pixels = new Float32Array(width * height * 4);
4730
+ const data = environmentMap.data;
4731
+ const integerScale = environmentMapIntegerScale(data);
4732
+ for (let index = 0; index < width * height; index += 1) {
4733
+ const sourceOffset = index * 4;
4734
+ const targetOffset = index * 4;
4735
+ pixels[targetOffset] = readEnvironmentMapComponent(data, sourceOffset, fallbackColor[0], integerScale);
4736
+ pixels[targetOffset + 1] = readEnvironmentMapComponent(data, sourceOffset + 1, fallbackColor[1], integerScale);
4737
+ pixels[targetOffset + 2] = readEnvironmentMapComponent(data, sourceOffset + 2, fallbackColor[2], integerScale);
4738
+ pixels[targetOffset + 3] = readEnvironmentMapComponent(data, sourceOffset + 3, fallbackColor[3] ?? 1, integerScale);
4733
4739
  }
4734
- return device.createRenderPipeline(descriptor);
4740
+ return pixels;
4735
4741
  }
4736
- function createWavefrontDeviceDescriptor(adapter, options = {}, memory = null) {
4737
- const descriptor = { ...options.deviceDescriptor ?? {} };
4738
- const descriptorRequiredLimits = { ...descriptor.requiredLimits ?? {} };
4739
- const requiredLimits = {
4740
- ...descriptorRequiredLimits,
4741
- ...options.requiredLimits ?? {}
4742
+ function environmentUvToDirection(u, v, rotationRadians = 0) {
4743
+ const angle = (u - rotationRadians / (2 * Math.PI) - 0.5) * 2 * Math.PI;
4744
+ const theta = v * Math.PI;
4745
+ const sinTheta = Math.sin(theta);
4746
+ return [
4747
+ Math.cos(angle) * sinTheta,
4748
+ Math.cos(theta),
4749
+ Math.sin(angle) * sinTheta
4750
+ ];
4751
+ }
4752
+ function sampleEnvironmentPixelsBilinear(pixels, width, height, u, v) {
4753
+ const wrappedU = (u % 1 + 1) % 1;
4754
+ const clampedV = clamp2(v, 0, 1);
4755
+ const x = wrappedU * width - 0.5;
4756
+ const y = clampedV * height - 0.5;
4757
+ const x0 = (Math.floor(x) % width + width) % width;
4758
+ const y0 = clamp2(Math.floor(y), 0, height - 1);
4759
+ const x1 = (x0 + 1) % width;
4760
+ const y1 = clamp2(y0 + 1, 0, height - 1);
4761
+ const tx = x - Math.floor(x);
4762
+ const ty = y - Math.floor(y);
4763
+ const read = (px, py) => {
4764
+ const offset = (py * width + px) * 4;
4765
+ return [pixels[offset], pixels[offset + 1], pixels[offset + 2], pixels[offset + 3]];
4742
4766
  };
4743
- const exposedStorageBufferLimit = Number(adapter?.limits?.maxStorageBuffersPerShaderStage);
4744
- if (Number.isFinite(exposedStorageBufferLimit)) {
4745
- if (exposedStorageBufferLimit < TRACE_STORAGE_BUFFER_BINDINGS) {
4746
- throw new Error(
4747
- `Wavefront mesh tracing requires maxStorageBuffersPerShaderStage>=${TRACE_STORAGE_BUFFER_BINDINGS}, but this adapter exposes ${exposedStorageBufferLimit}.`
4748
- );
4767
+ const a = read(x0, y0);
4768
+ const b = read(x1, y0);
4769
+ const c = read(x0, y1);
4770
+ const d = read(x1, y1);
4771
+ const mixPair = (first, second, factor) => first * (1 - factor) + second * factor;
4772
+ return [
4773
+ mixPair(mixPair(a[0], b[0], tx), mixPair(c[0], d[0], tx), ty),
4774
+ mixPair(mixPair(a[1], b[1], tx), mixPair(c[1], d[1], tx), ty),
4775
+ mixPair(mixPair(a[2], b[2], tx), mixPair(c[2], d[2], tx), ty),
4776
+ mixPair(mixPair(a[3], b[3], tx), mixPair(c[3], d[3], tx), ty)
4777
+ ];
4778
+ }
4779
+ function directionToEnvironmentUv(direction, rotationRadians = 0) {
4780
+ const unitDirection = normalize(direction, [0, 1, 0]);
4781
+ const rotationTurns = rotationRadians / (2 * Math.PI);
4782
+ const u = ((Math.atan2(unitDirection[2], unitDirection[0]) / (2 * Math.PI) + 0.5 + rotationTurns) % 1 + 1) % 1;
4783
+ const v = Math.acos(clamp2(unitDirection[1], -1, 1)) / Math.PI;
4784
+ return [u, clamp2(v, 0, 1)];
4785
+ }
4786
+ function sampleEnvironmentRadiance(pixels, width, height, direction, rotationRadians = 0) {
4787
+ const [u, v] = directionToEnvironmentUv(direction, rotationRadians);
4788
+ return sampleEnvironmentPixelsBilinear(pixels, width, height, u, v);
4789
+ }
4790
+ function createFloat16RgbaUploadFromLevels(levels) {
4791
+ return levels.map((level) => {
4792
+ const rowBytes = level.width * 8;
4793
+ const bytesPerRow = alignTo(rowBytes, 256);
4794
+ const bytes = new Uint8Array(bytesPerRow * level.height);
4795
+ const view = new DataView(bytes.buffer);
4796
+ for (let y = 0; y < level.height; y += 1) {
4797
+ for (let x = 0; x < level.width; x += 1) {
4798
+ const sourceOffset = (y * level.width + x) * 4;
4799
+ const targetOffset = y * bytesPerRow + x * 8;
4800
+ view.setUint16(targetOffset, float32ToFloat16Bits(level.data[sourceOffset]), true);
4801
+ view.setUint16(targetOffset + 2, float32ToFloat16Bits(level.data[sourceOffset + 1]), true);
4802
+ view.setUint16(targetOffset + 4, float32ToFloat16Bits(level.data[sourceOffset + 2]), true);
4803
+ view.setUint16(targetOffset + 6, float32ToFloat16Bits(level.data[sourceOffset + 3]), true);
4804
+ }
4749
4805
  }
4750
- requiredLimits.maxStorageBuffersPerShaderStage = Math.max(
4751
- Number(requiredLimits.maxStorageBuffersPerShaderStage ?? 0),
4752
- TRACE_STORAGE_BUFFER_BINDINGS
4753
- );
4754
- }
4755
- const exposedSampledTextureLimit = Number(adapter?.limits?.maxSampledTexturesPerShaderStage);
4756
- if (Number.isFinite(exposedSampledTextureLimit)) {
4757
- if (exposedSampledTextureLimit < TRACE_SAMPLED_TEXTURE_BINDINGS) {
4758
- throw new Error(
4759
- `Wavefront material tracing requires maxSampledTexturesPerShaderStage>=${TRACE_SAMPLED_TEXTURE_BINDINGS}, but this adapter exposes ${exposedSampledTextureLimit}.`
4760
- );
4806
+ return Object.freeze({ bytes, bytesPerRow, width: level.width, height: level.height });
4807
+ });
4808
+ }
4809
+ function createPrefilteredEnvironmentLevels(environmentMap, fallbackColor) {
4810
+ const sourcePixels = createLinearEnvironmentPixels(environmentMap, fallbackColor);
4811
+ const sourceWidth = Math.max(1, environmentMap.width);
4812
+ const sourceHeight = Math.max(1, environmentMap.height);
4813
+ const mipLevelCount = Math.max(1, Math.floor(Math.log2(Math.max(sourceWidth, sourceHeight))) + 1);
4814
+ const levels = [
4815
+ Object.freeze({
4816
+ width: sourceWidth,
4817
+ height: sourceHeight,
4818
+ data: sourcePixels
4819
+ })
4820
+ ];
4821
+ for (let mipLevel = 1; mipLevel < mipLevelCount; mipLevel += 1) {
4822
+ const width = Math.max(1, sourceWidth >> mipLevel);
4823
+ const height = Math.max(1, sourceHeight >> mipLevel);
4824
+ const roughness = mipLevelCount <= 1 ? 0 : mipLevel / (mipLevelCount - 1);
4825
+ const data = new Float32Array(width * height * 4);
4826
+ const sampleCount = roughness < 0.25 ? 64 : roughness < 0.6 ? 96 : 128;
4827
+ for (let y = 0; y < height; y += 1) {
4828
+ for (let x = 0; x < width; x += 1) {
4829
+ const direction = environmentUvToDirection((x + 0.5) / width, (y + 0.5) / height, environmentMap.rotationRadians);
4830
+ const normal = normalize(direction, [0, 1, 0]);
4831
+ const viewDirection = normal;
4832
+ let totalWeight = 0;
4833
+ const accum = [0, 0, 0];
4834
+ for (let sampleIndex = 0; sampleIndex < sampleCount; sampleIndex += 1) {
4835
+ const xi = hammersley(sampleIndex, sampleCount);
4836
+ const halfVector = importanceSampleGgx(xi, roughness, normal);
4837
+ const viewDotHalf = Math.max(dot(viewDirection, halfVector), 0);
4838
+ const lightDirection = normalize(
4839
+ subtract(scale(halfVector, 2 * viewDotHalf), viewDirection),
4840
+ normal
4841
+ );
4842
+ const nDotL = Math.max(dot(normal, lightDirection), 0);
4843
+ if (nDotL <= 1e-6) {
4844
+ continue;
4845
+ }
4846
+ const radiance = sampleEnvironmentRadiance(
4847
+ sourcePixels,
4848
+ sourceWidth,
4849
+ sourceHeight,
4850
+ lightDirection,
4851
+ environmentMap.rotationRadians
4852
+ );
4853
+ accum[0] += radiance[0] * nDotL;
4854
+ accum[1] += radiance[1] * nDotL;
4855
+ accum[2] += radiance[2] * nDotL;
4856
+ totalWeight += nDotL;
4857
+ }
4858
+ const offset = (y * width + x) * 4;
4859
+ data[offset] = accum[0] / Math.max(totalWeight, 1e-6);
4860
+ data[offset + 1] = accum[1] / Math.max(totalWeight, 1e-6);
4861
+ data[offset + 2] = accum[2] / Math.max(totalWeight, 1e-6);
4862
+ data[offset + 3] = 1;
4863
+ }
4761
4864
  }
4762
- requiredLimits.maxSampledTexturesPerShaderStage = Math.max(
4763
- Number(requiredLimits.maxSampledTexturesPerShaderStage ?? 0),
4764
- TRACE_SAMPLED_TEXTURE_BINDINGS
4765
- );
4766
- }
4767
- const sceneStorageBufferBindingBytes = Number(
4768
- memory?.sceneStorageBufferBindingBytes ?? 0
4769
- );
4770
- const requestedStorageBufferBindingSize = Math.max(
4771
- readRequestedDeviceLimit(descriptorRequiredLimits, "maxStorageBufferBindingSize"),
4772
- readRequestedDeviceLimit(options.requiredLimits, "maxStorageBufferBindingSize"),
4773
- sceneStorageBufferBindingBytes > WEBGPU_DEFAULT_MAX_STORAGE_BUFFER_BINDING_SIZE ? sceneStorageBufferBindingBytes : 0
4774
- );
4775
- if (requestedStorageBufferBindingSize > 0) {
4776
- assertAdapterSupportsRequiredLimit(
4777
- adapter,
4778
- "maxStorageBufferBindingSize",
4779
- requestedStorageBufferBindingSize
4780
- );
4781
- requiredLimits.maxStorageBufferBindingSize = requestedStorageBufferBindingSize;
4782
- }
4783
- const requestedMaxBufferSize = Math.max(
4784
- readRequestedDeviceLimit(descriptorRequiredLimits, "maxBufferSize"),
4785
- readRequestedDeviceLimit(options.requiredLimits, "maxBufferSize"),
4786
- sceneStorageBufferBindingBytes > WEBGPU_DEFAULT_MAX_BUFFER_SIZE ? sceneStorageBufferBindingBytes : 0,
4787
- requestedStorageBufferBindingSize > WEBGPU_DEFAULT_MAX_BUFFER_SIZE ? requestedStorageBufferBindingSize : 0
4788
- );
4789
- if (requestedMaxBufferSize > 0) {
4790
- assertAdapterSupportsRequiredLimit(adapter, "maxBufferSize", requestedMaxBufferSize);
4791
- requiredLimits.maxBufferSize = requestedMaxBufferSize;
4865
+ levels.push(Object.freeze({ width, height, data }));
4792
4866
  }
4793
- const requiredFeatures = Array.from(descriptor.requiredFeatures ?? []);
4794
- const adapterFeatures = adapter?.features;
4795
- const timestampQuerySupported = typeof adapterFeatures?.has === "function" ? adapterFeatures.has("timestamp-query") : adapterFeatures && typeof adapterFeatures[Symbol.iterator] === "function" ? Array.from(adapterFeatures).includes("timestamp-query") : false;
4796
- if (options.gpuTimestamps !== false && timestampQuerySupported && !requiredFeatures.includes("timestamp-query")) {
4797
- requiredFeatures.push("timestamp-query");
4867
+ return Object.freeze({
4868
+ levels,
4869
+ mipLevelCount,
4870
+ width: sourceWidth,
4871
+ height: sourceHeight
4872
+ });
4873
+ }
4874
+ function createEnvironmentSamplingTables(environmentMap, fallbackColor) {
4875
+ if (!environmentMapHasSamplingData(environmentMap)) {
4876
+ return Object.freeze({
4877
+ width: 1,
4878
+ height: 1,
4879
+ pdf: new Float32Array([1]),
4880
+ marginalCdf: new Float32Array([1]),
4881
+ conditionalCdf: new Float32Array([1]),
4882
+ hasImportanceData: false
4883
+ });
4798
4884
  }
4799
- if (requiredFeatures.length > 0) {
4800
- descriptor.requiredFeatures = requiredFeatures;
4885
+ const pixels = createLinearEnvironmentPixels(environmentMap, fallbackColor);
4886
+ const width = Math.max(1, environmentMap.width);
4887
+ const height = Math.max(1, environmentMap.height);
4888
+ const pdf = new Float32Array(width * height);
4889
+ const marginalCdf = new Float32Array(height);
4890
+ const conditionalCdf = new Float32Array(width * height);
4891
+ const rowSums = new Float32Array(height);
4892
+ let totalWeight = 0;
4893
+ for (let y = 0; y < height; y += 1) {
4894
+ const theta = (y + 0.5) / height * Math.PI;
4895
+ const sinTheta = Math.max(Math.sin(theta), 1e-4);
4896
+ let rowWeight = 0;
4897
+ for (let x = 0; x < width; x += 1) {
4898
+ const offset = (y * width + x) * 4;
4899
+ const luminance = pixels[offset] * 0.2126 + pixels[offset + 1] * 0.7152 + pixels[offset + 2] * 0.0722;
4900
+ const weight = Math.max(luminance * sinTheta, 1e-6);
4901
+ pdf[y * width + x] = weight;
4902
+ rowWeight += weight;
4903
+ conditionalCdf[y * width + x] = rowWeight;
4904
+ }
4905
+ rowSums[y] = rowWeight;
4906
+ totalWeight += rowWeight;
4907
+ if (rowWeight > 0) {
4908
+ for (let x = 0; x < width; x += 1) {
4909
+ conditionalCdf[y * width + x] /= rowWeight;
4910
+ }
4911
+ } else {
4912
+ for (let x = 0; x < width; x += 1) {
4913
+ conditionalCdf[y * width + x] = (x + 1) / width;
4914
+ }
4915
+ }
4916
+ marginalCdf[y] = totalWeight;
4801
4917
  }
4802
- if (Object.keys(requiredLimits).length > 0) {
4803
- descriptor.requiredLimits = {
4804
- ...descriptor.requiredLimits ?? {},
4805
- ...requiredLimits
4806
- };
4918
+ for (let y = 0; y < height; y += 1) {
4919
+ marginalCdf[y] /= Math.max(totalWeight, 1e-6);
4807
4920
  }
4808
- return Object.keys(descriptor).length > 0 ? descriptor : void 0;
4921
+ for (let index = 0; index < pdf.length; index += 1) {
4922
+ pdf[index] /= Math.max(totalWeight, 1e-6);
4923
+ }
4924
+ return Object.freeze({
4925
+ width,
4926
+ height,
4927
+ pdf,
4928
+ marginalCdf,
4929
+ conditionalCdf,
4930
+ hasImportanceData: true
4931
+ });
4809
4932
  }
4810
- function readGpuLimit(adapter, device, name) {
4811
- const adapterValue = Number(adapter?.limits?.[name]);
4812
- if (Number.isFinite(adapterValue)) {
4813
- return adapterValue;
4933
+ function createEnvironmentMapResource(device, constants, environmentMap, fallbackColor) {
4934
+ if (environmentMap.view) {
4935
+ return Object.freeze({
4936
+ view: environmentMap.view,
4937
+ sampler: environmentMap.sampler ?? device.createSampler({
4938
+ label: "plasius.wavefront.environmentMapSampler",
4939
+ addressModeU: "repeat",
4940
+ addressModeV: "clamp-to-edge",
4941
+ magFilter: "linear",
4942
+ minFilter: "linear",
4943
+ mipmapFilter: "linear"
4944
+ }),
4945
+ texture: null,
4946
+ ownsTexture: false,
4947
+ width: Math.max(1, environmentMap.width),
4948
+ height: Math.max(1, environmentMap.height),
4949
+ mipLevelCount: Math.max(1, environmentMap.mipLevelCount ?? 1)
4950
+ });
4814
4951
  }
4815
- const deviceValue = Number(device?.limits?.[name]);
4816
- return Number.isFinite(deviceValue) ? deviceValue : null;
4952
+ if (environmentMap.texture && typeof environmentMap.texture.createView === "function") {
4953
+ return Object.freeze({
4954
+ view: environmentMap.texture.createView(),
4955
+ sampler: environmentMap.sampler ?? device.createSampler({
4956
+ label: "plasius.wavefront.environmentMapSampler",
4957
+ addressModeU: "repeat",
4958
+ addressModeV: "clamp-to-edge",
4959
+ magFilter: "linear",
4960
+ minFilter: "linear",
4961
+ mipmapFilter: "linear"
4962
+ }),
4963
+ texture: environmentMap.texture,
4964
+ ownsTexture: false,
4965
+ width: Math.max(1, environmentMap.width),
4966
+ height: Math.max(1, environmentMap.height),
4967
+ mipLevelCount: Math.max(1, environmentMap.mipLevelCount ?? 1)
4968
+ });
4969
+ }
4970
+ const prefiltered = createPrefilteredEnvironmentLevels(environmentMap, fallbackColor);
4971
+ const uploads = createFloat16RgbaUploadFromLevels(prefiltered.levels);
4972
+ const texture = device.createTexture({
4973
+ label: environmentMap.enabled ? "plasius.wavefront.environmentMap" : "plasius.wavefront.environmentMapFallback",
4974
+ size: { width: prefiltered.width, height: prefiltered.height },
4975
+ format: "rgba16float",
4976
+ mipLevelCount: prefiltered.mipLevelCount,
4977
+ usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
4978
+ });
4979
+ uploads.forEach((upload, mipLevel) => {
4980
+ device.queue.writeTexture(
4981
+ { texture, mipLevel },
4982
+ upload.bytes,
4983
+ { bytesPerRow: upload.bytesPerRow, rowsPerImage: upload.height },
4984
+ { width: upload.width, height: upload.height, depthOrArrayLayers: 1 }
4985
+ );
4986
+ });
4987
+ return Object.freeze({
4988
+ view: texture.createView(),
4989
+ sampler: environmentMap.sampler ?? device.createSampler({
4990
+ label: "plasius.wavefront.environmentMapSampler",
4991
+ addressModeU: "repeat",
4992
+ addressModeV: "clamp-to-edge",
4993
+ magFilter: "linear",
4994
+ minFilter: "linear",
4995
+ mipmapFilter: "linear"
4996
+ }),
4997
+ texture,
4998
+ ownsTexture: true,
4999
+ width: prefiltered.width,
5000
+ height: prefiltered.height,
5001
+ mipLevelCount: prefiltered.mipLevelCount
5002
+ });
4817
5003
  }
4818
- function createAdapterInfoSnapshot(adapter) {
4819
- const info = adapter?.info;
4820
- if (!info || typeof info !== "object") {
4821
- return null;
5004
+ function createEnvironmentSamplingTextureResource(device, constants, environmentMap, fallbackColor) {
5005
+ const tables = createEnvironmentSamplingTables(environmentMap, fallbackColor);
5006
+ const rowBytes = tables.width * 8;
5007
+ const bytesPerRow = alignTo(rowBytes, 256);
5008
+ const bytes = new Uint8Array(bytesPerRow * tables.height);
5009
+ const view = new DataView(bytes.buffer);
5010
+ for (let y = 0; y < tables.height; y += 1) {
5011
+ for (let x = 0; x < tables.width; x += 1) {
5012
+ const probability = tables.pdf[y * tables.width + x];
5013
+ const conditional = tables.conditionalCdf[y * tables.width + x];
5014
+ const marginal = tables.marginalCdf[y];
5015
+ const offset = y * bytesPerRow + x * 8;
5016
+ view.setUint16(offset, float32ToFloat16Bits(probability), true);
5017
+ view.setUint16(offset + 2, float32ToFloat16Bits(conditional), true);
5018
+ view.setUint16(offset + 4, float32ToFloat16Bits(marginal), true);
5019
+ view.setUint16(offset + 6, float32ToFloat16Bits(1), true);
5020
+ }
4822
5021
  }
5022
+ const texture = device.createTexture({
5023
+ label: "plasius.wavefront.environmentSampling",
5024
+ size: { width: tables.width, height: tables.height },
5025
+ format: "rgba16float",
5026
+ usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
5027
+ });
5028
+ device.queue.writeTexture(
5029
+ { texture },
5030
+ bytes,
5031
+ { bytesPerRow, rowsPerImage: tables.height },
5032
+ { width: tables.width, height: tables.height, depthOrArrayLayers: 1 }
5033
+ );
4823
5034
  return Object.freeze({
4824
- vendor: typeof info.vendor === "string" ? info.vendor : "",
4825
- architecture: typeof info.architecture === "string" ? info.architecture : "",
4826
- device: typeof info.device === "string" ? info.device : "",
4827
- description: typeof info.description === "string" ? info.description : ""
5035
+ view: texture.createView(),
5036
+ texture,
5037
+ ownsTexture: true,
5038
+ hasImportanceData: tables.hasImportanceData
4828
5039
  });
4829
5040
  }
4830
- function createGpuAdapterParallelismDiagnostics(adapter, device) {
5041
+ function createBrdfLutResource(device, constants, size = DEFAULT_BRDF_LUT_SIZE) {
5042
+ const upload = createBrdfLutUploadBytes(size);
5043
+ const texture = device.createTexture({
5044
+ label: "plasius.wavefront.brdfLut",
5045
+ size: { width: upload.width, height: upload.height },
5046
+ format: "rgba16float",
5047
+ usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
5048
+ });
5049
+ device.queue.writeTexture(
5050
+ { texture },
5051
+ upload.bytes,
5052
+ { bytesPerRow: upload.bytesPerRow, rowsPerImage: upload.height },
5053
+ { width: upload.width, height: upload.height, depthOrArrayLayers: 1 }
5054
+ );
4831
5055
  return Object.freeze({
4832
- physicalCoreCount: null,
4833
- physicalCoreCountAvailable: false,
4834
- physicalCoreCountUnavailableReason: "WebGPU does not expose physical GPU core counts.",
4835
- adapterInfo: createAdapterInfoSnapshot(adapter),
4836
- adapterLimits: Object.freeze({
4837
- maxComputeInvocationsPerWorkgroup: readGpuLimit(adapter, device, "maxComputeInvocationsPerWorkgroup"),
4838
- maxComputeWorkgroupSizeX: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeX"),
4839
- maxComputeWorkgroupSizeY: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeY"),
4840
- maxComputeWorkgroupSizeZ: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeZ"),
4841
- maxComputeWorkgroupsPerDimension: readGpuLimit(adapter, device, "maxComputeWorkgroupsPerDimension"),
4842
- maxStorageBuffersPerShaderStage: readGpuLimit(adapter, device, "maxStorageBuffersPerShaderStage"),
4843
- maxStorageBufferBindingSize: readGpuLimit(adapter, device, "maxStorageBufferBindingSize"),
4844
- maxBufferSize: readGpuLimit(adapter, device, "maxBufferSize")
5056
+ view: texture.createView(),
5057
+ sampler: device.createSampler({
5058
+ label: "plasius.wavefront.brdfLutSampler",
5059
+ addressModeU: "clamp-to-edge",
5060
+ addressModeV: "clamp-to-edge",
5061
+ magFilter: "linear",
5062
+ minFilter: "linear"
4845
5063
  }),
4846
- configuredWorkgroupSize: WORKGROUP_SIZE
5064
+ texture,
5065
+ ownsTexture: true,
5066
+ width: upload.width,
5067
+ height: upload.height
4847
5068
  });
4848
5069
  }
4849
- function createEnvironmentMapSnapshot(environmentMap) {
4850
- return Object.freeze({
4851
- enabled: environmentMap.enabled,
4852
- width: environmentMap.width,
4853
- height: environmentMap.height,
4854
- mipLevelCount: environmentMap.mipLevelCount ?? 1,
4855
- projection: environmentMap.projection,
4856
- intensity: environmentMap.intensity,
4857
- rotationRadians: environmentMap.rotationRadians,
4858
- ambientStrength: environmentMap.ambientStrength,
4859
- hasImportanceData: environmentMap.hasImportanceData === true
5070
+ function createMediumTextureResource(device, constants, mediums) {
5071
+ const normalized = Array.isArray(mediums) && mediums.length > 0 ? mediums : [{ id: 0 }];
5072
+ const width = Math.max(
5073
+ 1,
5074
+ normalized.reduce((maximum, medium) => Math.max(maximum, medium.id ?? 0), 0) + 1
5075
+ );
5076
+ const level = {
5077
+ width,
5078
+ height: MEDIUM_TABLE_ROWS,
5079
+ data: new Float32Array(width * MEDIUM_TABLE_ROWS * 4)
5080
+ };
5081
+ for (const medium of normalized) {
5082
+ const mediumId = Math.max(0, Math.trunc(Number(medium.id) || 0));
5083
+ const absorptionOffset = mediumId * 4;
5084
+ level.data[absorptionOffset] = Math.max(0, medium.absorption?.[0] ?? 0);
5085
+ level.data[absorptionOffset + 1] = Math.max(0, medium.absorption?.[1] ?? 0);
5086
+ level.data[absorptionOffset + 2] = Math.max(0, medium.absorption?.[2] ?? 0);
5087
+ level.data[absorptionOffset + 3] = Math.max(0, medium.phaseModel ?? 0);
5088
+ const scatteringOffset = (width + mediumId) * 4;
5089
+ level.data[scatteringOffset] = Math.max(0, medium.scattering?.[0] ?? 0);
5090
+ level.data[scatteringOffset + 1] = Math.max(0, medium.scattering?.[1] ?? 0);
5091
+ level.data[scatteringOffset + 2] = Math.max(0, medium.scattering?.[2] ?? 0);
5092
+ level.data[scatteringOffset + 3] = Math.max(0, medium.density ?? 0);
5093
+ }
5094
+ const upload = createFloat16RgbaUploadFromLevels([level])[0];
5095
+ const texture = device.createTexture({
5096
+ label: "plasius.wavefront.mediumTable",
5097
+ size: { width, height: MEDIUM_TABLE_ROWS },
5098
+ format: "rgba16float",
5099
+ usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
5100
+ });
5101
+ device.queue.writeTexture(
5102
+ { texture },
5103
+ upload.bytes,
5104
+ { bytesPerRow: upload.bytesPerRow, rowsPerImage: upload.height },
5105
+ { width, height: MEDIUM_TABLE_ROWS, depthOrArrayLayers: 1 }
5106
+ );
5107
+ return Object.freeze({
5108
+ texture,
5109
+ view: texture.createView(),
5110
+ ownsTexture: true,
5111
+ count: normalized.length,
5112
+ width
4860
5113
  });
4861
5114
  }
4862
- function nowMs3() {
4863
- if (typeof performance !== "undefined" && typeof performance.now === "function") {
4864
- return performance.now();
5115
+ function mediumTablesEqual(left, right) {
5116
+ const leftMediums = Array.isArray(left) ? left : [];
5117
+ const rightMediums = Array.isArray(right) ? right : [];
5118
+ if (leftMediums.length !== rightMediums.length) {
5119
+ return false;
4865
5120
  }
4866
- return Date.now();
4867
- }
4868
- function estimateAccelerationBuildWaitFactor(config) {
4869
- if (config?.gpuAccelerationBuildRequired !== true) {
4870
- return 1;
5121
+ for (let index = 0; index < leftMediums.length; index += 1) {
5122
+ const leftMedium = leftMediums[index];
5123
+ const rightMedium = rightMediums[index];
5124
+ if ((leftMedium?.id ?? 0) !== (rightMedium?.id ?? 0)) {
5125
+ return false;
5126
+ }
5127
+ if ((leftMedium?.phaseModel ?? 0) !== (rightMedium?.phaseModel ?? 0)) {
5128
+ return false;
5129
+ }
5130
+ if ((leftMedium?.density ?? 0) !== (rightMedium?.density ?? 0)) {
5131
+ return false;
5132
+ }
5133
+ for (let component = 0; component < 3; component += 1) {
5134
+ if ((leftMedium?.absorption?.[component] ?? 0) !== (rightMedium?.absorption?.[component] ?? 0)) {
5135
+ return false;
5136
+ }
5137
+ if ((leftMedium?.scattering?.[component] ?? 0) !== (rightMedium?.scattering?.[component] ?? 0)) {
5138
+ return false;
5139
+ }
5140
+ }
4871
5141
  }
4872
- const bvhSortStageCount = Array.isArray(config?.bvhSortStages) ? config.bvhSortStages.length : 0;
4873
- const bvhBuildLevelCount = Array.isArray(config?.bvhBuildLevels) ? config.bvhBuildLevels.length : 0;
4874
- const accelerationStageCount = 2 + bvhSortStageCount + bvhBuildLevelCount;
4875
- return Math.max(1, 1 + accelerationStageCount / 96);
5142
+ return true;
4876
5143
  }
4877
- function estimateSubmittedGpuWorkTiming(config, tileCount, overrideTimeoutMs = null, options = {}) {
4878
- if (Number.isFinite(overrideTimeoutMs)) {
4879
- const overrideMs = Math.max(1, Math.trunc(Number(overrideTimeoutMs)));
4880
- return Object.freeze({
4881
- timeoutMs: overrideMs,
4882
- maxWaitMs: overrideMs
4883
- });
4884
- }
4885
- const samplesPerPixel = Math.max(
4886
- 1,
4887
- Number(config?.renderedSamplesPerPixel ?? config?.samplesPerPixel ?? 1)
4888
- );
4889
- const maxDepth = Math.max(1, Number(config?.maxDepth ?? 1));
4890
- const deferredResolvePasses = config?.deferredPathResolve ? 1 : 0;
4891
- const denoisePasses = config?.denoise ? samplesPerPixel < 4 ? 2 : 1 : 0;
4892
- const tiles = Math.max(1, Number(tileCount ?? 1));
4893
- const estimatedPasses = tiles * (samplesPerPixel * (maxDepth + 1 + deferredResolvePasses) + denoisePasses + 1);
4894
- const triangleCount = Math.max(0, Number(config?.triangleCount ?? 0));
4895
- const geometryFactor = Math.max(1, triangleCount / 131072);
4896
- const includeAccelerationBuild = options.includeAccelerationBuild === true;
4897
- const accelerationFactor = includeAccelerationBuild ? estimateAccelerationBuildWaitFactor(config) : 1;
4898
- const estimatedWindowMs = Math.round(
4899
- (GPU_SUBMITTED_WORK_TIMEOUT_MS + estimatedPasses * 5) * geometryFactor * accelerationFactor
4900
- );
4901
- const timeoutMs = Math.min(
4902
- GPU_MAX_SUBMITTED_WORK_TIMEOUT_MS,
4903
- Math.max(GPU_SUBMITTED_WORK_TIMEOUT_MS, estimatedWindowMs)
4904
- );
4905
- const maxWaitMultiplier = includeAccelerationBuild ? 3 : 2;
4906
- const maxWaitMs = Math.min(
4907
- GPU_MAX_SUBMITTED_WORK_DEADLINE_MS,
4908
- Math.max(timeoutMs, estimatedWindowMs * maxWaitMultiplier)
5144
+ function createAtlasTextureResource(device, constants, atlas, label) {
5145
+ const upload = createRgba8TextureUpload(atlas);
5146
+ const texture = device.createTexture({
5147
+ label,
5148
+ size: { width: upload.width, height: upload.height },
5149
+ format: "rgba8unorm",
5150
+ usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
5151
+ });
5152
+ device.queue.writeTexture(
5153
+ { texture },
5154
+ upload.bytes,
5155
+ { bytesPerRow: upload.bytesPerRow, rowsPerImage: upload.height },
5156
+ { width: upload.width, height: upload.height, depthOrArrayLayers: 1 }
4909
5157
  );
4910
5158
  return Object.freeze({
4911
- timeoutMs,
4912
- maxWaitMs
5159
+ texture,
5160
+ view: texture.createView(),
5161
+ ownsTexture: true
4913
5162
  });
4914
5163
  }
4915
5164
 
5165
+ // src/wavefront-instance-shader.js
5166
+ var WAVEFRONT_INSTANCE_SHADER_WGSL = `
5167
+ struct MeshInstance {
5168
+ world: mat4x4<f32>,
5169
+ inverse: mat4x4<f32>,
5170
+ objectId: u32,
5171
+ root: u32,
5172
+ handedness: f32,
5173
+ _pad: u32,
5174
+ };
5175
+ @group(0) @binding(47) var meshInstances: texture_2d<u32>;
5176
+ fn read_mesh_instance(index: u32) -> MeshInstance {
5177
+ let y = i32(index);
5178
+ let metadata = textureLoad(meshInstances, vec2<i32>(8, y), 0);
5179
+ return MeshInstance(
5180
+ mat4x4<f32>(bitcast<vec4<f32>>(textureLoad(meshInstances, vec2<i32>(0, y), 0)),
5181
+ bitcast<vec4<f32>>(textureLoad(meshInstances, vec2<i32>(1, y), 0)),
5182
+ bitcast<vec4<f32>>(textureLoad(meshInstances, vec2<i32>(2, y), 0)),
5183
+ bitcast<vec4<f32>>(textureLoad(meshInstances, vec2<i32>(3, y), 0))),
5184
+ mat4x4<f32>(bitcast<vec4<f32>>(textureLoad(meshInstances, vec2<i32>(4, y), 0)),
5185
+ bitcast<vec4<f32>>(textureLoad(meshInstances, vec2<i32>(5, y), 0)),
5186
+ bitcast<vec4<f32>>(textureLoad(meshInstances, vec2<i32>(6, y), 0)),
5187
+ bitcast<vec4<f32>>(textureLoad(meshInstances, vec2<i32>(7, y), 0))),
5188
+ metadata.x, metadata.y, bitcast<f32>(metadata.z), 0u);
5189
+ }
5190
+
5191
+ fn instance_normal(instance: MeshInstance, normal: vec3<f32>) -> vec3<f32> {
5192
+ let normalMatrix = transpose(mat3x3<f32>(instance.inverse[0].xyz, instance.inverse[1].xyz, instance.inverse[2].xyz));
5193
+ return safe_normalize(normalMatrix * normal, vec3<f32>(0.0, 1.0, 0.0));
5194
+ }
5195
+
5196
+ fn triangle_for_candidate(candidate: Candidate) -> TriangleRecord {
5197
+ var triangle = triangles[candidate.triangleIndex];
5198
+ let instance = read_mesh_instance(candidate.instanceIndex);
5199
+ triangle.v0 = instance.world * vec4<f32>(triangle.v0.xyz, 1.0);
5200
+ triangle.v1 = instance.world * vec4<f32>(triangle.v1.xyz, 1.0);
5201
+ triangle.v2 = instance.world * vec4<f32>(triangle.v2.xyz, 1.0);
5202
+ triangle.meshId = instance.objectId;
5203
+ return triangle;
5204
+ }
5205
+
5206
+ fn intersect_bvh(ray: RayRecord, initialNearest: f32) -> Candidate {
5207
+ var nearest = initialNearest;
5208
+ var best = no_candidate();
5209
+ var stack = array<u32, 64>();
5210
+ var stackSize = 1u;
5211
+ stack[0] = 0u;
5212
+ loop {
5213
+ if (stackSize == 0u) { break; }
5214
+ stackSize -= 1u;
5215
+ let node = bvhNodes[stack[stackSize]];
5216
+ if (!intersect_bounds(ray, node.boundsMin.xyz, node.boundsMax.xyz, nearest)) { continue; }
5217
+ if (node.triangleCount == 0u) {
5218
+ stack[stackSize] = node.childOrFirst;
5219
+ stack[stackSize + 1u] = node.rightChild;
5220
+ stackSize += 2u;
5221
+ } else {
5222
+ let instanceIndex = node.childOrFirst;
5223
+ let instance = read_mesh_instance(instanceIndex);
5224
+ var localRay = ray;
5225
+ localRay.origin = instance.inverse * vec4<f32>(ray.origin.xyz, 1.0);
5226
+ // Keep this direction unnormalized so t remains the world-space ray parameter.
5227
+ localRay.direction = instance.inverse * vec4<f32>(ray.direction.xyz, 0.0);
5228
+ var candidate = intersect_geometry_bvh(localRay, nearest, instance.root);
5229
+ if (candidate.hit == 1u && candidate.distance < nearest) {
5230
+ candidate.geometricNormal = instance_normal(instance, candidate.geometricNormal);
5231
+ candidate.shadingNormal = repair_shading_normal(candidate.geometricNormal,
5232
+ instance_normal(instance, candidate.shadingNormal));
5233
+ if (instance.handedness < 0.0) { candidate.frontFace = 1u - candidate.frontFace; }
5234
+ candidate.instanceIndex = instanceIndex;
5235
+ nearest = candidate.distance;
5236
+ best = candidate;
5237
+ }
5238
+ }
5239
+ }
5240
+ return best;
5241
+ }
5242
+ `;
5243
+ var WAVEFRONT_UNINSTANCED_SHADER_WGSL = `
5244
+ fn triangle_for_candidate(candidate: Candidate) -> TriangleRecord {
5245
+ return triangles[candidate.triangleIndex];
5246
+ }
5247
+ fn intersect_bvh(ray: RayRecord, initialNearest: f32) -> Candidate {
5248
+ return intersect_geometry_bvh(ray, initialNearest, 0u);
5249
+ }
5250
+ `;
5251
+
4916
5252
  // src/wavefront-shader-bvh.js
4917
5253
  var WAVEFRONT_SHADER_BVH_WGSL = `
4918
5254
  fn default_mesh_range() -> MeshRange {
@@ -5375,6 +5711,7 @@ fn no_candidate() -> Candidate {
5375
5711
  1u,
5376
5712
  0xffffffffu,
5377
5713
  0xffffffffu,
5714
+ 0xffffffffu,
5378
5715
  0u,
5379
5716
  0u
5380
5717
  );
@@ -5401,6 +5738,7 @@ fn surface_candidate(
5401
5738
  uv,
5402
5739
  frontFace,
5403
5740
  triangleIndex,
5741
+ 0xffffffffu,
5404
5742
  primitiveId,
5405
5743
  materialRefId,
5406
5744
  mediumRefId
@@ -5462,7 +5800,7 @@ fn intersect_triangle(ray: RayRecord, triangle: TriangleRecord, triangleIndex: u
5462
5800
  );
5463
5801
  }
5464
5802
 
5465
- fn intersect_bvh(ray: RayRecord, initialNearest: f32) -> Candidate {
5803
+ fn intersect_geometry_bvh(ray: RayRecord, initialNearest: f32, root: u32) -> Candidate {
5466
5804
  var nearest = initialNearest;
5467
5805
  var best = no_candidate();
5468
5806
  if (config.bvhNodeCount == 0u || config.triangleCount == 0u) {
@@ -5471,7 +5809,7 @@ fn intersect_bvh(ray: RayRecord, initialNearest: f32) -> Candidate {
5471
5809
 
5472
5810
  var stack = array<u32, 64>();
5473
5811
  var stackSize = 1u;
5474
- stack[0] = 0u;
5812
+ stack[0] = root;
5475
5813
 
5476
5814
  loop {
5477
5815
  if (stackSize == 0u) {
@@ -5762,7 +6100,7 @@ fn intersectActiveQueue(@builtin(global_invocation_id) globalId: vec3<u32>) {
5762
6100
  if (meshCandidate.hit == 1u && meshCandidate.distance < nearest) {
5763
6101
  nearest = meshCandidate.distance;
5764
6102
  candidate = meshCandidate;
5765
- hitTriangle = triangles[meshCandidate.triangleIndex];
6103
+ hitTriangle = triangle_for_candidate(meshCandidate);
5766
6104
  }
5767
6105
 
5768
6106
  if (candidate.hit == 0u) {
@@ -6916,6 +7254,7 @@ struct Candidate {
6916
7254
  uv: vec2<f32>,
6917
7255
  frontFace: u32,
6918
7256
  triangleIndex: u32,
7257
+ instanceIndex: u32,
6919
7258
  primitiveId: u32,
6920
7259
  materialRefId: u32,
6921
7260
  mediumRefId: u32,
@@ -8513,6 +8852,7 @@ var WAVEFRONT_COMPUTE_WGSL = [
8513
8852
  WAVEFRONT_SHADER_MATERIALS_WGSL,
8514
8853
  WAVEFRONT_SHADER_LIGHTING_WGSL,
8515
8854
  WAVEFRONT_SHADER_BVH_WGSL,
8855
+ WAVEFRONT_UNINSTANCED_SHADER_WGSL,
8516
8856
  WAVEFRONT_SHADER_KERNELS_WGSL
8517
8857
  ].join("\n");
8518
8858
  function createWavefrontPathTracingComputeShaderSource(options = {}) {
@@ -8524,7 +8864,7 @@ function createWavefrontPathTracingComputeShaderSource(options = {}) {
8524
8864
  if (workgroupSize !== WAVEFRONT_COMPUTE_WORKGROUP_SIZE) {
8525
8865
  throw new Error(`wavefront mesh compute currently requires workgroupSize=${WAVEFRONT_COMPUTE_WORKGROUP_SIZE}`);
8526
8866
  }
8527
- return WAVEFRONT_COMPUTE_WGSL;
8867
+ return options.instanced === true ? WAVEFRONT_COMPUTE_WGSL.replace(WAVEFRONT_UNINSTANCED_SHADER_WGSL, WAVEFRONT_INSTANCE_SHADER_WGSL) : WAVEFRONT_COMPUTE_WGSL;
8528
8868
  }
8529
8869
 
8530
8870
  // src/wavefront-progressive-shader.js
@@ -9528,7 +9868,7 @@ function chooseSplitAxis(triangles) {
9528
9868
  }
9529
9869
  return extent[1] >= extent[2] ? 1 : 2;
9530
9870
  }
9531
- function buildBvh(triangles, maxLeafTriangles = 4) {
9871
+ function buildWavefrontBoundsBvh(triangles, maxLeafTriangles = 4) {
9532
9872
  if (triangles.length === 0) {
9533
9873
  return Object.freeze({ nodes: Object.freeze([]), triangles: Object.freeze([]) });
9534
9874
  }
@@ -9580,7 +9920,7 @@ function createWavefrontMeshAcceleration(meshes = [], gpuMaterialSource = null)
9580
9920
  const source = Array.isArray(meshes) ? meshes : [meshes];
9581
9921
  const resolvedMaterialSource = gpuMaterialSource ?? createWavefrontGpuMaterialSource(source);
9582
9922
  const triangles = createMeshTriangleRecords(source, resolvedMaterialSource);
9583
- return buildBvh(triangles);
9923
+ return buildWavefrontBoundsBvh(triangles);
9584
9924
  }
9585
9925
  function estimateMeshSourceShape(meshes) {
9586
9926
  const source = Array.isArray(meshes) ? meshes : [];
@@ -10398,6 +10738,145 @@ function dispatchWavefrontGpuAccelerationBuild({
10398
10738
  return true;
10399
10739
  }
10400
10740
 
10741
+ // src/wavefront-instances.js
10742
+ var WAVEFRONT_INSTANCE_BYTES = 144;
10743
+ var freeze = Object.freeze;
10744
+ var uint = (value) => Number.isInteger(value) && value >= 0 && value < 4294967295;
10745
+ function affineTransform(value) {
10746
+ 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) {
10747
+ throw new Error("Instance transform must be a finite affine column-major matrix.");
10748
+ }
10749
+ const m = Array.from(value, Math.fround);
10750
+ const [a, b, c, , d, e, f, , g, h, i] = m;
10751
+ const determinant = a * (e * i - f * h) - d * (b * i - c * h) + g * (b * f - c * e);
10752
+ if (!Number.isFinite(determinant) || Math.abs(determinant) < 1e-12) {
10753
+ throw new Error("Instance transform must be invertible.");
10754
+ }
10755
+ const inv = [
10756
+ (e * i - f * h) / determinant,
10757
+ (c * h - b * i) / determinant,
10758
+ (b * f - c * e) / determinant,
10759
+ 0,
10760
+ (f * g - d * i) / determinant,
10761
+ (a * i - c * g) / determinant,
10762
+ (c * d - a * f) / determinant,
10763
+ 0,
10764
+ (d * h - e * g) / determinant,
10765
+ (b * g - a * h) / determinant,
10766
+ (a * e - b * d) / determinant,
10767
+ 0,
10768
+ 0,
10769
+ 0,
10770
+ 0,
10771
+ 1
10772
+ ];
10773
+ for (let r = 0; r < 3; r++) inv[12 + r] = -(inv[r] * m[12] + inv[4 + r] * m[13] + inv[8 + r] * m[14]);
10774
+ if (!inv.every((n) => Number.isFinite(n) && Math.abs(n) <= 1e12)) throw new Error("Instance inverse transform is out of range.");
10775
+ return { transform: freeze(m), inverse: freeze(inv.map((n) => n === 0 ? 0 : n)), handedness: Math.sign(determinant) };
10776
+ }
10777
+ function transformInstancePoint(m, p) {
10778
+ return [0, 1, 2].map((r) => m[r] * p[0] + m[4 + r] * p[1] + m[8 + r] * p[2] + m[12 + r]);
10779
+ }
10780
+ function meshBounds(mesh) {
10781
+ const localMin = [Infinity, Infinity, Infinity], localMax = [-Infinity, -Infinity, -Infinity];
10782
+ for (let i = 0; i < mesh.positions.length; i += 3) for (let a = 0; a < 3; a++) {
10783
+ localMin[a] = Math.min(localMin[a], Math.fround(mesh.positions[i + a]));
10784
+ localMax[a] = Math.max(localMax[a], Math.fround(mesh.positions[i + a]));
10785
+ }
10786
+ return { min: localMin, max: localMax };
10787
+ }
10788
+ function boundsOf(local, transform) {
10789
+ const localMin = local.min, localMax = local.max;
10790
+ const min = [Infinity, Infinity, Infinity], max = [-Infinity, -Infinity, -Infinity];
10791
+ for (let corner = 0; corner < 8; corner++) {
10792
+ const p = transformInstancePoint(transform, [0, 1, 2].map((a) => corner & 1 << a ? localMax[a] : localMin[a]));
10793
+ for (let a = 0; a < 3; a++) {
10794
+ min[a] = Math.min(min[a], p[a]);
10795
+ max[a] = Math.max(max[a], p[a]);
10796
+ }
10797
+ }
10798
+ if (![...min, ...max].every(Number.isFinite)) throw new Error("Instance mesh has invalid bounds.");
10799
+ return freeze({ min: freeze(min), max: freeze(max) });
10800
+ }
10801
+ function prepareWavefrontInstances(meshes, instances) {
10802
+ if (!Array.isArray(instances) || instances.length === 0) throw new Error("Instances must be a nonempty array.");
10803
+ const byId = /* @__PURE__ */ new Map(), localBounds = /* @__PURE__ */ new Map();
10804
+ for (const mesh of meshes) {
10805
+ if (!uint(mesh.id) || byId.has(mesh.id)) throw new Error("Instanced mesh IDs must be unique unsigned integers.");
10806
+ if (mesh.materialKind === MATERIAL_EMISSIVE || emissionPower(mesh.emission ?? [0, 0, 0, 1]) > 1e-4) {
10807
+ throw new Error("Instanced emissive meshes require per-instance light sampling, which is not supported yet.");
10808
+ }
10809
+ byId.set(mesh.id, mesh);
10810
+ localBounds.set(mesh.id, meshBounds(mesh));
10811
+ }
10812
+ const ids = /* @__PURE__ */ new Set(), used = /* @__PURE__ */ new Set();
10813
+ const records = instances.map((instance) => {
10814
+ if (!instance || !uint(instance.id) || ids.has(instance.id)) throw new Error("Instance IDs must be unique unsigned integers.");
10815
+ const mesh = byId.get(instance.meshId);
10816
+ if (!mesh) throw new Error("Instance references an unknown mesh.");
10817
+ ids.add(instance.id);
10818
+ used.add(mesh.id);
10819
+ const affine = affineTransform(instance.transform);
10820
+ const bounds = boundsOf(localBounds.get(mesh.id), affine.transform);
10821
+ return freeze({
10822
+ ...affine,
10823
+ id: instance.id,
10824
+ meshId: mesh.id,
10825
+ bounds,
10826
+ centroid: freeze(bounds.min.map((v, a) => (v + bounds.max[a]) / 2))
10827
+ });
10828
+ });
10829
+ if (used.size !== meshes.length) throw new Error("Every instanced mesh must have at least one instance.");
10830
+ return freeze({ records: freeze(records), geometryCount: meshes.length });
10831
+ }
10832
+ function buildWavefrontInstances(meshes, plan, materialSource) {
10833
+ const tlas = buildWavefrontBoundsBvh(plan.records, 1);
10834
+ const nodes = tlas.nodes.map((node) => {
10835
+ const pad = node.bounds.min.map((v, a) => Math.max(1, Math.abs(v), Math.abs(node.bounds.max[a])) * 1e-6);
10836
+ return freeze({ ...node, bounds: freeze({
10837
+ min: freeze(node.bounds.min.map((v, a) => v - pad[a])),
10838
+ max: freeze(node.bounds.max.map((v, a) => v + pad[a]))
10839
+ }) });
10840
+ }), triangles = [], roots = /* @__PURE__ */ new Map();
10841
+ for (const [meshIndex, mesh] of meshes.entries()) {
10842
+ const blas = createWavefrontMeshAcceleration([mesh], materialSource);
10843
+ const nodeOffset = nodes.length, triangleOffset = triangles.length;
10844
+ roots.set(mesh.id, nodeOffset);
10845
+ for (const node of blas.nodes) nodes.push(freeze({
10846
+ ...node,
10847
+ firstTriangle: node.firstTriangle + (node.triangleCount > 0 ? triangleOffset : nodeOffset),
10848
+ leftChild: node.leftChild + nodeOffset,
10849
+ rightChild: node.rightChild + nodeOffset
10850
+ }));
10851
+ for (const triangle of blas.triangles) triangles.push(freeze({
10852
+ ...triangle,
10853
+ triangleId: triangle.triangleId + triangleOffset,
10854
+ materialSlot: meshIndex
10855
+ }));
10856
+ }
10857
+ const records = tlas.triangles.map((record) => freeze({ ...record, root: roots.get(record.meshId) }));
10858
+ const buffer = new ArrayBuffer(records.length * WAVEFRONT_INSTANCE_BYTES);
10859
+ const floats = new Float32Array(buffer), uints = new Uint32Array(buffer);
10860
+ records.forEach((record, index) => {
10861
+ const offset = index * 36;
10862
+ floats.set(record.transform, offset);
10863
+ floats.set(record.inverse, offset + 16);
10864
+ uints[offset + 32] = record.id;
10865
+ uints[offset + 33] = record.root;
10866
+ floats[offset + 34] = record.handedness;
10867
+ });
10868
+ return freeze({
10869
+ records: freeze(records),
10870
+ buffer,
10871
+ instanceCount: records.length,
10872
+ geometryCount: meshes.length,
10873
+ instanceBytes: buffer.byteLength,
10874
+ tlasNodeCount: tlas.nodes.length,
10875
+ blasNodeCount: nodes.length - tlas.nodes.length,
10876
+ acceleration: freeze({ nodes: freeze(nodes), triangles: freeze(triangles) })
10877
+ });
10878
+ }
10879
+
10401
10880
  // src/wavefront-config.js
10402
10881
  var WEBGPU_DEFAULT_MIN_UNIFORM_BUFFER_OFFSET_ALIGNMENT = 256;
10403
10882
  function allocatedRecordBytes(count, recordBytes) {
@@ -10671,6 +11150,7 @@ function estimateWavefrontPathTracingMemory(options = {}) {
10671
11150
  const progressiveBytes = options.progressive === true ? width * height * 16 + 16 : 0;
10672
11151
  const pathVertexBytes = tilePixelCapacity * (maxDepth + 1) * PATH_VERTEX_RECORD_BYTES;
10673
11152
  const sceneObjectBytes = sceneObjectCapacity * SCENE_OBJECT_RECORD_BYTES;
11153
+ const instanceBytes = readNonNegativeInteger2("instanceBytes", options.instanceBytes, 0);
10674
11154
  const triangleBytes = allocatedRecordBytes(triangleCapacity, TRIANGLE_RECORD_BYTES);
10675
11155
  const materialTableBytes = 0;
10676
11156
  const bvhNodeBytes = bvhNodeCapacity * BVH_NODE_RECORD_BYTES;
@@ -10726,6 +11206,7 @@ function estimateWavefrontPathTracingMemory(options = {}) {
10726
11206
  pathVertexBytes,
10727
11207
  sceneObjectBytes,
10728
11208
  triangleBytes,
11209
+ instanceBytes,
10729
11210
  materialTableBytes,
10730
11211
  bvhNodeBytes,
10731
11212
  bvhCombinedBytes,
@@ -10755,6 +11236,7 @@ function estimateWavefrontPathTracingMemoryForConfig(config, limits = {}) {
10755
11236
  maxDepth: config.maxDepth,
10756
11237
  sceneObjectCapacity: config.sceneObjectCapacity,
10757
11238
  triangleCapacity: config.triangleCapacity,
11239
+ instanceBytes: config.instancing?.instanceBytes ?? 0,
10758
11240
  bvhNodeCapacity: config.bvhNodeCapacity,
10759
11241
  bvhLeafSortCapacity: config.bvhLeafSortCapacity,
10760
11242
  emissiveTriangleCapacity: config.emissiveTriangleCapacity,
@@ -10773,6 +11255,7 @@ function estimateWavefrontPathTracingMemoryForConfig(config, limits = {}) {
10773
11255
  function createWavefrontPathTracingComputeConfig(options = {}) {
10774
11256
  assertAnalyticDisplayQualityPolicy(options);
10775
11257
  const accelerationBuildMode = resolveAccelerationBuildMode(options);
11258
+ if (options.instances !== void 0 && accelerationBuildMode === "gpu") throw new Error("Instance BLAS construction currently requires cpu-upload acceleration.");
10776
11259
  const canvas = options.canvas;
10777
11260
  const width = readPositiveInteger2("width", options.width, getCanvasDimension(canvas, "width", DEFAULT_WIDTH));
10778
11261
  const height = readPositiveInteger2("height", options.height, getCanvasDimension(canvas, "height", DEFAULT_HEIGHT));
@@ -10803,9 +11286,16 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
10803
11286
  );
10804
11287
  const meshes = normalizeMeshes(options);
10805
11288
  const meshSourceShape = estimateMeshSourceShape(meshes);
11289
+ if (options.memoryBudgetBytes !== void 0) {
11290
+ if (!Number.isSafeInteger(options.memoryBudgetBytes) || options.memoryBudgetBytes <= 0) throw new Error("GPU memory budget must be a positive safe integer.");
11291
+ const geometryMinimumBytes = meshSourceShape.triangleCount * TRIANGLE_RECORD_BYTES + (options.instances?.length ?? 0) * 144;
11292
+ if (!Number.isSafeInteger(geometryMinimumBytes) || geometryMinimumBytes > options.memoryBudgetBytes) throw new Error("Unique geometry exceeds the GPU memory budget before acceleration construction.");
11293
+ }
11294
+ const instancePlan = options.instances === void 0 ? null : prepareWavefrontInstances(meshes, options.instances);
10806
11295
  const gpuMaterialSource = meshes.length > 0 ? createWavefrontGpuMaterialSource(meshes) : createWavefrontGpuMaterialSource([]);
10807
11296
  const gpuMeshSource = meshes.length > 0 ? createWavefrontGpuMeshSource(meshes, gpuMaterialSource) : createWavefrontGpuMeshSource([]);
10808
- const meshAcceleration = accelerationBuildMode === "cpu-upload" ? createWavefrontMeshAcceleration(meshes, gpuMaterialSource) : Object.freeze({ nodes: Object.freeze([]), triangles: Object.freeze([]) });
11297
+ const instancing = instancePlan ? buildWavefrontInstances(meshes, instancePlan, gpuMaterialSource) : null;
11298
+ const meshAcceleration = instancing?.acceleration ?? (accelerationBuildMode === "cpu-upload" ? createWavefrontMeshAcceleration(meshes, gpuMaterialSource) : Object.freeze({ nodes: Object.freeze([]), triangles: Object.freeze([]) }));
10809
11299
  const emissiveTriangleIndices = createWavefrontEmissiveTriangleIndexSource(
10810
11300
  meshes,
10811
11301
  options.emissiveTriangleCapacity
@@ -10883,6 +11373,7 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
10883
11373
  gpuAccelerationBuildRequired: accelerationBuildMode === "gpu" && triangleCount > 0,
10884
11374
  gpuMeshSource,
10885
11375
  gpuMaterialSource,
11376
+ instancing,
10886
11377
  meshAcceleration,
10887
11378
  emissiveTriangleIndices,
10888
11379
  emissiveTriangleCount: emissiveTriangleIndices.count,
@@ -10921,6 +11412,7 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
10921
11412
  tilePixelCapacity,
10922
11413
  maxDepth,
10923
11414
  sceneObjectCapacity,
11415
+ instanceBytes: instancing?.instanceBytes ?? 0,
10924
11416
  triangleCapacity,
10925
11417
  bvhNodeCapacity,
10926
11418
  bvhLeafSortCapacity,
@@ -10954,6 +11446,7 @@ function createWavefrontTraceBindGroup({
10954
11446
  counterBuffer,
10955
11447
  outputView,
10956
11448
  triangleBuffer,
11449
+ instanceView,
10957
11450
  bvhNodeBuffer,
10958
11451
  radianceView,
10959
11452
  environmentPortalBuffer,
@@ -11011,7 +11504,8 @@ function createWavefrontTraceBindGroup({
11011
11504
  { binding: 41, resource: extensionAtlasResources.specularColor.view },
11012
11505
  { binding: 42, resource: extensionAtlasResources.iridescence.view },
11013
11506
  { binding: 43, resource: extensionAtlasResources.iridescenceThickness.view },
11014
- { binding: 44, resource: extensionAtlasResources.anisotropy.view }
11507
+ { binding: 44, resource: extensionAtlasResources.anisotropy.view },
11508
+ ...instanceView ? [{ binding: 47, resource: instanceView }] : []
11015
11509
  ]
11016
11510
  });
11017
11511
  }
@@ -11109,115 +11603,39 @@ function createWavefrontDenoiseBindGroups({
11109
11603
  inputView: radianceView,
11110
11604
  targetView: denoiseScratchView,
11111
11605
  label: "plasius.wavefront.bind.denoise.radianceToScratch"
11112
- }),
11113
- resolve: createWavefrontDenoiseResolveBindGroup({
11114
- device,
11115
- denoiseResolveBindGroupLayout,
11116
- configBuffer,
11117
- inputView: denoiseScratchView,
11118
- targetView: outputView,
11119
- label: "plasius.wavefront.bind.denoise.scratchToOutput"
11120
- }),
11121
- directResolve: createWavefrontDenoiseResolveBindGroup({
11122
- device,
11123
- denoiseResolveBindGroupLayout,
11124
- configBuffer,
11125
- inputView: radianceView,
11126
- targetView: outputView,
11127
- label: "plasius.wavefront.bind.denoise.radianceToOutput"
11128
- })
11129
- });
11130
- }
11131
- function createWavefrontPresentBindGroup({
11132
- device,
11133
- presentBindGroupLayout,
11134
- outputView,
11135
- sampler
11136
- }) {
11137
- return device.createBindGroup({
11138
- label: "plasius.wavefront.presentBindGroup",
11139
- layout: presentBindGroupLayout,
11140
- entries: [
11141
- { binding: 0, resource: outputView },
11142
- { binding: 1, resource: sampler }
11143
- ]
11144
- });
11145
- }
11146
-
11147
- // src/wavefront-gpu-synchronization.js
11148
- async function waitForSubmittedGpuWork(device, options = {}) {
11149
- if (typeof device.queue.onSubmittedWorkDone !== "function") {
11150
- return true;
11151
- }
11152
- const timeoutMs = Math.max(
11153
- 1,
11154
- Number.isFinite(options.timeoutMs) ? Number(options.timeoutMs) : GPU_SUBMITTED_WORK_TIMEOUT_MS
11155
- );
11156
- const maxWaitMs = Math.max(
11157
- timeoutMs,
11158
- Number.isFinite(options.maxWaitMs) ? Number(options.maxWaitMs) : timeoutMs
11159
- );
11160
- const allowTimeout = options.allowTimeout !== false;
11161
- const completionPromise = device.queue.onSubmittedWorkDone().then(
11162
- () => ({ status: "done" }),
11163
- (error) => {
11164
- throw error;
11165
- }
11166
- );
11167
- const lossPromise = typeof device.lost?.then === "function" ? device.lost.then((info) => {
11168
- throw new Error(
11169
- `WebGPU device lost while waiting for submitted work (${info?.reason ?? "unknown"}).`
11170
- );
11171
- }) : null;
11172
- const startedAtMs = nowMs3();
11173
- while (true) {
11174
- const elapsedMs = Math.max(0, nowMs3() - startedAtMs);
11175
- const remainingMs = Math.max(0, maxWaitMs - elapsedMs);
11176
- if (remainingMs <= 0) {
11177
- if (!allowTimeout) {
11178
- throw new Error(`Timed out after ${Math.round(maxWaitMs)} ms waiting for submitted GPU work.`);
11179
- }
11180
- console.warn(
11181
- `[plasius.wavefront] Submitted GPU work did not report completion within ${Math.round(maxWaitMs)} ms; continuing.`
11182
- );
11183
- return false;
11184
- }
11185
- const waitWindowMs = Math.max(1, Math.min(timeoutMs, remainingMs));
11186
- let timeoutHandle = null;
11187
- let resolveTimeoutPromise = null;
11188
- let timeoutSettled = false;
11189
- const settleTimeoutPromise = (value) => {
11190
- if (timeoutSettled) {
11191
- return;
11192
- }
11193
- timeoutSettled = true;
11194
- resolveTimeoutPromise?.(value);
11195
- };
11196
- const timeoutPromise = new Promise((resolve) => {
11197
- resolveTimeoutPromise = resolve;
11198
- timeoutHandle = setTimeout(
11199
- () => settleTimeoutPromise({ status: "timeout" }),
11200
- waitWindowMs
11201
- );
11202
- });
11203
- let result;
11204
- try {
11205
- result = await Promise.race(
11206
- [completionPromise, timeoutPromise, lossPromise].filter(Boolean)
11207
- );
11208
- } finally {
11209
- if (timeoutHandle !== null) {
11210
- clearTimeout(timeoutHandle);
11211
- settleTimeoutPromise({ status: "cancelled" });
11212
- }
11213
- }
11214
- if (result?.status === "done") {
11215
- return true;
11216
- }
11217
- if (result?.status !== "timeout") {
11218
- return true;
11219
- }
11220
- }
11606
+ }),
11607
+ resolve: createWavefrontDenoiseResolveBindGroup({
11608
+ device,
11609
+ denoiseResolveBindGroupLayout,
11610
+ configBuffer,
11611
+ inputView: denoiseScratchView,
11612
+ targetView: outputView,
11613
+ label: "plasius.wavefront.bind.denoise.scratchToOutput"
11614
+ }),
11615
+ directResolve: createWavefrontDenoiseResolveBindGroup({
11616
+ device,
11617
+ denoiseResolveBindGroupLayout,
11618
+ configBuffer,
11619
+ inputView: radianceView,
11620
+ targetView: outputView,
11621
+ label: "plasius.wavefront.bind.denoise.radianceToOutput"
11622
+ })
11623
+ });
11624
+ }
11625
+ function createWavefrontPresentBindGroup({
11626
+ device,
11627
+ presentBindGroupLayout,
11628
+ outputView,
11629
+ sampler
11630
+ }) {
11631
+ return device.createBindGroup({
11632
+ label: "plasius.wavefront.presentBindGroup",
11633
+ layout: presentBindGroupLayout,
11634
+ entries: [
11635
+ { binding: 0, resource: outputView },
11636
+ { binding: 1, resource: sampler }
11637
+ ]
11638
+ });
11221
11639
  }
11222
11640
 
11223
11641
  // src/wavefront-frame-telemetry.js
@@ -12044,7 +12462,7 @@ function dispatchWavefrontFrameAwaitingGpu({
12044
12462
  }
12045
12463
 
12046
12464
  // src/wavefront-pipelines.js
12047
- function createWavefrontBindGroupLayouts(device, constants) {
12465
+ function createWavefrontBindGroupLayouts(device, constants, instanced = false) {
12048
12466
  const trace = device.createBindGroupLayout({
12049
12467
  label: "plasius.wavefront.traceBindGroupLayout",
12050
12468
  entries: [
@@ -12096,7 +12514,8 @@ function createWavefrontBindGroupLayouts(device, constants) {
12096
12514
  { binding: 41, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
12097
12515
  { binding: 42, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
12098
12516
  { binding: 43, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
12099
- { binding: 44, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } }
12517
+ { binding: 44, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
12518
+ ...instanced ? [{ binding: 47, visibility: constants.shader.COMPUTE, texture: { sampleType: "uint" } }] : []
12100
12519
  ]
12101
12520
  });
12102
12521
  const acceleration = device.createBindGroupLayout({
@@ -12291,12 +12710,12 @@ async function createWavefrontPresentPipeline(device, presentBindGroupLayout, fo
12291
12710
  primitive: { topology: "triangle-list" }
12292
12711
  });
12293
12712
  }
12294
- async function createWavefrontPipelineResources({ device, constants, format }) {
12295
- const bindGroupLayouts = createWavefrontBindGroupLayouts(device, constants);
12713
+ async function createWavefrontPipelineResources({ device, constants, format, instanced = false }) {
12714
+ const bindGroupLayouts = createWavefrontBindGroupLayouts(device, constants, instanced);
12296
12715
  const pipelineLayouts = createWavefrontPipelineLayouts(device, bindGroupLayouts);
12297
12716
  const computeShader = device.createShaderModule({
12298
12717
  label: "plasius.wavefront.computeShader",
12299
- code: WAVEFRONT_COMPUTE_WGSL
12718
+ code: createWavefrontPathTracingComputeShaderSource({ instanced })
12300
12719
  });
12301
12720
  await assertShaderModuleCompiles(computeShader, "plasius.wavefront.computeShader");
12302
12721
  return Object.freeze({
@@ -12729,890 +13148,911 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
12729
13148
  if (!adapter) {
12730
13149
  throw new Error("Unable to acquire a WebGPU adapter for wavefront path tracing.");
12731
13150
  }
12732
- const device = await adapter.requestDevice(
13151
+ const rawDevice = await adapter.requestDevice(
12733
13152
  createWavefrontDeviceDescriptor(adapter, options, initialConfig.memory)
12734
13153
  );
12735
- const gpuAdapterParallelism = createGpuAdapterParallelismDiagnostics(adapter, device);
12736
- const context = canvas.getContext("webgpu");
12737
- if (!context || typeof context.configure !== "function") {
12738
- throw new Error("Canvas WebGPU context does not support configure().");
12739
- }
12740
- const format = options.format ?? (typeof navigatorRef.gpu.getPreferredCanvasFormat === "function" ? navigatorRef.gpu.getPreferredCanvasFormat() : "bgra8unorm");
12741
- let config = initialConfig;
12742
- const safeTileSize = clampTileSizeForDevice(config, device);
12743
- if (safeTileSize !== config.tileSize) {
12744
- config = createWavefrontPathTracingComputeConfig({
12745
- ...options,
12746
- canvas,
12747
- width: config.width,
12748
- height: config.height,
12749
- tileSize: safeTileSize,
12750
- tilePixelCapacity: Math.min(
12751
- config.tilePixelCapacity,
12752
- safeTileSize * safeTileSize
12753
- )
12754
- });
12755
- }
12756
- config = Object.freeze({
12757
- ...config,
12758
- memory: estimateWavefrontPathTracingMemoryForConfig(config, device.limits)
12759
- });
12760
- canvas.width = config.width;
12761
- canvas.height = config.height;
12762
- context.configure({
12763
- device,
12764
- format,
12765
- alphaMode: options.alpha === true ? "premultiplied" : "opaque"
12766
- });
12767
- const bufferUsage = constants.buffer.STORAGE | constants.buffer.COPY_DST;
12768
- const memory = config.memory;
12769
- const activeQueue = createBuffer(device, bufferUsage, memory.queueBytes, "plasius.wavefront.activeQueue");
12770
- const nextQueue = createBuffer(device, bufferUsage, memory.queueBytes, "plasius.wavefront.nextQueue");
12771
- const hitBuffer = createBuffer(device, bufferUsage, memory.hitBytes, "plasius.wavefront.hitBuffer");
12772
- const accumulationBuffer = createBuffer(
12773
- device,
12774
- bufferUsage,
12775
- memory.accumulationBytes,
12776
- "plasius.wavefront.accumulation"
12777
- );
12778
- const pathVertexBuffer = createBuffer(
12779
- device,
12780
- bufferUsage,
12781
- memory.pathVertexBytes,
12782
- "plasius.wavefront.pathVertices"
12783
- );
12784
- const sceneObjectBuffer = createBuffer(
12785
- device,
12786
- constants.buffer.STORAGE | constants.buffer.COPY_DST,
12787
- memory.sceneObjectBytes,
12788
- "plasius.wavefront.sceneObjects"
12789
- );
12790
- const triangleBuffer = createBuffer(
12791
- device,
12792
- constants.buffer.STORAGE | constants.buffer.COPY_DST,
12793
- memory.triangleBytes,
12794
- "plasius.wavefront.triangles"
12795
- );
12796
- const bvhNodeBuffer = createBuffer(
12797
- device,
12798
- constants.buffer.STORAGE | constants.buffer.COPY_DST,
12799
- memory.bvhCombinedBytes,
12800
- "plasius.wavefront.bvhNodes"
12801
- );
12802
- const meshVertexBuffer = createBuffer(
12803
- device,
12804
- constants.buffer.STORAGE | constants.buffer.COPY_DST,
12805
- memory.meshVertexBytes,
12806
- "plasius.wavefront.meshVertices"
12807
- );
12808
- const meshIndexBuffer = createBuffer(
12809
- device,
12810
- constants.buffer.STORAGE | constants.buffer.COPY_DST,
12811
- memory.meshIndexBytes,
12812
- "plasius.wavefront.meshIndices"
12813
- );
12814
- const meshRangeBuffer = createBuffer(
12815
- device,
12816
- constants.buffer.STORAGE | constants.buffer.COPY_DST,
12817
- memory.meshRangeBytes,
12818
- "plasius.wavefront.meshRanges"
12819
- );
12820
- const environmentPortalBuffer = createBuffer(
12821
- device,
12822
- constants.buffer.STORAGE | constants.buffer.COPY_DST,
12823
- memory.environmentPortalBytes,
12824
- "plasius.wavefront.environmentPortals"
12825
- );
12826
- const bvhLeafRefBuffer = createBuffer(
12827
- device,
12828
- constants.buffer.STORAGE | constants.buffer.COPY_DST,
12829
- memory.bvhLeafReferenceBytes,
12830
- "plasius.wavefront.bvhLeafRefs"
12831
- );
12832
- const tiles = createTiles(config.width, config.height, config.tileSize);
12833
- const configBufferStride = memory.configBufferStride;
12834
- const outputConfigSlotCount = config.deferredPathResolve ? 0 : tiles.length;
12835
- const frameConfigSlotCount = Math.max(
12836
- 1,
12837
- tiles.length * config.samplesPerPixel + outputConfigSlotCount + (config.denoise || config.progressive ? 1 : 0)
12838
- );
12839
- const configBuffer = createBuffer(
12840
- device,
12841
- constants.buffer.UNIFORM | constants.buffer.COPY_DST,
12842
- memory.frameConfigBytes,
12843
- "plasius.wavefront.frameConfig"
12844
- );
12845
- const bvhBuildConfigBuffer = createBuffer(
12846
- device,
12847
- constants.buffer.UNIFORM | constants.buffer.COPY_DST,
12848
- memory.bvhBuildConfigBytes,
12849
- "plasius.wavefront.bvhBuildConfig"
12850
- );
12851
- const counterBuffer = createBuffer(
12852
- device,
12853
- constants.buffer.STORAGE | constants.buffer.COPY_DST | constants.buffer.COPY_SRC,
12854
- memory.counterBytes,
12855
- "plasius.wavefront.counters"
12856
- );
12857
- const activeDispatchBuffer = createBuffer(
12858
- device,
12859
- constants.buffer.INDIRECT | constants.buffer.COPY_DST,
12860
- memory.indirectDispatchBytes,
12861
- "plasius.wavefront.activeDispatchArgs"
12862
- );
12863
- let packedScene = packWavefrontSceneObjects(config.sceneObjects, config.sceneObjectCapacity);
12864
- device.queue.writeBuffer(sceneObjectBuffer, 0, packedScene.buffer);
12865
- const packedTriangles = packWavefrontTriangles(
12866
- config.meshAcceleration.triangles,
12867
- Math.max(1, config.triangleCapacity)
12868
- );
12869
- const packedBvhNodes = packWavefrontBvhNodes(
12870
- config.meshAcceleration.nodes,
12871
- Math.max(1, config.bvhNodeCapacity + config.emissiveTriangleCapacity)
12872
- );
12873
- const packedBvhNodeUints = new Uint32Array(packedBvhNodes.buffer);
12874
- const packedBvhNodeFloats = new Float32Array(packedBvhNodes.buffer);
12875
- const orderedEmissiveTriangleIndices = resolveOrderedEmissiveTriangleIndices(config);
12876
- const emissiveWeights = orderedEmissiveTriangleIndices.map(
12877
- (triangleIndex) => emissiveTriangleWeight(config.meshAcceleration.triangles[triangleIndex])
12878
- );
12879
- const totalEmissiveWeight = emissiveWeights.reduce((total, weight) => total + weight, 0);
12880
- let cumulativeEmissiveWeight = 0;
12881
- orderedEmissiveTriangleIndices.forEach((triangleIndex, index) => {
12882
- const nodeOffset = (config.bvhNodeCapacity + index) * (BVH_NODE_RECORD_BYTES / 4);
12883
- cumulativeEmissiveWeight += emissiveWeights[index] ?? 0;
12884
- packedBvhNodeFloats[nodeOffset] = cumulativeEmissiveWeight;
12885
- packedBvhNodeFloats[nodeOffset + 1] = emissiveWeights[index] ?? 0;
12886
- packedBvhNodeFloats[nodeOffset + 2] = totalEmissiveWeight;
12887
- packedBvhNodeUints[nodeOffset + 8] = triangleIndex;
12888
- });
12889
- device.queue.writeBuffer(triangleBuffer, 0, packedTriangles.buffer);
12890
- device.queue.writeBuffer(bvhNodeBuffer, 0, packedBvhNodes.buffer);
12891
- device.queue.writeBuffer(meshVertexBuffer, 0, config.gpuMeshSource.vertices.buffer);
12892
- device.queue.writeBuffer(meshIndexBuffer, 0, config.gpuMeshSource.indices.buffer);
12893
- device.queue.writeBuffer(meshRangeBuffer, 0, config.gpuMeshSource.meshes.buffer);
12894
- const packedEnvironmentPortals = packEnvironmentPortals(
12895
- config.environmentPortals,
12896
- Math.max(1, config.environmentPortalCapacity)
12897
- );
12898
- device.queue.writeBuffer(environmentPortalBuffer, 0, packedEnvironmentPortals.buffer);
12899
- const radianceTexture = device.createTexture({
12900
- label: "plasius.wavefront.radiance",
12901
- size: { width: config.width, height: config.height },
12902
- format: "rgba16float",
12903
- usage: constants.texture.STORAGE_BINDING | constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
12904
- });
12905
- const radianceView = radianceTexture.createView();
12906
- const denoiseScratchTexture = device.createTexture({
12907
- label: "plasius.wavefront.denoiseScratch",
12908
- size: { width: config.width, height: config.height },
12909
- format: "rgba16float",
12910
- usage: constants.texture.STORAGE_BINDING | constants.texture.TEXTURE_BINDING
12911
- });
12912
- const denoiseScratchView = denoiseScratchTexture.createView();
12913
- const outputTexture = device.createTexture({
12914
- label: "plasius.wavefront.output",
12915
- size: { width: config.width, height: config.height },
12916
- format: "rgba8unorm",
12917
- usage: constants.texture.STORAGE_BINDING | constants.texture.TEXTURE_BINDING | constants.texture.COPY_SRC
12918
- });
12919
- const outputView = outputTexture.createView();
12920
- const sampler = device.createSampler({
12921
- label: "plasius.wavefront.presentSampler",
12922
- magFilter: "nearest",
12923
- minFilter: "nearest"
12924
- });
12925
- const environmentMapResource = createEnvironmentMapResource(
12926
- device,
12927
- constants,
12928
- config.environmentMap,
12929
- config.environmentColor
12930
- );
12931
- const environmentSamplingResource = createEnvironmentSamplingTextureResource(
12932
- device,
12933
- constants,
12934
- config.environmentMap,
12935
- config.environmentColor
12936
- );
12937
- let mediumTextureResource = createMediumTextureResource(
12938
- device,
12939
- constants,
12940
- config.mediums
12941
- );
12942
- config = Object.freeze({
12943
- ...config,
12944
- environmentMap: Object.freeze({
12945
- ...config.environmentMap,
12946
- width: environmentMapResource.width,
12947
- height: environmentMapResource.height,
12948
- mipLevelCount: environmentMapResource.mipLevelCount,
12949
- hasImportanceData: environmentSamplingResource.hasImportanceData
12950
- })
12951
- });
12952
- const brdfLutResource = createBrdfLutResource(device, constants);
12953
- const baseColorAtlasResource = createAtlasTextureResource(
12954
- device,
12955
- constants,
12956
- config.gpuMaterialSource.baseColorAtlas,
12957
- "plasius.wavefront.materialAtlas.baseColor"
12958
- );
12959
- const metallicRoughnessAtlasResource = createAtlasTextureResource(
12960
- device,
12961
- constants,
12962
- config.gpuMaterialSource.metallicRoughnessAtlas,
12963
- "plasius.wavefront.materialAtlas.metallicRoughness"
12964
- );
12965
- const normalAtlasResource = createAtlasTextureResource(
12966
- device,
12967
- constants,
12968
- config.gpuMaterialSource.normalAtlas,
12969
- "plasius.wavefront.materialAtlas.normal"
12970
- );
12971
- const occlusionAtlasResource = createAtlasTextureResource(
12972
- device,
12973
- constants,
12974
- config.gpuMaterialSource.occlusionAtlas,
12975
- "plasius.wavefront.materialAtlas.occlusion"
12976
- );
12977
- const emissiveAtlasResource = createAtlasTextureResource(
12978
- device,
12979
- constants,
12980
- config.gpuMaterialSource.emissiveAtlas,
12981
- "plasius.wavefront.materialAtlas.emissive"
12982
- );
12983
- const extensionAtlasNames = [
12984
- "clearcoat",
12985
- "clearcoatRoughness",
12986
- "clearcoatNormal",
12987
- "transmission",
12988
- "thickness",
12989
- "sheenColor",
12990
- "sheenRoughness",
12991
- "specular",
12992
- "specularColor",
12993
- "iridescence",
12994
- "iridescenceThickness",
12995
- "anisotropy"
12996
- ];
12997
- const extensionAtlasResources = Object.freeze(
12998
- Object.fromEntries(
12999
- extensionAtlasNames.map((name) => [
13000
- name,
13001
- createAtlasTextureResource(
13154
+ const allocationTracker = createGpuAllocationTracker(rawDevice, { budgetBytes: options.memoryBudgetBytes, storageUsage: constants.buffer.STORAGE });
13155
+ const device = allocationTracker.device;
13156
+ let context, finishAllocationScopes;
13157
+ try {
13158
+ let createTraceBindGroups = function() {
13159
+ return createWavefrontTraceBindGroups({
13160
+ device,
13161
+ traceBindGroupLayout,
13162
+ activeQueue,
13163
+ nextQueue,
13164
+ frameConfigBuffer: configBuffer,
13165
+ hitBuffer,
13166
+ accumulationBuffer,
13167
+ sceneObjectBuffer,
13168
+ counterBuffer,
13169
+ outputView,
13170
+ triangleBuffer,
13171
+ instanceView,
13172
+ bvhNodeBuffer,
13173
+ radianceView,
13174
+ environmentPortalBuffer,
13175
+ environmentMapResource,
13176
+ pathVertexBuffer,
13177
+ baseColorAtlasResource,
13178
+ metallicRoughnessAtlasResource,
13179
+ normalAtlasResource,
13180
+ occlusionAtlasResource,
13181
+ emissiveAtlasResource,
13182
+ materialAtlasSampler,
13183
+ brdfLutResource,
13184
+ environmentSamplingResource,
13185
+ mediumTextureResource,
13186
+ extensionAtlasResources
13187
+ });
13188
+ }, ensureFrameTelemetryResources = function() {
13189
+ if (!frameTelemetryResources) {
13190
+ frameTelemetryResources = createWavefrontFrameTelemetryResources({
13002
13191
  device,
13003
13192
  constants,
13004
- config.gpuMaterialSource.extensionAtlases[name],
13005
- `plasius.wavefront.materialAtlas.${name}`
13006
- )
13007
- ])
13008
- )
13009
- );
13010
- const materialAtlasSampler = device.createSampler({
13011
- label: "plasius.wavefront.materialAtlasSampler",
13012
- addressModeU: "clamp-to-edge",
13013
- addressModeV: "clamp-to-edge",
13014
- magFilter: "linear",
13015
- minFilter: "linear"
13016
- });
13017
- const {
13018
- bindGroupLayouts: {
13019
- trace: traceBindGroupLayout,
13020
- acceleration: accelerationBindGroupLayout,
13021
- denoiseRadiance: denoiseRadianceBindGroupLayout,
13022
- denoiseResolve: denoiseResolveBindGroupLayout,
13023
- present: presentBindGroupLayout
13024
- },
13025
- computePipelines: pipelines,
13026
- presentPipeline
13027
- } = await createWavefrontPipelineResources({ device, constants, format });
13028
- function createTraceBindGroups() {
13029
- return createWavefrontTraceBindGroups({
13030
- device,
13031
- traceBindGroupLayout,
13032
- activeQueue,
13033
- nextQueue,
13034
- frameConfigBuffer: configBuffer,
13035
- hitBuffer,
13036
- accumulationBuffer,
13037
- sceneObjectBuffer,
13038
- counterBuffer,
13039
- outputView,
13040
- triangleBuffer,
13041
- bvhNodeBuffer,
13042
- radianceView,
13043
- environmentPortalBuffer,
13044
- environmentMapResource,
13045
- pathVertexBuffer,
13046
- baseColorAtlasResource,
13047
- metallicRoughnessAtlasResource,
13048
- normalAtlasResource,
13049
- occlusionAtlasResource,
13050
- emissiveAtlasResource,
13051
- materialAtlasSampler,
13052
- brdfLutResource,
13053
- environmentSamplingResource,
13054
- mediumTextureResource,
13055
- extensionAtlasResources
13056
- });
13057
- }
13058
- let bindGroups = createTraceBindGroups();
13059
- const bvhBuildBindGroup = createWavefrontBvhBuildBindGroup({
13060
- device,
13061
- accelerationBindGroupLayout,
13062
- bvhBuildConfigBuffer,
13063
- triangleBuffer,
13064
- bvhNodeBuffer,
13065
- meshVertexBuffer,
13066
- meshIndexBuffer,
13067
- meshRangeBuffer,
13068
- bvhLeafRefBuffer
13069
- });
13070
- const {
13071
- radiance: denoiseRadianceBindGroup,
13072
- resolve: denoiseResolveBindGroup,
13073
- directResolve: denoiseDirectResolveBindGroup
13074
- } = createWavefrontDenoiseBindGroups({
13075
- device,
13076
- denoiseRadianceBindGroupLayout,
13077
- denoiseResolveBindGroupLayout,
13078
- configBuffer,
13079
- radianceView,
13080
- denoiseScratchView,
13081
- outputView
13082
- });
13083
- const presentBindGroup = createWavefrontPresentBindGroup({
13084
- device,
13085
- presentBindGroupLayout,
13086
- outputView,
13087
- sampler
13088
- });
13089
- const progressive = await createWavefrontProgressiveResources({ device, constants, config, configBuffer, radianceTexture, radianceView, outputView });
13090
- let activeFrameTelemetry = null;
13091
- let frameTelemetryResources = null;
13092
- const frameEncoder = createWavefrontFrameEncoder({
13093
- getConfig: () => config,
13094
- progressive,
13095
- getBindGroups: () => bindGroups,
13096
- pipelines,
13097
- counterBuffer,
13098
- activeDispatchBuffer,
13099
- denoiseRadianceBindGroup,
13100
- denoiseResolveBindGroup,
13101
- denoiseDirectResolveBindGroup,
13102
- presentPipeline,
13103
- presentBindGroup,
13104
- context,
13105
- getFrameTelemetry: () => activeFrameTelemetry
13106
- });
13107
- let frame = 0;
13108
- let accelerationBuilt = !config.gpuAccelerationBuildRequired;
13109
- let accelerationBuildCount = 0;
13110
- let activeCameraOptions = options.camera ?? DEFAULT_CAMERA2;
13111
- let lastCompletedFrameTimeMs = null;
13112
- let lastCompletedSamplesPerPixel = Math.max(1, config.samplesPerPixel);
13113
- let lastGpuParallelism = createGpuParallelismDiagnostics(
13114
- gpuAdapterParallelism,
13115
- createGpuParallelismCounters()
13116
- );
13117
- const waitForSubmittedGpuWork2 = (waitOptions = {}) => waitForSubmittedGpuWork(device, waitOptions);
13118
- function ensureFrameTelemetryResources() {
13119
- if (!frameTelemetryResources) {
13120
- frameTelemetryResources = createWavefrontFrameTelemetryResources({
13193
+ maxRayCountRecords: tiles.length * config.samplesPerPixel * config.maxDepth
13194
+ });
13195
+ }
13196
+ return frameTelemetryResources;
13197
+ }, resolveRenderedSamplesPerPixel = function(renderOptions = {}, awaitGPUCompletion = true) {
13198
+ return resolveWavefrontRenderedSamplesPerPixel({
13199
+ config,
13200
+ renderOptions,
13201
+ awaitGPUCompletion,
13202
+ lastCompletedFrameTimeMs,
13203
+ lastCompletedSamplesPerPixel
13204
+ });
13205
+ }, createFrameStats = function({
13206
+ frameIndex,
13207
+ accelerationBuildSubmitted,
13208
+ frameSubmissionCount,
13209
+ parallelismCounters,
13210
+ renderedSamplesPerPixel,
13211
+ targetSamplesPerPixel,
13212
+ frameTimeBudgetMs,
13213
+ budgetConstrained
13214
+ }) {
13215
+ const stats = createWavefrontFrameStats({
13216
+ config,
13217
+ tiles,
13218
+ frame,
13219
+ frameIndex,
13220
+ accelerationBuilt,
13221
+ accelerationBuildCount,
13222
+ accelerationBuildSubmitted,
13223
+ frameSubmissionCount,
13224
+ frameConfigSlotCount,
13225
+ gpuAdapterParallelism,
13226
+ parallelismCounters,
13227
+ renderedSamplesPerPixel,
13228
+ targetSamplesPerPixel,
13229
+ frameTimeBudgetMs,
13230
+ budgetConstrained
13231
+ });
13232
+ lastGpuParallelism = stats.gpuParallelism;
13233
+ return stats.frameStats;
13234
+ }, writeFrameConfigSlot = function(slot, tile, frameIndex, buildRange = {}) {
13235
+ if (slot >= frameConfigSlotCount) {
13236
+ throw new Error("Wavefront frame config slot capacity exceeded.");
13237
+ }
13238
+ const offset = slot * configBufferStride;
13239
+ device.queue.writeBuffer(
13240
+ configBuffer,
13241
+ offset,
13242
+ createConfigPayload(config, tile, frameIndex, buildRange)
13243
+ );
13244
+ return offset;
13245
+ }, createFrameConfigWriter = function(frameIndex) {
13246
+ let slot = 0;
13247
+ return (tile, buildRange = {}) => {
13248
+ const offset = writeFrameConfigSlot(slot, tile, frameIndex, buildRange);
13249
+ slot += 1;
13250
+ return offset;
13251
+ };
13252
+ }, dispatchGpuAccelerationBuild = function(frameIndex, parallelism) {
13253
+ const submitted = dispatchWavefrontGpuAccelerationBuild({
13254
+ config,
13255
+ accelerationBuilt,
13256
+ tiles,
13121
13257
  device,
13122
- constants,
13123
- maxRayCountRecords: tiles.length * config.samplesPerPixel * config.maxDepth
13258
+ bvhBuildConfigBuffer,
13259
+ configBufferStride,
13260
+ bvhBuildBindGroup,
13261
+ pipelines,
13262
+ parallelism,
13263
+ frameIndex
13264
+ });
13265
+ if (submitted) {
13266
+ accelerationBuilt = true;
13267
+ accelerationBuildCount += 1;
13268
+ }
13269
+ return submitted;
13270
+ }, dispatchFrame = function(frameIndex, parallelism, renderedSamplesPerPixel = config.samplesPerPixel) {
13271
+ return dispatchWavefrontFrame({
13272
+ config,
13273
+ tiles,
13274
+ device,
13275
+ frameIndex,
13276
+ parallelism,
13277
+ renderedSamplesPerPixel,
13278
+ frameEncoder,
13279
+ createFrameConfigWriter
13280
+ });
13281
+ }, renderOnce = function(renderOptions = {}, resolvedSamplingPlan = null) {
13282
+ const frameStartTimeMs = nowMs3();
13283
+ frame += 1;
13284
+ const frameIndex = frame + config.frameIndex;
13285
+ const samplingPlan = resolvedSamplingPlan ?? resolveRenderedSamplesPerPixel(renderOptions, false);
13286
+ const parallelismCounters = createGpuParallelismCounters();
13287
+ const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex, parallelismCounters);
13288
+ const frameSubmissionCount = dispatchFrame(
13289
+ frameIndex,
13290
+ parallelismCounters,
13291
+ samplingPlan.renderedSamplesPerPixel
13292
+ );
13293
+ const frameTimeMs = Math.max(0, nowMs3() - frameStartTimeMs);
13294
+ return Object.freeze({
13295
+ ...createFrameStats({
13296
+ frameIndex,
13297
+ accelerationBuildSubmitted,
13298
+ frameSubmissionCount,
13299
+ parallelismCounters,
13300
+ renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel,
13301
+ targetSamplesPerPixel: samplingPlan.targetSamplesPerPixel,
13302
+ frameTimeBudgetMs: samplingPlan.frameTimeBudgetMs,
13303
+ budgetConstrained: samplingPlan.budgetConstrained
13304
+ }),
13305
+ gpuWorkerJobs: createGpuWorkerJobDiagnostics(
13306
+ lastGpuParallelism,
13307
+ frameSubmissionCount + (accelerationBuildSubmitted ? 1 : 0),
13308
+ frameTimeMs,
13309
+ false
13310
+ ),
13311
+ timings: createWavefrontFrameTimingTelemetry({
13312
+ status: "not-requested",
13313
+ source: "cpu-submit",
13314
+ timestampQueryStatus: "not-recorded",
13315
+ totalRenderJobTimeMs: frameTimeMs
13316
+ })
13317
+ });
13318
+ }, dispatchFrameAwaitingGpu = function(frameIndex, parallelism, renderedSamplesPerPixel = config.samplesPerPixel, optionsForFrame = {}) {
13319
+ return dispatchWavefrontFrameAwaitingGpu({
13320
+ config,
13321
+ tiles,
13322
+ device,
13323
+ frameIndex,
13324
+ parallelism,
13325
+ renderedSamplesPerPixel,
13326
+ frameEncoder,
13327
+ writeFrameConfigSlot,
13328
+ optionsForFrame
13329
+ });
13330
+ }, rebuildLiveConfig = function(overrides = {}) {
13331
+ return createWavefrontPathTracingComputeConfig({
13332
+ ...options,
13333
+ canvas,
13334
+ width: config.width,
13335
+ height: config.height,
13336
+ maxDepth: config.maxDepth,
13337
+ tileSize: config.tileSize,
13338
+ samplesPerPixel: config.samplesPerPixel,
13339
+ sceneObjectCapacity: config.sceneObjectCapacity,
13340
+ sceneObjects: packedScene.objects,
13341
+ camera: activeCameraOptions,
13342
+ environmentMap: {
13343
+ ...config.environmentMap
13344
+ },
13345
+ strictPhysicalLowSppLighting: config.strictPhysicalLowSppLighting,
13346
+ featureFlags: options.featureFlags,
13347
+ frameIndex: config.frameIndex,
13348
+ ...overrides
13349
+ });
13350
+ }, rebuildMediumResources = function(nextConfig) {
13351
+ const previousMediumTextureResource = mediumTextureResource;
13352
+ mediumTextureResource = createMediumTextureResource(device, constants, nextConfig.mediums);
13353
+ bindGroups = createTraceBindGroups();
13354
+ if (previousMediumTextureResource?.ownsTexture) {
13355
+ previousMediumTextureResource.texture?.destroy?.();
13356
+ }
13357
+ }, updateSceneObjects = function(sceneObjects) {
13358
+ progressive?.reset();
13359
+ const nextPackedScene = packWavefrontSceneObjects(sceneObjects, config.sceneObjectCapacity);
13360
+ packedScene = nextPackedScene;
13361
+ const nextConfig = rebuildLiveConfig();
13362
+ if (!mediumTablesEqual(config.mediums, nextConfig.mediums)) {
13363
+ rebuildMediumResources(nextConfig);
13364
+ }
13365
+ config = nextConfig;
13366
+ device.queue.writeBuffer(sceneObjectBuffer, 0, packedScene.buffer);
13367
+ return config;
13368
+ }, updateCamera = function(cameraOptions = {}) {
13369
+ progressive?.reset();
13370
+ activeCameraOptions = cameraOptions;
13371
+ config = updateWavefrontCameraConfig(config, cameraOptions);
13372
+ return config;
13373
+ }, getSnapshot = function() {
13374
+ return Object.freeze({
13375
+ frame,
13376
+ accumulatedSamplesPerPixel: progressive?.samples ?? 0,
13377
+ width: config.width,
13378
+ height: config.height,
13379
+ maxDepth: config.maxDepth,
13380
+ tiles: tiles.length,
13381
+ tileSize: config.tileSize,
13382
+ samplesPerPixel: config.samplesPerPixel,
13383
+ maxFramePassesPerSubmission: config.maxFramePassesPerSubmission,
13384
+ sceneObjectCount: config.sceneObjectCount,
13385
+ triangleCount: config.triangleCount,
13386
+ instanceCount: config.instancing?.instanceCount ?? 0,
13387
+ geometryCount: config.instancing?.geometryCount ?? config.gpuMeshSource.meshes.count,
13388
+ emissiveTriangleCount: config.emissiveTriangleCount,
13389
+ environmentPortalCount: config.environmentPortalCount,
13390
+ environmentPortalMode: config.environmentPortalMode,
13391
+ mediumCount: config.mediumCount,
13392
+ environmentMap: createEnvironmentMapSnapshot(config.environmentMap),
13393
+ deferredPathResolve: config.deferredPathResolve,
13394
+ strictPhysicalLowSppLighting: config.strictPhysicalLowSppLighting,
13395
+ transportExperiments: config.transportExperiments,
13396
+ transportExperimentFlags: config.transportExperimentFlags,
13397
+ bvhNodeCount: config.bvhNodeCount,
13398
+ displayQuality: config.displayQuality,
13399
+ accelerationBuildMode: config.accelerationBuildMode,
13400
+ gpuAccelerationBuildRequired: config.gpuAccelerationBuildRequired,
13401
+ accelerationBuilt,
13402
+ accelerationBuildCount,
13403
+ frameConfigSlots: frameConfigSlotCount,
13404
+ gpuParallelism: lastGpuParallelism,
13405
+ memory: config.memory,
13406
+ allocations: allocationTracker.snapshot()
13407
+ });
13408
+ }, destroy = function() {
13409
+ progressive?.destroy();
13410
+ frameTelemetryResources?.destroy?.();
13411
+ activeQueue.destroy?.();
13412
+ nextQueue.destroy?.();
13413
+ hitBuffer.destroy?.();
13414
+ accumulationBuffer.destroy?.();
13415
+ pathVertexBuffer.destroy?.();
13416
+ sceneObjectBuffer.destroy?.();
13417
+ triangleBuffer.destroy?.();
13418
+ instanceTexture?.destroy?.();
13419
+ bvhNodeBuffer.destroy?.();
13420
+ meshVertexBuffer.destroy?.();
13421
+ meshIndexBuffer.destroy?.();
13422
+ meshRangeBuffer.destroy?.();
13423
+ environmentPortalBuffer.destroy?.();
13424
+ bvhLeafRefBuffer.destroy?.();
13425
+ configBuffer.destroy?.();
13426
+ bvhBuildConfigBuffer.destroy?.();
13427
+ counterBuffer.destroy?.();
13428
+ activeDispatchBuffer.destroy?.();
13429
+ radianceTexture.destroy?.();
13430
+ denoiseScratchTexture.destroy?.();
13431
+ outputTexture.destroy?.();
13432
+ if (environmentMapResource.ownsTexture) {
13433
+ environmentMapResource.texture?.destroy?.();
13434
+ }
13435
+ if (environmentSamplingResource.ownsTexture) {
13436
+ environmentSamplingResource.texture?.destroy?.();
13437
+ }
13438
+ if (mediumTextureResource.ownsTexture) {
13439
+ mediumTextureResource.texture?.destroy?.();
13440
+ }
13441
+ brdfLutResource.texture?.destroy?.();
13442
+ if (baseColorAtlasResource.ownsTexture) {
13443
+ baseColorAtlasResource.texture?.destroy?.();
13444
+ }
13445
+ if (metallicRoughnessAtlasResource.ownsTexture) {
13446
+ metallicRoughnessAtlasResource.texture?.destroy?.();
13447
+ }
13448
+ if (normalAtlasResource.ownsTexture) {
13449
+ normalAtlasResource.texture?.destroy?.();
13450
+ }
13451
+ if (occlusionAtlasResource.ownsTexture) {
13452
+ occlusionAtlasResource.texture?.destroy?.();
13453
+ }
13454
+ if (emissiveAtlasResource.ownsTexture) {
13455
+ emissiveAtlasResource.texture?.destroy?.();
13456
+ }
13457
+ Object.values(extensionAtlasResources).forEach((resource) => {
13458
+ if (resource.ownsTexture) {
13459
+ resource.texture?.destroy?.();
13460
+ }
13461
+ });
13462
+ context.unconfigure?.();
13463
+ allocationTracker.destroy();
13464
+ };
13465
+ finishAllocationScopes = openGpuAllocationErrorScopes(rawDevice);
13466
+ const gpuAdapterParallelism = createGpuAdapterParallelismDiagnostics(adapter, device);
13467
+ context = canvas.getContext("webgpu");
13468
+ if (!context || typeof context.configure !== "function") {
13469
+ throw new Error("Canvas WebGPU context does not support configure().");
13470
+ }
13471
+ const format = options.format ?? (typeof navigatorRef.gpu.getPreferredCanvasFormat === "function" ? navigatorRef.gpu.getPreferredCanvasFormat() : "bgra8unorm");
13472
+ let config = initialConfig;
13473
+ const safeTileSize = clampTileSizeForDevice(config, device);
13474
+ if (safeTileSize !== config.tileSize) {
13475
+ config = createWavefrontPathTracingComputeConfig({
13476
+ ...options,
13477
+ canvas,
13478
+ width: config.width,
13479
+ height: config.height,
13480
+ tileSize: safeTileSize,
13481
+ tilePixelCapacity: Math.min(
13482
+ config.tilePixelCapacity,
13483
+ safeTileSize * safeTileSize
13484
+ )
13124
13485
  });
13125
13486
  }
13126
- return frameTelemetryResources;
13127
- }
13128
- function resolveRenderedSamplesPerPixel(renderOptions = {}, awaitGPUCompletion = true) {
13129
- return resolveWavefrontRenderedSamplesPerPixel({
13130
- config,
13131
- renderOptions,
13132
- awaitGPUCompletion,
13133
- lastCompletedFrameTimeMs,
13134
- lastCompletedSamplesPerPixel
13487
+ config = Object.freeze({
13488
+ ...config,
13489
+ memory: estimateWavefrontPathTracingMemoryForConfig(config, device.limits)
13490
+ });
13491
+ canvas.width = config.width;
13492
+ canvas.height = config.height;
13493
+ context.configure({
13494
+ device: rawDevice,
13495
+ format,
13496
+ alphaMode: options.alpha === true ? "premultiplied" : "opaque"
13497
+ });
13498
+ const bufferUsage = constants.buffer.STORAGE | constants.buffer.COPY_DST;
13499
+ const memory = config.memory;
13500
+ let instanceTexture = null, instanceView = null;
13501
+ if (config.instancing) {
13502
+ const count = config.instancing.instanceCount;
13503
+ instanceTexture = device.createTexture({
13504
+ label: "plasius.wavefront.instances",
13505
+ size: { width: 9, height: count },
13506
+ format: "rgba32uint",
13507
+ usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
13508
+ });
13509
+ const upload = new Uint8Array(count * 256), source = new Uint8Array(config.instancing.buffer);
13510
+ for (let i = 0; i < count; i++) upload.set(source.subarray(i * 144, (i + 1) * 144), i * 256);
13511
+ device.queue.writeTexture({ texture: instanceTexture }, upload, { bytesPerRow: 256, rowsPerImage: count }, { width: 9, height: count, depthOrArrayLayers: 1 });
13512
+ instanceView = instanceTexture.createView();
13513
+ }
13514
+ const activeQueue = createBuffer(device, bufferUsage, memory.queueBytes, "plasius.wavefront.activeQueue");
13515
+ const nextQueue = createBuffer(device, bufferUsage, memory.queueBytes, "plasius.wavefront.nextQueue");
13516
+ const hitBuffer = createBuffer(device, bufferUsage, memory.hitBytes, "plasius.wavefront.hitBuffer");
13517
+ const accumulationBuffer = createBuffer(
13518
+ device,
13519
+ bufferUsage,
13520
+ memory.accumulationBytes,
13521
+ "plasius.wavefront.accumulation"
13522
+ );
13523
+ const pathVertexBuffer = createBuffer(
13524
+ device,
13525
+ bufferUsage,
13526
+ memory.pathVertexBytes,
13527
+ "plasius.wavefront.pathVertices"
13528
+ );
13529
+ const sceneObjectBuffer = createBuffer(
13530
+ device,
13531
+ constants.buffer.STORAGE | constants.buffer.COPY_DST,
13532
+ memory.sceneObjectBytes,
13533
+ "plasius.wavefront.sceneObjects"
13534
+ );
13535
+ const triangleBuffer = createBuffer(
13536
+ device,
13537
+ constants.buffer.STORAGE | constants.buffer.COPY_DST,
13538
+ memory.triangleBytes,
13539
+ "plasius.wavefront.triangles"
13540
+ );
13541
+ const bvhNodeBuffer = createBuffer(
13542
+ device,
13543
+ constants.buffer.STORAGE | constants.buffer.COPY_DST,
13544
+ memory.bvhCombinedBytes,
13545
+ "plasius.wavefront.bvhNodes"
13546
+ );
13547
+ const meshVertexBuffer = createBuffer(
13548
+ device,
13549
+ constants.buffer.STORAGE | constants.buffer.COPY_DST,
13550
+ memory.meshVertexBytes,
13551
+ "plasius.wavefront.meshVertices"
13552
+ );
13553
+ const meshIndexBuffer = createBuffer(
13554
+ device,
13555
+ constants.buffer.STORAGE | constants.buffer.COPY_DST,
13556
+ memory.meshIndexBytes,
13557
+ "plasius.wavefront.meshIndices"
13558
+ );
13559
+ const meshRangeBuffer = createBuffer(
13560
+ device,
13561
+ constants.buffer.STORAGE | constants.buffer.COPY_DST,
13562
+ memory.meshRangeBytes,
13563
+ "plasius.wavefront.meshRanges"
13564
+ );
13565
+ const environmentPortalBuffer = createBuffer(
13566
+ device,
13567
+ constants.buffer.STORAGE | constants.buffer.COPY_DST,
13568
+ memory.environmentPortalBytes,
13569
+ "plasius.wavefront.environmentPortals"
13570
+ );
13571
+ const bvhLeafRefBuffer = createBuffer(
13572
+ device,
13573
+ constants.buffer.STORAGE | constants.buffer.COPY_DST,
13574
+ memory.bvhLeafReferenceBytes,
13575
+ "plasius.wavefront.bvhLeafRefs"
13576
+ );
13577
+ const tiles = createTiles(config.width, config.height, config.tileSize);
13578
+ const configBufferStride = memory.configBufferStride;
13579
+ const outputConfigSlotCount = config.deferredPathResolve ? 0 : tiles.length;
13580
+ const frameConfigSlotCount = Math.max(
13581
+ 1,
13582
+ tiles.length * config.samplesPerPixel + outputConfigSlotCount + (config.denoise || config.progressive ? 1 : 0)
13583
+ );
13584
+ const configBuffer = createBuffer(
13585
+ device,
13586
+ constants.buffer.UNIFORM | constants.buffer.COPY_DST,
13587
+ memory.frameConfigBytes,
13588
+ "plasius.wavefront.frameConfig"
13589
+ );
13590
+ const bvhBuildConfigBuffer = createBuffer(
13591
+ device,
13592
+ constants.buffer.UNIFORM | constants.buffer.COPY_DST,
13593
+ memory.bvhBuildConfigBytes,
13594
+ "plasius.wavefront.bvhBuildConfig"
13595
+ );
13596
+ const counterBuffer = createBuffer(
13597
+ device,
13598
+ constants.buffer.STORAGE | constants.buffer.COPY_DST | constants.buffer.COPY_SRC,
13599
+ memory.counterBytes,
13600
+ "plasius.wavefront.counters"
13601
+ );
13602
+ const activeDispatchBuffer = createBuffer(
13603
+ device,
13604
+ constants.buffer.INDIRECT | constants.buffer.COPY_DST,
13605
+ memory.indirectDispatchBytes,
13606
+ "plasius.wavefront.activeDispatchArgs"
13607
+ );
13608
+ let packedScene = packWavefrontSceneObjects(config.sceneObjects, config.sceneObjectCapacity);
13609
+ device.queue.writeBuffer(sceneObjectBuffer, 0, packedScene.buffer);
13610
+ const packedTriangles = packWavefrontTriangles(
13611
+ config.meshAcceleration.triangles,
13612
+ Math.max(1, config.triangleCapacity)
13613
+ );
13614
+ const packedBvhNodes = packWavefrontBvhNodes(
13615
+ config.meshAcceleration.nodes,
13616
+ Math.max(1, config.bvhNodeCapacity + config.emissiveTriangleCapacity)
13617
+ );
13618
+ const packedBvhNodeUints = new Uint32Array(packedBvhNodes.buffer);
13619
+ const packedBvhNodeFloats = new Float32Array(packedBvhNodes.buffer);
13620
+ const orderedEmissiveTriangleIndices = resolveOrderedEmissiveTriangleIndices(config);
13621
+ const emissiveWeights = orderedEmissiveTriangleIndices.map(
13622
+ (triangleIndex) => emissiveTriangleWeight(config.meshAcceleration.triangles[triangleIndex])
13623
+ );
13624
+ const totalEmissiveWeight = emissiveWeights.reduce((total, weight) => total + weight, 0);
13625
+ let cumulativeEmissiveWeight = 0;
13626
+ orderedEmissiveTriangleIndices.forEach((triangleIndex, index) => {
13627
+ const nodeOffset = (config.bvhNodeCapacity + index) * (BVH_NODE_RECORD_BYTES / 4);
13628
+ cumulativeEmissiveWeight += emissiveWeights[index] ?? 0;
13629
+ packedBvhNodeFloats[nodeOffset] = cumulativeEmissiveWeight;
13630
+ packedBvhNodeFloats[nodeOffset + 1] = emissiveWeights[index] ?? 0;
13631
+ packedBvhNodeFloats[nodeOffset + 2] = totalEmissiveWeight;
13632
+ packedBvhNodeUints[nodeOffset + 8] = triangleIndex;
13135
13633
  });
13136
- }
13137
- function createFrameStats({
13138
- frameIndex,
13139
- accelerationBuildSubmitted,
13140
- frameSubmissionCount,
13141
- parallelismCounters,
13142
- renderedSamplesPerPixel,
13143
- targetSamplesPerPixel,
13144
- frameTimeBudgetMs,
13145
- budgetConstrained
13146
- }) {
13147
- const stats = createWavefrontFrameStats({
13148
- config,
13149
- tiles,
13150
- frame,
13151
- frameIndex,
13152
- accelerationBuilt,
13153
- accelerationBuildCount,
13154
- accelerationBuildSubmitted,
13155
- frameSubmissionCount,
13156
- frameConfigSlotCount,
13157
- gpuAdapterParallelism,
13158
- parallelismCounters,
13159
- renderedSamplesPerPixel,
13160
- targetSamplesPerPixel,
13161
- frameTimeBudgetMs,
13162
- budgetConstrained
13634
+ await uploadWavefrontBuffer(device, triangleBuffer, packedTriangles.buffer);
13635
+ await uploadWavefrontBuffer(device, bvhNodeBuffer, packedBvhNodes.buffer);
13636
+ await uploadWavefrontBuffer(device, meshVertexBuffer, config.gpuMeshSource.vertices.buffer);
13637
+ await uploadWavefrontBuffer(device, meshIndexBuffer, config.gpuMeshSource.indices.buffer);
13638
+ await uploadWavefrontBuffer(device, meshRangeBuffer, config.gpuMeshSource.meshes.buffer);
13639
+ const packedEnvironmentPortals = packEnvironmentPortals(
13640
+ config.environmentPortals,
13641
+ Math.max(1, config.environmentPortalCapacity)
13642
+ );
13643
+ device.queue.writeBuffer(environmentPortalBuffer, 0, packedEnvironmentPortals.buffer);
13644
+ const radianceTexture = device.createTexture({
13645
+ label: "plasius.wavefront.radiance",
13646
+ size: { width: config.width, height: config.height },
13647
+ format: "rgba16float",
13648
+ usage: constants.texture.STORAGE_BINDING | constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
13163
13649
  });
13164
- lastGpuParallelism = stats.gpuParallelism;
13165
- return stats.frameStats;
13166
- }
13167
- async function readTerminationMetrics() {
13168
- return readWavefrontTerminationMetrics({
13650
+ const radianceView = radianceTexture.createView();
13651
+ const denoiseScratchTexture = device.createTexture({
13652
+ label: "plasius.wavefront.denoiseScratch",
13653
+ size: { width: config.width, height: config.height },
13654
+ format: "rgba16float",
13655
+ usage: constants.texture.STORAGE_BINDING | constants.texture.TEXTURE_BINDING
13656
+ });
13657
+ const denoiseScratchView = denoiseScratchTexture.createView();
13658
+ const outputTexture = device.createTexture({
13659
+ label: "plasius.wavefront.output",
13660
+ size: { width: config.width, height: config.height },
13661
+ format: "rgba8unorm",
13662
+ usage: constants.texture.STORAGE_BINDING | constants.texture.TEXTURE_BINDING | constants.texture.COPY_SRC
13663
+ });
13664
+ const outputView = outputTexture.createView();
13665
+ const sampler = device.createSampler({
13666
+ label: "plasius.wavefront.presentSampler",
13667
+ magFilter: "nearest",
13668
+ minFilter: "nearest"
13669
+ });
13670
+ const environmentMapResource = createEnvironmentMapResource(
13169
13671
  device,
13170
13672
  constants,
13171
- counterBuffer,
13172
- waitForSubmittedGpuWork: waitForSubmittedGpuWork2
13173
- });
13174
- }
13175
- function writeFrameConfigSlot(slot, tile, frameIndex, buildRange = {}) {
13176
- if (slot >= frameConfigSlotCount) {
13177
- throw new Error("Wavefront frame config slot capacity exceeded.");
13178
- }
13179
- const offset = slot * configBufferStride;
13180
- device.queue.writeBuffer(
13181
- configBuffer,
13182
- offset,
13183
- createConfigPayload(config, tile, frameIndex, buildRange)
13673
+ config.environmentMap,
13674
+ config.environmentColor
13184
13675
  );
13185
- return offset;
13186
- }
13187
- function createFrameConfigWriter(frameIndex) {
13188
- let slot = 0;
13189
- return (tile, buildRange = {}) => {
13190
- const offset = writeFrameConfigSlot(slot, tile, frameIndex, buildRange);
13191
- slot += 1;
13192
- return offset;
13193
- };
13194
- }
13195
- function dispatchGpuAccelerationBuild(frameIndex, parallelism) {
13196
- const submitted = dispatchWavefrontGpuAccelerationBuild({
13197
- config,
13198
- accelerationBuilt,
13199
- tiles,
13676
+ const environmentSamplingResource = createEnvironmentSamplingTextureResource(
13200
13677
  device,
13201
- bvhBuildConfigBuffer,
13202
- configBufferStride,
13203
- bvhBuildBindGroup,
13204
- pipelines,
13205
- parallelism,
13206
- frameIndex
13207
- });
13208
- if (submitted) {
13209
- accelerationBuilt = true;
13210
- accelerationBuildCount += 1;
13211
- }
13212
- return submitted;
13213
- }
13214
- function dispatchFrame(frameIndex, parallelism, renderedSamplesPerPixel = config.samplesPerPixel) {
13215
- return dispatchWavefrontFrame({
13216
- config,
13217
- tiles,
13678
+ constants,
13679
+ config.environmentMap,
13680
+ config.environmentColor
13681
+ );
13682
+ let mediumTextureResource = createMediumTextureResource(
13218
13683
  device,
13219
- frameIndex,
13220
- parallelism,
13221
- renderedSamplesPerPixel,
13222
- frameEncoder,
13223
- createFrameConfigWriter
13224
- });
13225
- }
13226
- function renderOnce(renderOptions = {}, resolvedSamplingPlan = null) {
13227
- const frameStartTimeMs = nowMs3();
13228
- frame += 1;
13229
- const frameIndex = frame + config.frameIndex;
13230
- const samplingPlan = resolvedSamplingPlan ?? resolveRenderedSamplesPerPixel(renderOptions, false);
13231
- const parallelismCounters = createGpuParallelismCounters();
13232
- const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex, parallelismCounters);
13233
- const frameSubmissionCount = dispatchFrame(
13234
- frameIndex,
13235
- parallelismCounters,
13236
- samplingPlan.renderedSamplesPerPixel
13684
+ constants,
13685
+ config.mediums
13237
13686
  );
13238
- const frameTimeMs = Math.max(0, nowMs3() - frameStartTimeMs);
13239
- return Object.freeze({
13240
- ...createFrameStats({
13241
- frameIndex,
13242
- accelerationBuildSubmitted,
13243
- frameSubmissionCount,
13244
- parallelismCounters,
13245
- renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel,
13246
- targetSamplesPerPixel: samplingPlan.targetSamplesPerPixel,
13247
- frameTimeBudgetMs: samplingPlan.frameTimeBudgetMs,
13248
- budgetConstrained: samplingPlan.budgetConstrained
13249
- }),
13250
- gpuWorkerJobs: createGpuWorkerJobDiagnostics(
13251
- lastGpuParallelism,
13252
- frameSubmissionCount + (accelerationBuildSubmitted ? 1 : 0),
13253
- frameTimeMs,
13254
- false
13255
- ),
13256
- timings: createWavefrontFrameTimingTelemetry({
13257
- status: "not-requested",
13258
- source: "cpu-submit",
13259
- timestampQueryStatus: "not-recorded",
13260
- totalRenderJobTimeMs: frameTimeMs
13687
+ config = Object.freeze({
13688
+ ...config,
13689
+ environmentMap: Object.freeze({
13690
+ ...config.environmentMap,
13691
+ width: environmentMapResource.width,
13692
+ height: environmentMapResource.height,
13693
+ mipLevelCount: environmentMapResource.mipLevelCount,
13694
+ hasImportanceData: environmentSamplingResource.hasImportanceData
13261
13695
  })
13262
13696
  });
13263
- }
13264
- function dispatchFrameAwaitingGpu(frameIndex, parallelism, renderedSamplesPerPixel = config.samplesPerPixel, optionsForFrame = {}) {
13265
- return dispatchWavefrontFrameAwaitingGpu({
13266
- config,
13267
- tiles,
13697
+ const brdfLutResource = createBrdfLutResource(device, constants);
13698
+ const baseColorAtlasResource = createAtlasTextureResource(
13268
13699
  device,
13269
- frameIndex,
13270
- parallelism,
13271
- renderedSamplesPerPixel,
13272
- frameEncoder,
13273
- writeFrameConfigSlot,
13274
- optionsForFrame
13275
- });
13276
- }
13277
- async function readOutputProbe(optionsForProbe = {}) {
13278
- return readWavefrontOutputProbe({
13700
+ constants,
13701
+ config.gpuMaterialSource.baseColorAtlas,
13702
+ "plasius.wavefront.materialAtlas.baseColor"
13703
+ );
13704
+ const metallicRoughnessAtlasResource = createAtlasTextureResource(
13279
13705
  device,
13280
13706
  constants,
13281
- config,
13282
- outputTexture,
13283
- waitForSubmittedGpuWork: waitForSubmittedGpuWork2,
13284
- optionsForProbe
13707
+ config.gpuMaterialSource.metallicRoughnessAtlas,
13708
+ "plasius.wavefront.materialAtlas.metallicRoughness"
13709
+ );
13710
+ const normalAtlasResource = createAtlasTextureResource(
13711
+ device,
13712
+ constants,
13713
+ config.gpuMaterialSource.normalAtlas,
13714
+ "plasius.wavefront.materialAtlas.normal"
13715
+ );
13716
+ const occlusionAtlasResource = createAtlasTextureResource(
13717
+ device,
13718
+ constants,
13719
+ config.gpuMaterialSource.occlusionAtlas,
13720
+ "plasius.wavefront.materialAtlas.occlusion"
13721
+ );
13722
+ const emissiveAtlasResource = createAtlasTextureResource(
13723
+ device,
13724
+ constants,
13725
+ config.gpuMaterialSource.emissiveAtlas,
13726
+ "plasius.wavefront.materialAtlas.emissive"
13727
+ );
13728
+ const extensionAtlasNames = [
13729
+ "clearcoat",
13730
+ "clearcoatRoughness",
13731
+ "clearcoatNormal",
13732
+ "transmission",
13733
+ "thickness",
13734
+ "sheenColor",
13735
+ "sheenRoughness",
13736
+ "specular",
13737
+ "specularColor",
13738
+ "iridescence",
13739
+ "iridescenceThickness",
13740
+ "anisotropy"
13741
+ ];
13742
+ const extensionAtlasResources = Object.freeze(
13743
+ Object.fromEntries(
13744
+ extensionAtlasNames.map((name) => [
13745
+ name,
13746
+ createAtlasTextureResource(
13747
+ device,
13748
+ constants,
13749
+ config.gpuMaterialSource.extensionAtlases[name],
13750
+ `plasius.wavefront.materialAtlas.${name}`
13751
+ )
13752
+ ])
13753
+ )
13754
+ );
13755
+ const materialAtlasSampler = device.createSampler({
13756
+ label: "plasius.wavefront.materialAtlasSampler",
13757
+ addressModeU: "clamp-to-edge",
13758
+ addressModeV: "clamp-to-edge",
13759
+ magFilter: "linear",
13760
+ minFilter: "linear"
13285
13761
  });
13286
- }
13287
- async function renderFrame(renderOptions = {}) {
13288
- const awaitGPUCompletion = renderOptions.awaitGPUCompletion !== false;
13289
- const samplingPlan = resolveRenderedSamplesPerPixel(renderOptions, awaitGPUCompletion);
13290
- const useThrottledHighSamplePath = awaitGPUCompletion && samplingPlan.renderedSamplesPerPixel >= 8;
13291
- const telemetryRequested = renderOptions.readStats === true;
13292
- const telemetryCanRecord = telemetryRequested && awaitGPUCompletion;
13293
- const telemetryResourcesForFrame = telemetryCanRecord ? ensureFrameTelemetryResources() : null;
13294
- if (telemetryResourcesForFrame?.available) {
13295
- telemetryResourcesForFrame.beginFrame();
13296
- activeFrameTelemetry = telemetryResourcesForFrame;
13297
- }
13298
- const frameStartTimeMs = nowMs3();
13299
- let frameStats;
13300
- try {
13301
- if (useThrottledHighSamplePath) {
13302
- frame += 1;
13303
- const frameIndex = frame + config.frameIndex;
13304
- const parallelismCounters = createGpuParallelismCounters();
13305
- const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex, parallelismCounters);
13306
- let frameSubmissionCount = 0;
13307
- let frameConfigSlot = 0;
13308
- if (accelerationBuildSubmitted) {
13309
- const accelerationWaitOptions = {
13310
- ...estimateSubmittedGpuWorkTiming(
13311
- { ...config, renderedSamplesPerPixel: 1 },
13312
- 1,
13313
- renderOptions.submittedWorkTimeoutMs,
13314
- { includeAccelerationBuild: true }
13315
- ),
13316
- allowTimeout: false
13317
- };
13318
- await waitForSubmittedGpuWork2(accelerationWaitOptions);
13319
- }
13320
- for (let tileIndex = 0; tileIndex < tiles.length; tileIndex += 1) {
13321
- const tileRangeDispatch = dispatchFrameAwaitingGpu(
13322
- frameIndex,
13323
- parallelismCounters,
13324
- samplingPlan.renderedSamplesPerPixel,
13325
- {
13326
- sampleRangeStart: 0,
13327
- sampleRangeEnd: samplingPlan.renderedSamplesPerPixel,
13328
- tileStartIndex: tileIndex,
13329
- tileEndIndex: tileIndex + 1,
13330
- startingSubmissionCount: frameSubmissionCount,
13331
- startingSlot: frameConfigSlot,
13332
- includeDenoise: tileIndex + 1 >= tiles.length,
13333
- includePresent: tileIndex + 1 >= tiles.length
13334
- }
13335
- );
13336
- frameSubmissionCount = tileRangeDispatch.submissionCount;
13337
- frameConfigSlot = tileRangeDispatch.slot;
13338
- const tileWaitOptions = {
13339
- ...estimateSubmittedGpuWorkTiming(
13340
- { ...config, renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel },
13341
- 1,
13342
- renderOptions.submittedWorkTimeoutMs,
13762
+ const {
13763
+ bindGroupLayouts: {
13764
+ trace: traceBindGroupLayout,
13765
+ acceleration: accelerationBindGroupLayout,
13766
+ denoiseRadiance: denoiseRadianceBindGroupLayout,
13767
+ denoiseResolve: denoiseResolveBindGroupLayout,
13768
+ present: presentBindGroupLayout
13769
+ },
13770
+ computePipelines: pipelines,
13771
+ presentPipeline
13772
+ } = await createWavefrontPipelineResources({ device, constants, format, instanced: !!config.instancing });
13773
+ let bindGroups = createTraceBindGroups();
13774
+ const bvhBuildBindGroup = createWavefrontBvhBuildBindGroup({
13775
+ device,
13776
+ accelerationBindGroupLayout,
13777
+ bvhBuildConfigBuffer,
13778
+ triangleBuffer,
13779
+ bvhNodeBuffer,
13780
+ meshVertexBuffer,
13781
+ meshIndexBuffer,
13782
+ meshRangeBuffer,
13783
+ bvhLeafRefBuffer
13784
+ });
13785
+ const {
13786
+ radiance: denoiseRadianceBindGroup,
13787
+ resolve: denoiseResolveBindGroup,
13788
+ directResolve: denoiseDirectResolveBindGroup
13789
+ } = createWavefrontDenoiseBindGroups({
13790
+ device,
13791
+ denoiseRadianceBindGroupLayout,
13792
+ denoiseResolveBindGroupLayout,
13793
+ configBuffer,
13794
+ radianceView,
13795
+ denoiseScratchView,
13796
+ outputView
13797
+ });
13798
+ const presentBindGroup = createWavefrontPresentBindGroup({
13799
+ device,
13800
+ presentBindGroupLayout,
13801
+ outputView,
13802
+ sampler
13803
+ });
13804
+ const progressive = await createWavefrontProgressiveResources({ device, constants, config, configBuffer, radianceTexture, radianceView, outputView });
13805
+ let activeFrameTelemetry = null;
13806
+ let frameTelemetryResources = null;
13807
+ const frameEncoder = createWavefrontFrameEncoder({
13808
+ getConfig: () => config,
13809
+ progressive,
13810
+ getBindGroups: () => bindGroups,
13811
+ pipelines,
13812
+ counterBuffer,
13813
+ activeDispatchBuffer,
13814
+ denoiseRadianceBindGroup,
13815
+ denoiseResolveBindGroup,
13816
+ denoiseDirectResolveBindGroup,
13817
+ presentPipeline,
13818
+ presentBindGroup,
13819
+ context,
13820
+ getFrameTelemetry: () => activeFrameTelemetry
13821
+ });
13822
+ let frame = 0;
13823
+ let accelerationBuilt = !config.gpuAccelerationBuildRequired;
13824
+ let accelerationBuildCount = 0;
13825
+ let activeCameraOptions = options.camera ?? DEFAULT_CAMERA2;
13826
+ let lastCompletedFrameTimeMs = null;
13827
+ let lastCompletedSamplesPerPixel = Math.max(1, config.samplesPerPixel);
13828
+ let lastGpuParallelism = createGpuParallelismDiagnostics(
13829
+ gpuAdapterParallelism,
13830
+ createGpuParallelismCounters()
13831
+ );
13832
+ const waitForSubmittedGpuWork2 = (waitOptions = {}) => waitForSubmittedGpuWork(device, waitOptions);
13833
+ async function readTerminationMetrics() {
13834
+ return readWavefrontTerminationMetrics({
13835
+ device,
13836
+ constants,
13837
+ counterBuffer,
13838
+ waitForSubmittedGpuWork: waitForSubmittedGpuWork2
13839
+ });
13840
+ }
13841
+ async function readOutputProbe(optionsForProbe = {}) {
13842
+ return readWavefrontOutputProbe({
13843
+ device,
13844
+ constants,
13845
+ config,
13846
+ outputTexture,
13847
+ waitForSubmittedGpuWork: waitForSubmittedGpuWork2,
13848
+ optionsForProbe
13849
+ });
13850
+ }
13851
+ async function renderFrame(renderOptions = {}) {
13852
+ const awaitGPUCompletion = renderOptions.awaitGPUCompletion !== false;
13853
+ const samplingPlan = resolveRenderedSamplesPerPixel(renderOptions, awaitGPUCompletion);
13854
+ const useThrottledHighSamplePath = awaitGPUCompletion && samplingPlan.renderedSamplesPerPixel >= 8;
13855
+ const telemetryRequested = renderOptions.readStats === true;
13856
+ const telemetryCanRecord = telemetryRequested && awaitGPUCompletion;
13857
+ const telemetryResourcesForFrame = telemetryCanRecord ? ensureFrameTelemetryResources() : null;
13858
+ if (telemetryResourcesForFrame?.available) {
13859
+ telemetryResourcesForFrame.beginFrame();
13860
+ activeFrameTelemetry = telemetryResourcesForFrame;
13861
+ }
13862
+ const frameStartTimeMs = nowMs3();
13863
+ let frameStats;
13864
+ try {
13865
+ if (useThrottledHighSamplePath) {
13866
+ frame += 1;
13867
+ const frameIndex = frame + config.frameIndex;
13868
+ const parallelismCounters = createGpuParallelismCounters();
13869
+ const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex, parallelismCounters);
13870
+ let frameSubmissionCount = 0;
13871
+ let frameConfigSlot = 0;
13872
+ if (accelerationBuildSubmitted) {
13873
+ const accelerationWaitOptions = {
13874
+ ...estimateSubmittedGpuWorkTiming(
13875
+ { ...config, renderedSamplesPerPixel: 1 },
13876
+ 1,
13877
+ renderOptions.submittedWorkTimeoutMs,
13878
+ { includeAccelerationBuild: true }
13879
+ ),
13880
+ allowTimeout: false
13881
+ };
13882
+ await waitForSubmittedGpuWork2(accelerationWaitOptions);
13883
+ }
13884
+ for (let tileIndex = 0; tileIndex < tiles.length; tileIndex += 1) {
13885
+ const tileRangeDispatch = dispatchFrameAwaitingGpu(
13886
+ frameIndex,
13887
+ parallelismCounters,
13888
+ samplingPlan.renderedSamplesPerPixel,
13343
13889
  {
13344
- includeDenoise: tileIndex + 1 >= tiles.length && config.denoise,
13890
+ sampleRangeStart: 0,
13891
+ sampleRangeEnd: samplingPlan.renderedSamplesPerPixel,
13892
+ tileStartIndex: tileIndex,
13893
+ tileEndIndex: tileIndex + 1,
13894
+ startingSubmissionCount: frameSubmissionCount,
13895
+ startingSlot: frameConfigSlot,
13896
+ includeDenoise: tileIndex + 1 >= tiles.length,
13345
13897
  includePresent: tileIndex + 1 >= tiles.length
13346
13898
  }
13347
- ),
13899
+ );
13900
+ frameSubmissionCount = tileRangeDispatch.submissionCount;
13901
+ frameConfigSlot = tileRangeDispatch.slot;
13902
+ const tileWaitOptions = {
13903
+ ...estimateSubmittedGpuWorkTiming(
13904
+ { ...config, renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel },
13905
+ 1,
13906
+ renderOptions.submittedWorkTimeoutMs,
13907
+ {
13908
+ includeDenoise: tileIndex + 1 >= tiles.length && config.denoise,
13909
+ includePresent: tileIndex + 1 >= tiles.length
13910
+ }
13911
+ ),
13912
+ allowTimeout: false
13913
+ };
13914
+ await waitForSubmittedGpuWork2(tileWaitOptions);
13915
+ }
13916
+ frameStats = createFrameStats({
13917
+ frameIndex,
13918
+ accelerationBuildSubmitted,
13919
+ frameSubmissionCount,
13920
+ parallelismCounters,
13921
+ renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel,
13922
+ targetSamplesPerPixel: samplingPlan.targetSamplesPerPixel,
13923
+ frameTimeBudgetMs: samplingPlan.frameTimeBudgetMs,
13924
+ budgetConstrained: samplingPlan.budgetConstrained
13925
+ });
13926
+ } else {
13927
+ const submittedWorkTiming = estimateSubmittedGpuWorkTiming(
13928
+ { ...config, renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel },
13929
+ tiles.length,
13930
+ renderOptions.submittedWorkTimeoutMs,
13931
+ { includeAccelerationBuild: config.gpuAccelerationBuildRequired && !accelerationBuilt }
13932
+ );
13933
+ const submissionWaitOptions = awaitGPUCompletion ? {
13934
+ timeoutMs: submittedWorkTiming.timeoutMs,
13935
+ maxWaitMs: submittedWorkTiming.maxWaitMs,
13348
13936
  allowTimeout: false
13937
+ } : {
13938
+ timeoutMs: submittedWorkTiming.timeoutMs,
13939
+ maxWaitMs: submittedWorkTiming.maxWaitMs
13349
13940
  };
13350
- await waitForSubmittedGpuWork2(tileWaitOptions);
13351
- }
13352
- frameStats = createFrameStats({
13353
- frameIndex,
13354
- accelerationBuildSubmitted,
13355
- frameSubmissionCount,
13356
- parallelismCounters,
13357
- renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel,
13358
- targetSamplesPerPixel: samplingPlan.targetSamplesPerPixel,
13359
- frameTimeBudgetMs: samplingPlan.frameTimeBudgetMs,
13360
- budgetConstrained: samplingPlan.budgetConstrained
13361
- });
13362
- } else {
13363
- const submittedWorkTiming = estimateSubmittedGpuWorkTiming(
13364
- { ...config, renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel },
13365
- tiles.length,
13366
- renderOptions.submittedWorkTimeoutMs,
13367
- { includeAccelerationBuild: config.gpuAccelerationBuildRequired && !accelerationBuilt }
13368
- );
13369
- const submissionWaitOptions = awaitGPUCompletion ? {
13370
- timeoutMs: submittedWorkTiming.timeoutMs,
13371
- maxWaitMs: submittedWorkTiming.maxWaitMs,
13372
- allowTimeout: false
13373
- } : {
13374
- timeoutMs: submittedWorkTiming.timeoutMs,
13375
- maxWaitMs: submittedWorkTiming.maxWaitMs
13376
- };
13377
- frameStats = renderOnce(renderOptions, samplingPlan);
13378
- if (awaitGPUCompletion) {
13379
- await waitForSubmittedGpuWork2(submissionWaitOptions);
13941
+ frameStats = renderOnce(renderOptions, samplingPlan);
13942
+ if (awaitGPUCompletion) {
13943
+ await waitForSubmittedGpuWork2(submissionWaitOptions);
13944
+ }
13380
13945
  }
13946
+ } finally {
13947
+ activeFrameTelemetry = null;
13381
13948
  }
13382
- } finally {
13383
- activeFrameTelemetry = null;
13384
- }
13385
- const frameTimeMs = Math.max(0, nowMs3() - frameStartTimeMs);
13386
- if (awaitGPUCompletion) {
13387
- lastCompletedFrameTimeMs = frameTimeMs;
13388
- lastCompletedSamplesPerPixel = frameStats.renderedSamplesPerPixel ?? frameStats.samplesPerPixel;
13389
- }
13390
- const expectedPrimaryRays = config.width * config.height * samplingPlan.renderedSamplesPerPixel;
13391
- const expectedRayCounts = tiles.length * samplingPlan.renderedSamplesPerPixel * config.maxDepth;
13392
- const telemetryReadback = telemetryCanRecord ? await telemetryResourcesForFrame.readFrame({
13393
- expectedPrimaryRays,
13394
- expectedRayCounts,
13395
- waitForSubmittedGpuWork: waitForSubmittedGpuWork2
13396
- }) : null;
13397
- const rayCounts = !telemetryRequested ? createWavefrontRayCountTelemetry() : !awaitGPUCompletion ? createWavefrontRayCountTelemetry({
13398
- status: "unavailable",
13399
- expectedPrimaryRays,
13400
- expectedRayCounts,
13401
- reason: "gpu-work-not-awaited"
13402
- }) : telemetryReadback.rayCounts;
13403
- const timings = !telemetryRequested ? createWavefrontFrameTimingTelemetry({
13404
- status: "not-requested",
13405
- source: awaitGPUCompletion ? "queue-completion" : "cpu-submit",
13406
- timestampQueryStatus: "not-recorded",
13407
- totalRenderJobTimeMs: frameTimeMs
13408
- }) : !awaitGPUCompletion ? createWavefrontFrameTimingTelemetry({
13409
- status: "unavailable",
13410
- source: "cpu-submit",
13411
- timestampQueryStatus: "not-recorded",
13412
- totalRenderJobTimeMs: frameTimeMs,
13413
- reason: "gpu-work-not-awaited"
13414
- }) : telemetryReadback.timestampQueryStatus === "available" ? createWavefrontFrameTimingTelemetry({
13415
- status: "available",
13416
- source: "timestamp-query",
13417
- timestampQueryStatus: "available",
13418
- totalGpuTimeMs: telemetryReadback.totalGpuTimeMs,
13419
- totalRenderJobTimeMs: frameTimeMs
13420
- }) : createWavefrontFrameTimingTelemetry({
13421
- status: "fallback",
13422
- source: "queue-completion",
13423
- timestampQueryStatus: telemetryReadback.timestampQueryStatus,
13424
- totalRenderJobTimeMs: frameTimeMs,
13425
- reason: telemetryReadback.reason
13426
- });
13427
- const deviceLossStatus = awaitGPUCompletion ? "not-detected" : typeof device.lost?.then === "function" ? "pending" : "not-exposed";
13428
- frameStats = Object.freeze({
13429
- ...frameStats,
13430
- deviceLossStatus,
13431
- secondaryRays: rayCounts.status === "available" ? rayCounts.secondaryRays : null,
13432
- totalPathSegments: rayCounts.status === "available" ? rayCounts.totalPathSegments : null,
13433
- rayCounts,
13434
- timings,
13435
- telemetryMemoryBytes: telemetryResourcesForFrame?.memoryBytes ?? 0,
13436
- gpuWorkerJobs: createGpuWorkerJobDiagnostics(
13437
- frameStats.gpuParallelism,
13438
- frameStats.commandSubmissions,
13439
- frameTimeMs,
13440
- awaitGPUCompletion
13441
- )
13442
- });
13443
- const terminationMetrics = awaitGPUCompletion && renderOptions.readStats === true ? await readTerminationMetrics() : EMPTY_TERMINATION_METRICS;
13444
- const probe = renderOptions.readOutputProbe === false ? null : await readOutputProbe(renderOptions.probe);
13445
- const maxChannel = probe ? Math.max(...probe.rgba.slice(0, 3)) : 0;
13446
- const completedFrame = Object.freeze({
13447
- ...frameStats,
13448
- outputProbe: probe ? Object.freeze({
13449
- ...probe,
13450
- sampledPixels: 1,
13451
- nonZeroSamples: maxChannel > 0 ? 1 : 0,
13452
- maxChannel
13453
- }) : null,
13454
- bounces: [],
13455
- termination: terminationMetrics.termination,
13456
- terminalRadiance: terminationMetrics.terminalRadiance,
13457
- radianceDiagnostics: terminationMetrics.radianceDiagnostics,
13458
- transportContributions: terminationMetrics.transportContributions,
13459
- queueOverflow: terminationMetrics.queueOverflow
13460
- });
13949
+ const frameTimeMs = Math.max(0, nowMs3() - frameStartTimeMs);
13950
+ if (awaitGPUCompletion) {
13951
+ lastCompletedFrameTimeMs = frameTimeMs;
13952
+ lastCompletedSamplesPerPixel = frameStats.renderedSamplesPerPixel ?? frameStats.samplesPerPixel;
13953
+ }
13954
+ const expectedPrimaryRays = config.width * config.height * samplingPlan.renderedSamplesPerPixel;
13955
+ const expectedRayCounts = tiles.length * samplingPlan.renderedSamplesPerPixel * config.maxDepth;
13956
+ const telemetryReadback = telemetryCanRecord ? await telemetryResourcesForFrame.readFrame({
13957
+ expectedPrimaryRays,
13958
+ expectedRayCounts,
13959
+ waitForSubmittedGpuWork: waitForSubmittedGpuWork2
13960
+ }) : null;
13961
+ const rayCounts = !telemetryRequested ? createWavefrontRayCountTelemetry() : !awaitGPUCompletion ? createWavefrontRayCountTelemetry({
13962
+ status: "unavailable",
13963
+ expectedPrimaryRays,
13964
+ expectedRayCounts,
13965
+ reason: "gpu-work-not-awaited"
13966
+ }) : telemetryReadback.rayCounts;
13967
+ const timings = !telemetryRequested ? createWavefrontFrameTimingTelemetry({
13968
+ status: "not-requested",
13969
+ source: awaitGPUCompletion ? "queue-completion" : "cpu-submit",
13970
+ timestampQueryStatus: "not-recorded",
13971
+ totalRenderJobTimeMs: frameTimeMs
13972
+ }) : !awaitGPUCompletion ? createWavefrontFrameTimingTelemetry({
13973
+ status: "unavailable",
13974
+ source: "cpu-submit",
13975
+ timestampQueryStatus: "not-recorded",
13976
+ totalRenderJobTimeMs: frameTimeMs,
13977
+ reason: "gpu-work-not-awaited"
13978
+ }) : telemetryReadback.timestampQueryStatus === "available" ? createWavefrontFrameTimingTelemetry({
13979
+ status: "available",
13980
+ source: "timestamp-query",
13981
+ timestampQueryStatus: "available",
13982
+ totalGpuTimeMs: telemetryReadback.totalGpuTimeMs,
13983
+ totalRenderJobTimeMs: frameTimeMs
13984
+ }) : createWavefrontFrameTimingTelemetry({
13985
+ status: "fallback",
13986
+ source: "queue-completion",
13987
+ timestampQueryStatus: telemetryReadback.timestampQueryStatus,
13988
+ totalRenderJobTimeMs: frameTimeMs,
13989
+ reason: telemetryReadback.reason
13990
+ });
13991
+ const deviceLossStatus = awaitGPUCompletion ? "not-detected" : typeof device.lost?.then === "function" ? "pending" : "not-exposed";
13992
+ frameStats = Object.freeze({
13993
+ ...frameStats,
13994
+ deviceLossStatus,
13995
+ secondaryRays: rayCounts.status === "available" ? rayCounts.secondaryRays : null,
13996
+ totalPathSegments: rayCounts.status === "available" ? rayCounts.totalPathSegments : null,
13997
+ rayCounts,
13998
+ timings,
13999
+ telemetryMemoryBytes: telemetryResourcesForFrame?.memoryBytes ?? 0,
14000
+ gpuWorkerJobs: createGpuWorkerJobDiagnostics(
14001
+ frameStats.gpuParallelism,
14002
+ frameStats.commandSubmissions,
14003
+ frameTimeMs,
14004
+ awaitGPUCompletion
14005
+ )
14006
+ });
14007
+ const terminationMetrics = awaitGPUCompletion && renderOptions.readStats === true ? await readTerminationMetrics() : EMPTY_TERMINATION_METRICS;
14008
+ const probe = renderOptions.readOutputProbe === false ? null : await readOutputProbe(renderOptions.probe);
14009
+ const maxChannel = probe ? Math.max(...probe.rgba.slice(0, 3)) : 0;
14010
+ const completedFrame = Object.freeze({
14011
+ ...frameStats,
14012
+ outputProbe: probe ? Object.freeze({
14013
+ ...probe,
14014
+ sampledPixels: 1,
14015
+ nonZeroSamples: maxChannel > 0 ? 1 : 0,
14016
+ maxChannel
14017
+ }) : null,
14018
+ bounces: [],
14019
+ termination: terminationMetrics.termination,
14020
+ terminalRadiance: terminationMetrics.terminalRadiance,
14021
+ radianceDiagnostics: terminationMetrics.radianceDiagnostics,
14022
+ transportContributions: terminationMetrics.transportContributions,
14023
+ queueOverflow: terminationMetrics.queueOverflow
14024
+ });
14025
+ return Object.freeze({
14026
+ ...completedFrame,
14027
+ transportGuardrails: createWavefrontTransportGuardrailSummary(completedFrame)
14028
+ });
14029
+ }
14030
+ await finishAllocationScopes();
13461
14031
  return Object.freeze({
13462
- ...completedFrame,
13463
- transportGuardrails: createWavefrontTransportGuardrailSummary(completedFrame)
13464
- });
13465
- }
13466
- function rebuildLiveConfig(overrides = {}) {
13467
- return createWavefrontPathTracingComputeConfig({
13468
- ...options,
13469
14032
  canvas,
13470
- width: config.width,
13471
- height: config.height,
13472
- maxDepth: config.maxDepth,
13473
- tileSize: config.tileSize,
13474
- samplesPerPixel: config.samplesPerPixel,
13475
- sceneObjectCapacity: config.sceneObjectCapacity,
13476
- sceneObjects: packedScene.objects,
13477
- camera: activeCameraOptions,
13478
- environmentMap: {
13479
- ...config.environmentMap
13480
- },
13481
- strictPhysicalLowSppLighting: config.strictPhysicalLowSppLighting,
13482
- featureFlags: options.featureFlags,
13483
- frameIndex: config.frameIndex,
13484
- ...overrides
14033
+ context,
14034
+ device: rawDevice,
14035
+ format,
14036
+ config,
14037
+ renderOnce,
14038
+ renderFrame,
14039
+ readOutputProbe,
14040
+ updateSceneObjects,
14041
+ updateCamera,
14042
+ resetAccumulation: () => progressive?.reset(),
14043
+ getSnapshot,
14044
+ destroy
13485
14045
  });
13486
- }
13487
- function rebuildMediumResources(nextConfig) {
13488
- const previousMediumTextureResource = mediumTextureResource;
13489
- mediumTextureResource = createMediumTextureResource(device, constants, nextConfig.mediums);
13490
- bindGroups = createTraceBindGroups();
13491
- if (previousMediumTextureResource?.ownsTexture) {
13492
- previousMediumTextureResource.texture?.destroy?.();
13493
- }
13494
- }
13495
- function updateSceneObjects(sceneObjects) {
13496
- progressive?.reset();
13497
- const nextPackedScene = packWavefrontSceneObjects(sceneObjects, config.sceneObjectCapacity);
13498
- packedScene = nextPackedScene;
13499
- const nextConfig = rebuildLiveConfig();
13500
- if (!mediumTablesEqual(config.mediums, nextConfig.mediums)) {
13501
- rebuildMediumResources(nextConfig);
14046
+ } catch (error) {
14047
+ try {
14048
+ await finishAllocationScopes?.();
14049
+ } catch {
13502
14050
  }
13503
- config = nextConfig;
13504
- device.queue.writeBuffer(sceneObjectBuffer, 0, packedScene.buffer);
13505
- return config;
13506
- }
13507
- function updateCamera(cameraOptions = {}) {
13508
- progressive?.reset();
13509
- activeCameraOptions = cameraOptions;
13510
- config = updateWavefrontCameraConfig(config, cameraOptions);
13511
- return config;
13512
- }
13513
- function getSnapshot() {
13514
- return Object.freeze({
13515
- frame,
13516
- accumulatedSamplesPerPixel: progressive?.samples ?? 0,
13517
- width: config.width,
13518
- height: config.height,
13519
- maxDepth: config.maxDepth,
13520
- tiles: tiles.length,
13521
- tileSize: config.tileSize,
13522
- samplesPerPixel: config.samplesPerPixel,
13523
- maxFramePassesPerSubmission: config.maxFramePassesPerSubmission,
13524
- sceneObjectCount: config.sceneObjectCount,
13525
- triangleCount: config.triangleCount,
13526
- emissiveTriangleCount: config.emissiveTriangleCount,
13527
- environmentPortalCount: config.environmentPortalCount,
13528
- environmentPortalMode: config.environmentPortalMode,
13529
- mediumCount: config.mediumCount,
13530
- environmentMap: createEnvironmentMapSnapshot(config.environmentMap),
13531
- deferredPathResolve: config.deferredPathResolve,
13532
- strictPhysicalLowSppLighting: config.strictPhysicalLowSppLighting,
13533
- transportExperiments: config.transportExperiments,
13534
- transportExperimentFlags: config.transportExperimentFlags,
13535
- bvhNodeCount: config.bvhNodeCount,
13536
- displayQuality: config.displayQuality,
13537
- accelerationBuildMode: config.accelerationBuildMode,
13538
- gpuAccelerationBuildRequired: config.gpuAccelerationBuildRequired,
13539
- accelerationBuilt,
13540
- accelerationBuildCount,
13541
- frameConfigSlots: frameConfigSlotCount,
13542
- gpuParallelism: lastGpuParallelism,
13543
- memory: config.memory
13544
- });
13545
- }
13546
- function destroy() {
13547
- progressive?.destroy();
13548
- frameTelemetryResources?.destroy?.();
13549
- activeQueue.destroy?.();
13550
- nextQueue.destroy?.();
13551
- hitBuffer.destroy?.();
13552
- accumulationBuffer.destroy?.();
13553
- pathVertexBuffer.destroy?.();
13554
- sceneObjectBuffer.destroy?.();
13555
- triangleBuffer.destroy?.();
13556
- bvhNodeBuffer.destroy?.();
13557
- meshVertexBuffer.destroy?.();
13558
- meshIndexBuffer.destroy?.();
13559
- meshRangeBuffer.destroy?.();
13560
- environmentPortalBuffer.destroy?.();
13561
- bvhLeafRefBuffer.destroy?.();
13562
- configBuffer.destroy?.();
13563
- bvhBuildConfigBuffer.destroy?.();
13564
- counterBuffer.destroy?.();
13565
- activeDispatchBuffer.destroy?.();
13566
- radianceTexture.destroy?.();
13567
- denoiseScratchTexture.destroy?.();
13568
- outputTexture.destroy?.();
13569
- if (environmentMapResource.ownsTexture) {
13570
- environmentMapResource.texture?.destroy?.();
13571
- }
13572
- if (environmentSamplingResource.ownsTexture) {
13573
- environmentSamplingResource.texture?.destroy?.();
13574
- }
13575
- if (mediumTextureResource.ownsTexture) {
13576
- mediumTextureResource.texture?.destroy?.();
13577
- }
13578
- brdfLutResource.texture?.destroy?.();
13579
- if (baseColorAtlasResource.ownsTexture) {
13580
- baseColorAtlasResource.texture?.destroy?.();
13581
- }
13582
- if (metallicRoughnessAtlasResource.ownsTexture) {
13583
- metallicRoughnessAtlasResource.texture?.destroy?.();
13584
- }
13585
- if (normalAtlasResource.ownsTexture) {
13586
- normalAtlasResource.texture?.destroy?.();
13587
- }
13588
- if (occlusionAtlasResource.ownsTexture) {
13589
- occlusionAtlasResource.texture?.destroy?.();
13590
- }
13591
- if (emissiveAtlasResource.ownsTexture) {
13592
- emissiveAtlasResource.texture?.destroy?.();
13593
- }
13594
- Object.values(extensionAtlasResources).forEach((resource) => {
13595
- if (resource.ownsTexture) {
13596
- resource.texture?.destroy?.();
13597
- }
13598
- });
13599
- context.unconfigure?.();
14051
+ context?.unconfigure?.();
14052
+ allocationTracker.destroy();
14053
+ rawDevice.destroy?.();
14054
+ throw error;
13600
14055
  }
13601
- return Object.freeze({
13602
- canvas,
13603
- context,
13604
- device,
13605
- format,
13606
- config,
13607
- renderOnce,
13608
- renderFrame,
13609
- readOutputProbe,
13610
- updateSceneObjects,
13611
- updateCamera,
13612
- resetAccumulation: () => progressive?.reset(),
13613
- getSnapshot,
13614
- destroy
13615
- });
13616
14056
  }
13617
14057
  async function renderWavefrontPathTracingComputeFrame(options = {}) {
13618
14058
  const renderer = await createWavefrontPathTracingComputeRenderer(options);