@plasius/gpu-renderer 0.1.14 → 0.1.15

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.
@@ -4,6 +4,7 @@ const DEFAULT_MAX_DEPTH = 6;
4
4
  const DEFAULT_TILE_SIZE = 128;
5
5
  const DEFAULT_SAMPLES_PER_PIXEL = 1;
6
6
  const DEFAULT_SCENE_OBJECT_CAPACITY = 128;
7
+ const DEFAULT_ENVIRONMENT_PORTAL_CAPACITY = 32;
7
8
  const WORKGROUP_SIZE = 64;
8
9
  const RAY_RECORD_BYTES = 80;
9
10
  const HIT_RECORD_BYTES = 208;
@@ -14,9 +15,11 @@ const TRIANGLE_RECORD_BYTES = 208;
14
15
  const BVH_NODE_RECORD_BYTES = 48;
15
16
  const BVH_LEAF_REF_RECORD_BYTES = 16;
16
17
  const EMISSIVE_TRIANGLE_INDEX_BYTES = 4;
18
+ const ENVIRONMENT_PORTAL_RECORD_BYTES = 96;
17
19
  const ACCUMULATION_RECORD_BYTES = 16;
18
- const CONFIG_BUFFER_BYTES = 256;
20
+ const CONFIG_BUFFER_BYTES = 272;
19
21
  const COUNTER_BUFFER_BYTES = 16;
22
+ const TRACE_STORAGE_BUFFER_BINDINGS = 9;
20
23
  const HIT_TYPE_SURFACE = 0;
21
24
  const HIT_TYPE_EMISSIVE = 1;
22
25
  const MATERIAL_DIFFUSE = 0;
