@voluma/vlam 0.2.3 → 0.3.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 +3 -2
- package/dist/budget-governor.d.ts +4 -4
- package/dist/camera-budget-governor.d.ts +11 -11
- package/dist/chunk-cache-budget.d.ts +5 -5
- package/dist/chunk-fetch-scheduler.d.ts +4 -4
- package/dist/chunk-loader-DlNwggfG.js +1859 -0
- package/dist/chunk-loader-DlNwggfG.js.map +1 -0
- package/dist/chunk-loader.d.ts +2 -2
- package/dist/compute-sorter.d.ts +1 -1
- package/dist/effects.d.ts +26 -16
- package/dist/effects.js +199 -165
- package/dist/effects.js.map +1 -1
- package/dist/formats/ksplat/index.d.ts +1 -1
- package/dist/formats/lcc/collision-mesh.d.ts +2 -2
- package/dist/formats/lcc.js +17 -16
- package/dist/formats/lcc.js.map +1 -1
- package/dist/formats/ply/index.d.ts +1 -1
- package/dist/formats/ply/parse-splat-ply.d.ts +2 -2
- package/dist/formats/ply.js +1 -1
- package/dist/formats/ply.js.map +1 -1
- package/dist/formats/rad/index.d.ts +1 -1
- package/dist/formats/rad/parse-rad.d.ts +1 -1
- package/dist/formats/rad/rad.d.ts +2 -2
- package/dist/formats/rad.js +3 -3
- package/dist/formats/rad.js.map +1 -1
- package/dist/formats/sog/index.d.ts +1 -1
- package/dist/formats/sog/parse-sog.d.ts +4 -4
- package/dist/formats/sog.js.map +1 -1
- package/dist/formats/splat/index.d.ts +1 -1
- package/dist/formats/spz/index.d.ts +1 -1
- package/dist/index.d.ts +16 -25
- package/dist/index.js +253 -9047
- package/dist/index.js.map +1 -1
- package/dist/{load-scene.d.ts → load-splat-data.d.ts} +15 -13
- package/dist/loaders.d.ts +15 -0
- package/dist/loaders.js +26 -0
- package/dist/loaders.js.map +1 -0
- package/dist/loading-wLo8vRbA.js.map +1 -1
- package/dist/loading.d.ts +8 -8
- package/dist/{splat-mesh-types-CTN_duw2.js → lod-scheduler-B0a_uBlv.js} +133 -140
- package/dist/lod-scheduler-B0a_uBlv.js.map +1 -0
- package/dist/lod-scheduler.d.ts +1 -1
- package/dist/lod-source.d.ts +5 -5
- package/dist/{splat-scene.d.ts → merged-splat-mesh.d.ts} +14 -15
- package/dist/orientation.d.ts +7 -7
- package/dist/{ply-header-BwgExCn-.js → ply-header-B9WllbPg.js} +3 -3
- package/dist/{ply-header-BwgExCn-.js.map → ply-header-B9WllbPg.js.map} +1 -1
- package/dist/radix-sorter-R0FCSqKu.js +178 -0
- package/dist/radix-sorter-R0FCSqKu.js.map +1 -0
- package/dist/relighting-CSQF3UcL.js +969 -0
- package/dist/relighting-CSQF3UcL.js.map +1 -0
- package/dist/relighting.d.ts +1 -1
- package/dist/selection.d.ts +13 -0
- package/dist/selection.js +137 -0
- package/dist/selection.js.map +1 -0
- package/dist/source-transform.d.ts +4 -4
- package/dist/{splat-budget-DXew_86c.js → splat-budget-DuVZspPQ.js} +7 -7
- package/dist/{splat-budget-DXew_86c.js.map → splat-budget-DuVZspPQ.js.map} +1 -1
- package/dist/splat-budget.d.ts +1 -1
- package/dist/splat-data-BZM5dhhx.js.map +1 -1
- package/dist/splat-data.d.ts +4 -4
- package/dist/splat-mesh-aQdcWA6v.js +1942 -0
- package/dist/splat-mesh-aQdcWA6v.js.map +1 -0
- package/dist/splat-mesh-material.d.ts +2 -2
- package/dist/splat-mesh-types-BZIko-_9.js +18 -0
- package/dist/splat-mesh-types-BZIko-_9.js.map +1 -0
- package/dist/splat-mesh-types.d.ts +14 -8
- package/dist/splat-mesh.d.ts +4 -4
- package/dist/splat-modifier.d.ts +5 -5
- package/dist/splat-partition.d.ts +1 -1
- package/dist/static-lod-entry.d.ts +12 -0
- package/dist/static-lod-splat-mesh.d.ts +2 -2
- package/dist/{static-lod-worker-D2fyqQZO.js → static-lod-worker-DktKnJkR.js} +46 -46
- package/dist/{static-lod-worker-D2fyqQZO.js.map → static-lod-worker-DktKnJkR.js.map} +1 -1
- package/dist/static-lod.js +148 -0
- package/dist/static-lod.js.map +1 -0
- package/dist/streamed-splat-mesh.d.ts +21 -27
- package/dist/streaming.d.ts +23 -0
- package/dist/streaming.js +2877 -0
- package/dist/streaming.js.map +1 -0
- package/dist/{unified-splat-renderer.d.ts → unified-splat-mesh.d.ts} +10 -10
- package/dist/unified-work-buffer.d.ts +66 -0
- package/dist/unified.d.ts +11 -0
- package/dist/unified.js +738 -0
- package/dist/unified.js.map +1 -0
- package/dist/webgpu-limits-Y3CI_6_6.js +43 -0
- package/dist/webgpu-limits-Y3CI_6_6.js.map +1 -0
- package/dist/webgpu-limits.d.ts +0 -59
- package/dist/work-buffer-gather.d.ts +1 -1
- package/package.json +24 -4
- package/dist/splat-mesh-types-CTN_duw2.js.map +0 -1
|
@@ -0,0 +1,2877 @@
|
|
|
1
|
+
var Me = Object.defineProperty;
|
|
2
|
+
var Fe = (o, i, e) => i in o ? Me(o, i, { enumerable: !0, configurable: !0, writable: !0, value: e }) : o[i] = e;
|
|
3
|
+
var d = (o, i, e) => Fe(o, typeof i != "symbol" ? i + "" : i, e);
|
|
4
|
+
import { h as Be, f as W, a as Ee, l as ke } from "./splat-budget-DuVZspPQ.js";
|
|
5
|
+
import * as w from "three/webgpu";
|
|
6
|
+
import { S as Pe, a as se, y as xe } from "./splat-mesh-aQdcWA6v.js";
|
|
7
|
+
import { L as We, r as C } from "./lod-scheduler-B0a_uBlv.js";
|
|
8
|
+
import { S as q, t as ee, r as Ae, i as G, a as _ } from "./loading-wLo8vRbA.js";
|
|
9
|
+
import { y as Le } from "./relighting-CSQF3UcL.js";
|
|
10
|
+
import { C as Ve } from "./chunk-loader-DlNwggfG.js";
|
|
11
|
+
import { s as Ie } from "./sh-pack-D5wAe5gg.js";
|
|
12
|
+
import { w as P } from "./logging-BfPdd7NJ.js";
|
|
13
|
+
import { r as Ne, M as De, a as ne, i as j } from "./splat-mesh-types-BZIko-_9.js";
|
|
14
|
+
const Ge = 32e3;
|
|
15
|
+
function _e(o) {
|
|
16
|
+
return o.map((i) => ({
|
|
17
|
+
adds: [i],
|
|
18
|
+
removes: [],
|
|
19
|
+
leafStart: i.leafStart,
|
|
20
|
+
leafEnd: i.leafEnd,
|
|
21
|
+
addCount: i.count
|
|
22
|
+
})).sort((i, e) => i.leafStart - e.leafStart);
|
|
23
|
+
}
|
|
24
|
+
function ze(o, i) {
|
|
25
|
+
const e = [...o, ...i.map(([, n]) => n.run)];
|
|
26
|
+
if (e.length > 0 && e.every((n) => n.coverageGroup !== void 0)) {
|
|
27
|
+
const n = /* @__PURE__ */ new Map(), r = (l) => {
|
|
28
|
+
let c = n.get(l);
|
|
29
|
+
return c || (c = { adds: [], removes: [] }, n.set(l, c)), c;
|
|
30
|
+
};
|
|
31
|
+
for (const l of o)
|
|
32
|
+
r(l.coverageGroup).adds.push(l);
|
|
33
|
+
for (const l of i)
|
|
34
|
+
r(l[1].run.coverageGroup).removes.push(l);
|
|
35
|
+
const a = [];
|
|
36
|
+
for (const l of n.values()) {
|
|
37
|
+
const c = /* @__PURE__ */ new Map(), h = (u, g) => `${u}:${g}`, f = (u, g) => {
|
|
38
|
+
const v = h(u, g);
|
|
39
|
+
let m = c.get(v);
|
|
40
|
+
return m || (m = { adds: [], removes: [], leafStart: u, leafEnd: g, addCount: 0 }, c.set(v, m)), m;
|
|
41
|
+
};
|
|
42
|
+
for (const u of l.adds) {
|
|
43
|
+
const g = f(u.leafStart, u.leafEnd);
|
|
44
|
+
g.adds.push(u), g.addCount += u.count;
|
|
45
|
+
}
|
|
46
|
+
for (const u of l.removes) {
|
|
47
|
+
const g = u[1].run, v = f(g.leafStart, g.leafEnd);
|
|
48
|
+
v.removes.push(u), v.leafStart = Math.min(v.leafStart, g.leafStart), v.leafEnd = Math.max(v.leafEnd, g.leafEnd);
|
|
49
|
+
}
|
|
50
|
+
a.push(...c.values());
|
|
51
|
+
}
|
|
52
|
+
return a.sort((l, c) => l.leafStart - c.leafStart);
|
|
53
|
+
}
|
|
54
|
+
const t = [
|
|
55
|
+
...o.map((n) => ({
|
|
56
|
+
start: n.leafStart,
|
|
57
|
+
end: n.leafEnd,
|
|
58
|
+
add: n,
|
|
59
|
+
remove: void 0
|
|
60
|
+
})),
|
|
61
|
+
...i.map((n) => ({
|
|
62
|
+
start: n[1].run.leafStart,
|
|
63
|
+
end: n[1].run.leafEnd,
|
|
64
|
+
add: void 0,
|
|
65
|
+
remove: n
|
|
66
|
+
}))
|
|
67
|
+
].sort((n, r) => n.start - r.start || r.end - n.end), s = [];
|
|
68
|
+
for (const n of t) {
|
|
69
|
+
const r = s[s.length - 1];
|
|
70
|
+
(!r || n.start >= r.leafEnd) && s.push({
|
|
71
|
+
adds: [],
|
|
72
|
+
removes: [],
|
|
73
|
+
leafStart: n.start,
|
|
74
|
+
leafEnd: n.end,
|
|
75
|
+
addCount: 0
|
|
76
|
+
});
|
|
77
|
+
const a = s[s.length - 1];
|
|
78
|
+
a.leafEnd = Math.max(a.leafEnd, n.end), n.add && (a.adds.push(n.add), a.addCount += n.add.count), n.remove && a.removes.push(n.remove);
|
|
79
|
+
}
|
|
80
|
+
return s;
|
|
81
|
+
}
|
|
82
|
+
function re(o) {
|
|
83
|
+
if (o.adds.length === 0) return 1e3;
|
|
84
|
+
const i = -Math.max(...o.adds.map((e) => e.level));
|
|
85
|
+
return o.removes.length === 0 ? -1e3 + i : i;
|
|
86
|
+
}
|
|
87
|
+
function Oe(o) {
|
|
88
|
+
return o.length > 0 && o.every(
|
|
89
|
+
(i) => [...i.adds, ...i.removes.map(([, e]) => e.run)].every(
|
|
90
|
+
(e) => e.coverageGroup !== void 0
|
|
91
|
+
)
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
function Ue(o, i) {
|
|
95
|
+
const e = (n) => {
|
|
96
|
+
const r = [...n.adds, ...n.removes.map(([, a]) => a.run)];
|
|
97
|
+
return {
|
|
98
|
+
removeOnly: n.adds.length === 0 ? 1 : 0,
|
|
99
|
+
view: r.some((a) => a.inView !== !1) ? 0 : 1,
|
|
100
|
+
finest: n.adds.some((a) => a.level === 0) ? 0 : 1,
|
|
101
|
+
screen: Math.min(...r.map((a) => a.screenImportance ?? Number.POSITIVE_INFINITY)),
|
|
102
|
+
distance: Math.min(...r.map((a) => a.distance ?? Number.POSITIVE_INFINITY))
|
|
103
|
+
};
|
|
104
|
+
}, t = e(o), s = e(i);
|
|
105
|
+
return t.removeOnly - s.removeOnly || t.view - s.view || t.finest - s.finest || t.screen - s.screen || t.distance - s.distance || o.leafStart - i.leafStart;
|
|
106
|
+
}
|
|
107
|
+
function ae(o) {
|
|
108
|
+
switch (o) {
|
|
109
|
+
case "environment":
|
|
110
|
+
return -1;
|
|
111
|
+
case "finest-target":
|
|
112
|
+
return 0;
|
|
113
|
+
case "coverage":
|
|
114
|
+
return 1;
|
|
115
|
+
case "target":
|
|
116
|
+
return 2;
|
|
117
|
+
default:
|
|
118
|
+
return 3;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function He(o) {
|
|
122
|
+
return o === "background" ? "base" : "priority";
|
|
123
|
+
}
|
|
124
|
+
function D(o, i) {
|
|
125
|
+
return o.coverageGroup < 0 ? `file:${i}` : o.phase === "finest-target" ? `cell:${o.coverageGroup}` : `slice:${o.coverageGroup}:${o.leafStart}:${o.leafEnd}`;
|
|
126
|
+
}
|
|
127
|
+
function $e(o, i = 10, e = !1) {
|
|
128
|
+
const t = te(i), s = /* @__PURE__ */ new Map();
|
|
129
|
+
for (const [n, r] of o) {
|
|
130
|
+
if (r.phase === "environment") continue;
|
|
131
|
+
const a = D(r, n), l = s.get(a);
|
|
132
|
+
if (!l) {
|
|
133
|
+
s.set(a, {
|
|
134
|
+
distance: r.distance,
|
|
135
|
+
count: 1,
|
|
136
|
+
inView: r.inView,
|
|
137
|
+
screenImportance: r.screenImportance,
|
|
138
|
+
finest: r.phase === "finest-target"
|
|
139
|
+
});
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
r.distance < l.distance && (l.distance = r.distance), r.inView && (l.inView = !0), r.screenImportance < l.screenImportance && (l.screenImportance = r.screenImportance), r.phase === "finest-target" && (l.finest = !0), l.count++;
|
|
143
|
+
}
|
|
144
|
+
for (const [n, r] of o) {
|
|
145
|
+
if (r.phase === "environment") {
|
|
146
|
+
r.kind = "priority", r.groupDistance = 0, r.groupPending = 1, r.groupInView = !0, r.groupScreenImportance = Number.NEGATIVE_INFINITY, r.groupFinest = !0, r.groupId = "environment", r.groupClass = 0;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const a = D(r, n), l = s.get(a);
|
|
150
|
+
if (!l) continue;
|
|
151
|
+
const c = l.inView ? 0 : l.distance <= t ? 1 : 2, h = c === 0 || c === 1 && e ? "priority" : "base";
|
|
152
|
+
r.groupDistance = l.distance, r.groupPending = l.count, r.groupInView = l.inView, r.groupScreenImportance = l.screenImportance, r.groupFinest = l.finest, r.groupId = a, r.groupClass = c, r.kind = h;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function te(o, i = 2) {
|
|
156
|
+
return o;
|
|
157
|
+
}
|
|
158
|
+
function Ye(o) {
|
|
159
|
+
return o.adds.some((i) => i.level === 0);
|
|
160
|
+
}
|
|
161
|
+
function qe(o, i, e = 2) {
|
|
162
|
+
if (o.level === 0) return "finest-target";
|
|
163
|
+
const t = o.distance ?? Number.POSITIVE_INFINITY;
|
|
164
|
+
return o.inView === !1 && t > te(i) ? "background" : "target";
|
|
165
|
+
}
|
|
166
|
+
function je(o, i, e = 2) {
|
|
167
|
+
const t = o.distance ?? Number.POSITIVE_INFINITY;
|
|
168
|
+
return o.inView === !1 && t > te(i) ? "background" : "coverage";
|
|
169
|
+
}
|
|
170
|
+
function oe(o, i, e, t) {
|
|
171
|
+
const s = t.distance ?? Number.POSITIVE_INFINITY, n = t.level, r = t.inView !== !1, a = t.coverageGroup ?? -1, l = t.screenImportance ?? Number.POSITIVE_INFINITY, c = He(e), h = o.get(i);
|
|
172
|
+
if (!h) {
|
|
173
|
+
const v = D(
|
|
174
|
+
{ phase: e, coverageGroup: a, leafStart: t.leafStart, leafEnd: t.leafEnd },
|
|
175
|
+
i
|
|
176
|
+
);
|
|
177
|
+
o.set(i, {
|
|
178
|
+
kind: c,
|
|
179
|
+
phase: e,
|
|
180
|
+
distance: s,
|
|
181
|
+
level: n,
|
|
182
|
+
inView: r,
|
|
183
|
+
coverageGroup: a,
|
|
184
|
+
leafStart: t.leafStart,
|
|
185
|
+
leafEnd: t.leafEnd,
|
|
186
|
+
screenImportance: l,
|
|
187
|
+
groupDistance: s,
|
|
188
|
+
groupPending: 1,
|
|
189
|
+
groupInView: r,
|
|
190
|
+
groupScreenImportance: l,
|
|
191
|
+
groupFinest: e === "finest-target",
|
|
192
|
+
groupId: v,
|
|
193
|
+
// Provisional; finalized in stampClassicFetchGroups().
|
|
194
|
+
groupClass: 2
|
|
195
|
+
});
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const f = ae(e) < ae(h.phase), u = e === h.phase && (s < h.distance || s === h.distance && n < h.level), g = e === h.phase && s === h.distance && n === h.level && r && !h.inView;
|
|
199
|
+
(f || u || g) && o.set(i, {
|
|
200
|
+
kind: c,
|
|
201
|
+
phase: e,
|
|
202
|
+
distance: s,
|
|
203
|
+
level: n,
|
|
204
|
+
inView: r || h.inView,
|
|
205
|
+
coverageGroup: a >= 0 ? a : h.coverageGroup,
|
|
206
|
+
leafStart: t.leafStart,
|
|
207
|
+
leafEnd: t.leafEnd,
|
|
208
|
+
screenImportance: l,
|
|
209
|
+
groupDistance: Math.min(s, h.groupDistance),
|
|
210
|
+
groupPending: h.groupPending,
|
|
211
|
+
groupInView: r || h.groupInView,
|
|
212
|
+
groupScreenImportance: Math.min(l, h.groupScreenImportance),
|
|
213
|
+
groupFinest: e === "finest-target" || h.groupFinest,
|
|
214
|
+
groupId: h.groupId,
|
|
215
|
+
groupClass: h.groupClass
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
function X(o, i, e, t) {
|
|
219
|
+
const s = o.groupDistance ?? o.distance, n = i.groupDistance ?? i.distance, r = o.groupPending ?? 1, a = i.groupPending ?? 1, l = o.groupId ?? D(o, e), c = i.groupId ?? D(i, t), h = o.groupClass ?? (o.inView ? 0 : 2), f = i.groupClass ?? (i.inView ? 0 : 2), u = o.groupScreenImportance ?? o.screenImportance, g = i.groupScreenImportance ?? i.screenImportance, v = o.groupFinest ? 0 : 1, m = i.groupFinest ? 0 : 1, b = (y) => y === "coverage" ? 0 : y === "background" ? 2 : 1, T = o.phase === "environment" ? 0 : 1, I = i.phase === "environment" ? 0 : 1;
|
|
220
|
+
return T - I || h - f || v - m || u - g || s - n || a - r || (l < c ? -1 : l > c ? 1 : 0) || b(o.phase) - b(i.phase) || o.level - i.level || e - t;
|
|
221
|
+
}
|
|
222
|
+
function K(o, i, e) {
|
|
223
|
+
if (i < 0 || e < 0 || i + e > o.count)
|
|
224
|
+
throw new Error(
|
|
225
|
+
`Splat range [${i}, ${i + e}) exceeds its chunk's ${o.count} splats; the manifest and chunk data disagree.`
|
|
226
|
+
);
|
|
227
|
+
const t = o.shPacked;
|
|
228
|
+
return {
|
|
229
|
+
count: e,
|
|
230
|
+
positions: o.positions.subarray(i * 3, (i + e) * 3),
|
|
231
|
+
colors: o.colors.subarray(i * 4, (i + e) * 4),
|
|
232
|
+
covariances: o.covariances.subarray(i * 6, (i + e) * 6),
|
|
233
|
+
// Per-splat SH is splat-major, so it slices like everything else. Palette
|
|
234
|
+
// shN (`chunk.sh`) is deliberately not carried: its labels index a
|
|
235
|
+
// per-file codebook the shared pool has no way to hold.
|
|
236
|
+
...t ? {
|
|
237
|
+
shPacked: {
|
|
238
|
+
...t,
|
|
239
|
+
packed: t.packed.subarray(
|
|
240
|
+
i * le(t.bands),
|
|
241
|
+
(i + e) * le(t.bands)
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
} : {},
|
|
245
|
+
// Per-splat frontier `parent_size` (foveated `.rad`) slices splat-major like
|
|
246
|
+
// the rest; uploaded into `covarianceB.w` by the pool.
|
|
247
|
+
...o.frontierParent ? { frontierParent: o.frontierParent.subarray(i, i + e) } : {}
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function le(o) {
|
|
251
|
+
return o === 1 ? 3 : o === 2 ? 8 : 15;
|
|
252
|
+
}
|
|
253
|
+
function Xe(o) {
|
|
254
|
+
var i;
|
|
255
|
+
return o.positions.byteLength + o.colors.byteLength + o.covariances.byteLength + // SH is the largest part of an LCC Quality chunk (64 B/splat against the
|
|
256
|
+
// base 32); omitting it would let the CPU cache run ~3x over its cap.
|
|
257
|
+
(((i = o.shPacked) == null ? void 0 : i.packed.byteLength) ?? 0);
|
|
258
|
+
}
|
|
259
|
+
function Ke(o) {
|
|
260
|
+
const i = o.reason;
|
|
261
|
+
return i instanceof Error ? i : new DOMException("Aborted", "AbortError");
|
|
262
|
+
}
|
|
263
|
+
function Qe(o) {
|
|
264
|
+
const i = o ?? Ge;
|
|
265
|
+
if (!Number.isInteger(i) || i <= 0)
|
|
266
|
+
throw new RangeError("StreamedSplatMesh maxSplatsPerSwap must be a positive integer.");
|
|
267
|
+
return i;
|
|
268
|
+
}
|
|
269
|
+
function ce(o) {
|
|
270
|
+
const i = o ?? 1;
|
|
271
|
+
if (!Number.isFinite(i) || i <= 0)
|
|
272
|
+
throw new RangeError("StreamedSplatMesh lodScale must be a positive finite number.");
|
|
273
|
+
return i;
|
|
274
|
+
}
|
|
275
|
+
function Je() {
|
|
276
|
+
return Be();
|
|
277
|
+
}
|
|
278
|
+
function Ze(o, i) {
|
|
279
|
+
const e = o;
|
|
280
|
+
if (typeof e != "object" || e === null)
|
|
281
|
+
throw new Error("Streamed SOG manifest is not a JSON object.");
|
|
282
|
+
if (e.version !== 1)
|
|
283
|
+
throw new Error(`Unsupported Streamed SOG manifest version: ${e.version} (expected 1).`);
|
|
284
|
+
if (!Array.isArray(e.filenames) || e.filenames.some((l) => typeof l != "string"))
|
|
285
|
+
throw new Error('Streamed SOG manifest "filenames" must be an array of strings.');
|
|
286
|
+
if (!Array.isArray(e.counts) || e.counts.some((l) => !Number.isSafeInteger(l) || l < 0))
|
|
287
|
+
throw new Error('Streamed SOG manifest "counts" must be an array of non-negative integers.');
|
|
288
|
+
if (!Number.isSafeInteger(e.lodLevels) || e.lodLevels < 1 || e.lodLevels > 64)
|
|
289
|
+
throw new Error(`Streamed SOG manifest declares an invalid lodLevels: ${e.lodLevels}.`);
|
|
290
|
+
const t = e.filenames.map((l) => l.replace(/\/?meta\.json$/, "/")), s = t.map(
|
|
291
|
+
(l) => i.resolve(l) ?? `${i.manifestUrl}#${l}`
|
|
292
|
+
), n = [], r = e.lodLevels, a = [e.tree];
|
|
293
|
+
for (; a.length > 0; ) {
|
|
294
|
+
const l = a.pop();
|
|
295
|
+
if (typeof l != "object" || l === null)
|
|
296
|
+
throw new Error("Streamed SOG manifest tree contains a malformed node.");
|
|
297
|
+
if (l.children) {
|
|
298
|
+
if (!Array.isArray(l.children) || l.children.length !== 2)
|
|
299
|
+
throw new Error("Streamed SOG manifest tree node must have exactly two children.");
|
|
300
|
+
a.push(l.children[1], l.children[0]);
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
const c = new Array(r).fill(
|
|
304
|
+
void 0
|
|
305
|
+
);
|
|
306
|
+
for (const [h, f] of Object.entries(l.lods ?? {})) {
|
|
307
|
+
const u = Number(h);
|
|
308
|
+
u >= 0 && u < r && (et(f, e.filenames.length), c[u] = f);
|
|
309
|
+
}
|
|
310
|
+
n.push({ bounds: he(l.bound), lods: c });
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
leaves: n,
|
|
314
|
+
chunkUrls: s,
|
|
315
|
+
chunkDirectories: t,
|
|
316
|
+
counts: e.counts,
|
|
317
|
+
lodLevels: r,
|
|
318
|
+
bounds: he(e.tree.bound)
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
function et(o, i) {
|
|
322
|
+
if (typeof o != "object" || o === null || !Number.isSafeInteger(o.file) || o.file < 0 || o.file >= i || !Number.isSafeInteger(o.offset) || o.offset < 0 || !Number.isSafeInteger(o.count) || o.count < 0)
|
|
323
|
+
throw new Error(
|
|
324
|
+
`Streamed SOG manifest leaf declares an invalid LOD range (file ${o == null ? void 0 : o.file}, offset ${o == null ? void 0 : o.offset}, count ${o == null ? void 0 : o.count}).`
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
function he(o) {
|
|
328
|
+
return new w.Box3(
|
|
329
|
+
new w.Vector3(o.min[0], o.min[1], o.min[2]),
|
|
330
|
+
new w.Vector3(o.max[0], o.max[1], o.max[2])
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
function tt(o, i, e, t = 0) {
|
|
334
|
+
const s = Ze(o, i), n = t >= 1 ? t : void 0, r = s.chunkUrls.map((a, l) => {
|
|
335
|
+
var f;
|
|
336
|
+
const c = (f = s.chunkDirectories) == null ? void 0 : f[l], h = c ? i.directoryFiles(c) : null;
|
|
337
|
+
if (!(!h && !n))
|
|
338
|
+
return {
|
|
339
|
+
format: "sog",
|
|
340
|
+
...h ? { files: h } : {},
|
|
341
|
+
...n ? { sog: { packShBands: n } } : {}
|
|
342
|
+
};
|
|
343
|
+
});
|
|
344
|
+
return {
|
|
345
|
+
source: new We(s, e),
|
|
346
|
+
chunkUrls: s.chunkUrls,
|
|
347
|
+
...r.some(Boolean) ? { chunkOptions: r } : {},
|
|
348
|
+
...n ? { shBands: n } : {},
|
|
349
|
+
chunkKind: "directory",
|
|
350
|
+
// Streamed SOG chunks are unbundled directories
|
|
351
|
+
bounds: s.bounds,
|
|
352
|
+
pinnedFiles: st(s),
|
|
353
|
+
maxResidentSplats: s.counts[0] ?? e.budget,
|
|
354
|
+
minimumCoverageSplats: it(s)
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
function it(o) {
|
|
358
|
+
let i = 0;
|
|
359
|
+
for (const e of o.leaves)
|
|
360
|
+
for (let t = e.lods.length - 1; t >= 0; t--) {
|
|
361
|
+
const s = e.lods[t];
|
|
362
|
+
if (s) {
|
|
363
|
+
i += s.count;
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return Math.max(1, i);
|
|
368
|
+
}
|
|
369
|
+
function st(o) {
|
|
370
|
+
const i = /* @__PURE__ */ new Set();
|
|
371
|
+
for (const e of o.leaves)
|
|
372
|
+
for (let t = e.lods.length - 1; t >= 0; t--) {
|
|
373
|
+
const s = e.lods[t];
|
|
374
|
+
if (s) {
|
|
375
|
+
i.add(s.file);
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return i;
|
|
380
|
+
}
|
|
381
|
+
function nt(o, i) {
|
|
382
|
+
return {
|
|
383
|
+
manifestUrl: o,
|
|
384
|
+
resolve: (e) => new URL(e, o).href,
|
|
385
|
+
size: (e) => rt(new URL(e, o).href, i),
|
|
386
|
+
directoryFiles: () => null,
|
|
387
|
+
dispose: () => {
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
async function rt(o, i) {
|
|
392
|
+
var e;
|
|
393
|
+
try {
|
|
394
|
+
const t = await fetch(o, { ...ee(i), method: "HEAD" });
|
|
395
|
+
if (await ue(t), t.ok) {
|
|
396
|
+
const s = t.headers.get("content-length");
|
|
397
|
+
if (s !== null) return de(Number(s));
|
|
398
|
+
} else if (t.status === 404)
|
|
399
|
+
return null;
|
|
400
|
+
} catch {
|
|
401
|
+
}
|
|
402
|
+
try {
|
|
403
|
+
const t = await fetch(o, {
|
|
404
|
+
...ee(i),
|
|
405
|
+
headers: { ...(i == null ? void 0 : i.headers) ?? {}, Range: "bytes=0-0" }
|
|
406
|
+
});
|
|
407
|
+
if (await ue(t), t.status !== 206) return null;
|
|
408
|
+
const s = (e = t.headers.get("content-range")) == null ? void 0 : e.split("/")[1];
|
|
409
|
+
return s === void 0 || s === "*" ? null : de(Number(s));
|
|
410
|
+
} catch {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
async function ue(o) {
|
|
415
|
+
var i;
|
|
416
|
+
try {
|
|
417
|
+
await ((i = o.body) == null ? void 0 : i.cancel());
|
|
418
|
+
} catch {
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
function de(o) {
|
|
422
|
+
return Number.isSafeInteger(o) && o >= 0 ? o : null;
|
|
423
|
+
}
|
|
424
|
+
const at = [
|
|
425
|
+
{ test: (o) => o.endsWith(".lcc2"), format: "lcc2" },
|
|
426
|
+
{ test: (o) => o.endsWith(".lcc"), format: "lcc" },
|
|
427
|
+
// The `.rad` header of a `--rad-chunked` set; `.radc` chunk files are not
|
|
428
|
+
// manifests (they end in `.radc`, so `endsWith('.rad')` excludes them).
|
|
429
|
+
{ test: (o) => o.endsWith(".rad"), format: "rad" },
|
|
430
|
+
{ test: (o) => o === "lod-meta.json", format: "streamed-sog" }
|
|
431
|
+
];
|
|
432
|
+
function ot(o) {
|
|
433
|
+
const i = [];
|
|
434
|
+
for (const a of o.keys()) {
|
|
435
|
+
const l = $(a).toLowerCase(), c = at.find((h) => h.test(l));
|
|
436
|
+
c && Re(a) === lt(o, c) && i.push({ path: a, format: c.format });
|
|
437
|
+
}
|
|
438
|
+
if (i.length === 0)
|
|
439
|
+
throw new q(
|
|
440
|
+
"That folder has no streamed-scene manifest in it (expected a .lcc, .lcc2 or lod-meta.json file).",
|
|
441
|
+
{ phase: "manifest", url: "local-folder", retryable: !1 }
|
|
442
|
+
);
|
|
443
|
+
if (i.length > 1) {
|
|
444
|
+
const a = i.map((l) => $(l.path)).join(", ");
|
|
445
|
+
throw new q(
|
|
446
|
+
`That folder holds more than one scene manifest (${a}) - drop one scene at a time.`,
|
|
447
|
+
{ phase: "manifest", url: "local-folder", retryable: !1 }
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
const e = i[0], t = e.path.includes("/") ? e.path.slice(0, e.path.lastIndexOf("/") + 1) : "", s = /* @__PURE__ */ new Map(), n = (a) => {
|
|
451
|
+
const l = s.get(a);
|
|
452
|
+
if (l !== void 0) return l;
|
|
453
|
+
const c = o.get(a);
|
|
454
|
+
if (!c) return null;
|
|
455
|
+
const h = URL.createObjectURL(c);
|
|
456
|
+
return s.set(a, h), h;
|
|
457
|
+
};
|
|
458
|
+
return { source: {
|
|
459
|
+
manifestUrl: n(e.path),
|
|
460
|
+
resolve: (a) => n(t + Q(a)),
|
|
461
|
+
size: (a) => {
|
|
462
|
+
var l;
|
|
463
|
+
return Promise.resolve(((l = o.get(t + Q(a))) == null ? void 0 : l.size) ?? null);
|
|
464
|
+
},
|
|
465
|
+
directoryFiles: (a) => {
|
|
466
|
+
const l = t + Q(a).replace(/\/?$/, "/"), c = {};
|
|
467
|
+
for (const h of o.keys()) {
|
|
468
|
+
if (!h.startsWith(l)) continue;
|
|
469
|
+
const f = n(h);
|
|
470
|
+
f && (c[h.slice(l.length)] = f);
|
|
471
|
+
}
|
|
472
|
+
return Object.keys(c).length > 0 ? c : null;
|
|
473
|
+
},
|
|
474
|
+
dispose: () => {
|
|
475
|
+
for (const a of s.values()) URL.revokeObjectURL(a);
|
|
476
|
+
s.clear();
|
|
477
|
+
}
|
|
478
|
+
}, format: e.format, name: $(e.path) };
|
|
479
|
+
}
|
|
480
|
+
function Q(o) {
|
|
481
|
+
return o.replace(/^\.?\//, "");
|
|
482
|
+
}
|
|
483
|
+
function $(o) {
|
|
484
|
+
return o.slice(o.lastIndexOf("/") + 1);
|
|
485
|
+
}
|
|
486
|
+
function Re(o) {
|
|
487
|
+
return o.split("/").length - 1;
|
|
488
|
+
}
|
|
489
|
+
function lt(o, i) {
|
|
490
|
+
let e = 1 / 0;
|
|
491
|
+
for (const t of o.keys())
|
|
492
|
+
i.test($(t).toLowerCase()) && (e = Math.min(e, Re(t)));
|
|
493
|
+
return e;
|
|
494
|
+
}
|
|
495
|
+
const fe = {
|
|
496
|
+
coneFov0: 90,
|
|
497
|
+
coneFov: 120,
|
|
498
|
+
coneFoveate: 0.4,
|
|
499
|
+
behindFoveate: 0.2
|
|
500
|
+
}, F = 2048, ct = 6e4, ht = 0.25, ut = 8, dt = 600, ft = 250, gt = 4, pt = 500, mt = 3, vt = 4e6, ge = 65536, St = 2 * 1024 * 1024 * 1024;
|
|
501
|
+
function bt(o) {
|
|
502
|
+
const i = 52 + (o.shBands ? 16 * Math.ceil(Ie(o.shBands) / 4) : 0), t = (o.chunkSize === void 0 ? void 0 : o.chunkSize * o.chunkUrls.length) ?? o.contentSplatCount ?? o.maxResidentSplats;
|
|
503
|
+
return Math.max(1, t) * i;
|
|
504
|
+
}
|
|
505
|
+
class Y extends Pe {
|
|
506
|
+
constructor(e, t, s, n, r, a = !1) {
|
|
507
|
+
var u, g, v;
|
|
508
|
+
super({ capacity: s }, n);
|
|
509
|
+
d(this, "scene");
|
|
510
|
+
d(this, "loader", new Ve());
|
|
511
|
+
/**
|
|
512
|
+
* Cap the *classic* (non-page-table) chunk cache evicts against.
|
|
513
|
+
*
|
|
514
|
+
* Mutable because a shared {@link ChunkCacheBudget} re-splits it as the camera
|
|
515
|
+
* moves; without a budget it stays at the value `options.cpuCacheBytes` or the
|
|
516
|
+
* device default set at construction.
|
|
517
|
+
*/
|
|
518
|
+
d(this, "cpuCacheBytes");
|
|
519
|
+
d(this, "budgetValue");
|
|
520
|
+
d(this, "maximumBudget");
|
|
521
|
+
/** Spark's per-mesh `lodScale`; divides the page-table cut limit. */
|
|
522
|
+
d(this, "lodScaleValue");
|
|
523
|
+
/** Set once the governed budget has been reported as exceeding an explicit
|
|
524
|
+
* `foveationDrawBudget`, so the warning is issued at most once. */
|
|
525
|
+
d(this, "warnedDrawTargetCap", !1);
|
|
526
|
+
d(this, "stagedSwapsEnabled");
|
|
527
|
+
/** Classic LCC must keep old cell coverage while a replacement is pending. */
|
|
528
|
+
d(this, "neverRetireCoverageEarly");
|
|
529
|
+
d(this, "appendCap");
|
|
530
|
+
d(this, "onPerformanceEvent");
|
|
531
|
+
d(this, "compactionCount", 0);
|
|
532
|
+
d(this, "cache", /* @__PURE__ */ new Map());
|
|
533
|
+
/** Running byte total of {@link cache}; maintained by {@link cacheChunk}, eviction and dispose. */
|
|
534
|
+
d(this, "cacheBytesTotal", 0);
|
|
535
|
+
/** In-flight chunk fetches. The kind is kept so a weight change can shed the
|
|
536
|
+
* speculative ones without touching the detail that is actually on screen. */
|
|
537
|
+
d(this, "fetching", /* @__PURE__ */ new Map());
|
|
538
|
+
/** This mesh's camera-projected share of the scene's fetch bandwidth. */
|
|
539
|
+
d(this, "fetchWeight");
|
|
540
|
+
/** Scene-wide fetch arbitration, when the host shares one; see `requestChunk`. */
|
|
541
|
+
d(this, "fetchScheduler");
|
|
542
|
+
d(this, "fetchHandle");
|
|
543
|
+
/**
|
|
544
|
+
* Blob-URL dataset from {@link loadLocal}, owned by this mesh so its object
|
|
545
|
+
* URLs are revoked on {@link dispose} rather than leaking for the document's
|
|
546
|
+
* lifetime. Undefined for every network-loaded mesh.
|
|
547
|
+
*/
|
|
548
|
+
d(this, "localSource");
|
|
549
|
+
/** Scene-wide chunk-cache ceiling, when the host shares one. */
|
|
550
|
+
d(this, "cacheBudget");
|
|
551
|
+
d(this, "cacheBudgetHandle");
|
|
552
|
+
/**
|
|
553
|
+
* The cap this mesh's frontier worker is currently evicting against.
|
|
554
|
+
*
|
|
555
|
+
* Mirrored on the main thread so `applyCacheAllowance` can skip no-op posts
|
|
556
|
+
* and so `fetchCounts.cacheLimitBytes` stays truthful between plans.
|
|
557
|
+
*/
|
|
558
|
+
d(this, "cacheLimitBytes", 0);
|
|
559
|
+
d(this, "resident", /* @__PURE__ */ new Map());
|
|
560
|
+
/** Replacement runs hidden while their pool data is uploaded in bounded segments. */
|
|
561
|
+
d(this, "staged", /* @__PURE__ */ new Map());
|
|
562
|
+
/** Files awaiting a backoff retry after a transient fetch/decode error. */
|
|
563
|
+
d(this, "retrying", /* @__PURE__ */ new Map());
|
|
564
|
+
/** Files given up on after {@link MAX_CHUNK_ATTEMPTS} failures. */
|
|
565
|
+
d(this, "failedFiles", /* @__PURE__ */ new Set());
|
|
566
|
+
/**
|
|
567
|
+
* When true, each resident run writes its LOD `level` into the `lodLevel`
|
|
568
|
+
* float channel for false-color debug modifiers.
|
|
569
|
+
*/
|
|
570
|
+
d(this, "lodLevelDebug", !1);
|
|
571
|
+
d(this, "lodLevelChannelReady", !1);
|
|
572
|
+
d(this, "lodLevelScratch");
|
|
573
|
+
/** Desired-but-not-resident files this tick; protected from cache eviction. */
|
|
574
|
+
d(this, "neededFiles", /* @__PURE__ */ new Set());
|
|
575
|
+
/** Non-null in `foveationMode: 'page-table'`: the worker that owns the chunk
|
|
576
|
+
* cache + traversal + pager off the main thread, and the always-active slab it
|
|
577
|
+
* pages the returned frontier into. */
|
|
578
|
+
d(this, "frontierWorker");
|
|
579
|
+
/**
|
|
580
|
+
* The frontier's slots, as a list of equally sized pages rather than one
|
|
581
|
+
* contiguous run.
|
|
582
|
+
*
|
|
583
|
+
* Slot `i` lives in page `i / slabPageSplats` at offset `i %
|
|
584
|
+
* slabPageSplats`. The pager only ever addresses slots, so where those
|
|
585
|
+
* pages sit in the pool is the mesh's business - which is what lets a mesh
|
|
586
|
+
* hold non-contiguous storage, and ultimately lets several meshes interleave
|
|
587
|
+
* in one pool instead of each reserving its whole ceiling as one block.
|
|
588
|
+
* (Spark's pager does the same thing one level down, binding fixed pages to
|
|
589
|
+
* `(source, chunk)` pairs.)
|
|
590
|
+
*/
|
|
591
|
+
d(this, "slabPages", []);
|
|
592
|
+
/** Most slots the slab may ever hold - the construction capacity. */
|
|
593
|
+
d(this, "slabCeiling", 0);
|
|
594
|
+
/** Slot count the worker's pager was last told about. */
|
|
595
|
+
d(this, "pagerSlots", 0);
|
|
596
|
+
/** Consecutive ticks the wave gate has held retirements back. */
|
|
597
|
+
d(this, "retireHeldTicks", 0);
|
|
598
|
+
/** Backing store for {@link planTimings}. */
|
|
599
|
+
d(this, "planTimingsValue", {
|
|
600
|
+
applyMs: 0,
|
|
601
|
+
worstApplyMs: 0,
|
|
602
|
+
writeMs: 0,
|
|
603
|
+
residentMs: 0,
|
|
604
|
+
moves: 0,
|
|
605
|
+
appends: 0,
|
|
606
|
+
worstSplats: 0
|
|
607
|
+
});
|
|
608
|
+
/**
|
|
609
|
+
* Backing store for {@link fetchCounts}. Lifetime totals, because the question
|
|
610
|
+
* they answer is about a *steady state* - "this keeps streaming after the view
|
|
611
|
+
* settled" - which a per-frame or windowed number cannot express.
|
|
612
|
+
*/
|
|
613
|
+
d(this, "fetchCountsValue", {
|
|
614
|
+
priority: 0,
|
|
615
|
+
base: 0,
|
|
616
|
+
sweep: 0,
|
|
617
|
+
evicted: 0,
|
|
618
|
+
uncovered: 0,
|
|
619
|
+
retiredEarly: 0,
|
|
620
|
+
cacheFull: !1,
|
|
621
|
+
cacheBytes: 0,
|
|
622
|
+
cacheLimitBytes: 0
|
|
623
|
+
});
|
|
624
|
+
/**
|
|
625
|
+
* The screen-radius band the scene asked for, kept so the band can be scaled
|
|
626
|
+
* with the solved frontier cut and always relative to the original - scaling
|
|
627
|
+
* the live values repeatedly would drift. Null when the scene has no band.
|
|
628
|
+
*/
|
|
629
|
+
d(this, "frontierBandBase", null);
|
|
630
|
+
/**
|
|
631
|
+
* Splats per slab page for this mesh: {@link SLAB_PAGE_SPLATS}, or the whole
|
|
632
|
+
* capacity when that is smaller. Spark can use one fixed page size because
|
|
633
|
+
* its pool is a single large arena; here a mesh may be smaller than a page,
|
|
634
|
+
* and rounding it up to one would waste most of the reservation.
|
|
635
|
+
*/
|
|
636
|
+
d(this, "slabPageSplats", ge);
|
|
637
|
+
/** Target drawn-splat count for the page-table frontier; see the constructor. */
|
|
638
|
+
d(this, "pageTableDrawBudget", 0);
|
|
639
|
+
/** The unclamped draw target (`foveationDrawBudget` or the default), kept so
|
|
640
|
+
* `setBudget` can re-derive the effective draw budget when the pool budget
|
|
641
|
+
* moves (e.g. under a `BudgetGovernor`). */
|
|
642
|
+
d(this, "pageTableDrawTarget", 0);
|
|
643
|
+
/** Whether {@link pageTableDrawTarget} came from an explicit
|
|
644
|
+
* `foveationDrawBudget` - a caller-chosen hard cap worth warning about when a
|
|
645
|
+
* governed budget outgrows it, rather than the library's own default. */
|
|
646
|
+
d(this, "pageTableDrawTargetExplicit", !1);
|
|
647
|
+
/** Last frontier's drawn (non-degenerate) splat count - the true on-screen size
|
|
648
|
+
* in `page-table` mode, where the slab is fully "active" but mostly degenerate. */
|
|
649
|
+
d(this, "pageTableDrawn", 0);
|
|
650
|
+
/** Monotonic reschedule id; a stale plan (superseded by a newer request) is
|
|
651
|
+
* dropped. `pageTableInFlight` coalesces to one outstanding traversal. */
|
|
652
|
+
d(this, "pageTableSeq", 0);
|
|
653
|
+
d(this, "pageTableInFlight", !1);
|
|
654
|
+
d(this, "pageTableDisposed", !1);
|
|
655
|
+
/** Files whose data has been forwarded to the worker (so we don't refetch). */
|
|
656
|
+
d(this, "pageTableCachedFiles", /* @__PURE__ */ new Set());
|
|
657
|
+
/** Chunks the last frontier wanted but did not have, biggest-on-screen first.
|
|
658
|
+
* These outrank the background sweep - they are the detail actually on screen. */
|
|
659
|
+
d(this, "pageTableFetchPriority", []);
|
|
660
|
+
/** Frontier-cut target node size (px) and foveation ramp; see `frontierView`. */
|
|
661
|
+
d(this, "pageTableTargetPx", se);
|
|
662
|
+
d(this, "pageTableFoveation", fe);
|
|
663
|
+
/** Drawing-buffer height, sampled in `update` so `reschedule` can derive the
|
|
664
|
+
* cut limit the same way the material does (`targetPx / focalY`). */
|
|
665
|
+
d(this, "pageTableViewportY", 0);
|
|
666
|
+
/** Frontier cut on foveated `size / distance`. Re-derived each reschedule once
|
|
667
|
+
* the drawing buffer is known; the initial value only covers the first frame. */
|
|
668
|
+
d(this, "pageTableLimit", 0.02);
|
|
669
|
+
/**
|
|
670
|
+
* Whether the worker's cache is sitting at its cap: sweeping past that point
|
|
671
|
+
* only evicts what the frontier is using.
|
|
672
|
+
*
|
|
673
|
+
* Re-derived from every plan rather than latched. It used to latch on the
|
|
674
|
+
* first eviction, which was safe only while the cap was sized to the capture
|
|
675
|
+
* and evictions therefore meant "this will never fit". Under a scene-wide
|
|
676
|
+
* {@link ChunkCacheBudget} evictions are routine - a far mesh gives bytes back
|
|
677
|
+
* and is trimmed - and latching would kill its sweep for the session, so a
|
|
678
|
+
* mesh that went cold could never re-warm when the camera returned.
|
|
679
|
+
*/
|
|
680
|
+
d(this, "pageTableCacheAtLimit", !1);
|
|
681
|
+
/** Per-splat channels whose edits survive chunk eviction/reload (M7.6). */
|
|
682
|
+
d(this, "persistentChannels", /* @__PURE__ */ new Map());
|
|
683
|
+
/** Chunk-file index of the always-resident environment tile, if the scene ships one. */
|
|
684
|
+
d(this, "envFile");
|
|
685
|
+
/** Whether the environment tile should be visible; toggled live. */
|
|
686
|
+
d(this, "envEnabled");
|
|
687
|
+
/** Pool handle of the environment tile once it has loaded (kept for toggling). */
|
|
688
|
+
d(this, "envHandle");
|
|
689
|
+
/** Env splat count, measured when the tile decodes; 0 until then. */
|
|
690
|
+
d(this, "envSplatCount", 0);
|
|
691
|
+
/** Set when the env tile is larger than the whole pool - terminal, warned once. */
|
|
692
|
+
d(this, "envUnfit", !1);
|
|
693
|
+
/**
|
|
694
|
+
* Startup hold. `'capture'` waits for the first schedule after the host
|
|
695
|
+
* applies the final camera; `'holding'` freezes that coverage set.
|
|
696
|
+
*/
|
|
697
|
+
d(this, "initialRevealPhase", "off");
|
|
698
|
+
/** Which hold, if any, was armed at construction. Survives release for recapture. */
|
|
699
|
+
d(this, "initialRevealHold", "off");
|
|
700
|
+
/** Frozen nearby-detail / in-view coverage runs for {@link initialRevealPhase} `'holding'`. */
|
|
701
|
+
d(this, "frozenCriticalRuns", null);
|
|
702
|
+
/** Timestamp of the final-camera capture that began the current hold. */
|
|
703
|
+
d(this, "initialRevealStartedAt");
|
|
704
|
+
d(this, "initialRevealStateValue", { status: "disabled" });
|
|
705
|
+
/** Fetch settings this mesh was loaded with, reused for collision meshes. */
|
|
706
|
+
d(this, "requestOptions");
|
|
707
|
+
/** In-flight or settled collision load; see {@link loadCollisionMeshes}. */
|
|
708
|
+
d(this, "collisionTiles");
|
|
709
|
+
d(this, "collisionAbort");
|
|
710
|
+
d(this, "pendingWork", !0);
|
|
711
|
+
d(this, "lastScheduleTime", -1 / 0);
|
|
712
|
+
/** Reused leaf-coverage bitmap for {@link substituteCoverage}; grows only. */
|
|
713
|
+
d(this, "coverageScratch");
|
|
714
|
+
d(this, "lastCameraPos", new w.Vector3(1 / 0, 1 / 0, 1 / 0));
|
|
715
|
+
d(this, "lastCameraQuat", new w.Quaternion());
|
|
716
|
+
this.scene = e, this.budgetValue = t, this.maximumBudget = n.maxBudget === void 0 ? t : Math.max(t, W(n.maxBudget)), this.lodScaleValue = ce(n.lodScale), this.stagedSwapsEnabled = n.experimentalStagedSwaps !== !1, this.neverRetireCoverageEarly = a, this.appendCap = Qe(n.maxSplatsPerSwap);
|
|
717
|
+
const l = n.initialReveal === "hold-coverage" && this.scene.source.coverageRunsFor !== void 0, c = n.initialReveal === "hold-near-l0" && a;
|
|
718
|
+
l || c ? (this.initialRevealHold = l ? "hold-coverage" : "hold-near-l0", this.initialRevealPhase = "capture", this.initialRevealStateValue = {
|
|
719
|
+
status: "pending",
|
|
720
|
+
stagedSplats: 0,
|
|
721
|
+
totalSplats: 0,
|
|
722
|
+
readyGroups: 0,
|
|
723
|
+
totalGroups: 0
|
|
724
|
+
}) : (this.initialRevealHold = "off", this.initialRevealPhase = "off", this.initialRevealStateValue = { status: "disabled" }), this.onPerformanceEvent = n.onPerformanceEvent, this.cpuCacheBytes = n.cpuCacheBytes ?? Je(), this.requestOptions = n.request, this.envFile = (u = e.environment) == null ? void 0 : u.file, this.envEnabled = n.environmentEnabled !== !1, this.fetchWeight = n.fetchWeight, this.fetchScheduler = n.fetchScheduler, this.cacheBudget = n.cacheBudget, this.fetchHandle = (g = this.fetchScheduler) == null ? void 0 : g.register({
|
|
725
|
+
// No weight supplied: claim an equal share rather than none, so a partly
|
|
726
|
+
// wired host degrades to round-robin instead of silently starving.
|
|
727
|
+
weight: () => {
|
|
728
|
+
var m;
|
|
729
|
+
return ((m = this.fetchWeight) == null ? void 0 : m.call(this)) ?? 1;
|
|
730
|
+
},
|
|
731
|
+
onSlotAvailable: () => {
|
|
732
|
+
this.pendingWork = !0;
|
|
733
|
+
},
|
|
734
|
+
shedFetches: (m) => this.abortFetches(m)
|
|
735
|
+
});
|
|
736
|
+
const h = j(n.foveationMode), f = h ? Math.max(
|
|
737
|
+
this.cpuCacheBytes,
|
|
738
|
+
Math.min(St, bt(e))
|
|
739
|
+
) : this.cpuCacheBytes;
|
|
740
|
+
if (this.cacheBudgetHandle = (v = this.cacheBudget) == null ? void 0 : v.register({
|
|
741
|
+
// The governor weight `fetchWeight` already carries, so cache and network
|
|
742
|
+
// follow the same camera-projected measure. The `1` fallback matches
|
|
743
|
+
// `requestChunk`: a host that never wired weights gives every mesh an
|
|
744
|
+
// equal claim rather than none.
|
|
745
|
+
weight: () => {
|
|
746
|
+
var m;
|
|
747
|
+
return ((m = this.fetchWeight) == null ? void 0 : m.call(this)) ?? 1;
|
|
748
|
+
},
|
|
749
|
+
ceilingBytes: f,
|
|
750
|
+
onAllowanceChanged: (m) => this.applyCacheAllowance(m)
|
|
751
|
+
}), this.cacheLimitBytes = this.cacheBudget && this.cacheBudgetHandle ? this.cacheBudget.allowanceFor(this.cacheBudgetHandle) : f, h || (this.cpuCacheBytes = this.cacheLimitBytes), this.fetchCountsValue.cacheLimitBytes = this.cacheLimitBytes, j(n.foveationMode)) {
|
|
752
|
+
if (!r)
|
|
753
|
+
throw new Error("StreamedSplatMesh: page-table foveation requires the frontier worker.");
|
|
754
|
+
this.pageTableDrawTarget = n.foveationDrawBudget ?? vt, this.pageTableDrawTargetExplicit = n.foveationDrawBudget !== void 0, this.pageTableDrawBudget = Math.min(t, this.pageTableDrawTarget), this.pageTableTargetPx = n.foveationTargetPx ?? se, this.pageTableFoveation = { ...fe, ...n.frontierFoveation }, (n.minSplatScreenRadius !== void 0 || n.maxSplatScreenRadius !== void 0) && (this.frontierBandBase = {
|
|
755
|
+
min: n.minSplatScreenRadius ?? 0,
|
|
756
|
+
max: n.maxSplatScreenRadius ?? 0
|
|
757
|
+
}), this.slabPageSplats = Math.min(ge, s), this.slabCeiling = s, this.syncSlabPages(this.pageTableDrawBudget), this.frontierWorker = new r(), this.frontierWorker.onmessage = (b) => this.applyFrontierPlan(b.data), this.pagerSlots = this.slabSlots, this.postToWorker({
|
|
758
|
+
type: "init",
|
|
759
|
+
capacity: this.pagerSlots,
|
|
760
|
+
chunkSize: e.chunkSize ?? 65536,
|
|
761
|
+
cpuCacheBytes: this.cacheLimitBytes
|
|
762
|
+
});
|
|
763
|
+
const m = e.bootstrapChunk;
|
|
764
|
+
m && this.forwardChunkToWorker(m.file, m.data);
|
|
765
|
+
} else
|
|
766
|
+
this.frontierWorker = null;
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Fetches a scene manifest and prepares a mesh sized to the budget.
|
|
770
|
+
* Accepts a Streamed SOG manifest (`lod-meta.json`) or an XGRIDS `.lcc2` or
|
|
771
|
+
* `.lcc` (manifest v3–v5) dataset - all stream through the same machinery. Both LCC
|
|
772
|
+
* generations are normalized to the established XGRIDS/Spark Three.js
|
|
773
|
+
* coordinate frame; streamed SOG orientation is unchanged.
|
|
774
|
+
*
|
|
775
|
+
* A `.lcc` dataset needs a server that answers HTTP range requests: its
|
|
776
|
+
* splats live in one large `data.bin` that is never fetched whole.
|
|
777
|
+
*
|
|
778
|
+
* @param manifestUrl - URL of the scene's `lod-meta.json`, `.lcc2` or `.lcc`
|
|
779
|
+
* file; relative URLs resolve against `options.baseUrl` (or the page).
|
|
780
|
+
* @throws Rejects with {@link SplatLoadError} on any resolve/fetch/parse
|
|
781
|
+
* failure, or a `DOMException` named `AbortError` when `options.signal` fires.
|
|
782
|
+
*/
|
|
783
|
+
static async load(e, t = {}) {
|
|
784
|
+
const s = Ae(e, t.baseUrl).href, n = s.toLowerCase(), r = t.format !== void 0 && t.format !== "auto" ? t.format : n.endsWith(".lcc2") ? "lcc2" : n.endsWith(".lcc") ? "lcc" : n.endsWith(".rad") ? "rad" : "streamed-sog";
|
|
785
|
+
return Y.fromSource(
|
|
786
|
+
nt(s, t.request),
|
|
787
|
+
r,
|
|
788
|
+
t
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Prepares a mesh from a folder dropped into the page - the same streamed
|
|
793
|
+
* formats, read straight off the user's disk with no server and no upload.
|
|
794
|
+
*
|
|
795
|
+
* Every file becomes a `blob:` URL, which answers range requests exactly as
|
|
796
|
+
* an HTTP origin does, so a multi-hundred-megabyte `.lcc` `data.bin` streams
|
|
797
|
+
* chunk-by-chunk rather than being read whole.
|
|
798
|
+
*
|
|
799
|
+
* @param files - The folder's files, keyed by path relative to its root
|
|
800
|
+
* (as the demo drop-zone `readDirectory` walk produces).
|
|
801
|
+
* @throws Rejects with {@link SplatLoadError} - phase `'manifest'` when the
|
|
802
|
+
* folder holds no (or more than one) recognizable scene manifest - or a
|
|
803
|
+
* `DOMException` named `AbortError` when `options.signal` fires.
|
|
804
|
+
*/
|
|
805
|
+
static async loadLocal(e, t = {}) {
|
|
806
|
+
let s;
|
|
807
|
+
try {
|
|
808
|
+
s = ot(e);
|
|
809
|
+
} catch (n) {
|
|
810
|
+
throw n instanceof q ? n : new q(n instanceof Error ? n.message : String(n), {
|
|
811
|
+
phase: "manifest",
|
|
812
|
+
url: "local-folder",
|
|
813
|
+
retryable: !1,
|
|
814
|
+
cause: n
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
try {
|
|
818
|
+
const n = await Y.fromSource(s.source, s.format, t);
|
|
819
|
+
return n.localSource = s.source, n;
|
|
820
|
+
} catch (n) {
|
|
821
|
+
throw s.source.dispose(), n;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
/** Shared load path: fetch the manifest from a source, then build the scene. */
|
|
825
|
+
static async fromSource(e, t, s) {
|
|
826
|
+
const n = s.deviceProfile, r = W(s.budget, n, {
|
|
827
|
+
format: t,
|
|
828
|
+
...s.budgetCap === void 0 ? {} : { cap: s.budgetCap }
|
|
829
|
+
}), a = s.maxBudget === void 0 ? r : W(s.maxBudget, n);
|
|
830
|
+
if (a < r)
|
|
831
|
+
throw new RangeError(
|
|
832
|
+
`StreamedSplatMesh: maxBudget (${a}) must be >= budget (${r}).`
|
|
833
|
+
);
|
|
834
|
+
const l = {
|
|
835
|
+
budget: a,
|
|
836
|
+
lodBaseDistance: s.lodBaseDistance ?? 10,
|
|
837
|
+
lodMultiplier: s.lodMultiplier ?? 2
|
|
838
|
+
}, c = s.shBands ?? (Ne(s.performanceProfile, n) === "smooth" ? 0 : De), h = Ee(n), f = s.budget === void 0 && s.maxBudget === void 0 && s.budgetCap === void 0, u = s.signal;
|
|
839
|
+
u == null || u.throwIfAborted();
|
|
840
|
+
let g;
|
|
841
|
+
if (t === "rad")
|
|
842
|
+
try {
|
|
843
|
+
const { buildRadScene: p } = await import("./formats/rad.js");
|
|
844
|
+
g = await p(e, l, s.request, c, f);
|
|
845
|
+
} catch (p) {
|
|
846
|
+
throw G(p) ? p : _(p, { phase: "manifest", url: e.manifestUrl });
|
|
847
|
+
}
|
|
848
|
+
else {
|
|
849
|
+
let p;
|
|
850
|
+
try {
|
|
851
|
+
p = await fetch(e.manifestUrl, ee(s.request, u));
|
|
852
|
+
} catch (S) {
|
|
853
|
+
throw G(S) ? S : _(S, { phase: "fetch", url: e.manifestUrl });
|
|
854
|
+
}
|
|
855
|
+
if (!p.ok)
|
|
856
|
+
throw _(
|
|
857
|
+
new Error(`Failed to load manifest ${e.manifestUrl}: HTTP ${p.status}`),
|
|
858
|
+
{ phase: "manifest", url: e.manifestUrl, status: p.status }
|
|
859
|
+
);
|
|
860
|
+
try {
|
|
861
|
+
const S = await p.json();
|
|
862
|
+
if (t === "lcc2") {
|
|
863
|
+
const { buildLcc2Scene: M } = await import("./formats/lcc.js");
|
|
864
|
+
g = M(S, e, l, s.shBands ?? 0);
|
|
865
|
+
} else if (t === "lcc") {
|
|
866
|
+
const { buildLccScene: M } = await import("./formats/lcc.js");
|
|
867
|
+
g = await M(S, e, { ...l, shBands: c });
|
|
868
|
+
} else
|
|
869
|
+
g = tt(S, e, l, s.shBands ?? 0);
|
|
870
|
+
} catch (S) {
|
|
871
|
+
throw G(S) ? S : _(S, { phase: "manifest", url: e.manifestUrl });
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
u == null || u.throwIfAborted();
|
|
875
|
+
const v = (t === "lcc" || t === "rad") && f ? ke(a, g.maxResidentSplats, n) : a, m = s.maxBudget === void 0 ? v : Math.min(r, v);
|
|
876
|
+
g.source.budget = m;
|
|
877
|
+
const b = Math.min(v, g.maxResidentSplats), T = s.experimentalStagedSwaps !== !1 ? 1.5 : 1.4, I = Math.max(
|
|
878
|
+
1,
|
|
879
|
+
Math.ceil(b * T / F)
|
|
880
|
+
), y = g.foveation ? ne(
|
|
881
|
+
s.foveationMode,
|
|
882
|
+
t === "rad" ? "page-table" : "frontier"
|
|
883
|
+
) : s.foveationMode === void 0 ? void 0 : ne(s.foveationMode);
|
|
884
|
+
let A;
|
|
885
|
+
if (j(y)) {
|
|
886
|
+
A = (await import("./frontier-worker-CAnK_Cmu.js")).default, u == null || u.throwIfAborted();
|
|
887
|
+
const S = g.source;
|
|
888
|
+
S.needsParentSizes !== void 0 && (S.needsParentSizes = !1);
|
|
889
|
+
}
|
|
890
|
+
const R = new Y(
|
|
891
|
+
g,
|
|
892
|
+
m,
|
|
893
|
+
I * F,
|
|
894
|
+
{
|
|
895
|
+
...s,
|
|
896
|
+
// Classic LCC's useful first frame is the bounded nearby-detail set,
|
|
897
|
+
// not its coarse shell. `.lcc2` waits for in-view coarsest coverage
|
|
898
|
+
// so empty octree squares do not flash. Keep every other format
|
|
899
|
+
// progressive, and let a caller explicitly request progressive for A/B.
|
|
900
|
+
...t === "lcc" && s.initialReveal === void 0 ? { initialReveal: "hold-near-l0" } : {},
|
|
901
|
+
...t === "lcc2" && s.initialReveal === void 0 ? { initialReveal: "hold-coverage" } : {},
|
|
902
|
+
// The resolved ceiling, not the caller's raw option: it may have been
|
|
903
|
+
// lifted to a moderate capture's leaf count above.
|
|
904
|
+
maxBudget: v,
|
|
905
|
+
// This line overrides `...options` above, so a declined request has to
|
|
906
|
+
// survive it - that is how `.rad` came to ignore both the `smooth`
|
|
907
|
+
// profile and an explicit `shBands: 0`. Only *zero* is re-applied here,
|
|
908
|
+
// never a partial reduction: the builders already honour partial
|
|
909
|
+
// requests by generating that many bands, whereas forcing a smaller
|
|
910
|
+
// count past one would mismatch the decoded chunk and degrade to
|
|
911
|
+
// neutral SH (see `SplatMesh.writePackedSh`).
|
|
912
|
+
shBands: c === 0 ? 0 : g.shBands ?? 0,
|
|
913
|
+
// Spark ships Mip-Splatting antialiasing ON (blurAmount 0.3 *with* opacity
|
|
914
|
+
// compensation `α·√(detRaw/detBlur)`). Match that default for `.rad`: the
|
|
915
|
+
// 0.3 low-pass without the compensation makes splats too opaque (uniform
|
|
916
|
+
// blur) and leaves anisotropic splats bright (needle spikes).
|
|
917
|
+
antialias: s.antialias ?? (t === "rad" ? !0 : void 0),
|
|
918
|
+
// Older XGRIDS LCC uses a smaller, compensated projected low-pass.
|
|
919
|
+
...t === "lcc" ? { projectedFilterProfile: "lcc" } : {},
|
|
920
|
+
// Match Spark's `.rad` render exactly: the LOD alpha encoding + merged-node
|
|
921
|
+
// σ-cutoff/super-Gaussian, and the √8 (≈2.83σ) base cutoff Spark defaults to.
|
|
922
|
+
// An explicit `lodAlpha` (e.g. `?lodAlpha=0`) wins for A/B.
|
|
923
|
+
//
|
|
924
|
+
// The √8 cutoff is *desktop only* - see `recommendedRadMaxStdDev`, which
|
|
925
|
+
// returns undefined on mobile so the `SplatMesh` constructor applies the
|
|
926
|
+
// same 4 ceiling `.rad` was the only format escaping. An explicit
|
|
927
|
+
// `maxStdDev` still wins, through `...options` above.
|
|
928
|
+
...t === "rad" ? {
|
|
929
|
+
lodAlpha: s.lodAlpha ?? !0,
|
|
930
|
+
...s.maxStdDev === void 0 && h !== void 0 ? { maxStdDev: h } : {}
|
|
931
|
+
} : {},
|
|
932
|
+
// A foveated scene renders whole chunks and picks the LOD cut per splat.
|
|
933
|
+
// `.rad` defaults to Spark's selected-index page table (only the frontier is
|
|
934
|
+
// paged to the GPU, so the whole device budget buys on-screen detail); other
|
|
935
|
+
// foveated formats keep the GPU `frontier` cut. `foveationMode: 'band'` (or
|
|
936
|
+
// `'frontier'`) forces the legacy paths for A/B. Overrides any caller blob cull.
|
|
937
|
+
...g.foveation ? {
|
|
938
|
+
foveationMode: y,
|
|
939
|
+
minSplatScreenRadius: g.foveation.minScreenRadiusPx,
|
|
940
|
+
maxSplatScreenRadius: g.foveation.maxScreenRadiusPx
|
|
941
|
+
} : {}
|
|
942
|
+
},
|
|
943
|
+
A,
|
|
944
|
+
t === "lcc"
|
|
945
|
+
), L = g.formatTransform ?? (R.orientation === "y-up" ? xe(t) : null);
|
|
946
|
+
return L && (R.matrix.copy(L), R.matrix.decompose(R.position, R.quaternion, R.scale), R.matrixWorldNeedsUpdate = !0), u != null && u.aborted && (R.dispose(), u.throwIfAborted()), R;
|
|
947
|
+
}
|
|
948
|
+
/** Slots the currently reserved pages can hold. */
|
|
949
|
+
get slabSlots() {
|
|
950
|
+
let e = 0;
|
|
951
|
+
for (const t of this.slabPages) e += t.count;
|
|
952
|
+
return e;
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Reserves or releases slab pages so the slab can hold `wanted` slots, and
|
|
956
|
+
* tells the worker's pager the new slot count.
|
|
957
|
+
*
|
|
958
|
+
* This is the mechanism that makes a shared pool worth having: storage follows
|
|
959
|
+
* the governed budget, so approaching a mesh grows its pages while the ones
|
|
960
|
+
* behind you hand theirs back, instead of every mesh holding its ceiling for
|
|
961
|
+
* the whole session. Growth stops at the construction ceiling and at whatever
|
|
962
|
+
* the pool can actually spare - a mesh that cannot grow simply stays coarse
|
|
963
|
+
* rather than throwing.
|
|
964
|
+
*/
|
|
965
|
+
syncSlabPages(e) {
|
|
966
|
+
if (this.slabCeiling === 0) return;
|
|
967
|
+
const t = Math.max(this.slabPageSplats, Math.min(this.slabCeiling, e));
|
|
968
|
+
let s = this.slabSlots;
|
|
969
|
+
for (; s < t; ) {
|
|
970
|
+
const n = Math.min(this.slabPageSplats, this.slabCeiling - s);
|
|
971
|
+
if (n <= 0) break;
|
|
972
|
+
try {
|
|
973
|
+
this.slabPages.push(this.reserveInactiveRange(n));
|
|
974
|
+
} catch {
|
|
975
|
+
break;
|
|
976
|
+
}
|
|
977
|
+
s += n;
|
|
978
|
+
}
|
|
979
|
+
for (; this.slabPages.length > 1; ) {
|
|
980
|
+
const n = this.slabPages[this.slabPages.length - 1];
|
|
981
|
+
if (s - n.count < t) break;
|
|
982
|
+
this.slabPages.pop(), s -= n.count, this.removeRange(n);
|
|
983
|
+
}
|
|
984
|
+
s !== this.pagerSlots && (this.pagerSlots = s, this.postToWorker({ type: "resize", capacity: s }), this.pageTableDrawn > s && this.setSlabResident(s), this.pendingWork = !0, this.lastScheduleTime = -1 / 0);
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* Writes `data` at slot `slot`, splitting the write where it crosses a page
|
|
988
|
+
* boundary. The pager's runs are contiguous in *slot* space, which page
|
|
989
|
+
* storage no longer guarantees is contiguous in the pool.
|
|
990
|
+
*/
|
|
991
|
+
writeSlabSlots(e, t, s) {
|
|
992
|
+
let n = 0;
|
|
993
|
+
for (; n < s; ) {
|
|
994
|
+
const r = t + n, a = this.slabPages[Math.floor(r / this.slabPageSplats)];
|
|
995
|
+
if (!a) return;
|
|
996
|
+
const l = r % this.slabPageSplats, c = Math.min(s - n, this.slabPageSplats - l);
|
|
997
|
+
this.overwriteRangeData(a, K(e, n, c), l), n += c;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
/** Zeros slots `[slot, slot + count)`, splitting at page boundaries like
|
|
1001
|
+
* {@link writeSlabSlots}, so freed slots hold nothing drawable. */
|
|
1002
|
+
degenerateSlabSlots(e, t) {
|
|
1003
|
+
let s = 0;
|
|
1004
|
+
for (; s < t; ) {
|
|
1005
|
+
const n = e + s, r = this.slabPages[Math.floor(n / this.slabPageSplats)];
|
|
1006
|
+
if (!r) return;
|
|
1007
|
+
const a = n % this.slabPageSplats, l = Math.min(t - s, this.slabPageSplats - a);
|
|
1008
|
+
this.degenerateRange(r, a, l), s += l;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Draws exactly the first `resident` slots: pages below the boundary are
|
|
1013
|
+
* fully active, the page containing it is partially active, the rest are
|
|
1014
|
+
* inactive. Freed tail slots simply leave the active list; they are also
|
|
1015
|
+
* degenerated (see {@link degenerateSlabSlots}) so that even a slot drawn by
|
|
1016
|
+
* mistake shows nothing.
|
|
1017
|
+
*/
|
|
1018
|
+
setSlabResident(e) {
|
|
1019
|
+
for (let t = 0; t < this.slabPages.length; t++) {
|
|
1020
|
+
const s = Math.min(
|
|
1021
|
+
this.slabPageSplats,
|
|
1022
|
+
Math.max(0, e - t * this.slabPageSplats)
|
|
1023
|
+
);
|
|
1024
|
+
this.setRangeActivePrefix(this.slabPages[t], s);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
/** Typed post to the frontier worker. */
|
|
1028
|
+
postToWorker(e, t = []) {
|
|
1029
|
+
var s;
|
|
1030
|
+
(s = this.frontierWorker) == null || s.postMessage(e, t);
|
|
1031
|
+
}
|
|
1032
|
+
/**
|
|
1033
|
+
* Applies a new decoded-chunk allowance from the scene's shared
|
|
1034
|
+
* {@link ChunkCacheBudget}.
|
|
1035
|
+
*
|
|
1036
|
+
* Only the cap moves; nothing is dropped here. Both cache implementations
|
|
1037
|
+
* evict lazily against it - the page-table worker inside its next
|
|
1038
|
+
* `reschedule`, against a frontier that is still current, and the classic
|
|
1039
|
+
* path in `evictChunks` on the next tick. Dropping chunks synchronously would
|
|
1040
|
+
* pull them out from under resident splats.
|
|
1041
|
+
*
|
|
1042
|
+
* `pageTableCacheAtLimit` is recomputed here rather than waiting for the next
|
|
1043
|
+
* plan, so a *raised* allowance re-arms the background sweep on this tick
|
|
1044
|
+
* instead of one idle interval later.
|
|
1045
|
+
*/
|
|
1046
|
+
applyCacheAllowance(e) {
|
|
1047
|
+
this.disposed || e !== this.cacheLimitBytes && (this.cacheLimitBytes = e, this.frontierWorker ? this.postToWorker({ type: "cacheBudget", cpuCacheBytes: e }) : this.cpuCacheBytes = e, this.fetchCountsValue.cacheLimitBytes = e, this.pageTableCacheAtLimit = this.fetchCountsValue.cacheBytes >= e, this.pendingWork = !0);
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Whether this scene ships collision meshes - true for an XGRIDS `.lcc` /
|
|
1051
|
+
* `.lcc2` dataset that carries them, false for a Streamed SOG scene, which
|
|
1052
|
+
* has none.
|
|
1053
|
+
*/
|
|
1054
|
+
get hasCollisionMeshes() {
|
|
1055
|
+
var e;
|
|
1056
|
+
return (((e = this.scene.collision) == null ? void 0 : e.meshes.length) ?? 0) > 0;
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Fetches and parses this scene's collision geometry: the triangle meshes an
|
|
1060
|
+
* XGRIDS `.lcc` (`collision.lci`) or `.lcc2` (`data/mesh/*.ply`) capture
|
|
1061
|
+
* ships beside its splats, for hosts that want collision, ground probes or
|
|
1062
|
+
* other spatial queries.
|
|
1063
|
+
*
|
|
1064
|
+
* The geometry is source-local, like {@link StreamedScene.bounds} - apply
|
|
1065
|
+
* this mesh's `matrixWorld` to put it in the frame the splats render in.
|
|
1066
|
+
* VLAM! builds no acceleration structure over it and never consults it.
|
|
1067
|
+
*
|
|
1068
|
+
* Tiles are fetched once and cached; concurrent callers share one load, and
|
|
1069
|
+
* a failed load can be retried by calling again. Resolves `[]` for a scene
|
|
1070
|
+
* without collision.
|
|
1071
|
+
*
|
|
1072
|
+
* @throws a `DOMException` named `AbortError` if cancelled, or if
|
|
1073
|
+
* {@link dispose} is called while the load is in flight.
|
|
1074
|
+
*/
|
|
1075
|
+
async loadCollisionMeshes(e = {}) {
|
|
1076
|
+
var r;
|
|
1077
|
+
(r = e.signal) == null || r.throwIfAborted();
|
|
1078
|
+
const t = this.scene.collision;
|
|
1079
|
+
if (!t || t.meshes.length === 0) return [];
|
|
1080
|
+
if (!this.collisionTiles) {
|
|
1081
|
+
const a = new AbortController();
|
|
1082
|
+
this.collisionAbort = a, this.collisionTiles = import("./formats/lcc.js").then(
|
|
1083
|
+
({ loadCollisionMeshTiles: l }) => l(t, {
|
|
1084
|
+
...this.requestOptions ? { request: this.requestOptions } : {},
|
|
1085
|
+
signal: a.signal
|
|
1086
|
+
})
|
|
1087
|
+
).catch((l) => {
|
|
1088
|
+
throw this.collisionTiles = void 0, l;
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
const { signal: s } = e;
|
|
1092
|
+
if (!s) return this.collisionTiles;
|
|
1093
|
+
let n;
|
|
1094
|
+
try {
|
|
1095
|
+
return await Promise.race([
|
|
1096
|
+
this.collisionTiles,
|
|
1097
|
+
new Promise((a, l) => {
|
|
1098
|
+
n = () => l(Ke(s)), s.addEventListener("abort", n, { once: !0 });
|
|
1099
|
+
})
|
|
1100
|
+
]);
|
|
1101
|
+
} finally {
|
|
1102
|
+
n && s.removeEventListener("abort", n);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Whether this scene ships an always-resident environment/background tile -
|
|
1107
|
+
* true for an XGRIDS `.lcc2` capture that carries one (its `env.sog` sky),
|
|
1108
|
+
* false for Streamed SOG, `.lcc`, `.rad`, or an `.lcc2` without one.
|
|
1109
|
+
*/
|
|
1110
|
+
get hasEnvironment() {
|
|
1111
|
+
return this.envFile !== void 0;
|
|
1112
|
+
}
|
|
1113
|
+
/** Whether the environment tile is currently set to render. */
|
|
1114
|
+
get environmentEnabled() {
|
|
1115
|
+
return this.envEnabled;
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* Splats in the environment tile, measured when it decoded - the manifest
|
|
1119
|
+
* does not carry the count. 0 until the tile has loaded (or if the scene
|
|
1120
|
+
* ships none). These sit outside the LOD budget, drawing from the pool's
|
|
1121
|
+
* capacity headroom.
|
|
1122
|
+
*/
|
|
1123
|
+
get environmentSplatCount() {
|
|
1124
|
+
return this.envSplatCount;
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Shows or hides the scene's environment/background tile. The switch is
|
|
1128
|
+
* instant and never refetches: once loaded, the tile stays in the pool and
|
|
1129
|
+
* only its active flag flips. Enabling before the tile has loaded triggers
|
|
1130
|
+
* its (one-time) load on the next update. No-op on a scene without one.
|
|
1131
|
+
*/
|
|
1132
|
+
setEnvironmentEnabled(e) {
|
|
1133
|
+
this.envFile === void 0 || e === this.envEnabled || (this.envEnabled = e, this.envHandle !== void 0 ? this.setRangeActive(this.envHandle, e) : e && (this.pendingWork = !0, this.lastScheduleTime = -1 / 0));
|
|
1134
|
+
}
|
|
1135
|
+
/** The active-splat budget this mesh keeps within. */
|
|
1136
|
+
get budget() {
|
|
1137
|
+
return this.budgetValue;
|
|
1138
|
+
}
|
|
1139
|
+
/**
|
|
1140
|
+
* The ceiling {@link setBudget} clamps to - {@link StreamedSplatMeshOptions.maxBudget}
|
|
1141
|
+
* when one was given, otherwise the construction budget.
|
|
1142
|
+
*
|
|
1143
|
+
* The pool was allocated for this number and cannot grow, so it is a hard
|
|
1144
|
+
* limit on what any governor can hand this mesh. Read it to check that a
|
|
1145
|
+
* shared-budget setup can actually deliver the share it is computing.
|
|
1146
|
+
*/
|
|
1147
|
+
get maxBudget() {
|
|
1148
|
+
return this.maximumBudget;
|
|
1149
|
+
}
|
|
1150
|
+
/** Alias used by hosts that manage static and streamed auto-LOD uniformly. */
|
|
1151
|
+
get budgetCeiling() {
|
|
1152
|
+
return this.maximumBudget;
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* The capture's real content size, when the format declares it (`.rad` reports
|
|
1156
|
+
* its leaf count) - the splat count needed to hold this mesh at full
|
|
1157
|
+
* resolution, independent of the budget it was constructed with.
|
|
1158
|
+
*
|
|
1159
|
+
* A host splitting one budget across several streamed meshes should clamp each
|
|
1160
|
+
* share to this: a mesh cannot spend more than it contains, so budget handed
|
|
1161
|
+
* past it buys nothing and is better given to a mesh that can use it. Note it
|
|
1162
|
+
* is *not* `maxBudget`: a foveated `.rad` reports `maxResidentSplats` as the
|
|
1163
|
+
* requested budget, because its pool holds a camera-directed resident set
|
|
1164
|
+
* rather than the whole tree.
|
|
1165
|
+
*
|
|
1166
|
+
* `undefined` when the format does not declare a content size.
|
|
1167
|
+
*/
|
|
1168
|
+
get contentSplatCount() {
|
|
1169
|
+
return this.scene.contentSplatCount;
|
|
1170
|
+
}
|
|
1171
|
+
/**
|
|
1172
|
+
* The drawn-splat target currently driving the `.rad` page-table frontier -
|
|
1173
|
+
* the governed budget, capped by
|
|
1174
|
+
* {@link SplatMeshOptions.foveationDrawBudget}. `0` on a mesh that is not in
|
|
1175
|
+
* `foveationMode: 'page-table'`, which has no frontier to target.
|
|
1176
|
+
*
|
|
1177
|
+
* This is the number that decides how deep the traversal descends, so it is
|
|
1178
|
+
* what to watch when checking that a near mesh really did receive more
|
|
1179
|
+
* detail: {@link budget} is the pool's allowance, this is what is spent.
|
|
1180
|
+
*/
|
|
1181
|
+
get drawBudget() {
|
|
1182
|
+
return this.frontierWorker ? this.pageTableDrawBudget : 0;
|
|
1183
|
+
}
|
|
1184
|
+
/**
|
|
1185
|
+
* Spark's per-mesh `lodScale` (see {@link StreamedSplatMeshOptions.lodScale}).
|
|
1186
|
+
* Mutable: raise it to sharpen a focused mesh, lower it to coarsen a
|
|
1187
|
+
* background one. Page-table `.rad` only.
|
|
1188
|
+
*
|
|
1189
|
+
* @throws {RangeError} if set to a value that is not positive and finite.
|
|
1190
|
+
*/
|
|
1191
|
+
get lodScale() {
|
|
1192
|
+
return this.lodScaleValue;
|
|
1193
|
+
}
|
|
1194
|
+
set lodScale(e) {
|
|
1195
|
+
const t = ce(e);
|
|
1196
|
+
t !== this.lodScaleValue && (this.lodScaleValue = t, this.pendingWork = !0, this.lastScheduleTime = -1 / 0);
|
|
1197
|
+
}
|
|
1198
|
+
/** In `page-table` mode the slab is fully active but mostly degenerate, so the
|
|
1199
|
+
* base `activeSplatCount` (slab size) is not the on-screen count - report the
|
|
1200
|
+
* frontier's drawn size instead. */
|
|
1201
|
+
get activeSplatCount() {
|
|
1202
|
+
return this.frontierWorker ? this.pageTableDrawn : super.activeSplatCount;
|
|
1203
|
+
}
|
|
1204
|
+
/**
|
|
1205
|
+
* Updates the LOD budget used for future scheduling within allocated capacity.
|
|
1206
|
+
*
|
|
1207
|
+
* @returns the budget actually in effect, which is `budget` clamped to
|
|
1208
|
+
* {@link maxBudget}. A `BudgetGovernor` reads this return value to detect a
|
|
1209
|
+
* capped member and hand the remainder to the others, so the clamp is
|
|
1210
|
+
* reported rather than hidden.
|
|
1211
|
+
*/
|
|
1212
|
+
setBudget(e) {
|
|
1213
|
+
const t = Math.min(W(e), this.maximumBudget);
|
|
1214
|
+
return t === this.budgetValue ? this.budgetValue : (this.budgetValue = t, this.scene.source.budget = t, this.frontierWorker && (this.pageTableDrawBudget = Math.min(t, this.pageTableDrawTarget), this.syncSlabPages(this.pageTableDrawBudget), this.pageTableDrawTargetExplicit && this.pageTableDrawTarget < t && !this.warnedDrawTargetCap && (this.warnedDrawTargetCap = !0, P(
|
|
1215
|
+
`StreamedSplatMesh: budget raised to ${t} but foveationDrawBudget caps the drawn frontier at ${this.pageTableDrawTarget}; the extra budget cannot buy detail. Raise or drop foveationDrawBudget to let the shared budget through.`
|
|
1216
|
+
))), this.pendingWork = !0, this.lastScheduleTime = -1 / 0, t);
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* What the LOD scheduler last decided, or `undefined` on sources that do not
|
|
1220
|
+
* schedule by leaf (the `.rad` page table, prefix readers).
|
|
1221
|
+
*
|
|
1222
|
+
* Distinct from {@link activeSplatCount}, and the distinction is the whole
|
|
1223
|
+
* point: `desired` is what the scheduler asked for, `activeSplatCount` is
|
|
1224
|
+
* what the pool ended up drawing. Equal means the cut is applied; `desired`
|
|
1225
|
+
* far below the budget means the *scheduler* declined to spend it, which is a
|
|
1226
|
+
* different bug from the mesh failing to apply what it was given.
|
|
1227
|
+
*/
|
|
1228
|
+
get lodStats() {
|
|
1229
|
+
return this.scene.source.stats;
|
|
1230
|
+
}
|
|
1231
|
+
/** Number of chunk files currently decoded and held. In page-table mode the
|
|
1232
|
+
* worker owns the cache - the main-thread map is always empty there, so report
|
|
1233
|
+
* what has been forwarded to it instead of a permanent zero. */
|
|
1234
|
+
get residentChunkCount() {
|
|
1235
|
+
return this.frontierWorker ? this.pageTableCachedFiles.size : this.cache.size;
|
|
1236
|
+
}
|
|
1237
|
+
/** Chunk fetches currently in flight. */
|
|
1238
|
+
get pendingChunkCount() {
|
|
1239
|
+
return this.fetching.size;
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* Main-thread cost of applying paging plans in `foveationMode: 'page-table'`.
|
|
1243
|
+
*
|
|
1244
|
+
* A plan is applied whole, off the render loop's own timing, so its cost does
|
|
1245
|
+
* not appear in {@link getUpdateTimings} - but it lands on the same thread and
|
|
1246
|
+
* a churning frontier can make it the largest stall in a frame. `worst*`
|
|
1247
|
+
* accumulate over the mesh's lifetime; the rest describe the most recent plan.
|
|
1248
|
+
*/
|
|
1249
|
+
get planTimings() {
|
|
1250
|
+
return this.planTimingsValue;
|
|
1251
|
+
}
|
|
1252
|
+
/**
|
|
1253
|
+
* Lifetime chunk-fetch totals by kind, plus page-table cache state.
|
|
1254
|
+
*
|
|
1255
|
+
* Diagnostic for the question "why is this still streaming after the view
|
|
1256
|
+
* settled?", which the three fetch sources answer differently and which no
|
|
1257
|
+
* other reading distinguishes:
|
|
1258
|
+
*
|
|
1259
|
+
* - **`sweep` climbing** - speculative file-order pre-warming of the whole
|
|
1260
|
+
* capture. Declined by the `smooth` profile; see `sweepAllowed`.
|
|
1261
|
+
* - **`priority` / `base` climbing while `evicted` climbs too** - the
|
|
1262
|
+
* frontier's touched set does not fit the worker cache, so chunks are
|
|
1263
|
+
* evicted and immediately refetched. Streaming never ends because it cannot.
|
|
1264
|
+
* - **`priority` / `base` climbing with `evicted` flat** - ordinary refinement
|
|
1265
|
+
* still converging on the cut; it should stop on its own.
|
|
1266
|
+
*
|
|
1267
|
+
* `uncovered` and `retiredEarly` answer a different question - "why are there
|
|
1268
|
+
* holes?" - and between them cover both ways this class can render nothing
|
|
1269
|
+
* where it should render something:
|
|
1270
|
+
*
|
|
1271
|
+
* - **`uncovered` climbing after the scene settles** - `substituteCoverage`
|
|
1272
|
+
* wanted a leaf's coarsest level as a stand-in and its chunk was not
|
|
1273
|
+
* cached. Expected briefly during initial load; afterwards it should not
|
|
1274
|
+
* move, because the coarsest files are pinned against eviction.
|
|
1275
|
+
* - **`retiredEarly` climbing** - coverage was retired before its replacement
|
|
1276
|
+
* landed, under pool pressure or past the retirement hold bound. This is
|
|
1277
|
+
* the swap path rather than the substitute path, and it is the one that
|
|
1278
|
+
* scales with the budget.
|
|
1279
|
+
*
|
|
1280
|
+
* Both are counted in whole leaves/groups, monotonically: they answer "did
|
|
1281
|
+
* this happen, and is it still happening", not "how much is missing now".
|
|
1282
|
+
*/
|
|
1283
|
+
get fetchCounts() {
|
|
1284
|
+
return this.fetchCountsValue;
|
|
1285
|
+
}
|
|
1286
|
+
/** Chunk files given up on after repeated fetch/decode failures. */
|
|
1287
|
+
get failedChunkCount() {
|
|
1288
|
+
return this.failedFiles.size;
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* Forgets all permanent chunk failures so their regions are fetched again.
|
|
1292
|
+
* Failures are otherwise terminal for the mesh's lifetime - call this when
|
|
1293
|
+
* the cause was transient (e.g. connectivity restored, `online` event).
|
|
1294
|
+
*/
|
|
1295
|
+
retryFailedChunks() {
|
|
1296
|
+
this.failedFiles.size !== 0 && (this.failedFiles.clear(), this.retrying.clear(), this.pendingWork = !0);
|
|
1297
|
+
}
|
|
1298
|
+
/**
|
|
1299
|
+
* Whether the scene is still resolving toward its target detail - chunks
|
|
1300
|
+
* are fetching, or a retry/append is pending. Goes false once the view
|
|
1301
|
+
* has settled (useful to drive a loading indicator).
|
|
1302
|
+
*/
|
|
1303
|
+
get isStreaming() {
|
|
1304
|
+
return this.pendingWork || this.fetching.size > 0 || this.retrying.size > 0;
|
|
1305
|
+
}
|
|
1306
|
+
/** The LOD distance model (mutable; e.g. raise to force the finest level). */
|
|
1307
|
+
get lodBaseDistance() {
|
|
1308
|
+
return this.scene.source.lodBaseDistance;
|
|
1309
|
+
}
|
|
1310
|
+
set lodBaseDistance(e) {
|
|
1311
|
+
this.scene.source.lodBaseDistance = e, this.pendingWork = !0;
|
|
1312
|
+
}
|
|
1313
|
+
update(e, t, s = {}) {
|
|
1314
|
+
const n = performance.now();
|
|
1315
|
+
e.updateMatrixWorld(), this.updateWorldMatrix(!0, !1);
|
|
1316
|
+
const r = Le(e, t);
|
|
1317
|
+
this.frontierWorker && (r ? this.pageTableViewportY = r.height : (t.getDrawingBufferSize(Se), this.pageTableViewportY = Se.y));
|
|
1318
|
+
const a = (r == null ? void 0 : r.head) ?? e, l = this.shouldReschedule(a, n) ? this.reschedule(a, n) : null;
|
|
1319
|
+
if (super.update(e, t, s), l && this.onPerformanceEvent) {
|
|
1320
|
+
const c = this.getUpdateTimings();
|
|
1321
|
+
l.cpuMs += performance.now() - l.timestamp, l.activeListMs = c.activeListMs, l.uploadMs = c.uploadMs, l.sortSubmitMs = c.sortSubmitMs, l.stagingTextureAllocations = c.stagingTextureAllocations, l.activeListUpdateRanges = c.activeListUpdateRanges, this.onPerformanceEvent(l);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
/** Root bounds of the whole scene, valid before any chunk has loaded. */
|
|
1325
|
+
computeSplatBounds() {
|
|
1326
|
+
return this.scene.bounds.clone();
|
|
1327
|
+
}
|
|
1328
|
+
/**
|
|
1329
|
+
* Enables or disables writing each resident run's **resolved** LOD level into
|
|
1330
|
+
* the `lodLevel` float channel (for a false-color debug modifier: 0 = finest).
|
|
1331
|
+
* Values come from applied desired runs after budget resolution - not from
|
|
1332
|
+
* distance ambition alone. Call before assigning a modifier that reads the channel.
|
|
1333
|
+
*/
|
|
1334
|
+
setLodLevelDebug(e) {
|
|
1335
|
+
if (e) {
|
|
1336
|
+
this.lodLevelChannelReady || (this.defineChannel("lodLevel", { type: "float", fill: -1 }), this.lodLevelChannelReady = !0), this.lodLevelDebug = !0;
|
|
1337
|
+
for (const { run: t, handle: s } of this.resident.values())
|
|
1338
|
+
this.writeLodLevelChannel(s, t.level);
|
|
1339
|
+
for (const { run: t, handle: s, uploadedCount: n } of this.staged.values())
|
|
1340
|
+
n === t.count && this.writeLodLevelChannel(s, t.level);
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
this.lodLevelDebug = !1;
|
|
1344
|
+
}
|
|
1345
|
+
/** Whether {@link setLodLevelDebug} is currently writing levels. */
|
|
1346
|
+
get isLodLevelDebug() {
|
|
1347
|
+
return this.lodLevelDebug;
|
|
1348
|
+
}
|
|
1349
|
+
/**
|
|
1350
|
+
* Startup-hold progress for {@link StreamedSplatMeshOptions.initialReveal}.
|
|
1351
|
+
* Hosts using `'hold-near-l0'` or `'hold-coverage'` should keep the mesh
|
|
1352
|
+
* invisible while `status === 'pending'`, then reveal on `'ready'` or
|
|
1353
|
+
* `'degraded'`.
|
|
1354
|
+
*/
|
|
1355
|
+
get initialRevealState() {
|
|
1356
|
+
return this.initialRevealStateValue;
|
|
1357
|
+
}
|
|
1358
|
+
/**
|
|
1359
|
+
* Captures a fresh startup-hold set on the next {@link update}. Hosts that
|
|
1360
|
+
* apply their final initial camera pose after the mesh first receives frames
|
|
1361
|
+
* should call this before lifting their loading cover. It is a no-op when
|
|
1362
|
+
* the hold was never armed (progressive startup, or a format without the
|
|
1363
|
+
* matching LodSource hook).
|
|
1364
|
+
*/
|
|
1365
|
+
recaptureInitialReveal() {
|
|
1366
|
+
this.initialRevealHold !== "off" && (this.frozenCriticalRuns = null, this.initialRevealStartedAt = void 0, this.initialRevealPhase = "capture", this.initialRevealStateValue = {
|
|
1367
|
+
status: "pending",
|
|
1368
|
+
stagedSplats: 0,
|
|
1369
|
+
totalSplats: 0,
|
|
1370
|
+
readyGroups: 0,
|
|
1371
|
+
totalGroups: 0
|
|
1372
|
+
}, this.pendingWork = !0);
|
|
1373
|
+
}
|
|
1374
|
+
writeLodLevelChannel(e, t) {
|
|
1375
|
+
!this.lodLevelDebug || e.count === 0 || ((!this.lodLevelScratch || this.lodLevelScratch.length < e.count) && (this.lodLevelScratch = new Float32Array(e.count)), this.lodLevelScratch.fill(t, 0, e.count), this.writeChannel(e, "lodLevel", this.lodLevelScratch.subarray(0, e.count)));
|
|
1376
|
+
}
|
|
1377
|
+
/**
|
|
1378
|
+
* Declares a per-splat channel whose values **persist across LOD churn**:
|
|
1379
|
+
* edits are stored sparsely keyed by `(chunk file, local index)` - a stable
|
|
1380
|
+
* splat identity in the streaming design - and re-applied whenever a chunk
|
|
1381
|
+
* is (re)appended. Paint a region with {@link paintPersistent}, orbit away
|
|
1382
|
+
* until it is evicted, come back, and the values return. See M7.6.
|
|
1383
|
+
*
|
|
1384
|
+
* Wraps {@link SplatMesh.defineChannel}; read it from a modifier with
|
|
1385
|
+
* `ctx.channel(name)` as usual.
|
|
1386
|
+
*/
|
|
1387
|
+
definePersistentChannel(e, t = {}) {
|
|
1388
|
+
this.defineChannel(e, t), this.persistentChannels.set(e, {
|
|
1389
|
+
type: t.type ?? "float",
|
|
1390
|
+
fill: t.fill ?? 0,
|
|
1391
|
+
maxEdits: Math.max(1, Math.floor(t.maxEdits ?? 1e6)),
|
|
1392
|
+
edits: /* @__PURE__ */ new Map(),
|
|
1393
|
+
total: 0,
|
|
1394
|
+
warned: !1
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
/**
|
|
1398
|
+
* Sets a persistent channel to `value` for every currently-resident splat
|
|
1399
|
+
* within `radius` (world units) of `worldPoint`, and records the edit so it
|
|
1400
|
+
* survives eviction/reload. Splats whose chunk is not currently decoded on
|
|
1401
|
+
* the CPU cannot be located and are skipped (they are usually far from the
|
|
1402
|
+
* camera); their painted neighbours in resident chunks are unaffected.
|
|
1403
|
+
*
|
|
1404
|
+
* The radius assumes this mesh's world transform is rigid (rotation +
|
|
1405
|
+
* translation, as the built-in format transforms are); a scaled mesh would
|
|
1406
|
+
* distort the brush. Edit a persistent channel only through this method -
|
|
1407
|
+
* direct {@link SplatMesh.writeChannel} writes are not recorded and are
|
|
1408
|
+
* overwritten by the next re-apply.
|
|
1409
|
+
*
|
|
1410
|
+
* @returns the number of splats edited this call.
|
|
1411
|
+
* @throws {Error} if the channel was not declared with
|
|
1412
|
+
* {@link definePersistentChannel}.
|
|
1413
|
+
*/
|
|
1414
|
+
paintPersistent(e, t, s, n) {
|
|
1415
|
+
const r = this.persistentChannels.get(e);
|
|
1416
|
+
if (!r)
|
|
1417
|
+
throw new Error(
|
|
1418
|
+
`StreamedSplatMesh.paintPersistent: channel "${e}" is not a persistent channel. Call definePersistentChannel("${e}") first.`
|
|
1419
|
+
);
|
|
1420
|
+
V.copy(t), this.worldToLocal(V);
|
|
1421
|
+
const a = s * s;
|
|
1422
|
+
let l = 0;
|
|
1423
|
+
const c = /* @__PURE__ */ new Set();
|
|
1424
|
+
for (const { run: h } of this.resident.values()) {
|
|
1425
|
+
const f = this.cache.get(h.file);
|
|
1426
|
+
if (!f) continue;
|
|
1427
|
+
const u = f.data.positions, g = r.edits.get(h.file) ?? /* @__PURE__ */ new Map();
|
|
1428
|
+
let v = !1;
|
|
1429
|
+
for (let m = 0; m < h.count; m++) {
|
|
1430
|
+
const b = h.offset + m, T = u[b * 3 + 0] - V.x, I = u[b * 3 + 1] - V.y, y = u[b * 3 + 2] - V.z;
|
|
1431
|
+
if (!(T * T + I * I + y * y > a) && !g.has(b)) {
|
|
1432
|
+
if (r.total >= r.maxEdits) {
|
|
1433
|
+
r.warned || (r.warned = !0, P(
|
|
1434
|
+
`StreamedSplatMesh.paintPersistent: channel "${e}" hit its maxEdits cap (${r.maxEdits}); further new edits are dropped.`
|
|
1435
|
+
));
|
|
1436
|
+
continue;
|
|
1437
|
+
}
|
|
1438
|
+
r.total++, g.set(b, n), v = !0, l++;
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
v && (r.edits.set(h.file, g), c.add(h.file));
|
|
1442
|
+
}
|
|
1443
|
+
if (c.size > 0)
|
|
1444
|
+
for (const { run: h, handle: f } of this.resident.values())
|
|
1445
|
+
c.has(h.file) && this.applyPersistentRun(e, r, h, f);
|
|
1446
|
+
return l;
|
|
1447
|
+
}
|
|
1448
|
+
/**
|
|
1449
|
+
* Clears every stored edit for a persistent channel and zeroes the value on
|
|
1450
|
+
* all currently-resident splats. Chunks that are not resident are covered by
|
|
1451
|
+
* the emptied store - they reload at the channel's fill value.
|
|
1452
|
+
*
|
|
1453
|
+
* @throws {Error} if the channel is not a persistent channel.
|
|
1454
|
+
*/
|
|
1455
|
+
clearPersistentChannel(e) {
|
|
1456
|
+
const t = this.persistentChannels.get(e);
|
|
1457
|
+
if (!t)
|
|
1458
|
+
throw new Error(
|
|
1459
|
+
`StreamedSplatMesh.clearPersistentChannel: channel "${e}" is not a persistent channel.`
|
|
1460
|
+
);
|
|
1461
|
+
t.edits.clear(), t.total = 0;
|
|
1462
|
+
for (const { run: s, handle: n } of this.resident.values()) {
|
|
1463
|
+
const r = t.type === "byte" ? new Uint8Array(s.count) : new Float32Array(s.count);
|
|
1464
|
+
this.writeChannel(n, e, r);
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
dispose() {
|
|
1468
|
+
var e, t, s, n, r;
|
|
1469
|
+
if (!this.disposed) {
|
|
1470
|
+
this.loader.dispose(), this.frontierWorker && (this.frontierWorker.onmessage = null, this.frontierWorker.terminate()), this.pageTableDisposed = !0, (e = this.collisionAbort) == null || e.abort(), (t = this.collisionTiles) == null || t.catch(() => {
|
|
1471
|
+
}), this.collisionTiles = void 0;
|
|
1472
|
+
for (const { controller: a } of this.fetching.values()) a.abort();
|
|
1473
|
+
if (this.fetching.clear(), this.fetchHandle && ((s = this.fetchScheduler) == null || s.unregister(this.fetchHandle)), this.cacheBudgetHandle) {
|
|
1474
|
+
const a = this.cacheBudgetHandle;
|
|
1475
|
+
this.cacheBudgetHandle = void 0, (n = this.cacheBudget) == null || n.unregister(a);
|
|
1476
|
+
}
|
|
1477
|
+
(r = this.localSource) == null || r.dispose(), this.localSource = void 0, this.cache.clear(), this.cacheBytesTotal = 0, this.retrying.clear(), this.failedFiles.clear(), this.neededFiles.clear(), this.persistentChannels.clear(), this.resident.clear(), this.staged.clear(), this.pageTableCachedFiles.clear(), this.envHandle = void 0, this.envSplatCount = 0, super.dispose();
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
shouldReschedule(e, t) {
|
|
1481
|
+
if (this.pendingWork || t - this.lastScheduleTime > ft) return !0;
|
|
1482
|
+
e.getWorldPosition(pe);
|
|
1483
|
+
const s = this.scene.bounds.getBoundingSphere(wt).radius || 1;
|
|
1484
|
+
return pe.distanceTo(this.lastCameraPos) > s * 25e-4 ? !0 : (e.getWorldQuaternion(me), me.angleTo(this.lastCameraQuat) > 87e-4);
|
|
1485
|
+
}
|
|
1486
|
+
reschedule(e, t) {
|
|
1487
|
+
var L;
|
|
1488
|
+
const s = performance.now();
|
|
1489
|
+
let n = null;
|
|
1490
|
+
if (this.onPerformanceEvent !== void 0) {
|
|
1491
|
+
n = { resident: /* @__PURE__ */ new Map(), staged: /* @__PURE__ */ new Map() };
|
|
1492
|
+
for (const [p, S] of this.resident) n.resident.set(p, S.run.count);
|
|
1493
|
+
for (const [p, S] of this.staged) n.staged.set(p, S.uploadedCount);
|
|
1494
|
+
}
|
|
1495
|
+
const r = this.compactionCount;
|
|
1496
|
+
if (this.pendingWork = !1, this.lastScheduleTime = t, e.getWorldPosition(this.lastCameraPos), e.getWorldQuaternion(this.lastCameraQuat), x.copy(this.lastCameraPos), this.worldToLocal(x), ve.multiplyMatrices(e.projectionMatrix, e.matrixWorldInverse).multiply(this.matrixWorld), z.setFromProjectionMatrix(ve), this.frontierWorker) {
|
|
1497
|
+
e.getWorldDirection(B).add(this.lastCameraPos), this.worldToLocal(B), B.sub(x).normalize();
|
|
1498
|
+
const p = e.projectionMatrix.elements[5] * this.pageTableViewportY / 2;
|
|
1499
|
+
if (p > 0 && (this.pageTableLimit = this.pageTableTargetPx / p), this.reschedulePageTable(x, B, z, t), this.onPerformanceEvent === void 0) return null;
|
|
1500
|
+
const S = performance.now();
|
|
1501
|
+
return {
|
|
1502
|
+
timestamp: S,
|
|
1503
|
+
cpuMs: S - s,
|
|
1504
|
+
activeListMs: 0,
|
|
1505
|
+
uploadMs: 0,
|
|
1506
|
+
sortSubmitMs: 0,
|
|
1507
|
+
stagingTextureAllocations: 0,
|
|
1508
|
+
activeListUpdateRanges: 0,
|
|
1509
|
+
appendedCount: 0,
|
|
1510
|
+
removedCount: 0,
|
|
1511
|
+
stagedCount: 0,
|
|
1512
|
+
uploadCount: 0,
|
|
1513
|
+
activeCount: this.pageTableDrawn,
|
|
1514
|
+
forcedSort: !1,
|
|
1515
|
+
compacted: !1
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1518
|
+
e.getWorldDirection(B).add(this.lastCameraPos), this.worldToLocal(B), B.sub(x).normalize();
|
|
1519
|
+
const a = this.scene.source.computeDesiredRuns(
|
|
1520
|
+
x,
|
|
1521
|
+
z,
|
|
1522
|
+
t,
|
|
1523
|
+
B
|
|
1524
|
+
), l = this.captureOrContinueInitialReveal(
|
|
1525
|
+
a,
|
|
1526
|
+
t,
|
|
1527
|
+
x,
|
|
1528
|
+
z
|
|
1529
|
+
), c = l !== null, h = l ?? a, f = /* @__PURE__ */ new Map(), u = /* @__PURE__ */ new Set();
|
|
1530
|
+
for (const p of h)
|
|
1531
|
+
f.set(C(p), p), u.add(p.file);
|
|
1532
|
+
for (const [p, S] of this.staged)
|
|
1533
|
+
f.has(p) || c && ((L = this.frozenCriticalRuns) != null && L.some((M) => C(M) === p)) || (this.removeRange(S.handle), this.staged.delete(p));
|
|
1534
|
+
for (const [p, { controller: S }] of this.fetching)
|
|
1535
|
+
!u.has(p) && !this.scene.pinnedFiles.has(p) && S.abort();
|
|
1536
|
+
for (const p of this.retrying.keys())
|
|
1537
|
+
!u.has(p) && !this.scene.pinnedFiles.has(p) && this.retrying.delete(p);
|
|
1538
|
+
this.neededFiles.clear();
|
|
1539
|
+
for (const p of h) {
|
|
1540
|
+
const S = C(p);
|
|
1541
|
+
if (this.resident.has(S)) continue;
|
|
1542
|
+
const M = this.staged.get(S);
|
|
1543
|
+
M && M.uploadedCount === p.count || this.neededFiles.add(p.file);
|
|
1544
|
+
}
|
|
1545
|
+
this.envFile !== void 0 && this.envEnabled && this.envHandle === void 0 && !this.envUnfit && !this.failedFiles.has(this.envFile) && this.neededFiles.add(this.envFile);
|
|
1546
|
+
const g = h.filter((p) => !this.resident.has(C(p))), v = c ? [] : [...this.resident.entries()].filter(([p]) => !f.has(p)), m = c ? (
|
|
1547
|
+
// Mesh is invisible during the hold, so L0 cell-atomicity (no holes) is
|
|
1548
|
+
// irrelevant - commit each frozen slice as it lands so a partial home
|
|
1549
|
+
// cell cannot block reveal behind sibling subchunks still fetching.
|
|
1550
|
+
_e(g)
|
|
1551
|
+
) : ze(g, v), b = !c && Oe(m);
|
|
1552
|
+
m.sort(
|
|
1553
|
+
(p, S) => b ? Ue(p, S) : re(p) - re(S)
|
|
1554
|
+
);
|
|
1555
|
+
const T = /* @__PURE__ */ new Map();
|
|
1556
|
+
this.updateEnvironment(t, T);
|
|
1557
|
+
let I = 0, y = !1, A = !1, R = !1;
|
|
1558
|
+
for (const p of m) {
|
|
1559
|
+
if (!b && p.removes.length > 0 && y) {
|
|
1560
|
+
if (this.neverRetireCoverageEarly || !A && this.retireHeldTicks < dt) {
|
|
1561
|
+
this.pendingWork = !0, R = !0;
|
|
1562
|
+
continue;
|
|
1563
|
+
}
|
|
1564
|
+
this.fetchCountsValue.retiredEarly++;
|
|
1565
|
+
}
|
|
1566
|
+
if (p.adds.length === 0) {
|
|
1567
|
+
this.applyGroup(p, t);
|
|
1568
|
+
continue;
|
|
1569
|
+
}
|
|
1570
|
+
const S = p.adds.filter((E) => !this.cache.has(E.file)), M = c || Ye(p) && this.initialRevealHold !== "hold-coverage";
|
|
1571
|
+
if (S.length > 0) {
|
|
1572
|
+
let E = !1;
|
|
1573
|
+
for (const k of S)
|
|
1574
|
+
this.failedFiles.has(k.file) || (oe(
|
|
1575
|
+
T,
|
|
1576
|
+
k.file,
|
|
1577
|
+
qe(k, this.scene.source.lodBaseDistance),
|
|
1578
|
+
k
|
|
1579
|
+
), E = !0);
|
|
1580
|
+
if (c || this.substituteCoverage(p, t, T, M), E && (this.pendingWork = !0, y = !0), !c) continue;
|
|
1581
|
+
}
|
|
1582
|
+
if (c && this.environmentPendingForReveal()) {
|
|
1583
|
+
this.pendingWork = !0, y = !0;
|
|
1584
|
+
continue;
|
|
1585
|
+
}
|
|
1586
|
+
if ((c || this.stagedSwapsEnabled && p.addCount > this.appendCap) && this.canStageGroup(p)) {
|
|
1587
|
+
const E = this.stageGroup(p, t, Math.max(0, this.appendCap - I));
|
|
1588
|
+
if (I += E, !p.adds.every((k) => {
|
|
1589
|
+
var ie;
|
|
1590
|
+
return ((ie = this.staged.get(C(k))) == null ? void 0 : ie.uploadedCount) === k.count;
|
|
1591
|
+
})) {
|
|
1592
|
+
this.pendingWork = !0, y = !0;
|
|
1593
|
+
continue;
|
|
1594
|
+
}
|
|
1595
|
+
if (E > 0 && !c) {
|
|
1596
|
+
this.deferNextSortRequest(), this.pendingWork = !0, y = !0;
|
|
1597
|
+
continue;
|
|
1598
|
+
}
|
|
1599
|
+
this.commitStagedGroup(p);
|
|
1600
|
+
continue;
|
|
1601
|
+
}
|
|
1602
|
+
if (!c && I > 0 && I + p.addCount > this.appendCap) {
|
|
1603
|
+
this.pendingWork = !0, y = !0;
|
|
1604
|
+
continue;
|
|
1605
|
+
}
|
|
1606
|
+
if (c) {
|
|
1607
|
+
this.pendingWork = !0, y = !0;
|
|
1608
|
+
continue;
|
|
1609
|
+
}
|
|
1610
|
+
if (!this.applyGroup(p, t)) {
|
|
1611
|
+
this.pendingWork = !0, A = !0;
|
|
1612
|
+
continue;
|
|
1613
|
+
}
|
|
1614
|
+
I += p.addCount;
|
|
1615
|
+
}
|
|
1616
|
+
return this.flushClassicFetches(T, this.scene.source.lodBaseDistance, c), this.retireHeldTicks = R ? this.retireHeldTicks + 1 : 0, c && (this.finishInitialRevealIfComplete(), this.initialRevealPhase === "holding" && (this.pendingWork = !0)), this.fetchCountsValue.cacheBytes = this.cacheBytesTotal, this.fetchCountsValue.cacheLimitBytes = this.cpuCacheBytes, this.cacheBytesTotal > this.cpuCacheBytes && (this.fetchCountsValue.cacheFull = !0), this.evictChunks(t), n === null ? null : this.createPerformanceEvent(
|
|
1617
|
+
n.resident,
|
|
1618
|
+
n.staged,
|
|
1619
|
+
r,
|
|
1620
|
+
s
|
|
1621
|
+
);
|
|
1622
|
+
}
|
|
1623
|
+
rowAlignedSplats(e) {
|
|
1624
|
+
return Math.ceil(e / F) * F;
|
|
1625
|
+
}
|
|
1626
|
+
/**
|
|
1627
|
+
* Startup hold seeds: the coverage group containing (or nearest to) the
|
|
1628
|
+
* camera within {@link LodSource.lodBaseDistance}. HiRes tiles often fail the
|
|
1629
|
+
* frustum test when most of the cell sits behind the camera - do **not**
|
|
1630
|
+
* require `inView`, or the hold seeds a screen-facing neighbour instead.
|
|
1631
|
+
* Coarser home levels come from {@link LodSource.runsAtLevelFor}.
|
|
1632
|
+
*/
|
|
1633
|
+
selectHomeSeedRuns(e) {
|
|
1634
|
+
var r;
|
|
1635
|
+
const t = this.scene.source.lodBaseDistance, s = e.filter(
|
|
1636
|
+
(a) => a.coverageGroup !== void 0 && (a.distance ?? Number.POSITIVE_INFINITY) <= t
|
|
1637
|
+
).sort(
|
|
1638
|
+
(a, l) => (a.distance ?? Number.POSITIVE_INFINITY) - (l.distance ?? Number.POSITIVE_INFINITY) || // Same distance: prefer in-view, then finer.
|
|
1639
|
+
(a.inView === !0 ? 0 : 1) - (l.inView === !0 ? 0 : 1) || a.level - l.level || a.leafStart - l.leafStart
|
|
1640
|
+
), n = (r = s[0]) == null ? void 0 : r.coverageGroup;
|
|
1641
|
+
return n === void 0 ? [] : s.filter((a) => a.coverageGroup === n);
|
|
1642
|
+
}
|
|
1643
|
+
coarsenHomeRuns(e, t) {
|
|
1644
|
+
var r;
|
|
1645
|
+
const s = [], n = this.scene.source;
|
|
1646
|
+
for (const a of e) {
|
|
1647
|
+
if (a.level === t) {
|
|
1648
|
+
s.push(a);
|
|
1649
|
+
continue;
|
|
1650
|
+
}
|
|
1651
|
+
const l = ((r = n.runsAtLevelFor) == null ? void 0 : r.call(n, a.leafStart, a.leafEnd, t)) ?? [];
|
|
1652
|
+
if (l.length !== 0)
|
|
1653
|
+
for (const c of l)
|
|
1654
|
+
s.push({
|
|
1655
|
+
...c,
|
|
1656
|
+
distance: a.distance,
|
|
1657
|
+
inView: a.inView,
|
|
1658
|
+
...a.coverageGroup === void 0 ? {} : { coverageGroup: a.coverageGroup },
|
|
1659
|
+
...a.screenImportance === void 0 ? {} : { screenImportance: a.screenImportance }
|
|
1660
|
+
});
|
|
1661
|
+
}
|
|
1662
|
+
return s;
|
|
1663
|
+
}
|
|
1664
|
+
criticalRunsFitCapacity(e) {
|
|
1665
|
+
let t = 0;
|
|
1666
|
+
for (const s of e) t += this.rowAlignedSplats(s.count);
|
|
1667
|
+
return t <= this.freeSplatCapacity;
|
|
1668
|
+
}
|
|
1669
|
+
publishInitialRevealProgress(e) {
|
|
1670
|
+
const t = /* @__PURE__ */ new Map();
|
|
1671
|
+
for (const l of e) {
|
|
1672
|
+
const c = l.coverageGroup !== void 0 ? `g:${l.coverageGroup}` : C(l);
|
|
1673
|
+
let h = t.get(c);
|
|
1674
|
+
h || (h = [], t.set(c, h)), h.push(l);
|
|
1675
|
+
}
|
|
1676
|
+
let s = 0, n = 0, r = 0;
|
|
1677
|
+
for (const l of t.values()) {
|
|
1678
|
+
let c = !0;
|
|
1679
|
+
for (const h of l) {
|
|
1680
|
+
n += h.count;
|
|
1681
|
+
const f = C(h);
|
|
1682
|
+
if (this.resident.has(f)) {
|
|
1683
|
+
s += h.count;
|
|
1684
|
+
continue;
|
|
1685
|
+
}
|
|
1686
|
+
const u = this.staged.get(f);
|
|
1687
|
+
s += (u == null ? void 0 : u.uploadedCount) ?? 0, (!u || u.uploadedCount !== h.count) && (c = !1);
|
|
1688
|
+
}
|
|
1689
|
+
c && r++;
|
|
1690
|
+
}
|
|
1691
|
+
const a = this.initialRevealStateValue;
|
|
1692
|
+
if (a.status === "degraded") {
|
|
1693
|
+
this.initialRevealStateValue = {
|
|
1694
|
+
status: "degraded",
|
|
1695
|
+
reason: a.reason,
|
|
1696
|
+
stagedSplats: s,
|
|
1697
|
+
totalSplats: n,
|
|
1698
|
+
readyGroups: r,
|
|
1699
|
+
totalGroups: t.size
|
|
1700
|
+
};
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
this.initialRevealStateValue = {
|
|
1704
|
+
status: "pending",
|
|
1705
|
+
stagedSplats: s,
|
|
1706
|
+
totalSplats: n,
|
|
1707
|
+
readyGroups: r,
|
|
1708
|
+
totalGroups: t.size
|
|
1709
|
+
};
|
|
1710
|
+
}
|
|
1711
|
+
releaseInitialReveal(e, t) {
|
|
1712
|
+
const s = this.frozenCriticalRuns ?? [];
|
|
1713
|
+
this.publishInitialRevealProgress(s);
|
|
1714
|
+
const n = this.initialRevealStateValue, r = n.status === "pending" || n.status === "degraded" ? n.stagedSplats : 0, a = n.status === "pending" || n.status === "degraded" ? n.totalSplats : 0, l = n.status === "pending" || n.status === "degraded" ? n.readyGroups : 0, c = n.status === "pending" || n.status === "degraded" ? n.totalGroups : 0;
|
|
1715
|
+
e === "ready" ? this.initialRevealStateValue = { status: "ready" } : this.initialRevealStateValue = {
|
|
1716
|
+
status: "degraded",
|
|
1717
|
+
reason: t ?? "fetch-failed",
|
|
1718
|
+
stagedSplats: r,
|
|
1719
|
+
totalSplats: a,
|
|
1720
|
+
readyGroups: l,
|
|
1721
|
+
totalGroups: c
|
|
1722
|
+
}, this.frozenCriticalRuns = null, this.initialRevealPhase = "released", this.pendingWork = !0;
|
|
1723
|
+
}
|
|
1724
|
+
/**
|
|
1725
|
+
* `.lcc2` coverage hold: freeze coarsest covering runs for in-view cells.
|
|
1726
|
+
* Missing `coverageRunsFor` (or an empty result after fallback) releases
|
|
1727
|
+
* immediately so the mesh does not stay hidden with nothing to fetch.
|
|
1728
|
+
*/
|
|
1729
|
+
captureCoverageHold(e, t, s) {
|
|
1730
|
+
var r, a;
|
|
1731
|
+
const n = ((a = (r = this.scene.source).coverageRunsFor) == null ? void 0 : a.call(r, e, t)) ?? [];
|
|
1732
|
+
if (n.length === 0) {
|
|
1733
|
+
if (this.environmentPendingForReveal()) {
|
|
1734
|
+
this.frozenCriticalRuns = [], this.initialRevealStartedAt = s, this.initialRevealPhase = "holding", this.publishInitialRevealProgress([]);
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
this.initialRevealStateValue = { status: "ready" }, this.initialRevealPhase = "released";
|
|
1738
|
+
return;
|
|
1739
|
+
}
|
|
1740
|
+
if (!this.criticalRunsFitCapacity(n)) {
|
|
1741
|
+
this.frozenCriticalRuns = n, this.releaseInitialReveal("degraded", "capacity");
|
|
1742
|
+
return;
|
|
1743
|
+
}
|
|
1744
|
+
this.frozenCriticalRuns = n, this.initialRevealStartedAt = s, this.initialRevealPhase = "holding", this.publishInitialRevealProgress(n);
|
|
1745
|
+
}
|
|
1746
|
+
captureOrContinueInitialReveal(e, t, s, n) {
|
|
1747
|
+
if (this.initialRevealPhase === "off" || this.initialRevealPhase === "released") return null;
|
|
1748
|
+
if (this.initialRevealPhase === "capture")
|
|
1749
|
+
if (this.initialRevealHold === "hold-coverage")
|
|
1750
|
+
this.captureCoverageHold(s, n, t);
|
|
1751
|
+
else {
|
|
1752
|
+
const r = this.selectHomeSeedRuns(e);
|
|
1753
|
+
let a = [];
|
|
1754
|
+
if (r.length === 0) {
|
|
1755
|
+
const l = this.scene.source.lodBaseDistance * this.scene.source.lodMultiplier * this.scene.source.lodMultiplier, c = e.filter(
|
|
1756
|
+
(f) => f.level <= 2 && f.coverageGroup !== void 0 && (f.distance ?? Number.POSITIVE_INFINITY) <= l
|
|
1757
|
+
), h = [...c].sort(
|
|
1758
|
+
(f, u) => (f.distance ?? Number.POSITIVE_INFINITY) - (u.distance ?? Number.POSITIVE_INFINITY) || (f.inView === !0 ? 0 : 1) - (u.inView === !0 ? 0 : 1) || (f.screenImportance ?? Number.POSITIVE_INFINITY) - (u.screenImportance ?? Number.POSITIVE_INFINITY) || f.leafStart - u.leafStart
|
|
1759
|
+
)[0];
|
|
1760
|
+
if (!h)
|
|
1761
|
+
return this.initialRevealStateValue = { status: "ready" }, this.initialRevealPhase = "released", null;
|
|
1762
|
+
if (a = h.coverageGroup !== void 0 ? c.filter((f) => f.coverageGroup === h.coverageGroup) : [h], this.criticalRunsFitCapacity(a) || (a = h.coverageGroup !== void 0 ? c.filter(
|
|
1763
|
+
(f) => f.coverageGroup === h.coverageGroup && this.criticalRunsFitCapacity([f])
|
|
1764
|
+
).slice(0, 1) : c.filter((f) => this.criticalRunsFitCapacity([f])).slice(0, 1)), a.length === 0 || !this.criticalRunsFitCapacity(a))
|
|
1765
|
+
return this.frozenCriticalRuns = a.length > 0 ? a : [h], this.releaseInitialReveal("degraded", "capacity"), null;
|
|
1766
|
+
this.frozenCriticalRuns = a, this.initialRevealStartedAt = t, this.initialRevealPhase = "holding", this.publishInitialRevealProgress(a);
|
|
1767
|
+
} else {
|
|
1768
|
+
for (const l of [0, 1, 2]) {
|
|
1769
|
+
const c = this.coarsenHomeRuns(r, l);
|
|
1770
|
+
if (c.length !== 0) {
|
|
1771
|
+
if (this.criticalRunsFitCapacity(c)) {
|
|
1772
|
+
a = c;
|
|
1773
|
+
break;
|
|
1774
|
+
}
|
|
1775
|
+
a = c;
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
if (a.length === 0)
|
|
1779
|
+
return this.initialRevealStateValue = { status: "ready" }, this.initialRevealPhase = "released", null;
|
|
1780
|
+
if (!this.criticalRunsFitCapacity(a))
|
|
1781
|
+
return this.frozenCriticalRuns = a, this.releaseInitialReveal("degraded", "capacity"), null;
|
|
1782
|
+
this.frozenCriticalRuns = a, this.initialRevealStartedAt = t, this.initialRevealPhase = "holding", this.publishInitialRevealProgress(a);
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
if (this.initialRevealPhase !== "holding" || !this.frozenCriticalRuns) return null;
|
|
1786
|
+
if (this.initialRevealStartedAt !== void 0 && t - this.initialRevealStartedAt >= ct)
|
|
1787
|
+
return this.releaseInitialReveal("degraded", "timeout"), null;
|
|
1788
|
+
for (const r of this.frozenCriticalRuns)
|
|
1789
|
+
if (this.failedFiles.has(r.file) && !this.resident.has(C(r))) {
|
|
1790
|
+
const a = this.staged.get(C(r));
|
|
1791
|
+
if (!a || a.uploadedCount !== r.count)
|
|
1792
|
+
return this.releaseInitialReveal("degraded", "fetch-failed"), null;
|
|
1793
|
+
}
|
|
1794
|
+
return this.publishInitialRevealProgress(this.frozenCriticalRuns), this.frozenCriticalRuns;
|
|
1795
|
+
}
|
|
1796
|
+
/** After staging/commits, release the hold when every frozen run is resident. */
|
|
1797
|
+
finishInitialRevealIfComplete() {
|
|
1798
|
+
this.initialRevealPhase !== "holding" || !this.frozenCriticalRuns || (this.publishInitialRevealProgress(this.frozenCriticalRuns), !this.environmentPendingForReveal() && this.frozenCriticalRuns.every((e) => this.resident.has(C(e))) && this.releaseInitialReveal("ready"));
|
|
1799
|
+
}
|
|
1800
|
+
/**
|
|
1801
|
+
* Startup hold still needs the environment tile when the scene ships one
|
|
1802
|
+
* and it starts enabled. Failed / unfit / disabled tiles do not block reveal.
|
|
1803
|
+
*/
|
|
1804
|
+
environmentPendingForReveal() {
|
|
1805
|
+
return this.envFile !== void 0 && this.envEnabled && !this.envUnfit && this.envHandle === void 0 && !this.failedFiles.has(this.envFile);
|
|
1806
|
+
}
|
|
1807
|
+
/** Creates a performance event for a changed streamed-LOD tick, if any. */
|
|
1808
|
+
createPerformanceEvent(e, t, s, n) {
|
|
1809
|
+
let r = 0, a = 0;
|
|
1810
|
+
for (const [u, g] of this.resident)
|
|
1811
|
+
a += g.run.count, e.has(u) || (r += g.run.count);
|
|
1812
|
+
let l = 0;
|
|
1813
|
+
for (const [u, g] of e)
|
|
1814
|
+
this.resident.has(u) || (l += g);
|
|
1815
|
+
let c = 0;
|
|
1816
|
+
for (const [u, g] of this.staged)
|
|
1817
|
+
c += Math.max(0, g.uploadedCount - (t.get(u) ?? 0));
|
|
1818
|
+
const h = this.compactionCount !== s;
|
|
1819
|
+
if (r === 0 && l === 0 && c === 0 && !h) return null;
|
|
1820
|
+
const f = performance.now();
|
|
1821
|
+
return {
|
|
1822
|
+
timestamp: f,
|
|
1823
|
+
cpuMs: f - n,
|
|
1824
|
+
activeListMs: 0,
|
|
1825
|
+
uploadMs: 0,
|
|
1826
|
+
sortSubmitMs: 0,
|
|
1827
|
+
stagingTextureAllocations: 0,
|
|
1828
|
+
activeListUpdateRanges: 0,
|
|
1829
|
+
appendedCount: r,
|
|
1830
|
+
removedCount: l,
|
|
1831
|
+
stagedCount: c,
|
|
1832
|
+
uploadCount: r + c,
|
|
1833
|
+
activeCount: a,
|
|
1834
|
+
forcedSort: a === 0 || r + l >= a * ht,
|
|
1835
|
+
compacted: h
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
/** Returns whether all new rows can coexist with the currently visible region. */
|
|
1839
|
+
canStageGroup(e) {
|
|
1840
|
+
return e.adds.filter((s) => !this.staged.has(C(s))).reduce((s, n) => s + this.rowAlignedSplats(n.count), 0) <= this.freeSplatCapacity;
|
|
1841
|
+
}
|
|
1842
|
+
/**
|
|
1843
|
+
* Uploads a bounded part of a replacement without rendering it.
|
|
1844
|
+
* Skips runs whose chunks are not yet cached so siblings can stage out of order.
|
|
1845
|
+
*/
|
|
1846
|
+
stageGroup(e, t, s) {
|
|
1847
|
+
if (s <= 0) return 0;
|
|
1848
|
+
let n = 0;
|
|
1849
|
+
for (const r of e.adds) {
|
|
1850
|
+
const a = C(r), l = this.cache.get(r.file);
|
|
1851
|
+
if (!l) continue;
|
|
1852
|
+
let c = this.staged.get(a);
|
|
1853
|
+
if (!c) {
|
|
1854
|
+
let f;
|
|
1855
|
+
try {
|
|
1856
|
+
f = this.reserveInactiveRange(r.count);
|
|
1857
|
+
} catch {
|
|
1858
|
+
this.compactionCount++, this.compact();
|
|
1859
|
+
try {
|
|
1860
|
+
f = this.reserveInactiveRange(r.count);
|
|
1861
|
+
} catch {
|
|
1862
|
+
this.pendingWork = !0;
|
|
1863
|
+
break;
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
c = { run: r, handle: f, uploadedCount: 0 }, this.staged.set(a, c);
|
|
1867
|
+
}
|
|
1868
|
+
if (c.uploadedCount === r.count) continue;
|
|
1869
|
+
const h = Math.min(r.count - c.uploadedCount, s - n);
|
|
1870
|
+
if (h <= 0) break;
|
|
1871
|
+
if (this.writeInactiveRange(
|
|
1872
|
+
c.handle,
|
|
1873
|
+
K(l.data, r.offset + c.uploadedCount, h),
|
|
1874
|
+
c.uploadedCount
|
|
1875
|
+
), c.uploadedCount += h, l.lastUsed = t, n += h, c.uploadedCount === r.count) {
|
|
1876
|
+
this.writeLodLevelChannel(c.handle, r.level);
|
|
1877
|
+
for (const [f, u] of this.persistentChannels)
|
|
1878
|
+
this.applyPersistentRun(f, u, r, c.handle);
|
|
1879
|
+
}
|
|
1880
|
+
if (n >= s) break;
|
|
1881
|
+
}
|
|
1882
|
+
return n;
|
|
1883
|
+
}
|
|
1884
|
+
/** Switches a fully staged region from old to new visibility in one tick. */
|
|
1885
|
+
commitStagedGroup(e) {
|
|
1886
|
+
for (const t of e.adds) {
|
|
1887
|
+
const s = this.staged.get(C(t));
|
|
1888
|
+
if (!s || s.uploadedCount !== t.count)
|
|
1889
|
+
throw new Error("StreamedSplatMesh: incomplete staged group commit.");
|
|
1890
|
+
}
|
|
1891
|
+
for (const [t, s] of e.removes)
|
|
1892
|
+
this.resident.has(t) && (this.removeRange(s.handle), this.resident.delete(t));
|
|
1893
|
+
for (const t of e.adds) {
|
|
1894
|
+
const s = C(t), n = this.staged.get(s);
|
|
1895
|
+
this.setRangeActive(n.handle, !0), this.resident.set(s, n), this.staged.delete(s);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
/**
|
|
1899
|
+
* Applies one swap group atomically within this tick: removals first
|
|
1900
|
+
* (freeing pool rows for the replacements), then all adds. Returns false
|
|
1901
|
+
* without touching anything when the group cannot fit even after its own
|
|
1902
|
+
* removals - the caller defers it and the old runs keep rendering.
|
|
1903
|
+
*/
|
|
1904
|
+
applyGroup(e, t) {
|
|
1905
|
+
const s = (a) => Math.ceil(a / F) * F, n = e.adds.reduce((a, l) => a + s(l.count), 0), r = e.removes.reduce((a, [, l]) => a + s(l.run.count), 0);
|
|
1906
|
+
if (n > this.freeSplatCapacity + r) return !1;
|
|
1907
|
+
for (const [a, l] of e.removes)
|
|
1908
|
+
this.resident.has(a) && (this.removeRange(l.handle), this.resident.delete(a));
|
|
1909
|
+
for (const a of e.adds)
|
|
1910
|
+
this.appendRun(a, t);
|
|
1911
|
+
return !0;
|
|
1912
|
+
}
|
|
1913
|
+
/** Appends one run from the cache, compacting the pool on fragmentation. */
|
|
1914
|
+
appendRun(e, t) {
|
|
1915
|
+
const s = this.cache.get(e.file);
|
|
1916
|
+
if (!s) return;
|
|
1917
|
+
const n = K(s.data, e.offset, e.count);
|
|
1918
|
+
let r;
|
|
1919
|
+
try {
|
|
1920
|
+
r = this.appendRange(n);
|
|
1921
|
+
} catch {
|
|
1922
|
+
this.compactionCount++, this.compact();
|
|
1923
|
+
try {
|
|
1924
|
+
r = this.appendRange(n);
|
|
1925
|
+
} catch {
|
|
1926
|
+
this.pendingWork = !0;
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
this.resident.set(C(e), { run: e, handle: r }), s.lastUsed = t, this.writeLodLevelChannel(r, e.level);
|
|
1931
|
+
for (const [a, l] of this.persistentChannels)
|
|
1932
|
+
this.applyPersistentRun(a, l, e, r);
|
|
1933
|
+
}
|
|
1934
|
+
/**
|
|
1935
|
+
* Writes the stored edits for one run's `[offset, offset + count)` splats
|
|
1936
|
+
* into its freshly appended pool range. No-op when the file has no edits.
|
|
1937
|
+
*/
|
|
1938
|
+
applyPersistentRun(e, t, s, n) {
|
|
1939
|
+
const r = t.edits.get(s.file);
|
|
1940
|
+
if (!r || r.size === 0) return;
|
|
1941
|
+
const a = t.type === "byte" ? new Uint8Array(s.count) : new Float32Array(s.count);
|
|
1942
|
+
t.fill && a.fill(t.fill);
|
|
1943
|
+
let l = !1;
|
|
1944
|
+
for (let c = 0; c < s.count; c++) {
|
|
1945
|
+
const h = r.get(s.offset + c);
|
|
1946
|
+
h !== void 0 && (a[c] = h, l = !0);
|
|
1947
|
+
}
|
|
1948
|
+
l && this.writeChannel(n, e, a);
|
|
1949
|
+
}
|
|
1950
|
+
/**
|
|
1951
|
+
* Covers a deferred group's leaves that no resident run covers with each
|
|
1952
|
+
* leaf's coarsest (pinned, hence cached) level, so a region waiting on a
|
|
1953
|
+
* fetch shows coarse detail instead of nothing. The substitutes are
|
|
1954
|
+
* intentionally not "desired": the next reschedule swaps them for the
|
|
1955
|
+
* real level once its chunk has arrived.
|
|
1956
|
+
*
|
|
1957
|
+
* Near-camera refinements (`distance <= lodBaseDistance`) skip the coarse
|
|
1958
|
+
* paint and its pin fetch entirely - cold load requests only the target cut
|
|
1959
|
+
* for that cell. Far gaps keep the shell.
|
|
1960
|
+
*/
|
|
1961
|
+
substituteCoverage(e, t, s, n) {
|
|
1962
|
+
const r = e.leafEnd - e.leafStart;
|
|
1963
|
+
(this.coverageScratch === void 0 || this.coverageScratch.length < r) && (this.coverageScratch = new Uint8Array(r));
|
|
1964
|
+
const a = this.coverageScratch;
|
|
1965
|
+
a.fill(0, 0, r);
|
|
1966
|
+
for (const { run: h } of this.resident.values()) {
|
|
1967
|
+
const f = Math.max(h.leafStart, e.leafStart), u = Math.min(h.leafEnd, e.leafEnd);
|
|
1968
|
+
u > f && a.fill(1, f - e.leafStart, u - e.leafStart);
|
|
1969
|
+
}
|
|
1970
|
+
const l = a.subarray(0, r);
|
|
1971
|
+
let c = 0;
|
|
1972
|
+
for (; c < r; ) {
|
|
1973
|
+
const h = l.indexOf(0, c);
|
|
1974
|
+
if (h < 0) break;
|
|
1975
|
+
const f = l.indexOf(1, h), u = f < 0 ? r : f, g = e.leafStart + h, v = e.leafStart + u;
|
|
1976
|
+
for (const m of this.scene.source.coarsestRunsFor(g, v))
|
|
1977
|
+
if (!this.resident.has(C(m))) {
|
|
1978
|
+
if (n) {
|
|
1979
|
+
this.fetchCountsValue.uncovered += Math.min(m.leafEnd, v) - Math.max(m.leafStart, g);
|
|
1980
|
+
continue;
|
|
1981
|
+
}
|
|
1982
|
+
if (!this.cache.has(m.file)) {
|
|
1983
|
+
oe(
|
|
1984
|
+
s,
|
|
1985
|
+
m.file,
|
|
1986
|
+
je(m, this.scene.source.lodBaseDistance),
|
|
1987
|
+
m
|
|
1988
|
+
), this.fetchCountsValue.uncovered += Math.min(m.leafEnd, v) - Math.max(m.leafStart, g);
|
|
1989
|
+
continue;
|
|
1990
|
+
}
|
|
1991
|
+
for (const [b, T] of this.resident)
|
|
1992
|
+
T.run.leafStart < m.leafEnd && T.run.leafEnd > m.leafStart && (this.removeRange(T.handle), this.resident.delete(b));
|
|
1993
|
+
this.appendRun(m, t);
|
|
1994
|
+
}
|
|
1995
|
+
c = u;
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
/** Issues pending classic-path chunk wants in group-priority order. */
|
|
1999
|
+
flushClassicFetches(e, t, s = !1) {
|
|
2000
|
+
if (e.size === 0) return;
|
|
2001
|
+
$e(e, t, s);
|
|
2002
|
+
const n = [...e.entries()].sort(
|
|
2003
|
+
(r, a) => X(r[1], a[1], r[0], a[0])
|
|
2004
|
+
);
|
|
2005
|
+
this.preemptClassicFetches(n);
|
|
2006
|
+
for (const [r, a] of n) this.requestChunk(r, a.kind, a);
|
|
2007
|
+
}
|
|
2008
|
+
/**
|
|
2009
|
+
* A camera turn must not wait for all eight old visible requests to finish.
|
|
2010
|
+
* Only classic requests carry a precise rank; page-table work retains its
|
|
2011
|
+
* own scheduler and is never cancelled here.
|
|
2012
|
+
*/
|
|
2013
|
+
preemptClassicFetches(e) {
|
|
2014
|
+
var t;
|
|
2015
|
+
for (const [s, n] of e) {
|
|
2016
|
+
if (this.cache.has(s) || this.fetching.has(s)) continue;
|
|
2017
|
+
let r, a;
|
|
2018
|
+
for (const [l, c] of this.fetching)
|
|
2019
|
+
c.classicWant && (!a || X(c.classicWant, a, l, r) > 0) && (r = l, a = c.classicWant);
|
|
2020
|
+
if (a && r !== void 0 && X(n, a, s, r) < 0 && ((t = this.fetching.get(r)) == null || t.controller.abort()), this.fetching.size >= this.maxInflight) return;
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
/**
|
|
2024
|
+
* Loads the always-resident environment tile once, on the first update after
|
|
2025
|
+
* it is wanted. The tile has no LOD ladder and no manifest count, so it is
|
|
2026
|
+
* appended whole (measuring its splat count at decode) and thereafter toggled
|
|
2027
|
+
* by flipping its pool range active - never scheduled, refetched, or evicted.
|
|
2028
|
+
* When `pending` is supplied, a miss is ranked as an `'environment'` want so
|
|
2029
|
+
* it issues ahead of LOD coverage.
|
|
2030
|
+
*/
|
|
2031
|
+
updateEnvironment(e, t) {
|
|
2032
|
+
const s = this.envFile;
|
|
2033
|
+
if (s === void 0 || this.envHandle !== void 0 || !this.envEnabled || this.envUnfit)
|
|
2034
|
+
return;
|
|
2035
|
+
const n = this.cache.get(s);
|
|
2036
|
+
if (!n) {
|
|
2037
|
+
this.failedFiles.has(s) || (t ? this.enqueueEnvironmentFetch(t, s) : this.requestChunk(s, "priority"), this.pendingWork = !0);
|
|
2038
|
+
return;
|
|
2039
|
+
}
|
|
2040
|
+
const r = Math.ceil(n.data.count / F) * F;
|
|
2041
|
+
if (r > this.capacity) {
|
|
2042
|
+
this.envUnfit = !0, P(
|
|
2043
|
+
`the environment tile (${n.data.count} splats) exceeds the pool capacity (${this.capacity}); it will not be shown. Raise the splat budget to fit it.`
|
|
2044
|
+
);
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
2047
|
+
if (r > this.freeSplatCapacity) {
|
|
2048
|
+
this.pendingWork = !0;
|
|
2049
|
+
return;
|
|
2050
|
+
}
|
|
2051
|
+
let a;
|
|
2052
|
+
try {
|
|
2053
|
+
a = this.appendRange(n.data);
|
|
2054
|
+
} catch {
|
|
2055
|
+
this.compactionCount++, this.compact();
|
|
2056
|
+
try {
|
|
2057
|
+
a = this.appendRange(n.data);
|
|
2058
|
+
} catch {
|
|
2059
|
+
this.pendingWork = !0;
|
|
2060
|
+
return;
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
this.envHandle = a, this.envSplatCount = n.data.count, n.lastUsed = e;
|
|
2064
|
+
}
|
|
2065
|
+
/** Ranks the env tile ahead of every LOD want in {@link flushClassicFetches}. */
|
|
2066
|
+
enqueueEnvironmentFetch(e, t) {
|
|
2067
|
+
e.has(t) || e.set(t, {
|
|
2068
|
+
kind: "priority",
|
|
2069
|
+
phase: "environment",
|
|
2070
|
+
distance: 0,
|
|
2071
|
+
level: 0,
|
|
2072
|
+
inView: !0,
|
|
2073
|
+
coverageGroup: -1,
|
|
2074
|
+
leafStart: 0,
|
|
2075
|
+
leafEnd: 0,
|
|
2076
|
+
screenImportance: Number.NEGATIVE_INFINITY,
|
|
2077
|
+
groupDistance: 0,
|
|
2078
|
+
groupPending: 1,
|
|
2079
|
+
groupInView: !0,
|
|
2080
|
+
groupScreenImportance: Number.NEGATIVE_INFINITY,
|
|
2081
|
+
groupFinest: !0,
|
|
2082
|
+
groupId: "environment",
|
|
2083
|
+
groupClass: 0
|
|
2084
|
+
});
|
|
2085
|
+
}
|
|
2086
|
+
/**
|
|
2087
|
+
* Page-table reschedule (`foveationMode: 'page-table'`): posts the camera to the
|
|
2088
|
+
* worker, which owns the cache + traversal + pager and replies asynchronously
|
|
2089
|
+
* with a paging plan. Coalesced to one outstanding request so the main thread
|
|
2090
|
+
* never blocks. Also drives chunk fetching, in priority order.
|
|
2091
|
+
*/
|
|
2092
|
+
reschedulePageTable(e, t, s, n) {
|
|
2093
|
+
for (const a of this.pageTableFetchPriority) this.requestChunk(a, "priority");
|
|
2094
|
+
const r = new Set(this.pageTableFetchPriority);
|
|
2095
|
+
for (const a of this.scene.source.computeDesiredRuns(e, s, n))
|
|
2096
|
+
r.add(a.file), this.requestChunk(a.file, "base");
|
|
2097
|
+
for (const [a, l] of this.fetching)
|
|
2098
|
+
l.kind !== "sweep" && !r.has(a) && !this.scene.pinnedFiles.has(a) && l.controller.abort();
|
|
2099
|
+
if (!this.pageTableCacheAtLimit && this.sweepAllowed()) {
|
|
2100
|
+
const a = Math.max(1, this.maxInflight - mt), l = this.scene.chunkUrls.length;
|
|
2101
|
+
for (let c = 0; c < l && this.fetching.size < a; c++)
|
|
2102
|
+
this.pageTableCachedFiles.has(c) || this.requestChunk(c, "sweep");
|
|
2103
|
+
}
|
|
2104
|
+
this.pageTableInFlight || (this.pageTableInFlight = !0, this.postToWorker({
|
|
2105
|
+
type: "reschedule",
|
|
2106
|
+
seq: ++this.pageTableSeq,
|
|
2107
|
+
cameraLocal: [e.x, e.y, e.z],
|
|
2108
|
+
cameraForward: [t.x, t.y, t.z],
|
|
2109
|
+
...this.pageTableFoveation,
|
|
2110
|
+
// Spark's cut is `pixel_scale × lodScale ≤ limit`, and the traversal only
|
|
2111
|
+
// ever sees one side of that - so scaling the limit down by `lodScale` is
|
|
2112
|
+
// the same comparison, with no protocol change.
|
|
2113
|
+
limit: this.pageTableLimit / this.lodScaleValue,
|
|
2114
|
+
budget: this.pageTableDrawBudget
|
|
2115
|
+
}));
|
|
2116
|
+
}
|
|
2117
|
+
/**
|
|
2118
|
+
* Applies a paging plan from the worker to the slab - fast memcpy writes only,
|
|
2119
|
+
* no traversal or gather on the main thread - then fetches the chunks the
|
|
2120
|
+
* frontier wants next, and reschedules again if chunks are still streaming.
|
|
2121
|
+
*/
|
|
2122
|
+
applyFrontierPlan(e) {
|
|
2123
|
+
if (this.pageTableInFlight = !1, this.pageTableDisposed || this.slabPages.length === 0) return;
|
|
2124
|
+
const t = this.pagerSlots;
|
|
2125
|
+
e.capacity !== t && (this.pendingWork = !0, this.lastScheduleTime = -1 / 0);
|
|
2126
|
+
const s = performance.now(), n = e.moveSlots;
|
|
2127
|
+
for (let u = 0; u < n.length; ) {
|
|
2128
|
+
let g = 1;
|
|
2129
|
+
for (; u + g < n.length && n[u + g] === n[u] + g; )
|
|
2130
|
+
g++;
|
|
2131
|
+
const v = n[u], m = Math.min(g, t - v);
|
|
2132
|
+
m > 0 && this.writeSlabSlots(yt(e.moves, u, m), v, m), u += g;
|
|
2133
|
+
}
|
|
2134
|
+
if (e.appends.count > 0) {
|
|
2135
|
+
const u = Math.min(e.appends.count, t - e.appendStart);
|
|
2136
|
+
u > 0 && this.writeSlabSlots(e.appends, e.appendStart, u);
|
|
2137
|
+
}
|
|
2138
|
+
const r = performance.now(), a = Math.min(e.residentCount, t);
|
|
2139
|
+
this.setSlabResident(a);
|
|
2140
|
+
const l = Math.min(e.degenerateStart, t), c = Math.min(e.degenerateCount, t - l);
|
|
2141
|
+
c > 0 && this.degenerateSlabSlots(l, c);
|
|
2142
|
+
const h = performance.now();
|
|
2143
|
+
this.pageTableDrawn = a, e.gatherMissing > 0 && P(
|
|
2144
|
+
`StreamedSplatMesh: page-table plan gathered ${e.gatherMissing} splats from evicted chunks; they render as holes.`
|
|
2145
|
+
);
|
|
2146
|
+
const f = this.planTimingsValue;
|
|
2147
|
+
if (f.applyMs = h - s, f.writeMs = r - s, f.residentMs = h - r, f.moves = e.moveSlots.length, f.appends = e.appends.count, f.applyMs > f.worstApplyMs && (f.worstApplyMs = f.applyMs, f.worstSplats = f.moves + f.appends), this.frontierBandBase !== null && e.solvedLimit > 0 && this.pageTableLimit > 0) {
|
|
2148
|
+
const u = Math.min(1, e.solvedLimit / this.pageTableLimit);
|
|
2149
|
+
this.setScreenRadiusBand(
|
|
2150
|
+
this.frontierBandBase.min * u,
|
|
2151
|
+
this.frontierBandBase.max * u
|
|
2152
|
+
);
|
|
2153
|
+
}
|
|
2154
|
+
this.invalidateSort(), e.dropped > 0 && P(
|
|
2155
|
+
`StreamedSplatMesh: page-table slab full, dropped ${e.dropped} frontier splats (draw budget ${this.pageTableDrawBudget} exceeds the pool).`
|
|
2156
|
+
);
|
|
2157
|
+
for (let u = 0; u < e.evicted.length; u++)
|
|
2158
|
+
this.pageTableCachedFiles.delete(e.evicted[u]);
|
|
2159
|
+
this.fetchCountsValue.cacheBytes = e.cacheBytes, this.fetchCountsValue.cacheLimitBytes = e.cacheLimitBytes, this.pageTableCacheAtLimit = e.cacheBytes >= e.cacheLimitBytes, e.evicted.length > 0 && (this.fetchCountsValue.cacheFull = !0, this.fetchCountsValue.evicted += e.evicted.length), this.pageTableFetchPriority = Array.from(e.touched);
|
|
2160
|
+
for (const u of this.pageTableFetchPriority) this.requestChunk(u, "priority");
|
|
2161
|
+
(this.fetching.size > 0 || !e.converged) && (this.pendingWork = !0, e.converged || (this.lastScheduleTime = -1 / 0));
|
|
2162
|
+
}
|
|
2163
|
+
/** Forwards a decoded chunk's arrays to the worker (buffers transferred) so the
|
|
2164
|
+
* worker's cache/traversal/gather can use it. */
|
|
2165
|
+
forwardChunkToWorker(e, t) {
|
|
2166
|
+
const s = t.radTree;
|
|
2167
|
+
if (!s) return;
|
|
2168
|
+
this.pageTableCachedFiles.add(e);
|
|
2169
|
+
const n = this.shBands > 0 ? t.shPacked : void 0;
|
|
2170
|
+
this.postToWorker(
|
|
2171
|
+
{
|
|
2172
|
+
type: "chunk",
|
|
2173
|
+
file: e,
|
|
2174
|
+
count: t.count,
|
|
2175
|
+
positions: t.positions,
|
|
2176
|
+
colors: t.colors,
|
|
2177
|
+
covariances: t.covariances,
|
|
2178
|
+
childCount: s.childCount,
|
|
2179
|
+
childStart: s.childStart,
|
|
2180
|
+
size: s.size,
|
|
2181
|
+
shBands: (n == null ? void 0 : n.bands) ?? 0,
|
|
2182
|
+
...n ? { shPacked: n.packed, shRange: n.range } : {}
|
|
2183
|
+
},
|
|
2184
|
+
[
|
|
2185
|
+
t.positions.buffer,
|
|
2186
|
+
t.colors.buffer,
|
|
2187
|
+
t.covariances.buffer,
|
|
2188
|
+
s.childCount.buffer,
|
|
2189
|
+
s.childStart.buffer,
|
|
2190
|
+
s.size.buffer,
|
|
2191
|
+
...n ? [n.packed.buffer] : []
|
|
2192
|
+
]
|
|
2193
|
+
);
|
|
2194
|
+
}
|
|
2195
|
+
/** Parallel chunk fetches. HTTP/2 multiplexes them; on HTTP/1.1 the browser's
|
|
2196
|
+
* per-host cap simply queues. Same cap for classic and page-table so near
|
|
2197
|
+
* detail is not structurally starved on the non-page-table path. */
|
|
2198
|
+
get maxInflight() {
|
|
2199
|
+
return ut;
|
|
2200
|
+
}
|
|
2201
|
+
/**
|
|
2202
|
+
* Whether this mesh may run its speculative background sweep. A mesh with no
|
|
2203
|
+
* weight is hidden or suspended; a mesh with no `fetchWeight` at all is a
|
|
2204
|
+
* host that never asked for arbitration, and keeps the old behaviour.
|
|
2205
|
+
*/
|
|
2206
|
+
/**
|
|
2207
|
+
* Sets this mesh's share of the scene's fetch bandwidth, as
|
|
2208
|
+
* {@link StreamedSplatMeshOptions.fetchWeight} does at load.
|
|
2209
|
+
*
|
|
2210
|
+
* The weight normally closes over the mesh itself (`() =>
|
|
2211
|
+
* governor.weightOf(mesh)`), which a host cannot express until `load`
|
|
2212
|
+
* resolves - hence a setter as well as an option. Pass `undefined` to go back
|
|
2213
|
+
* to unarbitrated sweeping.
|
|
2214
|
+
*/
|
|
2215
|
+
setFetchWeight(e) {
|
|
2216
|
+
this.fetchWeight = e;
|
|
2217
|
+
}
|
|
2218
|
+
sweepAllowed() {
|
|
2219
|
+
if (this.performanceProfile === "smooth") return !1;
|
|
2220
|
+
if (this.fetchWeight === void 0) return !0;
|
|
2221
|
+
const e = this.fetchWeight();
|
|
2222
|
+
return Number.isFinite(e) && e > 0;
|
|
2223
|
+
}
|
|
2224
|
+
/** Aborts in-flight fetches of one kind; their slots return through `finally`. */
|
|
2225
|
+
abortFetches(e) {
|
|
2226
|
+
for (const t of this.fetching.values())
|
|
2227
|
+
t.kind === e && t.controller.abort();
|
|
2228
|
+
}
|
|
2229
|
+
requestChunk(e, t, s) {
|
|
2230
|
+
var l, c;
|
|
2231
|
+
if (this.cache.has(e) || this.pageTableCachedFiles.has(e) || this.fetching.has(e) || this.fetching.size >= this.maxInflight || this.failedFiles.has(e)) return;
|
|
2232
|
+
const n = this.retrying.get(e);
|
|
2233
|
+
if (n && performance.now() < n.readyAt) return;
|
|
2234
|
+
this.fetchCountsValue[t]++;
|
|
2235
|
+
const r = this.scene.chunkUrls[e];
|
|
2236
|
+
if (r === void 0) {
|
|
2237
|
+
P(`StreamedSplatMesh: manifest references unknown chunk file #${e}.`), this.failedFiles.add(e);
|
|
2238
|
+
return;
|
|
2239
|
+
}
|
|
2240
|
+
if (this.fetchHandle && !((l = this.fetchScheduler) != null && l.tryAcquire(this.fetchHandle, t))) return;
|
|
2241
|
+
const a = new AbortController();
|
|
2242
|
+
this.fetching.set(e, { controller: a, kind: t, classicWant: s }), this.loader.load(r, {
|
|
2243
|
+
kind: this.scene.chunkKind,
|
|
2244
|
+
signal: a.signal,
|
|
2245
|
+
...(c = this.scene.chunkOptions) == null ? void 0 : c[e]
|
|
2246
|
+
}).then((h) => {
|
|
2247
|
+
var f, u;
|
|
2248
|
+
this.disposed || (this.retrying.delete(e), (u = (f = this.scene.source).onChunkDecoded) == null || u.call(f, e, h), this.frontierWorker ? (this.forwardChunkToWorker(e, h), this.pendingWork = !0) : this.cacheChunk(e, h));
|
|
2249
|
+
}).catch((h) => {
|
|
2250
|
+
var u;
|
|
2251
|
+
if (G(h)) return;
|
|
2252
|
+
const f = (((u = this.retrying.get(e)) == null ? void 0 : u.attempts) ?? 0) + 1;
|
|
2253
|
+
if (f >= gt)
|
|
2254
|
+
this.retrying.delete(e), this.failedFiles.add(e), P(
|
|
2255
|
+
`StreamedSplatMesh: gave up on chunk #${e} (${r}) after ${f} attempts.`,
|
|
2256
|
+
h
|
|
2257
|
+
);
|
|
2258
|
+
else {
|
|
2259
|
+
const g = pt * 2 ** (f - 1);
|
|
2260
|
+
this.retrying.set(e, { attempts: f, readyAt: performance.now() + g });
|
|
2261
|
+
}
|
|
2262
|
+
}).finally(() => {
|
|
2263
|
+
var h;
|
|
2264
|
+
this.fetching.delete(e), this.fetchHandle && ((h = this.fetchScheduler) == null || h.release(this.fetchHandle)), this.pendingWork = !0;
|
|
2265
|
+
});
|
|
2266
|
+
}
|
|
2267
|
+
/**
|
|
2268
|
+
* Stores a decoded chunk while keeping {@link cacheBytesTotal} in step. The
|
|
2269
|
+
* counter replaces a full-cache re-sum on every reschedule; every mutation
|
|
2270
|
+
* of {@link cache} (this method, eviction, dispose's clear) maintains it.
|
|
2271
|
+
*/
|
|
2272
|
+
cacheChunk(e, t) {
|
|
2273
|
+
const s = this.cache.get(e);
|
|
2274
|
+
s !== void 0 && (this.cacheBytesTotal -= s.bytes);
|
|
2275
|
+
const n = Xe(t);
|
|
2276
|
+
this.cache.set(e, { data: t, bytes: n, lastUsed: performance.now() }), this.cacheBytesTotal += n;
|
|
2277
|
+
}
|
|
2278
|
+
evictChunks(e) {
|
|
2279
|
+
let t = this.cacheBytesTotal;
|
|
2280
|
+
if (t <= this.cpuCacheBytes) return;
|
|
2281
|
+
const s = [...this.cache.entries()].filter(
|
|
2282
|
+
([n, r]) => r.lastUsed !== e && !this.scene.pinnedFiles.has(n) && !this.neededFiles.has(n)
|
|
2283
|
+
).sort((n, r) => n[1].lastUsed - r[1].lastUsed);
|
|
2284
|
+
for (const [n, r] of s) {
|
|
2285
|
+
if (t <= this.cpuCacheBytes) break;
|
|
2286
|
+
this.cache.delete(n), this.cacheBytesTotal -= r.bytes, t -= r.bytes, this.fetchCountsValue.evicted++;
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
const V = new w.Vector3(), pe = new w.Vector3(), me = new w.Quaternion(), x = new w.Vector3(), ve = new w.Matrix4(), z = new w.Frustum(), wt = new w.Sphere(), B = new w.Vector3(), Se = new w.Vector2();
|
|
2291
|
+
function be(o) {
|
|
2292
|
+
return Math.ceil(3 * Ie(o) / 4);
|
|
2293
|
+
}
|
|
2294
|
+
function yt(o, i, e) {
|
|
2295
|
+
const t = o.shPacked;
|
|
2296
|
+
return {
|
|
2297
|
+
count: e,
|
|
2298
|
+
positions: o.positions.subarray(i * 3, (i + e) * 3),
|
|
2299
|
+
colors: o.colors.subarray(i * 4, (i + e) * 4),
|
|
2300
|
+
covariances: o.covariances.subarray(i * 6, (i + e) * 6),
|
|
2301
|
+
...t ? {
|
|
2302
|
+
shPacked: {
|
|
2303
|
+
...t,
|
|
2304
|
+
packed: t.packed.subarray(
|
|
2305
|
+
i * be(t.bands),
|
|
2306
|
+
(i + e) * be(t.bands)
|
|
2307
|
+
)
|
|
2308
|
+
}
|
|
2309
|
+
} : {}
|
|
2310
|
+
};
|
|
2311
|
+
}
|
|
2312
|
+
const Ct = 1;
|
|
2313
|
+
class Tt {
|
|
2314
|
+
constructor(i = {}) {
|
|
2315
|
+
d(this, "entries", /* @__PURE__ */ new Map());
|
|
2316
|
+
d(this, "total");
|
|
2317
|
+
d(this, "hysteresis");
|
|
2318
|
+
this.total = W(i.totalBudget);
|
|
2319
|
+
const e = i.hysteresis ?? 0.1;
|
|
2320
|
+
if (!Number.isFinite(e) || e < 0)
|
|
2321
|
+
throw new RangeError("BudgetGovernor hysteresis must be a non-negative finite number.");
|
|
2322
|
+
this.hysteresis = e;
|
|
2323
|
+
}
|
|
2324
|
+
/** The shared budget currently being split across members. */
|
|
2325
|
+
get totalBudget() {
|
|
2326
|
+
return this.total;
|
|
2327
|
+
}
|
|
2328
|
+
/** Replaces the shared total and reallocates. */
|
|
2329
|
+
setTotalBudget(i) {
|
|
2330
|
+
const e = W(i);
|
|
2331
|
+
e !== this.total && (this.total = e, this.reallocate());
|
|
2332
|
+
}
|
|
2333
|
+
/** Number of registered members. */
|
|
2334
|
+
get size() {
|
|
2335
|
+
return this.entries.size;
|
|
2336
|
+
}
|
|
2337
|
+
/**
|
|
2338
|
+
* Adds a member and reallocates the shared budget. The member's current
|
|
2339
|
+
* budget is remembered and restored when it leaves the governor.
|
|
2340
|
+
*
|
|
2341
|
+
* @param member - The mesh (or compatible object) to govern.
|
|
2342
|
+
* @param options - `weight` (default `1`): the member's share is
|
|
2343
|
+
* proportional to its weight - e.g. main mesh `7`, additional mesh `3` reproduces
|
|
2344
|
+
* the old 0.7 host split. `0` registers the member suspended.
|
|
2345
|
+
*/
|
|
2346
|
+
register(i, e = {}) {
|
|
2347
|
+
if (this.entries.has(i))
|
|
2348
|
+
throw new Error("BudgetGovernor: member is already registered.");
|
|
2349
|
+
const t = J(e.weight ?? 1);
|
|
2350
|
+
this.entries.set(i, {
|
|
2351
|
+
member: i,
|
|
2352
|
+
weight: t,
|
|
2353
|
+
applied: i.budget,
|
|
2354
|
+
restoreBudget: i.budget
|
|
2355
|
+
}), this.reallocate();
|
|
2356
|
+
}
|
|
2357
|
+
/**
|
|
2358
|
+
* Removes a member, restores the budget it had when it registered, and
|
|
2359
|
+
* reallocates the total across the remaining members. No-op for a member
|
|
2360
|
+
* that is not registered (so disposing hosts need not track membership).
|
|
2361
|
+
*/
|
|
2362
|
+
unregister(i) {
|
|
2363
|
+
const e = this.entries.get(i);
|
|
2364
|
+
e !== void 0 && (this.entries.delete(i), e.member.setBudget(e.restoreBudget), this.reallocate());
|
|
2365
|
+
}
|
|
2366
|
+
/**
|
|
2367
|
+
* Changes a member's priority weight and reallocates. `0` suspends the
|
|
2368
|
+
* member (see the class doc); any positive weight resumes it.
|
|
2369
|
+
*/
|
|
2370
|
+
setWeight(i, e) {
|
|
2371
|
+
const t = this.entryOf(i), s = J(e);
|
|
2372
|
+
s !== t.weight && (t.weight = s, this.reallocate());
|
|
2373
|
+
}
|
|
2374
|
+
/**
|
|
2375
|
+
* Writes several weights, then reallocates **once**.
|
|
2376
|
+
*
|
|
2377
|
+
* Prefer this to a loop of {@link setWeight} whenever more than one weight
|
|
2378
|
+
* changes together - as a camera-driven reweight does. Each reallocation
|
|
2379
|
+
* pushes `setBudget` to every member, and for a streamed mesh that forces an
|
|
2380
|
+
* LOD reschedule, so N separate calls cost N passes over the whole group to
|
|
2381
|
+
* reach a state one pass would have produced.
|
|
2382
|
+
*
|
|
2383
|
+
* @param weights - `[member, weight]` pairs. Every member must be registered;
|
|
2384
|
+
* unlisted members keep their current weight.
|
|
2385
|
+
* @throws {Error} if any member is not registered - checked before anything
|
|
2386
|
+
* is written, so a bad pair leaves every weight untouched.
|
|
2387
|
+
* @throws {RangeError} if any weight is not a non-negative finite number.
|
|
2388
|
+
*/
|
|
2389
|
+
setWeights(i) {
|
|
2390
|
+
const e = [...i].map(
|
|
2391
|
+
([s, n]) => [this.entryOf(s), J(n)]
|
|
2392
|
+
);
|
|
2393
|
+
let t = !1;
|
|
2394
|
+
for (const [s, n] of e)
|
|
2395
|
+
s.weight !== n && (s.weight = n, t = !0);
|
|
2396
|
+
t && this.reallocate();
|
|
2397
|
+
}
|
|
2398
|
+
/** The budget the governor last applied to a member, if registered. */
|
|
2399
|
+
budgetOf(i) {
|
|
2400
|
+
var e;
|
|
2401
|
+
return (e = this.entries.get(i)) == null ? void 0 : e.applied;
|
|
2402
|
+
}
|
|
2403
|
+
/**
|
|
2404
|
+
* Recomputes and applies every member's share. Called automatically by all
|
|
2405
|
+
* mutators; call it manually only if a member's internal ceiling changed
|
|
2406
|
+
* outside the governor's view.
|
|
2407
|
+
*/
|
|
2408
|
+
reallocate() {
|
|
2409
|
+
let i = [];
|
|
2410
|
+
for (const t of this.entries.values())
|
|
2411
|
+
t.weight === 0 ? this.applyTarget(t, Ct) : i.push(t);
|
|
2412
|
+
let e = this.total;
|
|
2413
|
+
for (; i.length > 0; ) {
|
|
2414
|
+
const t = i.reduce((r, a) => r + a.weight, 0), s = [];
|
|
2415
|
+
let n = 0;
|
|
2416
|
+
for (const r of i) {
|
|
2417
|
+
const a = Math.max(1, Math.floor(e * r.weight / t));
|
|
2418
|
+
this.applyTarget(r, a) && (s.push(r), n += r.applied);
|
|
2419
|
+
}
|
|
2420
|
+
if (s.length === 0) break;
|
|
2421
|
+
e = Math.max(0, e - n), i = i.filter((r) => !s.includes(r));
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
/**
|
|
2425
|
+
* Restores every member's pre-registration budget and empties the governor.
|
|
2426
|
+
* The governor itself stays usable (dispose is just "unregister everyone").
|
|
2427
|
+
*/
|
|
2428
|
+
dispose() {
|
|
2429
|
+
for (const i of this.entries.values())
|
|
2430
|
+
i.member.setBudget(i.restoreBudget);
|
|
2431
|
+
this.entries.clear();
|
|
2432
|
+
}
|
|
2433
|
+
/** The entry for a registered member, or a thrown error naming the problem. */
|
|
2434
|
+
entryOf(i) {
|
|
2435
|
+
const e = this.entries.get(i);
|
|
2436
|
+
if (e === void 0)
|
|
2437
|
+
throw new Error("BudgetGovernor: member is not registered.");
|
|
2438
|
+
return e;
|
|
2439
|
+
}
|
|
2440
|
+
/**
|
|
2441
|
+
* Pushes `target` to a member, with the grow dead-band. Returns true when
|
|
2442
|
+
* the member clamped below its target (it is capped and cannot absorb more).
|
|
2443
|
+
*/
|
|
2444
|
+
applyTarget(i, e) {
|
|
2445
|
+
if (e > i.applied) {
|
|
2446
|
+
if (e - i.applied <= this.hysteresis * i.applied) return !1;
|
|
2447
|
+
} else if (e === i.applied)
|
|
2448
|
+
return !1;
|
|
2449
|
+
return i.applied = i.member.setBudget(e), i.applied < e;
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
function J(o) {
|
|
2453
|
+
if (!Number.isFinite(o) || o < 0)
|
|
2454
|
+
throw new RangeError(
|
|
2455
|
+
"BudgetGovernor member weight must be a non-negative finite number (0 suspends the member)."
|
|
2456
|
+
);
|
|
2457
|
+
return o;
|
|
2458
|
+
}
|
|
2459
|
+
const we = new w.Vector3(), ye = new w.Matrix4(), Ce = new w.Frustum(), Z = new w.Box3(), O = new w.Sphere();
|
|
2460
|
+
class Ot {
|
|
2461
|
+
constructor(i = {}) {
|
|
2462
|
+
d(this, "entries", /* @__PURE__ */ new Map());
|
|
2463
|
+
d(this, "budgetGovernor");
|
|
2464
|
+
d(this, "minIntervalMs");
|
|
2465
|
+
d(this, "weightDeadband");
|
|
2466
|
+
d(this, "falloff");
|
|
2467
|
+
d(this, "offScreenWeight");
|
|
2468
|
+
d(this, "minWeight");
|
|
2469
|
+
d(this, "maxWeight");
|
|
2470
|
+
d(this, "lastUpdateAt", -1 / 0);
|
|
2471
|
+
/** Set by membership/priority changes: the next update ignores both damps. */
|
|
2472
|
+
d(this, "forceNext", !0);
|
|
2473
|
+
if (this.budgetGovernor = i.governor ?? new Tt(i), this.minIntervalMs = N(i.minIntervalMs ?? 250, "minIntervalMs"), this.weightDeadband = N(i.weightDeadband ?? 0.15, "weightDeadband"), this.falloff = U(i.falloff ?? 1, "falloff"), this.offScreenWeight = U(i.offScreenWeight ?? 0.25, "offScreenWeight"), this.minWeight = U(i.minWeight ?? 0.05, "minWeight"), this.maxWeight = U(i.maxWeight ?? 8, "maxWeight"), this.maxWeight < this.minWeight)
|
|
2474
|
+
throw new RangeError("CameraBudgetGovernor maxWeight must be >= minWeight.");
|
|
2475
|
+
}
|
|
2476
|
+
/** The governor this drives; use it for fixed-weight members and diagnostics. */
|
|
2477
|
+
get governor() {
|
|
2478
|
+
return this.budgetGovernor;
|
|
2479
|
+
}
|
|
2480
|
+
/** The shared total being split, from the underlying governor. */
|
|
2481
|
+
get totalBudget() {
|
|
2482
|
+
return this.budgetGovernor.totalBudget;
|
|
2483
|
+
}
|
|
2484
|
+
/** Replaces the shared total (e.g. the presenting budget on `sessionstart`). */
|
|
2485
|
+
setTotalBudget(i) {
|
|
2486
|
+
this.budgetGovernor.setTotalBudget(i);
|
|
2487
|
+
}
|
|
2488
|
+
/** Number of camera-weighted members. */
|
|
2489
|
+
get size() {
|
|
2490
|
+
return this.entries.size;
|
|
2491
|
+
}
|
|
2492
|
+
/**
|
|
2493
|
+
* Adds a member, registers it with the underlying governor, and forces a
|
|
2494
|
+
* reweight on the next {@link update}.
|
|
2495
|
+
*
|
|
2496
|
+
* @throws {Error} if the member is already registered here.
|
|
2497
|
+
* @throws {RangeError} if `priority` or `fixedWeight` is invalid.
|
|
2498
|
+
*/
|
|
2499
|
+
register(i, e = {}) {
|
|
2500
|
+
if (this.entries.has(i))
|
|
2501
|
+
throw new Error("CameraBudgetGovernor: member is already registered.");
|
|
2502
|
+
const t = N(e.priority ?? 1, "priority"), s = e.fixedWeight === void 0 ? void 0 : N(e.fixedWeight, "fixedWeight"), n = s ?? t;
|
|
2503
|
+
this.budgetGovernor.register(i, { weight: n }), this.entries.set(i, { member: i, priority: t, fixedWeight: s, applied: n }), this.forceNext = !0;
|
|
2504
|
+
}
|
|
2505
|
+
/**
|
|
2506
|
+
* Removes a member from this helper and the underlying governor, restoring
|
|
2507
|
+
* the budget it had when it joined. No-op for an unknown member.
|
|
2508
|
+
*/
|
|
2509
|
+
unregister(i) {
|
|
2510
|
+
this.entries.delete(i) && (this.budgetGovernor.unregister(i), this.forceNext = !0);
|
|
2511
|
+
}
|
|
2512
|
+
/**
|
|
2513
|
+
* Changes a member's priority tier. Takes effect on the next {@link update},
|
|
2514
|
+
* which is forced - a deliberate focus change should not wait out the
|
|
2515
|
+
* interval or be swallowed by the dead-band.
|
|
2516
|
+
*
|
|
2517
|
+
* @throws {Error} if the member is not registered here.
|
|
2518
|
+
* @throws {RangeError} if `priority` is negative or not finite.
|
|
2519
|
+
*/
|
|
2520
|
+
setPriority(i, e) {
|
|
2521
|
+
const t = this.entries.get(i);
|
|
2522
|
+
if (t === void 0)
|
|
2523
|
+
throw new Error("CameraBudgetGovernor: member is not registered.");
|
|
2524
|
+
const s = N(e, "priority");
|
|
2525
|
+
s !== t.priority && (t.priority = s, this.forceNext = !0);
|
|
2526
|
+
}
|
|
2527
|
+
/**
|
|
2528
|
+
* Recomputes every member's weight from the camera and applies them in one
|
|
2529
|
+
* batch. Call once per frame, after the scene graph is up to date.
|
|
2530
|
+
*
|
|
2531
|
+
* Needs no renderer: weights are a ratio, so viewport size cancels out.
|
|
2532
|
+
*
|
|
2533
|
+
* Skipped - returning `false` - when called inside `minIntervalMs` of the
|
|
2534
|
+
* last reweight, or when no member's weight moved by more than
|
|
2535
|
+
* `weightDeadband`. Membership and priority changes force it through both.
|
|
2536
|
+
*
|
|
2537
|
+
* @param camera - The view detail should follow. In an immersive session pass
|
|
2538
|
+
* the head/`ArrayCamera`, not the idle application camera.
|
|
2539
|
+
* @param now - Timestamp in ms on the `performance.now` clock; defaults to it.
|
|
2540
|
+
* @returns whether weights were reapplied.
|
|
2541
|
+
*/
|
|
2542
|
+
update(i, e = performance.now()) {
|
|
2543
|
+
if (this.entries.size === 0) return !1;
|
|
2544
|
+
const t = this.forceNext;
|
|
2545
|
+
if (!t && e - this.lastUpdateAt < this.minIntervalMs) return !1;
|
|
2546
|
+
i.updateMatrixWorld(), i.getWorldPosition(we), ye.multiplyMatrices(i.projectionMatrix, i.matrixWorldInverse), Ce.setFromProjectionMatrix(ye);
|
|
2547
|
+
const s = [];
|
|
2548
|
+
let n = !1;
|
|
2549
|
+
for (const r of this.entries.values()) {
|
|
2550
|
+
const a = this.weightFor(r);
|
|
2551
|
+
s.push([r.member, a]), !n && this.isSignificant(r.applied, a) && (n = !0);
|
|
2552
|
+
}
|
|
2553
|
+
if (!t && !n) return !1;
|
|
2554
|
+
this.budgetGovernor.setWeights(s);
|
|
2555
|
+
for (const [r, a] of s) {
|
|
2556
|
+
const l = this.entries.get(r);
|
|
2557
|
+
l !== void 0 && (l.applied = a);
|
|
2558
|
+
}
|
|
2559
|
+
return this.lastUpdateAt = e, this.forceNext = !1, !0;
|
|
2560
|
+
}
|
|
2561
|
+
/** The weight currently written for a member, if registered here. */
|
|
2562
|
+
weightOf(i) {
|
|
2563
|
+
var e;
|
|
2564
|
+
return (e = this.entries.get(i)) == null ? void 0 : e.applied;
|
|
2565
|
+
}
|
|
2566
|
+
/** The budget the underlying governor last applied to a member. */
|
|
2567
|
+
budgetOf(i) {
|
|
2568
|
+
return this.budgetGovernor.budgetOf(i);
|
|
2569
|
+
}
|
|
2570
|
+
/**
|
|
2571
|
+
* Unregisters every member, restoring the budget each had when it joined.
|
|
2572
|
+
* The helper stays usable afterwards. A governor passed in by the host keeps
|
|
2573
|
+
* any members the host registered on it directly.
|
|
2574
|
+
*/
|
|
2575
|
+
dispose() {
|
|
2576
|
+
for (const i of [...this.entries.keys()]) this.budgetGovernor.unregister(i);
|
|
2577
|
+
this.entries.clear(), this.lastUpdateAt = -1 / 0, this.forceNext = !0;
|
|
2578
|
+
}
|
|
2579
|
+
/**
|
|
2580
|
+
* A member's weight: `priority × clamp((radius / distance) ^ falloff) ×
|
|
2581
|
+
* offScreen`.
|
|
2582
|
+
*
|
|
2583
|
+
* `radius / distance` is the tangent of the member's half angular size - the
|
|
2584
|
+
* same `size / distance` measure Spark's `pixel_scale` traversal ranks nodes
|
|
2585
|
+
* by, one level up at whole-mesh granularity. Distance is measured to the
|
|
2586
|
+
* bounding sphere's *surface*, so a large mesh is not penalized for having a
|
|
2587
|
+
* distant center, and is floored so a camera inside the bounds saturates at
|
|
2588
|
+
* `maxWeight` rather than dividing by zero.
|
|
2589
|
+
*/
|
|
2590
|
+
weightFor(i) {
|
|
2591
|
+
if (i.fixedWeight !== void 0) return i.fixedWeight;
|
|
2592
|
+
const { member: e, priority: t } = i;
|
|
2593
|
+
if (t === 0 || !(e.effectiveVisibility ?? e.visible)) return 0;
|
|
2594
|
+
if (e.updateWorldMatrix(!0, !1), Z.copy(e.computeSplatBounds()), Z.isEmpty()) return t * this.minWeight;
|
|
2595
|
+
Z.applyMatrix4(e.matrixWorld).getBoundingSphere(O);
|
|
2596
|
+
const s = O.radius;
|
|
2597
|
+
if (!(s > 0)) return t * this.minWeight;
|
|
2598
|
+
const n = O.center.distanceTo(we) - s, r = Math.max(n, s * 1e-3, 1e-6), a = It((s / r) ** this.falloff, this.minWeight, this.maxWeight);
|
|
2599
|
+
if (!Number.isFinite(a)) return t * this.minWeight;
|
|
2600
|
+
const l = Ce.intersectsSphere(O) ? 1 : this.offScreenWeight;
|
|
2601
|
+
return t * a * l;
|
|
2602
|
+
}
|
|
2603
|
+
/** Whether a weight moved enough to be worth a reallocation. */
|
|
2604
|
+
isSignificant(i, e) {
|
|
2605
|
+
return i === 0 || e === 0 ? i !== e : Math.abs(e - i) > this.weightDeadband * i;
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
function It(o, i, e) {
|
|
2609
|
+
return Math.min(e, Math.max(i, o));
|
|
2610
|
+
}
|
|
2611
|
+
function N(o, i) {
|
|
2612
|
+
if (!Number.isFinite(o) || o < 0)
|
|
2613
|
+
throw new RangeError(`CameraBudgetGovernor ${i} must be a non-negative finite number.`);
|
|
2614
|
+
return o;
|
|
2615
|
+
}
|
|
2616
|
+
function U(o, i) {
|
|
2617
|
+
if (!Number.isFinite(o) || o <= 0)
|
|
2618
|
+
throw new RangeError(`CameraBudgetGovernor ${i} must be a positive finite number.`);
|
|
2619
|
+
return o;
|
|
2620
|
+
}
|
|
2621
|
+
const Rt = 16, Mt = 1;
|
|
2622
|
+
class Ut {
|
|
2623
|
+
constructor(i = {}) {
|
|
2624
|
+
d(this, "entries", /* @__PURE__ */ new Set());
|
|
2625
|
+
d(this, "maxGlobalInflight");
|
|
2626
|
+
d(this, "perMeshFloor");
|
|
2627
|
+
d(this, "inflightCount", 0);
|
|
2628
|
+
d(this, "disposed", !1);
|
|
2629
|
+
this.maxGlobalInflight = Math.max(
|
|
2630
|
+
1,
|
|
2631
|
+
Math.floor(i.maxGlobalInflight ?? Rt)
|
|
2632
|
+
), this.perMeshFloor = Math.max(0, Math.floor(i.perMeshFloor ?? Mt));
|
|
2633
|
+
}
|
|
2634
|
+
/** Total slots granted and not yet released. */
|
|
2635
|
+
get inflight() {
|
|
2636
|
+
return this.inflightCount;
|
|
2637
|
+
}
|
|
2638
|
+
/** Meshes currently registered. */
|
|
2639
|
+
get clientCount() {
|
|
2640
|
+
return this.entries.size;
|
|
2641
|
+
}
|
|
2642
|
+
register(i) {
|
|
2643
|
+
const e = { client: i, held: 0, demanding: !1, live: !0 };
|
|
2644
|
+
return this.entries.add(e), e;
|
|
2645
|
+
}
|
|
2646
|
+
/**
|
|
2647
|
+
* Drops a mesh. Slots it still holds are released here: a disposing mesh
|
|
2648
|
+
* aborts its fetches, and those aborts land after the handle is gone.
|
|
2649
|
+
*/
|
|
2650
|
+
unregister(i) {
|
|
2651
|
+
const e = i;
|
|
2652
|
+
this.entries.delete(e) && (e.live = !1, e.held > 0 && (this.inflightCount -= e.held, e.held = 0, this.wake()));
|
|
2653
|
+
}
|
|
2654
|
+
/**
|
|
2655
|
+
* Grants a fetch slot, or denies and remembers the demand.
|
|
2656
|
+
*
|
|
2657
|
+
* A denial is not a failure and must not feed a mesh's retry backoff - the
|
|
2658
|
+
* mesh simply did not fetch this tick, and will be woken (or reschedule on
|
|
2659
|
+
* its own within the idle interval) to ask again.
|
|
2660
|
+
*/
|
|
2661
|
+
tryAcquire(i, e) {
|
|
2662
|
+
const t = i;
|
|
2663
|
+
if (this.disposed || !t.live) return !1;
|
|
2664
|
+
const s = H(t.client.weight());
|
|
2665
|
+
return e === "sweep" && s <= 0 ? (t.demanding = !1, !1) : this.inflightCount >= this.maxGlobalInflight || t.held >= this.shareFor(t, s) ? (t.demanding = !0, !1) : (t.held++, t.demanding = !1, this.inflightCount++, !0);
|
|
2666
|
+
}
|
|
2667
|
+
/**
|
|
2668
|
+
* Returns a slot. Must be called exactly once for every granted acquire -
|
|
2669
|
+
* on success, on failure **and** on abort - or the pipe leaks capacity until
|
|
2670
|
+
* the scene is torn down.
|
|
2671
|
+
*/
|
|
2672
|
+
release(i) {
|
|
2673
|
+
const e = i;
|
|
2674
|
+
e.held <= 0 || (e.held--, this.inflightCount--, this.wake());
|
|
2675
|
+
}
|
|
2676
|
+
/**
|
|
2677
|
+
* Re-examines weights after the camera moved. Call it once per frame, right
|
|
2678
|
+
* after the budget governor's own update.
|
|
2679
|
+
*
|
|
2680
|
+
* Meshes that just lost all weight shed their pre-warming sweeps, so a focus
|
|
2681
|
+
* change frees the pipe now rather than when a dozen far requests happen to
|
|
2682
|
+
* finish.
|
|
2683
|
+
*/
|
|
2684
|
+
weightsChanged() {
|
|
2685
|
+
if (!this.disposed) {
|
|
2686
|
+
for (const i of this.entries)
|
|
2687
|
+
i.held > 0 && H(i.client.weight()) <= 0 && i.client.shedFetches("sweep");
|
|
2688
|
+
this.wake();
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
/**
|
|
2692
|
+
* Releases every registration. Meshes are not disposed - the scheduler is
|
|
2693
|
+
* shared and does not own them, exactly as a shared `SplatPool` does not own
|
|
2694
|
+
* its meshes.
|
|
2695
|
+
*/
|
|
2696
|
+
dispose() {
|
|
2697
|
+
this.disposed = !0;
|
|
2698
|
+
for (const i of this.entries)
|
|
2699
|
+
i.live = !1, i.held = 0, i.demanding = !1;
|
|
2700
|
+
this.entries.clear(), this.inflightCount = 0;
|
|
2701
|
+
}
|
|
2702
|
+
/**
|
|
2703
|
+
* The most slots this mesh may hold: its weight-proportional share of the
|
|
2704
|
+
* *whole* pipe, floored so it can always trickle and capped so its siblings
|
|
2705
|
+
* can always reach their own floors.
|
|
2706
|
+
*
|
|
2707
|
+
* This is a ceiling, not a reservation. The global cap does the real
|
|
2708
|
+
* limiting, so a heavy mesh can use capacity its idle siblings are leaving on
|
|
2709
|
+
* the table, while a far mesh's ceiling stays at its floor however early it
|
|
2710
|
+
* asks - which is what keeps the first mesh to reschedule from taking the
|
|
2711
|
+
* pipe and holding it. Dividing the *remainder* after every participant's
|
|
2712
|
+
* floor instead would invert the whole point on a large scene: thirteen
|
|
2713
|
+
* additional meshes and a main at floor 1 consume a 16-slot pipe entirely, leaving the
|
|
2714
|
+
* focused mesh a smaller share than the far ones it is competing with.
|
|
2715
|
+
*
|
|
2716
|
+
* The denominator counts every mesh that plausibly wants the pipe - all
|
|
2717
|
+
* registered meshes except those both weightless and idle, plus the caller.
|
|
2718
|
+
*/
|
|
2719
|
+
shareFor(i, e) {
|
|
2720
|
+
let t = 0, s = 0;
|
|
2721
|
+
for (const l of this.entries) {
|
|
2722
|
+
const c = l === i ? e : H(l.client.weight());
|
|
2723
|
+
l !== i && c <= 0 && l.held === 0 && !l.demanding || (t++, s += c);
|
|
2724
|
+
}
|
|
2725
|
+
if (t <= 1) return this.maxGlobalInflight;
|
|
2726
|
+
const n = s > 0 ? this.maxGlobalInflight * e / s : this.maxGlobalInflight / t, r = Math.max(1, this.perMeshFloor, Math.round(n)), a = Math.max(1, this.perMeshFloor) * (t - 1);
|
|
2727
|
+
return Math.max(1, Math.min(r, this.maxGlobalInflight - a));
|
|
2728
|
+
}
|
|
2729
|
+
/** Pokes the heaviest mesh that was denied since its last grant. */
|
|
2730
|
+
wake() {
|
|
2731
|
+
if (this.disposed || this.inflightCount >= this.maxGlobalInflight) return;
|
|
2732
|
+
let i = null, e = -1 / 0;
|
|
2733
|
+
for (const t of this.entries) {
|
|
2734
|
+
if (!t.demanding) continue;
|
|
2735
|
+
const s = H(t.client.weight());
|
|
2736
|
+
s > e && (i = t, e = s);
|
|
2737
|
+
}
|
|
2738
|
+
i && (i.demanding = !1, i.client.onSlotAvailable());
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
function H(o) {
|
|
2742
|
+
return Number.isFinite(o) && o > 0 ? o : 0;
|
|
2743
|
+
}
|
|
2744
|
+
const Ft = 32 * 1024 * 1024, Bt = 250, Et = 0.15;
|
|
2745
|
+
class Ht {
|
|
2746
|
+
constructor(i) {
|
|
2747
|
+
d(this, "entries", /* @__PURE__ */ new Set());
|
|
2748
|
+
d(this, "perMeshFloorBytes");
|
|
2749
|
+
d(this, "minIntervalMs");
|
|
2750
|
+
d(this, "deadband");
|
|
2751
|
+
d(this, "totalBytesValue");
|
|
2752
|
+
d(this, "lastAllocationMs", Number.NEGATIVE_INFINITY);
|
|
2753
|
+
d(this, "disposed", !1);
|
|
2754
|
+
if (!Number.isFinite(i.totalBytes) || i.totalBytes <= 0)
|
|
2755
|
+
throw new RangeError("Chunk cache totalBytes must be a positive finite number.");
|
|
2756
|
+
this.totalBytesValue = Math.floor(i.totalBytes), this.perMeshFloorBytes = Math.max(
|
|
2757
|
+
0,
|
|
2758
|
+
Math.floor(i.perMeshFloorBytes ?? Ft)
|
|
2759
|
+
), this.minIntervalMs = Math.max(0, i.minIntervalMs ?? Bt), this.deadband = Math.max(0, i.deadband ?? Et);
|
|
2760
|
+
}
|
|
2761
|
+
/** Bytes shared across every registered mesh. */
|
|
2762
|
+
get totalBytes() {
|
|
2763
|
+
return this.totalBytesValue;
|
|
2764
|
+
}
|
|
2765
|
+
/**
|
|
2766
|
+
* Resizes the scene envelope and re-splits immediately. For a host reacting
|
|
2767
|
+
* to a quality change or a device-memory signal.
|
|
2768
|
+
*/
|
|
2769
|
+
setTotalBytes(i) {
|
|
2770
|
+
if (!Number.isFinite(i) || i <= 0)
|
|
2771
|
+
throw new RangeError("Chunk cache totalBytes must be a positive finite number.");
|
|
2772
|
+
const e = Math.floor(i);
|
|
2773
|
+
e !== this.totalBytesValue && (this.totalBytesValue = e, this.allocate());
|
|
2774
|
+
}
|
|
2775
|
+
/** Meshes currently registered. */
|
|
2776
|
+
get clientCount() {
|
|
2777
|
+
return this.entries.size;
|
|
2778
|
+
}
|
|
2779
|
+
/**
|
|
2780
|
+
* Adds a mesh and re-splits at once, so the caller can read its opening
|
|
2781
|
+
* allowance out of {@link allowanceFor} before posting worker init.
|
|
2782
|
+
*/
|
|
2783
|
+
register(i) {
|
|
2784
|
+
const e = { client: i, allowance: 0, live: !0 };
|
|
2785
|
+
return this.entries.add(e), this.allocate({ force: !0, silentFor: e }), e;
|
|
2786
|
+
}
|
|
2787
|
+
/** Drops a mesh and hands its bytes back to the siblings. */
|
|
2788
|
+
unregister(i) {
|
|
2789
|
+
const e = i;
|
|
2790
|
+
this.entries.delete(e) && (e.live = !1, e.allowance = 0, this.allocate({ force: !0 }));
|
|
2791
|
+
}
|
|
2792
|
+
/** This mesh's current allowance in bytes, or 0 once unregistered. */
|
|
2793
|
+
allowanceFor(i) {
|
|
2794
|
+
const e = i;
|
|
2795
|
+
return e.live ? e.allowance : 0;
|
|
2796
|
+
}
|
|
2797
|
+
/**
|
|
2798
|
+
* Re-splits after the camera moved. Call it once per frame, right beside
|
|
2799
|
+
* {@link ChunkFetchScheduler.weightsChanged} - the two read the same weights,
|
|
2800
|
+
* so cache and network follow the same measure.
|
|
2801
|
+
*
|
|
2802
|
+
* Rate-limited and dead-banded; calling it every frame is the intended use.
|
|
2803
|
+
*/
|
|
2804
|
+
weightsChanged(i = Date.now()) {
|
|
2805
|
+
this.disposed || i - this.lastAllocationMs < this.minIntervalMs || (this.lastAllocationMs = i, this.allocate());
|
|
2806
|
+
}
|
|
2807
|
+
/**
|
|
2808
|
+
* Releases every registration. Meshes are not disposed - the budget is shared
|
|
2809
|
+
* and does not own them, exactly as a shared `SplatPool` does not own its
|
|
2810
|
+
* meshes.
|
|
2811
|
+
*/
|
|
2812
|
+
dispose() {
|
|
2813
|
+
this.disposed = !0;
|
|
2814
|
+
for (const i of this.entries)
|
|
2815
|
+
i.live = !1, i.allowance = 0;
|
|
2816
|
+
this.entries.clear();
|
|
2817
|
+
}
|
|
2818
|
+
/**
|
|
2819
|
+
* Cap-aware waterfill: weight-proportional shares, floored so every mesh can
|
|
2820
|
+
* hold its coarse base, clamped at each mesh's `ceilingBytes`, and the surplus
|
|
2821
|
+
* from clamped meshes redistributed over the rest until it settles.
|
|
2822
|
+
*
|
|
2823
|
+
* The invariant callers depend on is `Σ allowance <= totalBytes`. It holds in
|
|
2824
|
+
* every branch, including the degenerate one where the floors alone exceed
|
|
2825
|
+
* the envelope - there the floors are scaled down proportionally rather than
|
|
2826
|
+
* silently overcommitting, because a budget that can be exceeded by
|
|
2827
|
+
* registering more meshes is the bug this class exists to fix.
|
|
2828
|
+
*/
|
|
2829
|
+
allocate(i = {}) {
|
|
2830
|
+
if (this.disposed) return;
|
|
2831
|
+
const e = [...this.entries];
|
|
2832
|
+
if (e.length === 0) return;
|
|
2833
|
+
const t = this.totalBytesValue, s = Math.min(this.perMeshFloorBytes, Math.floor(t / e.length)), n = /* @__PURE__ */ new Map(), r = new Set(e);
|
|
2834
|
+
let a = t;
|
|
2835
|
+
for (let l = 0; l <= e.length && r.size > 0; l++) {
|
|
2836
|
+
const c = Math.max(0, a - s * r.size);
|
|
2837
|
+
let h = 0;
|
|
2838
|
+
for (const u of r) h += Te(u.client.weight());
|
|
2839
|
+
const f = [];
|
|
2840
|
+
for (const u of r) {
|
|
2841
|
+
const g = Te(u.client.weight()), v = h > 0 ? c * g / h : c / r.size, m = kt(u.client.ceilingBytes), b = Math.floor(s + v);
|
|
2842
|
+
b >= m ? (n.set(u, m), f.push(u)) : n.set(u, b);
|
|
2843
|
+
}
|
|
2844
|
+
if (f.length === 0) break;
|
|
2845
|
+
for (const u of f)
|
|
2846
|
+
r.delete(u), a -= n.get(u) ?? 0;
|
|
2847
|
+
a = Math.max(0, a);
|
|
2848
|
+
}
|
|
2849
|
+
for (const l of e) {
|
|
2850
|
+
const c = n.get(l) ?? 0, h = l.allowance;
|
|
2851
|
+
l.allowance = c, l !== i.silentFor && (!i.force && !this.movedEnough(h, c) || h !== c && l.client.onAllowanceChanged(c));
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
/** Suppresses reposts for changes too small to alter any mesh's behaviour. */
|
|
2855
|
+
movedEnough(i, e) {
|
|
2856
|
+
return i === e ? !1 : i === 0 || e === 0 ? !0 : Math.abs(e - i) / i >= this.deadband;
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
function Te(o) {
|
|
2860
|
+
return Number.isFinite(o) && o > 0 ? o : 0;
|
|
2861
|
+
}
|
|
2862
|
+
function kt(o) {
|
|
2863
|
+
return Number.isFinite(o) && o > 0 ? Math.floor(o) : Number.MAX_SAFE_INTEGER;
|
|
2864
|
+
}
|
|
2865
|
+
export {
|
|
2866
|
+
Tt as BudgetGovernor,
|
|
2867
|
+
Ot as CameraBudgetGovernor,
|
|
2868
|
+
Ht as ChunkCacheBudget,
|
|
2869
|
+
Ut as ChunkFetchScheduler,
|
|
2870
|
+
q as SplatLoadError,
|
|
2871
|
+
Y as StreamedSplatMesh,
|
|
2872
|
+
ot as createLocalDataset,
|
|
2873
|
+
nt as httpDatasetSource,
|
|
2874
|
+
G as isAbortError,
|
|
2875
|
+
Be as resolveCpuCacheBytes
|
|
2876
|
+
};
|
|
2877
|
+
//# sourceMappingURL=streaming.js.map
|