@plasius/gpu-renderer 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -76,6 +76,7 @@ var DEFAULT_HEIGHT = 720;
76
76
  var DEFAULT_MAX_DEPTH = 6;
77
77
  var DEFAULT_TILE_SIZE = 128;
78
78
  var DEFAULT_SAMPLES_PER_PIXEL = 1;
79
+ var DEFAULT_MAX_FRAME_PASSES_PER_SUBMISSION = 256;
79
80
  var DEFAULT_SCENE_OBJECT_CAPACITY = 128;
80
81
  var DEFAULT_ENVIRONMENT_PORTAL_CAPACITY = 32;
81
82
  var WORKGROUP_SIZE = 64;
@@ -93,11 +94,12 @@ var BVH_LEAF_REF_RECORD_BYTES = 16;
93
94
  var EMISSIVE_TRIANGLE_INDEX_BYTES = 4;
94
95
  var ENVIRONMENT_PORTAL_RECORD_BYTES = 96;
95
96
  var ACCUMULATION_RECORD_BYTES = 16;
96
- var CONFIG_BUFFER_BYTES = 272;
97
+ var PATH_VERTEX_RECORD_BYTES = 16;
98
+ var CONFIG_BUFFER_BYTES = 304;
97
99
  var COUNTER_DISPATCH_ARGS_OFFSET = 16;
98
100
  var INDIRECT_DISPATCH_ARGS_BYTES = 12;
99
101
  var COUNTER_BUFFER_BYTES = 32;
100
- var TRACE_STORAGE_BUFFER_BINDINGS = 9;
102
+ var TRACE_STORAGE_BUFFER_BINDINGS = 10;
101
103
  var MATERIAL_DIFFUSE = 0;
102
104
  var MATERIAL_METAL = 1;
103
105
  var MATERIAL_DIELECTRIC = 2;
@@ -120,7 +122,8 @@ var DEFAULT_ENVIRONMENT_LIGHTING = Object.freeze({
120
122
  sunColor: Object.freeze([2.8, 2.65, 2.35, 1]),
121
123
  intensity: 1,
122
124
  mode: 0,
123
- exposure: 1
125
+ exposure: 1,
126
+ sunlitBaseline: 0.16
124
127
  });
125
128
  var wavefrontPathTracingComputeLimits = Object.freeze({
126
129
  workgroupSize: WORKGROUP_SIZE,
@@ -137,6 +140,7 @@ var wavefrontPathTracingComputeLimits = Object.freeze({
137
140
  emissiveTriangleMetadataRecordBytes: BVH_NODE_RECORD_BYTES,
138
141
  environmentPortalRecordBytes: ENVIRONMENT_PORTAL_RECORD_BYTES,
139
142
  accumulationRecordBytes: ACCUMULATION_RECORD_BYTES,
143
+ pathVertexRecordBytes: PATH_VERTEX_RECORD_BYTES,
140
144
  counterRecordBytes: COUNTER_BUFFER_BYTES,
141
145
  indirectDispatchRecordBytes: INDIRECT_DISPATCH_ARGS_BYTES
142
146
  });
@@ -213,6 +217,33 @@ function asColor(value, fallback = [1, 1, 1, 1]) {
213
217
  clamp(readFiniteNumber("color[3]", value[3], fallback[3] ?? 1), 0, 1)
214
218
  ];
215
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
+ }
216
247
  function emissionPower(emission) {
217
248
  return Math.max(0, emission?.[0] ?? 0) + Math.max(0, emission?.[1] ?? 0) + Math.max(0, emission?.[2] ?? 0);
218
249
  }
@@ -860,7 +891,15 @@ function resolveEnvironmentLighting(input, environmentColor, ambientColor) {
860
891
  sunColor: Object.freeze(asColor(source.sunColor, DEFAULT_ENVIRONMENT_LIGHTING.sunColor)),
861
892
  intensity: Math.max(1e-4, readFiniteNumber("environmentLighting.intensity", source.intensity, DEFAULT_ENVIRONMENT_LIGHTING.intensity)),
862
893
  mode: readNonNegativeInteger("environmentLighting.mode", source.mode, DEFAULT_ENVIRONMENT_LIGHTING.mode),
863
- 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
+ )
864
903
  });
865
904
  }
