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.
- package/README.md +9 -0
- package/dist/_types/core/scene/ICENode.d.ts +6 -1
- package/dist/_types/renderer/Color.d.ts +2 -0
- package/dist/_types/renderer/FakeRendererBackend.d.ts +1 -1
- package/dist/_types/renderer/InstanceBatch.d.ts +14 -0
- package/dist/_types/renderer/RenderBatch.d.ts +17 -0
- package/dist/_types/renderer/Renderables.d.ts +9 -0
- package/dist/_types/renderer/RendererProfiler.d.ts +19 -0
- package/dist/_types/renderer/RendererSelection.d.ts +12 -0
- package/dist/_types/renderer/WebGL2GeometryBatch.d.ts +2 -9
- package/dist/_types/renderer/WebGL2InstanceBatch.d.ts +3 -14
- package/dist/_types/renderer/WebGL2RenderBatch.d.ts +1 -16
- package/dist/_types/renderer/WebGL2RendererBackend.d.ts +9 -2
- package/dist/_types/renderer/WebGPUBundleCache.d.ts +14 -0
- package/dist/_types/renderer/WebGPUCapabilities.d.ts +23 -0
- package/dist/_types/renderer/WebGPUDrawStorage.d.ts +27 -0
- package/dist/_types/renderer/WebGPUGeometryCache.d.ts +45 -0
- package/dist/_types/renderer/WebGPUInstanceCache.d.ts +24 -0
- package/dist/_types/renderer/WebGPUInstanceStorage.d.ts +39 -0
- package/dist/_types/renderer/WebGPUPicker.d.ts +30 -0
- package/dist/_types/renderer/WebGPUProbe.d.ts +21 -0
- package/dist/_types/renderer/WebGPURenderPipeline.d.ts +22 -0
- package/dist/_types/renderer/WebGPURenderPlan.d.ts +23 -0
- package/dist/_types/renderer/WebGPURendererBackend.d.ts +111 -0
- package/dist/_types/renderer/WebGPUResources.d.ts +9 -0
- package/dist/_types/renderer/WebGPUSelectionMask.d.ts +18 -0
- package/dist/_types/renderer/WebGPUTimestampProfiler.d.ts +16 -0
- package/dist/_types/renderer/WebGPUVisibility.d.ts +27 -0
- package/dist/_types/renderer/index.d.ts +21 -7
- package/dist/_types/renderer/types.d.ts +15 -3
- package/dist/_types/runtime/ICE3.d.ts +14 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3204 -143
- package/dist/index.js.map +4 -4
- 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/
|
|
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/
|
|
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
|
-
|
|
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/
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
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
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
const
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
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
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
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
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
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({
|
|
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
|
|
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,
|