@plasius/gpu-renderer 0.2.3 → 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.
@@ -21,11 +21,12 @@ const BVH_LEAF_REF_RECORD_BYTES = 16;
21
21
  const EMISSIVE_TRIANGLE_INDEX_BYTES = 4;
22
22
  const ENVIRONMENT_PORTAL_RECORD_BYTES = 96;
23
23
  const ACCUMULATION_RECORD_BYTES = 16;
24
- const CONFIG_BUFFER_BYTES = 272;
24
+ const PATH_VERTEX_RECORD_BYTES = 16;
25
+ const CONFIG_BUFFER_BYTES = 304;
25
26
  const COUNTER_DISPATCH_ARGS_OFFSET = 16;
26
27
  const INDIRECT_DISPATCH_ARGS_BYTES = 12;
27
28
  const COUNTER_BUFFER_BYTES = 32;
28
- const TRACE_STORAGE_BUFFER_BINDINGS = 9;
29
+ const TRACE_STORAGE_BUFFER_BINDINGS = 10;
29
30
  const HIT_TYPE_SURFACE = 0;
30
31
  const HIT_TYPE_EMISSIVE = 1;
31
32
  const MATERIAL_DIFFUSE = 0;
@@ -53,6 +54,7 @@ const DEFAULT_ENVIRONMENT_LIGHTING = Object.freeze({
53
54
  intensity: 1,
54
55
  mode: 0,
55
56
  exposure: 1,
57
+ sunlitBaseline: 0.16,
56
58
  });
57
59
 
58
60
  export const wavefrontPathTracingComputeLimits = Object.freeze({
@@ -70,6 +72,7 @@ export const wavefrontPathTracingComputeLimits = Object.freeze({
70
72
  emissiveTriangleMetadataRecordBytes: BVH_NODE_RECORD_BYTES,
71
73
  environmentPortalRecordBytes: ENVIRONMENT_PORTAL_RECORD_BYTES,
72
74
  accumulationRecordBytes: ACCUMULATION_RECORD_BYTES,
75
+ pathVertexRecordBytes: PATH_VERTEX_RECORD_BYTES,
73
76
  counterRecordBytes: COUNTER_BUFFER_BYTES,
74
77
  indirectDispatchRecordBytes: INDIRECT_DISPATCH_ARGS_BYTES,
75
78
  });
@@ -161,6 +164,39 @@ function asColor(value, fallback = [1, 1, 1, 1]) {
161
164
  ];
162
165
  }
163
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
+
164
200
  function emissionPower(emission) {
165
201
  return Math.max(0, emission?.[0] ?? 0) + Math.max(0, emission?.[1] ?? 0) + Math.max(0, emission?.[2] ?? 0);
166
202
  }
@@ -914,6 +950,14 @@ function resolveEnvironmentLighting(input, environmentColor, ambientColor) {
914
950
  intensity: Math.max(0.0001, readFiniteNumber("environmentLighting.intensity", source.intensity, DEFAULT_ENVIRONMENT_LIGHTING.intensity)),
915
951
  mode: readNonNegativeInteger("environmentLighting.mode", source.mode, DEFAULT_ENVIRONMENT_LIGHTING.mode),
916
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
+ ),
917
961
  });
918
962
  }
919
963
 
@@ -1117,6 +1161,11 @@ export function estimateWavefrontPathTracingMemory(options = {}) {
1117
1161
  options.tilePixelCapacity,
1118
1162
  DEFAULT_TILE_SIZE * DEFAULT_TILE_SIZE
1119
1163
  );