866
905
  function evaluateReferenceEnvironmentRadiance(config, origin, direction) {
@@ -1045,6 +1084,11 @@ function estimateWavefrontPathTracingMemory(options = {}) {
1045
1084
  options.tilePixelCapacity,
1046
1085
  DEFAULT_TILE_SIZE * DEFAULT_TILE_SIZE
1047
1086
  );
1087
+ const maxDepth = clamp(
1088
+ readPositiveInteger("maxDepth", options.maxDepth, DEFAULT_MAX_DEPTH),
1089
+ 1,
1090
+ 16
1091
+ );
1048
1092
  const sceneObjectCapacity = readPositiveInteger(
1049
1093
  "sceneObjectCapacity",
1050
1094
  options.sceneObjectCapacity,
@@ -1070,6 +1114,7 @@ function estimateWavefrontPathTracingMemory(options = {}) {
1070
1114
  const queueBytes = tilePixelCapacity * RAY_RECORD_BYTES;
1071
1115
  const hitBytes = tilePixelCapacity * HIT_RECORD_BYTES;
1072
1116
  const accumulationBytes = tilePixelCapacity * ACCUMULATION_RECORD_BYTES;
1117
+ const pathVertexBytes = tilePixelCapacity * (maxDepth + 1) * PATH_VERTEX_RECORD_BYTES;
1073
1118
  const sceneObjectBytes = sceneObjectCapacity * SCENE_OBJECT_RECORD_BYTES;
1074
1119
  const triangleBytes = triangleCapacity * TRIANGLE_RECORD_BYTES;
1075
1120
  const bvhNodeBytes = bvhNodeCapacity * BVH_NODE_RECORD_BYTES;
@@ -1081,6 +1126,7 @@ function estimateWavefrontPathTracingMemory(options = {}) {
1081
1126
  queuePairBytes: queueBytes * 2,
1082
1127
  hitBytes,
1083
1128
  accumulationBytes,
1129
+ pathVertexBytes,
1084
1130
  sceneObjectBytes,
1085
1131
  triangleBytes,
1086
1132
  bvhNodeBytes,
@@ -1090,7 +1136,7 @@ function estimateWavefrontPathTracingMemory(options = {}) {
1090
1136
  configBytes: CONFIG_BUFFER_BYTES,
1091
1137
  counterBytes: COUNTER_BUFFER_BYTES,
1092
1138
  indirectDispatchBytes: INDIRECT_DISPATCH_ARGS_BYTES,
1093
- 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
1094
1140
  });
1095
1141
  }
1096
1142
  function createWavefrontPathTracingComputeConfig(options = {}) {
@@ -1106,6 +1152,15 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
1106
1152
  1,
1107
1153
  64
1108
1154
  );
1155
+ const maxFramePassesPerSubmission = clamp(
1156
+ readPositiveInteger(
1157
+ "maxFramePassesPerSubmission",
1158
+ options.maxFramePassesPerSubmission,
1159
+ DEFAULT_MAX_FRAME_PASSES_PER_SUBMISSION
1160
+ ),
1161
+ 1,
1162
+ 4096
1163
+ );
1109
1164
  const tilePixelCapacity = readPositiveInteger(
1110
1165
  "tilePixelCapacity",
1111
1166
  options.tilePixelCapacity,
@@ -1166,6 +1221,10 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
1166
1221
  options.environmentPortalMode ?? options.portalMode ?? options.environmentLighting?.environmentPortalMode,
1167
1222
  environmentPortals.length > 0
1168
1223
  );
1224
+ const environmentMap = resolveEnvironmentMap(
1225
+ options.environmentMap ?? options.environmentTexture ?? options.environmentLighting?.environmentMap
1226
+ );
1227
+ const deferredPathResolve = resolveDeferredPathResolve(options);
1169
1228
  return Object.freeze({
1170
1229
  mode: rendererWavefrontComputeMode,
1171
1230
  width,
@@ -1173,6 +1232,7 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
1173
1232
  maxDepth,
1174
1233
  tileSize,
1175
1234
  samplesPerPixel,
1235
+ maxFramePassesPerSubmission,
1176
1236
  tilePixelCapacity,
1177
1237
  sceneObjects,
1178
1238
  sceneObjectCount: sceneObjects.length,
@@ -1199,12 +1259,15 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
1199
1259
  environmentPortalCount: environmentPortals.length,
1200
1260
  environmentPortalCapacity,
1201
1261
  environmentPortalMode,
1262
+ environmentMap,
1263
+ deferredPathResolve,
1202
1264
  displayQuality: options.displayQuality === true,
1203
1265
  requiresMeshBvhForDisplayQuality: true,
1204
1266
  denoise: options.denoise !== false,
1205
1267
  frameIndex: readNonNegativeInteger("frameIndex", options.frameIndex, 0),
1206
1268
  memory: estimateWavefrontPathTracingMemory({
1207
1269
  tilePixelCapacity,
1270
+ maxDepth,
1208
1271
  sceneObjectCapacity,
1209
1272
  triangleCapacity,
1210
1273
  bvhNodeCapacity,
@@ -1401,6 +1464,18 @@ function createConfigPayload(config, tile, frameIndex, buildRange = {}) {
1401
1464
  data.setUint32(260, config.environmentPortalMode ?? 0, true);
1402
1465
  data.setUint32(264, 0, true);
1403
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
+ ]);
1404
1479
  return bytes;
1405
1480
  }
1406
1481
  function createTiles(width, height, tileSize) {
@@ -1646,6 +1721,136 @@ function alignTo(value, alignment) {
1646
1721
  const resolvedAlignment = Math.max(1, alignment);
1647
1722
  return Math.ceil(value / resolvedAlignment) * resolvedAlignment;
1648
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
+ }
1649
1854
  async function getPipelineDiagnostics(shaderModule) {
1650
1855
  if (typeof shaderModule?.compilationInfo !== "function") {
1651
1856
  return "";
@@ -1854,6 +2059,8 @@ struct FrameConfig {
1854
2059
  environmentPortalMode: u32,
1855
2060
  _portalPad0: u32,
1856
2061
  _portalPad1: u32,
2062
+ environmentMapSettings: vec4<f32>,
2063
+ pathResolveSettings: vec4<f32>,
1857
2064
  };
1858
2065
 
1859
2066
  struct Counters {
@@ -1913,6 +2120,9 @@ struct EnvironmentPortal {
1913
2120
  @group(0) @binding(17) var finalDenoiseInputRadiance: texture_2d<f32>;
1914
2121
  @group(0) @binding(18) var denoisedOutputImage: texture_storage_2d<rgba8unorm, write>;
1915
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>>;
1916
2126
 
1917
2127
  fn hash_u32(value: u32) -> u32 {
1918
2128
  var x = value;
@@ -1957,6 +2167,89 @@ fn max_component(value: vec3<f32>) -> f32 {
1957
2167
  return max(max(value.x, value.y), value.z);
1958
2168
  }
1959
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
+
1960
2253
  fn environment_portal_radiance_scale(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
1961
2254
  if (config.environmentPortalCount == 0u || config.environmentPortalMode == 0u) {
1962
2255
  return vec3<f32>(1.0);
@@ -1996,22 +2289,9 @@ fn environment_portal_radiance_scale(origin: vec3<f32>, direction: vec3<f32>) ->
1996
2289
 
1997
2290
  fn environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
1998
2291
  let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
1999
- let upFactor = saturate(rayDirection.y * 0.5 + 0.5);
2000
- let sunDirection = safe_normalize(
2001
- config.environmentSunDirectionIntensity.xyz,
2002
- vec3<f32>(0.0, 1.0, 0.0)
2003
- );
2004
- let sunGlow = pow(saturate(dot(rayDirection, sunDirection)), 192.0);
2005
- let gradient =
2006
- config.environmentHorizonColor.xyz * (1.0 - upFactor) +
2007
- config.environmentZenithColor.xyz * upFactor;
2008
2292
  let portalScale = environment_portal_radiance_scale(origin, rayDirection);
2009
2293
  let portalHit = max_component(portalScale) > 0.0001;
2010
- return (
2011
- gradient +
2012
- config.environmentSunColor.xyz * sunGlow
2013
- ) *
2014
- max(config.environmentSunDirectionIntensity.w, 0.0001) *
2294
+ return base_environment_radiance(rayDirection) *
2015
2295
  select(vec3<f32>(1.0), portalScale, portalHit);
2016
2296
  }
2017
2297
 
@@ -2027,16 +2307,143 @@ fn gated_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f
2027
2307
  return environment_radiance(origin, direction);
2028
2308
  }
2029
2309
 
2030
- 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> {
2031
2335
  let normal = safe_normalize(hit.shadingNormal.xyz, vec3<f32>(0.0, 1.0, 0.0));
2032
- let surfaceColor = max(hit.color.xyz, config.ambientColor.xyz);
2033
2336
  let normalEnvironment = gated_environment_radiance(
2034
2337
  hit.position.xyz + normal * 0.003,
2035
2338
  normal
2036
2339
  );
2037
- 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));
2038
2352
  let materialFloor = select(0.7, 1.0, hit.materialKind == 0u || hit.materialKind == 3u);
2039
- 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
+ );
2363
+ }
2364
+
2365
+ fn direct_environment_portal_irradiance(origin: vec3<f32>, normal: vec3<f32>) -> vec3<f32> {
2366
+ if (config.environmentPortalCount == 0u || config.environmentPortalMode == 0u) {
2367
+ return vec3<f32>(0.0);
2368
+ }
2369
+
2370
+ var irradiance = vec3<f32>(0.0);
2371
+ for (var portalIndex = 0u; portalIndex < config.environmentPortalCount; portalIndex = portalIndex + 1u) {
2372
+ let portal = environmentPortals[portalIndex];
2373
+ if (portal.kind != 1u) {
2374
+ continue;
2375
+ }
2376
+
2377
+ let toPortal = portal.position.xyz - origin;
2378
+ let distanceSquared = max(dot(toPortal, toPortal), 0.01);
2379
+ let direction = safe_normalize(toPortal, normal);
2380
+ let surfaceFacing = saturate(dot(normal, direction));
2381
+ if (surfaceFacing <= 0.0001) {
2382
+ continue;
2383
+ }
2384
+
2385
+ let portalNormal = safe_normalize(portal.normal.xyz, vec3<f32>(0.0, 0.0, 1.0));
2386
+ let twoSided = (portal.flags & 1u) != 0u;
2387
+ let portalFacing = select(
2388
+ saturate(dot(-direction, portalNormal)),
2389
+ max(abs(dot(direction, portalNormal)), 0.15),
2390
+ twoSided
2391
+ );
2392
+ let area = max(portal.position.w, 0.0001);
2393
+ let distanceFalloff = clamp(area / max(distanceSquared, area * 0.25), 0.0, 2.5);
2394
+ irradiance = irradiance +
2395
+ portal.color.rgb *
2396
+ portal.normal.w *
2397
+ portal.color.a *
2398
+ surfaceFacing *
2399
+ portalFacing *
2400
+ distanceFalloff;
2401
+ }
2402
+ return irradiance;
2403
+ }
2404
+
2405
+ fn surface_direct_environment_contribution(ray: RayRecord, hit: HitRecord) -> vec3<f32> {
2406
+ let normal = safe_normalize(hit.shadingNormal.xyz, vec3<f32>(0.0, 1.0, 0.0));
2407
+ let origin = hit.position.xyz + normal * 0.003;
2408
+ let viewDirection = safe_normalize(-ray.direction.xyz, normal);
2409
+ let surfaceColor = clamp(max(hit.color.xyz, config.ambientColor.xyz * 0.35), vec3<f32>(0.0), vec3<f32>(1.0));
2410
+ let roughness = clamp(hit.material.x, 0.0, 1.0);
2411
+ let metallic = clamp(hit.material.y, 0.0, 1.0);
2412
+
2413
+ let normalEnvironment = gated_environment_radiance(origin, normal);
2414
+ let skyVisibility = 0.35 + saturate(normal.y * 0.5 + 0.5) * 0.45;
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);
2426
+
2427
+ let sunDirection = safe_normalize(
2428
+ config.environmentSunDirectionIntensity.xyz,
2429
+ vec3<f32>(0.0, 1.0, 0.0)
2430
+ );
2431
+ let sunFacing = saturate(dot(normal, sunDirection));
2432
+ let sunRadiance = gated_environment_radiance(origin, sunDirection);
2433
+ let sunIrradiance = sunRadiance * sunFacing * 0.2;
2434
+ let portalIrradiance = direct_environment_portal_irradiance(origin, normal);
2435
+
2436
+ let diffuseWeight = select(1.0 - metallic * 0.65, 0.22, hit.materialKind == 1u);
2437
+ let diffuse = surfaceColor * (skyIrradiance + sunIrradiance + portalIrradiance) * diffuseWeight;
2438
+
2439
+ let halfVector = safe_normalize(sunDirection + viewDirection, normal);
2440
+ let specularPower = 8.0 + (1.0 - roughness) * 96.0;
2441
+ let specularFacing = pow(saturate(dot(normal, halfVector)), specularPower) * sunFacing;
2442
+ let specularTint = mix(vec3<f32>(0.04), surfaceColor, metallic);
2443
+ let specular = specularTint * sunRadiance * specularFacing * select(0.16, 0.48, hit.materialKind == 1u || hit.materialKind == 2u);
2444
+
2445
+ let bounceWeight = select(1.0, 0.38, ray.bounce > 0u);
2446
+ return clamp_sample_radiance(ray.throughput.xyz * (diffuse + specular) * bounceWeight);
2040
2447
  }
2041
2448
 
2042
2449
  fn default_mesh_range() -> MeshRange {
@@ -2639,6 +3046,7 @@ fn generatePrimaryRays(@builtin(global_invocation_id) globalId: vec3<u32>) {
2639
3046
  return;
2640
3047
  }
2641
3048
  activeQueue[index] = make_ray(index);
3049
+ clear_deferred_path(index);
2642
3050
  if (u32(config.projectionAndSampling.w) == 0u) {
2643
3051
  accumulation[index] = vec4<f32>(0.0);
2644
3052
  }
@@ -2891,27 +3299,51 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
2891
3299
 
2892
3300
  if (hit.hitType == 1u) {
2893
3301
  let guidedLightWeight = select(1.0, 0.24, (ray.flags & RAY_FLAG_GUIDED_EMISSIVE) != 0u);
2894
- contribution = clamp_sample_radiance(
2895
- ray.throughput.xyz * max(hit.emission.xyz, hit.color.xyz) * guidedLightWeight
2896
- );
2897
- accumulation[ray.rayId] =
2898
- 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
+ }
2899
3310
  atomicAdd(&counters.terminatedCount, 1u);
2900
3311
  return;
2901
3312
  }
2902
3313
 
2903
3314
  if (hit.hitType == 2u) {
2904
- contribution = clamp_sample_radiance(ray.throughput.xyz * max(hit.color.xyz, config.ambientColor.xyz));
2905
- accumulation[ray.rayId] =
2906
- 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
+ }
2907
3322
  atomicAdd(&counters.terminatedCount, 1u);
2908
3323
  return;
2909
3324
  }
2910
3325
 
2911
- if (ray.bounce + 1u >= config.maxDepth) {
2912
- let terminalEnvironment = terminal_surface_environment_contribution(ray, hit);
3326
+ let response = surface_path_response(hit);
3327
+ record_deferred_path_response(ray, response);
3328
+
3329
+ let shouldEstimateDirectEnvironment =
3330
+ !deferred_path_resolve_enabled() &&
3331
+ (hit.materialKind == 0u || hit.materialKind == 1u) &&
3332
+ hit.material.z >= 0.95;
3333
+ if (shouldEstimateDirectEnvironment) {
3334
+ let directEnvironment = surface_direct_environment_contribution(ray, hit);
2913
3335
  accumulation[ray.rayId] =
2914
- accumulation[ray.rayId] + vec4<f32>(terminalEnvironment * sample_weight(), 1.0);
3336
+ accumulation[ray.rayId] + vec4<f32>(directEnvironment * sample_weight(), 0.0);
3337
+ }
3338
+
3339
+ if (ray.bounce + 1u >= config.maxDepth) {
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
+ }
2915
3347
  atomicAdd(&counters.terminatedCount, 1u);
2916
3348
  return;
2917
3349
  }
@@ -2920,17 +3352,17 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
2920
3352
  let scatter = scatter_direction(ray, hit, seed);
2921
3353
  let nextIndex = atomicAdd(&counters.nextCount, 1u);
2922
3354
  if (nextIndex >= config.tilePixelCount) {
2923
- let overflowEnvironment = terminal_surface_environment_contribution(ray, hit);
2924
- accumulation[ray.rayId] =
2925
- 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
+ }
2926
3362
  atomicAdd(&counters.terminatedCount, 1u);
2927
3363
  return;
2928
3364
  }
2929
- let color = clamp(hit.color.xyz, vec3<f32>(0.0), vec3<f32>(1.0));
2930
- let opacity = clamp(hit.material.z, 0.0, 1.0);
2931
- let materialEnergy = select(0.68, 0.92, hit.materialKind == 1u || hit.materialKind == 2u);
2932
- let transparentEnergy = select(materialEnergy, 0.9, hit.hitType == 3u);
2933
- let throughput = ray.throughput.xyz * mix(vec3<f32>(1.0), color, max(opacity, 0.18)) * transparentEnergy;
3365
+ let throughput = ray.throughput.xyz * response;
2934
3366
  nextQueue[nextIndex] = RayRecord(
2935
3367
  ray.rayId,
2936
3368
  ray.rayId,
@@ -2958,6 +3390,27 @@ fn compactAndSwapQueues(@builtin(global_invocation_id) globalId: vec3<u32>) {
2958
3390
  write_active_dispatch_args(activeCount);
2959
3391
  }
2960
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
+
2961
3414
  @compute @workgroup_size(64)
2962
3415
  fn accumulateTerminalRadiance(@builtin(global_invocation_id) globalId: vec3<u32>) {
2963
3416
  let index = globalId.x;
@@ -2967,7 +3420,12 @@ fn accumulateTerminalRadiance(@builtin(global_invocation_id) globalId: vec3<u32>
2967
3420
  let localX = index % config.tileWidth;
2968
3421
  let localY = index / config.tileWidth;
2969
3422
  let pixel = vec2<i32>(i32(config.tileX + localX), i32(config.tileY + localY));
2970
- 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
+ }
2971
3429
 
2972
3430
  textureStore(radianceImage, pixel, vec4<f32>(radiance, 1.0));
2973
3431
  if (config.denoise == 0u) {
@@ -3105,6 +3563,125 @@ function createWavefrontDeviceDescriptor(adapter, options = {}) {
3105
3563
  }
3106
3564
  return Object.keys(descriptor).length > 0 ? descriptor : void 0;
3107
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
+ }
3108
3685
  async function createWavefrontPathTracingComputeRenderer(options = {}) {
3109
3686
  assertAnalyticDisplayQualityPolicy(options);
3110
3687
  const constants = getGpuUsageConstants();
@@ -3124,6 +3701,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3124
3701
  throw new Error("Unable to acquire a WebGPU adapter for wavefront path tracing.");
3125
3702
  }
3126
3703
  const device = await adapter.requestDevice(createWavefrontDeviceDescriptor(adapter, options));
3704
+ const gpuAdapterParallelism = createGpuAdapterParallelismDiagnostics(adapter, device);
3127
3705
  const context = canvas.getContext("webgpu");
3128
3706
  if (!context || typeof context.configure !== "function") {
3129
3707
  throw new Error("Canvas WebGPU context does not support configure().");
@@ -3151,6 +3729,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3151
3729
  const rayQueueBytes = config.tilePixelCapacity * RAY_RECORD_BYTES;
3152
3730
  const hitBytes = config.tilePixelCapacity * HIT_RECORD_BYTES;
3153
3731
  const accumulationBytes = config.tilePixelCapacity * ACCUMULATION_RECORD_BYTES;
3732
+ const pathVertexBytes = config.tilePixelCapacity * (config.maxDepth + 1) * PATH_VERTEX_RECORD_BYTES;
3154
3733
  const activeQueue = createBuffer(device, bufferUsage, rayQueueBytes, "plasius.wavefront.activeQueue");
3155
3734
  const nextQueue = createBuffer(device, bufferUsage, rayQueueBytes, "plasius.wavefront.nextQueue");
3156
3735
  const hitBuffer = createBuffer(device, bufferUsage, hitBytes, "plasius.wavefront.hitBuffer");
@@ -3160,6 +3739,12 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3160
3739
  accumulationBytes,
3161
3740
  "plasius.wavefront.accumulation"
3162
3741
  );
3742
+ const pathVertexBuffer = createBuffer(
3743
+ device,
3744
+ bufferUsage,
3745
+ pathVertexBytes,
3746
+ "plasius.wavefront.pathVertices"
3747
+ );
3163
3748
  const sceneObjectBuffer = createBuffer(
3164
3749
  device,
3165
3750
  constants.buffer.STORAGE | constants.buffer.COPY_DST,
@@ -3214,9 +3799,10 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3214
3799
  CONFIG_BUFFER_BYTES,
3215
3800
  Number.isFinite(uniformOffsetAlignment) && uniformOffsetAlignment > 0 ? uniformOffsetAlignment : CONFIG_BUFFER_BYTES
3216
3801
  );
3802
+ const outputConfigSlotCount = config.deferredPathResolve ? 0 : tiles.length;
3217
3803
  const frameConfigSlotCount = Math.max(
3218
3804
  1,
3219
- tiles.length * config.samplesPerPixel + tiles.length + (config.denoise ? 1 : 0)
3805
+ tiles.length * config.samplesPerPixel + outputConfigSlotCount + (config.denoise ? 1 : 0)
3220
3806
  );
3221
3807
  const configBuffer = createBuffer(
3222
3808
  device,
@@ -3294,6 +3880,12 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3294
3880
  magFilter: "nearest",
3295
3881
  minFilter: "nearest"
3296
3882
  });
3883
+ const environmentMapResource = createEnvironmentMapResource(
3884
+ device,
3885
+ constants,
3886
+ config.environmentMap,
3887
+ config.environmentColor
3888
+ );
3297
3889
  const traceBindGroupLayout = device.createBindGroupLayout({
3298
3890
  label: "plasius.wavefront.traceBindGroupLayout",
3299
3891
  entries: [
@@ -3320,7 +3912,10 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3320
3912
  visibility: constants.shader.COMPUTE,
3321
3913
  storageTexture: { access: "write-only", format: "rgba16float" }
3322
3914
  },
3323
- { 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" } }
3324
3919
  ]
3325
3920
  });
3326
3921
  const accelerationBindGroupLayout = device.createBindGroupLayout({
@@ -3494,7 +4089,10 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3494
4089
  { binding: 8, resource: { buffer: triangleBuffer } },
3495
4090
  { binding: 9, resource: { buffer: bvhNodeBuffer } },
3496
4091
  { binding: 16, resource: radianceView },
3497
- { 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 } }
3498
4096
  ]
3499
4097
  });
3500
4098
  }
@@ -3583,6 +4181,11 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3583
4181
  let frame = 0;
3584
4182
  let accelerationBuilt = !config.gpuAccelerationBuildRequired;
3585
4183
  let accelerationBuildCount = 0;
4184
+ let activeCameraOptions = options.camera ?? DEFAULT_CAMERA;
4185
+ let lastGpuParallelism = createGpuParallelismDiagnostics(
4186
+ gpuAdapterParallelism,
4187
+ createGpuParallelismCounters()
4188
+ );
3586
4189
  function createFrameConfigWriter(frameIndex) {
3587
4190
  let slot = 0;
3588
4191
  return (tile, buildRange = {}) => {
@@ -3599,7 +4202,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3599
4202
  return offset;
3600
4203
  };
3601
4204
  }
3602
- function dispatchGpuAccelerationBuild(frameIndex) {
4205
+ function dispatchGpuAccelerationBuild(frameIndex, parallelism) {
3603
4206
  if (!config.gpuAccelerationBuildRequired || accelerationBuilt) {
3604
4207
  return false;
3605
4208
  }
@@ -3638,24 +4241,32 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3638
4241
  });
3639
4242
  passEncoder.setBindGroup(0, bvhBuildBindGroup, [0]);
3640
4243
  passEncoder.setPipeline(pipelines.prepareMeshTrianglesAndLeaves);
3641
- 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]);
3642
4247
  passEncoder.setPipeline(pipelines.sortBvhLeafRefs);
