@plasius/gpu-renderer 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -94,11 +94,12 @@ var BVH_LEAF_REF_RECORD_BYTES = 16;
94
94
  var EMISSIVE_TRIANGLE_INDEX_BYTES = 4;
95
95
  var ENVIRONMENT_PORTAL_RECORD_BYTES = 96;
96
96
  var ACCUMULATION_RECORD_BYTES = 16;
97
- var CONFIG_BUFFER_BYTES = 272;
97
+ var PATH_VERTEX_RECORD_BYTES = 16;
98
+ var CONFIG_BUFFER_BYTES = 304;
98
99
  var COUNTER_DISPATCH_ARGS_OFFSET = 16;
99
100
  var INDIRECT_DISPATCH_ARGS_BYTES = 12;
100
101
  var COUNTER_BUFFER_BYTES = 32;
101
- var TRACE_STORAGE_BUFFER_BINDINGS = 9;
102
+ var TRACE_STORAGE_BUFFER_BINDINGS = 10;
102
103
  var MATERIAL_DIFFUSE = 0;
103
104
  var MATERIAL_METAL = 1;
104
105
  var MATERIAL_DIELECTRIC = 2;
@@ -121,7 +122,8 @@ var DEFAULT_ENVIRONMENT_LIGHTING = Object.freeze({
121
122
  sunColor: Object.freeze([2.8, 2.65, 2.35, 1]),
122
123
  intensity: 1,
123
124
  mode: 0,
124
- exposure: 1
125
+ exposure: 1,
126
+ sunlitBaseline: 0.16
125
127
  });
126
128
  var wavefrontPathTracingComputeLimits = Object.freeze({
127
129
  workgroupSize: WORKGROUP_SIZE,
@@ -138,6 +140,7 @@ var wavefrontPathTracingComputeLimits = Object.freeze({
138
140
  emissiveTriangleMetadataRecordBytes: BVH_NODE_RECORD_BYTES,
139
141
  environmentPortalRecordBytes: ENVIRONMENT_PORTAL_RECORD_BYTES,
140
142
  accumulationRecordBytes: ACCUMULATION_RECORD_BYTES,
143
+ pathVertexRecordBytes: PATH_VERTEX_RECORD_BYTES,
141
144
  counterRecordBytes: COUNTER_BUFFER_BYTES,
142
145
  indirectDispatchRecordBytes: INDIRECT_DISPATCH_ARGS_BYTES
143
146
  });
@@ -214,6 +217,33 @@ function asColor(value, fallback = [1, 1, 1, 1]) {
214
217
  clamp(readFiniteNumber("color[3]", value[3], fallback[3] ?? 1), 0, 1)
215
218
  ];
216
219
  }
220
+ function resolveEnvironmentMap(input = null) {
221
+ const source = input && typeof input === "object" ? input : null;
222
+ const hasTexture = Boolean(source?.view || source?.texture || source?.data);
223
+ const width = readPositiveInteger("environmentMap.width", source?.width, 1);
224
+ const height = readPositiveInteger("environmentMap.height", source?.height, 1);
225
+ return Object.freeze({
226
+ enabled: hasTexture && source?.enabled !== false,
227
+ width,
228
+ height,
229
+ format: typeof source?.format === "string" ? source.format : "rgba16float",
230
+ projection: typeof source?.projection === "string" ? source.projection : "equirectangular",
231
+ texture: source?.texture ?? null,
232
+ view: source?.view ?? null,
233
+ sampler: source?.sampler ?? null,
234
+ data: source?.data ?? null,
235
+ intensity: Math.max(0, readFiniteNumber("environmentMap.intensity", source?.intensity ?? source?.radianceScale, 1)),
236
+ rotationRadians: readFiniteNumber("environmentMap.rotationRadians", source?.rotationRadians ?? source?.rotation, 0),
237
+ ambientStrength: Math.max(
238
+ 0,
239
+ readFiniteNumber("environmentMap.ambientStrength", source?.ambientStrength, 0.32)
240
+ )
241
+ });
242
+ }
243
+ function resolveDeferredPathResolve(options = {}) {
244
+ const value = options.deferredPathResolve ?? options.deferredResolve ?? options.pathResolve?.deferred ?? true;
245
+ return value !== false;
246
+ }
217
247
  function emissionPower(emission) {
218
248
  return Math.max(0, emission?.[0] ?? 0) + Math.max(0, emission?.[1] ?? 0) + Math.max(0, emission?.[2] ?? 0);
219
249
  }
@@ -861,7 +891,15 @@ function resolveEnvironmentLighting(input, environmentColor, ambientColor) {
861
891
  sunColor: Object.freeze(asColor(source.sunColor, DEFAULT_ENVIRONMENT_LIGHTING.sunColor)),
862
892
  intensity: Math.max(1e-4, readFiniteNumber("environmentLighting.intensity", source.intensity, DEFAULT_ENVIRONMENT_LIGHTING.intensity)),
863
893
  mode: readNonNegativeInteger("environmentLighting.mode", source.mode, DEFAULT_ENVIRONMENT_LIGHTING.mode),
864
- exposure: Math.max(1e-4, readFiniteNumber("environmentLighting.exposure", source.exposure, DEFAULT_ENVIRONMENT_LIGHTING.exposure))
894
+ exposure: Math.max(1e-4, readFiniteNumber("environmentLighting.exposure", source.exposure, DEFAULT_ENVIRONMENT_LIGHTING.exposure)),
895
+ sunlitBaseline: Math.max(
896
+ 0,
897
+ readFiniteNumber(
898
+ "environmentLighting.sunlitBaseline",
899
+ source.sunlitBaseline ?? source.daylightBaseline,
900
+ DEFAULT_ENVIRONMENT_LIGHTING.sunlitBaseline
901
+ )
902
+ )
865
903
  });
866
904
  }