1164
+ const maxDepth = clamp(
1165
+ readPositiveInteger("maxDepth", options.maxDepth, DEFAULT_MAX_DEPTH),
1166
+ 1,
1167
+ 16
1168
+ );
1120
1169
  const sceneObjectCapacity = readPositiveInteger(
1121
1170
  "sceneObjectCapacity",
1122
1171
  options.sceneObjectCapacity,
@@ -1142,6 +1191,7 @@ export function estimateWavefrontPathTracingMemory(options = {}) {
1142
1191
  const queueBytes = tilePixelCapacity * RAY_RECORD_BYTES;
1143
1192
  const hitBytes = tilePixelCapacity * HIT_RECORD_BYTES;
1144
1193
  const accumulationBytes = tilePixelCapacity * ACCUMULATION_RECORD_BYTES;
1194
+ const pathVertexBytes = tilePixelCapacity * (maxDepth + 1) * PATH_VERTEX_RECORD_BYTES;
1145
1195
  const sceneObjectBytes = sceneObjectCapacity * SCENE_OBJECT_RECORD_BYTES;
1146
1196
  const triangleBytes = triangleCapacity * TRIANGLE_RECORD_BYTES;
1147
1197
  const bvhNodeBytes = bvhNodeCapacity * BVH_NODE_RECORD_BYTES;
@@ -1156,6 +1206,7 @@ export function estimateWavefrontPathTracingMemory(options = {}) {
1156
1206
  queuePairBytes: queueBytes * 2,
1157
1207
  hitBytes,
1158
1208
  accumulationBytes,
1209
+ pathVertexBytes,
1159
1210
  sceneObjectBytes,
1160
1211
  triangleBytes,
1161
1212
  bvhNodeBytes,
@@ -1169,6 +1220,7 @@ export function estimateWavefrontPathTracingMemory(options = {}) {
1169
1220
  queueBytes * 2 +
1170
1221
  hitBytes +
1171
1222
  accumulationBytes +
1223
+ pathVertexBytes +
1172
1224
  sceneObjectBytes +
1173
1225
  triangleBytes +
1174
1226
  bvhNodeBytes +
@@ -1286,6 +1338,12 @@ export function createWavefrontPathTracingComputeConfig(options = {}) {
1286
1338
  options.environmentLighting?.environmentPortalMode,
1287
1339
  environmentPortals.length > 0
1288
1340
  );
1341
+ const environmentMap = resolveEnvironmentMap(
1342
+ options.environmentMap ??
1343
+ options.environmentTexture ??
1344
+ options.environmentLighting?.environmentMap
1345
+ );
1346
+ const deferredPathResolve = resolveDeferredPathResolve(options);
1289
1347
 
1290
1348
  return Object.freeze({
1291
1349
  mode: rendererWavefrontComputeMode,
@@ -1321,12 +1379,15 @@ export function createWavefrontPathTracingComputeConfig(options = {}) {
1321
1379
  environmentPortalCount: environmentPortals.length,
1322
1380
  environmentPortalCapacity,
1323
1381
  environmentPortalMode,
1382
+ environmentMap,
1383
+ deferredPathResolve,
1324
1384
  displayQuality: options.displayQuality === true,
1325
1385
  requiresMeshBvhForDisplayQuality: true,
1326
1386
  denoise: options.denoise !== false,
1327
1387
  frameIndex: readNonNegativeInteger("frameIndex", options.frameIndex, 0),
1328
1388
  memory: estimateWavefrontPathTracingMemory({
1329
1389
  tilePixelCapacity,
1390
+ maxDepth,
1330
1391
  sceneObjectCapacity,
1331
1392
  triangleCapacity,
1332
1393
  bvhNodeCapacity,
@@ -1550,6 +1611,18 @@ function createConfigPayload(config, tile, frameIndex, buildRange = {}) {
1550
1611
  data.setUint32(260, config.environmentPortalMode ?? 0, true);
1551
1612
  data.setUint32(264, 0, true);
1552
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
+ ]);
1553
1626
  return bytes;
1554
1627
  }
1555
1628
 
@@ -1822,6 +1895,149 @@ function alignTo(value, alignment) {
1822
1895
  return Math.ceil(value / resolvedAlignment) * resolvedAlignment;
1823
1896
  }
1824
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
+
1825
2041
  async function getPipelineDiagnostics(shaderModule) {
1826
2042
  if (typeof shaderModule?.compilationInfo !== "function") {
1827
2043
  return "";
@@ -2035,6 +2251,8 @@ struct FrameConfig {
2035
2251
  environmentPortalMode: u32,
2036
2252
  _portalPad0: u32,
2037
2253
  _portalPad1: u32,
2254
+ environmentMapSettings: vec4<f32>,
2255
+ pathResolveSettings: vec4<f32>,
2038
2256
  };
2039
2257
 
2040
2258
  struct Counters {
@@ -2094,6 +2312,9 @@ struct EnvironmentPortal {
2094
2312
  @group(0) @binding(17) var finalDenoiseInputRadiance: texture_2d<f32>;
2095
2313
  @group(0) @binding(18) var denoisedOutputImage: texture_storage_2d<rgba8unorm, write>;
2096
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>>;
2097
2318
 
2098
2319
  fn hash_u32(value: u32) -> u32 {
2099
2320
  var x = value;
@@ -2138,6 +2359,89 @@ fn max_component(value: vec3<f32>) -> f32 {
2138
2359
  return max(max(value.x, value.y), value.z);
2139
2360
  }
2140
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
+
2141
2445
  fn environment_portal_radiance_scale(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
2142
2446
  if (config.environmentPortalCount == 0u || config.environmentPortalMode == 0u) {
2143
2447
  return vec3<f32>(1.0);
@@ -2177,22 +2481,9 @@ fn environment_portal_radiance_scale(origin: vec3<f32>, direction: vec3<f32>) ->
2177
2481
 
2178
2482
  fn environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
2179
2483
  let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
2180
- let upFactor = saturate(rayDirection.y * 0.5 + 0.5);
2181
- let sunDirection = safe_normalize(
2182
- config.environmentSunDirectionIntensity.xyz,
2183
- vec3<f32>(0.0, 1.0, 0.0)
2184
- );
2185
- let sunGlow = pow(saturate(dot(rayDirection, sunDirection)), 192.0);
2186
- let gradient =
2187
- config.environmentHorizonColor.xyz * (1.0 - upFactor) +
2188
- config.environmentZenithColor.xyz * upFactor;
2189
2484
  let portalScale = environment_portal_radiance_scale(origin, rayDirection);
2190
2485
  let portalHit = max_component(portalScale) > 0.0001;
2191
- return (
2192
- gradient +
2193
- config.environmentSunColor.xyz * sunGlow
2194
- ) *
2195
- max(config.environmentSunDirectionIntensity.w, 0.0001) *
2486
+ return base_environment_radiance(rayDirection) *
2196
2487
  select(vec3<f32>(1.0), portalScale, portalHit);
2197
2488
  }
2198
2489
 
@@ -2208,16 +2499,59 @@ fn gated_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f
2208
2499
  return environment_radiance(origin, direction);
2209
2500
  }
2210
2501
 
2211
- 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> {
2212
2527
  let normal = safe_normalize(hit.shadingNormal.xyz, vec3<f32>(0.0, 1.0, 0.0));
2213
- let surfaceColor = max(hit.color.xyz, config.ambientColor.xyz);
2214
2528
  let normalEnvironment = gated_environment_radiance(
2215
2529
  hit.position.xyz + normal * 0.003,
2216
2530
  normal
2217
2531
  );
2218
- 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));
2219
2544
  let materialFloor = select(0.7, 1.0, hit.materialKind == 0u || hit.materialKind == 3u);
2220
- 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
+ );
2221
2555
  }
2222
2556
 
2223
2557
  fn direct_environment_portal_irradiance(origin: vec3<f32>, normal: vec3<f32>) -> vec3<f32> {
@@ -2270,7 +2604,17 @@ fn surface_direct_environment_contribution(ray: RayRecord, hit: HitRecord) -> ve
2270
2604
 
2271
2605
  let normalEnvironment = gated_environment_radiance(origin, normal);
2272
2606
  let skyVisibility = 0.35 + saturate(normal.y * 0.5 + 0.5) * 0.45;
2273
- let skyIrradiance = max(config.ambientColor.xyz * 0.72, normalEnvironment * skyVisibility * 0.16);
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);
2274
2618
 
2275
2619
  let sunDirection = safe_normalize(
2276
2620
  config.environmentSunDirectionIntensity.xyz,
@@ -2894,6 +3238,7 @@ fn generatePrimaryRays(@builtin(global_invocation_id) globalId: vec3<u32>) {
2894
3238
  return;
2895
3239
  }
2896
3240
  activeQueue[index] = make_ray(index);
3241
+ clear_deferred_path(index);
2897
3242
  if (u32(config.projectionAndSampling.w) == 0u) {
2898
3243
  accumulation[index] = vec4<f32>(0.0);
2899
3244
  }
@@ -3146,25 +3491,37 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
3146
3491
 
3147
3492
  if (hit.hitType == 1u) {
3148
3493
  let guidedLightWeight = select(1.0, 0.24, (ray.flags & RAY_FLAG_GUIDED_EMISSIVE) != 0u);
3149
- contribution = clamp_sample_radiance(
3150
- ray.throughput.xyz * max(hit.emission.xyz, hit.color.xyz) * guidedLightWeight
3151
- );
3152
- accumulation[ray.rayId] =
3153
- 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
+ }
3154
3502
  atomicAdd(&counters.terminatedCount, 1u);
3155
3503
  return;
3156
3504
  }
3157
3505
 
3158
3506
  if (hit.hitType == 2u) {
3159
- contribution = clamp_sample_radiance(ray.throughput.xyz * max(hit.color.xyz, config.ambientColor.xyz));
3160
- accumulation[ray.rayId] =
3161
- 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
+ }
3162
3514
  atomicAdd(&counters.terminatedCount, 1u);
3163
3515
  return;
3164
3516
  }
3165
3517
 
3518
+ let response = surface_path_response(hit);
3519
+ record_deferred_path_response(ray, response);
3520
+
3166
3521
  let shouldEstimateDirectEnvironment =
3167
- (hit.materialKind == 0u || hit.materialKind == 1u) && hit.material.z >= 0.95;
3522
+ !deferred_path_resolve_enabled() &&
3523
+ (hit.materialKind == 0u || hit.materialKind == 1u) &&
3524
+ hit.material.z >= 0.95;
3168
3525
  if (shouldEstimateDirectEnvironment) {
3169
3526
  let directEnvironment = surface_direct_environment_contribution(ray, hit);
3170
3527
  accumulation[ray.rayId] =
@@ -3172,9 +3529,13 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
3172
3529
  }
3173
3530
 
3174
3531
  if (ray.bounce + 1u >= config.maxDepth) {
3175
- let terminalEnvironment = terminal_surface_environment_contribution(ray, hit);
3176
- accumulation[ray.rayId] =
3177
- accumulation[ray.rayId] + vec4<f32>(terminalEnvironment * sample_weight(), 1.0);
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
+ }
3178
3539
  atomicAdd(&counters.terminatedCount, 1u);
3179
3540
  return;
3180
3541
  }
@@ -3183,17 +3544,17 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
3183
3544
  let scatter = scatter_direction(ray, hit, seed);
3184
3545
  let nextIndex = atomicAdd(&counters.nextCount, 1u);
3185
3546
  if (nextIndex >= config.tilePixelCount) {
3186
- let overflowEnvironment = terminal_surface_environment_contribution(ray, hit);
3187
- accumulation[ray.rayId] =
3188
- 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
+ }
3189
3554
  atomicAdd(&counters.terminatedCount, 1u);
3190
3555
  return;
3191
3556
  }
3192
- let color = clamp(hit.color.xyz, vec3<f32>(0.0), vec3<f32>(1.0));
3193
- let opacity = clamp(hit.material.z, 0.0, 1.0);
3194
- let materialEnergy = select(0.68, 0.92, hit.materialKind == 1u || hit.materialKind == 2u);
3195
- let transparentEnergy = select(materialEnergy, 0.9, hit.hitType == 3u);
3196
- let throughput = ray.throughput.xyz * mix(vec3<f32>(1.0), color, max(opacity, 0.18)) * transparentEnergy;
3557
+ let throughput = ray.throughput.xyz * response;
3197
3558
  nextQueue[nextIndex] = RayRecord(
3198
3559
  ray.rayId,
3199
3560
  ray.rayId,
@@ -3221,6 +3582,27 @@ fn compactAndSwapQueues(@builtin(global_invocation_id) globalId: vec3<u32>) {
3221
3582
  write_active_dispatch_args(activeCount);
3222
3583
  }
3223
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
+
3224
3606
  @compute @workgroup_size(64)
3225
3607
  fn accumulateTerminalRadiance(@builtin(global_invocation_id) globalId: vec3<u32>) {
3226
3608
  let index = globalId.x;
@@ -3230,7 +3612,12 @@ fn accumulateTerminalRadiance(@builtin(global_invocation_id) globalId: vec3<u32>
3230
3612
  let localX = index % config.tileWidth;
3231
3613
  let localY = index / config.tileWidth;
3232
3614
  let pixel = vec2<i32>(i32(config.tileX + localX), i32(config.tileY + localY));
3233
- 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
+ }
3234
3621
 
3235
3622
  textureStore(radianceImage, pixel, vec4<f32>(radiance, 1.0));
3236
3623
  if (config.denoise == 0u) {
@@ -3373,6 +3760,137 @@ function createWavefrontDeviceDescriptor(adapter, options = {}) {
3373
3760
  return Object.keys(descriptor).length > 0 ? descriptor : undefined;
3374
3761
  }
3375
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
+
3376
3894
  export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3377
3895
  assertAnalyticDisplayQualityPolicy(options);
3378
3896
  const constants = getGpuUsageConstants();
@@ -3394,6 +3912,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3394
3912
  }
3395
3913
 
3396
3914
  const device = await adapter.requestDevice(createWavefrontDeviceDescriptor(adapter, options));
3915
+ const gpuAdapterParallelism = createGpuAdapterParallelismDiagnostics(adapter, device);
3397
3916
  const context = canvas.getContext("webgpu");
3398
3917
  if (!context || typeof context.configure !== "function") {
3399
3918
  throw new Error("Canvas WebGPU context does not support configure().");
@@ -3427,6 +3946,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3427
3946
  const rayQueueBytes = config.tilePixelCapacity * RAY_RECORD_BYTES;
3428
3947
  const hitBytes = config.tilePixelCapacity * HIT_RECORD_BYTES;
3429
3948
  const accumulationBytes = config.tilePixelCapacity * ACCUMULATION_RECORD_BYTES;
3949
+ const pathVertexBytes = config.tilePixelCapacity * (config.maxDepth + 1) * PATH_VERTEX_RECORD_BYTES;
3430
3950
  const activeQueue = createBuffer(device, bufferUsage, rayQueueBytes, "plasius.wavefront.activeQueue");
3431
3951
  const nextQueue = createBuffer(device, bufferUsage, rayQueueBytes, "plasius.wavefront.nextQueue");
3432
3952
  const hitBuffer = createBuffer(device, bufferUsage, hitBytes, "plasius.wavefront.hitBuffer");
@@ -3436,6 +3956,12 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3436
3956
  accumulationBytes,
3437
3957
  "plasius.wavefront.accumulation"
3438
3958
  );
3959
+ const pathVertexBuffer = createBuffer(
3960
+ device,
3961
+ bufferUsage,
3962
+ pathVertexBytes,
3963
+ "plasius.wavefront.pathVertices"
3964
+ );
3439
3965
  const sceneObjectBuffer = createBuffer(
3440
3966
  device,
3441
3967
  constants.buffer.STORAGE | constants.buffer.COPY_DST,
@@ -3493,9 +4019,10 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3493
4019
  ? uniformOffsetAlignment
3494
4020
  : CONFIG_BUFFER_BYTES
3495
4021
  );
4022
+ const outputConfigSlotCount = config.deferredPathResolve ? 0 : tiles.length;
3496
4023
  const frameConfigSlotCount = Math.max(
3497
4024
  1,
3498
- tiles.length * config.samplesPerPixel + tiles.length + (config.denoise ? 1 : 0)
4025
+ tiles.length * config.samplesPerPixel + outputConfigSlotCount + (config.denoise ? 1 : 0)
3499
4026
  );
3500
4027
  const configBuffer = createBuffer(
3501
4028
  device,
@@ -3583,6 +4110,12 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3583
4110
  magFilter: "nearest",
3584
4111
  minFilter: "nearest",
3585
4112
  });
4113
+ const environmentMapResource = createEnvironmentMapResource(
4114
+ device,
4115
+ constants,
4116
+ config.environmentMap,
4117
+ config.environmentColor
4118
+ );
3586
4119
 
3587
4120
  const traceBindGroupLayout = device.createBindGroupLayout({
3588
4121
  label: "plasius.wavefront.traceBindGroupLayout",
@@ -3611,6 +4144,9 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3611
4144
  storageTexture: { access: "write-only", format: "rgba16float" },
3612
4145
  },
3613
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" } },
3614
4150
  ],
3615
4151
  });
3616
4152
  const accelerationBindGroupLayout = device.createBindGroupLayout({
@@ -3787,6 +4323,9 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3787
4323
  { binding: 9, resource: { buffer: bvhNodeBuffer } },
3788
4324
  { binding: 16, resource: radianceView },
3789
4325
  { binding: 19, resource: { buffer: environmentPortalBuffer } },
4326
+ { binding: 20, resource: environmentMapResource.view },
4327
+ { binding: 21, resource: environmentMapResource.sampler },
4328
+ { binding: 22, resource: { buffer: pathVertexBuffer } },
3790
4329
  ],
3791
4330
  });
3792
4331
  }
@@ -3881,6 +4420,10 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3881
4420
  let accelerationBuilt = !config.gpuAccelerationBuildRequired;
3882
4421
  let accelerationBuildCount = 0;
3883
4422
  let activeCameraOptions = options.camera ?? DEFAULT_CAMERA;
4423
+ let lastGpuParallelism = createGpuParallelismDiagnostics(
4424
+ gpuAdapterParallelism,
4425
+ createGpuParallelismCounters()
4426
+ );
3884
4427
 
3885
4428
  function createFrameConfigWriter(frameIndex) {
3886
4429
  let slot = 0;
@@ -3899,7 +4442,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3899
4442
  };
3900
4443
  }
3901
4444
 
3902
- function dispatchGpuAccelerationBuild(frameIndex) {
4445
+ function dispatchGpuAccelerationBuild(frameIndex, parallelism) {
3903
4446
  if (!config.gpuAccelerationBuildRequired || accelerationBuilt) {
3904
4447
  return false;
3905
4448
  }
@@ -3938,24 +4481,32 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3938
4481
  });
3939
4482
  passEncoder.setBindGroup(0, bvhBuildBindGroup, [0]);
3940
4483
  passEncoder.setPipeline(pipelines.prepareMeshTrianglesAndLeaves);
3941
- 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]);
3942
4487
  passEncoder.setPipeline(pipelines.sortBvhLeafRefs);
