@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.
package/dist/index.js CHANGED
@@ -22,11 +22,12 @@ var BVH_LEAF_REF_RECORD_BYTES = 16;
22
22
  var EMISSIVE_TRIANGLE_INDEX_BYTES = 4;
23
23
  var ENVIRONMENT_PORTAL_RECORD_BYTES = 96;
24
24
  var ACCUMULATION_RECORD_BYTES = 16;
25
- var CONFIG_BUFFER_BYTES = 272;
25
+ var PATH_VERTEX_RECORD_BYTES = 16;
26
+ var CONFIG_BUFFER_BYTES = 304;
26
27
  var COUNTER_DISPATCH_ARGS_OFFSET = 16;
27
28
  var INDIRECT_DISPATCH_ARGS_BYTES = 12;
28
29
  var COUNTER_BUFFER_BYTES = 32;
29
- var TRACE_STORAGE_BUFFER_BINDINGS = 9;
30
+ var TRACE_STORAGE_BUFFER_BINDINGS = 10;
30
31
  var MATERIAL_DIFFUSE = 0;
31
32
  var MATERIAL_METAL = 1;
32
33
  var MATERIAL_DIELECTRIC = 2;
@@ -49,7 +50,8 @@ var DEFAULT_ENVIRONMENT_LIGHTING = Object.freeze({
49
50
  sunColor: Object.freeze([2.8, 2.65, 2.35, 1]),
50
51
  intensity: 1,
51
52
  mode: 0,
52
- exposure: 1
53
+ exposure: 1,
54
+ sunlitBaseline: 0.16
53
55
  });
54
56
  var wavefrontPathTracingComputeLimits = Object.freeze({
55
57
  workgroupSize: WORKGROUP_SIZE,
@@ -66,6 +68,7 @@ var wavefrontPathTracingComputeLimits = Object.freeze({
66
68
  emissiveTriangleMetadataRecordBytes: BVH_NODE_RECORD_BYTES,
67
69
  environmentPortalRecordBytes: ENVIRONMENT_PORTAL_RECORD_BYTES,
68
70
  accumulationRecordBytes: ACCUMULATION_RECORD_BYTES,
71
+ pathVertexRecordBytes: PATH_VERTEX_RECORD_BYTES,
69
72
  counterRecordBytes: COUNTER_BUFFER_BYTES,
70
73
  indirectDispatchRecordBytes: INDIRECT_DISPATCH_ARGS_BYTES
71
74
  });
@@ -142,6 +145,33 @@ function asColor(value, fallback = [1, 1, 1, 1]) {
142
145
  clamp(readFiniteNumber("color[3]", value[3], fallback[3] ?? 1), 0, 1)
143
146
  ];
144
147
  }
148
+ function resolveEnvironmentMap(input = null) {
149
+ const source = input && typeof input === "object" ? input : null;
150
+ const hasTexture = Boolean(source?.view || source?.texture || source?.data);
151
+ const width = readPositiveInteger("environmentMap.width", source?.width, 1);
152
+ const height = readPositiveInteger("environmentMap.height", source?.height, 1);
153
+ return Object.freeze({
154
+ enabled: hasTexture && source?.enabled !== false,
155
+ width,
156
+ height,
157
+ format: typeof source?.format === "string" ? source.format : "rgba16float",
158
+ projection: typeof source?.projection === "string" ? source.projection : "equirectangular",
159
+ texture: source?.texture ?? null,
160
+ view: source?.view ?? null,
161
+ sampler: source?.sampler ?? null,
162
+ data: source?.data ?? null,
163
+ intensity: Math.max(0, readFiniteNumber("environmentMap.intensity", source?.intensity ?? source?.radianceScale, 1)),
164
+ rotationRadians: readFiniteNumber("environmentMap.rotationRadians", source?.rotationRadians ?? source?.rotation, 0),
165
+ ambientStrength: Math.max(
166
+ 0,
167
+ readFiniteNumber("environmentMap.ambientStrength", source?.ambientStrength, 0.32)
168
+ )
169
+ });
170
+ }
171
+ function resolveDeferredPathResolve(options = {}) {
172
+ const value = options.deferredPathResolve ?? options.deferredResolve ?? options.pathResolve?.deferred ?? true;
173
+ return value !== false;
174
+ }
145
175
  function emissionPower(emission) {
146
176
  return Math.max(0, emission?.[0] ?? 0) + Math.max(0, emission?.[1] ?? 0) + Math.max(0, emission?.[2] ?? 0);
147
177
  }
@@ -789,7 +819,15 @@ function resolveEnvironmentLighting(input, environmentColor, ambientColor) {
789
819
  sunColor: Object.freeze(asColor(source.sunColor, DEFAULT_ENVIRONMENT_LIGHTING.sunColor)),
790
820
  intensity: Math.max(1e-4, readFiniteNumber("environmentLighting.intensity", source.intensity, DEFAULT_ENVIRONMENT_LIGHTING.intensity)),
791
821
  mode: readNonNegativeInteger("environmentLighting.mode", source.mode, DEFAULT_ENVIRONMENT_LIGHTING.mode),
792
- exposure: Math.max(1e-4, readFiniteNumber("environmentLighting.exposure", source.exposure, DEFAULT_ENVIRONMENT_LIGHTING.exposure))
822
+ exposure: Math.max(1e-4, readFiniteNumber("environmentLighting.exposure", source.exposure, DEFAULT_ENVIRONMENT_LIGHTING.exposure)),
823
+ sunlitBaseline: Math.max(
824
+ 0,
825
+ readFiniteNumber(
826
+ "environmentLighting.sunlitBaseline",
827
+ source.sunlitBaseline ?? source.daylightBaseline,
828
+ DEFAULT_ENVIRONMENT_LIGHTING.sunlitBaseline
829
+ )
830
+ )
793
831
  });
794
832
  }
