@plasius/gpu-renderer 0.2.2 → 0.2.4

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.
@@ -3,6 +3,7 @@ const DEFAULT_HEIGHT = 720;
3
3
  const DEFAULT_MAX_DEPTH = 6;
4
4
  const DEFAULT_TILE_SIZE = 128;
5
5
  const DEFAULT_SAMPLES_PER_PIXEL = 1;
6
+ const DEFAULT_MAX_FRAME_PASSES_PER_SUBMISSION = 256;
6
7
  const DEFAULT_SCENE_OBJECT_CAPACITY = 128;
7
8
  const DEFAULT_ENVIRONMENT_PORTAL_CAPACITY = 32;
8
9
  const WORKGROUP_SIZE = 64;
@@ -20,11 +21,12 @@ const BVH_LEAF_REF_RECORD_BYTES = 16;
20
21
  const EMISSIVE_TRIANGLE_INDEX_BYTES = 4;
21
22
  const ENVIRONMENT_PORTAL_RECORD_BYTES = 96;
22
23
  const ACCUMULATION_RECORD_BYTES = 16;
23
- const CONFIG_BUFFER_BYTES = 272;
24
+ const PATH_VERTEX_RECORD_BYTES = 16;
25
+ const CONFIG_BUFFER_BYTES = 304;
24
26
  const COUNTER_DISPATCH_ARGS_OFFSET = 16;
25
27
  const INDIRECT_DISPATCH_ARGS_BYTES = 12;
26
28
  const COUNTER_BUFFER_BYTES = 32;
27
- const TRACE_STORAGE_BUFFER_BINDINGS = 9;
29
+ const TRACE_STORAGE_BUFFER_BINDINGS = 10;
28
30
  const HIT_TYPE_SURFACE = 0;
29
31
  const HIT_TYPE_EMISSIVE = 1;
30
32
  const MATERIAL_DIFFUSE = 0;
@@ -52,6 +54,7 @@ const DEFAULT_ENVIRONMENT_LIGHTING = Object.freeze({
52
54
  intensity: 1,
53
55
  mode: 0,
54
56
  exposure: 1,
57
+ sunlitBaseline: 0.16,
55
58
  });
56
59
 
57
60
  export const wavefrontPathTracingComputeLimits = Object.freeze({
@@ -69,6 +72,7 @@ export const wavefrontPathTracingComputeLimits = Object.freeze({
69
72
  emissiveTriangleMetadataRecordBytes: BVH_NODE_RECORD_BYTES,
70
73
  environmentPortalRecordBytes: ENVIRONMENT_PORTAL_RECORD_BYTES,
71
74
  accumulationRecordBytes: ACCUMULATION_RECORD_BYTES,
75
+ pathVertexRecordBytes: PATH_VERTEX_RECORD_BYTES,
72
76
  counterRecordBytes: COUNTER_BUFFER_BYTES,
73
77
  indirectDispatchRecordBytes: INDIRECT_DISPATCH_ARGS_BYTES,
74
78
  });
@@ -160,6 +164,39 @@ function asColor(value, fallback = [1, 1, 1, 1]) {
160
164
  ];
161
165
  }
162
166
 
167
+ function resolveEnvironmentMap(input = null) {
168
+ const source = input && typeof input === "object" ? input : null;
169
+ const hasTexture = Boolean(source?.view || source?.texture || source?.data);
170
+ const width = readPositiveInteger("environmentMap.width", source?.width, 1);
171
+ const height = readPositiveInteger("environmentMap.height", source?.height, 1);
172
+ return Object.freeze({
173
+ enabled: hasTexture && source?.enabled !== false,
174
+ width,
175
+ height,
176
+ format: typeof source?.format === "string" ? source.format : "rgba16float",
177
+ projection: typeof source?.projection === "string" ? source.projection : "equirectangular",
178
+ texture: source?.texture ?? null,
179
+ view: source?.view ?? null,
180
+ sampler: source?.sampler ?? null,
181
+ data: source?.data ?? null,
182
+ intensity: Math.max(0, readFiniteNumber("environmentMap.intensity", source?.intensity ?? source?.radianceScale, 1)),
183
+ rotationRadians: readFiniteNumber("environmentMap.rotationRadians", source?.rotationRadians ?? source?.rotation, 0),
184
+ ambientStrength: Math.max(
185
+ 0,
186
+ readFiniteNumber("environmentMap.ambientStrength", source?.ambientStrength, 0.32)
187
+ ),
188
+ });
189
+ }
190
+
191
+ function resolveDeferredPathResolve(options = {}) {
192
+ const value =
193
+ options.deferredPathResolve ??
194
+ options.deferredResolve ??
195
+ options.pathResolve?.deferred ??
196
+ true;
197
+ return value !== false;
198
+ }
199
+
163
200
  function emissionPower(emission) {
164
201
  return Math.max(0, emission?.[0] ?? 0) + Math.max(0, emission?.[1] ?? 0) + Math.max(0, emission?.[2] ?? 0);
165
202
  }
@@ -913,6 +950,14 @@ function resolveEnvironmentLighting(input, environmentColor, ambientColor) {
913
950
  intensity: Math.max(0.0001, readFiniteNumber("environmentLighting.intensity", source.intensity, DEFAULT_ENVIRONMENT_LIGHTING.intensity)),
914
951
  mode: readNonNegativeInteger("environmentLighting.mode", source.mode, DEFAULT_ENVIRONMENT_LIGHTING.mode),
915
952
  exposure: Math.max(0.0001, readFiniteNumber("environmentLighting.exposure", source.exposure, DEFAULT_ENVIRONMENT_LIGHTING.exposure)),
953
+ sunlitBaseline: Math.max(
954
+ 0,
955
+ readFiniteNumber(
956
+ "environmentLighting.sunlitBaseline",
957
+ source.sunlitBaseline ?? source.daylightBaseline,
958
+ DEFAULT_ENVIRONMENT_LIGHTING.sunlitBaseline
959
+ )
960
+ ),
916
961
  });
917
962
  }
918
963
 
@@ -1116,6 +1161,11 @@ export function estimateWavefrontPathTracingMemory(options = {}) {
1116
1161
  options.tilePixelCapacity,
1117
1162
  DEFAULT_TILE_SIZE * DEFAULT_TILE_SIZE
1118
1163
  );