3943
4488
  for (let stageIndex = 0; stageIndex < config.bvhSortStages.length; stageIndex += 1) {
3944
4489
  passEncoder.setBindGroup(0, bvhBuildBindGroup, [
3945
4490
  (stageIndex + 1) * configBufferStride,
3946
4491
  ]);
3947
- 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]);
3948
4495
  }
3949
4496
  passEncoder.setBindGroup(0, bvhBuildBindGroup, [0]);
3950
4497
  passEncoder.setPipeline(pipelines.writeSortedBvhLeaves);
3951
- 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]);
3952
4501
  passEncoder.setPipeline(pipelines.buildBvhInternalLevel);
3953
4502
  for (let levelIndex = 0; levelIndex < config.bvhBuildLevels.length; levelIndex += 1) {
3954
4503
  const buildLevel = config.bvhBuildLevels[levelIndex];
3955
4504
  passEncoder.setBindGroup(0, bvhBuildBindGroup, [
3956
4505
  (buildLevelConfigStart + levelIndex) * configBufferStride,
3957
4506
  ]);
3958
- 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]);
3959
4510
  }
3960
4511
  passEncoder.end();
3961
4512
  device.queue.submit([encoder.finish()]);
@@ -3964,7 +4515,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3964
4515
  return true;
3965
4516
  }