@@ -48,6 +51,7 @@ const DEFAULT_ENVIRONMENT_LIGHTING = Object.freeze({
48
51
 
49
52
  export const wavefrontPathTracingComputeLimits = Object.freeze({
50
53
  workgroupSize: WORKGROUP_SIZE,
54
+ traceStorageBufferBindings: TRACE_STORAGE_BUFFER_BINDINGS,
51
55
  rayRecordBytes: RAY_RECORD_BYTES,
52
56
  hitRecordBytes: HIT_RECORD_BYTES,
53
57
  sceneObjectRecordBytes: SCENE_OBJECT_RECORD_BYTES,
@@ -58,6 +62,7 @@ export const wavefrontPathTracingComputeLimits = Object.freeze({
58
62
  bvhLeafReferenceRecordBytes: BVH_LEAF_REF_RECORD_BYTES,
59
63
  emissiveTriangleIndexBytes: EMISSIVE_TRIANGLE_INDEX_BYTES,
60
64
  emissiveTriangleMetadataRecordBytes: BVH_NODE_RECORD_BYTES,
65
+ environmentPortalRecordBytes: ENVIRONMENT_PORTAL_RECORD_BYTES,
61
66
  accumulationRecordBytes: ACCUMULATION_RECORD_BYTES,
62
67
  });
63
68
 
@@ -877,6 +882,135 @@ function resolveEnvironmentLighting(input, environmentColor, ambientColor) {
877
882
  });
878
883
  }
879
884
 
885
+ function resolveEnvironmentPortalMode(value, hasPortals) {
886
+ if (value === undefined || value === null) {
887
+ return hasPortals ? 2 : 0;
888
+ }
889
+ if (Number.isInteger(value) && value >= 0 && value <= 2) {
890
+ return value;
891
+ }
892
+ if (value === "disabled") {
893
+ return 0;
894
+ }
895
+ if (value === "guide") {
896
+ return 1;
897
+ }
898
+ if (value === "guide-and-gate" || value === "gate") {
899
+ return 2;
900
+ }
901
+ throw new Error(
902
+ "environmentPortalMode must be disabled, guide, guide-and-gate, or an integer between 0 and 2."
903
+ );
904
+ }
905
+
906
+ function orthogonalPortalTangent(normal) {
907
+ if (Math.abs(normal[1]) < 0.92) {
908
+ return normalize(cross([0, 1, 0], normal), [1, 0, 0]);
909
+ }
910
+ return normalize(cross([1, 0, 0], normal), [0, 0, 1]);
911
+ }
912
+
913
+ function resolvePortalTangent(value, normal) {
914
+ const fallback = orthogonalPortalTangent(normal);
915
+ const tangent = asUnitVec3(value, fallback);
916
+ const projected = subtract(tangent, scale(normal, dot(tangent, normal)));
917
+ return normalize(projected, fallback);
918
+ }
919
+
920
+ function readPositiveFiniteNumber(name, value, fallback) {
921
+ const numeric = readFiniteNumber(name, value, fallback);
922
+ if (numeric <= 0) {
923
+ throw new Error(`${name} must be a positive finite number.`);
924
+ }
925
+ return numeric;
926
+ }
927
+
928
+ function readPortalExtent(name, value, halfName, halfValue) {
929
+ if (value !== undefined && value !== null) {
930
+ return readPositiveFiniteNumber(name, value, 1);
931
+ }
932
+ return readPositiveFiniteNumber(halfName, halfValue, 0.5) * 2;
933
+ }
934
+
935
+ function normalizeEnvironmentPortal(portal, index) {
936
+ if (!portal || typeof portal !== "object") {
937
+ throw new Error(`environmentPortals[${index}] must be an object.`);
938
+ }
939
+ const shape = portal.shape ?? portal.kind ?? "rectangle";
940
+ if (shape !== "rectangle") {
941
+ throw new Error(`environmentPortals[${index}].shape must be "rectangle".`);
942
+ }
943
+ const position = asVec3(portal.position ?? portal.center, [0, 0, 0]);
944
+ const normal = asUnitVec3(portal.normal, [0, 0, 1]);
945
+ const tangent = resolvePortalTangent(portal.tangent, normal);
946
+ const bitangent = normalize(cross(normal, tangent), [0, 1, 0]);
947
+ const width = readPortalExtent(
948
+ `environmentPortals[${index}].width`,
949
+ portal.width,
950
+ `environmentPortals[${index}].halfWidth`,
951
+ portal.halfWidth
952
+ );
953
+ const height = readPortalExtent(
954
+ `environmentPortals[${index}].height`,
955
+ portal.height,
956
+ `environmentPortals[${index}].halfHeight`,
957
+ portal.halfHeight
958
+ );
959
+ const radianceScale = Math.max(
960
+ 0,
961
+ readFiniteNumber(
962
+ `environmentPortals[${index}].radianceScale`,
963
+ portal.radianceScale ?? portal.intensity,
964
+ 1
965
+ )
966
+ );
967
+ return Object.freeze({
968
+ kind: 1,
969
+ flags: portal.twoSided === false ? 0 : 1,
970
+ position: Object.freeze([position[0], position[1], position[2], width * height]),
971
+ normal: Object.freeze([normal[0], normal[1], normal[2], radianceScale]),
972
+ tangent: Object.freeze([tangent[0], tangent[1], tangent[2], width * 0.5]),
973
+ bitangent: Object.freeze([bitangent[0], bitangent[1], bitangent[2], height * 0.5]),
974
+ color: Object.freeze(asColor(portal.color, [1, 1, 1, 1])),
975
+ });
976
+ }
977
+
978
+ function normalizeEnvironmentPortals(value) {
979
+ if (value === undefined || value === null) {
980
+ return Object.freeze([]);
981
+ }
982
+ if (!Array.isArray(value)) {
983
+ throw new Error("environmentPortals must be an array when provided.");
984
+ }
985
+ return Object.freeze(value.map(normalizeEnvironmentPortal));
986
+ }
987
+
988
+ function packEnvironmentPortals(portals, capacity) {
989
+ const bytes = new ArrayBuffer(capacity * ENVIRONMENT_PORTAL_RECORD_BYTES);
990
+ const data = new DataView(bytes);
991
+ const floatView = new Float32Array(bytes);
992
+ portals.forEach((portal, index) => {
993
+ const byteOffset = index * ENVIRONMENT_PORTAL_RECORD_BYTES;
994
+ const floatOffset = byteOffset / Float32Array.BYTES_PER_ELEMENT;
995
+ data.setUint32(byteOffset, portal.kind, true);
996
+ data.setUint32(byteOffset + 4, portal.flags, true);
997
+ data.setUint32(byteOffset + 8, 0, true);
998
+ data.setUint32(byteOffset + 12, 0, true);
999
+ writeVec4(floatView, floatOffset + 4, portal.position);
1000
+ writeVec4(floatView, floatOffset + 8, portal.normal);
1001
+ writeVec4(floatView, floatOffset + 12, portal.tangent);
1002
+ writeVec4(floatView, floatOffset + 16, portal.bitangent);
1003
+ writeVec4(floatView, floatOffset + 20, portal.color);
1004
+ });
1005
+ return Object.freeze({
1006
+ buffer: bytes,
1007
+ portals,
1008
+ count: portals.length,
1009
+ capacity,
1010
+ recordBytes: ENVIRONMENT_PORTAL_RECORD_BYTES,
1011
+ });
1012
+ }
1013
+
880
1014
  function getCanvasDimension(canvas, key, fallback) {
881
1015
  const value = Number(canvas?.[key]);
882
1016
  if (Number.isFinite(value) && value > 0) {
@@ -935,6 +1069,11 @@ export function estimateWavefrontPathTracingMemory(options = {}) {
935
1069
  options.emissiveTriangleCapacity,
936
1070
  0
937
1071
  );
1072
+ const environmentPortalCapacity = readNonNegativeInteger(
1073
+ "environmentPortalCapacity",
1074
+ options.environmentPortalCapacity,
1075
+ 0
1076
+ );
938
1077
  const queueBytes = tilePixelCapacity * RAY_RECORD_BYTES;
939
1078
  const hitBytes = tilePixelCapacity * HIT_RECORD_BYTES;
940
1079
  const accumulationBytes = tilePixelCapacity * ACCUMULATION_RECORD_BYTES;
@@ -944,6 +1083,8 @@ export function estimateWavefrontPathTracingMemory(options = {}) {
944
1083
  const bvhLeafReferenceBytes = bvhLeafSortCapacity * BVH_LEAF_REF_RECORD_BYTES;
945
1084
  const emissiveTriangleMetadataBytes =
946
1085
  emissiveTriangleCapacity * BVH_NODE_RECORD_BYTES;
1086
+ const environmentPortalBytes =
1087
+ environmentPortalCapacity * ENVIRONMENT_PORTAL_RECORD_BYTES;
947
1088
 
948
1089
  return Object.freeze({
949
1090
  queueBytes,
@@ -955,6 +1096,7 @@ export function estimateWavefrontPathTracingMemory(options = {}) {
955
1096
  bvhNodeBytes,
956
1097
  bvhLeafReferenceBytes,
957
1098
  emissiveTriangleMetadataBytes,
1099
+ environmentPortalBytes,
958
1100
  configBytes: CONFIG_BUFFER_BYTES,
959
1101
  counterBytes: COUNTER_BUFFER_BYTES,
960
1102
  totalHotBufferBytes:
@@ -966,6 +1108,7 @@ export function estimateWavefrontPathTracingMemory(options = {}) {
966
1108
  bvhNodeBytes +
967
1109
  bvhLeafReferenceBytes +
968
1110
  emissiveTriangleMetadataBytes +
1111
+ environmentPortalBytes +
969
1112
  CONFIG_BUFFER_BYTES +
970
1113
  COUNTER_BUFFER_BYTES,
971
1114
  });
@@ -1048,6 +1191,25 @@ export function createWavefrontPathTracingComputeConfig(options = {}) {
1048
1191
  environmentColor,
1049
1192
  ambientColor
1050
1193
  );
1194
+ const environmentPortals = normalizeEnvironmentPortals(
1195
+ options.environmentPortals ??
1196
+ options.environmentLightPortals ??
1197
+ options.environmentLighting?.environmentPortals
1198
+ );
1199
+ const environmentPortalCapacity = Math.max(
1200
+ environmentPortals.length,
1201
+ readNonNegativeInteger(
1202
+ "environmentPortalCapacity",
1203
+ options.environmentPortalCapacity,
1204
+ DEFAULT_ENVIRONMENT_PORTAL_CAPACITY
1205
+ )
1206
+ );
1207
+ const environmentPortalMode = resolveEnvironmentPortalMode(
1208
+ options.environmentPortalMode ??
1209
+ options.portalMode ??
1210
+ options.environmentLighting?.environmentPortalMode,
1211
+ environmentPortals.length > 0
1212
+ );
1051
1213
 
1052
1214
  return Object.freeze({
1053
1215
  width,
@@ -1077,6 +1239,10 @@ export function createWavefrontPathTracingComputeConfig(options = {}) {
1077
1239
  environmentColor: environmentLighting.environmentColor,
1078
1240
  ambientColor: environmentLighting.ambientColor,
1079
1241
  environmentLighting,
1242
+ environmentPortals,
1243
+ environmentPortalCount: environmentPortals.length,
1244
+ environmentPortalCapacity,
1245
+ environmentPortalMode,
1080
1246
  displayQuality: options.displayQuality === true,
1081
1247
  requiresMeshBvhForDisplayQuality: true,
1082
1248
  denoise: options.denoise !== false,
@@ -1088,6 +1254,7 @@ export function createWavefrontPathTracingComputeConfig(options = {}) {
1088
1254
  bvhNodeCapacity,
1089
1255
  bvhLeafSortCapacity,
1090
1256
  emissiveTriangleCapacity: emissiveTriangleIndices.capacity,
1257
+ environmentPortalCapacity,
1091
1258
  }),
1092
1259
  });
1093
1260
  }
@@ -1298,6 +1465,10 @@ function createConfigPayload(config, tile, frameIndex, buildRange = {}) {
1298
1465
  data.setUint32(244, buildRange.count ?? 0, true);
1299
1466
  data.setUint32(248, buildRange.sortItemCount ?? 0, true);
1300
1467
  data.setUint32(252, config.emissiveTriangleCount ?? 0, true);
1468
+ data.setUint32(256, config.environmentPortalCount ?? 0, true);
1469
+ data.setUint32(260, config.environmentPortalMode ?? 0, true);
1470
+ data.setUint32(264, 0, true);
1471
+ data.setUint32(268, 0, true);
1301
1472
  return bytes;
1302
1473
  }
1303
1474
 
@@ -1556,6 +1727,10 @@ struct FrameConfig {
1556
1727
  bvhBuildNodeCount: u32,
1557
1728
  bvhSortItemCount: u32,
1558
1729
  emissiveTriangleCount: u32,
1730
+ environmentPortalCount: u32,
1731
+ environmentPortalMode: u32,
1732
+ _portalPad0: u32,
1733
+ _portalPad1: u32,
1559
1734
  };
1560
1735
 
1561
1736
  struct Counters {
@@ -1579,6 +1754,18 @@ struct Candidate {
1579
1754
  mediumRefId: u32,
1580
1755
  };
1581
1756
 
1757
+ struct EnvironmentPortal {
1758
+ kind: u32,
1759
+ flags: u32,
1760
+ _pad0: u32,
1761
+ _pad1: u32,
1762
+ position: vec4<f32>,
1763
+ normal: vec4<f32>,
1764
+ tangent: vec4<f32>,
1765
+ bitangent: vec4<f32>,
1766
+ color: vec4<f32>,
1767
+ };
1768
+
1582
1769
  @group(0) @binding(0) var<storage, read_write> activeQueue: array<RayRecord>;
1583
1770
  @group(0) @binding(1) var<storage, read_write> nextQueue: array<RayRecord>;
1584
1771
  @group(0) @binding(2) var<storage, read_write> hits: array<HitRecord>;
@@ -1598,6 +1785,7 @@ struct Candidate {
1598
1785
  @group(0) @binding(16) var radianceImage: texture_storage_2d<rgba16float, write>;
1599
1786
  @group(0) @binding(17) var finalDenoiseInputRadiance: texture_2d<f32>;
1600
1787
  @group(0) @binding(18) var denoisedOutputImage: texture_storage_2d<rgba8unorm, write>;
1788
+ @group(0) @binding(19) var<storage, read> environmentPortals: array<EnvironmentPortal>;
1601
1789
 
1602
1790
  fn hash_u32(value: u32) -> u32 {
1603
1791
  var x = value;
@@ -1638,7 +1826,48 @@ fn saturate(value: f32) -> f32 {
1638
1826
  return clamp(value, 0.0, 1.0);
1639
1827
  }
1640
1828
 
1641
- fn environment_radiance(direction: vec3<f32>) -> vec3<f32> {
1829
+ fn max_component(value: vec3<f32>) -> f32 {
1830
+ return max(max(value.x, value.y), value.z);
1831
+ }
1832
+
1833
+ fn environment_portal_radiance_scale(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
1834
+ if (config.environmentPortalCount == 0u || config.environmentPortalMode == 0u) {
1835
+ return vec3<f32>(1.0);
1836
+ }
1837
+ var scale = vec3<f32>(0.0);
1838
+ for (var portalIndex = 0u; portalIndex < config.environmentPortalCount; portalIndex = portalIndex + 1u) {
1839
+ let portal = environmentPortals[portalIndex];
1840
+ if (portal.kind == 1u) {
1841
+ let portalNormal = safe_normalize(portal.normal.xyz, vec3<f32>(0.0, 0.0, 1.0));
1842
+ let denominator = dot(direction, portalNormal);
1843
+ let twoSided = (portal.flags & 1u) != 0u;
1844
+ var facing = abs(denominator) > 0.0001;
1845
+ if (!twoSided && denominator <= 0.0001) {
1846
+ facing = false;
1847
+ }
1848
+ if (facing) {
1849
+ let distance = dot(portal.position.xyz - origin, portalNormal) / denominator;
1850
+ if (distance > 0.001) {
1851
+ let hitPosition = origin + direction * distance;
1852
+ let local = hitPosition - portal.position.xyz;
1853
+ let tangent = safe_normalize(portal.tangent.xyz, vec3<f32>(1.0, 0.0, 0.0));
1854
+ let bitangent = safe_normalize(portal.bitangent.xyz, vec3<f32>(0.0, 1.0, 0.0));
1855
+ let u = dot(local, tangent);
1856
+ let v = dot(local, bitangent);
1857
+ if (abs(u) <= portal.tangent.w && abs(v) <= portal.bitangent.w) {
1858
+ let areaWeight = clamp(sqrt(max(portal.position.w, 0.0001)), 0.25, 4.0);
1859
+ let angleWeight = max(abs(denominator), 0.08);
1860
+ let portalScale = portal.color.rgb * portal.normal.w * portal.color.a * areaWeight * angleWeight;
1861
+ scale = max(scale, portalScale);
1862
+ }
1863
+ }
1864
+ }
1865
+ }
1866
+ }
1867
+ return scale;
1868
+ }
1869
+
1870
+ fn environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
1642
1871
  let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
1643
1872
  let upFactor = saturate(rayDirection.y * 0.5 + 0.5);
1644
1873
  let sunDirection = safe_normalize(
@@ -1649,10 +1878,26 @@ fn environment_radiance(direction: vec3<f32>) -> vec3<f32> {
1649
1878
  let gradient =
1650
1879
  config.environmentHorizonColor.xyz * (1.0 - upFactor) +
1651
1880
  config.environmentZenithColor.xyz * upFactor;
1881
+ let portalScale = environment_portal_radiance_scale(origin, rayDirection);
1882
+ let portalHit = max_component(portalScale) > 0.0001;
1652
1883
  return (
1653
1884
  gradient +
1654
1885
  config.environmentSunColor.xyz * sunGlow
1655
- ) * max(config.environmentSunDirectionIntensity.w, 0.0001);
1886
+ ) *
1887
+ max(config.environmentSunDirectionIntensity.w, 0.0001) *
1888
+ select(vec3<f32>(1.0), portalScale, portalHit);
1889
+ }
1890
+
1891
+ fn gated_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
1892
+ let portalScale = environment_portal_radiance_scale(origin, safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0)));
1893
+ if (
1894
+ config.environmentPortalCount > 0u &&
1895
+ config.environmentPortalMode == 2u &&
1896
+ max_component(portalScale) <= 0.0001
1897
+ ) {
1898
+ return config.ambientColor.xyz * 0.65;
1899
+ }
1900
+ return environment_radiance(origin, direction);
1656
1901
  }
1657
1902
 
1658
1903
  fn default_mesh_range() -> MeshRange {
@@ -1923,7 +2168,7 @@ fn make_ray(pixelIndex: u32) -> RayRecord {
1923
2168
  }
1924
2169
 
1925
2170
  fn make_miss(ray: RayRecord) -> HitRecord {
1926
- let radiance = environment_radiance(ray.direction.xyz);
2171
+ let radiance = gated_environment_radiance(ray.origin.xyz, ray.direction.xyz);
1927
2172
  return HitRecord(
1928
2173
  ray.rayId,
1929
2174
  ray.sourcePixelId,
@@ -2409,6 +2654,21 @@ fn sample_emissive_triangle_direction(hit: HitRecord, seed: u32, fallback: vec3<
2409
2654
  return safe_normalize(lightPoint - hit.position.xyz, fallback);
2410
2655
  }
2411
2656
 
2657
+ fn sample_environment_portal_direction(hit: HitRecord, seed: u32, fallback: vec3<f32>) -> vec3<f32> {
2658
+ if (config.environmentPortalCount == 0u || config.environmentPortalMode == 0u) {
2659
+ return fallback;
2660
+ }
2661
+ let portalSlot = min(
2662
+ u32(random01(seed + 211u) * f32(config.environmentPortalCount)),
2663
+ config.environmentPortalCount - 1u
2664
+ );
2665
+ let portal = environmentPortals[portalSlot];
2666
+ let u = (random01(seed + 223u) * 2.0 - 1.0) * portal.tangent.w;
2667
+ let v = (random01(seed + 227u) * 2.0 - 1.0) * portal.bitangent.w;
2668
+ let portalTarget = portal.position.xyz + portal.tangent.xyz * u + portal.bitangent.xyz * v;
2669
+ return safe_normalize(portalTarget - hit.position.xyz, fallback);
2670
+ }
2671
+
2412
2672
  fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult {
2413
2673
  let roughness = clamp(hit.material.x, 0.0, 1.0);
2414
2674
  if (hit.materialKind == 1u) {
@@ -2448,8 +2708,17 @@ fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult
2448
2708
  let canSampleLight = dot(hit.shadingNormal.xyz, guidedLight) > -0.04;
2449
2709
  let guideProbability = select(0.38, 0.72, ray.bounce == 0u);
2450
2710
  let useGuidedLight = canSampleLight && random01(seed + 37u) < guideProbability;
2711
+ let guidedPortal = sample_environment_portal_direction(hit, seed, randomDiffuse);
2712
+ let canSamplePortal = dot(hit.shadingNormal.xyz, guidedPortal) > -0.04;
2713
+ let useGuidedPortal =
2714
+ !useGuidedLight &&
2715
+ canSamplePortal &&
2716
+ config.environmentPortalCount > 0u &&
2717
+ config.environmentPortalMode > 0u &&
2718
+ random01(seed + 89u) < 0.58;
2719
+ let guidedDirection = select(randomDiffuse, guidedPortal, useGuidedPortal);
2451
2720
  return ScatterResult(
2452
- vec4<f32>(select(randomDiffuse, guidedLight, useGuidedLight), 0.0),
2721
+ vec4<f32>(select(guidedDirection, guidedLight, useGuidedLight), 0.0),
2453
2722
  select(0u, RAY_FLAG_GUIDED_EMISSIVE, useGuidedLight),
2454
2723
  0u,
2455
2724
  0u,
@@ -2658,6 +2927,32 @@ fn fragmentMain(in: VertexOut) -> @location(0) vec4<f32> {
2658
2927
  }
2659
2928
  `;
2660
2929
 
2930
+ function createWavefrontDeviceDescriptor(adapter, options = {}) {
2931
+ const requiredLimits = { ...(options.requiredLimits ?? {}) };
2932
+ const exposedStorageBufferLimit = Number(adapter?.limits?.maxStorageBuffersPerShaderStage);
2933
+ if (Number.isFinite(exposedStorageBufferLimit)) {
2934
+ if (exposedStorageBufferLimit < TRACE_STORAGE_BUFFER_BINDINGS) {
2935
+ throw new Error(
2936
+ `Wavefront mesh tracing requires maxStorageBuffersPerShaderStage>=${TRACE_STORAGE_BUFFER_BINDINGS}, ` +
2937
+ `but this adapter exposes ${exposedStorageBufferLimit}.`
2938
+ );
2939
+ }
2940
+ requiredLimits.maxStorageBuffersPerShaderStage = Math.max(
2941
+ Number(requiredLimits.maxStorageBuffersPerShaderStage ?? 0),
2942
+ TRACE_STORAGE_BUFFER_BINDINGS
2943
+ );
2944
+ }
2945
+
2946
+ const descriptor = { ...(options.deviceDescriptor ?? {}) };
2947
+ if (Object.keys(requiredLimits).length > 0) {
2948
+ descriptor.requiredLimits = {
2949
+ ...(descriptor.requiredLimits ?? {}),
2950
+ ...requiredLimits,
2951
+ };
2952
+ }
2953
+ return Object.keys(descriptor).length > 0 ? descriptor : undefined;
2954
+ }
2955
+
2661
2956
  export async function createWavefrontPathTracingComputeRenderer(options = {}) {
2662
2957
  assertAnalyticDisplayQualityPolicy(options);
2663
2958
  const constants = getGpuUsageConstants();
@@ -2678,7 +2973,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
2678
2973
  throw new Error("Unable to acquire a WebGPU adapter for wavefront path tracing.");
2679
2974
  }
2680
2975
 
2681
- const device = await adapter.requestDevice();
2976
+ const device = await adapter.requestDevice(createWavefrontDeviceDescriptor(adapter, options));
2682
2977
  const context = canvas.getContext("webgpu");
2683
2978
  if (!context || typeof context.configure !== "function") {
2684
2979
  throw new Error("Canvas WebGPU context does not support configure().");
@@ -2758,18 +3053,19 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
2758
3053
  Math.max(1, config.gpuMeshSource.meshes.count) * MESH_RANGE_RECORD_BYTES,
2759
3054
  "plasius.wavefront.meshRanges"
2760
3055
  );
3056
+ const environmentPortalBuffer = createBuffer(
3057
+ device,
3058
+ constants.buffer.STORAGE | constants.buffer.COPY_DST,
3059
+ Math.max(1, config.environmentPortalCapacity) * ENVIRONMENT_PORTAL_RECORD_BYTES,
3060
+ "plasius.wavefront.environmentPortals"
3061
+ );
2761
3062
  const bvhLeafRefBuffer = createBuffer(
2762
3063
  device,
2763
3064
  constants.buffer.STORAGE | constants.buffer.COPY_DST,
2764
3065
  Math.max(1, config.bvhLeafSortCapacity) * BVH_LEAF_REF_RECORD_BYTES,
2765
3066
  "plasius.wavefront.bvhLeafRefs"
2766
3067
  );
2767
- const configBuffer = createBuffer(
2768
- device,
2769
- constants.buffer.UNIFORM | constants.buffer.COPY_DST,
2770
- CONFIG_BUFFER_BYTES,
2771
- "plasius.wavefront.frameConfig"
2772
- );
3068
+ const tiles = createTiles(config.width, config.height, config.tileSize);
2773
3069
  const uniformOffsetAlignment = Number(device?.limits?.minUniformBufferOffsetAlignment);
2774
3070
  const configBufferStride = alignTo(
2775
3071
  CONFIG_BUFFER_BYTES,
@@ -2777,6 +3073,16 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
2777
3073
  ? uniformOffsetAlignment
2778
3074
  : CONFIG_BUFFER_BYTES
2779
3075
  );
3076
+ const frameConfigSlotCount = Math.max(
3077
+ 1,
3078
+ tiles.length * config.samplesPerPixel + tiles.length + (config.denoise ? 1 : 0)
3079
+ );
3080
+ const configBuffer = createBuffer(
3081
+ device,
3082
+ constants.buffer.UNIFORM | constants.buffer.COPY_DST,
3083
+ frameConfigSlotCount * configBufferStride,
3084
+ "plasius.wavefront.frameConfig"
3085
+ );
2780
3086
  const bvhBuildConfigSlots =
2781
3087
  1 + config.bvhSortStages.length + config.bvhBuildLevels.length;
2782
3088
  const bvhBuildConfigBuffer = createBuffer(
@@ -2812,6 +3118,11 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
2812
3118
  device.queue.writeBuffer(meshVertexBuffer, 0, config.gpuMeshSource.vertices.buffer);
2813
3119
  device.queue.writeBuffer(meshIndexBuffer, 0, config.gpuMeshSource.indices.buffer);
2814
3120
  device.queue.writeBuffer(meshRangeBuffer, 0, config.gpuMeshSource.meshes.buffer);
3121
+ const packedEnvironmentPortals = packEnvironmentPortals(
3122
+ config.environmentPortals,
3123
+ Math.max(1, config.environmentPortalCapacity)
3124
+ );
3125
+ device.queue.writeBuffer(environmentPortalBuffer, 0, packedEnvironmentPortals.buffer);
2815
3126
 
2816
3127
  const radianceTexture = device.createTexture({
2817
3128
  label: "plasius.wavefront.radiance",
@@ -2873,6 +3184,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
2873
3184
  visibility: constants.shader.COMPUTE,
2874
3185
  storageTexture: { access: "write-only", format: "rgba16float" },
2875
3186
  },
3187
+ { binding: 19, visibility: constants.shader.COMPUTE, buffer: { type: "read-only-storage" } },
2876
3188
  ],
2877
3189
  });
2878
3190
  const accelerationBindGroupLayout = device.createBindGroupLayout({
@@ -3048,6 +3360,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3048
3360
  { binding: 8, resource: { buffer: triangleBuffer } },
3049
3361
  { binding: 9, resource: { buffer: bvhNodeBuffer } },
3050
3362
  { binding: 16, resource: radianceView },
3363
+ { binding: 19, resource: { buffer: environmentPortalBuffer } },
3051
3364
  ],
3052
3365
  });
3053
3366
  }
@@ -3139,11 +3452,29 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3139
3452
  });
3140
3453
 
3141
3454
  let frame = 0;
3142
- const tiles = createTiles(config.width, config.height, config.tileSize);
3455
+ let accelerationBuilt = !config.gpuAccelerationBuildRequired;
3456
+ let accelerationBuildCount = 0;
3457
+
3458
+ function createFrameConfigWriter(frameIndex) {
3459
+ let slot = 0;
3460
+ return (tile, buildRange = {}) => {
3461
+ if (slot >= frameConfigSlotCount) {
3462
+ throw new Error("Wavefront frame config slot capacity exceeded.");
3463
+ }
3464
+ const offset = slot * configBufferStride;
3465
+ slot += 1;
3466
+ device.queue.writeBuffer(
3467
+ configBuffer,
3468
+ offset,
3469
+ createConfigPayload(config, tile, frameIndex, buildRange)
3470
+ );
3471
+ return offset;
3472
+ };
3473
+ }
3143
3474
 
3144
3475
  function dispatchGpuAccelerationBuild(frameIndex) {
3145
- if (!config.gpuAccelerationBuildRequired) {
3146
- return;
3476
+ if (!config.gpuAccelerationBuildRequired || accelerationBuilt) {
3477
+ return false;
3147
3478
  }
3148
3479
  const buildTile = tiles[0] ?? { x: 0, y: 0, width: 1, height: 1 };
3149
3480
  const encoder = device.createCommandEncoder({
@@ -3201,30 +3532,24 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3201
3532
  }
3202
3533
  passEncoder.end();
3203
3534
  device.queue.submit([encoder.finish()]);
3535
+ accelerationBuilt = true;
3536
+ accelerationBuildCount += 1;
3537
+ return true;
3204
3538
  }
3205
3539
 
3206
- function dispatchTileSample(tile, frameIndex, sampleIndex) {
3207
- const sampleWeight = 1 / config.samplesPerPixel;
3208
- const configPayload = createConfigPayload(config, tile, frameIndex, {
3209
- sampleIndex,
3210
- sampleWeight,
3211
- });
3212
- device.queue.writeBuffer(configBuffer, 0, configPayload);
3213
- const encoder = device.createCommandEncoder({
3214
- label: `plasius.wavefront.frame.${frameIndex}.tile.${tile.x}.${tile.y}.sample.${sampleIndex}`,
3215
- });
3540
+ function encodeTileSample(encoder, tile, configOffset) {
3216
3541
  const passEncoder = encoder.beginComputePass({
3217
3542
  label: "plasius.wavefront.computePass",
3218
3543
  });
3219
3544
  const tileWorkgroups = Math.ceil((tile.width * tile.height) / WORKGROUP_SIZE);
3220
3545
  const capacityWorkgroups = Math.ceil(config.tilePixelCapacity / WORKGROUP_SIZE);
3221
3546
 
3222
- passEncoder.setBindGroup(0, bindGroups[0], [0]);
3547
+ passEncoder.setBindGroup(0, bindGroups[0], [configOffset]);
3223
3548
  passEncoder.setPipeline(pipelines.generatePrimaryRays);
3224
3549
  passEncoder.dispatchWorkgroups(tileWorkgroups);
3225
3550
 
3226
3551
  for (let bounceIndex = 0; bounceIndex < config.maxDepth; bounceIndex += 1) {
3227
- passEncoder.setBindGroup(0, bindGroups[bounceIndex % 2], [0]);
3552
+ passEncoder.setBindGroup(0, bindGroups[bounceIndex % 2], [configOffset]);
3228
3553
  passEncoder.setPipeline(pipelines.intersectActiveQueue);
3229
3554
  passEncoder.dispatchWorkgroups(capacityWorkgroups);
3230
3555
  passEncoder.setPipeline(pipelines.resolveSurfaceRecords);
@@ -3234,58 +3559,28 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3234
3559
  }
3235
3560
 
3236
3561
  passEncoder.end();
3237
- device.queue.submit([encoder.finish()]);
3238
3562
  }
3239
3563
 
3240
- function dispatchTileOutput(tile, frameIndex) {
3241
- const configPayload = createConfigPayload(config, tile, frameIndex, {
3242
- sampleIndex: 0,
3243
- sampleWeight: 1 / config.samplesPerPixel,
3244
- });
3245
- device.queue.writeBuffer(configBuffer, 0, configPayload);
3246
- const encoder = device.createCommandEncoder({
3247
- label: `plasius.wavefront.frame.${frameIndex}.tile.${tile.x}.${tile.y}.output`,
3248
- });
3564
+ function encodeTileOutput(encoder, tile, configOffset) {
3249
3565
  const passEncoder = encoder.beginComputePass({
3250
3566
  label: "plasius.wavefront.outputPass",
3251
3567
  });
3252
3568
  const tileWorkgroups = Math.ceil((tile.width * tile.height) / WORKGROUP_SIZE);
3253
3569
 
3254
- passEncoder.setBindGroup(0, bindGroups[0], [0]);
3570
+ passEncoder.setBindGroup(0, bindGroups[0], [configOffset]);
3255
3571
  passEncoder.setPipeline(pipelines.accumulateTerminalRadiance);
3256
3572
  passEncoder.dispatchWorkgroups(tileWorkgroups);
3257
3573
  passEncoder.end();
3258
- device.queue.submit([encoder.finish()]);
3259
3574
  }
3260
3575
 
3261
- function dispatchTile(tile, frameIndex) {
3262
- for (let sampleIndex = 0; sampleIndex < config.samplesPerPixel; sampleIndex += 1) {
3263
- dispatchTileSample(tile, frameIndex, sampleIndex);
3264
- }
3265
- dispatchTileOutput(tile, frameIndex);
3266
- }
3267
-
3268
- function dispatchDenoise(frameIndex) {
3576
+ function encodeDenoise(encoder, configOffset) {
3269
3577
  if (!config.denoise) {
3270
3578
  return;
3271
3579
  }
3272
- device.queue.writeBuffer(
3273
- configBuffer,
3274
- 0,
3275
- createConfigPayload(
3276
- config,
3277
- { x: 0, y: 0, width: config.width, height: config.height },
3278
- frameIndex,
3279
- { sampleIndex: 0, sampleWeight: 1 / config.samplesPerPixel }
3280
- )
3281
- );
3282
- const encoder = device.createCommandEncoder({
3283
- label: `plasius.wavefront.frame.${frameIndex}.denoise`,
3284
- });
3285
3580
  const radiancePass = encoder.beginComputePass({
3286
3581
  label: "plasius.wavefront.denoiseRadiancePass",
3287
3582
  });
3288
- radiancePass.setBindGroup(0, denoiseRadianceBindGroup, [0]);
3583
+ radiancePass.setBindGroup(0, denoiseRadianceBindGroup, [configOffset]);
3289
3584
  radiancePass.setPipeline(pipelines.denoiseLinearRadiance);
3290
3585
  radiancePass.dispatchWorkgroups(Math.ceil(config.width / 8), Math.ceil(config.height / 8));
3291
3586
  radiancePass.end();
@@ -3293,18 +3588,14 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3293
3588
  const resolvePass = encoder.beginComputePass({
3294
3589
  label: "plasius.wavefront.denoiseResolvePass",
3295
3590
  });
3296
- resolvePass.setBindGroup(0, denoiseResolveBindGroup, [0]);
3591
+ resolvePass.setBindGroup(0, denoiseResolveBindGroup, [configOffset]);
3297
3592
  resolvePass.setPipeline(pipelines.resolveDenoisedOutputImage);
3298
3593
  resolvePass.dispatchWorkgroups(Math.ceil(config.width / 8), Math.ceil(config.height / 8));
3299
3594
  resolvePass.end();
3300
- device.queue.submit([encoder.finish()]);
3301
3595
  }
3302
3596
 
3303
- function present() {
3597
+ function encodePresent(encoder) {
3304
3598
  const texture = context.getCurrentTexture();
3305
- const encoder = device.createCommandEncoder({
3306
- label: `plasius.wavefront.present.${frame}`,
3307
- });
3308
3599
  const passEncoder = encoder.beginRenderPass({
3309
3600
  label: "plasius.wavefront.presentPass",
3310
3601
  colorAttachments: [
@@ -3320,17 +3611,44 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3320
3611
  passEncoder.setBindGroup(0, presentBindGroup);
3321
3612
  passEncoder.draw(3);
3322
3613
  passEncoder.end();
3614
+ }
3615
+
3616
+ function dispatchFrame(frameIndex) {
3617
+ const writeFrameConfig = createFrameConfigWriter(frameIndex);
3618
+ const encoder = device.createCommandEncoder({
3619
+ label: `plasius.wavefront.frame.${frameIndex}.batched`,
3620
+ });
3621
+ for (const tile of tiles) {
3622
+ for (let sampleIndex = 0; sampleIndex < config.samplesPerPixel; sampleIndex += 1) {
3623
+ const configOffset = writeFrameConfig(tile, {
3624
+ sampleIndex,
3625
+ sampleWeight: 1 / config.samplesPerPixel,
3626
+ });
3627
+ encodeTileSample(encoder, tile, configOffset);
3628
+ }
3629
+ const outputConfigOffset = writeFrameConfig(tile, {
3630
+ sampleIndex: 0,
3631
+ sampleWeight: 1 / config.samplesPerPixel,
3632
+ });
3633
+ encodeTileOutput(encoder, tile, outputConfigOffset);
3634
+ }
3635
+ if (config.denoise) {
3636
+ const denoiseConfigOffset = writeFrameConfig(
3637
+ { x: 0, y: 0, width: config.width, height: config.height },
3638
+ { sampleIndex: 0, sampleWeight: 1 / config.samplesPerPixel }
3639
+ );
3640
+ encodeDenoise(encoder, denoiseConfigOffset);
3641
+ }
3642
+ encodePresent(encoder);
3323
3643
  device.queue.submit([encoder.finish()]);
3644
+ return 1;
3324
3645
  }
3325
3646
 
3326
3647
  function renderOnce() {
3327
3648
  frame += 1;
3328
- dispatchGpuAccelerationBuild(frame + config.frameIndex);
3329
- for (const tile of tiles) {
3330
- dispatchTile(tile, frame + config.frameIndex);
3331
- }
3332
- dispatchDenoise(frame + config.frameIndex);
3333
- present();
3649
+ const frameIndex = frame + config.frameIndex;
3650
+ const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex);
3651
+ const frameSubmissionCount = dispatchFrame(frameIndex);
3334
3652
  return Object.freeze({
3335
3653
  frame,
3336
3654
  width: config.width,
@@ -3344,10 +3662,17 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3344
3662
  sceneObjectCount: config.sceneObjectCount,
3345
3663
  triangleCount: config.triangleCount,
3346
3664
  emissiveTriangleCount: config.emissiveTriangleCount,
3665
+ environmentPortalCount: config.environmentPortalCount,
3666
+ environmentPortalMode: config.environmentPortalMode,
3347
3667
  bvhNodeCount: config.bvhNodeCount,
3348
3668
  displayQuality: config.displayQuality,
3349
3669
  accelerationBuildMode: config.accelerationBuildMode,
3350
3670
  gpuAccelerationBuildRequired: config.gpuAccelerationBuildRequired,
3671
+ accelerationBuildSubmitted,
3672
+ accelerationBuilt,
3673
+ accelerationBuildCount,
3674
+ commandSubmissions: frameSubmissionCount + (accelerationBuildSubmitted ? 1 : 0),
3675
+ frameConfigSlots: frameConfigSlotCount,
3351
3676
  memory: config.memory,
3352
3677
  });
3353
3678
  }
@@ -3416,10 +3741,15 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3416
3741
  sceneObjectCount: config.sceneObjectCount,
3417
3742
  triangleCount: config.triangleCount,
3418
3743
  emissiveTriangleCount: config.emissiveTriangleCount,
3744
+ environmentPortalCount: config.environmentPortalCount,
3745
+ environmentPortalMode: config.environmentPortalMode,
3419
3746
  bvhNodeCount: config.bvhNodeCount,
3420
3747
  displayQuality: config.displayQuality,
3421
3748
  accelerationBuildMode: config.accelerationBuildMode,
3422
3749
  gpuAccelerationBuildRequired: config.gpuAccelerationBuildRequired,
3750
+ accelerationBuilt,
3751
+ accelerationBuildCount,
3752
+ frameConfigSlots: frameConfigSlotCount,
3423
3753
  memory: config.memory,
3424
3754
  });
3425
3755
  }
@@ -3435,6 +3765,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
3435
3765
  meshVertexBuffer.destroy?.();
3436
3766
  meshIndexBuffer.destroy?.();
3437
3767
  meshRangeBuffer.destroy?.();
3768
+ environmentPortalBuffer.destroy?.();
3438
3769
  bvhLeafRefBuffer.destroy?.();
3439
3770
  configBuffer.destroy?.();
3440
3771
  bvhBuildConfigBuffer.destroy?.();