1164
+ const maxDepth = clamp(
1165
+ readPositiveInteger("maxDepth", options.maxDepth, DEFAULT_MAX_DEPTH),
1166
+ 1,
1167
+ 16
1168
+ );
1119
1169
  const sceneObjectCapacity = readPositiveInteger(
1120
1170
  "sceneObjectCapacity",
1121
1171
  options.sceneObjectCapacity,
@@ -1141,6 +1191,7 @@ export function estimateWavefrontPathTracingMemory(options = {}) {
1141
1191
  const queueBytes = tilePixelCapacity * RAY_RECORD_BYTES;
1142
1192
  const hitBytes = tilePixelCapacity * HIT_RECORD_BYTES;
1143
1193
  const accumulationBytes = tilePixelCapacity * ACCUMULATION_RECORD_BYTES;
1194
+ const pathVertexBytes = tilePixelCapacity * (maxDepth + 1) * PATH_VERTEX_RECORD_BYTES;
1144
1195
  const sceneObjectBytes = sceneObjectCapacity * SCENE_OBJECT_RECORD_BYTES;
1145
1196
  const triangleBytes = triangleCapacity * TRIANGLE_RECORD_BYTES;
1146
1197
  const bvhNodeBytes = bvhNodeCapacity * BVH_NODE_RECORD_BYTES;
@@ -1155,6 +1206,7 @@ export function estimateWavefrontPathTracingMemory(options = {}) {
1155
1206
  queuePairBytes: queueBytes * 2,
1156
1207
  hitBytes,
1157
1208
  accumulationBytes,
1209
+ pathVertexBytes,
1158
1210
  sceneObjectBytes,
1159
1211
  triangleBytes,
1160
1212
  bvhNodeBytes,
@@ -1168,6 +1220,7 @@ export function estimateWavefrontPathTracingMemory(options = {}) {
1168
1220
  queueBytes * 2 +
1169
1221
  hitBytes +
1170
1222
  accumulationBytes +
1223
+ pathVertexBytes +
1171
1224
  sceneObjectBytes +
1172
1225
  triangleBytes +
1173
1226
  bvhNodeBytes +
@@ -1193,6 +1246,15 @@ export function createWavefrontPathTracingComputeConfig(options = {}) {
1193
1246
  1,
1194
1247
  64
1195
1248
  );
1249
+ const maxFramePassesPerSubmission = clamp(
1250
+ readPositiveInteger(
1251
+ "maxFramePassesPerSubmission",
1252
+ options.maxFramePassesPerSubmission,
1253
+ DEFAULT_MAX_FRAME_PASSES_PER_SUBMISSION
1254
+ ),
1255
+ 1,
1256
+ 4096
1257
+ );
1196
1258
  const tilePixelCapacity = readPositiveInteger(
1197
1259
  "tilePixelCapacity",
1198
1260
  options.tilePixelCapacity,
@@ -1276,6 +1338,12 @@ export function createWavefrontPathTracingComputeConfig(options = {}) {
1276
1338
  options.environmentLighting?.environmentPortalMode,
1277
1339
  environmentPortals.length > 0
1278
1340
  );
1341
+ const environmentMap = resolveEnvironmentMap(
1342
+ options.environmentMap ??
1343
+ options.environmentTexture ??
1344
+ options.environmentLighting?.environmentMap
1345
+ );
1346
+ const deferredPathResolve = resolveDeferredPathResolve(options);
1279
1347
 
1280
1348
  return Object.freeze({
1281
1349
  mode: rendererWavefrontComputeMode,
@@ -1284,6 +1352,7 @@ export function createWavefrontPathTracingComputeConfig(options = {}) {
1284
1352
  maxDepth,
1285
1353
  tileSize,
1286
1354
  samplesPerPixel,
1355
+ maxFramePassesPerSubmission,
1287
1356
  tilePixelCapacity,
1288
1357
  sceneObjects,
1289
1358
  sceneObjectCount: sceneObjects.length,
@@ -1310,12 +1379,15 @@ export function createWavefrontPathTracingComputeConfig(options = {}) {
1310
1379
  environmentPortalCount: environmentPortals.length,
1311
1380
  environmentPortalCapacity,
1312
1381
  environmentPortalMode,
1382
+ environmentMap,
1383
+ deferredPathResolve,
1313
1384
  displayQuality: options.displayQuality === true,
1314
1385
  requiresMeshBvhForDisplayQuality: true,
1315
1386
  denoise: options.denoise !== false,
1316
1387
  frameIndex: readNonNegativeInteger("frameIndex", options.frameIndex, 0),
1317
1388
  memory: estimateWavefrontPathTracingMemory({
1318
1389
  tilePixelCapacity,
1390
+ maxDepth,
1319
1391
  sceneObjectCapacity,
1320
1392
  triangleCapacity,
1321
1393
  bvhNodeCapacity,
@@ -1539,6 +1611,18 @@ function createConfigPayload(config, tile, frameIndex, buildRange = {}) {
1539
1611
  data.setUint32(260, config.environmentPortalMode ?? 0, true);
1540
1612
  data.setUint32(264, 0, true);
1541
1613
  data.setUint32(268, 0, true);
1614
+ writeVec4(floatView, 272, [
1615
+ config.environmentMap.enabled ? 1 : 0,
1616
+ config.environmentMap.intensity,
1617
+ config.environmentMap.rotationRadians,
1618
+ config.environmentMap.ambientStrength,
1619
+ ]);
1620
+ writeVec4(floatView, 288, [
1621
+ config.deferredPathResolve ? 1 : 0,
1622
+ config.environmentLighting.sunlitBaseline,
1623
+ 0,
1624
+ 0,
1625
+ ]);
1542
1626
  return bytes;
1543
1627
  }
1544
1628
 
@@ -1811,6 +1895,149 @@ function alignTo(value, alignment) {
1811
1895
  return Math.ceil(value / resolvedAlignment) * resolvedAlignment;
1812
1896
  }
1813
1897
 
1898
+ function float32ToFloat16Bits(value) {
1899
+ const floatView = new Float32Array(1);
1900
+ const intView = new Uint32Array(floatView.buffer);
1901
+ floatView[0] = Number.isFinite(value) ? value : 0;
1902
+ const x = intView[0];
1903
+ const sign = (x >> 16) & 0x8000;
1904
+ let mantissa = x & 0x7fffff;
1905
+ let exponent = (x >> 23) & 0xff;
1906
+
1907
+ if (exponent === 0xff) {
1908
+ return sign | (mantissa ? 0x7e00 : 0x7c00);
1909
+ }
1910
+
1911
+ exponent = exponent - 127 + 15;
1912
+ if (exponent >= 0x1f) {
1913
+ return sign | 0x7c00;
1914
+ }
1915
+ if (exponent <= 0) {
1916
+ if (exponent < -10) {
1917
+ return sign;
1918
+ }
1919
+ mantissa = (mantissa | 0x800000) >> (1 - exponent);
1920
+ return sign | ((mantissa + 0x1000) >> 13);
1921
+ }
1922
+ return sign | (exponent << 10) | ((mantissa + 0x1000) >> 13);
1923
+ }
1924
+
1925
+ function environmentMapIntegerScale(data) {
1926
+ if (data instanceof Uint8Array) {
1927
+ return 1 / 255;
1928
+ }
1929
+ if (data instanceof Uint16Array) {
1930
+ return 1 / 65535;
1931
+ }
1932
+ return 1;
1933
+ }
1934
+
1935
+ function readEnvironmentMapComponent(data, index, fallback, integerScale = 1) {
1936
+ if (!data || index >= data.length) {
1937
+ return fallback;
1938
+ }
1939
+ const value = Number(data[index]);
1940
+ return Number.isFinite(value) ? Math.max(0, value) * integerScale : fallback;
1941
+ }
1942
+
1943
+ function createEnvironmentMapUploadBytes(environmentMap, fallbackColor) {
1944
+ const width = Math.max(1, environmentMap.width);
1945
+ const height = Math.max(1, environmentMap.height);
1946
+ const rowBytes = width * 8;
1947
+ const bytesPerRow = alignTo(rowBytes, 256);
1948
+ const bytes = new Uint8Array(bytesPerRow * height);
1949
+ const data = environmentMap.data;
1950
+ const integerScale = environmentMapIntegerScale(data);
1951
+ const view = new DataView(bytes.buffer);
1952
+ const writeComponent = (targetOffset, sourceOffset, fallback) => {
1953
+ view.setUint16(
1954
+ targetOffset,
1955
+ float32ToFloat16Bits(
1956
+ readEnvironmentMapComponent(data, sourceOffset, fallback, integerScale)
1957
+ ),
1958
+ true
1959
+ );
1960
+ };
1961
+
1962
+ for (let y = 0; y < height; y += 1) {
1963
+ for (let x = 0; x < width; x += 1) {
1964
+ const sourceOffset = (y * width + x) * 4;
1965
+ const targetOffset = y * bytesPerRow + x * 8;
1966
+ writeComponent(targetOffset, sourceOffset, fallbackColor[0]);
1967
+ writeComponent(targetOffset + 2, sourceOffset + 1, fallbackColor[1]);
1968
+ writeComponent(targetOffset + 4, sourceOffset + 2, fallbackColor[2]);
1969
+ writeComponent(targetOffset + 6, sourceOffset + 3, fallbackColor[3] ?? 1);
1970
+ }
1971
+ }
1972
+
1973
+ return Object.freeze({
1974
+ bytes,
1975
+ bytesPerRow,
1976
+ width,
1977
+ height,
1978
+ });
1979
+ }
1980
+
1981
+ function createEnvironmentMapResource(device, constants, environmentMap, fallbackColor) {
1982
+ if (environmentMap.view) {
1983
+ return Object.freeze({
1984
+ view: environmentMap.view,
1985
+ sampler: environmentMap.sampler ?? device.createSampler({
1986
+ label: "plasius.wavefront.environmentMapSampler",
1987
+ addressModeU: "repeat",
1988
+ addressModeV: "clamp-to-edge",
1989
+ magFilter: "linear",
1990
+ minFilter: "linear",
1991
+ }),
1992
+ texture: null,
1993
+ ownsTexture: false,
1994
+ });
1995
+ }
1996
+
1997
+ if (environmentMap.texture && typeof environmentMap.texture.createView === "function") {
1998
+ return Object.freeze({
1999
+ view: environmentMap.texture.createView(),
2000
+ sampler: environmentMap.sampler ?? device.createSampler({
2001
+ label: "plasius.wavefront.environmentMapSampler",
2002
+ addressModeU: "repeat",
2003
+ addressModeV: "clamp-to-edge",
2004
+ magFilter: "linear",
2005
+ minFilter: "linear",
2006
+ }),
2007
+ texture: environmentMap.texture,
2008
+ ownsTexture: false,
2009
+ });
2010
+ }
2011
+
2012
+ const upload = createEnvironmentMapUploadBytes(environmentMap, fallbackColor);
2013
+ const texture = device.createTexture({
2014
+ label: environmentMap.enabled
2015
+ ? "plasius.wavefront.environmentMap"
2016
+ : "plasius.wavefront.environmentMapFallback",
2017
+ size: { width: upload.width, height: upload.height },
2018
+ format: "rgba16float",
2019
+ usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST,
2020
+ });
2021
+ device.queue.writeTexture(
2022
+ { texture },
2023
+ upload.bytes,
2024
+ { bytesPerRow: upload.bytesPerRow, rowsPerImage: upload.height },
2025
+ { width: upload.width, height: upload.height, depthOrArrayLayers: 1 }
2026
+ );
2027
+ return Object.freeze({
2028
+ view: texture.createView(),
2029
+ sampler: environmentMap.sampler ?? device.createSampler({
2030
+ label: "plasius.wavefront.environmentMapSampler",
2031
+ addressModeU: "repeat",
2032
+ addressModeV: "clamp-to-edge",
2033
+ magFilter: "linear",
2034
+ minFilter: "linear",
2035
+ }),
2036
+ texture,
2037
+ ownsTexture: true,
2038
+ });
2039
+ }
2040
+
1814
2041
  async function getPipelineDiagnostics(shaderModule) {
1815
2042
  if (typeof shaderModule?.compilationInfo !== "function") {
1816
2043
  return "";
@@ -2024,6 +2251,8 @@ struct FrameConfig {
2024
2251
  environmentPortalMode: u32,
2025
2252
  _portalPad0: u32,
2026
2253
  _portalPad1: u32,
2254
+ environmentMapSettings: vec4<f32>,
2255
+ pathResolveSettings: vec4<f32>,
2027
2256
  };
2028
2257
 
2029
2258
  struct Counters {
@@ -2083,6 +2312,9 @@ struct EnvironmentPortal {
2083
2312
  @group(0) @binding(17) var finalDenoiseInputRadiance: texture_2d<f32>;
2084
2313
  @group(0) @binding(18) var denoisedOutputImage: texture_storage_2d<rgba8unorm, write>;
2085
2314
  @group(0) @binding(19) var<storage, read> environmentPortals: array<EnvironmentPortal>;
2315
+ @group(0) @binding(20) var environmentMapTexture: texture_2d<f32>;
2316
+ @group(0) @binding(21) var environmentMapSampler: sampler;
2317
+ @group(0) @binding(22) var<storage, read_write> pathVertices: array<vec4<f32>>;
2086
2318
 
2087
2319
  fn hash_u32(value: u32) -> u32 {
2088
2320
  var x = value;
@@ -2127,6 +2359,89 @@ fn max_component(value: vec3<f32>) -> f32 {
2127
2359
  return max(max(value.x, value.y), value.z);
2128
2360
  }
2129
2361
 
2362
+ fn environment_map_enabled() -> bool {
2363
+ return config.environmentMapSettings.x > 0.5;
2364
+ }
2365
+
2366
+ fn deferred_path_resolve_enabled() -> bool {
2367
+ return config.pathResolveSettings.x > 0.5;
2368
+ }
2369
+
2370
+ fn path_vertex_count_per_ray() -> u32 {
2371
+ return config.maxDepth + 1u;
2372
+ }
2373
+
2374
+ fn path_vertex_index(rayId: u32, depth: u32) -> u32 {
2375
+ return rayId * path_vertex_count_per_ray() + min(depth, config.maxDepth);
2376
+ }
2377
+
2378
+ fn clear_deferred_path(rayId: u32) {
2379
+ if (!deferred_path_resolve_enabled()) {
2380
+ return;
2381
+ }
2382
+
2383
+ for (var depth = 0u; depth <= config.maxDepth; depth = depth + 1u) {
2384
+ pathVertices[path_vertex_index(rayId, depth)] = vec4<f32>(0.0);
2385
+ if (depth == config.maxDepth) {
2386
+ break;
2387
+ }
2388
+ }
2389
+ }
2390
+
2391
+ fn record_deferred_path_response(ray: RayRecord, response: vec3<f32>) {
2392
+ if (!deferred_path_resolve_enabled() || ray.rayId >= config.tilePixelCount || ray.bounce >= config.maxDepth) {
2393
+ return;
2394
+ }
2395
+ pathVertices[path_vertex_index(ray.rayId, ray.bounce)] =
2396
+ vec4<f32>(max(response, vec3<f32>(0.0)), 1.0);
2397
+ }
2398
+
2399
+ fn record_deferred_terminal_source(ray: RayRecord, sourceRadiance: vec3<f32>) {
2400
+ if (!deferred_path_resolve_enabled() || ray.rayId >= config.tilePixelCount) {
2401
+ return;
2402
+ }
2403
+ pathVertices[path_vertex_index(ray.rayId, config.maxDepth)] =
2404
+ vec4<f32>(clamp_sample_radiance(sourceRadiance), 1.0);
2405
+ }
2406
+
2407
+ fn environment_map_uv(direction: vec3<f32>) -> vec2<f32> {
2408
+ let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
2409
+ let rotationTurns = config.environmentMapSettings.z / 6.28318530718;
2410
+ let u = fract(atan2(rayDirection.z, rayDirection.x) / 6.28318530718 + 0.5 + rotationTurns);
2411
+ let v = acos(clamp(rayDirection.y, -1.0, 1.0)) / 3.14159265359;
2412
+ return vec2<f32>(u, clamp(v, 0.0, 1.0));
2413
+ }
2414
+
2415
+ fn environment_map_radiance(direction: vec3<f32>) -> vec3<f32> {
2416
+ let uv = environment_map_uv(direction);
2417
+ let texel = max(textureSampleLevel(environmentMapTexture, environmentMapSampler, uv, 0.0).rgb, vec3<f32>(0.0));
2418
+ return texel * max(config.environmentMapSettings.y, 0.0);
2419
+ }
2420
+
2421
+ fn procedural_environment_radiance(direction: vec3<f32>) -> vec3<f32> {
2422
+ let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
2423
+ let upFactor = saturate(rayDirection.y * 0.5 + 0.5);
2424
+ let sunDirection = safe_normalize(
2425
+ config.environmentSunDirectionIntensity.xyz,
2426
+ vec3<f32>(0.0, 1.0, 0.0)
2427
+ );
2428
+ let sunGlow = pow(saturate(dot(rayDirection, sunDirection)), 192.0);
2429
+ let gradient =
2430
+ config.environmentHorizonColor.xyz * (1.0 - upFactor) +
2431
+ config.environmentZenithColor.xyz * upFactor;
2432
+ return (
2433
+ gradient +
2434
+ config.environmentSunColor.xyz * sunGlow
2435
+ ) * max(config.environmentSunDirectionIntensity.w, 0.0001);
2436
+ }
2437
+
2438
+ fn base_environment_radiance(direction: vec3<f32>) -> vec3<f32> {
2439
+ if (environment_map_enabled()) {
2440
+ return environment_map_radiance(direction);
2441
+ }
2442
+ return procedural_environment_radiance(direction);
2443
+ }
2444
+
2130
2445
  fn environment_portal_radiance_scale(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
2131
2446
  if (config.environmentPortalCount == 0u || config.environmentPortalMode == 0u) {
2132
2447
  return vec3<f32>(1.0);
@@ -2166,22 +2481,9 @@ fn environment_portal_radiance_scale(origin: vec3<f32>, direction: vec3<f32>) ->
2166
2481
 
2167
2482
  fn environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
2168
2483
  let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
2169
- let upFactor = saturate(rayDirection.y * 0.5 + 0.5);
2170
- let sunDirection = safe_normalize(
2171
- config.environmentSunDirectionIntensity.xyz,
2172
- vec3<f32>(0.0, 1.0, 0.0)
2173
- );
2174
- let sunGlow = pow(saturate(dot(rayDirection, sunDirection)), 192.0);
2175
- let gradient =
2176
- config.environmentHorizonColor.xyz * (1.0 - upFactor) +
2177
- config.environmentZenithColor.xyz * upFactor;
2178
2484
  let portalScale = environment_portal_radiance_scale(origin, rayDirection);
2179
2485
  let portalHit = max_component(portalScale) > 0.0001;
2180
- return (
2181
- gradient +
2182
- config.environmentSunColor.xyz * sunGlow
2183
- ) *
2184
- max(config.environmentSunDirectionIntensity.w, 0.0001) *
2486
+ return base_environment_radiance(rayDirection) *
2185
2487
  select(vec3<f32>(1.0), portalScale, portalHit);
2186
2488
  }
2187
2489
 
@@ -2197,16 +2499,143 @@ fn gated_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f
2197
2499
  return environment_radiance(origin, direction);
2198
2500
  }
2199
2501
 
2200
- fn terminal_surface_environment_contribution(ray: RayRecord, hit: HitRecord) -> vec3<f32> {
2502
+ fn surface_path_response(hit: HitRecord) -> vec3<f32> {
2503
+ let color = clamp(hit.color.xyz, vec3<f32>(0.0), vec3<f32>(1.0));
2504
+ let opacity = clamp(hit.material.z, 0.0, 1.0);
2505
+ let materialEnergy = select(0.68, 0.92, hit.materialKind == 1u || hit.materialKind == 2u);
2506
+ let transparentEnergy = select(materialEnergy, 0.9, hit.hitType == 3u);
2507
+ return mix(vec3<f32>(1.0), color, max(opacity, 0.18)) * transparentEnergy;
2508
+ }
2509
+
2510
+ fn sunlit_baseline_radiance(normal: vec3<f32>) -> vec3<f32> {
2511
+ let baseline = max(config.pathResolveSettings.y, 0.0);
2512
+ if (baseline <= 0.000001) {
2513
+ return vec3<f32>(0.0);
2514
+ }
2515
+ let sunDirection = safe_normalize(
2516
+ config.environmentSunDirectionIntensity.xyz,
2517
+ vec3<f32>(0.0, 1.0, 0.0)
2518
+ );
2519
+ let sunFacing = saturate(dot(normal, sunDirection));
2520
+ let skyFacing = 0.35 + saturate(normal.y * 0.5 + 0.5) * 0.65;
2521
+ let directionalWeight = 0.38 + sunFacing * 0.62;
2522
+ let sunTint = max(config.environmentSunColor.xyz, vec3<f32>(0.0));
2523
+ return clamp_sample_radiance(sunTint * baseline * skyFacing * directionalWeight * 0.04);
2524
+ }
2525
+
2526
+ fn terminal_surface_environment_source(hit: HitRecord) -> vec3<f32> {
2201
2527
  let normal = safe_normalize(hit.shadingNormal.xyz, vec3<f32>(0.0, 1.0, 0.0));
2202
- let surfaceColor = max(hit.color.xyz, config.ambientColor.xyz);
2203
2528
  let normalEnvironment = gated_environment_radiance(
2204
2529
  hit.position.xyz + normal * 0.003,
2205
2530
  normal
2206
2531
  );
2207
- let environmentFloor = max(config.ambientColor.xyz, normalEnvironment * 0.12);
2532
+ let sunlitFloor = sunlit_baseline_radiance(normal);
2533
+ let ambientFloor = select(
2534
+ max(config.ambientColor.xyz, sunlitFloor * 0.82),
2535
+ max(config.ambientColor.xyz * 0.35, sunlitFloor * 0.58),
2536
+ environment_map_enabled()
2537
+ );
2538
+ let environmentInfluence = select(
2539
+ max(0.12, config.pathResolveSettings.y * 0.42),
2540
+ max(config.environmentMapSettings.w, max(0.12, config.pathResolveSettings.y * 0.42)),
2541
+ environment_map_enabled()
2542
+ );
2543
+ let environmentFloor = max(ambientFloor, max(sunlitFloor, normalEnvironment * environmentInfluence));
2208
2544
  let materialFloor = select(0.7, 1.0, hit.materialKind == 0u || hit.materialKind == 3u);
2209
- return clamp_sample_radiance(ray.throughput.xyz * surfaceColor * environmentFloor * materialFloor);
2545
+ return clamp_sample_radiance(environmentFloor * materialFloor);
2546
+ }
2547
+
2548
+ fn terminal_surface_environment_contribution(ray: RayRecord, hit: HitRecord) -> vec3<f32> {
2549
+ let surfaceColor = max(hit.color.xyz, config.ambientColor.xyz);
2550
+ return clamp_sample_radiance(
2551
+ ray.throughput.xyz *
2552
+ surfaceColor *
2553
+ terminal_surface_environment_source(hit)
2554
+ );
2555
+ }
2556
+
2557
+ fn direct_environment_portal_irradiance(origin: vec3<f32>, normal: vec3<f32>) -> vec3<f32> {
2558
+ if (config.environmentPortalCount == 0u || config.environmentPortalMode == 0u) {
2559
+ return vec3<f32>(0.0);
2560
+ }
2561
+
2562
+ var irradiance = vec3<f32>(0.0);
2563
+ for (var portalIndex = 0u; portalIndex < config.environmentPortalCount; portalIndex = portalIndex + 1u) {
2564
+ let portal = environmentPortals[portalIndex];
2565
+ if (portal.kind != 1u) {
2566
+ continue;
2567
+ }
2568
+
2569
+ let toPortal = portal.position.xyz - origin;
2570
+ let distanceSquared = max(dot(toPortal, toPortal), 0.01);
2571
+ let direction = safe_normalize(toPortal, normal);
2572
+ let surfaceFacing = saturate(dot(normal, direction));
2573
+ if (surfaceFacing <= 0.0001) {
2574
+ continue;
2575
+ }
2576
+
2577
+ let portalNormal = safe_normalize(portal.normal.xyz, vec3<f32>(0.0, 0.0, 1.0));
2578
+ let twoSided = (portal.flags & 1u) != 0u;
2579
+ let portalFacing = select(
2580
+ saturate(dot(-direction, portalNormal)),
2581
+ max(abs(dot(direction, portalNormal)), 0.15),
2582
+ twoSided
2583
+ );
2584
+ let area = max(portal.position.w, 0.0001);
2585
+ let distanceFalloff = clamp(area / max(distanceSquared, area * 0.25), 0.0, 2.5);
2586
+ irradiance = irradiance +
2587
+ portal.color.rgb *
2588
+ portal.normal.w *
2589
+ portal.color.a *
2590
+ surfaceFacing *
2591
+ portalFacing *
2592
+ distanceFalloff;
2593
+ }
2594
+ return irradiance;
2595
+ }
2596
+
2597
+ fn surface_direct_environment_contribution(ray: RayRecord, hit: HitRecord) -> vec3<f32> {
2598
+ let normal = safe_normalize(hit.shadingNormal.xyz, vec3<f32>(0.0, 1.0, 0.0));
2599
+ let origin = hit.position.xyz + normal * 0.003;
2600
+ let viewDirection = safe_normalize(-ray.direction.xyz, normal);
2601
+ let surfaceColor = clamp(max(hit.color.xyz, config.ambientColor.xyz * 0.35), vec3<f32>(0.0), vec3<f32>(1.0));
2602
+ let roughness = clamp(hit.material.x, 0.0, 1.0);
2603
+ let metallic = clamp(hit.material.y, 0.0, 1.0);
2604
+
2605
+ let normalEnvironment = gated_environment_radiance(origin, normal);
2606
+ let skyVisibility = 0.35 + saturate(normal.y * 0.5 + 0.5) * 0.45;
2607
+ let sunlitFloor = sunlit_baseline_radiance(normal);
2608
+ let ambientIrradiance = max(
2609
+ select(config.ambientColor.xyz * 0.72, config.ambientColor.xyz * 0.28, environment_map_enabled()),
2610
+ sunlitFloor * select(0.72, 0.45, environment_map_enabled())
2611
+ );
2612
+ let environmentIrradianceScale = select(
2613
+ max(0.16, config.pathResolveSettings.y * 0.45),
2614
+ max(config.environmentMapSettings.w, max(0.16, config.pathResolveSettings.y * 0.45)),
2615
+ environment_map_enabled()
2616
+ );
2617
+ let skyIrradiance = max(ambientIrradiance, normalEnvironment * skyVisibility * environmentIrradianceScale);
2618
+
2619
+ let sunDirection = safe_normalize(
2620
+ config.environmentSunDirectionIntensity.xyz,
2621
+ vec3<f32>(0.0, 1.0, 0.0)
2622
+ );
2623
+ let sunFacing = saturate(dot(normal, sunDirection));
2624
+ let sunRadiance = gated_environment_radiance(origin, sunDirection);
2625
+ let sunIrradiance = sunRadiance * sunFacing * 0.2;
2626
+ let portalIrradiance = direct_environment_portal_irradiance(origin, normal);
2627
+
2628
+ let diffuseWeight = select(1.0 - metallic * 0.65, 0.22, hit.materialKind == 1u);
2629
+ let diffuse = surfaceColor * (skyIrradiance + sunIrradiance + portalIrradiance) * diffuseWeight;
2630
+
2631
+ let halfVector = safe_normalize(sunDirection + viewDirection, normal);
2632
+ let specularPower = 8.0 + (1.0 - roughness) * 96.0;
2633
+ let specularFacing = pow(saturate(dot(normal, halfVector)), specularPower) * sunFacing;
2634
+ let specularTint = mix(vec3<f32>(0.04), surfaceColor, metallic);
2635
+ let specular = specularTint * sunRadiance * specularFacing * select(0.16, 0.48, hit.materialKind == 1u || hit.materialKind == 2u);
2636
+
2637
+ let bounceWeight = select(1.0, 0.38, ray.bounce > 0u);
2638
+ return clamp_sample_radiance(ray.throughput.xyz * (diffuse + specular) * bounceWeight);
2210
2639
  }
2211
2640
 
2212
2641
  fn default_mesh_range() -> MeshRange {
@@ -2809,6 +3238,7 @@ fn generatePrimaryRays(@builtin(global_invocation_id) globalId: vec3<u32>) {
2809
3238
  return;
2810
3239
  }
2811
3240
  activeQueue[index] = make_ray(index);
3241
+ clear_deferred_path(index);
2812
3242
  if (u32(config.projectionAndSampling.w) == 0u) {
2813
3243
  accumulation[index] = vec4<f32>(0.0);
2814
3244
  }
@@ -3061,27 +3491,51 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
3061
3491
 
3062
3492
  if (hit.hitType == 1u) {
3063
3493
  let guidedLightWeight = select(1.0, 0.24, (ray.flags & RAY_FLAG_GUIDED_EMISSIVE) != 0u);
3064
- contribution = clamp_sample_radiance(
3065
- ray.throughput.xyz * max(hit.emission.xyz, hit.color.xyz) * guidedLightWeight
3066
- );
3067
- accumulation[ray.rayId] =
3068
- accumulation[ray.rayId] + vec4<f32>(contribution * sample_weight(), 1.0);
3494
+ let sourceRadiance = max(hit.emission.xyz, hit.color.xyz) * guidedLightWeight;
3495
+ if (deferred_path_resolve_enabled()) {
3496
+ record_deferred_terminal_source(ray, sourceRadiance);
3497
+ } else {
3498
+ contribution = clamp_sample_radiance(ray.throughput.xyz * sourceRadiance);
3499
+ accumulation[ray.rayId] =
3500
+ accumulation[ray.rayId] + vec4<f32>(contribution * sample_weight(), 1.0);
3501
+ }
3069
3502
  atomicAdd(&counters.terminatedCount, 1u);
3070
3503
  return;
3071
3504
  }
3072
3505
 
3073
3506
  if (hit.hitType == 2u) {
3074
- contribution = clamp_sample_radiance(ray.throughput.xyz * max(hit.color.xyz, config.ambientColor.xyz));
3075
- accumulation[ray.rayId] =
3076
- accumulation[ray.rayId] + vec4<f32>(contribution * sample_weight(), 1.0);
3507
+ if (deferred_path_resolve_enabled()) {
3508
+ record_deferred_terminal_source(ray, hit.color.xyz);
3509
+ } else {
3510
+ contribution = clamp_sample_radiance(ray.throughput.xyz * max(hit.color.xyz, config.ambientColor.xyz));
3511
+ accumulation[ray.rayId] =
3512
+ accumulation[ray.rayId] + vec4<f32>(contribution * sample_weight(), 1.0);
3513
+ }
3077
3514
  atomicAdd(&counters.terminatedCount, 1u);
3078
3515
  return;
3079
3516
  }
3080
3517
 
3081
- if (ray.bounce + 1u >= config.maxDepth) {
3082
- let terminalEnvironment = terminal_surface_environment_contribution(ray, hit);
3518
+ let response = surface_path_response(hit);
3519
+ record_deferred_path_response(ray, response);
3520
+
3521
+ let shouldEstimateDirectEnvironment =
3522
+ !deferred_path_resolve_enabled() &&
3523
+ (hit.materialKind == 0u || hit.materialKind == 1u) &&
3524
+ hit.material.z >= 0.95;
3525
+ if (shouldEstimateDirectEnvironment) {
3526
+ let directEnvironment = surface_direct_environment_contribution(ray, hit);
3083
3527
  accumulation[ray.rayId] =
3084
- accumulation[ray.rayId] + vec4<f32>(terminalEnvironment * sample_weight(), 1.0);
3528
+ accumulation[ray.rayId] + vec4<f32>(directEnvironment * sample_weight(), 0.0);
3529
+ }
3530
+
3531
+ if (ray.bounce + 1u >= config.maxDepth) {
3532
+ if (deferred_path_resolve_enabled()) {
3533
+ record_deferred_terminal_source(ray, terminal_surface_environment_source(hit));
3534
+ } else {
3535
+ let terminalEnvironment = terminal_surface_environment_contribution(ray, hit);
3536
+ accumulation[ray.rayId] =
3537
+ accumulation[ray.rayId] + vec4<f32>(terminalEnvironment * sample_weight(), 1.0);
3538
+ }
3085
3539
  atomicAdd(&counters.terminatedCount, 1u);
3086
3540
  return;
3087
3541
  }
@@ -3090,17 +3544,17 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
3090
3544
  let scatter = scatter_direction(ray, hit, seed);
3091
3545
  let nextIndex = atomicAdd(&counters.nextCount, 1u);
3092
3546
  if (nextIndex >= config.tilePixelCount) {
3093
- let overflowEnvironment = terminal_surface_environment_contribution(ray, hit);
3094
- accumulation[ray.rayId] =
3095
- accumulation[ray.rayId] + vec4<f32>(overflowEnvironment * sample_weight(), 1.0);
3547
+ if (deferred_path_resolve_enabled()) {
3548
+ record_deferred_terminal_source(ray, terminal_surface_environment_source(hit));
3549
+ } else {
3550
+ let overflowEnvironment = terminal_surface_environment_contribution(ray, hit);
3551
+ accumulation[ray.rayId] =
3552
+ accumulation[ray.rayId] + vec4<f32>(overflowEnvironment * sample_weight(), 1.0);
3553
+ }
3096
3554
  atomicAdd(&counters.terminatedCount, 1u);
3097
3555
  return;
3098
3556
  }
3099
- let color = clamp(hit.color.xyz, vec3<f32>(0.0), vec3<f32>(1.0));
3100
- let opacity = clamp(hit.material.z, 0.0, 1.0);
3101
- let materialEnergy = select(0.68, 0.92, hit.materialKind == 1u || hit.materialKind == 2u);
3102
- let transparentEnergy = select(materialEnergy, 0.9, hit.hitType == 3u);
3103
- let throughput = ray.throughput.xyz * mix(vec3<f32>(1.0), color, max(opacity, 0.18)) * transparentEnergy;
3557
+ let throughput = ray.throughput.xyz * response;
3104
3558
  nextQueue[nextIndex] = RayRecord(
3105
3559
  ray.rayId,
3106
3560
  ray.rayId,
@@ -3128,6 +3582,27 @@ fn compactAndSwapQueues(@builtin(global_invocation_id) globalId: vec3<u32>) {
3128
3582
  write_active_dispatch_args(activeCount);
3129
3583
  }
3130
3584
 
3585
+ fn resolve_deferred_path_radiance(rayId: u32) -> vec3<f32> {
3586
+ let terminal = pathVertices[path_vertex_index(rayId, config.maxDepth)];
3587
+ if (terminal.w <= 0.0) {
3588
+ return vec3<f32>(0.0);
3589
+ }
3590
+
3591
+ var radiance = terminal.xyz;
3592
+ var depth = config.maxDepth;
3593
+ loop {
3594
+ if (depth == 0u) {
3595
+ break;
3596
+ }
3597
+ depth = depth - 1u;
3598
+ let response = pathVertices[path_vertex_index(rayId, depth)];
3599
+ if (response.w > 0.0) {
3600
+ radiance = radiance * response.xyz;
3601
+ }
3602
+ }
3603
+ return clamp_sample_radiance(radiance);
3604
+ }
3605
+
3131
3606
  @compute @workgroup_size(64)
3132
3607
  fn accumulateTerminalRadiance(@builtin(global_invocation_id) globalId: vec3<u32>) {
3133
3608
  let index = globalId.x;
@@ -3137,7 +3612,12 @@ fn accumulateTerminalRadiance(@builtin(global_invocation_id) globalId: vec3<u32>
3137
3612
  let localX = index % config.tileWidth;
3138
3613
  let localY = index / config.tileWidth;
3139
3614
  let pixel = vec2<i32>(i32(config.tileX + localX), i32(config.tileY + localY));
3140
- let radiance = max(accumulation[index].xyz, vec3<f32>(0.0));
3615
+ var radiance = max(accumulation[index].xyz, vec3<f32>(0.0));
3616
+ if (deferred_path_resolve_enabled()) {
3617
+ let resolved = resolve_deferred_path_radiance(index) * sample_weight();
3618
+ radiance = clamp_sample_radiance(radiance + resolved);
3619
+ accumulation[index] = vec4<f32>(radiance, 1.0);
3620
+ }
3141
3621
 
3142
3622
  textureStore(radianceImage, pixel, vec4<f32>(radiance, 1.0));
3143
3623
  if (config.denoise == 0u) {
@@ -3280,6 +3760,137 @@ function createWavefrontDeviceDescriptor(adapter, options = {}) {
3280
3760
  return Object.keys(descriptor).length > 0 ? descriptor : undefined;
3281
3761
  }
3282
3762
 
3763
+ function readGpuLimit(adapter, device, name) {
3764
+ const adapterValue = Number(adapter?.limits?.[name]);
3765
+ if (Number.isFinite(adapterValue)) {
3766
+ return adapterValue;
3767
+ }
3768
+ const deviceValue = Number(device?.limits?.[name]);
3769
+ return Number.isFinite(deviceValue) ? deviceValue : null;
3770
+ }
3771
+
3772
+ function createAdapterInfoSnapshot(adapter) {
3773
+ const info = adapter?.info;
3774
+ if (!info || typeof info !== "object") {
3775
+ return null;
3776
+ }
3777
+ return Object.freeze({
3778
+ vendor: typeof info.vendor === "string" ? info.vendor : "",
3779
+ architecture: typeof info.architecture === "string" ? info.architecture : "",
3780
+ device: typeof info.device === "string" ? info.device : "",
3781
+ description: typeof info.description === "string" ? info.description : "",
3782
+ });
3783
+ }
3784
+
3785
+ function createGpuAdapterParallelismDiagnostics(adapter, device) {
3786
+ return Object.freeze({
3787
+ physicalCoreCount: null,
3788
+ physicalCoreCountAvailable: false,
3789
+ physicalCoreCountUnavailableReason: "WebGPU does not expose physical GPU core counts.",
3790
+ adapterInfo: createAdapterInfoSnapshot(adapter),
3791
+ adapterLimits: Object.freeze({
3792
+ maxComputeInvocationsPerWorkgroup: readGpuLimit(adapter, device, "maxComputeInvocationsPerWorkgroup"),
3793
+ maxComputeWorkgroupSizeX: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeX"),
3794
+ maxComputeWorkgroupSizeY: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeY"),
3795
+ maxComputeWorkgroupSizeZ: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeZ"),
3796
+ maxComputeWorkgroupsPerDimension: readGpuLimit(adapter, device, "maxComputeWorkgroupsPerDimension"),
3797
+ maxStorageBuffersPerShaderStage: readGpuLimit(adapter, device, "maxStorageBuffersPerShaderStage"),
3798
+ maxStorageBufferBindingSize: readGpuLimit(adapter, device, "maxStorageBufferBindingSize"),
3799
+ }),
3800
+ configuredWorkgroupSize: WORKGROUP_SIZE,
3801
+ });
3802
+ }
3803
+
3804
+ function createGpuParallelismCounters() {
3805
+ return {
3806
+ directDispatches: 0,
3807
+ directWorkgroups: 0,
3808
+ directShaderInvocations: 0,
3809
+ multiWorkgroupDispatches: 0,
3810
+ largestDirectWorkgroupsPerDispatch: 0,
3811
+ indirectDispatches: 0,
3812
+ estimatedIndirectWorkgroupsUpperBound: 0,
3813
+ estimatedIndirectShaderInvocationsUpperBound: 0,
3814
+ indirectDispatchesWithMultiWorkgroupCapacity: 0,
3815
+ largestEstimatedIndirectWorkgroupsPerDispatch: 0,
3816
+ };
3817
+ }
3818
+
3819
+ function countDispatchWorkgroups(groups) {
3820
+ return groups.reduce((product, value) => {
3821
+ const numeric = Number(value ?? 1);
3822
+ const count = Number.isFinite(numeric) ? Math.max(1, Math.trunc(numeric)) : 1;
3823
+ return product * count;
3824
+ }, 1);
3825
+ }
3826
+
3827
+ function recordDirectDispatch(parallelism, groups, invocationsPerWorkgroup = WORKGROUP_SIZE) {
3828
+ const workgroups = countDispatchWorkgroups(groups);
3829
+ parallelism.directDispatches += 1;
3830
+ parallelism.directWorkgroups += workgroups;
3831
+ parallelism.directShaderInvocations += workgroups * invocationsPerWorkgroup;
3832
+ parallelism.largestDirectWorkgroupsPerDispatch = Math.max(
3833
+ parallelism.largestDirectWorkgroupsPerDispatch,
3834
+ workgroups
3835
+ );
3836
+ if (workgroups > 1) {
3837
+ parallelism.multiWorkgroupDispatches += 1;
3838
+ }
3839
+ }
3840
+
3841
+ function recordIndirectDispatch(parallelism, estimatedWorkgroupsUpperBound, invocationsPerWorkgroup = WORKGROUP_SIZE) {
3842
+ const workgroups = Math.max(1, Math.trunc(Number(estimatedWorkgroupsUpperBound) || 1));
3843
+ parallelism.indirectDispatches += 1;
3844
+ parallelism.estimatedIndirectWorkgroupsUpperBound += workgroups;
3845
+ parallelism.estimatedIndirectShaderInvocationsUpperBound += workgroups * invocationsPerWorkgroup;
3846
+ parallelism.largestEstimatedIndirectWorkgroupsPerDispatch = Math.max(
3847
+ parallelism.largestEstimatedIndirectWorkgroupsPerDispatch,
3848
+ workgroups
3849
+ );
3850
+ if (workgroups > 1) {
3851
+ parallelism.indirectDispatchesWithMultiWorkgroupCapacity += 1;
3852
+ }
3853
+ }
3854
+
3855
+ function createGpuParallelismDiagnostics(adapterDiagnostics, counters) {
3856
+ const totalEstimatedWorkgroupsUpperBound =
3857
+ counters.directWorkgroups + counters.estimatedIndirectWorkgroupsUpperBound;
3858
+ const totalEstimatedShaderInvocationsUpperBound =
3859
+ counters.directShaderInvocations + counters.estimatedIndirectShaderInvocationsUpperBound;
3860
+ const exposesMultiWorkgroupParallelism =
3861
+ counters.multiWorkgroupDispatches > 0 || counters.indirectDispatchesWithMultiWorkgroupCapacity > 0;
3862
+ return Object.freeze({
3863
+ ...adapterDiagnostics,
3864
+ directDispatches: counters.directDispatches,
3865
+ directWorkgroups: counters.directWorkgroups,
3866
+ directShaderInvocations: counters.directShaderInvocations,
3867
+ multiWorkgroupDispatches: counters.multiWorkgroupDispatches,
3868
+ largestDirectWorkgroupsPerDispatch: counters.largestDirectWorkgroupsPerDispatch,
3869
+ indirectDispatches: counters.indirectDispatches,
3870
+ estimatedIndirectWorkgroupsUpperBound: counters.estimatedIndirectWorkgroupsUpperBound,
3871
+ estimatedIndirectShaderInvocationsUpperBound: counters.estimatedIndirectShaderInvocationsUpperBound,
3872
+ indirectDispatchesWithMultiWorkgroupCapacity: counters.indirectDispatchesWithMultiWorkgroupCapacity,
3873
+ largestEstimatedIndirectWorkgroupsPerDispatch: counters.largestEstimatedIndirectWorkgroupsPerDispatch,
3874
+ totalEstimatedWorkgroupsUpperBound,
3875
+ totalEstimatedShaderInvocationsUpperBound,
3876
+ exposesMultiWorkgroupParallelism,
3877
+ likelyUsesMoreThanOnePhysicalGpuCore: null,
3878
+ coreUtilizationStatus: "not-exposed-by-webgpu",
3879
+ });
3880
+ }
3881
+
3882
+ function createEnvironmentMapSnapshot(environmentMap) {
3883
+ return Object.freeze({
3884
+ enabled: environmentMap.enabled,
3885
+ width: environmentMap.width,
3886
+ height: environmentMap.height,
3887
+ projection: environmentMap.projection,
3888
+ intensity: environmentMap.intensity,
3889
+ rotationRadians: environmentMap.rotationRadians,
3890
+ ambientStrength: environmentMap.ambientStrength,
3891
+ });
3892
+ }
3893
+
3283
3894
  export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3284
3895
  assertAnalyticDisplayQualityPolicy(options);
3285
3896
  const constants = getGpuUsageConstants();
@@ -3301,6 +3912,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3301
3912
  }
3302
3913
 
3303
3914
  const device = await adapter.requestDevice(createWavefrontDeviceDescriptor(adapter, options));
3915
+ const gpuAdapterParallelism = createGpuAdapterParallelismDiagnostics(adapter, device);
3304
3916
  const context = canvas.getContext("webgpu");
3305
3917
  if (!context || typeof context.configure !== "function") {
3306
3918
  throw new Error("Canvas WebGPU context does not support configure().");
@@ -3334,6 +3946,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3334
3946
  const rayQueueBytes = config.tilePixelCapacity * RAY_RECORD_BYTES;
3335
3947
  const hitBytes = config.tilePixelCapacity * HIT_RECORD_BYTES;
3336
3948
  const accumulationBytes = config.tilePixelCapacity * ACCUMULATION_RECORD_BYTES;
3949
+ const pathVertexBytes = config.tilePixelCapacity * (config.maxDepth + 1) * PATH_VERTEX_RECORD_BYTES;
3337
3950
  const activeQueue = createBuffer(device, bufferUsage, rayQueueBytes, "plasius.wavefront.activeQueue");
3338
3951
  const nextQueue = createBuffer(device, bufferUsage, rayQueueBytes, "plasius.wavefront.nextQueue");
3339
3952
  const hitBuffer = createBuffer(device, bufferUsage, hitBytes, "plasius.wavefront.hitBuffer");
@@ -3343,6 +3956,12 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3343
3956
  accumulationBytes,
3344
3957
  "plasius.wavefront.accumulation"
3345
3958
  );
3959
+ const pathVertexBuffer = createBuffer(
3960
+ device,
3961
+ bufferUsage,
3962
+ pathVertexBytes,
3963
+ "plasius.wavefront.pathVertices"
3964
+ );
3346
3965
  const sceneObjectBuffer = createBuffer(
3347
3966
  device,
3348
3967
  constants.buffer.STORAGE | constants.buffer.COPY_DST,
@@ -3400,9 +4019,10 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3400
4019
  ? uniformOffsetAlignment
3401
4020
  : CONFIG_BUFFER_BYTES
3402
4021
  );
4022
+ const outputConfigSlotCount = config.deferredPathResolve ? 0 : tiles.length;
3403
4023
  const frameConfigSlotCount = Math.max(
3404
4024
  1,
3405
- tiles.length * config.samplesPerPixel + tiles.length + (config.denoise ? 1 : 0)
4025
+ tiles.length * config.samplesPerPixel + outputConfigSlotCount + (config.denoise ? 1 : 0)
3406
4026
  );
3407
4027
  const configBuffer = createBuffer(
3408
4028
  device,
@@ -3490,6 +4110,12 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3490
4110
  magFilter: "nearest",
3491
4111
  minFilter: "nearest",
3492
4112
  });
4113
+ const environmentMapResource = createEnvironmentMapResource(
4114
+ device,
4115
+ constants,
4116
+ config.environmentMap,
4117
+ config.environmentColor
4118
+ );
3493
4119
 
3494
4120
  const traceBindGroupLayout = device.createBindGroupLayout({
3495
4121
  label: "plasius.wavefront.traceBindGroupLayout",
@@ -3518,6 +4144,9 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3518
4144
  storageTexture: { access: "write-only", format: "rgba16float" },
3519
4145
  },
3520
4146
  { binding: 19, visibility: constants.shader.COMPUTE, buffer: { type: "read-only-storage" } },
4147
+ { binding: 20, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
4148
+ { binding: 21, visibility: constants.shader.COMPUTE, sampler: { type: "filtering" } },
4149
+ { binding: 22, visibility: constants.shader.COMPUTE, buffer: { type: "storage" } },
3521
4150
  ],
3522
4151
  });
3523
4152
  const accelerationBindGroupLayout = device.createBindGroupLayout({
@@ -3694,6 +4323,9 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3694
4323
  { binding: 9, resource: { buffer: bvhNodeBuffer } },
3695
4324
  { binding: 16, resource: radianceView },
3696
4325
  { binding: 19, resource: { buffer: environmentPortalBuffer } },
4326
+ { binding: 20, resource: environmentMapResource.view },
4327
+ { binding: 21, resource: environmentMapResource.sampler },
4328
+ { binding: 22, resource: { buffer: pathVertexBuffer } },
3697
4329
  ],
3698
4330
  });
3699
4331
  }
@@ -3787,6 +4419,11 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3787
4419
  let frame = 0;
3788
4420
  let accelerationBuilt = !config.gpuAccelerationBuildRequired;
3789
4421
  let accelerationBuildCount = 0;
4422
+ let activeCameraOptions = options.camera ?? DEFAULT_CAMERA;
4423
+ let lastGpuParallelism = createGpuParallelismDiagnostics(
4424
+ gpuAdapterParallelism,
4425
+ createGpuParallelismCounters()
4426
+ );
3790
4427
 
3791
4428
  function createFrameConfigWriter(frameIndex) {
3792
4429
  let slot = 0;
@@ -3805,7 +4442,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3805
4442
  };
3806
4443
  }
3807
4444
 
3808
- function dispatchGpuAccelerationBuild(frameIndex) {
4445
+ function dispatchGpuAccelerationBuild(frameIndex, parallelism) {
3809
4446
  if (!config.gpuAccelerationBuildRequired || accelerationBuilt) {
3810
4447
  return false;
3811
4448
  }
@@ -3844,24 +4481,32 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3844
4481
  });
3845
4482
  passEncoder.setBindGroup(0, bvhBuildBindGroup, [0]);
3846
4483
  passEncoder.setPipeline(pipelines.prepareMeshTrianglesAndLeaves);
3847
- passEncoder.dispatchWorkgroups(Math.ceil(config.bvhLeafSortCapacity / WORKGROUP_SIZE));
4484
+ const prepareWorkgroups = Math.ceil(config.bvhLeafSortCapacity / WORKGROUP_SIZE);
4485
+ passEncoder.dispatchWorkgroups(prepareWorkgroups);
4486
+ recordDirectDispatch(parallelism, [prepareWorkgroups]);
3848
4487
  passEncoder.setPipeline(pipelines.sortBvhLeafRefs);
3849
4488
  for (let stageIndex = 0; stageIndex < config.bvhSortStages.length; stageIndex += 1) {
3850
4489
  passEncoder.setBindGroup(0, bvhBuildBindGroup, [
3851
4490
  (stageIndex + 1) * configBufferStride,
3852
4491
  ]);
3853
- passEncoder.dispatchWorkgroups(Math.ceil(config.bvhLeafSortCapacity / WORKGROUP_SIZE));
4492
+ const sortWorkgroups = Math.ceil(config.bvhLeafSortCapacity / WORKGROUP_SIZE);
4493
+ passEncoder.dispatchWorkgroups(sortWorkgroups);
4494
+ recordDirectDispatch(parallelism, [sortWorkgroups]);
3854
4495
  }
3855
4496
  passEncoder.setBindGroup(0, bvhBuildBindGroup, [0]);
3856
4497
  passEncoder.setPipeline(pipelines.writeSortedBvhLeaves);
3857
- passEncoder.dispatchWorkgroups(Math.ceil(config.triangleCount / WORKGROUP_SIZE));
4498
+ const leafWriteWorkgroups = Math.ceil(config.triangleCount / WORKGROUP_SIZE);
4499
+ passEncoder.dispatchWorkgroups(leafWriteWorkgroups);
4500
+ recordDirectDispatch(parallelism, [leafWriteWorkgroups]);
3858
4501
  passEncoder.setPipeline(pipelines.buildBvhInternalLevel);
3859
4502
  for (let levelIndex = 0; levelIndex < config.bvhBuildLevels.length; levelIndex += 1) {
3860
4503
  const buildLevel = config.bvhBuildLevels[levelIndex];
3861
4504
  passEncoder.setBindGroup(0, bvhBuildBindGroup, [
3862
4505
  (buildLevelConfigStart + levelIndex) * configBufferStride,
3863
4506
  ]);
3864
- passEncoder.dispatchWorkgroups(Math.ceil(buildLevel.count / WORKGROUP_SIZE));
4507
+ const levelWorkgroups = Math.ceil(buildLevel.count / WORKGROUP_SIZE);
4508
+ passEncoder.dispatchWorkgroups(levelWorkgroups);
4509
+ recordDirectDispatch(parallelism, [levelWorkgroups]);
3865
4510
  }
3866
4511
  passEncoder.end();
3867
4512
  device.queue.submit([encoder.finish()]);
@@ -3870,7 +4515,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3870
4515
  return true;
3871
4516
  }
3872
4517
 
3873
- function encodeTileSample(encoder, tile, configOffset) {
4518
+ function encodeTileSample(encoder, tile, configOffset, parallelism) {
3874
4519
  const generatePass = encoder.beginComputePass({
3875
4520
  label: "plasius.wavefront.generatePrimaryRaysPass",
3876
4521
  });
@@ -3879,6 +4524,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3879
4524
  generatePass.setBindGroup(0, bindGroups[0], [configOffset]);
3880
4525
  generatePass.setPipeline(pipelines.generatePrimaryRays);
3881
4526
  generatePass.dispatchWorkgroups(tileWorkgroups);
4527
+ recordDirectDispatch(parallelism, [tileWorkgroups]);
3882
4528
  generatePass.end();
3883
4529
 
3884
4530
  for (let bounceIndex = 0; bounceIndex < config.maxDepth; bounceIndex += 1) {
@@ -3895,15 +4541,18 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3895
4541
  passEncoder.setBindGroup(0, bindGroups[bounceIndex % 2], [configOffset]);
3896
4542
  passEncoder.setPipeline(pipelines.intersectActiveQueue);
3897
4543
  passEncoder.dispatchWorkgroupsIndirect(activeDispatchBuffer, 0);
4544
+ recordIndirectDispatch(parallelism, tileWorkgroups);
3898
4545
  passEncoder.setPipeline(pipelines.resolveSurfaceRecords);
3899
4546
  passEncoder.dispatchWorkgroupsIndirect(activeDispatchBuffer, 0);
4547
+ recordIndirectDispatch(parallelism, tileWorkgroups);
3900
4548
  passEncoder.setPipeline(pipelines.compactAndSwapQueues);
3901
4549
  passEncoder.dispatchWorkgroups(1);
4550
+ recordDirectDispatch(parallelism, [1], 1);
3902
4551
  passEncoder.end();
3903
4552
  }
3904
4553
  }
3905
4554
 
3906
- function encodeTileOutput(encoder, tile, configOffset) {
4555
+ function encodeTileOutput(encoder, tile, configOffset, parallelism) {
3907
4556
  const passEncoder = encoder.beginComputePass({
3908
4557
  label: "plasius.wavefront.outputPass",
3909
4558
  });
@@ -3912,19 +4561,23 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3912
4561
  passEncoder.setBindGroup(0, bindGroups[0], [configOffset]);
3913
4562
  passEncoder.setPipeline(pipelines.accumulateTerminalRadiance);
3914
4563
  passEncoder.dispatchWorkgroups(tileWorkgroups);
4564
+ recordDirectDispatch(parallelism, [tileWorkgroups]);
3915
4565
  passEncoder.end();
3916
4566
  }
3917
4567
 
3918
- function encodeDenoise(encoder, configOffset) {
4568
+ function encodeDenoise(encoder, configOffset, parallelism) {
3919
4569
  if (!config.denoise) {
3920
4570
  return;
3921
4571
  }
4572
+ const denoiseWorkgroupsX = Math.ceil(config.width / 8);
4573
+ const denoiseWorkgroupsY = Math.ceil(config.height / 8);
3922
4574
  const radiancePass = encoder.beginComputePass({
3923
4575
  label: "plasius.wavefront.denoiseRadiancePass",
3924
4576
  });
3925
4577
  radiancePass.setBindGroup(0, denoiseRadianceBindGroup, [configOffset]);
3926
4578
  radiancePass.setPipeline(pipelines.denoiseLinearRadiance);
3927
- radiancePass.dispatchWorkgroups(Math.ceil(config.width / 8), Math.ceil(config.height / 8));
4579
+ radiancePass.dispatchWorkgroups(denoiseWorkgroupsX, denoiseWorkgroupsY);
4580
+ recordDirectDispatch(parallelism, [denoiseWorkgroupsX, denoiseWorkgroupsY]);
3928
4581
  radiancePass.end();
3929
4582
 
3930
4583
  const resolvePass = encoder.beginComputePass({
@@ -3932,7 +4585,8 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3932
4585
  });
3933
4586
  resolvePass.setBindGroup(0, denoiseResolveBindGroup, [configOffset]);
3934
4587
  resolvePass.setPipeline(pipelines.resolveDenoisedOutputImage);
3935
- resolvePass.dispatchWorkgroups(Math.ceil(config.width / 8), Math.ceil(config.height / 8));
4588
+ resolvePass.dispatchWorkgroups(denoiseWorkgroupsX, denoiseWorkgroupsY);
4589
+ recordDirectDispatch(parallelism, [denoiseWorkgroupsX, denoiseWorkgroupsY]);
3936
4590
  resolvePass.end();
3937
4591
  }
3938
4592
 
@@ -3955,42 +4609,75 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3955
4609
  passEncoder.end();
3956
4610
  }
3957
4611
 
3958
- function dispatchFrame(frameIndex) {
4612
+ function dispatchFrame(frameIndex, parallelism) {
3959
4613
  const writeFrameConfig = createFrameConfigWriter(frameIndex);
3960
- const encoder = device.createCommandEncoder({
3961
- label: `plasius.wavefront.frame.${frameIndex}.batched`,
4614
+ let submissionCount = 0;
4615
+ let encodedFramePasses = 0;
4616
+ let encoder = device.createCommandEncoder({
4617
+ label: `plasius.wavefront.frame.${frameIndex}.batched.${submissionCount + 1}`,
3962
4618
  });
4619
+
4620
+ function submitCurrentEncoder() {
4621
+ if (encodedFramePasses <= 0) {
4622
+ return;
4623
+ }
4624
+ device.queue.submit([encoder.finish()]);
4625
+ submissionCount += 1;
4626
+ encodedFramePasses = 0;
4627
+ encoder = device.createCommandEncoder({
4628
+ label: `plasius.wavefront.frame.${frameIndex}.batched.${submissionCount + 1}`,
4629
+ });
4630
+ }
4631
+
4632
+ function reserveEncoder(passCount = 1) {
4633
+ if (
4634
+ encodedFramePasses > 0 &&
4635
+ encodedFramePasses + passCount > config.maxFramePassesPerSubmission
4636
+ ) {
4637
+ submitCurrentEncoder();
4638
+ }
4639
+ encodedFramePasses += passCount;
4640
+ return encoder;
4641
+ }
4642
+
3963
4643
  for (const tile of tiles) {
3964
4644
  for (let sampleIndex = 0; sampleIndex < config.samplesPerPixel; sampleIndex += 1) {
3965
4645
  const configOffset = writeFrameConfig(tile, {
3966
4646
  sampleIndex,
3967
4647
  sampleWeight: 1 / config.samplesPerPixel,
3968
4648
  });
3969
- encodeTileSample(encoder, tile, configOffset);
4649
+ encodeTileSample(reserveEncoder(), tile, configOffset, parallelism);
4650
+ if (config.deferredPathResolve) {
4651
+ encodeTileOutput(reserveEncoder(), tile, configOffset, parallelism);
4652
+ }
4653
+ }
4654
+ if (!config.deferredPathResolve) {
4655
+ const outputConfigOffset = writeFrameConfig(tile, {
4656
+ sampleIndex: 0,
4657
+ sampleWeight: 1 / config.samplesPerPixel,
4658
+ });
4659
+ encodeTileOutput(reserveEncoder(), tile, outputConfigOffset, parallelism);
3970
4660
  }
3971
- const outputConfigOffset = writeFrameConfig(tile, {
3972
- sampleIndex: 0,
3973
- sampleWeight: 1 / config.samplesPerPixel,
3974
- });
3975
- encodeTileOutput(encoder, tile, outputConfigOffset);
3976
4661
  }
3977
4662
  if (config.denoise) {
3978
4663
  const denoiseConfigOffset = writeFrameConfig(
3979
4664
  { x: 0, y: 0, width: config.width, height: config.height },
3980
4665
  { sampleIndex: 0, sampleWeight: 1 / config.samplesPerPixel }
3981
4666
  );
3982
- encodeDenoise(encoder, denoiseConfigOffset);
4667
+ encodeDenoise(reserveEncoder(), denoiseConfigOffset, parallelism);
3983
4668
  }
3984
- encodePresent(encoder);
3985
- device.queue.submit([encoder.finish()]);
3986
- return 1;
4669
+ encodePresent(reserveEncoder());
4670
+ submitCurrentEncoder();
4671
+ return submissionCount;
3987
4672
  }
3988
4673
 
3989
4674
  function renderOnce() {
3990
4675
  frame += 1;
3991
4676
  const frameIndex = frame + config.frameIndex;
3992
- const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex);
3993
- const frameSubmissionCount = dispatchFrame(frameIndex);
4677
+ const parallelismCounters = createGpuParallelismCounters();
4678
+ const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex, parallelismCounters);
4679
+ const frameSubmissionCount = dispatchFrame(frameIndex, parallelismCounters);
4680
+ lastGpuParallelism = createGpuParallelismDiagnostics(gpuAdapterParallelism, parallelismCounters);
3994
4681
  return Object.freeze({
3995
4682
  frame,
3996
4683
  width: config.width,
@@ -3999,6 +4686,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3999
4686
  tiles: tiles.length,
4000
4687
  tileSize: config.tileSize,
4001
4688
  samplesPerPixel: config.samplesPerPixel,
4689
+ maxFramePassesPerSubmission: config.maxFramePassesPerSubmission,
4002
4690
  screenRays: config.width * config.height,
4003
4691
  primaryRays: config.width * config.height * config.samplesPerPixel,
4004
4692
  sceneObjectCount: config.sceneObjectCount,
@@ -4006,6 +4694,8 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4006
4694
  emissiveTriangleCount: config.emissiveTriangleCount,
4007
4695
  environmentPortalCount: config.environmentPortalCount,
4008
4696
  environmentPortalMode: config.environmentPortalMode,
4697
+ environmentMap: createEnvironmentMapSnapshot(config.environmentMap),
4698
+ deferredPathResolve: config.deferredPathResolve,
4009
4699
  bvhNodeCount: config.bvhNodeCount,
4010
4700
  displayQuality: config.displayQuality,
4011
4701
  accelerationBuildMode: config.accelerationBuildMode,
@@ -4015,6 +4705,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4015
4705
  accelerationBuildCount,
4016
4706
  commandSubmissions: frameSubmissionCount + (accelerationBuildSubmitted ? 1 : 0),
4017
4707
  frameConfigSlots: frameConfigSlotCount,
4708
+ gpuParallelism: lastGpuParallelism,
4018
4709
  memory: config.memory,
4019
4710
  });
4020
4711
  }
@@ -4091,12 +4782,31 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4091
4782
  samplesPerPixel: config.samplesPerPixel,
4092
4783
  sceneObjectCapacity: config.sceneObjectCapacity,
4093
4784
  sceneObjects: packedScene.objects,
4785
+ camera: activeCameraOptions,
4094
4786
  frameIndex: config.frameIndex,
4095
4787
  });
4096
4788
  device.queue.writeBuffer(sceneObjectBuffer, 0, packedScene.buffer);
4097
4789
  return config;
4098
4790
  }
4099
4791
 
4792
+ function updateCamera(cameraOptions = {}) {
4793
+ activeCameraOptions = cameraOptions;
4794
+ config = createWavefrontPathTracingComputeConfig({
4795
+ ...options,
4796
+ canvas,
4797
+ width: config.width,
4798
+ height: config.height,
4799
+ maxDepth: config.maxDepth,
4800
+ tileSize: config.tileSize,
4801
+ samplesPerPixel: config.samplesPerPixel,
4802
+ sceneObjectCapacity: config.sceneObjectCapacity,
4803
+ sceneObjects: packedScene.objects,
4804
+ camera: activeCameraOptions,
4805
+ frameIndex: config.frameIndex,
4806
+ });
4807
+ return config;
4808
+ }
4809
+
4100
4810
  function getSnapshot() {
4101
4811
  return Object.freeze({
4102
4812
  frame,
@@ -4106,11 +4816,14 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4106
4816
  tiles: tiles.length,
4107
4817
  tileSize: config.tileSize,
4108
4818
  samplesPerPixel: config.samplesPerPixel,
4819
+ maxFramePassesPerSubmission: config.maxFramePassesPerSubmission,
4109
4820
  sceneObjectCount: config.sceneObjectCount,
4110
4821
  triangleCount: config.triangleCount,
4111
4822
  emissiveTriangleCount: config.emissiveTriangleCount,
4112
4823
  environmentPortalCount: config.environmentPortalCount,
4113
4824
  environmentPortalMode: config.environmentPortalMode,
4825
+ environmentMap: createEnvironmentMapSnapshot(config.environmentMap),
4826
+ deferredPathResolve: config.deferredPathResolve,
4114
4827
  bvhNodeCount: config.bvhNodeCount,
4115
4828
  displayQuality: config.displayQuality,
4116
4829
  accelerationBuildMode: config.accelerationBuildMode,
@@ -4118,6 +4831,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4118
4831
  accelerationBuilt,
4119
4832
  accelerationBuildCount,
4120
4833
  frameConfigSlots: frameConfigSlotCount,
4834
+ gpuParallelism: lastGpuParallelism,
4121
4835
  memory: config.memory,
4122
4836
  });
4123
4837
  }
@@ -4127,6 +4841,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4127
4841
  nextQueue.destroy?.();
4128
4842
  hitBuffer.destroy?.();
4129
4843
  accumulationBuffer.destroy?.();
4844
+ pathVertexBuffer.destroy?.();
4130
4845
  sceneObjectBuffer.destroy?.();
4131
4846
  triangleBuffer.destroy?.();
4132
4847
  bvhNodeBuffer.destroy?.();
@@ -4142,6 +4857,9 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4142
4857
  radianceTexture.destroy?.();
4143
4858
  denoiseScratchTexture.destroy?.();
4144
4859
  outputTexture.destroy?.();
4860
+ if (environmentMapResource.ownsTexture) {
4861
+ environmentMapResource.texture?.destroy?.();
4862
+ }
4145
4863
  context.unconfigure?.();
4146
4864
  }
4147
4865
 
@@ -4155,6 +4873,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4155
4873
  renderFrame,
4156
4874
  readOutputProbe,
4157
4875
  updateSceneObjects,
4876
+ updateCamera,
4158
4877
  getSnapshot,
4159
4878
  destroy,
4160
4879
  });