3966
4517
 
3967
- function encodeTileSample(encoder, tile, configOffset) {
4518
+ function encodeTileSample(encoder, tile, configOffset, parallelism) {
3968
4519
  const generatePass = encoder.beginComputePass({
3969
4520
  label: "plasius.wavefront.generatePrimaryRaysPass",
3970
4521
  });
@@ -3973,6 +4524,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3973
4524
  generatePass.setBindGroup(0, bindGroups[0], [configOffset]);
3974
4525
  generatePass.setPipeline(pipelines.generatePrimaryRays);
3975
4526
  generatePass.dispatchWorkgroups(tileWorkgroups);
4527
+ recordDirectDispatch(parallelism, [tileWorkgroups]);
3976
4528
  generatePass.end();
3977
4529
 
3978
4530
  for (let bounceIndex = 0; bounceIndex < config.maxDepth; bounceIndex += 1) {
@@ -3989,15 +4541,18 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3989
4541
  passEncoder.setBindGroup(0, bindGroups[bounceIndex % 2], [configOffset]);
3990
4542
  passEncoder.setPipeline(pipelines.intersectActiveQueue);
3991
4543
  passEncoder.dispatchWorkgroupsIndirect(activeDispatchBuffer, 0);
4544
+ recordIndirectDispatch(parallelism, tileWorkgroups);
3992
4545
  passEncoder.setPipeline(pipelines.resolveSurfaceRecords);
3993
4546
  passEncoder.dispatchWorkgroupsIndirect(activeDispatchBuffer, 0);
4547
+ recordIndirectDispatch(parallelism, tileWorkgroups);
3994
4548
  passEncoder.setPipeline(pipelines.compactAndSwapQueues);
3995
4549
  passEncoder.dispatchWorkgroups(1);
4550
+ recordDirectDispatch(parallelism, [1], 1);
3996
4551
  passEncoder.end();
3997
4552
  }
3998
4553
  }
3999
4554
 
4000
- function encodeTileOutput(encoder, tile, configOffset) {
4555
+ function encodeTileOutput(encoder, tile, configOffset, parallelism) {
4001
4556
  const passEncoder = encoder.beginComputePass({
4002
4557
  label: "plasius.wavefront.outputPass",
4003
4558
  });
@@ -4006,19 +4561,23 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4006
4561
  passEncoder.setBindGroup(0, bindGroups[0], [configOffset]);
4007
4562
  passEncoder.setPipeline(pipelines.accumulateTerminalRadiance);
4008
4563
  passEncoder.dispatchWorkgroups(tileWorkgroups);
4564
+ recordDirectDispatch(parallelism, [tileWorkgroups]);
4009
4565
  passEncoder.end();
4010
4566
  }
4011
4567
 
4012
- function encodeDenoise(encoder, configOffset) {
4568
+ function encodeDenoise(encoder, configOffset, parallelism) {
4013
4569
  if (!config.denoise) {
4014
4570
  return;
4015
4571
  }
4572
+ const denoiseWorkgroupsX = Math.ceil(config.width / 8);
4573
+ const denoiseWorkgroupsY = Math.ceil(config.height / 8);
4016
4574
  const radiancePass = encoder.beginComputePass({
4017
4575
  label: "plasius.wavefront.denoiseRadiancePass",
4018
4576
  });
4019
4577
  radiancePass.setBindGroup(0, denoiseRadianceBindGroup, [configOffset]);
4020
4578
  radiancePass.setPipeline(pipelines.denoiseLinearRadiance);
4021
- radiancePass.dispatchWorkgroups(Math.ceil(config.width / 8), Math.ceil(config.height / 8));
4579
+ radiancePass.dispatchWorkgroups(denoiseWorkgroupsX, denoiseWorkgroupsY);
4580
+ recordDirectDispatch(parallelism, [denoiseWorkgroupsX, denoiseWorkgroupsY]);
4022
4581
  radiancePass.end();
4023
4582
 
4024
4583
  const resolvePass = encoder.beginComputePass({
@@ -4026,7 +4585,8 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4026
4585
  });
4027
4586
  resolvePass.setBindGroup(0, denoiseResolveBindGroup, [configOffset]);
4028
4587
  resolvePass.setPipeline(pipelines.resolveDenoisedOutputImage);
4029
- resolvePass.dispatchWorkgroups(Math.ceil(config.width / 8), Math.ceil(config.height / 8));
4588
+ resolvePass.dispatchWorkgroups(denoiseWorkgroupsX, denoiseWorkgroupsY);
4589
+ recordDirectDispatch(parallelism, [denoiseWorkgroupsX, denoiseWorkgroupsY]);
4030
4590
  resolvePass.end();
4031
4591
  }
4032
4592
 
@@ -4049,7 +4609,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4049
4609
  passEncoder.end();
4050
4610
  }
4051
4611
 
4052
- function dispatchFrame(frameIndex) {
4612
+ function dispatchFrame(frameIndex, parallelism) {
4053
4613
  const writeFrameConfig = createFrameConfigWriter(frameIndex);
4054
4614
  let submissionCount = 0;
4055
4615
  let encodedFramePasses = 0;
@@ -4086,20 +4646,25 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4086
4646
  sampleIndex,
4087
4647
  sampleWeight: 1 / config.samplesPerPixel,
4088
4648
  });
4089
- encodeTileSample(reserveEncoder(), 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);
4090
4660
  }
4091
- const outputConfigOffset = writeFrameConfig(tile, {
4092
- sampleIndex: 0,
4093
- sampleWeight: 1 / config.samplesPerPixel,
4094
- });
4095
- encodeTileOutput(reserveEncoder(), tile, outputConfigOffset);
4096
4661
  }