3643
4248
  for (let stageIndex = 0; stageIndex < config.bvhSortStages.length; stageIndex += 1) {
3644
4249
  passEncoder.setBindGroup(0, bvhBuildBindGroup, [
3645
4250
  (stageIndex + 1) * configBufferStride
3646
4251
  ]);
3647
- 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]);
3648
4255
  }
3649
4256
  passEncoder.setBindGroup(0, bvhBuildBindGroup, [0]);
3650
4257
  passEncoder.setPipeline(pipelines.writeSortedBvhLeaves);
3651
- 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]);
3652
4261
  passEncoder.setPipeline(pipelines.buildBvhInternalLevel);
3653
4262
  for (let levelIndex = 0; levelIndex < config.bvhBuildLevels.length; levelIndex += 1) {
3654
4263
  const buildLevel = config.bvhBuildLevels[levelIndex];
3655
4264
  passEncoder.setBindGroup(0, bvhBuildBindGroup, [
3656
4265
  (buildLevelConfigStart + levelIndex) * configBufferStride
3657
4266
  ]);
3658
- 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]);
3659
4270
  }
3660
4271
  passEncoder.end();
3661
4272
  device.queue.submit([encoder.finish()]);
@@ -3663,7 +4274,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3663
4274
  accelerationBuildCount += 1;