867
905
  function evaluateReferenceEnvironmentRadiance(config, origin, direction) {
@@ -1046,6 +1084,11 @@ function estimateWavefrontPathTracingMemory(options = {}) {
1046
1084
  options.tilePixelCapacity,
1047
1085
  DEFAULT_TILE_SIZE * DEFAULT_TILE_SIZE
1048
1086
  );
1087
+ const maxDepth = clamp(
1088
+ readPositiveInteger("maxDepth", options.maxDepth, DEFAULT_MAX_DEPTH),
1089
+ 1,
1090
+ 16
1091
+ );
1049
1092
  const sceneObjectCapacity = readPositiveInteger(
1050
1093
  "sceneObjectCapacity",
1051
1094
  options.sceneObjectCapacity,
@@ -1071,6 +1114,7 @@ function estimateWavefrontPathTracingMemory(options = {}) {
1071
1114
  const queueBytes = tilePixelCapacity * RAY_RECORD_BYTES;
1072
1115
  const hitBytes = tilePixelCapacity * HIT_RECORD_BYTES;
1073
1116
  const accumulationBytes = tilePixelCapacity * ACCUMULATION_RECORD_BYTES;
1117
+ const pathVertexBytes = tilePixelCapacity * (maxDepth + 1) * PATH_VERTEX_RECORD_BYTES;
1074
1118
  const sceneObjectBytes = sceneObjectCapacity * SCENE_OBJECT_RECORD_BYTES;
1075
1119
  const triangleBytes = triangleCapacity * TRIANGLE_RECORD_BYTES;
1076
1120
  const bvhNodeBytes = bvhNodeCapacity * BVH_NODE_RECORD_BYTES;
@@ -1082,6 +1126,7 @@ function estimateWavefrontPathTracingMemory(options = {}) {
1082
1126
  queuePairBytes: queueBytes * 2,
1083
1127
  hitBytes,
1084
1128
  accumulationBytes,
1129
+ pathVertexBytes,
1085
1130
  sceneObjectBytes,
1086
1131
  triangleBytes,
1087
1132
  bvhNodeBytes,
@@ -1091,7 +1136,7 @@ function estimateWavefrontPathTracingMemory(options = {}) {
1091
1136
  configBytes: CONFIG_BUFFER_BYTES,
1092
1137
  counterBytes: COUNTER_BUFFER_BYTES,
1093
1138
  indirectDispatchBytes: INDIRECT_DISPATCH_ARGS_BYTES,
1094
- totalHotBufferBytes: queueBytes * 2 + hitBytes + accumulationBytes + sceneObjectBytes + triangleBytes + bvhNodeBytes + bvhLeafReferenceBytes + emissiveTriangleMetadataBytes + environmentPortalBytes + CONFIG_BUFFER_BYTES + COUNTER_BUFFER_BYTES + INDIRECT_DISPATCH_ARGS_BYTES
1139
+ totalHotBufferBytes: queueBytes * 2 + hitBytes + accumulationBytes + pathVertexBytes + sceneObjectBytes + triangleBytes + bvhNodeBytes + bvhLeafReferenceBytes + emissiveTriangleMetadataBytes + environmentPortalBytes + CONFIG_BUFFER_BYTES + COUNTER_BUFFER_BYTES + INDIRECT_DISPATCH_ARGS_BYTES
1095
1140
  });
1096
1141
  }
1097
1142
  function createWavefrontPathTracingComputeConfig(options = {}) {
@@ -1176,6 +1221,10 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
1176
1221
  options.environmentPortalMode ?? options.portalMode ?? options.environmentLighting?.environmentPortalMode,
1177
1222
  environmentPortals.length > 0
1178
1223
  );
1224
+ const environmentMap = resolveEnvironmentMap(
1225
+ options.environmentMap ?? options.environmentTexture ?? options.environmentLighting?.environmentMap
1226
+ );
1227
+ const deferredPathResolve = resolveDeferredPathResolve(options);
1179
1228
  return Object.freeze({
1180
1229
  mode: rendererWavefrontComputeMode,
1181
1230
  width,
@@ -1210,12 +1259,15 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
1210
1259
  environmentPortalCount: environmentPortals.length,
1211
1260
  environmentPortalCapacity,
1212
1261
  environmentPortalMode,
1262
+ environmentMap,
1263
+ deferredPathResolve,
1213
1264
  displayQuality: options.displayQuality === true,
1214
1265
  requiresMeshBvhForDisplayQuality: true,
1215
1266
  denoise: options.denoise !== false,
1216
1267
  frameIndex: readNonNegativeInteger("frameIndex", options.frameIndex, 0),
1217
1268
  memory: estimateWavefrontPathTracingMemory({
1218
1269
  tilePixelCapacity,
1270
+ maxDepth,
1219
1271
  sceneObjectCapacity,
1220
1272
  triangleCapacity,
1221
1273
  bvhNodeCapacity,
@@ -1412,6 +1464,18 @@ function createConfigPayload(config, tile, frameIndex, buildRange = {}) {
1412
1464
  data.setUint32(260, config.environmentPortalMode ?? 0, true);
1413
1465
  data.setUint32(264, 0, true);
1414
1466
  data.setUint32(268, 0, true);
1467
+ writeVec4(floatView, 272, [
1468
+ config.environmentMap.enabled ? 1 : 0,
1469
+ config.environmentMap.intensity,
1470
+ config.environmentMap.rotationRadians,
1471
+ config.environmentMap.ambientStrength
1472
+ ]);
1473
+ writeVec4(floatView, 288, [
1474
+ config.deferredPathResolve ? 1 : 0,
1475
+ config.environmentLighting.sunlitBaseline,
1476
+ 0,
1477
+ 0
1478
+ ]);
1415
1479
  return bytes;
1416
1480
  }
1417
1481
  function createTiles(width, height, tileSize) {
@@ -1657,6 +1721,136 @@ function alignTo(value, alignment) {
1657
1721
  const resolvedAlignment = Math.max(1, alignment);
1658
1722
  return Math.ceil(value / resolvedAlignment) * resolvedAlignment;
1659
1723
  }
1724
+ function float32ToFloat16Bits(value) {
1725
+ const floatView = new Float32Array(1);
1726
+ const intView = new Uint32Array(floatView.buffer);
1727
+ floatView[0] = Number.isFinite(value) ? value : 0;
1728
+ const x = intView[0];
1729
+ const sign = x >> 16 & 32768;
1730
+ let mantissa = x & 8388607;
1731
+ let exponent = x >> 23 & 255;
1732
+ if (exponent === 255) {
1733
+ return sign | (mantissa ? 32256 : 31744);
1734
+ }
1735
+ exponent = exponent - 127 + 15;
1736
+ if (exponent >= 31) {
1737
+ return sign | 31744;
1738
+ }
1739
+ if (exponent <= 0) {
1740
+ if (exponent < -10) {
1741
+ return sign;
1742
+ }
1743
+ mantissa = (mantissa | 8388608) >> 1 - exponent;
1744
+ return sign | mantissa + 4096 >> 13;
1745
+ }
1746
+ return sign | exponent << 10 | mantissa + 4096 >> 13;
1747
+ }
1748
+ function environmentMapIntegerScale(data) {
1749
+ if (data instanceof Uint8Array) {
1750
+ return 1 / 255;
1751
+ }
1752
+ if (data instanceof Uint16Array) {
1753
+ return 1 / 65535;
1754
+ }
1755
+ return 1;
1756
+ }
1757
+ function readEnvironmentMapComponent(data, index, fallback, integerScale = 1) {
1758
+ if (!data || index >= data.length) {
1759
+ return fallback;
1760
+ }
1761
+ const value = Number(data[index]);
1762
+ return Number.isFinite(value) ? Math.max(0, value) * integerScale : fallback;
1763
+ }
1764
+ function createEnvironmentMapUploadBytes(environmentMap, fallbackColor) {
1765
+ const width = Math.max(1, environmentMap.width);
1766
+ const height = Math.max(1, environmentMap.height);
1767
+ const rowBytes = width * 8;
1768
+ const bytesPerRow = alignTo(rowBytes, 256);
1769
+ const bytes = new Uint8Array(bytesPerRow * height);
1770
+ const data = environmentMap.data;
1771
+ const integerScale = environmentMapIntegerScale(data);
1772
+ const view = new DataView(bytes.buffer);
1773
+ const writeComponent = (targetOffset, sourceOffset, fallback) => {
1774
+ view.setUint16(
1775
+ targetOffset,
1776
+ float32ToFloat16Bits(
1777
+ readEnvironmentMapComponent(data, sourceOffset, fallback, integerScale)
1778
+ ),
1779
+ true
1780
+ );
1781
+ };
1782
+ for (let y = 0; y < height; y += 1) {
1783
+ for (let x = 0; x < width; x += 1) {
1784
+ const sourceOffset = (y * width + x) * 4;
1785
+ const targetOffset = y * bytesPerRow + x * 8;
1786
+ writeComponent(targetOffset, sourceOffset, fallbackColor[0]);
1787
+ writeComponent(targetOffset + 2, sourceOffset + 1, fallbackColor[1]);
1788
+ writeComponent(targetOffset + 4, sourceOffset + 2, fallbackColor[2]);
1789
+ writeComponent(targetOffset + 6, sourceOffset + 3, fallbackColor[3] ?? 1);
1790
+ }
1791
+ }
1792
+ return Object.freeze({
1793
+ bytes,
1794
+ bytesPerRow,
1795
+ width,
1796
+ height
1797
+ });
1798
+ }
1799
+ function createEnvironmentMapResource(device, constants, environmentMap, fallbackColor) {
1800
+ if (environmentMap.view) {
1801
+ return Object.freeze({
1802
+ view: environmentMap.view,
1803
+ sampler: environmentMap.sampler ?? device.createSampler({
1804
+ label: "plasius.wavefront.environmentMapSampler",
1805
+ addressModeU: "repeat",
1806
+ addressModeV: "clamp-to-edge",
1807
+ magFilter: "linear",
1808
+ minFilter: "linear"
1809
+ }),
1810
+ texture: null,
1811
+ ownsTexture: false
1812
+ });
1813
+ }
1814
+ if (environmentMap.texture && typeof environmentMap.texture.createView === "function") {
1815
+ return Object.freeze({
1816
+ view: environmentMap.texture.createView(),
1817
+ sampler: environmentMap.sampler ?? device.createSampler({
1818
+ label: "plasius.wavefront.environmentMapSampler",
1819
+ addressModeU: "repeat",
1820
+ addressModeV: "clamp-to-edge",
1821
+ magFilter: "linear",
1822
+ minFilter: "linear"
1823
+ }),
1824
+ texture: environmentMap.texture,
1825
+ ownsTexture: false
1826
+ });
1827
+ }
1828
+ const upload = createEnvironmentMapUploadBytes(environmentMap, fallbackColor);
1829
+ const texture = device.createTexture({
1830
+ label: environmentMap.enabled ? "plasius.wavefront.environmentMap" : "plasius.wavefront.environmentMapFallback",
1831
+ size: { width: upload.width, height: upload.height },
1832
+ format: "rgba16float",
1833
+ usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
1834
+ });
1835
+ device.queue.writeTexture(
1836
+ { texture },
1837
+ upload.bytes,
1838
+ { bytesPerRow: upload.bytesPerRow, rowsPerImage: upload.height },
1839
+ { width: upload.width, height: upload.height, depthOrArrayLayers: 1 }
1840
+ );
1841
+ return Object.freeze({
1842
+ view: texture.createView(),
1843
+ sampler: environmentMap.sampler ?? device.createSampler({
1844
+ label: "plasius.wavefront.environmentMapSampler",
1845
+ addressModeU: "repeat",
1846
+ addressModeV: "clamp-to-edge",
1847
+ magFilter: "linear",
1848
+ minFilter: "linear"
1849
+ }),
1850
+ texture,
1851
+ ownsTexture: true
1852
+ });
1853
+ }
1660
1854
  async function getPipelineDiagnostics(shaderModule) {
1661
1855
  if (typeof shaderModule?.compilationInfo !== "function") {
1662
1856
  return "";
@@ -1865,6 +2059,8 @@ struct FrameConfig {
1865
2059
  environmentPortalMode: u32,
1866
2060
  _portalPad0: u32,
1867
2061
  _portalPad1: u32,
2062
+ environmentMapSettings: vec4<f32>,
2063
+ pathResolveSettings: vec4<f32>,
1868
2064
  };
1869
2065
 
1870
2066
  struct Counters {
@@ -1924,6 +2120,9 @@ struct EnvironmentPortal {
1924
2120
  @group(0) @binding(17) var finalDenoiseInputRadiance: texture_2d<f32>;
1925
2121
  @group(0) @binding(18) var denoisedOutputImage: texture_storage_2d<rgba8unorm, write>;
1926
2122
  @group(0) @binding(19) var<storage, read> environmentPortals: array<EnvironmentPortal>;
2123
+ @group(0) @binding(20) var environmentMapTexture: texture_2d<f32>;
2124
+ @group(0) @binding(21) var environmentMapSampler: sampler;
2125
+ @group(0) @binding(22) var<storage, read_write> pathVertices: array<vec4<f32>>;
1927
2126
 
1928
2127
  fn hash_u32(value: u32) -> u32 {
1929
2128
  var x = value;
@@ -1968,6 +2167,89 @@ fn max_component(value: vec3<f32>) -> f32 {
1968
2167
  return max(max(value.x, value.y), value.z);
1969
2168
  }
1970
2169
 
2170
+ fn environment_map_enabled() -> bool {
2171
+ return config.environmentMapSettings.x > 0.5;
2172
+ }
2173
+
2174
+ fn deferred_path_resolve_enabled() -> bool {
2175
+ return config.pathResolveSettings.x > 0.5;
2176
+ }
2177
+
2178
+ fn path_vertex_count_per_ray() -> u32 {
2179
+ return config.maxDepth + 1u;
2180
+ }
2181
+
2182
+ fn path_vertex_index(rayId: u32, depth: u32) -> u32 {
2183
+ return rayId * path_vertex_count_per_ray() + min(depth, config.maxDepth);
2184
+ }
2185
+
2186
+ fn clear_deferred_path(rayId: u32) {
2187
+ if (!deferred_path_resolve_enabled()) {
2188
+ return;
2189
+ }
2190
+
2191
+ for (var depth = 0u; depth <= config.maxDepth; depth = depth + 1u) {
2192
+ pathVertices[path_vertex_index(rayId, depth)] = vec4<f32>(0.0);
2193
+ if (depth == config.maxDepth) {
2194
+ break;
2195
+ }
2196
+ }
2197
+ }
2198
+
2199
+ fn record_deferred_path_response(ray: RayRecord, response: vec3<f32>) {
2200
+ if (!deferred_path_resolve_enabled() || ray.rayId >= config.tilePixelCount || ray.bounce >= config.maxDepth) {
2201
+ return;
2202
+ }
2203
+ pathVertices[path_vertex_index(ray.rayId, ray.bounce)] =
2204
+ vec4<f32>(max(response, vec3<f32>(0.0)), 1.0);
2205
+ }
2206
+
2207
+ fn record_deferred_terminal_source(ray: RayRecord, sourceRadiance: vec3<f32>) {
2208
+ if (!deferred_path_resolve_enabled() || ray.rayId >= config.tilePixelCount) {
2209
+ return;
2210
+ }
2211
+ pathVertices[path_vertex_index(ray.rayId, config.maxDepth)] =
2212
+ vec4<f32>(clamp_sample_radiance(sourceRadiance), 1.0);
2213
+ }
2214
+
2215
+ fn environment_map_uv(direction: vec3<f32>) -> vec2<f32> {
2216
+ let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
2217
+ let rotationTurns = config.environmentMapSettings.z / 6.28318530718;
2218
+ let u = fract(atan2(rayDirection.z, rayDirection.x) / 6.28318530718 + 0.5 + rotationTurns);
2219
+ let v = acos(clamp(rayDirection.y, -1.0, 1.0)) / 3.14159265359;
2220
+ return vec2<f32>(u, clamp(v, 0.0, 1.0));
2221
+ }
2222
+
2223
+ fn environment_map_radiance(direction: vec3<f32>) -> vec3<f32> {
2224
+ let uv = environment_map_uv(direction);
2225
+ let texel = max(textureSampleLevel(environmentMapTexture, environmentMapSampler, uv, 0.0).rgb, vec3<f32>(0.0));
2226
+ return texel * max(config.environmentMapSettings.y, 0.0);
2227
+ }
2228
+
2229
+ fn procedural_environment_radiance(direction: vec3<f32>) -> vec3<f32> {
2230
+ let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
2231
+ let upFactor = saturate(rayDirection.y * 0.5 + 0.5);
2232
+ let sunDirection = safe_normalize(
2233
+ config.environmentSunDirectionIntensity.xyz,
2234
+ vec3<f32>(0.0, 1.0, 0.0)
2235
+ );
2236
+ let sunGlow = pow(saturate(dot(rayDirection, sunDirection)), 192.0);
2237
+ let gradient =
2238
+ config.environmentHorizonColor.xyz * (1.0 - upFactor) +
2239
+ config.environmentZenithColor.xyz * upFactor;
2240
+ return (
2241
+ gradient +
2242
+ config.environmentSunColor.xyz * sunGlow
2243
+ ) * max(config.environmentSunDirectionIntensity.w, 0.0001);
2244
+ }
2245
+
2246
+ fn base_environment_radiance(direction: vec3<f32>) -> vec3<f32> {
2247
+ if (environment_map_enabled()) {
2248
+ return environment_map_radiance(direction);
2249
+ }
2250
+ return procedural_environment_radiance(direction);
2251
+ }
2252
+
1971
2253
  fn environment_portal_radiance_scale(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
1972
2254
  if (config.environmentPortalCount == 0u || config.environmentPortalMode == 0u) {
1973
2255
  return vec3<f32>(1.0);
@@ -2007,22 +2289,9 @@ fn environment_portal_radiance_scale(origin: vec3<f32>, direction: vec3<f32>) ->
2007
2289
 
2008
2290
  fn environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
2009
2291
  let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
2010
- let upFactor = saturate(rayDirection.y * 0.5 + 0.5);
2011
- let sunDirection = safe_normalize(
2012
- config.environmentSunDirectionIntensity.xyz,
2013
- vec3<f32>(0.0, 1.0, 0.0)
2014
- );
2015
- let sunGlow = pow(saturate(dot(rayDirection, sunDirection)), 192.0);
2016
- let gradient =
2017
- config.environmentHorizonColor.xyz * (1.0 - upFactor) +
2018
- config.environmentZenithColor.xyz * upFactor;
2019
2292
  let portalScale = environment_portal_radiance_scale(origin, rayDirection);
2020
2293
  let portalHit = max_component(portalScale) > 0.0001;
2021
- return (
2022
- gradient +
2023
- config.environmentSunColor.xyz * sunGlow
2024
- ) *
2025
- max(config.environmentSunDirectionIntensity.w, 0.0001) *
2294
+ return base_environment_radiance(rayDirection) *
2026
2295
  select(vec3<f32>(1.0), portalScale, portalHit);
2027
2296
  }
2028
2297
 
@@ -2038,16 +2307,59 @@ fn gated_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f
2038
2307
  return environment_radiance(origin, direction);
2039
2308
  }
2040
2309
 
2041
- fn terminal_surface_environment_contribution(ray: RayRecord, hit: HitRecord) -> vec3<f32> {
2310
+ fn surface_path_response(hit: HitRecord) -> vec3<f32> {
2311
+ let color = clamp(hit.color.xyz, vec3<f32>(0.0), vec3<f32>(1.0));
2312
+ let opacity = clamp(hit.material.z, 0.0, 1.0);
2313
+ let materialEnergy = select(0.68, 0.92, hit.materialKind == 1u || hit.materialKind == 2u);
2314
+ let transparentEnergy = select(materialEnergy, 0.9, hit.hitType == 3u);
2315
+ return mix(vec3<f32>(1.0), color, max(opacity, 0.18)) * transparentEnergy;
2316
+ }
2317
+
2318
+ fn sunlit_baseline_radiance(normal: vec3<f32>) -> vec3<f32> {
2319
+ let baseline = max(config.pathResolveSettings.y, 0.0);
2320
+ if (baseline <= 0.000001) {
2321
+ return vec3<f32>(0.0);
2322
+ }
2323
+ let sunDirection = safe_normalize(
2324
+ config.environmentSunDirectionIntensity.xyz,
2325
+ vec3<f32>(0.0, 1.0, 0.0)
2326
+ );
2327
+ let sunFacing = saturate(dot(normal, sunDirection));
2328
+ let skyFacing = 0.35 + saturate(normal.y * 0.5 + 0.5) * 0.65;
2329
+ let directionalWeight = 0.38 + sunFacing * 0.62;
2330
+ let sunTint = max(config.environmentSunColor.xyz, vec3<f32>(0.0));
2331
+ return clamp_sample_radiance(sunTint * baseline * skyFacing * directionalWeight * 0.04);
2332
+ }
2333
+
2334
+ fn terminal_surface_environment_source(hit: HitRecord) -> vec3<f32> {
2042
2335
  let normal = safe_normalize(hit.shadingNormal.xyz, vec3<f32>(0.0, 1.0, 0.0));
2043
- let surfaceColor = max(hit.color.xyz, config.ambientColor.xyz);
2044
2336
  let normalEnvironment = gated_environment_radiance(
2045
2337
  hit.position.xyz + normal * 0.003,
2046
2338
  normal
2047
2339
  );
2048
- let environmentFloor = max(config.ambientColor.xyz, normalEnvironment * 0.12);
2340
+ let sunlitFloor = sunlit_baseline_radiance(normal);
2341
+ let ambientFloor = select(
2342
+ max(config.ambientColor.xyz, sunlitFloor * 0.82),
2343
+ max(config.ambientColor.xyz * 0.35, sunlitFloor * 0.58),
2344
+ environment_map_enabled()
2345
+ );
2346
+ let environmentInfluence = select(
2347
+ max(0.12, config.pathResolveSettings.y * 0.42),
2348
+ max(config.environmentMapSettings.w, max(0.12, config.pathResolveSettings.y * 0.42)),
2349
+ environment_map_enabled()
2350
+ );
2351
+ let environmentFloor = max(ambientFloor, max(sunlitFloor, normalEnvironment * environmentInfluence));
2049
2352
  let materialFloor = select(0.7, 1.0, hit.materialKind == 0u || hit.materialKind == 3u);
2050
- return clamp_sample_radiance(ray.throughput.xyz * surfaceColor * environmentFloor * materialFloor);
2353
+ return clamp_sample_radiance(environmentFloor * materialFloor);
2354
+ }
2355
+
2356
+ fn terminal_surface_environment_contribution(ray: RayRecord, hit: HitRecord) -> vec3<f32> {
2357
+ let surfaceColor = max(hit.color.xyz, config.ambientColor.xyz);
2358
+ return clamp_sample_radiance(
2359
+ ray.throughput.xyz *
2360
+ surfaceColor *
2361
+ terminal_surface_environment_source(hit)
2362
+ );
2051
2363
  }
2052
2364
 
2053
2365
  fn direct_environment_portal_irradiance(origin: vec3<f32>, normal: vec3<f32>) -> vec3<f32> {
@@ -2100,7 +2412,17 @@ fn surface_direct_environment_contribution(ray: RayRecord, hit: HitRecord) -> ve
2100
2412
 
2101
2413
  let normalEnvironment = gated_environment_radiance(origin, normal);
2102
2414
  let skyVisibility = 0.35 + saturate(normal.y * 0.5 + 0.5) * 0.45;
2103
- let skyIrradiance = max(config.ambientColor.xyz * 0.72, normalEnvironment * skyVisibility * 0.16);
2415
+ let sunlitFloor = sunlit_baseline_radiance(normal);
2416
+ let ambientIrradiance = max(
2417
+ select(config.ambientColor.xyz * 0.72, config.ambientColor.xyz * 0.28, environment_map_enabled()),
2418
+ sunlitFloor * select(0.72, 0.45, environment_map_enabled())
2419
+ );
2420
+ let environmentIrradianceScale = select(
2421
+ max(0.16, config.pathResolveSettings.y * 0.45),
2422
+ max(config.environmentMapSettings.w, max(0.16, config.pathResolveSettings.y * 0.45)),
2423
+ environment_map_enabled()
2424
+ );
2425
+ let skyIrradiance = max(ambientIrradiance, normalEnvironment * skyVisibility * environmentIrradianceScale);
2104
2426
 
2105
2427
  let sunDirection = safe_normalize(
2106
2428
  config.environmentSunDirectionIntensity.xyz,
@@ -2724,6 +3046,7 @@ fn generatePrimaryRays(@builtin(global_invocation_id) globalId: vec3<u32>) {
2724
3046
  return;
2725
3047
  }
2726
3048
  activeQueue[index] = make_ray(index);
3049
+ clear_deferred_path(index);
2727
3050
  if (u32(config.projectionAndSampling.w) == 0u) {
2728
3051
  accumulation[index] = vec4<f32>(0.0);
2729
3052
  }
@@ -2976,25 +3299,37 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
2976
3299
 
2977
3300
  if (hit.hitType == 1u) {
2978
3301
  let guidedLightWeight = select(1.0, 0.24, (ray.flags & RAY_FLAG_GUIDED_EMISSIVE) != 0u);
2979
- contribution = clamp_sample_radiance(
2980
- ray.throughput.xyz * max(hit.emission.xyz, hit.color.xyz) * guidedLightWeight
2981
- );
2982
- accumulation[ray.rayId] =
2983
- accumulation[ray.rayId] + vec4<f32>(contribution * sample_weight(), 1.0);
3302
+ let sourceRadiance = max(hit.emission.xyz, hit.color.xyz) * guidedLightWeight;
3303
+ if (deferred_path_resolve_enabled()) {
3304
+ record_deferred_terminal_source(ray, sourceRadiance);
3305
+ } else {
3306
+ contribution = clamp_sample_radiance(ray.throughput.xyz * sourceRadiance);
3307
+ accumulation[ray.rayId] =
3308
+ accumulation[ray.rayId] + vec4<f32>(contribution * sample_weight(), 1.0);
3309
+ }
2984
3310
  atomicAdd(&counters.terminatedCount, 1u);
2985
3311
  return;
2986
3312
  }
2987
3313
 
2988
3314
  if (hit.hitType == 2u) {
2989
- contribution = clamp_sample_radiance(ray.throughput.xyz * max(hit.color.xyz, config.ambientColor.xyz));
2990
- accumulation[ray.rayId] =
2991
- accumulation[ray.rayId] + vec4<f32>(contribution * sample_weight(), 1.0);
3315
+ if (deferred_path_resolve_enabled()) {
3316
+ record_deferred_terminal_source(ray, hit.color.xyz);
3317
+ } else {
3318
+ contribution = clamp_sample_radiance(ray.throughput.xyz * max(hit.color.xyz, config.ambientColor.xyz));
3319
+ accumulation[ray.rayId] =
3320
+ accumulation[ray.rayId] + vec4<f32>(contribution * sample_weight(), 1.0);
3321
+ }
2992
3322
  atomicAdd(&counters.terminatedCount, 1u);
2993
3323
  return;
2994
3324
  }
2995
3325
 
3326
+ let response = surface_path_response(hit);
3327
+ record_deferred_path_response(ray, response);
3328
+
2996
3329
  let shouldEstimateDirectEnvironment =
2997
- (hit.materialKind == 0u || hit.materialKind == 1u) && hit.material.z >= 0.95;
3330
+ !deferred_path_resolve_enabled() &&
3331
+ (hit.materialKind == 0u || hit.materialKind == 1u) &&
3332
+ hit.material.z >= 0.95;
2998
3333
  if (shouldEstimateDirectEnvironment) {
2999
3334
  let directEnvironment = surface_direct_environment_contribution(ray, hit);
3000
3335
  accumulation[ray.rayId] =
@@ -3002,9 +3337,13 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
3002
3337
  }
3003
3338
 
3004
3339
  if (ray.bounce + 1u >= config.maxDepth) {
3005
- let terminalEnvironment = terminal_surface_environment_contribution(ray, hit);
3006
- accumulation[ray.rayId] =
3007
- accumulation[ray.rayId] + vec4<f32>(terminalEnvironment * sample_weight(), 1.0);
3340
+ if (deferred_path_resolve_enabled()) {
3341
+ record_deferred_terminal_source(ray, terminal_surface_environment_source(hit));
3342
+ } else {
3343
+ let terminalEnvironment = terminal_surface_environment_contribution(ray, hit);
3344
+ accumulation[ray.rayId] =
3345
+ accumulation[ray.rayId] + vec4<f32>(terminalEnvironment * sample_weight(), 1.0);
3346
+ }
3008
3347
  atomicAdd(&counters.terminatedCount, 1u);
3009
3348
  return;
3010
3349
  }
@@ -3013,17 +3352,17 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
3013
3352
  let scatter = scatter_direction(ray, hit, seed);
3014
3353
  let nextIndex = atomicAdd(&counters.nextCount, 1u);
3015
3354
  if (nextIndex >= config.tilePixelCount) {
3016
- let overflowEnvironment = terminal_surface_environment_contribution(ray, hit);
3017
- accumulation[ray.rayId] =
3018
- accumulation[ray.rayId] + vec4<f32>(overflowEnvironment * sample_weight(), 1.0);
3355
+ if (deferred_path_resolve_enabled()) {
3356
+ record_deferred_terminal_source(ray, terminal_surface_environment_source(hit));
3357
+ } else {
3358
+ let overflowEnvironment = terminal_surface_environment_contribution(ray, hit);
3359
+ accumulation[ray.rayId] =
3360
+ accumulation[ray.rayId] + vec4<f32>(overflowEnvironment * sample_weight(), 1.0);
3361
+ }
3019
3362
  atomicAdd(&counters.terminatedCount, 1u);
3020
3363
  return;
3021
3364
  }
3022
- let color = clamp(hit.color.xyz, vec3<f32>(0.0), vec3<f32>(1.0));
3023
- let opacity = clamp(hit.material.z, 0.0, 1.0);
3024
- let materialEnergy = select(0.68, 0.92, hit.materialKind == 1u || hit.materialKind == 2u);
3025
- let transparentEnergy = select(materialEnergy, 0.9, hit.hitType == 3u);
3026
- let throughput = ray.throughput.xyz * mix(vec3<f32>(1.0), color, max(opacity, 0.18)) * transparentEnergy;
3365
+ let throughput = ray.throughput.xyz * response;
3027
3366
  nextQueue[nextIndex] = RayRecord(
3028
3367
  ray.rayId,
3029
3368
  ray.rayId,
@@ -3051,6 +3390,27 @@ fn compactAndSwapQueues(@builtin(global_invocation_id) globalId: vec3<u32>) {
3051
3390
  write_active_dispatch_args(activeCount);
3052
3391
  }
3053
3392
 
3393
+ fn resolve_deferred_path_radiance(rayId: u32) -> vec3<f32> {
3394
+ let terminal = pathVertices[path_vertex_index(rayId, config.maxDepth)];
3395
+ if (terminal.w <= 0.0) {
3396
+ return vec3<f32>(0.0);
3397
+ }
3398
+
3399
+ var radiance = terminal.xyz;
3400
+ var depth = config.maxDepth;
3401
+ loop {
3402
+ if (depth == 0u) {
3403
+ break;
3404
+ }
3405
+ depth = depth - 1u;
3406
+ let response = pathVertices[path_vertex_index(rayId, depth)];
3407
+ if (response.w > 0.0) {
3408
+ radiance = radiance * response.xyz;
3409
+ }
3410
+ }
3411
+ return clamp_sample_radiance(radiance);
3412
+ }
3413
+
3054
3414
  @compute @workgroup_size(64)
3055
3415
  fn accumulateTerminalRadiance(@builtin(global_invocation_id) globalId: vec3<u32>) {
3056
3416
  let index = globalId.x;
@@ -3060,7 +3420,12 @@ fn accumulateTerminalRadiance(@builtin(global_invocation_id) globalId: vec3<u32>
3060
3420
  let localX = index % config.tileWidth;
3061
3421
  let localY = index / config.tileWidth;
3062
3422
  let pixel = vec2<i32>(i32(config.tileX + localX), i32(config.tileY + localY));
3063
- let radiance = max(accumulation[index].xyz, vec3<f32>(0.0));
3423
+ var radiance = max(accumulation[index].xyz, vec3<f32>(0.0));
3424
+ if (deferred_path_resolve_enabled()) {
3425
+ let resolved = resolve_deferred_path_radiance(index) * sample_weight();
3426
+ radiance = clamp_sample_radiance(radiance + resolved);
3427
+ accumulation[index] = vec4<f32>(radiance, 1.0);
3428
+ }
3064
3429
 
3065
3430
  textureStore(radianceImage, pixel, vec4<f32>(radiance, 1.0));
3066
3431
  if (config.denoise == 0u) {
@@ -3198,6 +3563,125 @@ function createWavefrontDeviceDescriptor(adapter, options = {}) {
3198
3563
  }
3199
3564
  return Object.keys(descriptor).length > 0 ? descriptor : void 0;
3200
3565
  }
3566
+ function readGpuLimit(adapter, device, name) {
3567
+ const adapterValue = Number(adapter?.limits?.[name]);
3568
+ if (Number.isFinite(adapterValue)) {
3569
+ return adapterValue;
3570
+ }
3571
+ const deviceValue = Number(device?.limits?.[name]);
3572
+ return Number.isFinite(deviceValue) ? deviceValue : null;
3573
+ }
3574
+ function createAdapterInfoSnapshot(adapter) {
3575
+ const info = adapter?.info;
3576
+ if (!info || typeof info !== "object") {
3577
+ return null;
3578
+ }
3579
+ return Object.freeze({
3580
+ vendor: typeof info.vendor === "string" ? info.vendor : "",
3581
+ architecture: typeof info.architecture === "string" ? info.architecture : "",
3582
+ device: typeof info.device === "string" ? info.device : "",
3583
+ description: typeof info.description === "string" ? info.description : ""
3584
+ });
3585
+ }
3586
+ function createGpuAdapterParallelismDiagnostics(adapter, device) {
3587
+ return Object.freeze({
3588
+ physicalCoreCount: null,
3589
+ physicalCoreCountAvailable: false,
3590
+ physicalCoreCountUnavailableReason: "WebGPU does not expose physical GPU core counts.",
3591
+ adapterInfo: createAdapterInfoSnapshot(adapter),
3592
+ adapterLimits: Object.freeze({
3593
+ maxComputeInvocationsPerWorkgroup: readGpuLimit(adapter, device, "maxComputeInvocationsPerWorkgroup"),
3594
+ maxComputeWorkgroupSizeX: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeX"),
3595
+ maxComputeWorkgroupSizeY: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeY"),
3596
+ maxComputeWorkgroupSizeZ: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeZ"),
3597
+ maxComputeWorkgroupsPerDimension: readGpuLimit(adapter, device, "maxComputeWorkgroupsPerDimension"),
3598
+ maxStorageBuffersPerShaderStage: readGpuLimit(adapter, device, "maxStorageBuffersPerShaderStage"),
3599
+ maxStorageBufferBindingSize: readGpuLimit(adapter, device, "maxStorageBufferBindingSize")
3600
+ }),
3601
+ configuredWorkgroupSize: WORKGROUP_SIZE
3602
+ });
3603
+ }
3604
+ function createGpuParallelismCounters() {
3605
+ return {
3606
+ directDispatches: 0,
3607
+ directWorkgroups: 0,
3608
+ directShaderInvocations: 0,
3609
+ multiWorkgroupDispatches: 0,
3610
+ largestDirectWorkgroupsPerDispatch: 0,
3611
+ indirectDispatches: 0,
3612
+ estimatedIndirectWorkgroupsUpperBound: 0,
3613
+ estimatedIndirectShaderInvocationsUpperBound: 0,
3614
+ indirectDispatchesWithMultiWorkgroupCapacity: 0,
3615
+ largestEstimatedIndirectWorkgroupsPerDispatch: 0
3616
+ };
3617
+ }
3618
+ function countDispatchWorkgroups(groups) {
3619
+ return groups.reduce((product, value) => {
3620
+ const numeric = Number(value ?? 1);
3621
+ const count = Number.isFinite(numeric) ? Math.max(1, Math.trunc(numeric)) : 1;
3622
+ return product * count;
3623
+ }, 1);
3624
+ }
3625
+ function recordDirectDispatch(parallelism, groups, invocationsPerWorkgroup = WORKGROUP_SIZE) {
3626
+ const workgroups = countDispatchWorkgroups(groups);
3627
+ parallelism.directDispatches += 1;
3628
+ parallelism.directWorkgroups += workgroups;
3629
+ parallelism.directShaderInvocations += workgroups * invocationsPerWorkgroup;
3630
+ parallelism.largestDirectWorkgroupsPerDispatch = Math.max(
3631
+ parallelism.largestDirectWorkgroupsPerDispatch,
3632
+ workgroups
3633
+ );
3634
+ if (workgroups > 1) {
3635
+ parallelism.multiWorkgroupDispatches += 1;
3636
+ }
3637
+ }
3638
+ function recordIndirectDispatch(parallelism, estimatedWorkgroupsUpperBound, invocationsPerWorkgroup = WORKGROUP_SIZE) {
3639
+ const workgroups = Math.max(1, Math.trunc(Number(estimatedWorkgroupsUpperBound) || 1));
3640
+ parallelism.indirectDispatches += 1;
3641
+ parallelism.estimatedIndirectWorkgroupsUpperBound += workgroups;
3642
+ parallelism.estimatedIndirectShaderInvocationsUpperBound += workgroups * invocationsPerWorkgroup;
3643
+ parallelism.largestEstimatedIndirectWorkgroupsPerDispatch = Math.max(
3644
+ parallelism.largestEstimatedIndirectWorkgroupsPerDispatch,
3645
+ workgroups
3646
+ );
3647
+ if (workgroups > 1) {
3648
+ parallelism.indirectDispatchesWithMultiWorkgroupCapacity += 1;
3649
+ }
3650
+ }
3651
+ function createGpuParallelismDiagnostics(adapterDiagnostics, counters) {
3652
+ const totalEstimatedWorkgroupsUpperBound = counters.directWorkgroups + counters.estimatedIndirectWorkgroupsUpperBound;
3653
+ const totalEstimatedShaderInvocationsUpperBound = counters.directShaderInvocations + counters.estimatedIndirectShaderInvocationsUpperBound;
3654
+ const exposesMultiWorkgroupParallelism = counters.multiWorkgroupDispatches > 0 || counters.indirectDispatchesWithMultiWorkgroupCapacity > 0;
3655
+ return Object.freeze({
3656
+ ...adapterDiagnostics,
3657
+ directDispatches: counters.directDispatches,
3658
+ directWorkgroups: counters.directWorkgroups,
3659
+ directShaderInvocations: counters.directShaderInvocations,
3660
+ multiWorkgroupDispatches: counters.multiWorkgroupDispatches,
3661
+ largestDirectWorkgroupsPerDispatch: counters.largestDirectWorkgroupsPerDispatch,
3662
+ indirectDispatches: counters.indirectDispatches,
3663
+ estimatedIndirectWorkgroupsUpperBound: counters.estimatedIndirectWorkgroupsUpperBound,
3664
+ estimatedIndirectShaderInvocationsUpperBound: counters.estimatedIndirectShaderInvocationsUpperBound,
3665
+ indirectDispatchesWithMultiWorkgroupCapacity: counters.indirectDispatchesWithMultiWorkgroupCapacity,
3666
+ largestEstimatedIndirectWorkgroupsPerDispatch: counters.largestEstimatedIndirectWorkgroupsPerDispatch,
3667
+ totalEstimatedWorkgroupsUpperBound,
3668
+ totalEstimatedShaderInvocationsUpperBound,
3669
+ exposesMultiWorkgroupParallelism,
3670
+ likelyUsesMoreThanOnePhysicalGpuCore: null,
3671
+ coreUtilizationStatus: "not-exposed-by-webgpu"
3672
+ });
3673
+ }
3674
+ function createEnvironmentMapSnapshot(environmentMap) {
3675
+ return Object.freeze({
3676
+ enabled: environmentMap.enabled,
3677
+ width: environmentMap.width,
3678
+ height: environmentMap.height,
3679
+ projection: environmentMap.projection,
3680
+ intensity: environmentMap.intensity,
3681
+ rotationRadians: environmentMap.rotationRadians,
3682
+ ambientStrength: environmentMap.ambientStrength
3683
+ });
3684
+ }
3201
3685
  async function createWavefrontPathTracingComputeRenderer(options = {}) {
3202
3686
  assertAnalyticDisplayQualityPolicy(options);
3203
3687
  const constants = getGpuUsageConstants();
@@ -3217,6 +3701,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3217
3701
  throw new Error("Unable to acquire a WebGPU adapter for wavefront path tracing.");
3218
3702
  }
3219
3703
  const device = await adapter.requestDevice(createWavefrontDeviceDescriptor(adapter, options));
3704
+ const gpuAdapterParallelism = createGpuAdapterParallelismDiagnostics(adapter, device);
3220
3705
  const context = canvas.getContext("webgpu");
3221
3706
  if (!context || typeof context.configure !== "function") {
3222
3707
  throw new Error("Canvas WebGPU context does not support configure().");
@@ -3244,6 +3729,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3244
3729
  const rayQueueBytes = config.tilePixelCapacity * RAY_RECORD_BYTES;
3245
3730
  const hitBytes = config.tilePixelCapacity * HIT_RECORD_BYTES;
3246
3731
  const accumulationBytes = config.tilePixelCapacity * ACCUMULATION_RECORD_BYTES;
3732
+ const pathVertexBytes = config.tilePixelCapacity * (config.maxDepth + 1) * PATH_VERTEX_RECORD_BYTES;
3247
3733
  const activeQueue = createBuffer(device, bufferUsage, rayQueueBytes, "plasius.wavefront.activeQueue");
3248
3734
  const nextQueue = createBuffer(device, bufferUsage, rayQueueBytes, "plasius.wavefront.nextQueue");
3249
3735
  const hitBuffer = createBuffer(device, bufferUsage, hitBytes, "plasius.wavefront.hitBuffer");
@@ -3253,6 +3739,12 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3253
3739
  accumulationBytes,
3254
3740
  "plasius.wavefront.accumulation"
3255
3741
  );
3742
+ const pathVertexBuffer = createBuffer(
3743
+ device,
3744
+ bufferUsage,
3745
+ pathVertexBytes,
3746
+ "plasius.wavefront.pathVertices"
3747
+ );
3256
3748
  const sceneObjectBuffer = createBuffer(
3257
3749
  device,
3258
3750
  constants.buffer.STORAGE | constants.buffer.COPY_DST,
@@ -3307,9 +3799,10 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3307
3799
  CONFIG_BUFFER_BYTES,
3308
3800
  Number.isFinite(uniformOffsetAlignment) && uniformOffsetAlignment > 0 ? uniformOffsetAlignment : CONFIG_BUFFER_BYTES
3309
3801
  );
3802
+ const outputConfigSlotCount = config.deferredPathResolve ? 0 : tiles.length;
3310
3803
  const frameConfigSlotCount = Math.max(
3311
3804
  1,
3312
- tiles.length * config.samplesPerPixel + tiles.length + (config.denoise ? 1 : 0)
3805
+ tiles.length * config.samplesPerPixel + outputConfigSlotCount + (config.denoise ? 1 : 0)
3313
3806
  );
3314
3807
  const configBuffer = createBuffer(
3315
3808
  device,
@@ -3387,6 +3880,12 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3387
3880
  magFilter: "nearest",
3388
3881
  minFilter: "nearest"
3389
3882
  });
3883
+ const environmentMapResource = createEnvironmentMapResource(
3884
+ device,
3885
+ constants,
3886
+ config.environmentMap,
3887
+ config.environmentColor
3888
+ );
3390
3889
  const traceBindGroupLayout = device.createBindGroupLayout({
3391
3890
  label: "plasius.wavefront.traceBindGroupLayout",
3392
3891
  entries: [
@@ -3413,7 +3912,10 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3413
3912
  visibility: constants.shader.COMPUTE,
3414
3913
  storageTexture: { access: "write-only", format: "rgba16float" }
3415
3914
  },
3416
- { binding: 19, visibility: constants.shader.COMPUTE, buffer: { type: "read-only-storage" } }
3915
+ { binding: 19, visibility: constants.shader.COMPUTE, buffer: { type: "read-only-storage" } },
3916
+ { binding: 20, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
3917
+ { binding: 21, visibility: constants.shader.COMPUTE, sampler: { type: "filtering" } },
3918
+ { binding: 22, visibility: constants.shader.COMPUTE, buffer: { type: "storage" } }
3417
3919
  ]
3418
3920
  });
3419
3921
  const accelerationBindGroupLayout = device.createBindGroupLayout({
@@ -3587,7 +4089,10 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3587
4089
  { binding: 8, resource: { buffer: triangleBuffer } },
3588
4090
  { binding: 9, resource: { buffer: bvhNodeBuffer } },
3589
4091
  { binding: 16, resource: radianceView },
3590
- { binding: 19, resource: { buffer: environmentPortalBuffer } }
4092
+ { binding: 19, resource: { buffer: environmentPortalBuffer } },
4093
+ { binding: 20, resource: environmentMapResource.view },
4094
+ { binding: 21, resource: environmentMapResource.sampler },
4095
+ { binding: 22, resource: { buffer: pathVertexBuffer } }
3591
4096
  ]
3592
4097
  });
3593
4098
  }
@@ -3677,6 +4182,10 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3677
4182
  let accelerationBuilt = !config.gpuAccelerationBuildRequired;
3678
4183
  let accelerationBuildCount = 0;
3679
4184
  let activeCameraOptions = options.camera ?? DEFAULT_CAMERA;
4185
+ let lastGpuParallelism = createGpuParallelismDiagnostics(
4186
+ gpuAdapterParallelism,
4187
+ createGpuParallelismCounters()
4188
+ );
3680
4189
  function createFrameConfigWriter(frameIndex) {
3681
4190
  let slot = 0;
3682
4191
  return (tile, buildRange = {}) => {
@@ -3693,7 +4202,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3693
4202
  return offset;
3694
4203
  };
3695
4204
  }
3696
- function dispatchGpuAccelerationBuild(frameIndex) {
4205
+ function dispatchGpuAccelerationBuild(frameIndex, parallelism) {
3697
4206
  if (!config.gpuAccelerationBuildRequired || accelerationBuilt) {
3698
4207
  return false;
3699
4208
  }
@@ -3732,24 +4241,32 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3732
4241
  });
3733
4242
  passEncoder.setBindGroup(0, bvhBuildBindGroup, [0]);
3734
4243
  passEncoder.setPipeline(pipelines.prepareMeshTrianglesAndLeaves);
3735
- passEncoder.dispatchWorkgroups(Math.ceil(config.bvhLeafSortCapacity / WORKGROUP_SIZE));
4244
+ const prepareWorkgroups = Math.ceil(config.bvhLeafSortCapacity / WORKGROUP_SIZE);
4245
+ passEncoder.dispatchWorkgroups(prepareWorkgroups);
4246
+ recordDirectDispatch(parallelism, [prepareWorkgroups]);
3736
4247
  passEncoder.setPipeline(pipelines.sortBvhLeafRefs);
3737
4248
  for (let stageIndex = 0; stageIndex < config.bvhSortStages.length; stageIndex += 1) {
3738
4249
  passEncoder.setBindGroup(0, bvhBuildBindGroup, [
3739
4250
  (stageIndex + 1) * configBufferStride
3740
4251
  ]);
3741
- passEncoder.dispatchWorkgroups(Math.ceil(config.bvhLeafSortCapacity / WORKGROUP_SIZE));
4252
+ const sortWorkgroups = Math.ceil(config.bvhLeafSortCapacity / WORKGROUP_SIZE);
4253
+ passEncoder.dispatchWorkgroups(sortWorkgroups);
4254
+ recordDirectDispatch(parallelism, [sortWorkgroups]);
3742
4255
  }
3743
4256
  passEncoder.setBindGroup(0, bvhBuildBindGroup, [0]);
3744
4257
  passEncoder.setPipeline(pipelines.writeSortedBvhLeaves);
3745
- passEncoder.dispatchWorkgroups(Math.ceil(config.triangleCount / WORKGROUP_SIZE));
4258
+ const leafWriteWorkgroups = Math.ceil(config.triangleCount / WORKGROUP_SIZE);
4259
+ passEncoder.dispatchWorkgroups(leafWriteWorkgroups);
4260
+ recordDirectDispatch(parallelism, [leafWriteWorkgroups]);
3746
4261
  passEncoder.setPipeline(pipelines.buildBvhInternalLevel);
3747
4262
  for (let levelIndex = 0; levelIndex < config.bvhBuildLevels.length; levelIndex += 1) {
3748
4263
  const buildLevel = config.bvhBuildLevels[levelIndex];
3749
4264
  passEncoder.setBindGroup(0, bvhBuildBindGroup, [
3750
4265
  (buildLevelConfigStart + levelIndex) * configBufferStride
3751
4266
  ]);
3752
- passEncoder.dispatchWorkgroups(Math.ceil(buildLevel.count / WORKGROUP_SIZE));
4267
+ const levelWorkgroups = Math.ceil(buildLevel.count / WORKGROUP_SIZE);
4268
+ passEncoder.dispatchWorkgroups(levelWorkgroups);
4269
+ recordDirectDispatch(parallelism, [levelWorkgroups]);
3753
4270
  }
3754
4271
  passEncoder.end();
3755
4272
  device.queue.submit([encoder.finish()]);
@@ -3757,7 +4274,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3757
4274
  accelerationBuildCount += 1;
3758
4275
  return true;
3759
4276
  }
3760
- function encodeTileSample(encoder, tile, configOffset) {
4277
+ function encodeTileSample(encoder, tile, configOffset, parallelism) {
3761
4278
  const generatePass = encoder.beginComputePass({
3762
4279
  label: "plasius.wavefront.generatePrimaryRaysPass"
3763
4280
  });
@@ -3765,6 +4282,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3765
4282
  generatePass.setBindGroup(0, bindGroups[0], [configOffset]);
3766
4283
  generatePass.setPipeline(pipelines.generatePrimaryRays);
3767
4284
  generatePass.dispatchWorkgroups(tileWorkgroups);
4285
+ recordDirectDispatch(parallelism, [tileWorkgroups]);
3768
4286
  generatePass.end();
3769
4287
  for (let bounceIndex = 0; bounceIndex < config.maxDepth; bounceIndex += 1) {
3770
4288
  encoder.copyBufferToBuffer(
@@ -3780,14 +4298,17 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3780
4298
  passEncoder.setBindGroup(0, bindGroups[bounceIndex % 2], [configOffset]);
3781
4299
  passEncoder.setPipeline(pipelines.intersectActiveQueue);
3782
4300
  passEncoder.dispatchWorkgroupsIndirect(activeDispatchBuffer, 0);
4301
+ recordIndirectDispatch(parallelism, tileWorkgroups);
3783
4302
  passEncoder.setPipeline(pipelines.resolveSurfaceRecords);
3784
4303
  passEncoder.dispatchWorkgroupsIndirect(activeDispatchBuffer, 0);
4304
+ recordIndirectDispatch(parallelism, tileWorkgroups);
3785
4305
  passEncoder.setPipeline(pipelines.compactAndSwapQueues);
3786
4306
  passEncoder.dispatchWorkgroups(1);
4307
+ recordDirectDispatch(parallelism, [1], 1);
3787
4308
  passEncoder.end();
3788
4309
  }
3789
4310
  }
3790
- function encodeTileOutput(encoder, tile, configOffset) {
4311
+ function encodeTileOutput(encoder, tile, configOffset, parallelism) {
3791
4312
  const passEncoder = encoder.beginComputePass({
3792
4313
  label: "plasius.wavefront.outputPass"
3793
4314
  });
@@ -3795,25 +4316,30 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3795
4316
  passEncoder.setBindGroup(0, bindGroups[0], [configOffset]);
3796
4317
  passEncoder.setPipeline(pipelines.accumulateTerminalRadiance);
3797
4318
  passEncoder.dispatchWorkgroups(tileWorkgroups);
4319
+ recordDirectDispatch(parallelism, [tileWorkgroups]);
3798
4320
  passEncoder.end();
3799
4321
  }
3800
- function encodeDenoise(encoder, configOffset) {
4322
+ function encodeDenoise(encoder, configOffset, parallelism) {
3801
4323
  if (!config.denoise) {
3802
4324
  return;
3803
4325
  }
4326
+ const denoiseWorkgroupsX = Math.ceil(config.width / 8);
4327
+ const denoiseWorkgroupsY = Math.ceil(config.height / 8);
3804
4328
  const radiancePass = encoder.beginComputePass({
3805
4329
  label: "plasius.wavefront.denoiseRadiancePass"
3806
4330
  });
3807
4331
  radiancePass.setBindGroup(0, denoiseRadianceBindGroup, [configOffset]);
3808
4332
  radiancePass.setPipeline(pipelines.denoiseLinearRadiance);
3809
- radiancePass.dispatchWorkgroups(Math.ceil(config.width / 8), Math.ceil(config.height / 8));
4333
+ radiancePass.dispatchWorkgroups(denoiseWorkgroupsX, denoiseWorkgroupsY);
4334
+ recordDirectDispatch(parallelism, [denoiseWorkgroupsX, denoiseWorkgroupsY]);
3810
4335
  radiancePass.end();
3811
4336
  const resolvePass = encoder.beginComputePass({
3812
4337
  label: "plasius.wavefront.denoiseResolvePass"
3813
4338
  });
3814
4339
  resolvePass.setBindGroup(0, denoiseResolveBindGroup, [configOffset]);
3815
4340
  resolvePass.setPipeline(pipelines.resolveDenoisedOutputImage);
3816
- resolvePass.dispatchWorkgroups(Math.ceil(config.width / 8), Math.ceil(config.height / 8));
4341
+ resolvePass.dispatchWorkgroups(denoiseWorkgroupsX, denoiseWorkgroupsY);
4342
+ recordDirectDispatch(parallelism, [denoiseWorkgroupsX, denoiseWorkgroupsY]);
3817
4343
  resolvePass.end();
3818
4344
  }
3819
4345
  function encodePresent(encoder) {
@@ -3834,7 +4360,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3834
4360
  passEncoder.draw(3);
3835
4361
  passEncoder.end();
3836
4362
  }
3837
- function dispatchFrame(frameIndex) {
4363
+ function dispatchFrame(frameIndex, parallelism) {
3838
4364
  const writeFrameConfig = createFrameConfigWriter(frameIndex);
3839
4365
  let submissionCount = 0;
3840
4366
  let encodedFramePasses = 0;
@@ -3865,20 +4391,25 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3865
4391
  sampleIndex,
3866
4392
  sampleWeight: 1 / config.samplesPerPixel
3867
4393
  });
3868
- encodeTileSample(reserveEncoder(), tile, configOffset);
4394
+ encodeTileSample(reserveEncoder(), tile, configOffset, parallelism);
4395
+ if (config.deferredPathResolve) {
4396
+ encodeTileOutput(reserveEncoder(), tile, configOffset, parallelism);
4397
+ }
4398
+ }
4399
+ if (!config.deferredPathResolve) {
4400
+ const outputConfigOffset = writeFrameConfig(tile, {
4401
+ sampleIndex: 0,
4402
+ sampleWeight: 1 / config.samplesPerPixel
4403
+ });
4404
+ encodeTileOutput(reserveEncoder(), tile, outputConfigOffset, parallelism);
3869
4405
  }
3870
- const outputConfigOffset = writeFrameConfig(tile, {
3871
- sampleIndex: 0,
3872
- sampleWeight: 1 / config.samplesPerPixel
3873
- });
3874
- encodeTileOutput(reserveEncoder(), tile, outputConfigOffset);
3875
4406
  }
3876
4407
  if (config.denoise) {
3877
4408
  const denoiseConfigOffset = writeFrameConfig(
3878
4409
  { x: 0, y: 0, width: config.width, height: config.height },
3879
4410
  { sampleIndex: 0, sampleWeight: 1 / config.samplesPerPixel }
3880
4411
  );
3881
- encodeDenoise(reserveEncoder(), denoiseConfigOffset);
4412
+ encodeDenoise(reserveEncoder(), denoiseConfigOffset, parallelism);
3882
4413
  }
3883
4414
  encodePresent(reserveEncoder());
3884
4415
  submitCurrentEncoder();
@@ -3887,8 +4418,10 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3887
4418
  function renderOnce() {
3888
4419
  frame += 1;
3889
4420
  const frameIndex = frame + config.frameIndex;
3890
- const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex);
3891
- const frameSubmissionCount = dispatchFrame(frameIndex);
4421
+ const parallelismCounters = createGpuParallelismCounters();
4422
+ const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex, parallelismCounters);
4423
+ const frameSubmissionCount = dispatchFrame(frameIndex, parallelismCounters);
4424
+ lastGpuParallelism = createGpuParallelismDiagnostics(gpuAdapterParallelism, parallelismCounters);
3892
4425
  return Object.freeze({
3893
4426
  frame,
3894
4427
  width: config.width,
@@ -3905,6 +4438,8 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3905
4438
  emissiveTriangleCount: config.emissiveTriangleCount,
3906
4439
  environmentPortalCount: config.environmentPortalCount,
3907
4440
  environmentPortalMode: config.environmentPortalMode,
4441
+ environmentMap: createEnvironmentMapSnapshot(config.environmentMap),
4442
+ deferredPathResolve: config.deferredPathResolve,
3908
4443
  bvhNodeCount: config.bvhNodeCount,
3909
4444
  displayQuality: config.displayQuality,
3910
4445
  accelerationBuildMode: config.accelerationBuildMode,
@@ -3914,6 +4449,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3914
4449
  accelerationBuildCount,
3915
4450
  commandSubmissions: frameSubmissionCount + (accelerationBuildSubmitted ? 1 : 0),
3916
4451
  frameConfigSlots: frameConfigSlotCount,
4452
+ gpuParallelism: lastGpuParallelism,
3917
4453
  memory: config.memory
3918
4454
  });
3919
4455
  }
@@ -4022,6 +4558,8 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4022
4558
  emissiveTriangleCount: config.emissiveTriangleCount,
4023
4559
  environmentPortalCount: config.environmentPortalCount,
4024
4560
  environmentPortalMode: config.environmentPortalMode,
4561
+ environmentMap: createEnvironmentMapSnapshot(config.environmentMap),
4562
+ deferredPathResolve: config.deferredPathResolve,
4025
4563
  bvhNodeCount: config.bvhNodeCount,
4026
4564
  displayQuality: config.displayQuality,
4027
4565
  accelerationBuildMode: config.accelerationBuildMode,
@@ -4029,6 +4567,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4029
4567
  accelerationBuilt,
4030
4568
  accelerationBuildCount,
4031
4569
  frameConfigSlots: frameConfigSlotCount,
4570
+ gpuParallelism: lastGpuParallelism,
4032
4571
  memory: config.memory
4033
4572
  });
4034
4573
  }
@@ -4037,6 +4576,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4037
4576
  nextQueue.destroy?.();
4038
4577
  hitBuffer.destroy?.();
4039
4578
  accumulationBuffer.destroy?.();
4579
+ pathVertexBuffer.destroy?.();
4040
4580
  sceneObjectBuffer.destroy?.();
4041
4581
  triangleBuffer.destroy?.();
4042
4582
  bvhNodeBuffer.destroy?.();
@@ -4052,6 +4592,9 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4052
4592
  radianceTexture.destroy?.();
4053
4593
  denoiseScratchTexture.destroy?.();
4054
4594
  outputTexture.destroy?.();
4595
+ if (environmentMapResource.ownsTexture) {
4596
+ environmentMapResource.texture?.destroy?.();
4597
+ }
4055
4598
  context.unconfigure?.();
4056
4599
  }
4057
4600
  return Object.freeze({