4097
4662
  if (config.denoise) {
4098
4663
  const denoiseConfigOffset = writeFrameConfig(
4099
4664
  { x: 0, y: 0, width: config.width, height: config.height },
4100
4665
  { sampleIndex: 0, sampleWeight: 1 / config.samplesPerPixel }
4101
4666
  );
4102
- encodeDenoise(reserveEncoder(), denoiseConfigOffset);
4667
+ encodeDenoise(reserveEncoder(), denoiseConfigOffset, parallelism);
4103
4668
  }
4104
4669
  encodePresent(reserveEncoder());
4105
4670
  submitCurrentEncoder();
@@ -4109,8 +4674,10 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4109
4674
  function renderOnce() {
4110
4675
  frame += 1;
4111
4676
  const frameIndex = frame + config.frameIndex;
4112
- const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex);
4113
- 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);
4114
4681
  return Object.freeze({
4115
4682
  frame,
4116
4683
  width: config.width,
@@ -4127,6 +4694,8 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4127
4694
  emissiveTriangleCount: config.emissiveTriangleCount,
4128
4695
  environmentPortalCount: config.environmentPortalCount,
4129
4696
  environmentPortalMode: config.environmentPortalMode,
4697
+ environmentMap: createEnvironmentMapSnapshot(config.environmentMap),
4698
+ deferredPathResolve: config.deferredPathResolve,
4130
4699
  bvhNodeCount: config.bvhNodeCount,
4131
4700
  displayQuality: config.displayQuality,
4132
4701
  accelerationBuildMode: config.accelerationBuildMode,
@@ -4136,6 +4705,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4136
4705
  accelerationBuildCount,
4137
4706
  commandSubmissions: frameSubmissionCount + (accelerationBuildSubmitted ? 1 : 0),
4138
4707
  frameConfigSlots: frameConfigSlotCount,
4708
+ gpuParallelism: lastGpuParallelism,
4139
4709
  memory: config.memory,
4140
4710
  });
