ice3 0.1.0 → 0.2.0

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.
Files changed (35) hide show
  1. package/README.md +9 -0
  2. package/dist/_types/core/scene/ICENode.d.ts +6 -1
  3. package/dist/_types/renderer/Color.d.ts +2 -0
  4. package/dist/_types/renderer/FakeRendererBackend.d.ts +1 -1
  5. package/dist/_types/renderer/InstanceBatch.d.ts +14 -0
  6. package/dist/_types/renderer/RenderBatch.d.ts +17 -0
  7. package/dist/_types/renderer/Renderables.d.ts +9 -0
  8. package/dist/_types/renderer/RendererProfiler.d.ts +19 -0
  9. package/dist/_types/renderer/RendererSelection.d.ts +12 -0
  10. package/dist/_types/renderer/WebGL2GeometryBatch.d.ts +2 -9
  11. package/dist/_types/renderer/WebGL2InstanceBatch.d.ts +3 -14
  12. package/dist/_types/renderer/WebGL2RenderBatch.d.ts +1 -16
  13. package/dist/_types/renderer/WebGL2RendererBackend.d.ts +9 -2
  14. package/dist/_types/renderer/WebGPUBundleCache.d.ts +14 -0
  15. package/dist/_types/renderer/WebGPUCapabilities.d.ts +23 -0
  16. package/dist/_types/renderer/WebGPUDrawStorage.d.ts +27 -0
  17. package/dist/_types/renderer/WebGPUGeometryCache.d.ts +45 -0
  18. package/dist/_types/renderer/WebGPUInstanceCache.d.ts +24 -0
  19. package/dist/_types/renderer/WebGPUInstanceStorage.d.ts +39 -0
  20. package/dist/_types/renderer/WebGPUPicker.d.ts +30 -0
  21. package/dist/_types/renderer/WebGPUProbe.d.ts +21 -0
  22. package/dist/_types/renderer/WebGPURenderPipeline.d.ts +22 -0
  23. package/dist/_types/renderer/WebGPURenderPlan.d.ts +23 -0
  24. package/dist/_types/renderer/WebGPURendererBackend.d.ts +111 -0
  25. package/dist/_types/renderer/WebGPUResources.d.ts +9 -0
  26. package/dist/_types/renderer/WebGPUSelectionMask.d.ts +18 -0
  27. package/dist/_types/renderer/WebGPUTimestampProfiler.d.ts +16 -0
  28. package/dist/_types/renderer/WebGPUVisibility.d.ts +27 -0
  29. package/dist/_types/renderer/index.d.ts +21 -7
  30. package/dist/_types/renderer/types.d.ts +15 -3
  31. package/dist/_types/runtime/ICE3.d.ts +14 -6
  32. package/dist/index.d.ts +1 -1
  33. package/dist/index.js +3204 -143
  34. package/dist/index.js.map +4 -4
  35. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -532,11 +532,12 @@ var ICENode = class extends ICEEventDispatcher {
532
532
  position;
533
533
  rotation;
534
534
  scale;
535
- visible = true;
536
535
  _parent = null;
537
536
  _children = [];
538
537
  worldMatrix = new ICEMatrix4();
539
538
  localMatrixOverride = null;
539
+ _visible = true;
540
+ _revision = 0;
540
541
  constructor(options) {
541
542
  super();
542
543
  if (!options.id.trim()) {
@@ -560,6 +561,18 @@ var ICENode = class extends ICEEventDispatcher {
560
561
  get typeId() {
561
562
  return "ice3:Node";
562
563
  }
564
+ get visible() {
565
+ return this._visible;
566
+ }
567
+ set visible(value) {
568
+ if (value === this._visible)
569
+ return;
570
+ this._visible = value;
571
+ this.markSceneChanged();
572
+ }
573
+ get revision() {
574
+ return this.getRoot()._revision;
575
+ }
563
576
  get parent() {
564
577
  return this._parent;
565
578
  }
@@ -582,6 +595,7 @@ var ICENode = class extends ICEEventDispatcher {
582
595
  }
583
596
  node._parent = this;
584
597
  this._children.push(node);
598
+ this.markSceneChanged();
585
599
  return this;
586
600
  }
587
601
  removeChild(node, options) {
@@ -591,6 +605,7 @@ var ICENode = class extends ICEEventDispatcher {
591
605
  }
592
606
  this._children.splice(index, 1);
593
607
  node._parent = null;
608
+ this.markSceneChanged();
594
609
  if (options?.dispose) {
595
610
  node.dispose();
596
611
  }
@@ -624,10 +639,14 @@ var ICENode = class extends ICEEventDispatcher {
624
639
  throw new TypeError("ICE local matrix must contain 16 finite values");
625
640
  }
626
641
  this.localMatrixOverride = new Float32Array(matrix);
642
+ this.markSceneChanged();
627
643
  return this;
628
644
  }
629
645
  clearLocalMatrix() {
646
+ if (this.localMatrixOverride === null)
647
+ return this;
630
648
  this.localMatrixOverride = null;
649
+ this.markSceneChanged();
631
650
  return this;
632
651
  }
633
652
  toJSON() {
@@ -658,6 +677,9 @@ var ICENode = class extends ICEEventDispatcher {
658
677
  }
659
678
  return current;
660
679
  }
680
+ markSceneChanged() {
681
+ this.getRoot()._revision += 1;
682
+ }
661
683
  isAncestorOf(node) {
662
684
  let current = node;
663
685
  while (current) {
@@ -839,7 +861,28 @@ function deserializeScene(value, registry) {
839
861
  return { scene, report: { errors } };
840
862
  }
841
863
 
842
- // packages/renderer/dist/WebGL2RenderBatch.js
864
+ // packages/renderer/dist/Renderables.js
865
+ var visit = (node, parentVisible, output) => {
866
+ const visible = parentVisible && node.visible;
867
+ if (visible && node instanceof ICEMesh) {
868
+ output.push({
869
+ id: node.id,
870
+ mesh: node,
871
+ worldMatrix: node.updateWorldMatrix().elements
872
+ });
873
+ }
874
+ for (const child of node.children) {
875
+ visit(child, visible, output);
876
+ }
877
+ };
878
+ function collectRenderables(scene, output = []) {
879
+ output.length = 0;
880
+ visit(scene, true, output);
881
+ return output;
882
+ }
883
+ var selectionIdentity = (mesh) => mesh.selectionId;
884
+
885
+ // packages/renderer/dist/RenderBatch.js
843
886
  function collectRenderBatches(scene, output = []) {
844
887
  output.length = 0;
845
888
  const visit2 = (node, parentVisible) => {
@@ -865,27 +908,6 @@ function isRenderBatch(node) {
865
908
  return Boolean(candidate.geometry && candidate.material && candidate.semanticIds && candidate.semanticNodeIds && candidate.semanticSelectionIds && typeof candidate.materialKey === "string");
866
909
  }
867
910
 
868
- // packages/renderer/dist/WebGL2GeometryBatch.js
869
- var visit = (node, parentVisible, output) => {
870
- const visible = parentVisible && node.visible;
871
- if (visible && node instanceof ICEMesh) {
872
- output.push({
873
- id: node.id,
874
- mesh: node,
875
- worldMatrix: node.updateWorldMatrix().elements
876
- });
877
- }
878
- for (const child of node.children) {
879
- visit(child, visible, output);
880
- }
881
- };
882
- function collectRenderables(scene, output = []) {
883
- output.length = 0;
884
- visit(scene, true, output);
885
- return output;
886
- }
887
- var selectionIdentity = (mesh) => mesh.selectionId;
888
-
889
911
  // packages/renderer/dist/WebGL2Program.js
890
912
  var VERTEX_SOURCE = `
891
913
  attribute vec3 a_position;
@@ -1026,6 +1048,42 @@ var WebGL2Program = class _WebGL2Program {
1026
1048
  }
1027
1049
  };
1028
1050
 
1051
+ // packages/renderer/dist/InstanceBatch.js
1052
+ function collectInstanceBatches(renderables, output = []) {
1053
+ output.length = 0;
1054
+ const byGeometry = /* @__PURE__ */ new Map();
1055
+ for (const renderable of renderables) {
1056
+ const byMaterial = byGeometry.get(renderable.mesh.geometry) ?? /* @__PURE__ */ new Map();
1057
+ const group = byMaterial.get(renderable.mesh.material) ?? [];
1058
+ group.push(renderable);
1059
+ byMaterial.set(renderable.mesh.material, group);
1060
+ byGeometry.set(renderable.mesh.geometry, byMaterial);
1061
+ }
1062
+ for (const [geometry, byMaterial] of byGeometry) {
1063
+ for (const [material, group] of byMaterial) {
1064
+ if (group.length < 2)
1065
+ continue;
1066
+ const instanceMatrices = new Float32Array(group.length * 16);
1067
+ const semanticIndices = new Uint32Array(group.length);
1068
+ for (let index = 0; index < group.length; index += 1) {
1069
+ instanceMatrices.set(group[index].worldMatrix, index * 16);
1070
+ semanticIndices[index] = index;
1071
+ }
1072
+ output.push({
1073
+ id: `instance:${group[0].id}`,
1074
+ geometry,
1075
+ material,
1076
+ instanceMatrices,
1077
+ semanticIds: group.map((renderable) => renderable.mesh.selectionId),
1078
+ semanticIndices,
1079
+ meshIds: group.map((renderable) => renderable.id),
1080
+ materialKey: `${material.color}:${material.edgeColor ?? ""}`
1081
+ });
1082
+ }
1083
+ }
1084
+ return output;
1085
+ }
1086
+
1029
1087
  // packages/renderer/dist/WebGL2InstanceBatch.js
1030
1088
  var WebGL2InstanceCache = class {
1031
1089
  entries = /* @__PURE__ */ new Map();
@@ -1135,40 +1193,6 @@ var WebGL2InstanceCache = class {
1135
1193
  this.gl.deleteVertexArray(entry.vertexArray);
1136
1194
  }
1137
1195
  };
1138
- function collectInstanceBatches(renderables, output = []) {
1139
- output.length = 0;
1140
- const byGeometry = /* @__PURE__ */ new Map();
1141
- for (const renderable of renderables) {
1142
- const byMaterial = byGeometry.get(renderable.mesh.geometry) ?? /* @__PURE__ */ new Map();
1143
- const group = byMaterial.get(renderable.mesh.material) ?? [];
1144
- group.push(renderable);
1145
- byMaterial.set(renderable.mesh.material, group);
1146
- byGeometry.set(renderable.mesh.geometry, byMaterial);
1147
- }
1148
- for (const [geometry, byMaterial] of byGeometry) {
1149
- for (const [material, group] of byMaterial) {
1150
- if (group.length < 2)
1151
- continue;
1152
- const instanceMatrices = new Float32Array(group.length * 16);
1153
- const semanticIndices = new Uint32Array(group.length);
1154
- for (let index = 0; index < group.length; index += 1) {
1155
- instanceMatrices.set(group[index].worldMatrix, index * 16);
1156
- semanticIndices[index] = index;
1157
- }
1158
- output.push({
1159
- id: `instance:${group[0].id}`,
1160
- geometry,
1161
- material,
1162
- instanceMatrices,
1163
- semanticIds: group.map((renderable) => renderable.mesh.selectionId),
1164
- semanticIndices,
1165
- meshIds: group.map((renderable) => renderable.id),
1166
- materialKey: `${material.color}:${material.edgeColor ?? ""}`
1167
- });
1168
- }
1169
- }
1170
- return output;
1171
- }
1172
1196
 
1173
1197
  // packages/renderer/dist/WebGL2GeometryCache.js
1174
1198
  var WebGL2GeometryCache = class {
@@ -1682,16 +1706,7 @@ var WebGL2Picker = class {
1682
1706
  }
1683
1707
  };
1684
1708
 
1685
- // packages/renderer/dist/WebGL2RendererBackend.js
1686
- var EDGE_PIXEL_OFFSETS = [
1687
- [0, 0],
1688
- [-1, 0],
1689
- [1, 0],
1690
- [0, -1],
1691
- [0, 1]
1692
- ];
1693
- var SELECTION_RGBA = [255 / 255, 208 / 255, 0];
1694
- var SELECTION_TINT_ALPHA = 0.65;
1709
+ // packages/renderer/dist/Color.js
1695
1710
  var COLOR_CACHE_LIMIT = 128;
1696
1711
  var COLOR_CACHE = /* @__PURE__ */ new Map();
1697
1712
  function colorToRgba(color) {
@@ -1718,6 +1733,54 @@ function colorToRgba(color) {
1718
1733
  COLOR_CACHE.set(key, rgba);
1719
1734
  return rgba;
1720
1735
  }
1736
+
1737
+ // packages/renderer/dist/RendererProfiler.js
1738
+ var RendererProfiler = class {
1739
+ enabled = false;
1740
+ cursor = 0;
1741
+ marks = /* @__PURE__ */ new Map();
1742
+ enable(value) {
1743
+ this.enabled = value;
1744
+ this.marks.clear();
1745
+ }
1746
+ get isEnabled() {
1747
+ return this.enabled;
1748
+ }
1749
+ begin() {
1750
+ if (!this.enabled)
1751
+ return;
1752
+ this.cursor = performance.now();
1753
+ this.marks.clear();
1754
+ }
1755
+ mark(stage) {
1756
+ if (!this.enabled)
1757
+ return;
1758
+ const now = performance.now();
1759
+ this.marks.set(stage, Math.max(0, now - this.cursor));
1760
+ this.cursor = now;
1761
+ }
1762
+ end() {
1763
+ if (!this.enabled)
1764
+ return null;
1765
+ return {
1766
+ collectMs: this.marks.get("collect") ?? 0,
1767
+ prepareAndUploadMs: this.marks.get("prepareAndUpload") ?? 0,
1768
+ encodeMs: this.marks.get("encode") ?? 0,
1769
+ submitMs: this.marks.get("submit") ?? Math.max(0, performance.now() - this.cursor)
1770
+ };
1771
+ }
1772
+ };
1773
+
1774
+ // packages/renderer/dist/WebGL2RendererBackend.js
1775
+ var EDGE_PIXEL_OFFSETS = [
1776
+ [0, 0],
1777
+ [-1, 0],
1778
+ [1, 0],
1779
+ [0, -1],
1780
+ [0, 1]
1781
+ ];
1782
+ var SELECTION_RGBA = [255 / 255, 208 / 255, 0];
1783
+ var SELECTION_TINT_ALPHA = 0.65;
1721
1784
  var WebGL2RendererBackend = class {
1722
1785
  capabilities = {
1723
1786
  backend: "webgl2",
@@ -1741,6 +1804,8 @@ var WebGL2RendererBackend = class {
1741
1804
  batchesScratch = [];
1742
1805
  instancesScratch = [];
1743
1806
  instancedMeshIds = /* @__PURE__ */ new Set();
1807
+ profiler = new RendererProfiler();
1808
+ lastFrameProfile = null;
1744
1809
  backgroundRgba = [
1745
1810
  0.035,
1746
1811
  0.055,
@@ -1830,6 +1895,8 @@ var WebGL2RendererBackend = class {
1830
1895
  if (!this.gl || !this.program || !this.geometryCache) {
1831
1896
  throw new ICEEngineError("ICE3_ENGINE_NOT_READY", "Renderer is not initialized");
1832
1897
  }
1898
+ this.profiler.begin();
1899
+ this.lastFrameProfile = null;
1833
1900
  const gl = this.gl;
1834
1901
  gl.clearColor(...this.backgroundRgba);
1835
1902
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
@@ -1855,7 +1922,10 @@ var WebGL2RendererBackend = class {
1855
1922
  const clipPixelY = 2 / Math.max(1, frame.viewport.height * frame.dpr);
1856
1923
  const renderables = collectRenderables(frame.scene, this.renderablesScratch);
1857
1924
  const instances = collectInstanceBatches(renderables, this.instancesScratch);
1925
+ const batches = collectRenderBatches(frame.scene, this.batchesScratch);
1926
+ this.profiler.mark("collect");
1858
1927
  this.program.setInstancing(true);
1928
+ this.profiler.mark("prepareAndUpload");
1859
1929
  for (const batch of instances) {
1860
1930
  const instance = this.instanceCache.retain(gl, this.geometryCache, batch);
1861
1931
  if (!instance)
@@ -1883,7 +1953,6 @@ var WebGL2RendererBackend = class {
1883
1953
  }
1884
1954
  this.program.setInstancing(false);
1885
1955
  this.program.setClipOffset(0, 0);
1886
- const batches = collectRenderBatches(frame.scene, this.batchesScratch);
1887
1956
  for (const draw of batches) {
1888
1957
  this.program.setClipOffset(0, 0);
1889
1958
  mvp.elements.set(viewProjection.elements);
@@ -2031,14 +2100,40 @@ var WebGL2RendererBackend = class {
2031
2100
  this.frameStats.gpuUploads = this.geometryCache.uploadCount;
2032
2101
  this.frameStats.gpuEvictions = this.geometryCache.evictionCount;
2033
2102
  this.frameStats.gpuBytes = this.geometryCache.totalBytes + this.instanceCache.totalBytes;
2103
+ this.profiler.mark("encode");
2104
+ this.profiler.mark("submit");
2105
+ this.lastFrameProfile = this.profiler.end();
2106
+ }
2107
+ enableProfiling(enabled) {
2108
+ this.profiler.enable(enabled);
2109
+ this.lastFrameProfile = null;
2034
2110
  }
2035
- pick(frame, point) {
2111
+ get frameProfile() {
2112
+ return this.lastFrameProfile;
2113
+ }
2114
+ async pick(frame, point) {
2036
2115
  if (!this.picker || !this.geometryCache) {
2037
2116
  throw new ICEEngineError("ICE3_PICK_UNAVAILABLE", "ICE3_PICK_UNAVAILABLE: picker is not initialized");
2038
2117
  }
2039
2118
  const renderables = collectRenderables(frame.scene, this.renderablesScratch);
2040
2119
  return this.picker.pick({ ...frame, point }, this.geometryCache, renderables, collectRenderBatches(frame.scene, this.batchesScratch), collectInstanceBatches(renderables, this.instancesScratch), this.instanceCache);
2041
2120
  }
2121
+ async capture() {
2122
+ if (!this.canvas) {
2123
+ throw new ICEEngineError("ICE3_ENGINE_NOT_READY", "Renderer is not initialized");
2124
+ }
2125
+ return new Promise((resolve, reject) => {
2126
+ this.canvas.toBlob((blob) => {
2127
+ if (blob)
2128
+ resolve(blob);
2129
+ else
2130
+ reject(new Error("ICE3_CAPTURE_UNAVAILABLE: PNG encoding failed"));
2131
+ }, "image/png");
2132
+ });
2133
+ }
2134
+ async finish() {
2135
+ this.gl?.finish();
2136
+ }
2042
2137
  dispose() {
2043
2138
  if (this.canvas) {
2044
2139
  this.canvas.removeEventListener("webglcontextlost", this.handleContextLost);
@@ -2081,73 +2176,2983 @@ var WebGL2RendererBackend = class {
2081
2176
  }
2082
2177
  };
2083
2178
 
2084
- // packages/renderer/dist/ICEWebGL2BatchGenerationUploader.js
2085
- function queueBatchGeneration(buffers, options) {
2086
- if (!Number.isInteger(options.generation) || options.generation < 0 || !Number.isInteger(options.bytesPerFrame) || options.bytesPerFrame < 1 || buffers.some((buffer) => !Number.isInteger(buffer.byteLength) || buffer.byteLength < 1 || buffer.byteLength > options.bytesPerFrame)) {
2087
- fail(options, new Error("ICE3_RENDER_BATCH_UPLOAD_INVALID: upload options are invalid"));
2088
- return { cancel: () => void 0 };
2089
- }
2090
- const schedule = options.schedule ?? globalThis.requestAnimationFrame;
2091
- const cancelSchedule = options.cancelSchedule ?? globalThis.cancelAnimationFrame;
2092
- const upload = options.upload ?? ((buffer) => buffer);
2093
- const release = options.release ?? (() => void 0);
2094
- const resources = [];
2095
- let next = 0;
2096
- let frame;
2097
- let settled = false;
2098
- const cleanup = () => {
2099
- if (frame !== void 0) {
2100
- cancelSchedule(frame);
2101
- frame = void 0;
2179
+ // packages/renderer/dist/WebGPUProbe.js
2180
+ var WEBGPU_TIMEOUT_MS = 3e3;
2181
+ var REQUIRED_LIMITS = [
2182
+ ["maxTextureDimension2D", 4096],
2183
+ ["maxBufferSize", 268435456],
2184
+ ["maxVertexBuffers", 2],
2185
+ ["maxBindGroups", 2]
2186
+ ];
2187
+ var PROBE_SHADER = `
2188
+ @vertex
2189
+ fn vertexMain(@builtin(vertex_index) index: u32) -> @builtin(position) vec4<f32> {
2190
+ var positions = array<vec2<f32>, 3>(
2191
+ vec2<f32>(-1.0, -1.0),
2192
+ vec2<f32>(3.0, -1.0),
2193
+ vec2<f32>(-1.0, 3.0),
2194
+ );
2195
+ return vec4<f32>(positions[index], 0.0, 1.0);
2196
+ }
2197
+
2198
+ @fragment
2199
+ fn fragmentMain() -> @location(0) vec4<f32> {
2200
+ return vec4<f32>(0.0, 0.0, 0.0, 1.0);
2201
+ }
2202
+ `;
2203
+ var successfulProbes = /* @__PURE__ */ new WeakMap();
2204
+ function probeWebGPU(options = {}) {
2205
+ const gpu = options.gpu ?? (typeof navigator === "undefined" ? void 0 : navigator.gpu);
2206
+ if (!gpu) {
2207
+ return Promise.resolve(failure("api-unavailable", "navigator.gpu is unavailable"));
2208
+ }
2209
+ const featureKey = [...options.optionalFeatures ?? []].sort().join("|");
2210
+ const probes = successfulProbes.get(gpu);
2211
+ const cached = probes?.get(featureKey);
2212
+ if (cached)
2213
+ return cached;
2214
+ const probe = executeProbe(gpu, options).then((result) => result.supported ? {
2215
+ ...result,
2216
+ release: () => {
2217
+ const current = successfulProbes.get(gpu);
2218
+ current?.delete(featureKey);
2219
+ if (current?.size === 0)
2220
+ successfulProbes.delete(gpu);
2221
+ }
2222
+ } : result);
2223
+ const featureProbes = probes ?? /* @__PURE__ */ new Map();
2224
+ featureProbes.set(featureKey, probe);
2225
+ successfulProbes.set(gpu, featureProbes);
2226
+ void probe.then((result) => {
2227
+ if (!result.supported) {
2228
+ featureProbes.delete(featureKey);
2229
+ if (featureProbes.size === 0)
2230
+ successfulProbes.delete(gpu);
2102
2231
  }
2103
- for (const resource of resources.splice(0))
2104
- release(resource);
2105
- };
2106
- const current = () => options.isCurrent?.() ?? true;
2107
- const failNow = (error) => {
2108
- if (settled)
2109
- return;
2110
- settled = true;
2111
- cleanup();
2112
- options.onFailed(options.generation, error);
2113
- };
2114
- const pump = () => {
2115
- frame = void 0;
2116
- if (settled)
2117
- return;
2118
- if (!current()) {
2119
- settled = true;
2120
- cleanup();
2121
- return;
2232
+ });
2233
+ return probe;
2234
+ }
2235
+ async function executeProbe(gpu, options) {
2236
+ const timeoutMs = options.timeoutMs ?? WEBGPU_TIMEOUT_MS;
2237
+ const timeout = /* @__PURE__ */ Symbol("timeout");
2238
+ let timeoutId;
2239
+ let adapter;
2240
+ try {
2241
+ adapter = await Promise.race([
2242
+ gpu.requestAdapter({ powerPreference: "high-performance" }),
2243
+ new Promise((resolve) => {
2244
+ timeoutId = setTimeout(() => resolve(timeout), timeoutMs);
2245
+ })
2246
+ ]);
2247
+ } catch (error) {
2248
+ if (timeoutId !== void 0)
2249
+ clearTimeout(timeoutId);
2250
+ return failure("adapter-unavailable", errorMessage(error));
2251
+ }
2252
+ if (timeoutId !== void 0)
2253
+ clearTimeout(timeoutId);
2254
+ if (adapter === timeout) {
2255
+ return failure("adapter-timeout", `Adapter lookup exceeded ${timeoutMs}ms`);
2256
+ }
2257
+ if (!adapter) {
2258
+ return failure("adapter-unavailable", "requestAdapter returned null");
2259
+ }
2260
+ for (const [name, minimum] of REQUIRED_LIMITS) {
2261
+ const value = adapter.limits[name];
2262
+ if (typeof value !== "number" || value < minimum) {
2263
+ return failure("limit-too-low", `${name} requires at least ${minimum}; received ${String(value)}`);
2122
2264
  }
2123
- let bytes = 0;
2124
- try {
2125
- while (next < buffers.length) {
2126
- const buffer = buffers[next];
2127
- if (resources.length > 0 && bytes + buffer.byteLength > options.bytesPerFrame) {
2128
- break;
2129
- }
2130
- bytes += buffer.byteLength;
2131
- next += 1;
2132
- resources.push(upload(buffer));
2133
- }
2134
- } catch (error) {
2135
- failNow(error);
2136
- return;
2265
+ }
2266
+ let device;
2267
+ try {
2268
+ const features = (options.optionalFeatures ?? []).filter((feature) => adapter.features.has(feature));
2269
+ device = await adapter.requestDevice({
2270
+ ...features.length > 0 ? { requiredFeatures: features } : {}
2271
+ });
2272
+ } catch (error) {
2273
+ return failure("device-failed", errorMessage(error));
2274
+ }
2275
+ const canvas = createProbeCanvas(options.createCanvas);
2276
+ if (!canvas) {
2277
+ device.destroy();
2278
+ return failure("document-unavailable", "No Canvas implementation is available");
2279
+ }
2280
+ let context;
2281
+ try {
2282
+ context = canvas.getContext("webgpu");
2283
+ } catch (error) {
2284
+ device.destroy();
2285
+ return failure("context-failed", errorMessage(error));
2286
+ }
2287
+ if (!context) {
2288
+ device.destroy();
2289
+ return failure("context-failed", "WebGPU canvas context is unavailable");
2290
+ }
2291
+ try {
2292
+ const format = gpu.getPreferredCanvasFormat();
2293
+ context.configure({
2294
+ device,
2295
+ format,
2296
+ alphaMode: "premultiplied"
2297
+ });
2298
+ const module = device.createShaderModule({ code: PROBE_SHADER });
2299
+ const pipeline = await device.createRenderPipelineAsync({
2300
+ layout: "auto",
2301
+ vertex: {
2302
+ module,
2303
+ entryPoint: "vertexMain"
2304
+ },
2305
+ fragment: {
2306
+ module,
2307
+ entryPoint: "fragmentMain",
2308
+ targets: [{ format }]
2309
+ },
2310
+ primitive: { topology: "triangle-list" }
2311
+ });
2312
+ const texture = context.getCurrentTexture();
2313
+ const encoder = device.createCommandEncoder();
2314
+ const pass = encoder.beginRenderPass({
2315
+ colorAttachments: [{
2316
+ view: texture.createView(),
2317
+ clearValue: { r: 0, g: 0, b: 0, a: 1 },
2318
+ loadOp: "clear",
2319
+ storeOp: "store"
2320
+ }]
2321
+ });
2322
+ pass.setPipeline(pipeline);
2323
+ pass.draw(3);
2324
+ pass.end();
2325
+ device.queue.submit([encoder.finish()]);
2326
+ const lost = await Promise.race([
2327
+ device.lost.then((info) => info.message || info.reason),
2328
+ new Promise((resolve) => {
2329
+ setTimeout(() => resolve(false), 0);
2330
+ })
2331
+ ]);
2332
+ if (lost) {
2333
+ device.destroy();
2334
+ return failure("device-lost", lost);
2137
2335
  }
2138
- if (next >= buffers.length) {
2139
- if (!current()) {
2140
- settled = true;
2141
- cleanup();
2142
- return;
2143
- }
2144
- try {
2145
- options.onActivate?.(options.generation, resources);
2146
- } catch (error) {
2147
- failNow(error);
2148
- return;
2149
- }
2150
- settled = true;
2336
+ return {
2337
+ supported: true,
2338
+ adapter,
2339
+ device,
2340
+ format,
2341
+ enabledFeatures: new Set(Array.from(device.features)),
2342
+ release: () => void 0
2343
+ };
2344
+ } catch (error) {
2345
+ device.destroy();
2346
+ return failure("pipeline-failed", errorMessage(error));
2347
+ }
2348
+ }
2349
+ function createProbeCanvas(createCanvas) {
2350
+ if (createCanvas)
2351
+ return createCanvas();
2352
+ if (typeof OffscreenCanvas !== "undefined") {
2353
+ return new OffscreenCanvas(64, 64);
2354
+ }
2355
+ if (typeof document !== "undefined") {
2356
+ const canvas = document.createElement("canvas");
2357
+ canvas.width = 64;
2358
+ canvas.height = 64;
2359
+ return canvas;
2360
+ }
2361
+ return void 0;
2362
+ }
2363
+ function failure(reason, detail) {
2364
+ return {
2365
+ supported: false,
2366
+ reason,
2367
+ ...detail ? { detail } : {}
2368
+ };
2369
+ }
2370
+ function errorMessage(error) {
2371
+ return error instanceof Error ? error.message : String(error);
2372
+ }
2373
+
2374
+ // packages/renderer/dist/WebGPUResources.js
2375
+ var WEBGPU_BUFFER_USAGE = {
2376
+ vertex: 32 | 8,
2377
+ index: 16 | 8
2378
+ };
2379
+ function alignToFourBytes(value) {
2380
+ return Math.ceil(value / 4) * 4;
2381
+ }
2382
+ function createGPUDataBuffer(device, data, usage, label) {
2383
+ const byteLength = alignToFourBytes(data.byteLength);
2384
+ let buffer;
2385
+ try {
2386
+ buffer = device.createBuffer({
2387
+ label,
2388
+ size: byteLength,
2389
+ usage,
2390
+ mappedAtCreation: false
2391
+ });
2392
+ const upload = data.byteLength === byteLength ? data : copyAndPad(data, byteLength);
2393
+ device.queue.writeBuffer(buffer, 0, upload);
2394
+ return buffer;
2395
+ } catch (error) {
2396
+ buffer?.destroy();
2397
+ throw new ICEEngineError("ICE3_GPU_RESOURCE_FAILED", `ICE3_GPU_RESOURCE_FAILED: ${label}`, { cause: error });
2398
+ }
2399
+ }
2400
+ function createEmptyGPUDataBuffer(device, byteLength, usage, label) {
2401
+ try {
2402
+ return device.createBuffer({
2403
+ label,
2404
+ size: Math.max(4, alignToFourBytes(byteLength)),
2405
+ usage,
2406
+ mappedAtCreation: false
2407
+ });
2408
+ } catch (error) {
2409
+ throw new ICEEngineError("ICE3_GPU_RESOURCE_FAILED", `ICE3_GPU_RESOURCE_FAILED: ${label}`, { cause: error });
2410
+ }
2411
+ }
2412
+ function writeGPUData(device, buffer, data) {
2413
+ try {
2414
+ device.queue.writeBuffer(buffer, 0, data);
2415
+ } catch (error) {
2416
+ throw new ICEEngineError("ICE3_GPU_RESOURCE_FAILED", "ICE3_GPU_RESOURCE_FAILED: buffer upload failed", { cause: error });
2417
+ }
2418
+ }
2419
+ function copyAndPad(data, byteLength) {
2420
+ const output = new Uint8Array(byteLength);
2421
+ output.set(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
2422
+ return output;
2423
+ }
2424
+
2425
+ // packages/renderer/dist/WebGPUGeometryCache.js
2426
+ var WebGPUGeometryCache = class {
2427
+ device;
2428
+ entries = /* @__PURE__ */ new Map();
2429
+ active = /* @__PURE__ */ new Set();
2430
+ pinned = /* @__PURE__ */ new Set();
2431
+ inactiveGraceFrames;
2432
+ maxBytes;
2433
+ uploads = 0;
2434
+ evictions = 0;
2435
+ frameId = 0;
2436
+ resourceGeneration = 0;
2437
+ constructor(options) {
2438
+ this.device = options.device;
2439
+ this.inactiveGraceFrames = options.inactiveGraceFrames ?? 60;
2440
+ this.maxBytes = options.maxBytes ?? 768 * 1024 * 1024;
2441
+ if (!Number.isInteger(this.inactiveGraceFrames) || this.inactiveGraceFrames < 0 || !Number.isInteger(this.maxBytes) || this.maxBytes < 1) {
2442
+ throw new Error("ICE3_GPU_CACHE_INVALID: cache options are invalid");
2443
+ }
2444
+ }
2445
+ retain(geometry) {
2446
+ this.active.add(geometry);
2447
+ const existing = this.entries.get(geometry);
2448
+ if (existing) {
2449
+ existing.lastUsedFrame = this.frameId;
2450
+ return existing;
2451
+ }
2452
+ const buffers = [];
2453
+ try {
2454
+ const vertexCount = geometry.positions.length / 3;
2455
+ const vertices = new Float32Array(vertexCount * 6);
2456
+ const colorStride = geometry.colors ? geometry.colors.length / vertexCount : 0;
2457
+ for (let index = 0; index < vertexCount; index += 1) {
2458
+ vertices[index * 6] = geometry.positions[index * 3];
2459
+ vertices[index * 6 + 1] = geometry.positions[index * 3 + 1];
2460
+ vertices[index * 6 + 2] = geometry.positions[index * 3 + 2];
2461
+ vertices[index * 6 + 3] = geometry.colors?.[index * colorStride] ?? 1;
2462
+ vertices[index * 6 + 4] = geometry.colors?.[index * colorStride + 1] ?? 1;
2463
+ vertices[index * 6 + 5] = geometry.colors?.[index * colorStride + 2] ?? 1;
2464
+ }
2465
+ const vertexBuffer = createGPUDataBuffer(this.device, vertices, WEBGPU_BUFFER_USAGE.vertex, `${geometry.typeId}:vertices`);
2466
+ buffers.push(vertexBuffer);
2467
+ const triangleIndexBuffer = createGPUDataBuffer(this.device, geometry.indices, WEBGPU_BUFFER_USAGE.index, `${geometry.typeId}:triangle-indices`);
2468
+ buffers.push(triangleIndexBuffer);
2469
+ const edgeIndexBuffer = geometry.edgeIndices ? createGPUDataBuffer(this.device, geometry.edgeIndices, WEBGPU_BUFFER_USAGE.index, `${geometry.typeId}:edge-indices`) : null;
2470
+ if (edgeIndexBuffer)
2471
+ buffers.push(edgeIndexBuffer);
2472
+ const semanticBuffer = geometry.semanticIndices ? createGPUDataBuffer(this.device, geometry.semanticIndices, WEBGPU_BUFFER_USAGE.vertex, `${geometry.typeId}:semantic`) : null;
2473
+ if (semanticBuffer)
2474
+ buffers.push(semanticBuffer);
2475
+ const entry = {
2476
+ vertexBuffer,
2477
+ triangleIndexBuffer,
2478
+ edgeIndexBuffer,
2479
+ semanticBuffer,
2480
+ triangleCount: geometry.indices.length,
2481
+ edgeCount: geometry.edgeIndices?.length ?? 0,
2482
+ indexFormat: geometry.indices instanceof Uint16Array ? "uint16" : "uint32",
2483
+ vertexCount,
2484
+ bytes: buffers.reduce((sum, buffer) => sum + buffer.size, 0),
2485
+ lastUsedFrame: this.frameId
2486
+ };
2487
+ this.entries.set(geometry, entry);
2488
+ this.uploads += buffers.length;
2489
+ this.resourceGeneration += 1;
2490
+ return entry;
2491
+ } catch (error) {
2492
+ for (const buffer of buffers)
2493
+ buffer.destroy();
2494
+ if (error instanceof ICEEngineError)
2495
+ throw error;
2496
+ throw new ICEEngineError("ICE3_GPU_RESOURCE_FAILED", "ICE3_GPU_RESOURCE_FAILED: geometry upload failed", { cause: error });
2497
+ }
2498
+ }
2499
+ pin(geometry) {
2500
+ this.pinned.add(geometry);
2501
+ const entry = this.retain(geometry);
2502
+ this.enforceBudget();
2503
+ if (this.totalBytes > this.maxBytes) {
2504
+ this.pinned.delete(geometry);
2505
+ this.deleteEntry(geometry, entry);
2506
+ throw new ICEEngineError("ICE3_GPU_RESOURCE_FAILED", "ICE3_GPU_RESOURCE_FAILED: geometry exceeds the GPU residency budget");
2507
+ }
2508
+ return entry;
2509
+ }
2510
+ unpin(geometry) {
2511
+ this.pinned.delete(geometry);
2512
+ }
2513
+ beginFrame() {
2514
+ this.frameId += 1;
2515
+ this.active = /* @__PURE__ */ new Set();
2516
+ }
2517
+ endFrame() {
2518
+ for (const [geometry, entry] of this.entries) {
2519
+ if (this.active.has(geometry) || this.pinned.has(geometry))
2520
+ continue;
2521
+ if (this.frameId - entry.lastUsedFrame < this.inactiveGraceFrames) {
2522
+ continue;
2523
+ }
2524
+ this.deleteEntry(geometry, entry);
2525
+ }
2526
+ this.enforceBudget();
2527
+ }
2528
+ clear() {
2529
+ for (const [geometry, entry] of this.entries) {
2530
+ this.deleteEntry(geometry, entry);
2531
+ }
2532
+ this.active.clear();
2533
+ this.pinned.clear();
2534
+ }
2535
+ dispose() {
2536
+ this.clear();
2537
+ }
2538
+ get totalBytes() {
2539
+ return [...this.entries.values()].reduce((sum, entry) => sum + entry.bytes, 0);
2540
+ }
2541
+ get uploadCount() {
2542
+ return this.uploads;
2543
+ }
2544
+ get evictionCount() {
2545
+ return this.evictions;
2546
+ }
2547
+ get generation() {
2548
+ return this.resourceGeneration;
2549
+ }
2550
+ enforceBudget() {
2551
+ let total = this.totalBytes;
2552
+ if (total <= this.maxBytes)
2553
+ return;
2554
+ const candidates = [...this.entries].filter(([geometry]) => !this.pinned.has(geometry)).sort((left, right) => left[1].lastUsedFrame - right[1].lastUsedFrame);
2555
+ for (const [geometry, entry] of candidates) {
2556
+ if (total <= this.maxBytes)
2557
+ break;
2558
+ this.deleteEntry(geometry, entry);
2559
+ total -= entry.bytes;
2560
+ }
2561
+ }
2562
+ deleteEntry(geometry, entry) {
2563
+ entry.vertexBuffer.destroy();
2564
+ entry.triangleIndexBuffer.destroy();
2565
+ entry.edgeIndexBuffer?.destroy();
2566
+ entry.semanticBuffer?.destroy();
2567
+ this.entries.delete(geometry);
2568
+ this.evictions += 1;
2569
+ this.resourceGeneration += 1;
2570
+ }
2571
+ };
2572
+
2573
+ // packages/renderer/dist/WebGPUInstanceCache.js
2574
+ var WebGPUInstanceCache = class {
2575
+ device;
2576
+ entries = /* @__PURE__ */ new Map();
2577
+ active = /* @__PURE__ */ new Set();
2578
+ bufferGeneration = 0;
2579
+ constructor(device) {
2580
+ this.device = device;
2581
+ }
2582
+ retain(batch) {
2583
+ this.active.add(batch.id);
2584
+ const matrixSize = batch.instanceMatrices.byteLength;
2585
+ const semanticSize = batch.semanticIndices.byteLength;
2586
+ let entry = this.entries.get(batch.id);
2587
+ if (!entry || entry.matrixCapacity < matrixSize || entry.semanticCapacity < semanticSize) {
2588
+ if (entry)
2589
+ this.deleteEntry(entry);
2590
+ entry = {
2591
+ matrixBuffer: createEmptyGPUDataBuffer(this.device, matrixSize, WEBGPU_BUFFER_USAGE.vertex, `${batch.id}:matrices`),
2592
+ semanticBuffer: createEmptyGPUDataBuffer(this.device, semanticSize, WEBGPU_BUFFER_USAGE.vertex, `${batch.id}:semantic`),
2593
+ instanceCount: batch.semanticIds.length,
2594
+ bytes: Math.ceil(matrixSize / 4) * 4 + Math.ceil(semanticSize / 4) * 4,
2595
+ generation: ++this.bufferGeneration,
2596
+ matrixCapacity: Math.max(4, Math.ceil(matrixSize / 4) * 4),
2597
+ semanticCapacity: Math.max(4, Math.ceil(semanticSize / 4) * 4)
2598
+ };
2599
+ this.entries.set(batch.id, entry);
2600
+ }
2601
+ if (entry.matrixSource !== batch.instanceMatrices) {
2602
+ const matrixBytes = toBytes(batch.instanceMatrices);
2603
+ if (!entry.matrixBytes || !sameBytes(entry.matrixBytes, matrixBytes)) {
2604
+ writeGPUData(this.device, entry.matrixBuffer, batch.instanceMatrices);
2605
+ }
2606
+ entry.matrixBytes = matrixBytes;
2607
+ entry.matrixSource = batch.instanceMatrices;
2608
+ }
2609
+ if (entry.semanticSource !== batch.semanticIndices) {
2610
+ const semanticBytes = toBytes(batch.semanticIndices);
2611
+ if (!entry.semanticBytes || !sameBytes(entry.semanticBytes, semanticBytes)) {
2612
+ writeGPUData(this.device, entry.semanticBuffer, batch.semanticIndices);
2613
+ }
2614
+ entry.semanticBytes = semanticBytes;
2615
+ entry.semanticSource = batch.semanticIndices;
2616
+ }
2617
+ entry.instanceCount = batch.semanticIds.length;
2618
+ return entry;
2619
+ }
2620
+ beginFrame() {
2621
+ this.active = /* @__PURE__ */ new Set();
2622
+ }
2623
+ endFrame() {
2624
+ for (const [id, entry] of this.entries) {
2625
+ if (this.active.has(id))
2626
+ continue;
2627
+ this.deleteEntry(entry);
2628
+ this.entries.delete(id);
2629
+ }
2630
+ }
2631
+ clear() {
2632
+ for (const entry of this.entries.values()) {
2633
+ this.deleteEntry(entry);
2634
+ }
2635
+ this.active.clear();
2636
+ }
2637
+ dispose() {
2638
+ this.clear();
2639
+ }
2640
+ get totalBytes() {
2641
+ return [...this.entries.values()].reduce((sum, entry) => sum + entry.bytes, 0);
2642
+ }
2643
+ get generation() {
2644
+ return this.bufferGeneration;
2645
+ }
2646
+ deleteEntry(entry) {
2647
+ entry.matrixBuffer.destroy();
2648
+ entry.semanticBuffer.destroy();
2649
+ this.bufferGeneration += 1;
2650
+ }
2651
+ };
2652
+ function toBytes(data) {
2653
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength).slice();
2654
+ }
2655
+ function sameBytes(left, right) {
2656
+ if (left.byteLength !== right.byteLength)
2657
+ return false;
2658
+ for (let index = 0; index < left.byteLength; index += 1) {
2659
+ if (left[index] !== right[index])
2660
+ return false;
2661
+ }
2662
+ return true;
2663
+ }
2664
+
2665
+ // packages/renderer/dist/WebGPURenderPipeline.js
2666
+ var WEBGPU_UNIFORM_STRIDE = 256;
2667
+ var WEBGPU_UNIFORM_BYTES = 128;
2668
+ var SHADER_SOURCE = (selectionMask, storageInstances, indirectInstances) => `
2669
+ struct FrameUniforms {
2670
+ worldViewProjection: mat4x4<f32>,
2671
+ color: vec4<f32>,
2672
+ clipOffset: vec2<f32>,
2673
+ useVertexColor: f32,
2674
+ useInstancing: f32,
2675
+ highlightSemantic: f32,
2676
+ useSemantic: f32,
2677
+ pickMode: f32,
2678
+ pickBase: f32,
2679
+ selectionBase: f32,
2680
+ useSelectionMask: f32,
2681
+ pageBase: f32,
2682
+ useIndirect: f32,
2683
+ }
2684
+
2685
+ ${storageInstances ? `
2686
+ struct InstanceRecord {
2687
+ transform: mat4x4<f32>,
2688
+ bounds: vec4<f32>,
2689
+ semantic: u32,
2690
+ pad0: u32,
2691
+ pad1: u32,
2692
+ pad2: u32,
2693
+ }` : ""}
2694
+
2695
+ struct VertexOutput {
2696
+ @builtin(position) position: vec4<f32>,
2697
+ @location(0) color: vec4<f32>,
2698
+ @location(1) @interpolate(flat) semantic: u32,
2699
+ }
2700
+
2701
+ @group(0) @binding(0) var<uniform> frame: FrameUniforms;
2702
+ ${selectionMask ? "@group(0) @binding(1) var<storage, read> selection: array<u32>;" : ""}
2703
+ ${storageInstances ? "@group(0) @binding(2) var<storage, read> instances: array<InstanceRecord>;" : ""}
2704
+ ${indirectInstances ? "@group(0) @binding(3) var<storage, read> visibleIndices: array<u32>;" : ""}
2705
+
2706
+ @vertex
2707
+ fn vertexMain(
2708
+ @location(0) position: vec3<f32>,
2709
+ @location(1) color: vec3<f32>,
2710
+ ${storageInstances ? `
2711
+ @builtin(instance_index) instanceIndex: u32,
2712
+ @location(2) vertexSemantic: u32,
2713
+ ` : `
2714
+ @location(2) instanceColumn0: vec4<f32>,
2715
+ @location(3) instanceColumn1: vec4<f32>,
2716
+ @location(4) instanceColumn2: vec4<f32>,
2717
+ @location(5) instanceColumn3: vec4<f32>,
2718
+ @location(6) instanceSemantic: u32,
2719
+ @location(7) vertexSemantic: u32,
2720
+ `}
2721
+ ) -> VertexOutput {
2722
+ ${storageInstances ? `
2723
+ let visibleIndex = ${indirectInstances ? "visibleIndices[instanceIndex]" : "instanceIndex - u32(frame.pageBase)"};
2724
+ let recordIndex = visibleIndex;
2725
+ let record = instances[recordIndex];
2726
+ let instanceMatrix = record.transform;
2727
+ let instanceSemantic = record.semantic;
2728
+ let localPosition = vec4<f32>(position, 1.0);
2729
+ let worldPosition = instanceMatrix * localPosition;
2730
+ ` : `
2731
+ let instanceMatrix = mat4x4<f32>(
2732
+ instanceColumn0,
2733
+ instanceColumn1,
2734
+ instanceColumn2,
2735
+ instanceColumn3
2736
+ );
2737
+ let localPosition = vec4<f32>(position, 1.0);
2738
+ let worldPosition = mix(
2739
+ localPosition,
2740
+ instanceMatrix * localPosition,
2741
+ frame.useInstancing
2742
+ );
2743
+ `}
2744
+ var output: VertexOutput;
2745
+ output.position = frame.worldViewProjection * worldPosition;
2746
+ output.position.xy += frame.clipOffset * output.position.w;
2747
+ output.color = vec4<f32>(
2748
+ mix(vec3<f32>(1.0), color, frame.useVertexColor) * frame.color.rgb,
2749
+ frame.color.a
2750
+ );
2751
+ output.semantic = select(
2752
+ vertexSemantic,
2753
+ instanceSemantic,
2754
+ ${storageInstances ? "true" : "frame.useInstancing > 0.5"}
2755
+ );
2756
+ return output;
2757
+ }
2758
+
2759
+ @fragment
2760
+ fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
2761
+ if (frame.pickMode > 0.5) {
2762
+ let id = u32(frame.pickBase) + input.semantic + 1u;
2763
+ return vec4<f32>(
2764
+ f32(id & 255u),
2765
+ f32((id >> 8u) & 255u),
2766
+ f32((id >> 16u) & 255u),
2767
+ 255.0
2768
+ ) / 255.0;
2769
+ }
2770
+ if (
2771
+ frame.useSemantic > 0.5
2772
+ && abs(f32(input.semantic) - frame.highlightSemantic) > 0.1
2773
+ ) {
2774
+ discard;
2775
+ }
2776
+ ${selectionMask ? `
2777
+ let isSelected =
2778
+ selection[u32(frame.selectionBase) + input.semantic] != 0u;
2779
+ if (frame.useSelectionMask > 0.5) {
2780
+ if (frame.useSemantic > 0.5 && !isSelected) {
2781
+ discard;
2782
+ }
2783
+ if (isSelected) {
2784
+ return vec4<f32>(
2785
+ mix(
2786
+ input.color.rgb,
2787
+ vec3<f32>(1.0, 208.0 / 255.0, 0.0),
2788
+ 0.65
2789
+ ),
2790
+ 1.0
2791
+ );
2792
+ }
2793
+ }` : ""}
2794
+ return input.color;
2795
+ }
2796
+ `;
2797
+ var VERTEX_LAYOUT = [
2798
+ {
2799
+ arrayStride: 24,
2800
+ stepMode: "vertex",
2801
+ attributes: [
2802
+ { shaderLocation: 0, offset: 0, format: "float32x3" },
2803
+ { shaderLocation: 1, offset: 12, format: "float32x3" }
2804
+ ]
2805
+ },
2806
+ {
2807
+ arrayStride: 80,
2808
+ stepMode: "instance",
2809
+ attributes: [
2810
+ { shaderLocation: 2, offset: 0, format: "float32x4" },
2811
+ { shaderLocation: 3, offset: 16, format: "float32x4" },
2812
+ { shaderLocation: 4, offset: 32, format: "float32x4" },
2813
+ { shaderLocation: 5, offset: 48, format: "float32x4" },
2814
+ { shaderLocation: 6, offset: 64, format: "uint32" }
2815
+ ]
2816
+ },
2817
+ {
2818
+ arrayStride: 4,
2819
+ stepMode: "vertex",
2820
+ attributes: [
2821
+ { shaderLocation: 7, offset: 0, format: "uint32" }
2822
+ ]
2823
+ }
2824
+ ];
2825
+ var STORAGE_VERTEX_LAYOUT = [
2826
+ VERTEX_LAYOUT[0],
2827
+ {
2828
+ arrayStride: 4,
2829
+ stepMode: "vertex",
2830
+ attributes: [
2831
+ { shaderLocation: 2, offset: 0, format: "uint32" }
2832
+ ]
2833
+ }
2834
+ ];
2835
+ var WebGPURenderPipeline = class _WebGPURenderPipeline {
2836
+ device;
2837
+ format;
2838
+ bindGroupLayout;
2839
+ pipelineLayout;
2840
+ triangle;
2841
+ line;
2842
+ selectionTriangle;
2843
+ selectionLine;
2844
+ selectionMask;
2845
+ storageInstances;
2846
+ indirectInstances;
2847
+ constructor(device, format, bindGroupLayout, pipelineLayout, triangle, line, selectionTriangle, selectionLine, selectionMask, storageInstances, indirectInstances) {
2848
+ this.device = device;
2849
+ this.format = format;
2850
+ this.bindGroupLayout = bindGroupLayout;
2851
+ this.pipelineLayout = pipelineLayout;
2852
+ this.triangle = triangle;
2853
+ this.line = line;
2854
+ this.selectionTriangle = selectionTriangle;
2855
+ this.selectionLine = selectionLine;
2856
+ this.selectionMask = selectionMask;
2857
+ this.storageInstances = storageInstances;
2858
+ this.indirectInstances = indirectInstances;
2859
+ }
2860
+ static async create(device, format, options = {}) {
2861
+ try {
2862
+ const storageInstances = Boolean(options.storageInstances);
2863
+ const indirectInstances = storageInstances && Boolean(options.indirectInstances);
2864
+ const selectionMask = Boolean(options.selectionMask || storageInstances);
2865
+ const bindGroupEntries = [{
2866
+ binding: 0,
2867
+ visibility: 3,
2868
+ buffer: {
2869
+ type: "uniform",
2870
+ hasDynamicOffset: true,
2871
+ minBindingSize: WEBGPU_UNIFORM_BYTES
2872
+ }
2873
+ }];
2874
+ if (selectionMask) {
2875
+ bindGroupEntries.push({
2876
+ binding: 1,
2877
+ visibility: 2,
2878
+ buffer: {
2879
+ type: "read-only-storage",
2880
+ minBindingSize: 4
2881
+ }
2882
+ });
2883
+ }
2884
+ if (storageInstances) {
2885
+ bindGroupEntries.push({
2886
+ binding: 2,
2887
+ visibility: 1,
2888
+ buffer: {
2889
+ type: "read-only-storage",
2890
+ minBindingSize: 96
2891
+ }
2892
+ });
2893
+ }
2894
+ if (indirectInstances) {
2895
+ bindGroupEntries.push({
2896
+ binding: 3,
2897
+ visibility: 1,
2898
+ buffer: {
2899
+ type: "read-only-storage",
2900
+ minBindingSize: 4
2901
+ }
2902
+ });
2903
+ }
2904
+ const bindGroupLayout = device.createBindGroupLayout({
2905
+ entries: bindGroupEntries
2906
+ });
2907
+ const pipelineLayout = device.createPipelineLayout({
2908
+ bindGroupLayouts: [bindGroupLayout]
2909
+ });
2910
+ const module = device.createShaderModule({
2911
+ code: SHADER_SOURCE(selectionMask, storageInstances, indirectInstances)
2912
+ });
2913
+ const createPipeline = (label, topology, selection = false) => device.createRenderPipelineAsync({
2914
+ label: indirectInstances ? `${label}:storage:indirect` : storageInstances ? `${label}:storage` : label,
2915
+ layout: pipelineLayout,
2916
+ vertex: {
2917
+ module,
2918
+ entryPoint: "vertexMain",
2919
+ buffers: storageInstances ? STORAGE_VERTEX_LAYOUT : VERTEX_LAYOUT
2920
+ },
2921
+ fragment: {
2922
+ module,
2923
+ entryPoint: "fragmentMain",
2924
+ targets: [{
2925
+ format,
2926
+ ...selection ? {
2927
+ blend: {
2928
+ color: {
2929
+ srcFactor: "src-alpha",
2930
+ dstFactor: "one-minus-src-alpha",
2931
+ operation: "add"
2932
+ },
2933
+ alpha: {
2934
+ srcFactor: "one",
2935
+ dstFactor: "one-minus-src-alpha",
2936
+ operation: "add"
2937
+ }
2938
+ }
2939
+ } : {}
2940
+ }]
2941
+ },
2942
+ primitive: {
2943
+ topology,
2944
+ cullMode: "none"
2945
+ },
2946
+ depthStencil: {
2947
+ format: "depth24plus",
2948
+ depthCompare: "less-equal",
2949
+ depthWriteEnabled: !selection
2950
+ },
2951
+ multisample: { count: 1 }
2952
+ });
2953
+ const [triangle, line, selectionTriangle, selectionLine] = await Promise.all([
2954
+ createPipeline("main:triangle", "triangle-list"),
2955
+ createPipeline("main:line", "line-list"),
2956
+ createPipeline("selection:triangle", "triangle-list", true),
2957
+ createPipeline("selection:line", "line-list", true)
2958
+ ]);
2959
+ return new _WebGPURenderPipeline(device, format, bindGroupLayout, pipelineLayout, triangle, line, selectionTriangle, selectionLine, selectionMask, storageInstances, indirectInstances);
2960
+ } catch (error) {
2961
+ throw new ICEEngineError("ICE3_WEBGPU_PIPELINE_FAILED", "ICE3_WEBGPU_PIPELINE_FAILED: render pipelines are unavailable", { cause: error });
2962
+ }
2963
+ }
2964
+ };
2965
+
2966
+ // packages/renderer/dist/WebGPUTimestampProfiler.js
2967
+ var WebGPUTimestampProfiler = class {
2968
+ device;
2969
+ slots;
2970
+ pending = [];
2971
+ reason;
2972
+ disposed = false;
2973
+ constructor(device, slotCount = 64) {
2974
+ this.device = device;
2975
+ if (!Number.isInteger(slotCount) || slotCount < 1) {
2976
+ throw new Error("ICE3_TIMESTAMP_INVALID: slot count must be positive");
2977
+ }
2978
+ this.slots = Array.from({ length: slotCount }, (_, index) => ({
2979
+ querySet: device.createQuerySet({
2980
+ label: `ice3:timestamp:${index}`,
2981
+ type: "timestamp",
2982
+ count: 2
2983
+ }),
2984
+ buffer: device.createBuffer({
2985
+ label: `ice3:timestamp-resolve:${index}`,
2986
+ size: 256,
2987
+ usage: 512 | 8 | 1
2988
+ }),
2989
+ pending: false
2990
+ }));
2991
+ }
2992
+ get unavailableReason() {
2993
+ return this.reason;
2994
+ }
2995
+ querySetFor(frameId) {
2996
+ return this.slotFor(frameId).querySet;
2997
+ }
2998
+ resolveFrame(encoder, frameId) {
2999
+ const slot = this.slotFor(frameId);
3000
+ if (slot.pending) {
3001
+ throw new Error("ICE3_TIMESTAMP_SLOT_BUSY");
3002
+ }
3003
+ encoder.resolveQuerySet(slot.querySet, 0, 2, slot.buffer, 0);
3004
+ this.pending.push(this.slotIndex(frameId));
3005
+ slot.pending = true;
3006
+ }
3007
+ async drain() {
3008
+ if (this.disposed)
3009
+ return [];
3010
+ const samples = [];
3011
+ for (const index of [...this.pending]) {
3012
+ const slot = this.slots[index];
3013
+ let mapped = false;
3014
+ try {
3015
+ await slot.buffer.mapAsync(1);
3016
+ mapped = true;
3017
+ const values = new BigUint64Array(slot.buffer.getMappedRange(), 0, 2);
3018
+ const start = values[0];
3019
+ const end = values[1];
3020
+ if (end <= start) {
3021
+ this.reason = "invalid-timestamp";
3022
+ } else {
3023
+ samples.push(Number(end - start) / 1e6);
3024
+ }
3025
+ } catch {
3026
+ this.reason = "map-failed";
3027
+ } finally {
3028
+ if (mapped)
3029
+ slot.buffer.unmap();
3030
+ slot.pending = false;
3031
+ const pendingIndex = this.pending.indexOf(index);
3032
+ if (pendingIndex >= 0)
3033
+ this.pending.splice(pendingIndex, 1);
3034
+ }
3035
+ }
3036
+ return samples;
3037
+ }
3038
+ dispose() {
3039
+ if (this.disposed)
3040
+ return;
3041
+ this.disposed = true;
3042
+ for (const slot of this.slots) {
3043
+ slot.querySet.destroy();
3044
+ slot.buffer.destroy();
3045
+ slot.pending = false;
3046
+ }
3047
+ this.pending.length = 0;
3048
+ }
3049
+ slotFor(frameId) {
3050
+ if (this.disposed) {
3051
+ throw new Error("ICE3_ENGINE_DESTROYED: timestamp profiler");
3052
+ }
3053
+ if (!Number.isSafeInteger(frameId) || frameId < 0) {
3054
+ throw new Error("ICE3_TIMESTAMP_INVALID: frame ID");
3055
+ }
3056
+ return this.slots[this.slotIndex(frameId)];
3057
+ }
3058
+ slotIndex(frameId) {
3059
+ return frameId % this.slots.length;
3060
+ }
3061
+ };
3062
+
3063
+ // packages/renderer/dist/WebGPUSelectionMask.js
3064
+ var STORAGE_USAGE = 128 | 8;
3065
+ function buildSelectionMask(ranges, selection) {
3066
+ const selected = new Set(selection);
3067
+ const bases = [];
3068
+ let count = 0;
3069
+ for (const range of ranges) {
3070
+ bases.push(count);
3071
+ count += range.length;
3072
+ }
3073
+ const words = new Uint32Array(Math.max(1, count));
3074
+ ranges.forEach((range, rangeIndex) => {
3075
+ const base = bases[rangeIndex];
3076
+ range.forEach((id, index) => {
3077
+ words[base + index] = selected.has(id) ? 1 : 0;
3078
+ });
3079
+ });
3080
+ return { words, bases };
3081
+ }
3082
+ var WebGPUSelectionMaskBuffer = class {
3083
+ device;
3084
+ maskBuffer = null;
3085
+ previous = null;
3086
+ maskGeneration = 0;
3087
+ constructor(device) {
3088
+ this.device = device;
3089
+ }
3090
+ get buffer() {
3091
+ return this.maskBuffer;
3092
+ }
3093
+ get generation() {
3094
+ return this.maskGeneration;
3095
+ }
3096
+ get totalBytes() {
3097
+ return this.maskBuffer?.size ?? 0;
3098
+ }
3099
+ update(words) {
3100
+ if (this.maskBuffer && this.maskBuffer.size < words.byteLength) {
3101
+ this.maskBuffer.destroy();
3102
+ this.maskBuffer = null;
3103
+ this.previous = null;
3104
+ }
3105
+ if (!this.maskBuffer) {
3106
+ this.maskBuffer = createEmptyGPUDataBuffer(this.device, words.byteLength, STORAGE_USAGE, "ice3:selection-mask");
3107
+ this.maskGeneration += 1;
3108
+ }
3109
+ const bytes = new Uint8Array(words.buffer, words.byteOffset, words.byteLength).slice();
3110
+ if (this.previous && sameBytes2(this.previous, bytes))
3111
+ return;
3112
+ writeGPUData(this.device, this.maskBuffer, words);
3113
+ this.previous = bytes;
3114
+ }
3115
+ dispose() {
3116
+ this.maskBuffer?.destroy();
3117
+ this.maskBuffer = null;
3118
+ this.previous = null;
3119
+ }
3120
+ };
3121
+ function sameBytes2(left, right) {
3122
+ if (left.byteLength !== right.byteLength)
3123
+ return false;
3124
+ for (let index = 0; index < left.byteLength; index += 1) {
3125
+ if (left[index] !== right[index])
3126
+ return false;
3127
+ }
3128
+ return true;
3129
+ }
3130
+
3131
+ // packages/renderer/dist/WebGPUDrawStorage.js
3132
+ var FLOATS_PER_DRAW = WEBGPU_UNIFORM_STRIDE / 4;
3133
+ var WebGPUDrawStorage = class {
3134
+ minimumCapacity;
3135
+ uniformBuffer;
3136
+ capacity;
3137
+ generationValue = 0;
3138
+ count = 0;
3139
+ constructor(minimumCapacity = 64) {
3140
+ this.minimumCapacity = minimumCapacity;
3141
+ if (!Number.isInteger(minimumCapacity) || minimumCapacity < 1) {
3142
+ throw new Error("ICE3_DRAW_STORAGE_INVALID");
3143
+ }
3144
+ this.capacity = drawCountPowerOfTwo(minimumCapacity);
3145
+ this.uniformBuffer = new Float32Array(this.capacity * FLOATS_PER_DRAW);
3146
+ }
3147
+ begin(requiredDraws) {
3148
+ this.ensureCapacity(requiredDraws);
3149
+ this.count = 0;
3150
+ }
3151
+ append(draw) {
3152
+ if (this.count >= this.capacity) {
3153
+ this.ensureCapacity(this.count + 1);
3154
+ }
3155
+ const index = this.count;
3156
+ const offset = index * FLOATS_PER_DRAW;
3157
+ this.uniformBuffer.set(draw.matrix, offset);
3158
+ this.uniformBuffer.set(draw.color, offset + 16);
3159
+ this.uniformBuffer[offset + 20] = 0;
3160
+ this.uniformBuffer[offset + 21] = 0;
3161
+ this.uniformBuffer[offset + 22] = draw.useVertexColor ? 1 : 0;
3162
+ this.uniformBuffer[offset + 23] = draw.useInstancing ? 1 : 0;
3163
+ this.uniformBuffer[offset + 24] = draw.semanticHighlight ?? 0;
3164
+ this.uniformBuffer[offset + 25] = draw.useSemantic ? 1 : 0;
3165
+ this.uniformBuffer[offset + 26] = 0;
3166
+ this.uniformBuffer[offset + 27] = 0;
3167
+ this.uniformBuffer[offset + 28] = draw.selectionBase;
3168
+ this.uniformBuffer[offset + 29] = draw.useSelectionMask ? 1 : 0;
3169
+ this.uniformBuffer[offset + 30] = draw.pageBase ?? 0;
3170
+ this.uniformBuffer[offset + 31] = draw.useIndirect ? 1 : 0;
3171
+ this.count += 1;
3172
+ return index;
3173
+ }
3174
+ get uniformData() {
3175
+ return this.uniformBuffer;
3176
+ }
3177
+ get drawCount() {
3178
+ return this.count;
3179
+ }
3180
+ get generation() {
3181
+ return this.generationValue;
3182
+ }
3183
+ ensureCapacity(requiredDraws) {
3184
+ const next = Math.max(this.minimumCapacity, drawCountPowerOfTwo(requiredDraws));
3185
+ if (next === this.capacity)
3186
+ return;
3187
+ this.capacity = next;
3188
+ this.uniformBuffer = new Float32Array(next * FLOATS_PER_DRAW);
3189
+ this.generationValue += 1;
3190
+ }
3191
+ };
3192
+ function drawCountPowerOfTwo(value) {
3193
+ let result = 1;
3194
+ while (result < Math.max(1, value))
3195
+ result *= 2;
3196
+ return result;
3197
+ }
3198
+
3199
+ // packages/renderer/dist/WebGPURenderPlan.js
3200
+ var WebGPURenderPlanCache = class {
3201
+ scene = null;
3202
+ current = null;
3203
+ renderablesScratch = [];
3204
+ instancesScratch = [];
3205
+ batchesScratch = [];
3206
+ get(scene) {
3207
+ if (this.current && this.scene === scene && this.current.revision === scene.revision) {
3208
+ return this.current;
3209
+ }
3210
+ const renderables = collectRenderables(scene, this.renderablesScratch);
3211
+ const instances = collectInstanceBatches(renderables, this.instancesScratch);
3212
+ const batches = collectRenderBatches(scene, this.batchesScratch);
3213
+ const instancedMeshIds = /* @__PURE__ */ new Set();
3214
+ for (const instance of instances) {
3215
+ for (const id of instance.meshIds)
3216
+ instancedMeshIds.add(id);
3217
+ }
3218
+ this.scene = scene;
3219
+ this.current = Object.freeze({
3220
+ scene,
3221
+ revision: scene.revision,
3222
+ renderables: Object.freeze([...renderables]),
3223
+ instances: Object.freeze([...instances]),
3224
+ batches: Object.freeze([...batches]),
3225
+ instancedMeshIds
3226
+ });
3227
+ return this.current;
3228
+ }
3229
+ invalidate() {
3230
+ this.scene = null;
3231
+ this.current = null;
3232
+ this.renderablesScratch.length = 0;
3233
+ this.instancesScratch.length = 0;
3234
+ this.batchesScratch.length = 0;
3235
+ }
3236
+ dispose() {
3237
+ this.invalidate();
3238
+ }
3239
+ };
3240
+
3241
+ // packages/renderer/dist/WebGPUBundleCache.js
3242
+ var WebGPUBundleCache = class {
3243
+ device;
3244
+ format;
3245
+ key = null;
3246
+ bundle = null;
3247
+ recreations = 0;
3248
+ constructor(device, format) {
3249
+ this.device = device;
3250
+ this.format = format;
3251
+ }
3252
+ get(key, record3) {
3253
+ if (this.key === key && this.bundle)
3254
+ return this.bundle;
3255
+ const encoder = this.device.createRenderBundleEncoder({
3256
+ label: `ice3:bundle:${key}`,
3257
+ colorFormats: [this.format],
3258
+ depthStencilFormat: "depth24plus"
3259
+ });
3260
+ record3(encoder);
3261
+ const bundle = encoder.finish();
3262
+ this.key = key;
3263
+ this.bundle = bundle;
3264
+ this.recreations += 1;
3265
+ return bundle;
3266
+ }
3267
+ get current() {
3268
+ return this.bundle;
3269
+ }
3270
+ get bundleRecreations() {
3271
+ return this.recreations;
3272
+ }
3273
+ invalidate() {
3274
+ this.key = null;
3275
+ this.bundle = null;
3276
+ }
3277
+ dispose() {
3278
+ this.invalidate();
3279
+ }
3280
+ };
3281
+
3282
+ // packages/renderer/dist/WebGPUCapabilities.js
3283
+ var WEBGPU_STORAGE_DEFAULT = false;
3284
+ var WEBGPU_GPU_DRIVEN_DEFAULT = false;
3285
+ var WEBGPU_INSTANCE_VISIBILITY_THRESHOLD = 1024;
3286
+ function detectWebGPUAcceleration(device) {
3287
+ const maxBindingBytes = device.limits.maxStorageBufferBindingSize ?? 0;
3288
+ const maxBuffers = device.limits.maxStorageBuffersPerShaderStage ?? 0;
3289
+ const alignment = device.limits.minStorageBufferOffsetAlignment ?? 256;
3290
+ const storage = maxBindingBytes >= 256 && maxBuffers >= 2 && alignment >= 4 && Number.isInteger(alignment);
3291
+ const visibility = storage && maxBuffers >= 3;
3292
+ return {
3293
+ tier: visibility ? "gpu-driven" : storage ? "storage" : "baseline",
3294
+ storage,
3295
+ visibility,
3296
+ maxStorageBufferBindingSize: maxBindingBytes,
3297
+ minStorageBufferOffsetAlignment: alignment
3298
+ };
3299
+ }
3300
+
3301
+ // packages/renderer/dist/WebGPUInstanceStorage.js
3302
+ var STORAGE_USAGE2 = 128 | 8;
3303
+ var INSTANCE_RECORD_BYTES = 96;
3304
+ var WebGPUInstanceStorage = class {
3305
+ device;
3306
+ entries = /* @__PURE__ */ new Map();
3307
+ active = /* @__PURE__ */ new Set();
3308
+ resourceGeneration = 0;
3309
+ uploadBytes = 0;
3310
+ maxBindingBytes;
3311
+ minOffsetAlignment;
3312
+ constructor(device, options) {
3313
+ this.device = device;
3314
+ this.maxBindingBytes = options?.maxBindingBytes ?? device.limits.maxStorageBufferBindingSize ?? 0;
3315
+ this.minOffsetAlignment = options?.minOffsetAlignment ?? device.limits.minStorageBufferOffsetAlignment ?? 256;
3316
+ if (this.maxBindingBytes < INSTANCE_RECORD_BYTES || this.minOffsetAlignment < 4) {
3317
+ throw new Error("ICE3_STORAGE_INVALID: limits are too low");
3318
+ }
3319
+ }
3320
+ plan(instanceCount, recordBytes = INSTANCE_RECORD_BYTES) {
3321
+ if (!Number.isInteger(instanceCount) || instanceCount < 0 || !Number.isInteger(recordBytes) || recordBytes < 1) {
3322
+ throw new Error("ICE3_STORAGE_INVALID: page input");
3323
+ }
3324
+ if (instanceCount === 0)
3325
+ return [];
3326
+ const stride = Math.ceil(recordBytes / 16) * 16;
3327
+ const perPage = Math.max(1, Math.floor(this.maxBindingBytes / stride));
3328
+ const pages = [];
3329
+ for (let first = 0; first < instanceCount; first += perPage) {
3330
+ pages.push({
3331
+ firstInstance: first,
3332
+ instanceCount: Math.min(perPage, instanceCount - first)
3333
+ });
3334
+ }
3335
+ return pages;
3336
+ }
3337
+ retain(batch, geometry) {
3338
+ this.active.add(batch.id);
3339
+ const existing = this.entries.get(batch.id);
3340
+ if (existing && existing.geometry === geometry && existing.matrixSource === batch.instanceMatrices && existing.semanticSource === batch.semanticIndices) {
3341
+ return existing;
3342
+ }
3343
+ if (existing)
3344
+ this.delete(existing);
3345
+ const bounds = geometry.getBounds();
3346
+ const radius = Math.hypot(bounds.max[0] - bounds.min[0], bounds.max[1] - bounds.min[1], bounds.max[2] - bounds.min[2]) / 2;
3347
+ const center = [
3348
+ (bounds.min[0] + bounds.max[0]) / 2,
3349
+ (bounds.min[1] + bounds.max[1]) / 2,
3350
+ (bounds.min[2] + bounds.max[2]) / 2
3351
+ ];
3352
+ const plans = this.plan(batch.semanticIds.length);
3353
+ const pages = plans.map((page) => {
3354
+ const data = new ArrayBuffer(page.instanceCount * INSTANCE_RECORD_BYTES);
3355
+ const floats = new Float32Array(data);
3356
+ const words = new Uint32Array(data);
3357
+ for (let local = 0; local < page.instanceCount; local += 1) {
3358
+ const source = page.firstInstance + local;
3359
+ const offset = local * 24;
3360
+ floats.set(batch.instanceMatrices.subarray(source * 16, source * 16 + 16), offset);
3361
+ floats.set([
3362
+ center[0],
3363
+ center[1],
3364
+ center[2],
3365
+ radius
3366
+ ], offset + 16);
3367
+ words[offset + 20] = batch.semanticIndices[source] ?? source;
3368
+ }
3369
+ const buffer = createEmptyGPUDataBuffer(this.device, data.byteLength, STORAGE_USAGE2, `ice3:instance-records:${batch.id}:${page.firstInstance}`);
3370
+ writeGPUData(this.device, buffer, floats);
3371
+ this.uploadBytes += data.byteLength;
3372
+ return { ...page, buffer };
3373
+ });
3374
+ const entry = {
3375
+ pages,
3376
+ generation: ++this.resourceGeneration,
3377
+ bytes: pages.reduce((sum, page) => sum + page.buffer.size, 0),
3378
+ geometry,
3379
+ matrixSource: batch.instanceMatrices,
3380
+ semanticSource: batch.semanticIndices
3381
+ };
3382
+ this.entries.set(batch.id, entry);
3383
+ return entry;
3384
+ }
3385
+ beginFrame() {
3386
+ this.active = /* @__PURE__ */ new Set();
3387
+ }
3388
+ endFrame() {
3389
+ for (const [id, entry] of this.entries) {
3390
+ if (this.active.has(id))
3391
+ continue;
3392
+ this.delete(entry);
3393
+ this.entries.delete(id);
3394
+ }
3395
+ }
3396
+ get generation() {
3397
+ return this.resourceGeneration;
3398
+ }
3399
+ get totalBytes() {
3400
+ return [...this.entries.values()].reduce((sum, entry) => sum + entry.bytes, 0);
3401
+ }
3402
+ get totalUploadBytes() {
3403
+ return this.uploadBytes;
3404
+ }
3405
+ dispose() {
3406
+ for (const entry of this.entries.values())
3407
+ this.delete(entry);
3408
+ this.entries.clear();
3409
+ this.active.clear();
3410
+ }
3411
+ delete(entry) {
3412
+ for (const page of entry.pages)
3413
+ page.buffer.destroy();
3414
+ this.resourceGeneration += 1;
3415
+ }
3416
+ };
3417
+
3418
+ // packages/renderer/dist/WebGPUVisibility.js
3419
+ var STORAGE_USAGE3 = 128 | 8;
3420
+ var INDIRECT_USAGE = 256 | 128 | 8 | 4;
3421
+ function createIndirectArguments(indexCount, firstIndex, baseVertex) {
3422
+ if (!Number.isInteger(indexCount) || indexCount < 0 || !Number.isInteger(firstIndex) || firstIndex < 0 || !Number.isInteger(baseVertex) || baseVertex < 0) {
3423
+ return void 0;
3424
+ }
3425
+ return new Uint32Array([
3426
+ indexCount,
3427
+ 0,
3428
+ firstIndex,
3429
+ baseVertex,
3430
+ 0
3431
+ ]);
3432
+ }
3433
+ var COMPUTE_SHADER = `
3434
+ struct FrameUniforms {
3435
+ worldViewProjection: mat4x4<f32>,
3436
+ }
3437
+
3438
+ struct InstanceRecord {
3439
+ transform: mat4x4<f32>,
3440
+ bounds: vec4<f32>,
3441
+ semantic: u32,
3442
+ pad0: u32,
3443
+ pad1: u32,
3444
+ pad2: u32,
3445
+ }
3446
+
3447
+ struct IndirectArguments {
3448
+ indexCount: u32,
3449
+ instanceCount: atomic<u32>,
3450
+ firstIndex: u32,
3451
+ baseVertex: i32,
3452
+ firstInstance: u32,
3453
+ }
3454
+
3455
+ @group(0) @binding(0) var<uniform> frame: FrameUniforms;
3456
+ @group(0) @binding(1) var<storage, read> instances: array<InstanceRecord>;
3457
+ @group(0) @binding(2) var<storage, read_write> visible: array<u32>;
3458
+ @group(0) @binding(3) var<storage, read_write> arguments: IndirectArguments;
3459
+
3460
+ @compute @workgroup_size(64)
3461
+ fn computeMain(@builtin(global_invocation_id) id: vec3<u32>) {
3462
+ let index = id.x;
3463
+ if (index >= arrayLength(&instances)) {
3464
+ return;
3465
+ }
3466
+ let instance = instances[index];
3467
+ let radius = instance.bounds.w;
3468
+ let localMin = instance.bounds.xyz - vec3<f32>(radius);
3469
+ let localMax = instance.bounds.xyz + vec3<f32>(radius);
3470
+ var worldMin = vec3<f32>(1e30);
3471
+ var worldMax = vec3<f32>(-1e30);
3472
+ for (var cornerIndex = 0u; cornerIndex < 8u; cornerIndex += 1u) {
3473
+ let corner = vec3<f32>(
3474
+ select(localMin.x, localMax.x, (cornerIndex & 1u) != 0u),
3475
+ select(localMin.y, localMax.y, (cornerIndex & 2u) != 0u),
3476
+ select(localMin.z, localMax.z, (cornerIndex & 4u) != 0u)
3477
+ );
3478
+ let world = (instance.transform * vec4<f32>(corner, 1.0)).xyz;
3479
+ worldMin = min(worldMin, world);
3480
+ worldMax = max(worldMax, world);
3481
+ }
3482
+ let r0 = vec4<f32>(
3483
+ frame.worldViewProjection[0][0],
3484
+ frame.worldViewProjection[1][0],
3485
+ frame.worldViewProjection[2][0],
3486
+ frame.worldViewProjection[3][0]
3487
+ );
3488
+ let r1 = vec4<f32>(
3489
+ frame.worldViewProjection[0][1],
3490
+ frame.worldViewProjection[1][1],
3491
+ frame.worldViewProjection[2][1],
3492
+ frame.worldViewProjection[3][1]
3493
+ );
3494
+ let r2 = vec4<f32>(
3495
+ frame.worldViewProjection[0][2],
3496
+ frame.worldViewProjection[1][2],
3497
+ frame.worldViewProjection[2][2],
3498
+ frame.worldViewProjection[3][2]
3499
+ );
3500
+ let r3 = vec4<f32>(
3501
+ frame.worldViewProjection[0][3],
3502
+ frame.worldViewProjection[1][3],
3503
+ frame.worldViewProjection[2][3],
3504
+ frame.worldViewProjection[3][3]
3505
+ );
3506
+ let planes = array<vec4<f32>, 6>(
3507
+ r3 + r0,
3508
+ r3 - r0,
3509
+ r3 + r1,
3510
+ r3 - r1,
3511
+ r3 + r2,
3512
+ r3 - r2
3513
+ );
3514
+ var culled = false;
3515
+ for (var planeIndex = 0u; planeIndex < 6u; planeIndex += 1u) {
3516
+ let plane = planes[planeIndex];
3517
+ let positive = select(worldMin, worldMax, plane.xyz >= vec3<f32>(0.0));
3518
+ if (dot(plane.xyz, positive) + plane.w < 0.0) {
3519
+ culled = true;
3520
+ break;
3521
+ }
3522
+ }
3523
+ if (culled) {
3524
+ return;
3525
+ }
3526
+ let slot = atomicAdd(&arguments.instanceCount, 1u);
3527
+ visible[slot] = index;
3528
+ }
3529
+ `;
3530
+ var WebGPUVisibilityEncoder = class _WebGPUVisibilityEncoder {
3531
+ device;
3532
+ bindGroupLayout;
3533
+ pipeline;
3534
+ entries = /* @__PURE__ */ new Map();
3535
+ constructor(device, bindGroupLayout, pipeline) {
3536
+ this.device = device;
3537
+ this.bindGroupLayout = bindGroupLayout;
3538
+ this.pipeline = pipeline;
3539
+ }
3540
+ static async create(device) {
3541
+ const bindGroupLayout = device.createBindGroupLayout({
3542
+ entries: [
3543
+ {
3544
+ binding: 0,
3545
+ visibility: 4,
3546
+ buffer: {
3547
+ type: "uniform",
3548
+ hasDynamicOffset: true,
3549
+ minBindingSize: 64
3550
+ }
3551
+ },
3552
+ {
3553
+ binding: 1,
3554
+ visibility: 4,
3555
+ buffer: { type: "read-only-storage", minBindingSize: 96 }
3556
+ },
3557
+ {
3558
+ binding: 2,
3559
+ visibility: 4,
3560
+ buffer: { type: "storage", minBindingSize: 4 }
3561
+ },
3562
+ {
3563
+ binding: 3,
3564
+ visibility: 4,
3565
+ buffer: { type: "storage", minBindingSize: 20 }
3566
+ }
3567
+ ]
3568
+ });
3569
+ const layout = device.createPipelineLayout({
3570
+ bindGroupLayouts: [bindGroupLayout]
3571
+ });
3572
+ const module = device.createShaderModule({ code: COMPUTE_SHADER });
3573
+ const pipeline = await device.createComputePipelineAsync({
3574
+ label: "ice3:visibility",
3575
+ layout,
3576
+ compute: { module, entryPoint: "computeMain" }
3577
+ });
3578
+ return new _WebGPUVisibilityEncoder(device, bindGroupLayout, pipeline);
3579
+ }
3580
+ retain(page, indexCount) {
3581
+ const byIndexCount = this.entries.get(page.buffer);
3582
+ const existing = byIndexCount?.get(indexCount);
3583
+ if (existing) {
3584
+ return existing;
3585
+ }
3586
+ const visibleBuffer = createEmptyGPUDataBuffer(this.device, page.instanceCount * 4, STORAGE_USAGE3, "ice3:visible-instances");
3587
+ const indirectBuffer = createEmptyGPUDataBuffer(this.device, 256, INDIRECT_USAGE, "ice3:indirect-arguments");
3588
+ writeGPUData(this.device, indirectBuffer, createIndirectArguments(indexCount, 0, 0));
3589
+ const entry = {
3590
+ visibleBuffer,
3591
+ indirectBuffer,
3592
+ instanceCount: page.instanceCount
3593
+ };
3594
+ const entries = byIndexCount ?? /* @__PURE__ */ new Map();
3595
+ entries.set(indexCount, entry);
3596
+ this.entries.set(page.buffer, entries);
3597
+ return entry;
3598
+ }
3599
+ encode(encoder, entry, page, uniformBuffer, uniformOffset) {
3600
+ encoder.clearBuffer(entry.indirectBuffer, 4, 4);
3601
+ const bindGroup = this.device.createBindGroup({
3602
+ label: "ice3:visibility-bindings",
3603
+ layout: this.bindGroupLayout,
3604
+ entries: [
3605
+ {
3606
+ binding: 0,
3607
+ resource: { buffer: uniformBuffer, offset: 0, size: 112 }
3608
+ },
3609
+ {
3610
+ binding: 1,
3611
+ resource: { buffer: page.buffer, offset: 0, size: page.buffer.size }
3612
+ },
3613
+ {
3614
+ binding: 2,
3615
+ resource: {
3616
+ buffer: entry.visibleBuffer,
3617
+ offset: 0,
3618
+ size: entry.visibleBuffer.size
3619
+ }
3620
+ },
3621
+ {
3622
+ binding: 3,
3623
+ resource: {
3624
+ buffer: entry.indirectBuffer,
3625
+ offset: 0,
3626
+ size: entry.indirectBuffer.size
3627
+ }
3628
+ }
3629
+ ]
3630
+ });
3631
+ const pass = encoder.beginComputePass({ label: "ice3:visibility-pass" });
3632
+ pass.setPipeline(this.pipeline);
3633
+ pass.setBindGroup(0, bindGroup, [uniformOffset]);
3634
+ pass.dispatchWorkgroups(Math.ceil(page.instanceCount / 64));
3635
+ pass.end();
3636
+ }
3637
+ async readStats(entry, page) {
3638
+ const readback = this.device.createBuffer({
3639
+ label: "ice3:visibility-stats",
3640
+ size: 4,
3641
+ usage: 1 | 8
3642
+ });
3643
+ const encoder = this.device.createCommandEncoder();
3644
+ encoder.copyBufferToBuffer(entry.indirectBuffer, 4, readback, 0, 4);
3645
+ this.device.queue.submit([encoder.finish()]);
3646
+ try {
3647
+ await readback.mapAsync(1);
3648
+ const visibleInstances = new Uint32Array(readback.getMappedRange())[0];
3649
+ return {
3650
+ visibleInstances,
3651
+ culledInstances: page.instanceCount - visibleInstances,
3652
+ dispatchCount: Math.ceil(page.instanceCount / 64)
3653
+ };
3654
+ } finally {
3655
+ readback.unmap();
3656
+ readback.destroy();
3657
+ }
3658
+ }
3659
+ dispose() {
3660
+ for (const entries of this.entries.values()) {
3661
+ for (const entry of entries.values())
3662
+ this.destroy(entry);
3663
+ }
3664
+ this.entries.clear();
3665
+ }
3666
+ destroy(entry) {
3667
+ entry.visibleBuffer.destroy();
3668
+ entry.indirectBuffer.destroy();
3669
+ }
3670
+ };
3671
+
3672
+ // packages/renderer/dist/WebGPUPicker.js
3673
+ var VERTEX_USAGE = 32 | 8;
3674
+ var UNIFORM_USAGE = 64 | 8;
3675
+ var PICK_TEXTURE_USAGE = 16 | 1;
3676
+ var DEPTH_USAGE = 16;
3677
+ var MAP_READ = 1;
3678
+ var COPY_DST = 8;
3679
+ var PICK_FORMAT = "rgba8unorm";
3680
+ var WebGPUPicker = class {
3681
+ device;
3682
+ pipelinePromise;
3683
+ instanceCache;
3684
+ identityInstances;
3685
+ zeroSemantics;
3686
+ renderPlan = new WebGPURenderPlanCache();
3687
+ target = null;
3688
+ depth = null;
3689
+ uniformBuffer = null;
3690
+ width = 0;
3691
+ height = 0;
3692
+ disposed = false;
3693
+ constructor(device, format) {
3694
+ this.device = device;
3695
+ void format;
3696
+ this.pipelinePromise = WebGPURenderPipeline.create(device, PICK_FORMAT);
3697
+ this.instanceCache = new WebGPUInstanceCache(device);
3698
+ this.identityInstances = device.createBuffer({
3699
+ label: "ice3:pick-identity-instances",
3700
+ size: 80,
3701
+ usage: VERTEX_USAGE
3702
+ });
3703
+ const identity = new Float32Array(20);
3704
+ identity.set([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
3705
+ device.queue.writeBuffer(this.identityInstances, 0, identity);
3706
+ this.zeroSemantics = device.createBuffer({
3707
+ label: "ice3:pick-zero-semantic",
3708
+ size: 4,
3709
+ usage: VERTEX_USAGE
3710
+ });
3711
+ device.queue.writeBuffer(this.zeroSemantics, 0, new Uint32Array([0]));
3712
+ }
3713
+ async pick(frame, geometryCache, pickIds) {
3714
+ if (this.disposed) {
3715
+ throw new ICEEngineError("ICE3_ENGINE_DESTROYED", "WebGPU picker is disposed");
3716
+ }
3717
+ const width = Math.max(1, Math.round(frame.viewport.width * frame.dpr));
3718
+ const height = Math.max(1, Math.round(frame.viewport.height * frame.dpr));
3719
+ this.ensureTarget(width, height);
3720
+ const pipeline = await this.pipelinePromise;
3721
+ this.instanceCache.beginFrame();
3722
+ geometryCache.beginFrame();
3723
+ const plan = this.renderPlan.get(frame.scene);
3724
+ const renderables = plan.renderables;
3725
+ const instances = plan.instances;
3726
+ const batches = plan.batches;
3727
+ const instancedIds = /* @__PURE__ */ new Set();
3728
+ const idBySelection = /* @__PURE__ */ new Map();
3729
+ for (const [key, value] of Object.entries(pickIds)) {
3730
+ idBySelection.set(value, Number(key));
3731
+ }
3732
+ const aspect = frame.viewport.width / Math.max(frame.viewport.height, 1);
3733
+ const viewProjection = frame.camera.getViewProjectionMatrix(aspect);
3734
+ const mvp = new ICEMatrix4();
3735
+ const world = new ICEMatrix4();
3736
+ const draws = [];
3737
+ const matrixFor = (matrix) => {
3738
+ mvp.elements.set(viewProjection.elements);
3739
+ world.elements.set(matrix);
3740
+ mvp.multiply(world);
3741
+ return new Float32Array(mvp.elements);
3742
+ };
3743
+ for (const batch of instances) {
3744
+ const ids = batch.semanticIds.map((id) => idBySelection.get(id)).filter((id) => id !== void 0);
3745
+ if (ids.length === 0)
3746
+ continue;
3747
+ const instance = this.instanceCache.retain(batch);
3748
+ for (const id of batch.meshIds)
3749
+ instancedIds.add(id);
3750
+ draws.push({
3751
+ matrix: new Float32Array(viewProjection.elements),
3752
+ base: Math.min(...ids) - 1,
3753
+ useInstancing: true,
3754
+ instanceCount: instance.instanceCount,
3755
+ geometry: geometryCache.retain(batch.geometry),
3756
+ instanceBuffer: instance.matrixBuffer,
3757
+ semanticBuffer: instance.semanticBuffer
3758
+ });
3759
+ }
3760
+ for (const draw of batches) {
3761
+ const ids = draw.batch.semanticSelectionIds.map((id) => idBySelection.get(id)).filter((id) => id !== void 0);
3762
+ if (ids.length === 0)
3763
+ continue;
3764
+ draws.push({
3765
+ matrix: matrixFor(draw.worldMatrix),
3766
+ base: Math.min(...ids) - 1,
3767
+ useInstancing: false,
3768
+ instanceCount: 1,
3769
+ geometry: geometryCache.retain(draw.batch.geometry),
3770
+ instanceBuffer: this.identityInstances,
3771
+ semanticBuffer: geometryCache.retain(draw.batch.geometry).semanticBuffer ?? this.zeroSemantics
3772
+ });
3773
+ }
3774
+ for (const renderable of renderables) {
3775
+ if (instancedIds.has(renderable.id))
3776
+ continue;
3777
+ const id = idBySelection.get(renderable.mesh.selectionId);
3778
+ if (id === void 0)
3779
+ continue;
3780
+ draws.push({
3781
+ matrix: matrixFor(renderable.worldMatrix),
3782
+ base: id - 1,
3783
+ useInstancing: false,
3784
+ instanceCount: 1,
3785
+ geometry: geometryCache.retain(renderable.mesh.geometry),
3786
+ instanceBuffer: this.identityInstances,
3787
+ semanticBuffer: this.zeroSemantics
3788
+ });
3789
+ }
3790
+ this.ensureUniformBuffer(draws.length);
3791
+ const uniforms = new Float32Array(draws.length * 64);
3792
+ for (let index2 = 0; index2 < draws.length; index2 += 1) {
3793
+ const draw = draws[index2];
3794
+ const offset = index2 * 64;
3795
+ uniforms.set(draw.matrix, offset);
3796
+ uniforms[offset + 16] = 1;
3797
+ uniforms[offset + 17] = 1;
3798
+ uniforms[offset + 18] = 1;
3799
+ uniforms[offset + 19] = 1;
3800
+ uniforms[offset + 23] = draw.useInstancing ? 1 : 0;
3801
+ uniforms[offset + 26] = 1;
3802
+ uniforms[offset + 27] = draw.base;
3803
+ }
3804
+ if (uniforms.length > 0) {
3805
+ this.device.queue.writeBuffer(this.uniformBuffer, 0, uniforms);
3806
+ }
3807
+ const bindGroup = this.device.createBindGroup({
3808
+ layout: pipeline.bindGroupLayout,
3809
+ entries: [{
3810
+ binding: 0,
3811
+ resource: {
3812
+ buffer: this.uniformBuffer,
3813
+ offset: 0,
3814
+ size: WEBGPU_UNIFORM_BYTES
3815
+ }
3816
+ }]
3817
+ });
3818
+ const encoder = this.device.createCommandEncoder();
3819
+ const pass = encoder.beginRenderPass({
3820
+ colorAttachments: [{
3821
+ view: this.target.createView(),
3822
+ clearValue: { r: 0, g: 0, b: 0, a: 1 },
3823
+ loadOp: "clear",
3824
+ storeOp: "store"
3825
+ }],
3826
+ depthStencilAttachment: {
3827
+ view: this.depth.createView(),
3828
+ depthClearValue: 1,
3829
+ depthLoadOp: "clear",
3830
+ depthStoreOp: "store"
3831
+ }
3832
+ });
3833
+ pass.setPipeline(pipeline.triangle);
3834
+ for (let index2 = 0; index2 < draws.length; index2 += 1) {
3835
+ const draw = draws[index2];
3836
+ pass.setBindGroup(0, bindGroup, [index2 * WEBGPU_UNIFORM_STRIDE]);
3837
+ pass.setVertexBuffer(0, draw.geometry.vertexBuffer);
3838
+ pass.setVertexBuffer(1, draw.instanceBuffer);
3839
+ pass.setVertexBuffer(2, draw.semanticBuffer);
3840
+ pass.setIndexBuffer(draw.geometry.triangleIndexBuffer, draw.geometry.indexFormat);
3841
+ pass.drawIndexed(draw.geometry.triangleCount, draw.instanceCount);
3842
+ }
3843
+ pass.end();
3844
+ const pixelX = Math.max(0, Math.min(this.width - 1, Math.floor(frame.point.x * frame.dpr)));
3845
+ const pixelY = Math.max(0, Math.min(this.height - 1, Math.floor(frame.point.y * frame.dpr)));
3846
+ const readback = this.device.createBuffer({
3847
+ label: "ice3:pick-readback",
3848
+ size: 256,
3849
+ usage: COPY_DST | MAP_READ
3850
+ });
3851
+ encoder.copyTextureToBuffer({
3852
+ texture: this.target,
3853
+ origin: {
3854
+ x: pixelX,
3855
+ y: pixelY,
3856
+ z: 0
3857
+ }
3858
+ }, {
3859
+ buffer: readback,
3860
+ bytesPerRow: 256,
3861
+ rowsPerImage: 1
3862
+ }, { width: 1, height: 1, depthOrArrayLayers: 1 });
3863
+ this.device.queue.submit([encoder.finish()]);
3864
+ let pixel;
3865
+ try {
3866
+ await readback.mapAsync(MAP_READ);
3867
+ pixel = new Uint8Array(readback.getMappedRange(), 0, 4).slice();
3868
+ } catch (error) {
3869
+ throw new ICEEngineError("ICE3_WEBGPU_PICK_UNAVAILABLE", "ICE3_WEBGPU_PICK_UNAVAILABLE: readback failed", { cause: error });
3870
+ } finally {
3871
+ readback.unmap();
3872
+ readback.destroy();
3873
+ this.instanceCache.endFrame();
3874
+ geometryCache.endFrame();
3875
+ }
3876
+ const index = pixel ? decodeSelectionIndex(pixel) : void 0;
3877
+ return index === void 0 ? void 0 : pickIds[index];
3878
+ }
3879
+ resize(width, height) {
3880
+ this.ensureTarget(width, height);
3881
+ }
3882
+ dispose() {
3883
+ if (this.disposed)
3884
+ return;
3885
+ this.disposed = true;
3886
+ this.target?.destroy();
3887
+ this.depth?.destroy();
3888
+ this.uniformBuffer?.destroy();
3889
+ this.identityInstances.destroy();
3890
+ this.zeroSemantics.destroy();
3891
+ this.instanceCache.dispose();
3892
+ this.renderPlan.dispose();
3893
+ this.target = null;
3894
+ this.depth = null;
3895
+ this.uniformBuffer = null;
3896
+ }
3897
+ ensureTarget(width, height) {
3898
+ if (this.target && this.depth && this.width === width && this.height === height) {
3899
+ return;
3900
+ }
3901
+ this.target?.destroy();
3902
+ this.depth?.destroy();
3903
+ this.target = this.device.createTexture({
3904
+ label: "ice3:pick-target",
3905
+ size: [width, height],
3906
+ format: PICK_FORMAT,
3907
+ usage: PICK_TEXTURE_USAGE
3908
+ });
3909
+ this.depth = this.device.createTexture({
3910
+ label: "ice3:pick-depth",
3911
+ size: [width, height],
3912
+ format: "depth24plus",
3913
+ usage: DEPTH_USAGE
3914
+ });
3915
+ this.width = width;
3916
+ this.height = height;
3917
+ }
3918
+ ensureUniformBuffer(draws) {
3919
+ const capacity = Math.max(1, draws);
3920
+ if (capacity <= this.uniformBufferDraws())
3921
+ return;
3922
+ this.uniformBuffer?.destroy();
3923
+ this.uniformBuffer = this.device.createBuffer({
3924
+ label: "ice3:pick-uniforms",
3925
+ size: capacity * WEBGPU_UNIFORM_STRIDE,
3926
+ usage: UNIFORM_USAGE
3927
+ });
3928
+ this.uniformBufferCapacity = capacity;
3929
+ }
3930
+ uniformBufferCapacity = 0;
3931
+ uniformBufferDraws() {
3932
+ return this.uniformBufferCapacity;
3933
+ }
3934
+ };
3935
+ function collectWebGPUPickIds(frame) {
3936
+ const pickIds = {};
3937
+ const seen = /* @__PURE__ */ new Set();
3938
+ let next = 1;
3939
+ const add2 = (id) => {
3940
+ if (seen.has(id))
3941
+ return;
3942
+ seen.add(id);
3943
+ pickIds[next] = id;
3944
+ next += 1;
3945
+ };
3946
+ const renderables = collectRenderables(frame.scene);
3947
+ const instances = collectInstanceBatches(renderables);
3948
+ for (const instance of instances) {
3949
+ for (const id of instance.semanticIds)
3950
+ add2(id);
3951
+ }
3952
+ const instancedIds = new Set(instances.flatMap((item) => item.meshIds));
3953
+ for (const batch of collectRenderBatches(frame.scene)) {
3954
+ for (const id of batch.batch.semanticSelectionIds)
3955
+ add2(id);
3956
+ }
3957
+ for (const renderable of renderables) {
3958
+ if (!instancedIds.has(renderable.id))
3959
+ add2(renderable.mesh.selectionId);
3960
+ }
3961
+ return pickIds;
3962
+ }
3963
+
3964
+ // packages/renderer/dist/WebGPURendererBackend.js
3965
+ var VERTEX_USAGE2 = 32 | 8;
3966
+ var UNIFORM_USAGE2 = 64 | 8;
3967
+ var RENDER_ATTACHMENT_USAGE = 16;
3968
+ var UNIFORM_BUFFER_INITIAL_DRAWS = 64;
3969
+ var SELECTION_RGBA2 = [1, 208 / 255, 0, 1];
3970
+ var SELECTION_TINT_RGBA = [1, 208 / 255, 0, 0.65];
3971
+ var EMPTY_SELECTION = new Uint32Array([0]);
3972
+ var EDGE_PIXEL_OFFSETS2 = [
3973
+ [0, 0],
3974
+ [-1, 0],
3975
+ [1, 0],
3976
+ [0, -1],
3977
+ [0, 1]
3978
+ ];
3979
+ var WebGPURendererBackend = class _WebGPURendererBackend {
3980
+ capabilities = {
3981
+ backend: "webgpu",
3982
+ maxTextureSize: 0,
3983
+ picking: true
3984
+ };
3985
+ adapter;
3986
+ device;
3987
+ format;
3988
+ pipeline;
3989
+ storagePipeline;
3990
+ gpuDrivenPipeline;
3991
+ visibilityEncoder;
3992
+ storageCache;
3993
+ acceleration;
3994
+ forceStoragePath;
3995
+ forceGpuDriven;
3996
+ releaseProbe;
3997
+ onContextLost;
3998
+ onContextRestored;
3999
+ onError;
4000
+ disposing = false;
4001
+ canvas = null;
4002
+ context = null;
4003
+ geometryCache = null;
4004
+ instanceCache = null;
4005
+ picker = null;
4006
+ selectionMaskBuffer = null;
4007
+ timestampProfiler = null;
4008
+ timestampProfilingEnabled = false;
4009
+ bundleCache;
4010
+ bundleActive = true;
4011
+ frameBindGroup = null;
4012
+ frameBindGroupKey = "";
4013
+ storageBindGroups = /* @__PURE__ */ new Map();
4014
+ uniformBufferGeneration = 0;
4015
+ optimizationWarnings = /* @__PURE__ */ new Set();
4016
+ identityInstances = null;
4017
+ zeroSemantics = null;
4018
+ uniformBuffer = null;
4019
+ depthTexture = null;
4020
+ uniformCapacity = UNIFORM_BUFFER_INITIAL_DRAWS;
4021
+ viewport = { x: 0, y: 0, width: 1, height: 1 };
4022
+ dpr = 1;
4023
+ backgroundRgba = [
4024
+ 0.035,
4025
+ 0.055,
4026
+ 0.09,
4027
+ 1
4028
+ ];
4029
+ frameStats = {
4030
+ drawCalls: 0,
4031
+ renderedMeshes: 0,
4032
+ renderedBatches: 0,
4033
+ instancingFallbacks: 0,
4034
+ gpuUploads: 0,
4035
+ gpuEvictions: 0,
4036
+ gpuBytes: 0
4037
+ };
4038
+ lastFrame = null;
4039
+ captureRequest = null;
4040
+ renderPlan = new WebGPURenderPlanCache();
4041
+ drawListScratch = [];
4042
+ selected = /* @__PURE__ */ new Set();
4043
+ drawStorage = new WebGPUDrawStorage();
4044
+ lastVisibilityEntries = [];
4045
+ mvpScratch = new ICEMatrix4();
4046
+ worldScratch = new ICEMatrix4();
4047
+ profiler = new RendererProfiler();
4048
+ lastFrameProfile = null;
4049
+ optimizationFailureReason = null;
4050
+ storageFailureReason = null;
4051
+ visibilityFailureReason = null;
4052
+ constructor(context, pipeline, storagePipeline, gpuDrivenPipeline, visibilityEncoder, storageCache, acceleration, forceStoragePath, forceGpuDriven, selectionMaskBuffer, optimizationFailureReason, storageFailureReason, visibilityFailureReason) {
4053
+ this.adapter = context.adapter;
4054
+ this.device = context.device;
4055
+ this.format = context.format;
4056
+ this.pipeline = pipeline;
4057
+ this.storagePipeline = storagePipeline;
4058
+ this.gpuDrivenPipeline = gpuDrivenPipeline;
4059
+ this.visibilityEncoder = visibilityEncoder;
4060
+ this.storageCache = storageCache;
4061
+ this.acceleration = acceleration;
4062
+ this.forceStoragePath = forceStoragePath;
4063
+ this.forceGpuDriven = forceGpuDriven;
4064
+ this.bundleCache = new WebGPUBundleCache(context.device, context.format);
4065
+ this.selectionMaskBuffer = selectionMaskBuffer;
4066
+ this.optimizationFailureReason = optimizationFailureReason;
4067
+ this.storageFailureReason = storageFailureReason;
4068
+ this.visibilityFailureReason = visibilityFailureReason;
4069
+ this.releaseProbe = context.release;
4070
+ this.watchDevice(this.device);
4071
+ }
4072
+ static async create(context, options = {}) {
4073
+ let selectionMaskBuffer = null;
4074
+ let optimizationFailureReason = null;
4075
+ let pipeline;
4076
+ try {
4077
+ selectionMaskBuffer = new WebGPUSelectionMaskBuffer(context.device);
4078
+ selectionMaskBuffer.update(new Uint32Array([0]));
4079
+ pipeline = await WebGPURenderPipeline.create(context.device, context.format, { selectionMask: true });
4080
+ } catch {
4081
+ selectionMaskBuffer?.dispose();
4082
+ selectionMaskBuffer = null;
4083
+ optimizationFailureReason = "selection-mask";
4084
+ pipeline = await WebGPURenderPipeline.create(context.device, context.format);
4085
+ }
4086
+ const acceleration = detectWebGPUAcceleration(context.device);
4087
+ const forceStoragePath = options.forceStoragePath ?? WEBGPU_STORAGE_DEFAULT;
4088
+ const forceGpuDriven = options.forceGpuDriven ?? WEBGPU_GPU_DRIVEN_DEFAULT;
4089
+ let storagePipeline = null;
4090
+ let gpuDrivenPipeline = null;
4091
+ let visibilityEncoder = null;
4092
+ let storageCache = null;
4093
+ let storageFailureReason = null;
4094
+ let visibilityFailureReason = null;
4095
+ if (forceStoragePath || forceGpuDriven) {
4096
+ if (!acceleration.storage) {
4097
+ storageFailureReason = "limit-too-low";
4098
+ } else {
4099
+ try {
4100
+ storagePipeline = await WebGPURenderPipeline.create(context.device, context.format, {
4101
+ selectionMask: true,
4102
+ storageInstances: true
4103
+ });
4104
+ storageCache = new WebGPUInstanceStorage(context.device);
4105
+ } catch {
4106
+ storagePipeline = null;
4107
+ storageCache?.dispose();
4108
+ storageCache = null;
4109
+ storageFailureReason = "pipeline-failed";
4110
+ }
4111
+ }
4112
+ }
4113
+ if (forceGpuDriven) {
4114
+ if (!storagePipeline || !storageCache) {
4115
+ visibilityFailureReason = storageFailureReason ?? "limit-too-low";
4116
+ } else if (!acceleration.visibility) {
4117
+ visibilityFailureReason = "limit-too-low";
4118
+ } else {
4119
+ try {
4120
+ gpuDrivenPipeline = await WebGPURenderPipeline.create(context.device, context.format, {
4121
+ selectionMask: true,
4122
+ storageInstances: true,
4123
+ indirectInstances: true
4124
+ });
4125
+ visibilityEncoder = await WebGPUVisibilityEncoder.create(context.device);
4126
+ } catch {
4127
+ gpuDrivenPipeline = null;
4128
+ visibilityEncoder?.dispose();
4129
+ visibilityEncoder = null;
4130
+ visibilityFailureReason = "pipeline-failed";
4131
+ }
4132
+ }
4133
+ }
4134
+ return new _WebGPURendererBackend(context, pipeline, storagePipeline, gpuDrivenPipeline, visibilityEncoder, storageCache, acceleration, forceStoragePath, forceGpuDriven, selectionMaskBuffer, optimizationFailureReason, storageFailureReason, visibilityFailureReason);
4135
+ }
4136
+ initialize(options) {
4137
+ const context = options.canvas.getContext("webgpu");
4138
+ if (!context) {
4139
+ throw new ICEEngineError("ICE3_WEBGPU_UNAVAILABLE", "ICE3_WEBGPU_UNAVAILABLE: WebGPU canvas context is unavailable");
4140
+ }
4141
+ context.configure({
4142
+ device: this.device,
4143
+ format: this.format,
4144
+ alphaMode: "premultiplied",
4145
+ usage: RENDER_ATTACHMENT_USAGE | 1
4146
+ });
4147
+ this.canvas = options.canvas;
4148
+ this.context = context;
4149
+ this.onContextLost = options.onContextLost;
4150
+ this.onContextRestored = options.onContextRestored;
4151
+ this.onError = options.onError;
4152
+ if (this.optimizationFailureReason) {
4153
+ const reason = this.optimizationFailureReason;
4154
+ this.optimizationFailureReason = null;
4155
+ this.reportOptimizationFailure("selection-mask", reason);
4156
+ }
4157
+ if (this.storageFailureReason) {
4158
+ const reason = this.storageFailureReason;
4159
+ this.storageFailureReason = null;
4160
+ this.reportOptimizationFailure("storage", reason);
4161
+ }
4162
+ if (this.visibilityFailureReason) {
4163
+ const reason = this.visibilityFailureReason;
4164
+ this.visibilityFailureReason = null;
4165
+ this.reportOptimizationFailure("visibility", reason);
4166
+ }
4167
+ this.geometryCache = new WebGPUGeometryCache({ device: this.device });
4168
+ this.instanceCache = new WebGPUInstanceCache(this.device);
4169
+ this.picker = new WebGPUPicker(this.device, this.format);
4170
+ this.timestampProfiler = this.device.features.has("timestamp-query") ? new WebGPUTimestampProfiler(this.device) : null;
4171
+ this.capabilities = {
4172
+ backend: "webgpu",
4173
+ maxTextureSize: this.device.limits.maxTextureDimension2D,
4174
+ picking: true
4175
+ };
4176
+ this.identityInstances = this.device.createBuffer({
4177
+ label: "ice3:identity-instances",
4178
+ size: 80,
4179
+ usage: VERTEX_USAGE2
4180
+ });
4181
+ const identity = new Float32Array(20);
4182
+ identity.set([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
4183
+ this.device.queue.writeBuffer(this.identityInstances, 0, identity);
4184
+ this.zeroSemantics = this.device.createBuffer({
4185
+ label: "ice3:zero-semantic",
4186
+ size: 4,
4187
+ usage: VERTEX_USAGE2
4188
+ });
4189
+ this.device.queue.writeBuffer(this.zeroSemantics, 0, new Uint32Array([0]));
4190
+ this.ensureUniformCapacity(UNIFORM_BUFFER_INITIAL_DRAWS);
4191
+ }
4192
+ resize(viewport, dpr) {
4193
+ if (!this.canvas || !this.device) {
4194
+ throw new ICEEngineError("ICE3_ENGINE_NOT_READY", "Renderer is not initialized");
4195
+ }
4196
+ this.viewport = { ...viewport };
4197
+ this.dpr = dpr;
4198
+ this.canvas.width = Math.max(1, Math.round(viewport.width * dpr));
4199
+ this.canvas.height = Math.max(1, Math.round(viewport.height * dpr));
4200
+ this.bundleCache.invalidate();
4201
+ this.frameBindGroup = null;
4202
+ this.depthTexture?.destroy();
4203
+ this.depthTexture = this.device.createTexture({
4204
+ label: "ice3:depth",
4205
+ size: [this.canvas.width, this.canvas.height],
4206
+ format: "depth24plus",
4207
+ usage: RENDER_ATTACHMENT_USAGE
4208
+ });
4209
+ this.picker?.resize(this.canvas.width, this.canvas.height);
4210
+ }
4211
+ setBackgroundColor(color) {
4212
+ this.backgroundRgba = colorToRgba(color);
4213
+ }
4214
+ prepareBatchGeometry(geometry) {
4215
+ this.requireGeometryCache().pin(geometry);
4216
+ }
4217
+ releaseBatchGeometry(geometry) {
4218
+ this.geometryCache?.unpin(geometry);
4219
+ }
4220
+ render(frame) {
4221
+ this.profiler.begin();
4222
+ this.lastFrameProfile = null;
4223
+ this.lastFrame = frame;
4224
+ const context = this.context;
4225
+ const cache = this.requireGeometryCache();
4226
+ const instanceCache = this.instanceCache;
4227
+ if (!context || !this.depthTexture || !this.identityInstances || !this.zeroSemantics || !instanceCache) {
4228
+ throw new ICEEngineError("ICE3_ENGINE_NOT_READY", "Renderer is not initialized");
4229
+ }
4230
+ this.frameStats = {
4231
+ drawCalls: 0,
4232
+ renderedMeshes: 0,
4233
+ renderedBatches: 0,
4234
+ instancingFallbacks: 0,
4235
+ gpuUploads: 0,
4236
+ gpuEvictions: 0,
4237
+ gpuBytes: 0
4238
+ };
4239
+ cache.beginFrame();
4240
+ instanceCache.beginFrame();
4241
+ this.storageCache?.beginFrame();
4242
+ const plan = this.renderPlan.get(frame.scene);
4243
+ const renderables = plan.renderables;
4244
+ const instances = plan.instances;
4245
+ const batches = plan.batches;
4246
+ this.profiler.mark("collect");
4247
+ const aspect = frame.viewport.width / Math.max(frame.viewport.height, 1);
4248
+ const viewProjection = frame.camera.getViewProjectionMatrix(aspect);
4249
+ const world = this.worldScratch;
4250
+ const mvp = this.mvpScratch;
4251
+ const drawList = this.drawListScratch;
4252
+ drawList.length = 0;
4253
+ this.lastVisibilityEntries.length = 0;
4254
+ this.drawStorage.begin(0);
4255
+ const instancedMeshIds = plan.instancedMeshIds;
4256
+ const selected = this.selected;
4257
+ selected.clear();
4258
+ for (const id of frame.selection)
4259
+ selected.add(id);
4260
+ const selectionMaskActive = this.selectionMaskBuffer !== null;
4261
+ const selectionMaskEnabled = selectionMaskActive && frame.selection.length > 0;
4262
+ let selectionEntryCount = 0;
4263
+ const selectionOverlays = [];
4264
+ const pushDraw = (draw, matrix, selectionIds) => {
4265
+ const selectionBase = selectionMaskEnabled ? selectionEntryCount : 0;
4266
+ this.drawStorage.append({
4267
+ ...draw,
4268
+ matrix,
4269
+ selectionBase,
4270
+ useSelectionMask: Boolean(draw.useSelectionMask),
4271
+ pageBase: draw.storagePage?.firstInstance ?? 0,
4272
+ useIndirect: Boolean(draw.visibilityEntry)
4273
+ });
4274
+ drawList.push({
4275
+ ...draw,
4276
+ ...selectionIds ? { selectionIds } : {},
4277
+ selectionBase
4278
+ });
4279
+ if (selectionMaskEnabled && selectionIds) {
4280
+ selectionEntryCount += selectionIds.length;
4281
+ }
4282
+ };
4283
+ const matrixFor = (source) => {
4284
+ mvp.elements.set(viewProjection.elements);
4285
+ world.elements.set(source);
4286
+ mvp.multiply(world);
4287
+ return mvp.elements;
4288
+ };
4289
+ const pushEdgeDraws = (geometry, instance, matrix, color, pipeline, selection) => {
4290
+ if (!geometry.edgeIndexBuffer || geometry.edgeCount === 0)
4291
+ return;
4292
+ if (selection)
4293
+ selectionOverlays.push(selection.selectionScope);
4294
+ const clipPixelX = 2 / Math.max(1, this.viewport.width * this.dpr);
4295
+ const clipPixelY = 2 / Math.max(1, this.viewport.height * this.dpr);
4296
+ for (const [offsetX, offsetY] of EDGE_PIXEL_OFFSETS2) {
4297
+ pushDraw({
4298
+ geometry,
4299
+ instanceBuffer: instance?.matrixBuffer ?? this.identityInstances,
4300
+ semanticBuffer: instance?.semanticBuffer ?? this.zeroSemantics,
4301
+ instanceCount: instance?.instanceCount ?? 1,
4302
+ color,
4303
+ useVertexColor: false,
4304
+ useInstancing: instance !== null,
4305
+ pipeline,
4306
+ clipOffset: [offsetX * clipPixelX, offsetY * clipPixelY],
4307
+ edge: true,
4308
+ ...selection ?? {},
4309
+ ...instance?.storagePage ? { storagePage: instance.storagePage } : {},
4310
+ ...instance?.storagePage && this.gpuDrivenActive && instance.storagePage.instanceCount >= WEBGPU_INSTANCE_VISIBILITY_THRESHOLD ? {
4311
+ visibilityEntry: this.visibilityEncoder.retain(instance.storagePage, pipeline === this.gpuDrivenPipeline?.line || pipeline === this.gpuDrivenPipeline?.selectionLine ? geometry.edgeCount : geometry.triangleCount)
4312
+ } : {}
4313
+ }, matrix, selection?.selectionIds);
4314
+ }
4315
+ };
4316
+ for (const batch of instances) {
4317
+ const geometry = cache.retain(batch.geometry);
4318
+ if (this.storageActive && this.storageCache && this.storagePipeline) {
4319
+ const storage = this.storageCache.retain(batch, batch.geometry);
4320
+ for (const page of storage.pages) {
4321
+ const useVisibility = this.gpuDrivenActive && page.instanceCount >= WEBGPU_INSTANCE_VISIBILITY_THRESHOLD;
4322
+ const pagePipeline = useVisibility ? this.gpuDrivenPipeline : this.storagePipeline;
4323
+ const visibilityEntry = useVisibility ? this.visibilityEncoder.retain(page, geometry.triangleCount) : void 0;
4324
+ if (visibilityEntry) {
4325
+ this.lastVisibilityEntries.push({
4326
+ entry: visibilityEntry,
4327
+ page
4328
+ });
4329
+ }
4330
+ const instance = {
4331
+ matrixBuffer: this.identityInstances,
4332
+ semanticBuffer: geometry.semanticBuffer ?? this.zeroSemantics,
4333
+ instanceCount: page.instanceCount,
4334
+ storagePage: page
4335
+ };
4336
+ pushDraw({
4337
+ geometry,
4338
+ instanceBuffer: instance.matrixBuffer,
4339
+ semanticBuffer: instance.semanticBuffer,
4340
+ instanceCount: instance.instanceCount,
4341
+ color: colorToRgba(batch.material.color),
4342
+ useVertexColor: Boolean(batch.geometry.colors),
4343
+ useInstancing: true,
4344
+ pipeline: pagePipeline.triangle,
4345
+ useSelectionMask: selectionMaskEnabled,
4346
+ storagePage: page,
4347
+ ...visibilityEntry ? { visibilityEntry } : {}
4348
+ }, viewProjection.elements, batch.semanticIds);
4349
+ if (batch.material.edgeColor) {
4350
+ pushEdgeDraws(geometry, instance, viewProjection.elements, colorToRgba(batch.material.edgeColor), pagePipeline.line);
4351
+ }
4352
+ if (selectionMaskEnabled && batch.material.edgeColor && batch.semanticIds.some((id) => selected.has(id))) {
4353
+ pushEdgeDraws(geometry, instance, viewProjection.elements, SELECTION_RGBA2, pagePipeline.selectionLine, {
4354
+ selectionIds: batch.semanticIds,
4355
+ useSelectionMask: true,
4356
+ useSemantic: true,
4357
+ selectionScope: batch.id
4358
+ });
4359
+ }
4360
+ }
4361
+ } else {
4362
+ const instance = instanceCache.retain(batch);
4363
+ pushDraw({
4364
+ geometry,
4365
+ instanceBuffer: instance.matrixBuffer,
4366
+ semanticBuffer: instance.semanticBuffer,
4367
+ instanceCount: instance.instanceCount,
4368
+ color: colorToRgba(batch.material.color),
4369
+ useVertexColor: Boolean(batch.geometry.colors),
4370
+ useInstancing: true,
4371
+ pipeline: this.pipeline.triangle,
4372
+ useSelectionMask: selectionMaskEnabled
4373
+ }, viewProjection.elements, batch.semanticIds);
4374
+ if (batch.material.edgeColor) {
4375
+ pushEdgeDraws(geometry, instance, viewProjection.elements, colorToRgba(batch.material.edgeColor), this.pipeline.line);
4376
+ }
4377
+ if (selectionMaskEnabled && batch.material.edgeColor && batch.semanticIds.some((id) => selected.has(id))) {
4378
+ pushEdgeDraws(geometry, instance, viewProjection.elements, SELECTION_RGBA2, this.pipeline.selectionLine, {
4379
+ selectionIds: batch.semanticIds,
4380
+ useSelectionMask: true,
4381
+ useSemantic: true,
4382
+ selectionScope: batch.id
4383
+ });
4384
+ } else if (!selectionMaskEnabled) {
4385
+ for (let semanticIndex = 0; semanticIndex < batch.semanticIds.length; semanticIndex += 1) {
4386
+ if (!selected.has(batch.semanticIds[semanticIndex]))
4387
+ continue;
4388
+ pushDraw({
4389
+ geometry,
4390
+ instanceBuffer: instance.matrixBuffer,
4391
+ semanticBuffer: instance.semanticBuffer,
4392
+ instanceCount: instance.instanceCount,
4393
+ color: [...SELECTION_RGBA2],
4394
+ useVertexColor: false,
4395
+ useInstancing: true,
4396
+ pipeline: this.pipeline.selectionTriangle,
4397
+ semanticHighlight: semanticIndex,
4398
+ useSemantic: true
4399
+ }, viewProjection.elements);
4400
+ }
4401
+ }
4402
+ }
4403
+ }
4404
+ for (const draw of batches) {
4405
+ const geometry = cache.retain(draw.batch.geometry);
4406
+ const matrix = matrixFor(draw.worldMatrix);
4407
+ pushDraw({
4408
+ geometry,
4409
+ instanceBuffer: this.identityInstances,
4410
+ semanticBuffer: geometry.semanticBuffer ?? this.zeroSemantics,
4411
+ instanceCount: 1,
4412
+ color: colorToRgba(draw.batch.material.color),
4413
+ useVertexColor: Boolean(draw.batch.geometry.colors),
4414
+ useInstancing: false,
4415
+ pipeline: this.pipeline.triangle,
4416
+ useSelectionMask: selectionMaskEnabled
4417
+ }, matrix, draw.batch.semanticSelectionIds);
4418
+ this.frameStats.renderedBatches += 1;
4419
+ if (draw.batch.material.edgeColor) {
4420
+ pushEdgeDraws(geometry, null, matrix, colorToRgba(draw.batch.material.edgeColor), this.pipeline.line);
4421
+ }
4422
+ if (selectionMaskEnabled && draw.batch.material.edgeColor && draw.batch.semanticSelectionIds.some((id) => selected.has(id))) {
4423
+ pushEdgeDraws(geometry, null, matrix, SELECTION_RGBA2, this.pipeline.selectionLine, {
4424
+ selectionIds: draw.batch.semanticSelectionIds,
4425
+ useSelectionMask: true,
4426
+ useSemantic: true,
4427
+ selectionScope: draw.id
4428
+ });
4429
+ } else if (!selectionMaskEnabled) {
4430
+ for (let semanticIndex = 0; semanticIndex < draw.batch.semanticSelectionIds.length; semanticIndex += 1) {
4431
+ if (!selected.has(draw.batch.semanticSelectionIds[semanticIndex])) {
4432
+ continue;
4433
+ }
4434
+ pushDraw({
4435
+ geometry,
4436
+ instanceBuffer: this.identityInstances,
4437
+ semanticBuffer: geometry.semanticBuffer ?? this.zeroSemantics,
4438
+ instanceCount: 1,
4439
+ color: [...SELECTION_RGBA2],
4440
+ useVertexColor: false,
4441
+ useInstancing: false,
4442
+ pipeline: this.pipeline.selectionTriangle,
4443
+ semanticHighlight: semanticIndex,
4444
+ useSemantic: true
4445
+ }, matrix);
4446
+ }
4447
+ }
4448
+ }
4449
+ for (const renderable of renderables) {
4450
+ if (instancedMeshIds.has(renderable.id))
4451
+ continue;
4452
+ const geometry = cache.retain(renderable.mesh.geometry);
4453
+ const matrix = matrixFor(renderable.worldMatrix);
4454
+ pushDraw({
4455
+ geometry,
4456
+ instanceBuffer: this.identityInstances,
4457
+ semanticBuffer: this.zeroSemantics,
4458
+ instanceCount: 1,
4459
+ color: colorToRgba(renderable.mesh.material.color),
4460
+ useVertexColor: Boolean(renderable.mesh.geometry.colors),
4461
+ useInstancing: false,
4462
+ pipeline: this.pipeline.triangle,
4463
+ useSelectionMask: selectionMaskEnabled
4464
+ }, matrix, [renderable.mesh.selectionId]);
4465
+ this.frameStats.renderedMeshes += 1;
4466
+ if (renderable.mesh.material.edgeColor) {
4467
+ pushEdgeDraws(geometry, null, matrix, colorToRgba(renderable.mesh.material.edgeColor), this.pipeline.line);
4468
+ }
4469
+ if (selected.has(renderable.mesh.selectionId)) {
4470
+ if (!selectionMaskEnabled) {
4471
+ pushDraw({
4472
+ geometry,
4473
+ instanceBuffer: this.identityInstances,
4474
+ semanticBuffer: this.zeroSemantics,
4475
+ instanceCount: 1,
4476
+ color: [...SELECTION_TINT_RGBA],
4477
+ useVertexColor: false,
4478
+ useInstancing: false,
4479
+ pipeline: this.pipeline.selectionTriangle
4480
+ }, matrix);
4481
+ }
4482
+ pushEdgeDraws(geometry, null, matrix, SELECTION_RGBA2, this.pipeline.selectionLine, selectionMaskEnabled ? {
4483
+ selectionIds: [renderable.mesh.selectionId],
4484
+ useSelectionMask: true,
4485
+ useSemantic: true,
4486
+ selectionScope: renderable.id
4487
+ } : void 0);
4488
+ }
4489
+ }
4490
+ const selectionLayout = selectionMaskEnabled ? buildSelectionMask(drawList.map((draw) => draw.selectionIds ?? []), frame.selection) : { words: EMPTY_SELECTION, bases: [] };
4491
+ this.selectionMaskBuffer?.update(selectionLayout.words);
4492
+ this.ensureUniformCapacity(this.drawStorage.drawCount);
4493
+ const uniforms = this.drawStorage.uniformData.subarray(0, this.drawStorage.drawCount * 64);
4494
+ if (uniforms.length > 0) {
4495
+ this.device.queue.writeBuffer(this.uniformBuffer, 0, uniforms);
4496
+ }
4497
+ this.profiler.mark("prepareAndUpload");
4498
+ const bindGroupKey = [
4499
+ this.uniformBufferGeneration,
4500
+ this.selectionMaskBuffer?.generation ?? 0,
4501
+ this.pipeline.selectionMask ? 1 : 0
4502
+ ].join(":");
4503
+ if (!this.frameBindGroup || this.frameBindGroupKey !== bindGroupKey) {
4504
+ const bindGroupEntries = [{
4505
+ binding: 0,
4506
+ resource: {
4507
+ buffer: this.uniformBuffer,
4508
+ offset: 0,
4509
+ size: WEBGPU_UNIFORM_BYTES
4510
+ }
4511
+ }];
4512
+ if (selectionMaskActive && this.selectionMaskBuffer?.buffer) {
4513
+ bindGroupEntries.push({
4514
+ binding: 1,
4515
+ resource: {
4516
+ buffer: this.selectionMaskBuffer.buffer,
4517
+ offset: 0,
4518
+ size: this.selectionMaskBuffer.buffer.size
4519
+ }
4520
+ });
4521
+ }
4522
+ this.frameBindGroup = this.device.createBindGroup({
4523
+ label: "ice3:frame-uniforms",
4524
+ layout: this.pipeline.bindGroupLayout,
4525
+ entries: bindGroupEntries
4526
+ });
4527
+ this.frameBindGroupKey = bindGroupKey;
4528
+ }
4529
+ const bindGroup = this.frameBindGroup;
4530
+ const encodeDraws = (encoder2) => {
4531
+ for (let index = 0; index < drawList.length; index += 1) {
4532
+ const draw = drawList[index];
4533
+ encoder2.setPipeline(draw.pipeline);
4534
+ const drawBindGroup = draw.storagePage ? this.storageBindGroupFor(draw.storagePage, draw.visibilityEntry?.visibleBuffer) : bindGroup;
4535
+ encoder2.setBindGroup(0, drawBindGroup, [
4536
+ index * WEBGPU_UNIFORM_STRIDE
4537
+ ]);
4538
+ encoder2.setVertexBuffer(0, draw.geometry.vertexBuffer);
4539
+ if (draw.storagePage) {
4540
+ encoder2.setVertexBuffer(1, draw.semanticBuffer);
4541
+ } else {
4542
+ encoder2.setVertexBuffer(1, draw.instanceBuffer);
4543
+ encoder2.setVertexBuffer(2, draw.semanticBuffer);
4544
+ }
4545
+ encoder2.setIndexBuffer(draw.edge ? draw.geometry.edgeIndexBuffer : draw.geometry.triangleIndexBuffer, draw.geometry.indexFormat);
4546
+ if (draw.visibilityEntry) {
4547
+ encoder2.drawIndexedIndirect(draw.visibilityEntry.indirectBuffer, 0);
4548
+ } else {
4549
+ encoder2.drawIndexed(draw.edge ? draw.geometry.edgeCount : draw.geometry.triangleCount, draw.instanceCount, 0, 0, draw.storagePage?.firstInstance ?? 0);
4550
+ }
4551
+ }
4552
+ };
4553
+ const bundleKey = [
4554
+ plan.revision,
4555
+ this.drawStorage.generation,
4556
+ cache.generation,
4557
+ instanceCache.generation,
4558
+ this.storageCache?.generation ?? 0,
4559
+ this.selectionMaskBuffer?.generation ?? 0,
4560
+ this.uniformBufferGeneration,
4561
+ this.format,
4562
+ selectionOverlays.join("|")
4563
+ ].join(":");
4564
+ let bundle = null;
4565
+ if (this.bundleActive && drawList.length > 0) {
4566
+ try {
4567
+ bundle = this.bundleCache.get(bundleKey, encodeDraws);
4568
+ } catch {
4569
+ this.bundleActive = false;
4570
+ this.bundleCache.invalidate();
4571
+ this.reportOptimizationFailure("bundle", "resource-failed");
4572
+ }
4573
+ }
4574
+ const targetTexture = context.getCurrentTexture();
4575
+ const encoder = this.device.createCommandEncoder();
4576
+ if (this.gpuDrivenActive) {
4577
+ const encodedVisibility = /* @__PURE__ */ new Set();
4578
+ for (let index = 0; index < drawList.length; index += 1) {
4579
+ const draw = drawList[index];
4580
+ if (!draw.visibilityEntry || !draw.storagePage || encodedVisibility.has(draw.visibilityEntry)) {
4581
+ continue;
4582
+ }
4583
+ encodedVisibility.add(draw.visibilityEntry);
4584
+ this.visibilityEncoder.encode(encoder, draw.visibilityEntry, draw.storagePage, this.uniformBuffer, index * WEBGPU_UNIFORM_STRIDE);
4585
+ }
4586
+ }
4587
+ const timestampProfiler = this.captureRequest || !this.timestampProfilingEnabled ? null : this.timestampProfiler;
4588
+ const timestampQuerySet = timestampProfiler?.querySetFor(frame.frameId);
4589
+ const pass = encoder.beginRenderPass({
4590
+ colorAttachments: [{
4591
+ view: targetTexture.createView(),
4592
+ clearValue: {
4593
+ r: this.backgroundRgba[0],
4594
+ g: this.backgroundRgba[1],
4595
+ b: this.backgroundRgba[2],
4596
+ a: this.backgroundRgba[3]
4597
+ },
4598
+ loadOp: "clear",
4599
+ storeOp: "store"
4600
+ }],
4601
+ depthStencilAttachment: {
4602
+ view: this.depthTexture.createView(),
4603
+ depthClearValue: 1,
4604
+ depthLoadOp: "clear",
4605
+ depthStoreOp: "store"
4606
+ },
4607
+ ...timestampQuerySet ? {
4608
+ timestampWrites: {
4609
+ querySet: timestampQuerySet,
4610
+ beginningOfPassWriteIndex: 0,
4611
+ endOfPassWriteIndex: 1
4612
+ }
4613
+ } : {}
4614
+ });
4615
+ if (bundle) {
4616
+ try {
4617
+ pass.executeBundles([bundle]);
4618
+ } catch {
4619
+ this.bundleActive = false;
4620
+ this.bundleCache.invalidate();
4621
+ this.reportOptimizationFailure("bundle", "runtime-failed");
4622
+ encodeDraws(pass);
4623
+ }
4624
+ } else {
4625
+ encodeDraws(pass);
4626
+ }
4627
+ this.frameStats.drawCalls += drawList.length;
4628
+ pass.end();
4629
+ if (timestampProfiler) {
4630
+ timestampProfiler.resolveFrame(encoder, frame.frameId);
4631
+ }
4632
+ if (this.captureRequest) {
4633
+ encoder.copyTextureToBuffer({ texture: targetTexture }, {
4634
+ buffer: this.captureRequest.buffer,
4635
+ bytesPerRow: this.captureRequest.bytesPerRow,
4636
+ rowsPerImage: this.canvas.height
4637
+ }, {
4638
+ width: this.canvas.width,
4639
+ height: this.canvas.height,
4640
+ depthOrArrayLayers: 1
4641
+ });
4642
+ this.captureRequest = null;
4643
+ }
4644
+ const commandBuffer = encoder.finish();
4645
+ this.profiler.mark("encode");
4646
+ this.device.queue.submit([commandBuffer]);
4647
+ this.profiler.mark("submit");
4648
+ cache.endFrame();
4649
+ instanceCache.endFrame();
4650
+ this.storageCache?.endFrame();
4651
+ this.frameStats.gpuUploads = cache.uploadCount;
4652
+ this.frameStats.gpuEvictions = cache.evictionCount;
4653
+ this.frameStats.gpuBytes = cache.totalBytes + instanceCache.totalBytes + (this.storageCache?.totalBytes ?? 0);
4654
+ this.lastFrameProfile = this.profiler.end();
4655
+ }
4656
+ enableProfiling(enabled) {
4657
+ this.profiler.enable(enabled);
4658
+ this.lastFrameProfile = null;
4659
+ }
4660
+ get frameProfile() {
4661
+ return this.lastFrameProfile;
4662
+ }
4663
+ async pick(frame, point) {
4664
+ if (!this.picker) {
4665
+ throw new ICEEngineError("ICE3_WEBGPU_PICK_UNAVAILABLE", "ICE3_WEBGPU_PICK_UNAVAILABLE: picker is not initialized");
4666
+ }
4667
+ try {
4668
+ return await this.picker.pick({ ...frame, point }, this.requireGeometryCache(), collectWebGPUPickIds(frame));
4669
+ } catch (error) {
4670
+ if (error instanceof ICEEngineError && error.code === "ICE3_WEBGPU_PICK_UNAVAILABLE") {
4671
+ throw error;
4672
+ }
4673
+ throw new ICEEngineError("ICE3_WEBGPU_PICK_UNAVAILABLE", "ICE3_WEBGPU_PICK_UNAVAILABLE: exact pick failed", { cause: error });
4674
+ }
4675
+ }
4676
+ dispose() {
4677
+ this.disposing = true;
4678
+ this.bundleCache.dispose();
4679
+ this.geometryCache?.dispose();
4680
+ this.renderPlan.dispose();
4681
+ this.picker?.dispose();
4682
+ this.selectionMaskBuffer?.dispose();
4683
+ this.storageCache?.dispose();
4684
+ this.visibilityEncoder?.dispose();
4685
+ this.timestampProfiler?.dispose();
4686
+ this.instanceCache?.dispose();
4687
+ this.identityInstances?.destroy();
4688
+ this.zeroSemantics?.destroy();
4689
+ this.uniformBuffer?.destroy();
4690
+ this.depthTexture?.destroy();
4691
+ this.context?.unconfigure();
4692
+ this.geometryCache = null;
4693
+ this.picker = null;
4694
+ this.selectionMaskBuffer = null;
4695
+ this.storageCache = null;
4696
+ this.visibilityEncoder = null;
4697
+ this.gpuDrivenPipeline = null;
4698
+ this.timestampProfiler = null;
4699
+ this.instanceCache = null;
4700
+ this.identityInstances = null;
4701
+ this.zeroSemantics = null;
4702
+ this.uniformBuffer = null;
4703
+ this.depthTexture = null;
4704
+ this.context = null;
4705
+ this.canvas = null;
4706
+ this.frameBindGroup = null;
4707
+ this.device.destroy();
4708
+ this.releaseProbe?.();
4709
+ this.onContextLost = void 0;
4710
+ this.onContextRestored = void 0;
4711
+ this.onError = void 0;
4712
+ }
4713
+ async capture() {
4714
+ if (!this.context || !this.canvas || !this.lastFrame) {
4715
+ throw new ICEEngineError("ICE3_ENGINE_NOT_READY", "Renderer has no frame to capture");
4716
+ }
4717
+ const width = this.canvas.width;
4718
+ const height = this.canvas.height;
4719
+ const bytesPerRow = Math.ceil(width * 4 / 256) * 256;
4720
+ const readback = this.device.createBuffer({
4721
+ label: "ice3:capture-readback",
4722
+ size: bytesPerRow * height,
4723
+ usage: 1 | 8
4724
+ });
4725
+ this.captureRequest = { buffer: readback, bytesPerRow };
4726
+ try {
4727
+ this.render(this.lastFrame);
4728
+ } catch (error) {
4729
+ this.captureRequest = null;
4730
+ readback.destroy();
4731
+ throw error;
4732
+ }
4733
+ let bytes;
4734
+ try {
4735
+ await readback.mapAsync(1);
4736
+ bytes = new Uint8Array(readback.getMappedRange()).slice();
4737
+ } finally {
4738
+ readback.unmap();
4739
+ readback.destroy();
4740
+ }
4741
+ for (let offset = 0; offset < bytes.length; offset += 4) {
4742
+ if (this.format === "bgra8unorm" || this.format === "bgra8unorm-srgb") {
4743
+ [bytes[offset], bytes[offset + 2]] = [
4744
+ bytes[offset + 2],
4745
+ bytes[offset]
4746
+ ];
4747
+ }
4748
+ }
4749
+ const imageBytes = new Uint8Array(width * height * 4);
4750
+ const rowBytes = width * 4;
4751
+ for (let row = 0; row < height; row += 1) {
4752
+ imageBytes.set(bytes.subarray(row * bytesPerRow, row * bytesPerRow + rowBytes), row * rowBytes);
4753
+ }
4754
+ return pixelsToPng(imageBytes, width, height);
4755
+ }
4756
+ async finish() {
4757
+ await this.device.queue.onSubmittedWorkDone?.();
4758
+ }
4759
+ async drainGpuTimings() {
4760
+ return this.timestampProfiler?.drain() ?? [];
4761
+ }
4762
+ enableTimestampProfiling(enabled) {
4763
+ this.timestampProfilingEnabled = enabled && this.timestampProfiler !== null;
4764
+ }
4765
+ get timestampProfilingAvailable() {
4766
+ return this.timestampProfiler !== null;
4767
+ }
4768
+ get timestampProfilingUnavailableReason() {
4769
+ return this.timestampProfiler ? this.timestampProfiler.unavailableReason : "timestamp-query unavailable";
4770
+ }
4771
+ get stats() {
4772
+ return this.frameStats;
4773
+ }
4774
+ async readVisibilityStats() {
4775
+ if (!this.visibilityEncoder) {
4776
+ return void 0;
4777
+ }
4778
+ let visibleInstances = 0;
4779
+ let culledInstances = 0;
4780
+ let dispatchCount = 0;
4781
+ const seen = /* @__PURE__ */ new Set();
4782
+ for (const item of this.lastVisibilityEntries) {
4783
+ if (seen.has(item.entry))
4784
+ continue;
4785
+ seen.add(item.entry);
4786
+ const result = await this.visibilityEncoder.readStats(item.entry, item.page);
4787
+ visibleInstances += result.visibleInstances;
4788
+ culledInstances += result.culledInstances;
4789
+ dispatchCount += result.dispatchCount;
4790
+ }
4791
+ return { visibleInstances, culledInstances, dispatchCount };
4792
+ }
4793
+ get storageActive() {
4794
+ return (this.forceStoragePath || this.forceGpuDriven) && this.storagePipeline !== null && this.storageCache !== null;
4795
+ }
4796
+ get gpuDrivenActive() {
4797
+ return this.forceGpuDriven && this.gpuDrivenPipeline !== null && this.visibilityEncoder !== null && this.storageCache !== null;
4798
+ }
4799
+ get optimizationStats() {
4800
+ return {
4801
+ tier: this.gpuDrivenActive ? "gpu-driven" : this.storageActive ? "storage" : "baseline",
4802
+ selectionMaskActive: this.selectionMaskBuffer !== null,
4803
+ bundleActive: this.bundleActive,
4804
+ storageActive: this.storageActive,
4805
+ visibilityActive: this.gpuDrivenActive,
4806
+ instanceUploadBytes: this.storageCache?.totalUploadBytes ?? 0,
4807
+ bundleRecreations: this.bundleCache.bundleRecreations,
4808
+ ...this.timestampProfilingUnavailableReason ? {
4809
+ timestampUnavailableReason: this.timestampProfilingUnavailableReason
4810
+ } : {}
4811
+ };
4812
+ }
4813
+ storageBindGroupFor(page, visibleBuffer) {
4814
+ const key = [
4815
+ this.uniformBufferGeneration,
4816
+ this.selectionMaskBuffer?.generation ?? 0
4817
+ ].join(":");
4818
+ const cached = this.storageBindGroups.get(page.buffer);
4819
+ if (cached?.key === key && cached.visibleBuffer === visibleBuffer) {
4820
+ return cached.group;
4821
+ }
4822
+ const entries = [{
4823
+ binding: 0,
4824
+ resource: {
4825
+ buffer: this.uniformBuffer,
4826
+ offset: 0,
4827
+ size: WEBGPU_UNIFORM_BYTES
4828
+ }
4829
+ }];
4830
+ if (this.selectionMaskBuffer?.buffer) {
4831
+ entries.push({
4832
+ binding: 1,
4833
+ resource: {
4834
+ buffer: this.selectionMaskBuffer.buffer,
4835
+ offset: 0,
4836
+ size: this.selectionMaskBuffer.buffer.size
4837
+ }
4838
+ });
4839
+ }
4840
+ entries.push({
4841
+ binding: 2,
4842
+ resource: {
4843
+ buffer: page.buffer,
4844
+ offset: 0,
4845
+ size: page.buffer.size
4846
+ }
4847
+ });
4848
+ if (visibleBuffer) {
4849
+ entries.push({
4850
+ binding: 3,
4851
+ resource: {
4852
+ buffer: visibleBuffer,
4853
+ offset: 0,
4854
+ size: visibleBuffer.size
4855
+ }
4856
+ });
4857
+ }
4858
+ const group = this.device.createBindGroup({
4859
+ label: "ice3:storage-instance-pipeline",
4860
+ layout: visibleBuffer ? this.gpuDrivenPipeline.bindGroupLayout : this.storagePipeline.bindGroupLayout,
4861
+ entries
4862
+ });
4863
+ this.storageBindGroups.set(page.buffer, {
4864
+ key,
4865
+ ...visibleBuffer ? { visibleBuffer } : {},
4866
+ group
4867
+ });
4868
+ return group;
4869
+ }
4870
+ reportOptimizationFailure(layer, reason) {
4871
+ const key = `${layer}/${reason}`;
4872
+ if (this.optimizationWarnings.has(key))
4873
+ return;
4874
+ this.optimizationWarnings.add(key);
4875
+ this.onError?.(new ICEEngineError("ICE3_RENDERER_OPTIMIZATION_UNAVAILABLE", `ICE3_RENDERER_OPTIMIZATION_UNAVAILABLE: ${key}`, { layer, reason }));
4876
+ }
4877
+ requireGeometryCache() {
4878
+ if (!this.geometryCache) {
4879
+ throw new ICEEngineError("ICE3_ENGINE_NOT_READY", "Renderer is not initialized");
4880
+ }
4881
+ return this.geometryCache;
4882
+ }
4883
+ ensureUniformCapacity(draws) {
4884
+ if (draws <= this.uniformCapacity && this.uniformBuffer)
4885
+ return;
4886
+ let capacity = Math.max(UNIFORM_BUFFER_INITIAL_DRAWS, drawCountPowerOfTwo2(draws));
4887
+ this.uniformBuffer?.destroy();
4888
+ this.uniformBuffer = this.device.createBuffer({
4889
+ label: "ice3:frame-uniforms",
4890
+ size: capacity * WEBGPU_UNIFORM_STRIDE,
4891
+ usage: UNIFORM_USAGE2
4892
+ });
4893
+ this.uniformCapacity = capacity;
4894
+ this.uniformBufferGeneration += 1;
4895
+ this.frameBindGroup = null;
4896
+ this.bundleCache.invalidate();
4897
+ }
4898
+ watchDevice(device) {
4899
+ void device.lost.then((info) => {
4900
+ if (this.disposing || device !== this.device || info.reason === "destroyed") {
4901
+ return;
4902
+ }
4903
+ void this.recoverDevice();
4904
+ });
4905
+ }
4906
+ async recoverDevice() {
4907
+ this.releaseProbe?.();
4908
+ this.onContextLost?.();
4909
+ this.geometryCache?.dispose();
4910
+ this.bundleCache.dispose();
4911
+ this.instanceCache?.dispose();
4912
+ this.picker?.dispose();
4913
+ this.selectionMaskBuffer?.dispose();
4914
+ this.storageCache?.dispose();
4915
+ this.timestampProfiler?.dispose();
4916
+ this.identityInstances?.destroy();
4917
+ this.zeroSemantics?.destroy();
4918
+ this.uniformBuffer?.destroy();
4919
+ this.depthTexture?.destroy();
4920
+ this.geometryCache = null;
4921
+ this.instanceCache = null;
4922
+ this.selectionMaskBuffer = null;
4923
+ this.storageCache = null;
4924
+ this.picker = null;
4925
+ this.timestampProfiler = null;
4926
+ this.identityInstances = null;
4927
+ this.zeroSemantics = null;
4928
+ this.uniformBuffer = null;
4929
+ this.depthTexture = null;
4930
+ this.uniformCapacity = 0;
4931
+ this.frameBindGroup = null;
4932
+ this.bundleActive = true;
4933
+ this.optimizationWarnings.clear();
4934
+ this.storagePipeline = null;
4935
+ this.storageBindGroups.clear();
4936
+ this.renderPlan.invalidate();
4937
+ try {
4938
+ const features = this.device.features.has("timestamp-query") ? ["timestamp-query"] : [];
4939
+ const device = await this.adapter.requestDevice({
4940
+ ...features.length > 0 ? { requiredFeatures: features } : {}
4941
+ });
4942
+ let selectionMaskBuffer = null;
4943
+ let pipeline;
4944
+ try {
4945
+ selectionMaskBuffer = new WebGPUSelectionMaskBuffer(device);
4946
+ selectionMaskBuffer.update(new Uint32Array([0]));
4947
+ pipeline = await WebGPURenderPipeline.create(device, this.format, { selectionMask: true });
4948
+ } catch {
4949
+ selectionMaskBuffer?.dispose();
4950
+ selectionMaskBuffer = null;
4951
+ pipeline = await WebGPURenderPipeline.create(device, this.format);
4952
+ }
4953
+ if (this.disposing) {
4954
+ selectionMaskBuffer?.dispose();
4955
+ device.destroy();
4956
+ return;
4957
+ }
4958
+ this.device = device;
4959
+ this.pipeline = pipeline;
4960
+ this.selectionMaskBuffer = selectionMaskBuffer;
4961
+ this.bundleCache = new WebGPUBundleCache(device, this.format);
4962
+ this.watchDevice(device);
4963
+ if (this.canvas) {
4964
+ this.initialize({
4965
+ canvas: this.canvas,
4966
+ ...this.onContextLost ? { onContextLost: this.onContextLost } : {},
4967
+ ...this.onContextRestored ? { onContextRestored: this.onContextRestored } : {}
4968
+ });
4969
+ this.resize(this.viewport, this.dpr);
4970
+ }
4971
+ this.onContextRestored?.();
4972
+ } catch (error) {
4973
+ if (this.disposing)
4974
+ return;
4975
+ this.onError?.(new ICEEngineError("ICE3_WEBGPU_DEVICE_LOST", `ICE3_WEBGPU_DEVICE_LOST: ${error instanceof Error ? error.message : String(error)}`, { cause: error }));
4976
+ }
4977
+ }
4978
+ };
4979
+ function drawCountPowerOfTwo2(value) {
4980
+ let result = 1;
4981
+ while (result < Math.max(1, value))
4982
+ result *= 2;
4983
+ return result;
4984
+ }
4985
+ async function pixelsToPng(bytes, width, height) {
4986
+ const pixels = new Uint8ClampedArray(bytes.buffer, bytes.byteOffset, width * height * 4).slice();
4987
+ if (typeof OffscreenCanvas !== "undefined") {
4988
+ const canvas2 = new OffscreenCanvas(width, height);
4989
+ const context2 = canvas2.getContext("2d");
4990
+ if (!context2)
4991
+ throw new Error("ICE3_GPU_RESOURCE_FAILED: 2D context");
4992
+ const image2 = typeof ImageData === "undefined" ? { data: pixels, width, height } : new ImageData(pixels, width, height);
4993
+ context2.putImageData(image2, 0, 0);
4994
+ return canvas2.convertToBlob({ type: "image/png" });
4995
+ }
4996
+ const canvas = document.createElement("canvas");
4997
+ canvas.width = width;
4998
+ canvas.height = height;
4999
+ const context = canvas.getContext("2d");
5000
+ if (!context)
5001
+ throw new Error("ICE3_GPU_RESOURCE_FAILED: 2D context");
5002
+ const image = typeof ImageData === "undefined" ? { data: pixels, width, height } : new ImageData(pixels, width, height);
5003
+ context.putImageData(image, 0, 0);
5004
+ return new Promise((resolve, reject) => {
5005
+ canvas.toBlob((blob) => {
5006
+ if (blob)
5007
+ resolve(blob);
5008
+ else
5009
+ reject(new Error("ICE3_GPU_RESOURCE_FAILED: PNG encoding failed"));
5010
+ }, "image/png");
5011
+ });
5012
+ }
5013
+
5014
+ // packages/renderer/dist/RendererSelection.js
5015
+ async function selectRendererBackend(preference, factories) {
5016
+ const startedAt = performance.now();
5017
+ if (preference === "auto" || preference === "webgl2") {
5018
+ const backend = factories.createWebGL2();
5019
+ return {
5020
+ backend,
5021
+ selection: {
5022
+ requested: preference,
5023
+ selected: backend.capabilities.backend,
5024
+ fallback: false,
5025
+ experimental: false,
5026
+ probeDurationMs: performance.now() - startedAt
5027
+ }
5028
+ };
5029
+ }
5030
+ try {
5031
+ const backend = await factories.createWebGPU();
5032
+ return {
5033
+ backend,
5034
+ selection: {
5035
+ requested: preference,
5036
+ selected: backend.capabilities.backend,
5037
+ fallback: false,
5038
+ experimental: true,
5039
+ probeDurationMs: performance.now() - startedAt
5040
+ }
5041
+ };
5042
+ } catch (error) {
5043
+ if (preference === "webgpu") {
5044
+ if (error instanceof ICEEngineError && error.code === "ICE3_WEBGPU_UNAVAILABLE") {
5045
+ throw error;
5046
+ }
5047
+ throw new ICEEngineError("ICE3_WEBGPU_UNAVAILABLE", `ICE3_WEBGPU_UNAVAILABLE: ${error instanceof Error ? error.message : String(error)}`, { reason: failureReason(error) });
5048
+ }
5049
+ const backend = factories.createWebGL2();
5050
+ const reason = failureReason(error);
5051
+ return {
5052
+ backend,
5053
+ selection: {
5054
+ requested: preference,
5055
+ selected: backend.capabilities.backend,
5056
+ fallback: true,
5057
+ experimental: false,
5058
+ probeDurationMs: performance.now() - startedAt,
5059
+ ...reason ? { fallbackReason: reason } : {}
5060
+ }
5061
+ };
5062
+ }
5063
+ }
5064
+ async function selectDefaultRendererBackend(preference) {
5065
+ return selectRendererBackend(preference, {
5066
+ createWebGPU: createDefaultWebGPU,
5067
+ createWebGL2: () => new WebGL2RendererBackend()
5068
+ });
5069
+ }
5070
+ async function createDefaultWebGPU() {
5071
+ const result = await probeWebGPU();
5072
+ if (!result.supported) {
5073
+ throw new ICEEngineError("ICE3_WEBGPU_UNAVAILABLE", `ICE3_WEBGPU_UNAVAILABLE: ${result.reason}${result.detail ? `: ${result.detail}` : ""}`, { reason: result.reason });
5074
+ }
5075
+ try {
5076
+ return await WebGPURendererBackend.create(result);
5077
+ } catch (error) {
5078
+ result.device.destroy();
5079
+ result.release();
5080
+ throw new ICEEngineError("ICE3_WEBGPU_UNAVAILABLE", `ICE3_WEBGPU_UNAVAILABLE: ${error instanceof Error ? error.message : String(error)}`, { reason: "pipeline-failed" });
5081
+ }
5082
+ }
5083
+ function failureReason(error) {
5084
+ const details = error?.details;
5085
+ const reason = details?.reason;
5086
+ return typeof reason === "string" ? reason : void 0;
5087
+ }
5088
+
5089
+ // packages/renderer/dist/ICEWebGL2BatchGenerationUploader.js
5090
+ function queueBatchGeneration(buffers, options) {
5091
+ if (!Number.isInteger(options.generation) || options.generation < 0 || !Number.isInteger(options.bytesPerFrame) || options.bytesPerFrame < 1 || buffers.some((buffer) => !Number.isInteger(buffer.byteLength) || buffer.byteLength < 1 || buffer.byteLength > options.bytesPerFrame)) {
5092
+ fail(options, new Error("ICE3_RENDER_BATCH_UPLOAD_INVALID: upload options are invalid"));
5093
+ return { cancel: () => void 0 };
5094
+ }
5095
+ const schedule = options.schedule ?? globalThis.requestAnimationFrame;
5096
+ const cancelSchedule = options.cancelSchedule ?? globalThis.cancelAnimationFrame;
5097
+ const upload = options.upload ?? ((buffer) => buffer);
5098
+ const release = options.release ?? (() => void 0);
5099
+ const resources = [];
5100
+ let next = 0;
5101
+ let frame;
5102
+ let settled = false;
5103
+ const cleanup = () => {
5104
+ if (frame !== void 0) {
5105
+ cancelSchedule(frame);
5106
+ frame = void 0;
5107
+ }
5108
+ for (const resource of resources.splice(0))
5109
+ release(resource);
5110
+ };
5111
+ const current = () => options.isCurrent?.() ?? true;
5112
+ const failNow = (error) => {
5113
+ if (settled)
5114
+ return;
5115
+ settled = true;
5116
+ cleanup();
5117
+ options.onFailed(options.generation, error);
5118
+ };
5119
+ const pump = () => {
5120
+ frame = void 0;
5121
+ if (settled)
5122
+ return;
5123
+ if (!current()) {
5124
+ settled = true;
5125
+ cleanup();
5126
+ return;
5127
+ }
5128
+ let bytes = 0;
5129
+ try {
5130
+ while (next < buffers.length) {
5131
+ const buffer = buffers[next];
5132
+ if (resources.length > 0 && bytes + buffer.byteLength > options.bytesPerFrame) {
5133
+ break;
5134
+ }
5135
+ bytes += buffer.byteLength;
5136
+ next += 1;
5137
+ resources.push(upload(buffer));
5138
+ }
5139
+ } catch (error) {
5140
+ failNow(error);
5141
+ return;
5142
+ }
5143
+ if (next >= buffers.length) {
5144
+ if (!current()) {
5145
+ settled = true;
5146
+ cleanup();
5147
+ return;
5148
+ }
5149
+ try {
5150
+ options.onActivate?.(options.generation, resources);
5151
+ } catch (error) {
5152
+ failNow(error);
5153
+ return;
5154
+ }
5155
+ settled = true;
2151
5156
  options.onUploaded(options.generation);
2152
5157
  return;
2153
5158
  }
@@ -2819,6 +5824,8 @@ var NAVIGATION_KEY_CODES = /* @__PURE__ */ new Set([
2819
5824
  ]);
2820
5825
  var ICE3 = class extends ICEEventDispatcher {
2821
5826
  backend;
5827
+ rendererPreference;
5828
+ rendererSelection;
2822
5829
  scheduler;
2823
5830
  interaction = new ICEInteractionState();
2824
5831
  layers = [];
@@ -2854,19 +5861,53 @@ var ICE3 = class extends ICEEventDispatcher {
2854
5861
  pointerGestures = /* @__PURE__ */ new Map();
2855
5862
  lastDiagnostic;
2856
5863
  instancingFallbackReported = false;
5864
+ pickGeneration = 0;
2857
5865
  removeInputListeners = [];
2858
5866
  handleWindowResize = () => {
2859
5867
  this.resize();
2860
5868
  };
2861
5869
  constructor(options = {}) {
2862
5870
  super();
5871
+ if (options.backend && options.renderer) {
5872
+ throw new ICEEngineError("ICE3_RENDERER_CONFIG_INVALID", "ICE3_RENDERER_CONFIG_INVALID: backend injection cannot be combined with a renderer preference");
5873
+ }
5874
+ this.rendererPreference = options.renderer;
2863
5875
  this.backend = options.backend ?? new WebGL2RendererBackend();
5876
+ this.rendererSelection = {
5877
+ requested: options.backend ? "custom" : options.renderer ?? "webgl2",
5878
+ selected: this.backend.capabilities.backend,
5879
+ fallback: false,
5880
+ experimental: this.backend.capabilities.backend === "webgpu"
5881
+ };
2864
5882
  this.scheduler = options.scheduler ?? new BrowserRenderScheduler();
2865
5883
  }
2866
5884
  init(target) {
2867
5885
  if (this.state !== "created") {
2868
5886
  throw new ICEEngineError("ICE3_ALREADY_INITIALIZED", "ICE3 engine can only be initialized once");
2869
5887
  }
5888
+ if (this.rendererPreference === "webgpu") {
5889
+ throw new ICEEngineError("ICE3_RENDERER_ASYNC_REQUIRED", "ICE3_RENDERER_ASYNC_REQUIRED: webgpu requires initAsync()");
5890
+ }
5891
+ return this.initializeWithBackend(target);
5892
+ }
5893
+ async initAsync(target) {
5894
+ if (this.state !== "created") {
5895
+ throw new ICEEngineError("ICE3_ALREADY_INITIALIZED", "ICE3 engine can only be initialized once");
5896
+ }
5897
+ if (this.rendererSelection.requested === "custom") {
5898
+ return this.initializeWithBackend(target);
5899
+ }
5900
+ const selected = await selectDefaultRendererBackend(this.rendererPreference ?? "webgl2");
5901
+ this.backend = selected.backend;
5902
+ this.rendererSelection = selected.selection;
5903
+ try {
5904
+ return this.initializeWithBackend(target);
5905
+ } catch (error) {
5906
+ selected.backend.dispose();
5907
+ throw error;
5908
+ }
5909
+ }
5910
+ initializeWithBackend(target) {
2870
5911
  if (typeof target === "string") {
2871
5912
  const element = document.querySelector(target);
2872
5913
  if (!element) {
@@ -2896,6 +5937,11 @@ var ICE3 = class extends ICEEventDispatcher {
2896
5937
  this.recordDiagnostic("ICE3_CONTEXT_RESTORED", "The GPU context was restored", "info");
2897
5938
  this.trigger("CONTEXT_RESTORED");
2898
5939
  this.requestRender("context-restored");
5940
+ },
5941
+ onError: (error) => {
5942
+ if (this.state === "destroyed")
5943
+ return;
5944
+ this.recordDiagnostic(error.code, error.message, "error");
2899
5945
  }
2900
5946
  });
2901
5947
  window.addEventListener("resize", this.handleWindowResize);
@@ -3034,23 +6080,34 @@ var ICE3 = class extends ICEEventDispatcher {
3034
6080
  this.requestRender("selection-changed");
3035
6081
  return this;
3036
6082
  }
3037
- pick(point) {
3038
- const result = this.samplePick(point);
6083
+ async pick(point) {
6084
+ const result = await this.samplePick(point);
3039
6085
  return result.kind === "hit" ? result.node : void 0;
3040
6086
  }
3041
- pickItem(point) {
3042
- const node = this.pick(point);
6087
+ async pickItem(point) {
6088
+ const node = await this.pick(point);
3043
6089
  if (!node)
3044
6090
  return Object.freeze({});
3045
- const item = this.queryScene({ ids: [node.id], limit: 1 })[0];
6091
+ const item = this.queryScene({
6092
+ selectionIds: [node.selectionId ?? node.id],
6093
+ limit: 1
6094
+ })[0];
3046
6095
  return Object.freeze(item ? { item } : {});
3047
6096
  }
6097
+ async capture() {
6098
+ this.assertActive();
6099
+ if (!this.backend.capture) {
6100
+ throw new ICEEngineError("ICE3_CAPTURE_UNAVAILABLE", "ICE3_CAPTURE_UNAVAILABLE: the active renderer cannot capture pixels");
6101
+ }
6102
+ return this.backend.capture();
6103
+ }
3048
6104
  getDiagnostics() {
3049
6105
  return {
3050
6106
  frameId: this.frameId,
3051
6107
  fps: this.fps,
3052
6108
  cpuFrameTimeMs: this.cpuFrameTimeMs,
3053
6109
  backend: this.backend.capabilities,
6110
+ rendererSelection: this.rendererSelection,
3054
6111
  nodeCount: this.scene ? countNodes(this.scene) : 0,
3055
6112
  layerCount: this.layers.length,
3056
6113
  labelCount: this.layers.reduce((total, layer) => total + layer.getLabels().length, 0),
@@ -3251,13 +6308,18 @@ var ICE3 = class extends ICEEventDispatcher {
3251
6308
  }
3252
6309
  this.inputRouter.pointerMove(point, event.pointerId);
3253
6310
  });
3254
- listen("pointerup", (event) => {
6311
+ listen("pointerup", async (event) => {
3255
6312
  const point = pointOf(event);
3256
6313
  const gesture = this.pointerGestures.get(event.pointerId);
3257
6314
  this.inputRouter.pointerUp(point, event.pointerId);
3258
6315
  this.pointerGestures.delete(event.pointerId);
6316
+ overlay.releasePointerCapture?.(event.pointerId);
3259
6317
  if (gesture && gesture.target.kind === "scene" && !gesture.dragged && isSelectionClick(gesture.start, point)) {
3260
- const result = this.samplePick(point);
6318
+ const generation = ++this.pickGeneration;
6319
+ const result = await this.samplePick(point);
6320
+ if (generation !== this.pickGeneration || this.state === "destroyed") {
6321
+ return;
6322
+ }
3261
6323
  const outcome = result.kind === "hit" ? {
3262
6324
  kind: "hit",
3263
6325
  id: result.selectionId
@@ -3267,7 +6329,6 @@ var ICE3 = class extends ICEEventDispatcher {
3267
6329
  this.setSelection(nextSelection);
3268
6330
  }
3269
6331
  }
3270
- overlay.releasePointerCapture?.(event.pointerId);
3271
6332
  });
3272
6333
  listen("pointercancel", (event) => {
3273
6334
  this.inputRouter.pointerCancel(event.pointerId);
@@ -3325,7 +6386,7 @@ var ICE3 = class extends ICEEventDispatcher {
3325
6386
  }[event.type];
3326
6387
  this.trigger(eventName, { target, event });
3327
6388
  }
3328
- samplePick(point) {
6389
+ async samplePick(point) {
3329
6390
  this.assertActive();
3330
6391
  if (!this.scene || !this.camera) {
3331
6392
  return { kind: "empty" };
@@ -3333,7 +6394,7 @@ var ICE3 = class extends ICEEventDispatcher {
3333
6394
  if (this.backend.capabilities.picking && this.backend.pick) {
3334
6395
  const startedAt = performance.now();
3335
6396
  try {
3336
- const selectionId = this.backend.pick({
6397
+ const selectionId = await this.backend.pick({
3337
6398
  frameId: this.frameId,
3338
6399
  scene: this.scene,
3339
6400
  camera: this.camera,