3664
4275
  return true;
3665
4276
  }
3666
- function encodeTileSample(encoder, tile, configOffset) {
4277
+ function encodeTileSample(encoder, tile, configOffset, parallelism) {
3667
4278
  const generatePass = encoder.beginComputePass({
3668
4279
  label: "plasius.wavefront.generatePrimaryRaysPass"
3669
4280
  });
@@ -3671,6 +4282,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3671
4282
  generatePass.setBindGroup(0, bindGroups[0], [configOffset]);
3672
4283
  generatePass.setPipeline(pipelines.generatePrimaryRays);
3673
4284
  generatePass.dispatchWorkgroups(tileWorkgroups);
4285
+ recordDirectDispatch(parallelism, [tileWorkgroups]);
3674
4286
  generatePass.end();
3675
4287
  for (let bounceIndex = 0; bounceIndex < config.maxDepth; bounceIndex += 1) {
3676
4288
  encoder.copyBufferToBuffer(
@@ -3686,14 +4298,17 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3686
4298
  passEncoder.setBindGroup(0, bindGroups[bounceIndex % 2], [configOffset]);
3687
4299
  passEncoder.setPipeline(pipelines.intersectActiveQueue);
3688
4300
  passEncoder.dispatchWorkgroupsIndirect(activeDispatchBuffer, 0);
4301
+ recordIndirectDispatch(parallelism, tileWorkgroups);
3689
4302
  passEncoder.setPipeline(pipelines.resolveSurfaceRecords);
3690
4303
  passEncoder.dispatchWorkgroupsIndirect(activeDispatchBuffer, 0);
4304
+ recordIndirectDispatch(parallelism, tileWorkgroups);
3691
4305
  passEncoder.setPipeline(pipelines.compactAndSwapQueues);
3692
4306
  passEncoder.dispatchWorkgroups(1);
4307
+ recordDirectDispatch(parallelism, [1], 1);
3693
4308
  passEncoder.end();
3694
4309
  }
3695
4310
  }
3696
- function encodeTileOutput(encoder, tile, configOffset) {
4311
+ function encodeTileOutput(encoder, tile, configOffset, parallelism) {
3697
4312
  const passEncoder = encoder.beginComputePass({
3698
4313
  label: "plasius.wavefront.outputPass"
3699
4314
  });
@@ -3701,25 +4316,30 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3701
4316
  passEncoder.setBindGroup(0, bindGroups[0], [configOffset]);
3702
4317
  passEncoder.setPipeline(pipelines.accumulateTerminalRadiance);
3703
4318
  passEncoder.dispatchWorkgroups(tileWorkgroups);
4319
+ recordDirectDispatch(parallelism, [tileWorkgroups]);
3704
4320
  passEncoder.end();
3705
4321
  }
3706
- function encodeDenoise(encoder, configOffset) {
4322
+ function encodeDenoise(encoder, configOffset, parallelism) {
3707
4323
  if (!config.denoise) {
3708
4324
  return;
3709
4325
  }
4326
+ const denoiseWorkgroupsX = Math.ceil(config.width / 8);
4327
+ const denoiseWorkgroupsY = Math.ceil(config.height / 8);
3710
4328
  const radiancePass = encoder.beginComputePass({
3711
4329
  label: "plasius.wavefront.denoiseRadiancePass"
3712
4330
  });
3713
4331
  radiancePass.setBindGroup(0, denoiseRadianceBindGroup, [configOffset]);
3714
4332
  radiancePass.setPipeline(pipelines.denoiseLinearRadiance);
3715
- radiancePass.dispatchWorkgroups(Math.ceil(config.width / 8), Math.ceil(config.height / 8));
4333
+ radiancePass.dispatchWorkgroups(denoiseWorkgroupsX, denoiseWorkgroupsY);
4334
+ recordDirectDispatch(parallelism, [denoiseWorkgroupsX, denoiseWorkgroupsY]);
3716
4335
  radiancePass.end();
3717
4336
  const resolvePass = encoder.beginComputePass({
3718
4337
  label: "plasius.wavefront.denoiseResolvePass"
3719
4338
  });
3720
4339
  resolvePass.setBindGroup(0, denoiseResolveBindGroup, [configOffset]);
3721
4340
  resolvePass.setPipeline(pipelines.resolveDenoisedOutputImage);
3722
- resolvePass.dispatchWorkgroups(Math.ceil(config.width / 8), Math.ceil(config.height / 8));
4341
+ resolvePass.dispatchWorkgroups(denoiseWorkgroupsX, denoiseWorkgroupsY);
4342
+ recordDirectDispatch(parallelism, [denoiseWorkgroupsX, denoiseWorkgroupsY]);
3723
4343
  resolvePass.end();
3724
4344
  }
3725
4345
  function encodePresent(encoder) {
@@ -3740,41 +4360,68 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3740
4360
  passEncoder.draw(3);
3741
4361
  passEncoder.end();
3742
4362
  }
3743
- function dispatchFrame(frameIndex) {
4363
+ function dispatchFrame(frameIndex, parallelism) {
3744
4364
  const writeFrameConfig = createFrameConfigWriter(frameIndex);
3745
- const encoder = device.createCommandEncoder({
3746
- label: `plasius.wavefront.frame.${frameIndex}.batched`
4365
+ let submissionCount = 0;
4366
+ let encodedFramePasses = 0;
4367
+ let encoder = device.createCommandEncoder({
4368
+ label: `plasius.wavefront.frame.${frameIndex}.batched.${submissionCount + 1}`
3747
4369
  });
4370
+ function submitCurrentEncoder() {
4371
+ if (encodedFramePasses <= 0) {
4372
+ return;
4373
+ }
4374
+ device.queue.submit([encoder.finish()]);
4375
+ submissionCount += 1;
4376
+ encodedFramePasses = 0;
4377
+ encoder = device.createCommandEncoder({
4378
+ label: `plasius.wavefront.frame.${frameIndex}.batched.${submissionCount + 1}`
4379
+ });
4380
+ }
4381
+ function reserveEncoder(passCount = 1) {
4382
+ if (encodedFramePasses > 0 && encodedFramePasses + passCount > config.maxFramePassesPerSubmission) {
4383
+ submitCurrentEncoder();
4384
+ }
4385
+ encodedFramePasses += passCount;
4386
+ return encoder;
4387
+ }
3748
4388
  for (const tile of tiles) {
3749
4389
  for (let sampleIndex = 0; sampleIndex < config.samplesPerPixel; sampleIndex += 1) {
3750
4390
  const configOffset = writeFrameConfig(tile, {
3751
4391
  sampleIndex,
3752
4392
  sampleWeight: 1 / config.samplesPerPixel
3753
4393
  });
3754
- encodeTileSample(encoder, 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);
3755
4405
  }
3756
- const outputConfigOffset = writeFrameConfig(tile, {
3757
- sampleIndex: 0,
3758
- sampleWeight: 1 / config.samplesPerPixel
3759
- });
3760
- encodeTileOutput(encoder, tile, outputConfigOffset);
3761
4406
  }
3762
4407
  if (config.denoise) {
3763
4408
  const denoiseConfigOffset = writeFrameConfig(
3764
4409
  { x: 0, y: 0, width: config.width, height: config.height },
3765
4410
  { sampleIndex: 0, sampleWeight: 1 / config.samplesPerPixel }
3766
4411
  );
3767
- encodeDenoise(encoder, denoiseConfigOffset);
4412
+ encodeDenoise(reserveEncoder(), denoiseConfigOffset, parallelism);
3768
4413
  }
3769
- encodePresent(encoder);
3770
- device.queue.submit([encoder.finish()]);
3771
- return 1;
4414
+ encodePresent(reserveEncoder());
4415
+ submitCurrentEncoder();
4416
+ return submissionCount;
3772
4417
  }
3773
4418
  function renderOnce() {
3774
4419
  frame += 1;
3775
4420
  const frameIndex = frame + config.frameIndex;
3776
- const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex);
3777
- 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);
3778
4425
  return Object.freeze({
3779
4426
  frame,
3780
4427
  width: config.width,
@@ -3783,6 +4430,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3783
4430
  tiles: tiles.length,
3784
4431
  tileSize: config.tileSize,
3785
4432
  samplesPerPixel: config.samplesPerPixel,
4433
+ maxFramePassesPerSubmission: config.maxFramePassesPerSubmission,
3786
4434
  screenRays: config.width * config.height,
3787
4435
  primaryRays: config.width * config.height * config.samplesPerPixel,
3788
4436
  sceneObjectCount: config.sceneObjectCount,
@@ -3790,6 +4438,8 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3790
4438
  emissiveTriangleCount: config.emissiveTriangleCount,
3791
4439
  environmentPortalCount: config.environmentPortalCount,
3792
4440
  environmentPortalMode: config.environmentPortalMode,
4441
+ environmentMap: createEnvironmentMapSnapshot(config.environmentMap),
4442
+ deferredPathResolve: config.deferredPathResolve,
3793
4443
  bvhNodeCount: config.bvhNodeCount,
3794
4444
  displayQuality: config.displayQuality,
3795
4445
  accelerationBuildMode: config.accelerationBuildMode,
@@ -3799,6 +4449,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3799
4449
  accelerationBuildCount,
3800
4450
  commandSubmissions: frameSubmissionCount + (accelerationBuildSubmitted ? 1 : 0),
3801
4451
  frameConfigSlots: frameConfigSlotCount,
4452
+ gpuParallelism: lastGpuParallelism,
3802
4453
  memory: config.memory
3803
4454
  });
3804
4455
  }
@@ -3869,11 +4520,29 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3869
4520
  samplesPerPixel: config.samplesPerPixel,
3870
4521
  sceneObjectCapacity: config.sceneObjectCapacity,
3871
4522
  sceneObjects: packedScene.objects,
4523
+ camera: activeCameraOptions,
3872
4524
  frameIndex: config.frameIndex
3873
4525
  });
3874
4526
  device.queue.writeBuffer(sceneObjectBuffer, 0, packedScene.buffer);
3875
4527
  return config;
3876
4528
  }
4529
+ function updateCamera(cameraOptions = {}) {
4530
+ activeCameraOptions = cameraOptions;
4531
+ config = createWavefrontPathTracingComputeConfig({
4532
+ ...options,
4533
+ canvas,
4534
+ width: config.width,
4535
+ height: config.height,
4536
+ maxDepth: config.maxDepth,
4537
+ tileSize: config.tileSize,
4538
+ samplesPerPixel: config.samplesPerPixel,
4539
+ sceneObjectCapacity: config.sceneObjectCapacity,
4540
+ sceneObjects: packedScene.objects,
4541
+ camera: activeCameraOptions,
4542
+ frameIndex: config.frameIndex
4543
+ });
4544
+ return config;
4545
+ }
3877
4546
  function getSnapshot() {
3878
4547
  return Object.freeze({
3879
4548
  frame,
@@ -3883,11 +4552,14 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3883
4552
  tiles: tiles.length,
3884
4553
  tileSize: config.tileSize,
3885
4554
  samplesPerPixel: config.samplesPerPixel,
4555
+ maxFramePassesPerSubmission: config.maxFramePassesPerSubmission,
3886
4556
  sceneObjectCount: config.sceneObjectCount,
3887
4557
  triangleCount: config.triangleCount,
3888
4558
  emissiveTriangleCount: config.emissiveTriangleCount,
3889
4559
  environmentPortalCount: config.environmentPortalCount,
3890
4560
  environmentPortalMode: config.environmentPortalMode,
4561
+ environmentMap: createEnvironmentMapSnapshot(config.environmentMap),
4562
+ deferredPathResolve: config.deferredPathResolve,
3891
4563
  bvhNodeCount: config.bvhNodeCount,
3892
4564
  displayQuality: config.displayQuality,
3893
4565
  accelerationBuildMode: config.accelerationBuildMode,
@@ -3895,6 +4567,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3895
4567
  accelerationBuilt,
3896
4568
  accelerationBuildCount,
3897
4569
  frameConfigSlots: frameConfigSlotCount,
4570
+ gpuParallelism: lastGpuParallelism,
3898
4571
  memory: config.memory
3899
4572
  });
3900
4573
  }
@@ -3903,6 +4576,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3903
4576
  nextQueue.destroy?.();
3904
4577
  hitBuffer.destroy?.();
3905
4578
  accumulationBuffer.destroy?.();
4579
+ pathVertexBuffer.destroy?.();
3906
4580
  sceneObjectBuffer.destroy?.();
3907
4581
  triangleBuffer.destroy?.();
3908
4582
  bvhNodeBuffer.destroy?.();
@@ -3918,6 +4592,9 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3918
4592
  radianceTexture.destroy?.();
3919
4593
  denoiseScratchTexture.destroy?.();
3920
4594
  outputTexture.destroy?.();
4595
+ if (environmentMapResource.ownsTexture) {
4596
+ environmentMapResource.texture?.destroy?.();
4597
+ }
3921
4598
  context.unconfigure?.();
3922
4599
  }
3923
4600
  return Object.freeze({
@@ -3930,6 +4607,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3930
4607
  renderFrame,
3931
4608
  readOutputProbe,
3932
4609
  updateSceneObjects,
4610
+ updateCamera,
3933
4611
  getSnapshot,
3934
4612
  destroy
3935
4613
  });