4141
4711
  }
@@ -4252,6 +4822,8 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4252
4822
  emissiveTriangleCount: config.emissiveTriangleCount,
4253
4823
  environmentPortalCount: config.environmentPortalCount,
4254
4824
  environmentPortalMode: config.environmentPortalMode,
4825
+ environmentMap: createEnvironmentMapSnapshot(config.environmentMap),
4826
+ deferredPathResolve: config.deferredPathResolve,
4255
4827
  bvhNodeCount: config.bvhNodeCount,
4256
4828
  displayQuality: config.displayQuality,
4257
4829
  accelerationBuildMode: config.accelerationBuildMode,
@@ -4259,6 +4831,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4259
4831
  accelerationBuilt,
4260
4832
  accelerationBuildCount,
4261
4833
  frameConfigSlots: frameConfigSlotCount,
4834
+ gpuParallelism: lastGpuParallelism,
4262
4835
  memory: config.memory,
4263
4836
  });
4264
4837
  }
@@ -4268,6 +4841,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4268
4841
  nextQueue.destroy?.();
4269
4842
  hitBuffer.destroy?.();
4270
4843
  accumulationBuffer.destroy?.();
4844
+ pathVertexBuffer.destroy?.();
4271
4845
  sceneObjectBuffer.destroy?.();
4272
4846
  triangleBuffer.destroy?.();
4273
4847
  bvhNodeBuffer.destroy?.();
@@ -4283,6 +4857,9 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
4283
4857
  radianceTexture.destroy?.();
4284
4858
  denoiseScratchTexture.destroy?.();
4285
4859
  outputTexture.destroy?.();
4860
+ if (environmentMapResource.ownsTexture) {
4861
+ environmentMapResource.texture?.destroy?.();
4862
+ }
4286
4863
  context.unconfigure?.();
4287
4864
  }
4288
4865