795
833
  function evaluateReferenceEnvironmentRadiance(config, origin, direction) {
@@ -974,6 +1012,11 @@ function estimateWavefrontPathTracingMemory(options = {}) {
974
1012
  options.tilePixelCapacity,
975
1013
  DEFAULT_TILE_SIZE * DEFAULT_TILE_SIZE
976
1014
  );
1015
+ const maxDepth = clamp(
1016
+ readPositiveInteger("maxDepth", options.maxDepth, DEFAULT_MAX_DEPTH),
1017
+ 1,
1018
+ 16
1019
+ );
977
1020
  const sceneObjectCapacity = readPositiveInteger(
978
1021
  "sceneObjectCapacity",
979
1022
  options.sceneObjectCapacity,
@@ -999,6 +1042,7 @@ function estimateWavefrontPathTracingMemory(options = {}) {
999
1042
  const queueBytes = tilePixelCapacity * RAY_RECORD_BYTES;
1000
1043
  const hitBytes = tilePixelCapacity * HIT_RECORD_BYTES;
1001
1044
  const accumulationBytes = tilePixelCapacity * ACCUMULATION_RECORD_BYTES;
1045
+ const pathVertexBytes = tilePixelCapacity * (maxDepth + 1) * PATH_VERTEX_RECORD_BYTES;
1002
1046
  const sceneObjectBytes = sceneObjectCapacity * SCENE_OBJECT_RECORD_BYTES;
1003
1047
  const triangleBytes = triangleCapacity * TRIANGLE_RECORD_BYTES;
1004
1048
  const bvhNodeBytes = bvhNodeCapacity * BVH_NODE_RECORD_BYTES;
@@ -1010,6 +1054,7 @@ function estimateWavefrontPathTracingMemory(options = {}) {
1010
1054
  queuePairBytes: queueBytes * 2,
1011
1055
  hitBytes,
1012
1056
  accumulationBytes,
1057
+ pathVertexBytes,
1013
1058
  sceneObjectBytes,
1014
1059
  triangleBytes,
1015
1060
  bvhNodeBytes,
@@ -1019,7 +1064,7 @@ function estimateWavefrontPathTracingMemory(options = {}) {
1019
1064
  configBytes: CONFIG_BUFFER_BYTES,
1020
1065
  counterBytes: COUNTER_BUFFER_BYTES,
1021
1066
  indirectDispatchBytes: INDIRECT_DISPATCH_ARGS_BYTES,
1022
- totalHotBufferBytes: queueBytes * 2 + hitBytes + accumulationBytes + sceneObjectBytes + triangleBytes + bvhNodeBytes + bvhLeafReferenceBytes + emissiveTriangleMetadataBytes + environmentPortalBytes + CONFIG_BUFFER_BYTES + COUNTER_BUFFER_BYTES + INDIRECT_DISPATCH_ARGS_BYTES
1067
+ totalHotBufferBytes: queueBytes * 2 + hitBytes + accumulationBytes + pathVertexBytes + sceneObjectBytes + triangleBytes + bvhNodeBytes + bvhLeafReferenceBytes + emissiveTriangleMetadataBytes + environmentPortalBytes + CONFIG_BUFFER_BYTES + COUNTER_BUFFER_BYTES + INDIRECT_DISPATCH_ARGS_BYTES
1023
1068
  });
1024
1069
  }
1025
1070
  function createWavefrontPathTracingComputeConfig(options = {}) {
@@ -1104,6 +1149,10 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
1104
1149
  options.environmentPortalMode ?? options.portalMode ?? options.environmentLighting?.environmentPortalMode,
1105
1150
  environmentPortals.length > 0
1106
1151
  );
1152
+ const environmentMap = resolveEnvironmentMap(
1153
+ options.environmentMap ?? options.environmentTexture ?? options.environmentLighting?.environmentMap
1154
+ );
1155
+ const deferredPathResolve = resolveDeferredPathResolve(options);
1107
1156
  return Object.freeze({
1108
1157
  mode: rendererWavefrontComputeMode,
1109
1158
  width,
@@ -1138,12 +1187,15 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
1138
1187
  environmentPortalCount: environmentPortals.length,
1139
1188
  environmentPortalCapacity,
1140
1189
  environmentPortalMode,
1190
+ environmentMap,
1191
+ deferredPathResolve,
1141
1192
  displayQuality: options.displayQuality === true,
1142
1193
  requiresMeshBvhForDisplayQuality: true,
1143
1194
  denoise: options.denoise !== false,
1144
1195
  frameIndex: readNonNegativeInteger("frameIndex", options.frameIndex, 0),
1145
1196
  memory: estimateWavefrontPathTracingMemory({
1146
1197
  tilePixelCapacity,
1198
+ maxDepth,
1147
1199
  sceneObjectCapacity,
1148
1200
  triangleCapacity,
1149
1201
  bvhNodeCapacity,
@@ -1340,6 +1392,18 @@ function createConfigPayload(config, tile, frameIndex, buildRange = {}) {
1340
1392
  data.setUint32(260, config.environmentPortalMode ?? 0, true);
1341
1393
  data.setUint32(264, 0, true);
1342
1394
  data.setUint32(268, 0, true);
1395
+ writeVec4(floatView, 272, [
1396
+ config.environmentMap.enabled ? 1 : 0,
1397
+ config.environmentMap.intensity,
1398
+ config.environmentMap.rotationRadians,
1399
+ config.environmentMap.ambientStrength
1400
+ ]);
1401
+ writeVec4(floatView, 288, [
1402
+ config.deferredPathResolve ? 1 : 0,
1403
+ config.environmentLighting.sunlitBaseline,
1404
+ 0,
1405
+ 0
1406
+ ]);
1343
1407
  return bytes;
1344
1408
  }
1345
1409
  function createTiles(width, height, tileSize) {
@@ -1585,6 +1649,136 @@ function alignTo(value, alignment) {
1585
1649
  const resolvedAlignment = Math.max(1, alignment);
1586
1650
  return Math.ceil(value / resolvedAlignment) * resolvedAlignment;
1587
1651
  }
1652
+ function float32ToFloat16Bits(value) {
1653
+ const floatView = new Float32Array(1);
1654
+ const intView = new Uint32Array(floatView.buffer);
1655
+ floatView[0] = Number.isFinite(value) ? value : 0;
1656
+ const x = intView[0];
1657
+ const sign = x >> 16 & 32768;
1658
+ let mantissa = x & 8388607;
1659
+ let exponent = x >> 23 & 255;
1660
+ if (exponent === 255) {
1661
+ return sign | (mantissa ? 32256 : 31744);
1662
+ }
1663
+ exponent = exponent - 127 + 15;
1664
+ if (exponent >= 31) {
1665
+ return sign | 31744;
1666
+ }
1667
+ if (exponent <= 0) {
1668
+ if (exponent < -10) {
1669
+ return sign;
1670
+ }
1671
+ mantissa = (mantissa | 8388608) >> 1 - exponent;
1672
+ return sign | mantissa + 4096 >> 13;
1673
+ }
1674
+ return sign | exponent << 10 | mantissa + 4096 >> 13;
1675
+ }
1676
+ function environmentMapIntegerScale(data) {
1677
+ if (data instanceof Uint8Array) {
1678
+ return 1 / 255;
1679
+ }
1680
+ if (data instanceof Uint16Array) {
1681
+ return 1 / 65535;
1682
+ }
1683
+ return 1;
1684
+ }
1685
+ function readEnvironmentMapComponent(data, index, fallback, integerScale = 1) {
1686
+ if (!data || index >= data.length) {
1687
+ return fallback;
1688
+ }
1689
+ const value = Number(data[index]);
1690
+ return Number.isFinite(value) ? Math.max(0, value) * integerScale : fallback;
1691
+ }
1692
+ function createEnvironmentMapUploadBytes(environmentMap, fallbackColor) {
1693
+ const width = Math.max(1, environmentMap.width);
1694
+ const height = Math.max(1, environmentMap.height);
1695
+ const rowBytes = width * 8;
1696
+ const bytesPerRow = alignTo(rowBytes, 256);
1697
+ const bytes = new Uint8Array(bytesPerRow * height);
1698
+ const data = environmentMap.data;
1699
+ const integerScale = environmentMapIntegerScale(data);
1700
+ const view = new DataView(bytes.buffer);
1701
+ const writeComponent = (targetOffset, sourceOffset, fallback) => {
1702
+ view.setUint16(
1703
+ targetOffset,
1704
+ float32ToFloat16Bits(
1705
+ readEnvironmentMapComponent(data, sourceOffset, fallback, integerScale)
1706
+ ),
1707
+ true
1708
+ );
1709
+ };
1710
+ for (let y = 0; y < height; y += 1) {
1711
+ for (let x = 0; x < width; x += 1) {
1712
+ const sourceOffset = (y * width + x) * 4;
1713
+ const targetOffset = y * bytesPerRow + x * 8;
1714
+ writeComponent(targetOffset, sourceOffset, fallbackColor[0]);
1715
+ writeComponent(targetOffset + 2, sourceOffset + 1, fallbackColor[1]);
1716
+ writeComponent(targetOffset + 4, sourceOffset + 2, fallbackColor[2]);
1717
+ writeComponent(targetOffset + 6, sourceOffset + 3, fallbackColor[3] ?? 1);
1718
+ }
1719
+ }
1720
+ return Object.freeze({
1721
+ bytes,
1722
+ bytesPerRow,
1723
+ width,
1724
+ height
1725
+ });
1726
+ }
1727
+ function createEnvironmentMapResource(device, constants, environmentMap, fallbackColor) {
1728
+ if (environmentMap.view) {
1729
+ return Object.freeze({
1730
+ view: environmentMap.view,
1731
+ sampler: environmentMap.sampler ?? device.createSampler({
1732
+ label: "plasius.wavefront.environmentMapSampler",
1733
+ addressModeU: "repeat",
1734
+ addressModeV: "clamp-to-edge",
1735
+ magFilter: "linear",
1736
+ minFilter: "linear"
1737
+ }),
1738
+ texture: null,
1739
+ ownsTexture: false
1740
+ });
1741
+ }
1742
+ if (environmentMap.texture && typeof environmentMap.texture.createView === "function") {
1743
+ return Object.freeze({
1744
+ view: environmentMap.texture.createView(),
1745
+ sampler: environmentMap.sampler ?? device.createSampler({
1746
+ label: "plasius.wavefront.environmentMapSampler",
1747
+ addressModeU: "repeat",
1748
+ addressModeV: "clamp-to-edge",
1749
+ magFilter: "linear",
1750
+ minFilter: "linear"
1751
+ }),
1752
+ texture: environmentMap.texture,
1753
+ ownsTexture: false
1754
+ });
1755
+ }
1756
+ const upload = createEnvironmentMapUploadBytes(environmentMap, fallbackColor);
1757
+ const texture = device.createTexture({
1758
+ label: environmentMap.enabled ? "plasius.wavefront.environmentMap" : "plasius.wavefront.environmentMapFallback",
1759
+ size: { width: upload.width, height: upload.height },
1760
+ format: "rgba16float",
1761
+ usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
1762
+ });
1763
+ device.queue.writeTexture(
1764
+ { texture },
1765
+ upload.bytes,
1766
+ { bytesPerRow: upload.bytesPerRow, rowsPerImage: upload.height },
1767
+ { width: upload.width, height: upload.height, depthOrArrayLayers: 1 }
1768
+ );
1769
+ return Object.freeze({
1770
+ view: texture.createView(),
1771
+ sampler: environmentMap.sampler ?? device.createSampler({
1772
+ label: "plasius.wavefront.environmentMapSampler",
1773
+ addressModeU: "repeat",
1774
+ addressModeV: "clamp-to-edge",
1775
+ magFilter: "linear",
1776
+ minFilter: "linear"
1777
+ }),
1778
+ texture,
1779
+ ownsTexture: true
1780
+ });
1781
+ }
1588
1782
  async function getPipelineDiagnostics(shaderModule) {
1589
1783
  if (typeof shaderModule?.compilationInfo !== "function") {
1590
1784
  return "";
@@ -1793,6 +1987,8 @@ struct FrameConfig {
1793
1987
  environmentPortalMode: u32,
1794
1988
  _portalPad0: u32,
1795
1989
  _portalPad1: u32,
1990
+ environmentMapSettings: vec4<f32>,
1991
+ pathResolveSettings: vec4<f32>,
1796
1992
  };
1797
1993
 
1798
1994
  struct Counters {
@@ -1852,6 +2048,9 @@ struct EnvironmentPortal {
1852
2048
  @group(0) @binding(17) var finalDenoiseInputRadiance: texture_2d<f32>;
1853
2049
  @group(0) @binding(18) var denoisedOutputImage: texture_storage_2d<rgba8unorm, write>;
1854
2050
  @group(0) @binding(19) var<storage, read> environmentPortals: array<EnvironmentPortal>;
2051
+ @group(0) @binding(20) var environmentMapTexture: texture_2d<f32>;
2052
+ @group(0) @binding(21) var environmentMapSampler: sampler;
2053
+ @group(0) @binding(22) var<storage, read_write> pathVertices: array<vec4<f32>>;
1855
2054
 
1856
2055
  fn hash_u32(value: u32) -> u32 {
1857
2056
  var x = value;
@@ -1896,6 +2095,89 @@ fn max_component(value: vec3<f32>) -> f32 {
1896
2095
  return max(max(value.x, value.y), value.z);
1897
2096
  }
1898
2097
 
2098
+ fn environment_map_enabled() -> bool {
2099
+ return config.environmentMapSettings.x > 0.5;
2100
+ }
2101
+
2102
+ fn deferred_path_resolve_enabled() -> bool {
2103
+ return config.pathResolveSettings.x > 0.5;
2104
+ }
2105
+
2106
+ fn path_vertex_count_per_ray() -> u32 {
2107
+ return config.maxDepth + 1u;
2108
+ }
2109
+
2110
+ fn path_vertex_index(rayId: u32, depth: u32) -> u32 {
2111
+ return rayId * path_vertex_count_per_ray() + min(depth, config.maxDepth);
2112
+ }
2113
+
2114
+ fn clear_deferred_path(rayId: u32) {
2115
+ if (!deferred_path_resolve_enabled()) {
2116
+ return;
2117
+ }
2118
+
2119
+ for (var depth = 0u; depth <= config.maxDepth; depth = depth + 1u) {
2120
+ pathVertices[path_vertex_index(rayId, depth)] = vec4<f32>(0.0);
2121
+ if (depth == config.maxDepth) {
2122
+ break;
2123
+ }
2124
+ }
2125
+ }
2126
+
2127
+ fn record_deferred_path_response(ray: RayRecord, response: vec3<f32>) {
2128
+ if (!deferred_path_resolve_enabled() || ray.rayId >= config.tilePixelCount || ray.bounce >= config.maxDepth) {
2129
+ return;
2130
+ }
2131
+ pathVertices[path_vertex_index(ray.rayId, ray.bounce)] =
2132
+ vec4<f32>(max(response, vec3<f32>(0.0)), 1.0);
2133
+ }
2134
+
2135
+ fn record_deferred_terminal_source(ray: RayRecord, sourceRadiance: vec3<f32>) {
2136
+ if (!deferred_path_resolve_enabled() || ray.rayId >= config.tilePixelCount) {
2137
+ return;
2138
+ }
2139
+ pathVertices[path_vertex_index(ray.rayId, config.maxDepth)] =
2140
+ vec4<f32>(clamp_sample_radiance(sourceRadiance), 1.0);
2141
+ }
2142
+
2143
+ fn environment_map_uv(direction: vec3<f32>) -> vec2<f32> {
2144
+ let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
2145
+ let rotationTurns = config.environmentMapSettings.z / 6.28318530718;
2146
+ let u = fract(atan2(rayDirection.z, rayDirection.x) / 6.28318530718 + 0.5 + rotationTurns);
2147
+ let v = acos(clamp(rayDirection.y, -1.0, 1.0)) / 3.14159265359;
2148
+ return vec2<f32>(u, clamp(v, 0.0, 1.0));
2149
+ }
2150
+
2151
+ fn environment_map_radiance(direction: vec3<f32>) -> vec3<f32> {
2152
+ let uv = environment_map_uv(direction);
2153
+ let texel = max(textureSampleLevel(environmentMapTexture, environmentMapSampler, uv, 0.0).rgb, vec3<f32>(0.0));
2154
+ return texel * max(config.environmentMapSettings.y, 0.0);
2155
+ }
2156
+
2157
+ fn procedural_environment_radiance(direction: vec3<f32>) -> vec3<f32> {
2158
+ let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
2159
+ let upFactor = saturate(rayDirection.y * 0.5 + 0.5);
2160
+ let sunDirection = safe_normalize(
2161
+ config.environmentSunDirectionIntensity.xyz,
2162
+ vec3<f32>(0.0, 1.0, 0.0)
2163
+ );
2164
+ let sunGlow = pow(saturate(dot(rayDirection, sunDirection)), 192.0);
2165
+ let gradient =
2166
+ config.environmentHorizonColor.xyz * (1.0 - upFactor) +
2167
+ config.environmentZenithColor.xyz * upFactor;
2168
+ return (
2169
+ gradient +
2170
+ config.environmentSunColor.xyz * sunGlow
2171
+ ) * max(config.environmentSunDirectionIntensity.w, 0.0001);
2172
+ }
2173
+
2174
+ fn base_environment_radiance(direction: vec3<f32>) -> vec3<f32> {
2175
+ if (environment_map_enabled()) {
2176
+ return environment_map_radiance(direction);
2177
+ }
2178
+ return procedural_environment_radiance(direction);
2179
+ }
2180
+
1899
2181
  fn environment_portal_radiance_scale(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
1900
2182
  if (config.environmentPortalCount == 0u || config.environmentPortalMode == 0u) {
1901
2183
  return vec3<f32>(1.0);
@@ -1935,22 +2217,9 @@ fn environment_portal_radiance_scale(origin: vec3<f32>, direction: vec3<f32>) ->
1935
2217
 
1936
2218
  fn environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
1937
2219
  let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
1938
- let upFactor = saturate(rayDirection.y * 0.5 + 0.5);
1939
- let sunDirection = safe_normalize(
1940
- config.environmentSunDirectionIntensity.xyz,
1941
- vec3<f32>(0.0, 1.0, 0.0)
1942
- );
1943
- let sunGlow = pow(saturate(dot(rayDirection, sunDirection)), 192.0);
1944
- let gradient =
1945
- config.environmentHorizonColor.xyz * (1.0 - upFactor) +
1946
- config.environmentZenithColor.xyz * upFactor;
1947
2220
  let portalScale = environment_portal_radiance_scale(origin, rayDirection);
1948
2221
  let portalHit = max_component(portalScale) > 0.0001;
1949
- return (
1950
- gradient +
1951
- config.environmentSunColor.xyz * sunGlow
1952
- ) *
1953
- max(config.environmentSunDirectionIntensity.w, 0.0001) *
2222
+ return base_environment_radiance(rayDirection) *
1954
2223
  select(vec3<f32>(1.0), portalScale, portalHit);
1955
2224
  }
1956
2225
 
@@ -1966,16 +2235,59 @@ fn gated_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f
1966
2235
  return environment_radiance(origin, direction);
1967
2236
  }
1968
2237
 
1969
- fn terminal_surface_environment_contribution(ray: RayRecord, hit: HitRecord) -> vec3<f32> {
2238
+ fn surface_path_response(hit: HitRecord) -> vec3<f32> {
2239
+ let color = clamp(hit.color.xyz, vec3<f32>(0.0), vec3<f32>(1.0));
2240
+ let opacity = clamp(hit.material.z, 0.0, 1.0);
2241
+ let materialEnergy = select(0.68, 0.92, hit.materialKind == 1u || hit.materialKind == 2u);
2242
+ let transparentEnergy = select(materialEnergy, 0.9, hit.hitType == 3u);
2243
+ return mix(vec3<f32>(1.0), color, max(opacity, 0.18)) * transparentEnergy;
2244
+ }
2245
+
2246
+ fn sunlit_baseline_radiance(normal: vec3<f32>) -> vec3<f32> {
2247
+ let baseline = max(config.pathResolveSettings.y, 0.0);
2248
+ if (baseline <= 0.000001) {
2249
+ return vec3<f32>(0.0);
2250
+ }
2251
+ let sunDirection = safe_normalize(
2252
+ config.environmentSunDirectionIntensity.xyz,
2253
+ vec3<f32>(0.0, 1.0, 0.0)
2254
+ );
2255
+ let sunFacing = saturate(dot(normal, sunDirection));
2256
+ let skyFacing = 0.35 + saturate(normal.y * 0.5 + 0.5) * 0.65;
2257
+ let directionalWeight = 0.38 + sunFacing * 0.62;
2258
+ let sunTint = max(config.environmentSunColor.xyz, vec3<f32>(0.0));
2259
+ return clamp_sample_radiance(sunTint * baseline * skyFacing * directionalWeight * 0.04);
2260
+ }
2261
+
2262
+ fn terminal_surface_environment_source(hit: HitRecord) -> vec3<f32> {
1970
2263
  let normal = safe_normalize(hit.shadingNormal.xyz, vec3<f32>(0.0, 1.0, 0.0));
1971
- let surfaceColor = max(hit.color.xyz, config.ambientColor.xyz);
1972
2264
  let normalEnvironment = gated_environment_radiance(
1973
2265
  hit.position.xyz + normal * 0.003,
1974
2266
  normal
1975
2267
  );
1976
- let environmentFloor = max(config.ambientColor.xyz, normalEnvironment * 0.12);
2268
+ let sunlitFloor = sunlit_baseline_radiance(normal);
2269
+ let ambientFloor = select(
2270
+ max(config.ambientColor.xyz, sunlitFloor * 0.82),
2271
+ max(config.ambientColor.xyz * 0.35, sunlitFloor * 0.58),
2272
+ environment_map_enabled()
2273
+ );
2274
+ let environmentInfluence = select(
2275
+ max(0.12, config.pathResolveSettings.y * 0.42),
2276
+ max(config.environmentMapSettings.w, max(0.12, config.pathResolveSettings.y * 0.42)),
2277
+ environment_map_enabled()
2278
+ );
2279
+ let environmentFloor = max(ambientFloor, max(sunlitFloor, normalEnvironment * environmentInfluence));
1977
2280
  let materialFloor = select(0.7, 1.0, hit.materialKind == 0u || hit.materialKind == 3u);
1978
- return clamp_sample_radiance(ray.throughput.xyz * surfaceColor * environmentFloor * materialFloor);
2281
+ return clamp_sample_radiance(environmentFloor * materialFloor);
2282
+ }
2283
+
2284
+ fn terminal_surface_environment_contribution(ray: RayRecord, hit: HitRecord) -> vec3<f32> {
2285
+ let surfaceColor = max(hit.color.xyz, config.ambientColor.xyz);
2286
+ return clamp_sample_radiance(
2287
+ ray.throughput.xyz *
2288
+ surfaceColor *
2289
+ terminal_surface_environment_source(hit)
2290
+ );
1979
2291
  }
1980
2292
 
1981
2293
  fn direct_environment_portal_irradiance(origin: vec3<f32>, normal: vec3<f32>) -> vec3<f32> {
@@ -2028,7 +2340,17 @@ fn surface_direct_environment_contribution(ray: RayRecord, hit: HitRecord) -> ve
2028
2340
 
2029
2341
  let normalEnvironment = gated_environment_radiance(origin, normal);
2030
2342
  let skyVisibility = 0.35 + saturate(normal.y * 0.5 + 0.5) * 0.45;
2031
- let skyIrradiance = max(config.ambientColor.xyz * 0.72, normalEnvironment * skyVisibility * 0.16);
2343
+ let sunlitFloor = sunlit_baseline_radiance(normal);
2344
+ let ambientIrradiance = max(
2345
+ select(config.ambientColor.xyz * 0.72, config.ambientColor.xyz * 0.28, environment_map_enabled()),
2346
+ sunlitFloor * select(0.72, 0.45, environment_map_enabled())
2347
+ );
2348
+ let environmentIrradianceScale = select(
2349
+ max(0.16, config.pathResolveSettings.y * 0.45),
2350
+ max(config.environmentMapSettings.w, max(0.16, config.pathResolveSettings.y * 0.45)),
2351
+ environment_map_enabled()
2352
+ );
2353
+ let skyIrradiance = max(ambientIrradiance, normalEnvironment * skyVisibility * environmentIrradianceScale);
2032
2354
 
2033
2355
  let sunDirection = safe_normalize(
2034
2356
  config.environmentSunDirectionIntensity.xyz,
@@ -2652,6 +2974,7 @@ fn generatePrimaryRays(@builtin(global_invocation_id) globalId: vec3<u32>) {
2652
2974
  return;
2653
2975
  }
2654
2976
  activeQueue[index] = make_ray(index);
2977
+ clear_deferred_path(index);
2655
2978
  if (u32(config.projectionAndSampling.w) == 0u) {
2656
2979
  accumulation[index] = vec4<f32>(0.0);
2657
2980
  }
@@ -2904,25 +3227,37 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
2904
3227
 
2905
3228
  if (hit.hitType == 1u) {
2906
3229
  let guidedLightWeight = select(1.0, 0.24, (ray.flags & RAY_FLAG_GUIDED_EMISSIVE) != 0u);
2907
- contribution = clamp_sample_radiance(
2908
- ray.throughput.xyz * max(hit.emission.xyz, hit.color.xyz) * guidedLightWeight
2909
- );
2910
- accumulation[ray.rayId] =
2911
- accumulation[ray.rayId] + vec4<f32>(contribution * sample_weight(), 1.0);
3230
+ let sourceRadiance = max(hit.emission.xyz, hit.color.xyz) * guidedLightWeight;
3231
+ if (deferred_path_resolve_enabled()) {
3232
+ record_deferred_terminal_source(ray, sourceRadiance);
3233
+ } else {
3234
+ contribution = clamp_sample_radiance(ray.throughput.xyz * sourceRadiance);
3235
+ accumulation[ray.rayId] =
3236
+ accumulation[ray.rayId] + vec4<f32>(contribution * sample_weight(), 1.0);
3237
+ }
2912
3238
  atomicAdd(&counters.terminatedCount, 1u);
2913
3239
  return;
2914
3240
  }
2915
3241
 
2916
3242
  if (hit.hitType == 2u) {
2917
- contribution = clamp_sample_radiance(ray.throughput.xyz * max(hit.color.xyz, config.ambientColor.xyz));
2918
- accumulation[ray.rayId] =
2919
- accumulation[ray.rayId] + vec4<f32>(contribution * sample_weight(), 1.0);
3243
+ if (deferred_path_resolve_enabled()) {
3244
+ record_deferred_terminal_source(ray, hit.color.xyz);
3245
+ } else {
3246
+ contribution = clamp_sample_radiance(ray.throughput.xyz * max(hit.color.xyz, config.ambientColor.xyz));
3247
+ accumulation[ray.rayId] =
3248
+ accumulation[ray.rayId] + vec4<f32>(contribution * sample_weight(), 1.0);
3249
+ }
2920
3250
  atomicAdd(&counters.terminatedCount, 1u);
2921
3251
  return;
2922
3252
  }
2923
3253
 
3254
+ let response = surface_path_response(hit);
3255
+ record_deferred_path_response(ray, response);
3256
+
2924
3257
  let shouldEstimateDirectEnvironment =
2925
- (hit.materialKind == 0u || hit.materialKind == 1u) && hit.material.z >= 0.95;
3258
+ !deferred_path_resolve_enabled() &&
3259
+ (hit.materialKind == 0u || hit.materialKind == 1u) &&
3260
+ hit.material.z >= 0.95;
2926
3261
  if (shouldEstimateDirectEnvironment) {
2927
3262
  let directEnvironment = surface_direct_environment_contribution(ray, hit);
2928
3263
  accumulation[ray.rayId] =
@@ -2930,9 +3265,13 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
2930
3265
  }
2931
3266
 
2932
3267
  if (ray.bounce + 1u >= config.maxDepth) {
2933
- let terminalEnvironment = terminal_surface_environment_contribution(ray, hit);
2934
- accumulation[ray.rayId] =
2935
- accumulation[ray.rayId] + vec4<f32>(terminalEnvironment * sample_weight(), 1.0);
3268
+ if (deferred_path_resolve_enabled()) {
3269
+ record_deferred_terminal_source(ray, terminal_surface_environment_source(hit));
3270
+ } else {
3271
+ let terminalEnvironment = terminal_surface_environment_contribution(ray, hit);
3272
+ accumulation[ray.rayId] =
3273
+ accumulation[ray.rayId] + vec4<f32>(terminalEnvironment * sample_weight(), 1.0);
3274
+ }
2936
3275
  atomicAdd(&counters.terminatedCount, 1u);
2937
3276
  return;
2938
3277
  }
@@ -2941,17 +3280,17 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
2941
3280
  let scatter = scatter_direction(ray, hit, seed);
2942
3281
  let nextIndex = atomicAdd(&counters.nextCount, 1u);
2943
3282
  if (nextIndex >= config.tilePixelCount) {
2944
- let overflowEnvironment = terminal_surface_environment_contribution(ray, hit);
2945
- accumulation[ray.rayId] =
2946
- accumulation[ray.rayId] + vec4<f32>(overflowEnvironment * sample_weight(), 1.0);
3283
+ if (deferred_path_resolve_enabled()) {
3284
+ record_deferred_terminal_source(ray, terminal_surface_environment_source(hit));
3285
+ } else {
3286
+ let overflowEnvironment = terminal_surface_environment_contribution(ray, hit);
3287
+ accumulation[ray.rayId] =
3288
+ accumulation[ray.rayId] + vec4<f32>(overflowEnvironment * sample_weight(), 1.0);
3289
+ }
2947
3290
  atomicAdd(&counters.terminatedCount, 1u);
2948
3291
  return;
2949
3292
  }
2950
- let color = clamp(hit.color.xyz, vec3<f32>(0.0), vec3<f32>(1.0));
2951
- let opacity = clamp(hit.material.z, 0.0, 1.0);
2952
- let materialEnergy = select(0.68, 0.92, hit.materialKind == 1u || hit.materialKind == 2u);
2953
- let transparentEnergy = select(materialEnergy, 0.9, hit.hitType == 3u);
2954
- let throughput = ray.throughput.xyz * mix(vec3<f32>(1.0), color, max(opacity, 0.18)) * transparentEnergy;
3293
+ let throughput = ray.throughput.xyz * response;
2955
3294
  nextQueue[nextIndex] = RayRecord(
2956
3295
  ray.rayId,
2957
3296
  ray.rayId,
@@ -2979,6 +3318,27 @@ fn compactAndSwapQueues(@builtin(global_invocation_id) globalId: vec3<u32>) {
2979
3318
  write_active_dispatch_args(activeCount);
2980
3319
  }
2981
3320
 
3321
+ fn resolve_deferred_path_radiance(rayId: u32) -> vec3<f32> {
3322
+ let terminal = pathVertices[path_vertex_index(rayId, config.maxDepth)];
3323
+ if (terminal.w <= 0.0) {
3324
+ return vec3<f32>(0.0);
3325
+ }
3326
+
3327
+ var radiance = terminal.xyz;
3328
+ var depth = config.maxDepth;
3329
+ loop {
3330
+ if (depth == 0u) {
3331
+ break;
3332
+ }
3333
+ depth = depth - 1u;
3334
+ let response = pathVertices[path_vertex_index(rayId, depth)];
3335
+ if (response.w > 0.0) {
3336
+ radiance = radiance * response.xyz;
3337
+ }
3338
+ }
3339
+ return clamp_sample_radiance(radiance);
3340
+ }
3341
+
2982
3342
  @compute @workgroup_size(64)
2983
3343
  fn accumulateTerminalRadiance(@builtin(global_invocation_id) globalId: vec3<u32>) {
2984
3344
  let index = globalId.x;
@@ -2988,7 +3348,12 @@ fn accumulateTerminalRadiance(@builtin(global_invocation_id) globalId: vec3<u32>
2988
3348
  let localX = index % config.tileWidth;
2989
3349
  let localY = index / config.tileWidth;
2990
3350
  let pixel = vec2<i32>(i32(config.tileX + localX), i32(config.tileY + localY));
2991
- let radiance = max(accumulation[index].xyz, vec3<f32>(0.0));
3351
+ var radiance = max(accumulation[index].xyz, vec3<f32>(0.0));
3352
+ if (deferred_path_resolve_enabled()) {
3353
+ let resolved = resolve_deferred_path_radiance(index) * sample_weight();
3354
+ radiance = clamp_sample_radiance(radiance + resolved);
3355
+ accumulation[index] = vec4<f32>(radiance, 1.0);
3356
+ }
2992
3357
 
2993
3358
  textureStore(radianceImage, pixel, vec4<f32>(radiance, 1.0));
2994
3359
  if (config.denoise == 0u) {
@@ -3126,6 +3491,125 @@ function createWavefrontDeviceDescriptor(adapter, options = {}) {
3126
3491
  }
3127
3492
  return Object.keys(descriptor).length > 0 ? descriptor : void 0;
3128
3493
  }
3494
+ function readGpuLimit(adapter, device, name) {
3495
+ const adapterValue = Number(adapter?.limits?.[name]);
3496
+ if (Number.isFinite(adapterValue)) {
3497
+ return adapterValue;
3498
+ }
3499
+ const deviceValue = Number(device?.limits?.[name]);
3500
+ return Number.isFinite(deviceValue) ? deviceValue : null;
3501
+ }
3502
+ function createAdapterInfoSnapshot(adapter) {
3503
+ const info = adapter?.info;
3504
+ if (!info || typeof info !== "object") {
3505
+ return null;
3506
+ }
3507
+ return Object.freeze({
3508
+ vendor: typeof info.vendor === "string" ? info.vendor : "",
3509
+ architecture: typeof info.architecture === "string" ? info.architecture : "",
3510
+ device: typeof info.device === "string" ? info.device : "",
3511
+ description: typeof info.description === "string" ? info.description : ""
3512
+ });
3513
+ }
3514
+ function createGpuAdapterParallelismDiagnostics(adapter, device) {
3515
+ return Object.freeze({
3516
+ physicalCoreCount: null,
3517
+ physicalCoreCountAvailable: false,
3518
+ physicalCoreCountUnavailableReason: "WebGPU does not expose physical GPU core counts.",
3519
+ adapterInfo: createAdapterInfoSnapshot(adapter),
3520
+ adapterLimits: Object.freeze({
3521
+ maxComputeInvocationsPerWorkgroup: readGpuLimit(adapter, device, "maxComputeInvocationsPerWorkgroup"),
3522
+ maxComputeWorkgroupSizeX: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeX"),
3523
+ maxComputeWorkgroupSizeY: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeY"),
3524
+ maxComputeWorkgroupSizeZ: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeZ"),
3525
+ maxComputeWorkgroupsPerDimension: readGpuLimit(adapter, device, "maxComputeWorkgroupsPerDimension"),
3526
+ maxStorageBuffersPerShaderStage: readGpuLimit(adapter, device, "maxStorageBuffersPerShaderStage"),
3527
+ maxStorageBufferBindingSize: readGpuLimit(adapter, device, "maxStorageBufferBindingSize")
3528
+ }),
3529
+ configuredWorkgroupSize: WORKGROUP_SIZE
3530
+ });
3531
+ }
3532
+ function createGpuParallelismCounters() {
3533
+ return {
3534
+ directDispatches: 0,
3535
+ directWorkgroups: 0,
3536
+ directShaderInvocations: 0,
3537
+ multiWorkgroupDispatches: 0,
3538
+ largestDirectWorkgroupsPerDispatch: 0,
3539
+ indirectDispatches: 0,
3540
+ estimatedIndirectWorkgroupsUpperBound: 0,
3541
+ estimatedIndirectShaderInvocationsUpperBound: 0,
3542
+ indirectDispatchesWithMultiWorkgroupCapacity: 0,
3543
+ largestEstimatedIndirectWorkgroupsPerDispatch: 0
3544
+ };
3545
+ }
3546
+ function countDispatchWorkgroups(groups) {
3547
+ return groups.reduce((product, value) => {
3548
+ const numeric = Number(value ?? 1);
3549
+ const count = Number.isFinite(numeric) ? Math.max(1, Math.trunc(numeric)) : 1;
3550
+ return product * count;
3551
+ }, 1);
3552
+ }
3553
+ function recordDirectDispatch(parallelism, groups, invocationsPerWorkgroup = WORKGROUP_SIZE) {
3554
+ const workgroups = countDispatchWorkgroups(groups);
3555
+ parallelism.directDispatches += 1;
3556
+ parallelism.directWorkgroups += workgroups;
3557
+ parallelism.directShaderInvocations += workgroups * invocationsPerWorkgroup;
3558
+ parallelism.largestDirectWorkgroupsPerDispatch = Math.max(
3559
+ parallelism.largestDirectWorkgroupsPerDispatch,
3560
+ workgroups
3561
+ );
3562
+ if (workgroups > 1) {
3563
+ parallelism.multiWorkgroupDispatches += 1;
3564
+ }
3565
+ }
3566
+ function recordIndirectDispatch(parallelism, estimatedWorkgroupsUpperBound, invocationsPerWorkgroup = WORKGROUP_SIZE) {
3567
+ const workgroups = Math.max(1, Math.trunc(Number(estimatedWorkgroupsUpperBound) || 1));
3568
+ parallelism.indirectDispatches += 1;
3569
+ parallelism.estimatedIndirectWorkgroupsUpperBound += workgroups;
3570
+ parallelism.estimatedIndirectShaderInvocationsUpperBound += workgroups * invocationsPerWorkgroup;
3571
+ parallelism.largestEstimatedIndirectWorkgroupsPerDispatch = Math.max(
3572
+ parallelism.largestEstimatedIndirectWorkgroupsPerDispatch,
3573
+ workgroups
3574
+ );
3575
+ if (workgroups > 1) {
3576
+ parallelism.indirectDispatchesWithMultiWorkgroupCapacity += 1;
3577
+ }
3578
+ }
3579
+ function createGpuParallelismDiagnostics(adapterDiagnostics, counters) {
3580
+ const totalEstimatedWorkgroupsUpperBound = counters.directWorkgroups + counters.estimatedIndirectWorkgroupsUpperBound;
3581
+ const totalEstimatedShaderInvocationsUpperBound = counters.directShaderInvocations + counters.estimatedIndirectShaderInvocationsUpperBound;
3582
+ const exposesMultiWorkgroupParallelism = counters.multiWorkgroupDispatches > 0 || counters.indirectDispatchesWithMultiWorkgroupCapacity > 0;
3583
+ return Object.freeze({
3584
+ ...adapterDiagnostics,
3585
+ directDispatches: counters.directDispatches,
3586
+ directWorkgroups: counters.directWorkgroups,
3587
+ directShaderInvocations: counters.directShaderInvocations,
3588
+ multiWorkgroupDispatches: counters.multiWorkgroupDispatches,
3589
+ largestDirectWorkgroupsPerDispatch: counters.largestDirectWorkgroupsPerDispatch,
3590
+ indirectDispatches: counters.indirectDispatches,
3591
+ estimatedIndirectWorkgroupsUpperBound: counters.estimatedIndirectWorkgroupsUpperBound,
3592
+ estimatedIndirectShaderInvocationsUpperBound: counters.estimatedIndirectShaderInvocationsUpperBound,
3593
+ indirectDispatchesWithMultiWorkgroupCapacity: counters.indirectDispatchesWithMultiWorkgroupCapacity,
3594
+ largestEstimatedIndirectWorkgroupsPerDispatch: counters.largestEstimatedIndirectWorkgroupsPerDispatch,
3595
+ totalEstimatedWorkgroupsUpperBound,
3596
+ totalEstimatedShaderInvocationsUpperBound,
3597
+ exposesMultiWorkgroupParallelism,
3598
+ likelyUsesMoreThanOnePhysicalGpuCore: null,
3599
+ coreUtilizationStatus: "not-exposed-by-webgpu"
3600
+ });
3601
+ }
3602
+ function createEnvironmentMapSnapshot(environmentMap) {
3603
+ return Object.freeze({
3604
+ enabled: environmentMap.enabled,
3605
+ width: environmentMap.width,
3606
+ height: environmentMap.height,
3607
+ projection: environmentMap.projection,
3608
+ intensity: environmentMap.intensity,
3609
+ rotationRadians: environmentMap.rotationRadians,
3610
+ ambientStrength: environmentMap.ambientStrength
3611
+ });
3612
+ }
3129
3613
  async function createWavefrontPathTracingComputeRenderer(options = {}) {
3130
3614
  assertAnalyticDisplayQualityPolicy(options);
3131
3615
  const constants = getGpuUsageConstants();
@@ -3145,6 +3629,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3145
3629
  throw new Error("Unable to acquire a WebGPU adapter for wavefront path tracing.");
3146
3630
  }
3147
3631
  const device = await adapter.requestDevice(createWavefrontDeviceDescriptor(adapter, options));
3632
+ const gpuAdapterParallelism = createGpuAdapterParallelismDiagnostics(adapter, device);
3148
3633
  const context = canvas.getContext("webgpu");
3149
3634
  if (!context || typeof context.configure !== "function") {
3150
3635
  throw new Error("Canvas WebGPU context does not support configure().");
@@ -3172,6 +3657,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3172
3657
  const rayQueueBytes = config.tilePixelCapacity * RAY_RECORD_BYTES;
3173
3658
  const hitBytes = config.tilePixelCapacity * HIT_RECORD_BYTES;
3174
3659
  const accumulationBytes = config.tilePixelCapacity * ACCUMULATION_RECORD_BYTES;
3660
+ const pathVertexBytes = config.tilePixelCapacity * (config.maxDepth + 1) * PATH_VERTEX_RECORD_BYTES;
3175
3661
  const activeQueue = createBuffer(device, bufferUsage, rayQueueBytes, "plasius.wavefront.activeQueue");
3176
3662
  const nextQueue = createBuffer(device, bufferUsage, rayQueueBytes, "plasius.wavefront.nextQueue");
3177
3663
  const hitBuffer = createBuffer(device, bufferUsage, hitBytes, "plasius.wavefront.hitBuffer");
@@ -3181,6 +3667,12 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3181
3667
  accumulationBytes,
3182
3668
  "plasius.wavefront.accumulation"
3183
3669
  );
3670
+ const pathVertexBuffer = createBuffer(
3671
+ device,
3672
+ bufferUsage,
3673
+ pathVertexBytes,
3674
+ "plasius.wavefront.pathVertices"
3675
+ );
3184
3676
  const sceneObjectBuffer = createBuffer(
3185
3677
  device,
3186
3678
  constants.buffer.STORAGE | constants.buffer.COPY_DST,
@@ -3235,9 +3727,10 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3235
3727
  CONFIG_BUFFER_BYTES,
3236
3728
  Number.isFinite(uniformOffsetAlignment) && uniformOffsetAlignment > 0 ? uniformOffsetAlignment : CONFIG_BUFFER_BYTES
3237
3729
  );
3730
+ const outputConfigSlotCount = config.deferredPathResolve ? 0 : tiles.length;
3238
3731
  const frameConfigSlotCount = Math.max(
3239
3732
  1,
3240
- tiles.length * config.samplesPerPixel + tiles.length + (config.denoise ? 1 : 0)
3733
+ tiles.length * config.samplesPerPixel + outputConfigSlotCount + (config.denoise ? 1 : 0)
3241
3734
  );
3242
3735
  const configBuffer = createBuffer(
3243
3736
  device,
@@ -3315,6 +3808,12 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3315
3808
  magFilter: "nearest",
3316
3809
  minFilter: "nearest"
3317
3810
  });
3811
+ const environmentMapResource = createEnvironmentMapResource(
3812
+ device,
3813
+ constants,
3814
+ config.environmentMap,
3815
+ config.environmentColor
3816
+ );
3318
3817
  const traceBindGroupLayout = device.createBindGroupLayout({
3319
3818
  label: "plasius.wavefront.traceBindGroupLayout",
3320
3819
  entries: [
@@ -3341,7 +3840,10 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3341
3840
  visibility: constants.shader.COMPUTE,
3342
3841
  storageTexture: { access: "write-only", format: "rgba16float" }
3343
3842
  },
3344
- { binding: 19, visibility: constants.shader.COMPUTE, buffer: { type: "read-only-storage" } }
3843
+ { binding: 19, visibility: constants.shader.COMPUTE, buffer: { type: "read-only-storage" } },
3844
+ { binding: 20, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
3845
+ { binding: 21, visibility: constants.shader.COMPUTE, sampler: { type: "filtering" } },
3846
+ { binding: 22, visibility: constants.shader.COMPUTE, buffer: { type: "storage" } }
3345
3847
  ]
3346
3848
  });
3347
3849
  const accelerationBindGroupLayout = device.createBindGroupLayout({
@@ -3515,7 +4017,10 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3515
4017
  { binding: 8, resource: { buffer: triangleBuffer } },
3516
4018
  { binding: 9, resource: { buffer: bvhNodeBuffer } },
3517
4019
  { binding: 16, resource: radianceView },
3518
- { binding: 19, resource: { buffer: environmentPortalBuffer } }
4020
+ { binding: 19, resource: { buffer: environmentPortalBuffer } },
4021
+ { binding: 20, resource: environmentMapResource.view },
4022
+ { binding: 21, resource: environmentMapResource.sampler },
4023
+ { binding: 22, resource: { buffer: pathVertexBuffer } }
3519
4024
  ]
3520
4025
  });
3521
4026
  }
@@ -3605,6 +4110,10 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3605
4110
  let accelerationBuilt = !config.gpuAccelerationBuildRequired;
3606
4111
  let accelerationBuildCount = 0;
3607
4112
  let activeCameraOptions = options.camera ?? DEFAULT_CAMERA;
4113
+ let lastGpuParallelism = createGpuParallelismDiagnostics(
4114
+ gpuAdapterParallelism,
4115
+ createGpuParallelismCounters()
4116
+ );
3608
4117
  function createFrameConfigWriter(frameIndex) {
3609
4118
  let slot = 0;
3610
4119
  return (tile, buildRange = {}) => {
@@ -3621,7 +4130,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3621
4130
  return offset;
3622
4131
  };
3623
4132
  }
3624
- function dispatchGpuAccelerationBuild(frameIndex) {
4133
+ function dispatchGpuAccelerationBuild(frameIndex, parallelism) {
3625
4134
  if (!config.gpuAccelerationBuildRequired || accelerationBuilt) {
3626
4135
  return false;
3627
4136
  }
@@ -3660,24 +4169,32 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3660
4169
  });
3661
4170
  passEncoder.setBindGroup(0, bvhBuildBindGroup, [0]);
3662
4171
  passEncoder.setPipeline(pipelines.prepareMeshTrianglesAndLeaves);
3663
- passEncoder.dispatchWorkgroups(Math.ceil(config.bvhLeafSortCapacity / WORKGROUP_SIZE));
4172
+ const prepareWorkgroups = Math.ceil(config.bvhLeafSortCapacity / WORKGROUP_SIZE);
4173
+ passEncoder.dispatchWorkgroups(prepareWorkgroups);
4174
+ recordDirectDispatch(parallelism, [prepareWorkgroups]);
3664
4175
  passEncoder.setPipeline(pipelines.sortBvhLeafRefs);
3665
4176
  for (let stageIndex = 0; stageIndex < config.bvhSortStages.length; stageIndex += 1) {
3666
4177
  passEncoder.setBindGroup(0, bvhBuildBindGroup, [
3667
4178
  (stageIndex + 1) * configBufferStride
3668
4179
  ]);
3669
- passEncoder.dispatchWorkgroups(Math.ceil(config.bvhLeafSortCapacity / WORKGROUP_SIZE));
4180
+ const sortWorkgroups = Math.ceil(config.bvhLeafSortCapacity / WORKGROUP_SIZE);
4181
+ passEncoder.dispatchWorkgroups(sortWorkgroups);
4182
+ recordDirectDispatch(parallelism, [sortWorkgroups]);
3670
4183
  }
3671
4184
  passEncoder.setBindGroup(0, bvhBuildBindGroup, [0]);
3672
4185
  passEncoder.setPipeline(pipelines.writeSortedBvhLeaves);
3673
- passEncoder.dispatchWorkgroups(Math.ceil(config.triangleCount / WORKGROUP_SIZE));
4186
+ const leafWriteWorkgroups = Math.ceil(config.triangleCount / WORKGROUP_SIZE);
4187
+ passEncoder.dispatchWorkgroups(leafWriteWorkgroups);
4188
+ recordDirectDispatch(parallelism, [leafWriteWorkgroups]);
3674
4189
  passEncoder.setPipeline(pipelines.buildBvhInternalLevel);
3675
4190
  for (let levelIndex = 0; levelIndex < config.bvhBuildLevels.length; levelIndex += 1) {
3676
4191
  const buildLevel = config.bvhBuildLevels[levelIndex];
3677
4192
  passEncoder.setBindGroup(0, bvhBuildBindGroup, [
3678
4193
  (buildLevelConfigStart + levelIndex) * configBufferStride
3679
4194
  ]);
3680
- passEncoder.dispatchWorkgroups(Math.ceil(buildLevel.count / WORKGROUP_SIZE));
4195
+ const levelWorkgroups = Math.ceil(buildLevel.count / WORKGROUP_SIZE);
4196
+ passEncoder.dispatchWorkgroups(levelWorkgroups);
4197
+ recordDirectDispatch(parallelism, [levelWorkgroups]);
3681
4198
  }
3682
4199
  passEncoder.end();
3683
4200
  device.queue.submit([encoder.finish()]);
@@ -3685,7 +4202,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3685
4202
  accelerationBuildCount += 1;
3686
4203
  return true;
3687
4204
  }
3688
- function encodeTileSample(encoder, tile, configOffset) {
4205
+ function encodeTileSample(encoder, tile, configOffset, parallelism) {
3689
4206
  const generatePass = encoder.beginComputePass({
3690
4207
  label: "plasius.wavefront.generatePrimaryRaysPass"
3691
4208
  });
@@ -3693,6 +4210,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3693
4210
  generatePass.setBindGroup(0, bindGroups[0], [configOffset]);
3694
4211
  generatePass.setPipeline(pipelines.generatePrimaryRays);
3695
4212
  generatePass.dispatchWorkgroups(tileWorkgroups);
4213
+ recordDirectDispatch(parallelism, [tileWorkgroups]);
3696
4214
  generatePass.end();
3697
4215
  for (let bounceIndex = 0; bounceIndex < config.maxDepth; bounceIndex += 1) {
3698
4216
  encoder.copyBufferToBuffer(
@@ -3708,14 +4226,17 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3708
4226
  passEncoder.setBindGroup(0, bindGroups[bounceIndex % 2], [configOffset]);
3709
4227
  passEncoder.setPipeline(pipelines.intersectActiveQueue);
3710
4228
  passEncoder.dispatchWorkgroupsIndirect(activeDispatchBuffer, 0);
4229
+ recordIndirectDispatch(parallelism, tileWorkgroups);
3711
4230
  passEncoder.setPipeline(pipelines.resolveSurfaceRecords);
3712
4231
  passEncoder.dispatchWorkgroupsIndirect(activeDispatchBuffer, 0);
4232
+ recordIndirectDispatch(parallelism, tileWorkgroups);
3713
4233
  passEncoder.setPipeline(pipelines.compactAndSwapQueues);
3714
4234
  passEncoder.dispatchWorkgroups(1);
4235
+ recordDirectDispatch(parallelism, [1], 1);
3715
4236
  passEncoder.end();
3716
4237
  }
3717
4238
  }
3718
- function encodeTileOutput(encoder, tile, configOffset) {
4239
+ function encodeTileOutput(encoder, tile, configOffset, parallelism) {
3719
4240
  const passEncoder = encoder.beginComputePass({
3720
4241
  label: "plasius.wavefront.outputPass"
3721
4242
  });
@@ -3723,25 +4244,30 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3723
4244
  passEncoder.setBindGroup(0, bindGroups[0], [configOffset]);
3724
4245
  passEncoder.setPipeline(pipelines.accumulateTerminalRadiance);
3725
4246
  passEncoder.dispatchWorkgroups(tileWorkgroups);
4247
+ recordDirectDispatch(parallelism, [tileWorkgroups]);
3726
4248
  passEncoder.end();
3727
4249
  }
3728
- function encodeDenoise(encoder, configOffset) {
4250
+ function encodeDenoise(encoder, configOffset, parallelism) {
3729
4251
  if (!config.denoise) {
3730
4252
  return;
3731
4253
  }
4254
+ const denoiseWorkgroupsX = Math.ceil(config.width / 8);
4255
+ const denoiseWorkgroupsY = Math.ceil(config.height / 8);
3732
4256
  const radiancePass = encoder.beginComputePass({
3733
4257
  label: "plasius.wavefront.denoiseRadiancePass"
3734
4258
  });
3735
4259
  radiancePass.setBindGroup(0, denoiseRadianceBindGroup, [configOffset]);
3736
4260
  radiancePass.setPipeline(pipelines.denoiseLinearRadiance);
3737
- radiancePass.dispatchWorkgroups(Math.ceil(config.width / 8), Math.ceil(config.height / 8));
4261
+ radiancePass.dispatchWorkgroups(denoiseWorkgroupsX, denoiseWorkgroupsY);
4262
+ recordDirectDispatch(parallelism, [denoiseWorkgroupsX, denoiseWorkgroupsY]);
3738
4263
  radiancePass.end();
3739
4264
  const resolvePass = encoder.beginComputePass({
3740
4265
  label: "plasius.wavefront.denoiseResolvePass"
3741
4266
  });
3742
4267
  resolvePass.setBindGroup(0, denoiseResolveBindGroup, [configOffset]);
3743
4268
  resolvePass.setPipeline(pipelines.resolveDenoisedOutputImage);
3744
- resolvePass.dispatchWorkgroups(Math.ceil(config.width / 8), Math.ceil(config.height / 8));
4269
+ resolvePass.dispatchWorkgroups(denoiseWorkgroupsX, denoiseWorkgroupsY);
4270
+ recordDirectDispatch(parallelism, [denoiseWorkgroupsX, denoiseWorkgroupsY]);
3745
4271
  resolvePass.end();
3746
4272
  }
3747
4273
  function encodePresent(encoder) {
@@ -3762,7 +4288,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3762
4288
  passEncoder.draw(3);
3763
4289
  passEncoder.end();
3764
4290
  }
3765
- function dispatchFrame(frameIndex) {
4291
+ function dispatchFrame(frameIndex, parallelism) {
3766
4292
  const writeFrameConfig = createFrameConfigWriter(frameIndex);
3767
4293
  let submissionCount = 0;
3768
4294
  let encodedFramePasses = 0;
@@ -3793,20 +4319,25 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3793
4319
  sampleIndex,
3794
4320
  sampleWeight: 1 / config.samplesPerPixel
3795
4321
  });
3796
- encodeTileSample(reserveEncoder(), tile, configOffset);
4322
+ encodeTileSample(reserveEncoder(), tile, configOffset, parallelism);
4323
+ if (config.deferredPathResolve) {
4324
+ encodeTileOutput(reserveEncoder(), tile, configOffset, parallelism);
4325
+ }
4326
+ }
4327
+ if (!config.deferredPathResolve) {
4328
+ const outputConfigOffset = writeFrameConfig(tile, {
4329
+ sampleIndex: 0,
4330
+ sampleWeight: 1 / config.samplesPerPixel
4331
+ });
4332
+ encodeTileOutput(reserveEncoder(), tile, outputConfigOffset, parallelism);
3797
4333
  }
3798
- const outputConfigOffset = writeFrameConfig(tile, {
3799
- sampleIndex: 0,
3800
- sampleWeight: 1 / config.samplesPerPixel
3801
- });
3802
- encodeTileOutput(reserveEncoder(), tile, outputConfigOffset);
3803
4334
  }
3804
4335
  if (config.denoise) {
3805
4336
  const denoiseConfigOffset = writeFrameConfig(
3806
4337
  { x: 0, y: 0, width: config.width, height: config.height },
3807
4338
  { sampleIndex: 0, sampleWeight: 1 / config.samplesPerPixel }
3808
4339
  );
3809
- encodeDenoise(reserveEncoder(), denoiseConfigOffset);
4340
+ encodeDenoise(reserveEncoder(), denoiseConfigOffset, parallelism);
3810
4341
  }
3811
4342
  encodePresent(reserveEncoder());
3812
4343
  submitCurrentEncoder();
@@ -3815,8 +4346,10 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3815
4346
  function renderOnce() {
3816
4347
  frame += 1;
3817
4348
  const frameIndex = frame + config.frameIndex;
3818
- const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex);
3819
- const frameSubmissionCount = dispatchFrame(frameIndex);
4349
+ const parallelismCounters = createGpuParallelismCounters();
4350
+ const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex, parallelismCounters);
4351
+ const frameSubmissionCount = dispatchFrame(frameIndex, parallelismCounters);
4352
+ lastGpuParallelism = createGpuParallelismDiagnostics(gpuAdapterParallelism, parallelismCounters);
3820
4353
  return Object.freeze({
3821
4354
  frame,
3822
4355
  width: config.width,
@@ -3833,6 +4366,8 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3833
4366
  emissiveTriangleCount: config.emissiveTriangleCount,
3834
4367
  environmentPortalCount: config.environmentPortalCount,
3835
4368
  environmentPortalMode: config.environmentPortalMode,
4369
+ environmentMap: createEnvironmentMapSnapshot(config.environmentMap),
4370
+ deferredPathResolve: config.deferredPathResolve,
3836
4371
  bvhNodeCount: config.bvhNodeCount,
3837
4372
  displayQuality: config.displayQuality,
3838
4373
  accelerationBuildMode: config.accelerationBuildMode,
@@ -3842,6 +4377,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3842
4377
  accelerationBuildCount,
3843
4378
  commandSubmissions: frameSubmissionCount + (accelerationBuildSubmitted ? 1 : 0),
3844
4379
  frameConfigSlots: frameConfigSlotCount,
4380
+ gpuParallelism: lastGpuParallelism,
3845
4381
  memory: config.memory
3846
4382
  });
3847
4383
  }
@@ -3950,6 +4486,8 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3950
4486
  emissiveTriangleCount: config.emissiveTriangleCount,
3951
4487
  environmentPortalCount: config.environmentPortalCount,
3952
4488
  environmentPortalMode: config.environmentPortalMode,
4489
+ environmentMap: createEnvironmentMapSnapshot(config.environmentMap),
4490
+ deferredPathResolve: config.deferredPathResolve,
3953
4491
  bvhNodeCount: config.bvhNodeCount,
3954
4492
  displayQuality: config.displayQuality,
3955
4493
  accelerationBuildMode: config.accelerationBuildMode,
@@ -3957,6 +4495,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3957
4495
  accelerationBuilt,
3958
4496
  accelerationBuildCount,
3959
4497
  frameConfigSlots: frameConfigSlotCount,
4498
+ gpuParallelism: lastGpuParallelism,
3960
4499
  memory: config.memory
3961
4500
  });
3962
4501
  }
@@ -3965,6 +4504,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3965
4504
  nextQueue.destroy?.();
3966
4505
  hitBuffer.destroy?.();
3967
4506
  accumulationBuffer.destroy?.();
4507
+ pathVertexBuffer.destroy?.();
3968
4508
  sceneObjectBuffer.destroy?.();
3969
4509
  triangleBuffer.destroy?.();
3970
4510
  bvhNodeBuffer.destroy?.();
@@ -3980,6 +4520,9 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3980
4520
  radianceTexture.destroy?.();
3981
4521
  denoiseScratchTexture.destroy?.();
3982
4522
  outputTexture.destroy?.();
4523
+ if (environmentMapResource.ownsTexture) {
4524
+ environmentMapResource.texture?.destroy?.();
4525
+ }
3983
4526
  context.unconfigure?.();
3984
4527
  }
3985
4528
  return Object.freeze({