@voluma/vlam 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +60 -0
  3. package/THIRD_PARTY_NOTICES.md +86 -0
  4. package/dist/budget-governor.d.ts +154 -0
  5. package/dist/camera-budget-governor.d.ts +234 -0
  6. package/dist/chunk-cache-budget.d.ts +156 -0
  7. package/dist/chunk-fetch-scheduler.d.ts +168 -0
  8. package/dist/chunk-loader.d.ts +85 -0
  9. package/dist/compute-sorter.d.ts +131 -0
  10. package/dist/create-splat-renderer.d.ts +99 -0
  11. package/dist/dataset-source.d.ts +55 -0
  12. package/dist/depth-of-field.d.ts +73 -0
  13. package/dist/effects.d.ts +337 -0
  14. package/dist/effects.js +180 -0
  15. package/dist/effects.js.map +1 -0
  16. package/dist/formats/ksplat/index.d.ts +8 -0
  17. package/dist/formats/ksplat/parse-ksplat.d.ts +9 -0
  18. package/dist/formats/ksplat.js +167 -0
  19. package/dist/formats/ksplat.js.map +1 -0
  20. package/dist/formats/lcc/collision-mesh.d.ts +37 -0
  21. package/dist/formats/lcc/collision-partition.d.ts +35 -0
  22. package/dist/formats/lcc/index.d.ts +16 -0
  23. package/dist/formats/lcc/lcc.d.ts +28 -0
  24. package/dist/formats/lcc/lcc2-transform.d.ts +18 -0
  25. package/dist/formats/lcc/lcc2.d.ts +4 -0
  26. package/dist/formats/lcc/parse-collision-lci.d.ts +28 -0
  27. package/dist/formats/lcc/parse-lcc.d.ts +177 -0
  28. package/dist/formats/lcc/parse-mesh-ply.d.ts +21 -0
  29. package/dist/formats/lcc.js +969 -0
  30. package/dist/formats/lcc.js.map +1 -0
  31. package/dist/formats/ply/index.d.ts +12 -0
  32. package/dist/formats/ply/parse-compressed-ply.d.ts +16 -0
  33. package/dist/formats/ply/parse-splat-ply.d.ts +46 -0
  34. package/dist/formats/ply.js +230 -0
  35. package/dist/formats/ply.js.map +1 -0
  36. package/dist/formats/rad/frontier-worker-protocol.d.ts +177 -0
  37. package/dist/formats/rad/index.d.ts +10 -0
  38. package/dist/formats/rad/parse-rad.d.ts +88 -0
  39. package/dist/formats/rad/rad-column-decoders.d.ts +15 -0
  40. package/dist/formats/rad/rad-foveated-source.d.ts +71 -0
  41. package/dist/formats/rad/rad-parent-sizes.d.ts +45 -0
  42. package/dist/formats/rad/rad.d.ts +132 -0
  43. package/dist/formats/rad.js +1098 -0
  44. package/dist/formats/rad.js.map +1 -0
  45. package/dist/formats/sog/index.d.ts +9 -0
  46. package/dist/formats/sog/parse-sog.d.ts +45 -0
  47. package/dist/formats/sog.js +257 -0
  48. package/dist/formats/sog.js.map +1 -0
  49. package/dist/formats/splat/index.d.ts +8 -0
  50. package/dist/formats/splat/parse-splat.d.ts +10 -0
  51. package/dist/formats/splat.js +39 -0
  52. package/dist/formats/splat.js.map +1 -0
  53. package/dist/formats/spz/index.d.ts +8 -0
  54. package/dist/formats/spz/parse-spz.d.ts +10 -0
  55. package/dist/formats/spz.js +218 -0
  56. package/dist/formats/spz.js.map +1 -0
  57. package/dist/frontier-worker-CAnK_Cmu.js +566 -0
  58. package/dist/frontier-worker-CAnK_Cmu.js.map +1 -0
  59. package/dist/half-float-Cs4FDq8Q.js +30 -0
  60. package/dist/half-float-Cs4FDq8Q.js.map +1 -0
  61. package/dist/half-float.d.ts +15 -0
  62. package/dist/index.d.ts +54 -0
  63. package/dist/index.js +9054 -0
  64. package/dist/index.js.map +1 -0
  65. package/dist/load-scene.d.ts +50 -0
  66. package/dist/load-worker-protocol.d.ts +82 -0
  67. package/dist/loading-wLo8vRbA.js +102 -0
  68. package/dist/loading-wLo8vRbA.js.map +1 -0
  69. package/dist/loading.d.ts +150 -0
  70. package/dist/lod-manifest.d.ts +61 -0
  71. package/dist/lod-scheduler.d.ts +229 -0
  72. package/dist/lod-source.d.ts +207 -0
  73. package/dist/logging-BfPdd7NJ.js +19 -0
  74. package/dist/logging-BfPdd7NJ.js.map +1 -0
  75. package/dist/logging.d.ts +37 -0
  76. package/dist/one-shot-worker-G80btnth.js +664 -0
  77. package/dist/one-shot-worker-G80btnth.js.map +1 -0
  78. package/dist/orientation.d.ts +35 -0
  79. package/dist/ply-header-BwgExCn-.js +125 -0
  80. package/dist/ply-header-BwgExCn-.js.map +1 -0
  81. package/dist/ply-header.d.ts +87 -0
  82. package/dist/radix-sort.d.ts +28 -0
  83. package/dist/radix-sorter.d.ts +46 -0
  84. package/dist/relighting.d.ts +50 -0
  85. package/dist/selection-volume.d.ts +87 -0
  86. package/dist/sh-pack-D5wAe5gg.js +54 -0
  87. package/dist/sh-pack-D5wAe5gg.js.map +1 -0
  88. package/dist/sh-pack.d.ts +49 -0
  89. package/dist/sort-scheduler.d.ts +62 -0
  90. package/dist/sort-worker-protocol.d.ts +37 -0
  91. package/dist/sorter.d.ts +31 -0
  92. package/dist/source-transform.d.ts +57 -0
  93. package/dist/splat-budget-PSojLJPO.js +157 -0
  94. package/dist/splat-budget-PSojLJPO.js.map +1 -0
  95. package/dist/splat-budget.d.ts +404 -0
  96. package/dist/splat-data-BZM5dhhx.js +10 -0
  97. package/dist/splat-data-BZM5dhhx.js.map +1 -0
  98. package/dist/splat-data.d.ts +150 -0
  99. package/dist/splat-depth-pack.d.ts +39 -0
  100. package/dist/splat-mesh-material.d.ts +255 -0
  101. package/dist/splat-mesh-picking.d.ts +87 -0
  102. package/dist/splat-mesh-pool.d.ts +241 -0
  103. package/dist/splat-mesh-types-8QDjTbZO.js +390 -0
  104. package/dist/splat-mesh-types-8QDjTbZO.js.map +1 -0
  105. package/dist/splat-mesh-types.d.ts +390 -0
  106. package/dist/splat-mesh.d.ts +737 -0
  107. package/dist/splat-modifier-stack.d.ts +38 -0
  108. package/dist/splat-modifier.d.ts +156 -0
  109. package/dist/splat-partition.d.ts +37 -0
  110. package/dist/splat-query.d.ts +51 -0
  111. package/dist/splat-scene.d.ts +127 -0
  112. package/dist/splat-sort-bounds.d.ts +11 -0
  113. package/dist/static-lod-splat-mesh.d.ts +53 -0
  114. package/dist/static-lod-worker-D2fyqQZO.js +474 -0
  115. package/dist/static-lod-worker-D2fyqQZO.js.map +1 -0
  116. package/dist/static-lod-worker-protocol.d.ts +37 -0
  117. package/dist/static-lod.d.ts +24 -0
  118. package/dist/storage-attribute-mirror.d.ts +103 -0
  119. package/dist/streamed-splat-mesh-utils.d.ts +146 -0
  120. package/dist/streamed-splat-mesh.d.ts +985 -0
  121. package/dist/unified-splat-renderer.d.ts +205 -0
  122. package/dist/webgpu-limits.d.ts +133 -0
  123. package/dist/work-buffer-gather.d.ts +119 -0
  124. package/dist/work-buffer-material.d.ts +42 -0
  125. package/dist/worker-sorter.d.ts +57 -0
  126. package/dist/xr-view.d.ts +92 -0
  127. package/package.json +135 -21
@@ -0,0 +1,985 @@
1
+ export * from './streamed-splat-mesh-utils';
2
+ import * as THREE from 'three/webgpu';
3
+ import { SplatMesh, type SplatChannelOptions, type SplatMeshOptions, type SplatUpdateOptions } from './splat-mesh';
4
+ import type { StaticLodSplatMesh, StaticLodSplatMeshLoadOptions } from './static-lod-splat-mesh';
5
+ import { type StreamedScene } from './lod-source';
6
+ import type { CollisionMeshTile } from './formats/lcc/collision-mesh';
7
+ import { type SplatRequestOptions, type StreamedSplatFormat } from './loading';
8
+ import { type SplatDeviceProfile } from './splat-budget';
9
+ import { type FrontierFoveation } from './formats/rad/frontier-worker-protocol';
10
+ import type { ChunkFetchScheduler } from './chunk-fetch-scheduler';
11
+ import type { ChunkCacheBudget } from './chunk-cache-budget';
12
+ /**
13
+ * Rough decoded size of a whole streamed scene, for sizing the cache floor:
14
+ * positions (12 B) + colors (4 B) + covariances (24 B) per splat, plus the LOD
15
+ * tree arrays a `.rad` chunk carries (`childCount` 4 B + `childStart` 4 B +
16
+ * `size` 4 B) and packed SH when the scene carries it. Deliberately an
17
+ * over-estimate - the floor should never be the reason a capture cannot hold
18
+ * itself, and it must match what the frontier worker charges its cache, or a
19
+ * capture the cap was sized to hold starts evicting itself mid-load.
20
+ *
21
+ * Exported for unit testing only; not part of the public API.
22
+ */
23
+ export declare function estimateSceneDecodedBytes(scene: StreamedScene): number;
24
+ /**
25
+ * Read-only startup-hold progress for classic `.lcc` {@link StreamedSplatMeshOptions.initialReveal}.
26
+ * Exported for hosts that gate visibility on the first useful near-L0 frame.
27
+ */
28
+ export type InitialRevealState = {
29
+ readonly status: 'disabled';
30
+ } | {
31
+ readonly status: 'pending';
32
+ readonly stagedSplats: number;
33
+ readonly totalSplats: number;
34
+ readonly readyGroups: number;
35
+ readonly totalGroups: number;
36
+ } | {
37
+ readonly status: 'ready';
38
+ } | {
39
+ readonly status: 'degraded';
40
+ readonly reason: 'capacity' | 'fetch-failed' | 'timeout';
41
+ readonly stagedSplats: number;
42
+ readonly totalSplats: number;
43
+ readonly readyGroups: number;
44
+ readonly totalGroups: number;
45
+ };
46
+ /** Options for {@link StreamedSplatMesh.load}. */
47
+ export interface StreamedSplatMeshOptions extends SplatMeshOptions {
48
+ /** Active-splat budget. Defaults to {@link resolveSplatBudget}. */
49
+ budget?: number;
50
+ /**
51
+ * A ceiling on the *resolved default* budget, for callers that want to
52
+ * tighten without overriding what the library knows.
53
+ *
54
+ * `budget` is absolute: it wins over the device tier, the format's cost class
55
+ * and everything else, because a caller who names a number has said they know
56
+ * better. That is the right contract, and the wrong tool for "the same as
57
+ * usual, but no more than N" - which is what a performance toggle or a host
58
+ * default actually means. Pinning a number there has twice shipped as a bug:
59
+ * a demo performance mode that *raised* the load on the weakest device tested,
60
+ * and a host whose default bypassed every device tier.
61
+ *
62
+ * Applied only when `budget` is omitted, and only downward - it never raises
63
+ * a budget the device would not otherwise have taken. It also suppresses the
64
+ * finest-level lift, which exists to raise a budget far enough to hold a
65
+ * scene whole and is exactly what "no more than N" rules out. Unrelated to
66
+ * {@link maxBudget}, which sizes the pool and bounds
67
+ * {@link StreamedSplatMesh.setBudget}.
68
+ *
69
+ * Forwarded to `resolveSplatBudget` as `SplatBudgetOptions.cap`.
70
+ *
71
+ * @throws {RangeError} at load if not a positive finite number.
72
+ */
73
+ budgetCap?: number;
74
+ /**
75
+ * Device signals for budget / quality defaults. Defaults to
76
+ * {@link detectSplatDeviceProfile}. Pass a profile enriched with
77
+ * {@link probeSplatGpuClass} so desktop integrated GPUs take the laptop
78
+ * tier instead of the workstation 8M path.
79
+ */
80
+ deviceProfile?: SplatDeviceProfile;
81
+ /**
82
+ * Ceiling {@link StreamedSplatMesh.setBudget} may raise this mesh to, and the
83
+ * size its pool is allocated from. Defaults to `budget`.
84
+ *
85
+ * Set this above `budget` when a `CameraBudgetGovernor` or `BudgetGovernor`
86
+ * should be able to *grow* this mesh's share: the pool is allocated once at
87
+ * construction and never grows, so without headroom reserved here a governed
88
+ * mesh can only ever be shrunk below the budget it was built with. That is
89
+ * the whole reason a hand-split `pool / N` marker stays coarse near the
90
+ * camera - every marker's ceiling was fixed at a quarter of the pool.
91
+ *
92
+ * It is not free: the pool costs its *ceiling* in memory whether or not the
93
+ * budget ever reaches it (~64 B of GPU pool plus ~56 B of CPU backing per
94
+ * splat, 1.5× for capacity slack). Price it with `estimateSplatPoolBytes`
95
+ * before choosing - for several markers the
96
+ * sum of the ceilings is what has to fit, not the shared budget. A ceiling
97
+ * around 1.5–2× a member's fair share is usually the right trade.
98
+ *
99
+ * @throws {RangeError} at load if below `budget`, or not a positive finite
100
+ * number.
101
+ */
102
+ maxBudget?: number;
103
+ /**
104
+ * Multiplier on this mesh's LOD detail, matching Spark's per-mesh `lodScale`:
105
+ * `> 1` refines further (finer cut, more splats drawn), `< 1` coarsens.
106
+ * Default `1`.
107
+ *
108
+ * **`.rad` `foveationMode: 'pagetable'` only** - it scales the frontier cut
109
+ * the page-table traversal is given (`pixel_scale × lodScale ≤ limit`, exactly
110
+ * Spark's formula). It does nothing on a mesh with no per-splat cut to scale:
111
+ * a moderate `.rad` read as a chunk prefix, or a Streamed SOG / LCC scene. For
112
+ * the GPU cut modes (`'band'` / `'frontier'`) the equivalent is
113
+ * {@link SplatMeshOptions.foveationTargetPx} at `1 / lodScale`.
114
+ *
115
+ * The draw budget still bounds the result, so raising this past the point
116
+ * where the budget binds sharpens nothing - give the mesh budget as well.
117
+ */
118
+ lodScale?: number;
119
+ /** Explicit format; by default the manifest's extension decides. */
120
+ format?: StreamedSplatFormat;
121
+ /** Serializable fetch settings for the manifest and its chunks. */
122
+ request?: SplatRequestOptions;
123
+ /**
124
+ * Cancels the load: the manifest fetch aborts, and {@link StreamedSplatMesh.load}
125
+ * rejects with a `DOMException` named `AbortError`. A mesh partially built
126
+ * when the signal fires is disposed - nothing leaks. Only read during load;
127
+ * later streaming is stopped by {@link SplatMesh.dispose}.
128
+ */
129
+ signal?: AbortSignal;
130
+ /** Base URL a relative manifest URL resolves against (like {@link loadScene}). */
131
+ baseUrl?: string | URL;
132
+ /** World-unit distance inside which the finest LOD is used. Default 10. */
133
+ lodBaseDistance?: number;
134
+ /** Distance ratio between successive LOD levels. Default 2. */
135
+ lodMultiplier?: number;
136
+ /** Cap on decoded chunk arrays cached on the CPU. Default by device memory. */
137
+ cpuCacheBytes?: number;
138
+ /**
139
+ * Foveation ramp for the `.rad` page-table frontier: detail is full inside
140
+ * `coneFov0` degrees of the view direction, falls off to `coneFoveate` by
141
+ * `coneFov`, and to `behindFoveate` directly behind the camera. Off-cone
142
+ * content is kept **coarse**, never dropped, so turning or zooming out never
143
+ * exposes an unpainted region. Defaults match Spark
144
+ * ({@link FRONTIER_FOVEATION_DEFAULTS}).
145
+ */
146
+ frontierFoveation?: Partial<FrontierFoveation>;
147
+ /**
148
+ * Keeps a complete multi-run replacement hidden while its uploads are
149
+ * spread over frames, then switches the region atomically. Enabled by
150
+ * default; set `false` only for legacy A/B comparison.
151
+ */
152
+ experimentalStagedSwaps?: boolean;
153
+ /**
154
+ * Maximum splats copied into the pool per LOD mutation tick. Defaults to
155
+ * 32,000; lower debug values trade refinement latency for shorter frames.
156
+ */
157
+ maxSplatsPerSwap?: number;
158
+ /**
159
+ * First-frame reveal policy for classic `.lcc` streaming.
160
+ *
161
+ * - `'progressive'`: cells become visible as each L0 coverage group commits
162
+ * - can show sparse near-detail while siblings load.
163
+ * - `'hold-near-l0'` (the default for classic `.lcc` when unset): hide the
164
+ * mesh until the camera's home coverage group is resident (L0 when it fits;
165
+ * otherwise coarsen via the leaf ladder L1→L2). Neighbours are not part of
166
+ * the hold - they compete via screenImportance and would steal the first
167
+ * fetch slots. Home selection uses distance within `lodBaseDistance` and
168
+ * does not require frustum intersection (HiRes tiles often fail `inView`
169
+ * when the camera stands inside looking out). Coarser rungs come from
170
+ * `LodSource.runsAtLevelFor`. Only home files are fetched during the hold.
171
+ * A one-minute watchdog also degrades if the cut cannot finish. Other
172
+ * streamed formats default to `'progressive'` and treat the hold as
173
+ * disabled.
174
+ *
175
+ * Does not make detail downloads instantaneous; it avoids lower-LOD traffic
176
+ * and multi-cell buildup before the first useful frame. The hold uses the
177
+ * **resolved** cut from the first schedule (after camera + format transform),
178
+ * not distance ambition alone.
179
+ */
180
+ initialReveal?: 'progressive' | 'hold-near-l0';
181
+ /** Receives lightweight LOD swap markers for performance attribution. */
182
+ onPerformanceEvent?: (event: StreamedSplatPerformanceEvent) => void;
183
+ /**
184
+ * View-dependent color (higher-order SH). This is the streaming counterpart
185
+ * of {@link SplatMeshOptions.shBands}: besides sizing the pool it decides
186
+ * whether SH is fetched/decoded at all. Two sources feed it - a `Quality` LCC
187
+ * `Quality` LCC (`.lcc`) capture, which stores SH per splat, and a Streamed SOG scene,
188
+ * whose per-file palette shN is converted to that same packed form at decode
189
+ * (M11; see `docs/formats/streamed-shn-notes.md`).
190
+ *
191
+ * **For LCC, unset (the default) means every band the capture carries** - so
192
+ * a Quality scene shows its real view-dependent color without the caller
193
+ * having to know the format. The exception is a `smooth` performance profile
194
+ * (the default on mobile), which defaults this to 0: SH roughly triples
195
+ * per-chunk bandwidth (`shcoef.bin` is 64 B/splat against `data.bin`'s 32) and
196
+ * adds up to 64 B/splat of pool textures (~384 MB over a 6M-splat pool at 3
197
+ * bands) - precisely the costs that profile avoids.
198
+ *
199
+ * **For Streamed SOG it is strictly opt-in** (unset = off): the manifest does
200
+ * not declare whether the tiles carry shN, so enabling the conversion - and
201
+ * the pool textures it needs - must be a deliberate choice, not a default.
202
+ *
203
+ * Set it explicitly to override either way: 0 forces SH off, and 1, 2 or 3
204
+ * keep 3, 8 or 15 coefficients per channel. For LCC the value is clamped to
205
+ * what the scene actually has (a `Portable` capture fetches and allocates
206
+ * nothing regardless); for SOG a scene with fewer bands zero-pads and one with
207
+ * no shN simply renders DC color, wasting the allocated textures.
208
+ *
209
+ * Only read at load: the pool's SH textures are allocated once, so a later
210
+ * {@link SplatMesh.setPerformanceProfile} does not change this.
211
+ */
212
+ shBands?: 0 | 1 | 2 | 3;
213
+ /**
214
+ * Whether the scene's always-resident environment/background tile (the
215
+ * `.lcc2` sky) starts visible. Default `true`. Toggle it live afterwards with
216
+ * {@link StreamedSplatMesh.setEnvironmentEnabled}. No effect on a scene that
217
+ * ships no environment tile.
218
+ */
219
+ environmentEnabled?: boolean;
220
+ /**
221
+ * This mesh's share of the scene's fetch bandwidth, as a camera-projected
222
+ * weight - normally `() => governor.weightOf(mesh) ?? 0`, so fetching is
223
+ * ordered by the same measure that already orders drawing.
224
+ *
225
+ * Read on demand, so it always reflects the current camera. Zero means hidden
226
+ * or suspended, and has one effect on its own: the background sweep that
227
+ * pre-warms the whole capture into the page-table cache stops. That sweep is
228
+ * pure speculation about a camera move that has not happened, and on a
229
+ * multi-marker scene it is most of the traffic competing with the marker the
230
+ * viewer is actually looking at.
231
+ *
232
+ * Unset (the default) leaves fetching exactly as it was: every mesh sweeps.
233
+ * Supply a {@link fetchScheduler} as well to also bound the total.
234
+ */
235
+ fetchWeight?: () => number;
236
+ /**
237
+ * Scene-wide fetch arbitration, shared by every streamed mesh the way a
238
+ * {@link SplatMeshOptions.pool} is - see {@link ChunkFetchScheduler}. Without
239
+ * one, each mesh fetches toward its own in-flight cap and a near marker's
240
+ * detail queues behind a dozen far markers' background traffic.
241
+ *
242
+ * The scheduler is *not* owned by the mesh: dispose unregisters this mesh and
243
+ * leaves the scheduler running for its siblings. Weights come from
244
+ * {@link fetchWeight}; without that every mesh weighs the same and the
245
+ * scheduler only bounds the total.
246
+ */
247
+ fetchScheduler?: ChunkFetchScheduler;
248
+ /**
249
+ * Scene-wide decoded-chunk cache ceiling, shared exactly as
250
+ * {@link fetchScheduler} and {@link SplatMeshOptions.pool} are - see
251
+ * {@link ChunkCacheBudget}.
252
+ *
253
+ * Without one, each `.rad` page-table mesh caps its own cache at
254
+ * `max(cpuCacheBytes, min(2 GiB, this capture's decoded size))`: the right
255
+ * number for a lone streamed scene, and no bound at all across a scene of
256
+ * markers, because every mesh gets its own and each is sized to its own
257
+ * capture. With one, that figure becomes this mesh's *ceiling* and the budget
258
+ * splits a scene total across every registered mesh by camera weight.
259
+ *
260
+ * This bounds retention, not prefetching: the background sweep still runs and
261
+ * still warms the cache, it just stops at the scene's allowance instead of at
262
+ * the size of the capture.
263
+ *
264
+ * The budget is *not* owned by the mesh: dispose unregisters this mesh and
265
+ * leaves it running for its siblings. Weights come from {@link fetchWeight}.
266
+ */
267
+ cacheBudget?: ChunkCacheBudget;
268
+ }
269
+ /** One streamed-LOD mutation tick, measured on the main thread. */
270
+ export interface StreamedSplatPerformanceEvent {
271
+ /** Timestamp after the tick, on the same clock as requestAnimationFrame. */
272
+ timestamp: number;
273
+ /** Main-thread time spent rescheduling and applying this tick. */
274
+ cpuMs: number;
275
+ /** Same-frame packed active-index rebuild time, after the LOD mutation. */
276
+ activeListMs: number;
277
+ /** Same-frame partial texture upload submission time. */
278
+ uploadMs: number;
279
+ /** CPU submission time for the depth-sort passes. */
280
+ sortSubmitMs: number;
281
+ /** Exact-height staging textures allocated during this update. */
282
+ stagingTextureAllocations: number;
283
+ /** WebGPU source-index ranges queued for upload before this tick's sort. */
284
+ activeListUpdateRanges: number;
285
+ appendedCount: number;
286
+ removedCount: number;
287
+ stagedCount: number;
288
+ uploadCount: number;
289
+ activeCount: number;
290
+ forcedSort: boolean;
291
+ compacted: boolean;
292
+ }
293
+ /** Options for {@link StreamedSplatMesh.definePersistentChannel}. */
294
+ export interface PersistentChannelOptions extends SplatChannelOptions {
295
+ /**
296
+ * Cap on the number of `(chunk, splat)` edits stored for this channel.
297
+ * Editing past the cap is dropped with a one-time warning. Default 1,000,000.
298
+ */
299
+ maxEdits?: number;
300
+ }
301
+ /**
302
+ * Streams a large splat scene - a Streamed SOG dataset (`lod-meta.json`), or
303
+ * an XGRIDS `.lcc2` or `.lcc` (manifest v3–v5) dataset - into the pool of a
304
+ * dynamic-capacity {@link SplatMesh}, keeping the resident splat count within
305
+ * a per-device budget.
306
+ *
307
+ * Each frame it asks the scene's {@link LodSource} which spatial regions
308
+ * should be resident for the current camera, fetches and decodes the chunk
309
+ * files that back them (off the main thread, via {@link ChunkLoader}), and
310
+ * appends/removes pool ranges to match - loading coarse first so the scene
311
+ * appears quickly and refining near the camera. A coarse full-scene shell
312
+ * always fits the budget, so the view is never blank and the budget is
313
+ * never exceeded.
314
+ *
315
+ * WebGPU only (inherited from the dynamic-capacity pool). View-dependent color
316
+ * (higher-order SH) works for every streamed format: LCC `Quality` captures store it per
317
+ * splat, and a SOG scene's per-file palette shN is converted to that same
318
+ * per-splat packed form at decode so it too survives the shared pool (M11, opt
319
+ * in via {@link StreamedSplatMeshOptions.shBands}; see
320
+ * `docs/formats/streamed-shn-notes.md`).
321
+ */
322
+ export declare class StreamedSplatMesh extends SplatMesh {
323
+ /**
324
+ * Loads a non-streamed splat file into a worker-built merged hierarchy.
325
+ * The returned static mesh shares this class's runtime budget contract.
326
+ */
327
+ static loadAutoLod(input: string | URL, options: StaticLodSplatMeshLoadOptions): Promise<StaticLodSplatMesh>;
328
+ private readonly scene;
329
+ private readonly loader;
330
+ /**
331
+ * Cap the *classic* (non-page-table) chunk cache evicts against.
332
+ *
333
+ * Mutable because a shared {@link ChunkCacheBudget} re-splits it as the camera
334
+ * moves; without a budget it stays at the value `options.cpuCacheBytes` or the
335
+ * device default set at construction.
336
+ */
337
+ private cpuCacheBytes;
338
+ private budgetValue;
339
+ private readonly maximumBudget;
340
+ /** Spark's per-mesh `lodScale`; divides the page-table cut limit. */
341
+ private lodScaleValue;
342
+ /** Set once the governed budget has been reported as exceeding an explicit
343
+ * `foveationDrawBudget`, so the warning is issued at most once. */
344
+ private warnedDrawTargetCap;
345
+ private readonly stagedSwapsEnabled;
346
+ /** Classic LCC must keep old cell coverage while a replacement is pending. */
347
+ private readonly neverRetireCoverageEarly;
348
+ private readonly appendCap;
349
+ private readonly onPerformanceEvent;
350
+ private compactionCount;
351
+ private readonly cache;
352
+ /** Running byte total of {@link cache}; maintained by {@link cacheChunk}, eviction and dispose. */
353
+ private cacheBytesTotal;
354
+ /** In-flight chunk fetches. The kind is kept so a weight change can shed the
355
+ * speculative ones without touching the detail that is actually on screen. */
356
+ private readonly fetching;
357
+ /** This mesh's camera-projected share of the scene's fetch bandwidth. */
358
+ private fetchWeight;
359
+ /** Scene-wide fetch arbitration, when the host shares one; see `requestChunk`. */
360
+ private readonly fetchScheduler;
361
+ private readonly fetchHandle;
362
+ /**
363
+ * Blob-URL dataset from {@link loadLocal}, owned by this mesh so its object
364
+ * URLs are revoked on {@link dispose} rather than leaking for the document's
365
+ * lifetime. Undefined for every network-loaded mesh.
366
+ */
367
+ private localSource;
368
+ /** Scene-wide chunk-cache ceiling, when the host shares one. */
369
+ private readonly cacheBudget;
370
+ private cacheBudgetHandle;
371
+ /**
372
+ * The cap this mesh's frontier worker is currently evicting against.
373
+ *
374
+ * Mirrored on the main thread so `applyCacheAllowance` can skip no-op posts
375
+ * and so `fetchCounts.cacheLimitBytes` stays truthful between plans.
376
+ */
377
+ private cacheLimitBytes;
378
+ private readonly resident;
379
+ /** Replacement runs hidden while their pool data is uploaded in bounded segments. */
380
+ private readonly staged;
381
+ /** Files awaiting a backoff retry after a transient fetch/decode error. */
382
+ private readonly retrying;
383
+ /** Files given up on after {@link MAX_CHUNK_ATTEMPTS} failures. */
384
+ private readonly failedFiles;
385
+ /**
386
+ * When true, each resident run writes its LOD `level` into the `lodLevel`
387
+ * float channel for false-color debug modifiers.
388
+ */
389
+ private lodLevelDebug;
390
+ private lodLevelChannelReady;
391
+ private lodLevelScratch;
392
+ /** Desired-but-not-resident files this tick; protected from cache eviction. */
393
+ private readonly neededFiles;
394
+ /** Non-null in `foveationMode: 'pagetable'`: the worker that owns the chunk
395
+ * cache + traversal + pager off the main thread, and the always-active slab it
396
+ * pages the returned frontier into. */
397
+ private readonly frontierWorker;
398
+ /**
399
+ * The frontier's slots, as a list of equally sized pages rather than one
400
+ * contiguous run.
401
+ *
402
+ * Slot `i` lives in page `i / slabPageSplats` at offset `i %
403
+ * slabPageSplats`. The pager only ever addresses slots, so where those
404
+ * pages sit in the pool is the mesh's business - which is what lets a mesh
405
+ * hold non-contiguous storage, and ultimately lets several meshes interleave
406
+ * in one pool instead of each reserving its whole ceiling as one block.
407
+ * (Spark's pager does the same thing one level down, binding fixed pages to
408
+ * `(source, chunk)` pairs.)
409
+ */
410
+ private readonly slabPages;
411
+ /** Most slots the slab may ever hold - the construction capacity. */
412
+ private slabCeiling;
413
+ /** Slot count the worker's pager was last told about. */
414
+ private pagerSlots;
415
+ /** Consecutive ticks the wave gate has held retirements back. */
416
+ private retireHeldTicks;
417
+ /** Backing store for {@link planTimings}. */
418
+ private readonly planTimingsValue;
419
+ /**
420
+ * Backing store for {@link fetchCounts}. Lifetime totals, because the question
421
+ * they answer is about a *steady state* - "this keeps streaming after the view
422
+ * settled" - which a per-frame or windowed number cannot express.
423
+ */
424
+ private readonly fetchCountsValue;
425
+ /**
426
+ * The screen-radius band the scene asked for, kept so the band can be scaled
427
+ * with the solved frontier cut and always relative to the original - scaling
428
+ * the live values repeatedly would drift. Null when the scene has no band.
429
+ */
430
+ private readonly frontierBandBase;
431
+ /**
432
+ * Splats per slab page for this mesh: {@link SLAB_PAGE_SPLATS}, or the whole
433
+ * capacity when that is smaller. Spark can use one fixed page size because
434
+ * its pool is a single large arena; here a mesh may be smaller than a page,
435
+ * and rounding it up to one would waste most of the reservation.
436
+ */
437
+ private readonly slabPageSplats;
438
+ /** Target drawn-splat count for the page-table frontier; see the constructor. */
439
+ private pageTableDrawBudget;
440
+ /** The unclamped draw target (`foveationDrawBudget` or the default), kept so
441
+ * `setBudget` can re-derive the effective draw budget when the pool budget
442
+ * moves (e.g. under a `BudgetGovernor`). */
443
+ private pageTableDrawTarget;
444
+ /** Whether {@link pageTableDrawTarget} came from an explicit
445
+ * `foveationDrawBudget` - a caller-chosen hard cap worth warning about when a
446
+ * governed budget outgrows it, rather than the library's own default. */
447
+ private pageTableDrawTargetExplicit;
448
+ /** Last frontier's drawn (non-degenerate) splat count - the true on-screen size
449
+ * in `pagetable` mode, where the slab is fully "active" but mostly degenerate. */
450
+ private pageTableDrawn;
451
+ /** Monotonic reschedule id; a stale plan (superseded by a newer request) is
452
+ * dropped. `pageTableInFlight` coalesces to one outstanding traversal. */
453
+ private pageTableSeq;
454
+ private pageTableInFlight;
455
+ private pageTableDisposed;
456
+ /** Files whose data has been forwarded to the worker (so we don't refetch). */
457
+ private readonly pageTableCachedFiles;
458
+ /** Chunks the last frontier wanted but did not have, biggest-on-screen first.
459
+ * These outrank the background sweep - they are the detail actually on screen. */
460
+ private pageTableFetchPriority;
461
+ /** Frontier-cut target node size (px) and foveation ramp; see `frontierView`. */
462
+ private pageTableTargetPx;
463
+ private pageTableFoveation;
464
+ /** Drawing-buffer height, sampled in `update` so `reschedule` can derive the
465
+ * cut limit the same way the material does (`targetPx / focalY`). */
466
+ private pageTableViewportY;
467
+ /** Frontier cut on foveated `size / distance`. Re-derived each reschedule once
468
+ * the drawing buffer is known; the initial value only covers the first frame. */
469
+ private pageTableLimit;
470
+ /**
471
+ * Whether the worker's cache is sitting at its cap: sweeping past that point
472
+ * only evicts what the frontier is using.
473
+ *
474
+ * Re-derived from every plan rather than latched. It used to latch on the
475
+ * first eviction, which was safe only while the cap was sized to the capture
476
+ * and evictions therefore meant "this will never fit". Under a scene-wide
477
+ * {@link ChunkCacheBudget} evictions are routine - a far mesh gives bytes back
478
+ * and is trimmed - and latching would kill its sweep for the session, so a
479
+ * mesh that went cold could never re-warm when the camera returned.
480
+ */
481
+ private pageTableCacheAtLimit;
482
+ /** Per-splat channels whose edits survive chunk eviction/reload (M7.6). */
483
+ private readonly persistentChannels;
484
+ /** Chunk-file index of the always-resident environment tile, if the scene ships one. */
485
+ private readonly envFile;
486
+ /** Whether the environment tile should be visible; toggled live. */
487
+ private envEnabled;
488
+ /** Pool handle of the environment tile once it has loaded (kept for toggling). */
489
+ private envHandle;
490
+ /** Env splat count, measured when the tile decodes; 0 until then. */
491
+ private envSplatCount;
492
+ /** Set when the env tile is larger than the whole pool - terminal, warned once. */
493
+ private envUnfit;
494
+ /**
495
+ * Classic `.lcc` startup hold. `'capture'` waits for the first schedule after
496
+ * the host applies the final camera; `'holding'` freezes that nearby-detail set.
497
+ */
498
+ private initialRevealPhase;
499
+ /** Frozen nearby-detail runs for {@link initialRevealPhase} `'holding'`. */
500
+ private frozenCriticalRuns;
501
+ /** Timestamp of the final-camera capture that began the current hold. */
502
+ private initialRevealStartedAt;
503
+ private initialRevealStateValue;
504
+ /** Fetch settings this mesh was loaded with, reused for collision meshes. */
505
+ private readonly requestOptions;
506
+ /** In-flight or settled collision load; see {@link loadCollisionMeshes}. */
507
+ private collisionTiles;
508
+ private collisionAbort;
509
+ private pendingWork;
510
+ private lastScheduleTime;
511
+ /** Reused leaf-coverage bitmap for {@link substituteCoverage}; grows only. */
512
+ private coverageScratch;
513
+ private readonly lastCameraPos;
514
+ private readonly lastCameraQuat;
515
+ /**
516
+ * Fetches a scene manifest and prepares a mesh sized to the budget.
517
+ * Accepts a Streamed SOG manifest (`lod-meta.json`) or an XGRIDS `.lcc2` or
518
+ * `.lcc` (manifest v3–v5) dataset - all stream through the same machinery. Both LCC
519
+ * generations are normalized to the established XGRIDS/Spark Three.js
520
+ * coordinate frame; streamed SOG orientation is unchanged.
521
+ *
522
+ * A `.lcc` dataset needs a server that answers HTTP range requests: its
523
+ * splats live in one large `data.bin` that is never fetched whole.
524
+ *
525
+ * @param manifestUrl - URL of the scene's `lod-meta.json`, `.lcc2` or `.lcc`
526
+ * file; relative URLs resolve against `options.baseUrl` (or the page).
527
+ * @throws Rejects with {@link SplatLoadError} on any resolve/fetch/parse
528
+ * failure, or a `DOMException` named `AbortError` when `options.signal` fires.
529
+ */
530
+ static load(manifestUrl: string | URL, options?: StreamedSplatMeshOptions): Promise<StreamedSplatMesh>;
531
+ /**
532
+ * Prepares a mesh from a folder dropped into the page - the same streamed
533
+ * formats, read straight off the user's disk with no server and no upload.
534
+ *
535
+ * Every file becomes a `blob:` URL, which answers range requests exactly as
536
+ * an HTTP origin does, so a multi-hundred-megabyte `.lcc` `data.bin` streams
537
+ * chunk-by-chunk rather than being read whole.
538
+ *
539
+ * @param files - The folder's files, keyed by path relative to its root
540
+ * (as the demo drop-zone `readDirectory` walk produces).
541
+ * @throws Rejects with {@link SplatLoadError} - phase `'manifest'` when the
542
+ * folder holds no (or more than one) recognizable scene manifest - or a
543
+ * `DOMException` named `AbortError` when `options.signal` fires.
544
+ */
545
+ static loadLocal(files: ReadonlyMap<string, File>, options?: StreamedSplatMeshOptions): Promise<StreamedSplatMesh>;
546
+ /** Shared load path: fetch the manifest from a source, then build the scene. */
547
+ private static fromSource;
548
+ private constructor();
549
+ /** Slots the currently reserved pages can hold. */
550
+ private get slabSlots();
551
+ /**
552
+ * Reserves or releases slab pages so the slab can hold `wanted` slots, and
553
+ * tells the worker's pager the new slot count.
554
+ *
555
+ * This is the mechanism that makes a shared pool worth having: storage follows
556
+ * the governed budget, so approaching a marker grows its pages while the ones
557
+ * behind you hand theirs back, instead of every marker holding its ceiling for
558
+ * the whole session. Growth stops at the construction ceiling and at whatever
559
+ * the pool can actually spare - a mesh that cannot grow simply stays coarse
560
+ * rather than throwing.
561
+ */
562
+ private syncSlabPages;
563
+ /**
564
+ * Writes `data` at slot `slot`, splitting the write where it crosses a page
565
+ * boundary. The pager's runs are contiguous in *slot* space, which page
566
+ * storage no longer guarantees is contiguous in the pool.
567
+ */
568
+ private writeSlabSlots;
569
+ /** Zeros slots `[slot, slot + count)`, splitting at page boundaries like
570
+ * {@link writeSlabSlots}, so freed slots hold nothing drawable. */
571
+ private degenerateSlabSlots;
572
+ /**
573
+ * Draws exactly the first `resident` slots: pages below the boundary are
574
+ * fully active, the page containing it is partially active, the rest are
575
+ * inactive. Freed tail slots simply leave the active list; they are also
576
+ * degenerated (see {@link degenerateSlabSlots}) so that even a slot drawn by
577
+ * mistake shows nothing.
578
+ */
579
+ private setSlabResident;
580
+ /** Typed post to the frontier worker. */
581
+ private postToWorker;
582
+ /**
583
+ * Applies a new decoded-chunk allowance from the scene's shared
584
+ * {@link ChunkCacheBudget}.
585
+ *
586
+ * Only the cap moves; nothing is dropped here. Both cache implementations
587
+ * evict lazily against it - the page-table worker inside its next
588
+ * `reschedule`, against a frontier that is still current, and the classic
589
+ * path in `evictChunks` on the next tick. Dropping chunks synchronously would
590
+ * pull them out from under resident splats.
591
+ *
592
+ * `pageTableCacheAtLimit` is recomputed here rather than waiting for the next
593
+ * plan, so a *raised* allowance re-arms the background sweep on this tick
594
+ * instead of one idle interval later.
595
+ */
596
+ private applyCacheAllowance;
597
+ /**
598
+ * Whether this scene ships collision meshes - true for an XGRIDS `.lcc` /
599
+ * `.lcc2` dataset that carries them, false for a Streamed SOG scene, which
600
+ * has none.
601
+ */
602
+ get hasCollisionMeshes(): boolean;
603
+ /**
604
+ * Fetches and parses this scene's collision geometry: the triangle meshes an
605
+ * XGRIDS `.lcc` (`collision.lci`) or `.lcc2` (`data/mesh/*.ply`) capture
606
+ * ships beside its splats, for hosts that want collision, ground probes or
607
+ * other spatial queries.
608
+ *
609
+ * The geometry is source-local, like {@link StreamedScene.bounds} - apply
610
+ * this mesh's `matrixWorld` to put it in the frame the splats render in.
611
+ * VLAM! builds no acceleration structure over it and never consults it.
612
+ *
613
+ * Tiles are fetched once and cached; concurrent callers share one load, and
614
+ * a failed load can be retried by calling again. Resolves `[]` for a scene
615
+ * without collision.
616
+ *
617
+ * @throws a `DOMException` named `AbortError` if cancelled, or if
618
+ * {@link dispose} is called while the load is in flight.
619
+ */
620
+ loadCollisionMeshes(options?: {
621
+ signal?: AbortSignal;
622
+ }): Promise<readonly CollisionMeshTile[]>;
623
+ /**
624
+ * Whether this scene ships an always-resident environment/background tile -
625
+ * true for an XGRIDS `.lcc2` capture that carries one (its `env.sog` sky),
626
+ * false for Streamed SOG, `.lcc`, `.rad`, or an `.lcc2` without one.
627
+ */
628
+ get hasEnvironment(): boolean;
629
+ /** Whether the environment tile is currently set to render. */
630
+ get environmentEnabled(): boolean;
631
+ /**
632
+ * Splats in the environment tile, measured when it decoded - the manifest
633
+ * does not carry the count. 0 until the tile has loaded (or if the scene
634
+ * ships none). These sit outside the LOD budget, drawing from the pool's
635
+ * capacity headroom.
636
+ */
637
+ get environmentSplatCount(): number;
638
+ /**
639
+ * Shows or hides the scene's environment/background tile. The switch is
640
+ * instant and never refetches: once loaded, the tile stays in the pool and
641
+ * only its active flag flips. Enabling before the tile has loaded triggers
642
+ * its (one-time) load on the next update. No-op on a scene without one.
643
+ */
644
+ setEnvironmentEnabled(enabled: boolean): void;
645
+ /** The active-splat budget this mesh keeps within. */
646
+ get budget(): number;
647
+ /**
648
+ * The ceiling {@link setBudget} clamps to - {@link StreamedSplatMeshOptions.maxBudget}
649
+ * when one was given, otherwise the construction budget.
650
+ *
651
+ * The pool was allocated for this number and cannot grow, so it is a hard
652
+ * limit on what any governor can hand this mesh. Read it to check that a
653
+ * shared-budget setup can actually deliver the share it is computing.
654
+ */
655
+ get maxBudget(): number;
656
+ /** Alias used by hosts that manage static and streamed auto-LOD uniformly. */
657
+ get budgetCeiling(): number;
658
+ /**
659
+ * The capture's real content size, when the format declares it (`.rad` reports
660
+ * its leaf count) - the splat count needed to hold this mesh at full
661
+ * resolution, independent of the budget it was constructed with.
662
+ *
663
+ * A host splitting one budget across several streamed meshes should clamp each
664
+ * share to this: a mesh cannot spend more than it contains, so budget handed
665
+ * past it buys nothing and is better given to a mesh that can use it. Note it
666
+ * is *not* `maxBudget`: a foveated `.rad` reports `maxResidentSplats` as the
667
+ * requested budget, because its pool holds a camera-directed resident set
668
+ * rather than the whole tree.
669
+ *
670
+ * `undefined` when the format does not declare a content size.
671
+ */
672
+ get contentSplatCount(): number | undefined;
673
+ /**
674
+ * The drawn-splat target currently driving the `.rad` page-table frontier -
675
+ * the governed budget, capped by
676
+ * {@link SplatMeshOptions.foveationDrawBudget}. `0` on a mesh that is not in
677
+ * `foveationMode: 'pagetable'`, which has no frontier to target.
678
+ *
679
+ * This is the number that decides how deep the traversal descends, so it is
680
+ * what to watch when checking that a near mesh really did receive more
681
+ * detail: {@link budget} is the pool's allowance, this is what is spent.
682
+ */
683
+ get drawBudget(): number;
684
+ /**
685
+ * Spark's per-mesh `lodScale` (see {@link StreamedSplatMeshOptions.lodScale}).
686
+ * Mutable: raise it to sharpen a focused mesh, lower it to coarsen a
687
+ * background one. Page-table `.rad` only.
688
+ *
689
+ * @throws {RangeError} if set to a value that is not positive and finite.
690
+ */
691
+ get lodScale(): number;
692
+ set lodScale(value: number);
693
+ /** In `pagetable` mode the slab is fully active but mostly degenerate, so the
694
+ * base `activeSplatCount` (slab size) is not the on-screen count - report the
695
+ * frontier's drawn size instead. */
696
+ get activeSplatCount(): number;
697
+ /**
698
+ * Updates the LOD budget used for future scheduling within allocated capacity.
699
+ *
700
+ * @returns the budget actually in effect, which is `budget` clamped to
701
+ * {@link maxBudget}. A `BudgetGovernor` reads this return value to detect a
702
+ * capped member and hand the remainder to the others, so the clamp is
703
+ * reported rather than hidden.
704
+ */
705
+ setBudget(budget: number): number;
706
+ /**
707
+ * What the LOD scheduler last decided, or `undefined` on sources that do not
708
+ * schedule by leaf (the `.rad` page table, prefix readers).
709
+ *
710
+ * Distinct from {@link activeSplatCount}, and the distinction is the whole
711
+ * point: `desired` is what the scheduler asked for, `activeSplatCount` is
712
+ * what the pool ended up drawing. Equal means the cut is applied; `desired`
713
+ * far below the budget means the *scheduler* declined to spend it, which is a
714
+ * different bug from the mesh failing to apply what it was given.
715
+ */
716
+ get lodStats(): Readonly<{
717
+ inFrustum: number;
718
+ leaves: number;
719
+ desired: number;
720
+ filled: number;
721
+ }> | undefined;
722
+ /** Number of chunk files currently decoded and held. In page-table mode the
723
+ * worker owns the cache - the main-thread map is always empty there, so report
724
+ * what has been forwarded to it instead of a permanent zero. */
725
+ get residentChunkCount(): number;
726
+ /** Chunk fetches currently in flight. */
727
+ get pendingChunkCount(): number;
728
+ /**
729
+ * Main-thread cost of applying paging plans in `foveationMode: 'pagetable'`.
730
+ *
731
+ * A plan is applied whole, off the render loop's own timing, so its cost does
732
+ * not appear in {@link getUpdateTimings} - but it lands on the same thread and
733
+ * a churning frontier can make it the largest stall in a frame. `worst*`
734
+ * accumulate over the mesh's lifetime; the rest describe the most recent plan.
735
+ */
736
+ get planTimings(): Readonly<{
737
+ applyMs: number;
738
+ worstApplyMs: number;
739
+ writeMs: number;
740
+ residentMs: number;
741
+ moves: number;
742
+ appends: number;
743
+ worstSplats: number;
744
+ }>;
745
+ /**
746
+ * Lifetime chunk-fetch totals by kind, plus page-table cache state.
747
+ *
748
+ * Diagnostic for the question "why is this still streaming after the view
749
+ * settled?", which the three fetch sources answer differently and which no
750
+ * other reading distinguishes:
751
+ *
752
+ * - **`sweep` climbing** - speculative file-order pre-warming of the whole
753
+ * capture. Declined by the `smooth` profile; see `sweepAllowed`.
754
+ * - **`priority` / `base` climbing while `evicted` climbs too** - the
755
+ * frontier's touched set does not fit the worker cache, so chunks are
756
+ * evicted and immediately refetched. Streaming never ends because it cannot.
757
+ * - **`priority` / `base` climbing with `evicted` flat** - ordinary refinement
758
+ * still converging on the cut; it should stop on its own.
759
+ *
760
+ * `uncovered` and `retiredEarly` answer a different question - "why are there
761
+ * holes?" - and between them cover both ways this class can render nothing
762
+ * where it should render something:
763
+ *
764
+ * - **`uncovered` climbing after the scene settles** - `substituteCoverage`
765
+ * wanted a leaf's coarsest level as a stand-in and its chunk was not
766
+ * cached. Expected briefly during initial load; afterwards it should not
767
+ * move, because the coarsest files are pinned against eviction.
768
+ * - **`retiredEarly` climbing** - coverage was retired before its replacement
769
+ * landed, under pool pressure or past the retirement hold bound. This is
770
+ * the swap path rather than the substitute path, and it is the one that
771
+ * scales with the budget.
772
+ *
773
+ * Both are counted in whole leaves/groups, monotonically: they answer "did
774
+ * this happen, and is it still happening", not "how much is missing now".
775
+ */
776
+ get fetchCounts(): Readonly<{
777
+ priority: number;
778
+ base: number;
779
+ sweep: number;
780
+ evicted: number;
781
+ uncovered: number;
782
+ retiredEarly: number;
783
+ cacheFull: boolean;
784
+ cacheBytes: number;
785
+ cacheLimitBytes: number;
786
+ }>;
787
+ /** Chunk files given up on after repeated fetch/decode failures. */
788
+ get failedChunkCount(): number;
789
+ /**
790
+ * Forgets all permanent chunk failures so their regions are fetched again.
791
+ * Failures are otherwise terminal for the mesh's lifetime - call this when
792
+ * the cause was transient (e.g. connectivity restored, `online` event).
793
+ */
794
+ retryFailedChunks(): void;
795
+ /**
796
+ * Whether the scene is still resolving toward its target detail - chunks
797
+ * are fetching, or a retry/append is pending. Goes false once the view
798
+ * has settled (useful to drive a loading indicator).
799
+ */
800
+ get isStreaming(): boolean;
801
+ /** The LOD distance model (mutable; e.g. raise to force the finest level). */
802
+ get lodBaseDistance(): number;
803
+ set lodBaseDistance(value: number);
804
+ update(camera: THREE.PerspectiveCamera, renderer: THREE.WebGPURenderer, options?: SplatUpdateOptions): void;
805
+ /** Root bounds of the whole scene, valid before any chunk has loaded. */
806
+ computeSplatBounds(): THREE.Box3;
807
+ /**
808
+ * Enables or disables writing each resident run's **resolved** LOD level into
809
+ * the `lodLevel` float channel (for a false-color debug modifier: 0 = finest).
810
+ * Values come from applied desired runs after budget resolution - not from
811
+ * distance ambition alone. Call before assigning a modifier that reads the channel.
812
+ */
813
+ setLodLevelDebug(enabled: boolean): void;
814
+ /** Whether {@link setLodLevelDebug} is currently writing levels. */
815
+ get isLodLevelDebug(): boolean;
816
+ /**
817
+ * Startup-hold progress for {@link StreamedSplatMeshOptions.initialReveal}.
818
+ * Classic `.lcc` hosts using the default (or explicitly opting into
819
+ * `'hold-near-l0'`) should keep the mesh invisible while
820
+ * `status === 'pending'`, then reveal on `'ready'` or `'degraded'`.
821
+ */
822
+ get initialRevealState(): InitialRevealState;
823
+ /**
824
+ * Captures a fresh classic `.lcc` nearby-detail startup set on the next
825
+ * {@link update}. Hosts that apply their final initial camera pose after the
826
+ * mesh first receives frames should call this before lifting their loading
827
+ * cover. It is a no-op for every other format and for progressive startup.
828
+ */
829
+ recaptureInitialReveal(): void;
830
+ private writeLodLevelChannel;
831
+ /**
832
+ * Declares a per-splat channel whose values **persist across LOD churn**:
833
+ * edits are stored sparsely keyed by `(chunk file, local index)` - a stable
834
+ * splat identity in the streaming design - and re-applied whenever a chunk
835
+ * is (re)appended. Paint a region with {@link paintPersistent}, orbit away
836
+ * until it is evicted, come back, and the values return. See M7.6.
837
+ *
838
+ * Wraps {@link SplatMesh.defineChannel}; read it from a modifier with
839
+ * `ctx.channel(name)` as usual.
840
+ */
841
+ definePersistentChannel(name: string, options?: PersistentChannelOptions): void;
842
+ /**
843
+ * Sets a persistent channel to `value` for every currently-resident splat
844
+ * within `radius` (world units) of `worldPoint`, and records the edit so it
845
+ * survives eviction/reload. Splats whose chunk is not currently decoded on
846
+ * the CPU cannot be located and are skipped (they are usually far from the
847
+ * camera); their painted neighbours in resident chunks are unaffected.
848
+ *
849
+ * The radius assumes this mesh's world transform is rigid (rotation +
850
+ * translation, as the built-in format transforms are); a scaled mesh would
851
+ * distort the brush. Edit a persistent channel only through this method -
852
+ * direct {@link SplatMesh.writeChannel} writes are not recorded and are
853
+ * overwritten by the next re-apply.
854
+ *
855
+ * @returns the number of splats edited this call.
856
+ * @throws {Error} if the channel was not declared with
857
+ * {@link definePersistentChannel}.
858
+ */
859
+ paintPersistent(name: string, worldPoint: THREE.Vector3, radius: number, value: number): number;
860
+ /**
861
+ * Clears every stored edit for a persistent channel and zeroes the value on
862
+ * all currently-resident splats. Chunks that are not resident are covered by
863
+ * the emptied store - they reload at the channel's fill value.
864
+ *
865
+ * @throws {Error} if the channel is not a persistent channel.
866
+ */
867
+ clearPersistentChannel(name: string): void;
868
+ dispose(): void;
869
+ private shouldReschedule;
870
+ private reschedule;
871
+ private rowAlignedSplats;
872
+ /**
873
+ * Startup hold seeds: the coverage group containing (or nearest to) the
874
+ * camera within {@link LodSource.lodBaseDistance}. HiRes tiles often fail the
875
+ * frustum test when most of the cell sits behind the camera - do **not**
876
+ * require `inView`, or the hold seeds a screen-facing neighbour instead.
877
+ * Coarser home levels come from {@link LodSource.runsAtLevelFor}.
878
+ */
879
+ private selectHomeSeedRuns;
880
+ private coarsenHomeRuns;
881
+ private criticalRunsFitCapacity;
882
+ private publishInitialRevealProgress;
883
+ private releaseInitialReveal;
884
+ private captureOrContinueInitialReveal;
885
+ /** After staging/commits, release the hold when every frozen run is resident. */
886
+ private finishInitialRevealIfComplete;
887
+ /** Creates a marker for a changed streamed-LOD tick, if any. */
888
+ private createPerformanceEvent;
889
+ /** Returns whether all new rows can coexist with the currently visible region. */
890
+ private canStageGroup;
891
+ /**
892
+ * Uploads a bounded part of a replacement without rendering it.
893
+ * Skips runs whose chunks are not yet cached so siblings can stage out of order.
894
+ */
895
+ private stageGroup;
896
+ /** Switches a fully staged region from old to new visibility in one tick. */
897
+ private commitStagedGroup;
898
+ /**
899
+ * Applies one swap group atomically within this tick: removals first
900
+ * (freeing pool rows for the replacements), then all adds. Returns false
901
+ * without touching anything when the group cannot fit even after its own
902
+ * removals - the caller defers it and the old runs keep rendering.
903
+ */
904
+ private applyGroup;
905
+ /** Appends one run from the cache, compacting the pool on fragmentation. */
906
+ private appendRun;
907
+ /**
908
+ * Writes the stored edits for one run's `[offset, offset + count)` splats
909
+ * into its freshly appended pool range. No-op when the file has no edits.
910
+ */
911
+ private applyPersistentRun;
912
+ /**
913
+ * Covers a deferred group's leaves that no resident run covers with each
914
+ * leaf's coarsest (pinned, hence cached) level, so a region waiting on a
915
+ * fetch shows coarse detail instead of nothing. The substitutes are
916
+ * intentionally not "desired": the next reschedule swaps them for the
917
+ * real level once its chunk has arrived.
918
+ *
919
+ * Near-camera refinements (`distance <= lodBaseDistance`) skip the coarse
920
+ * paint and its pin fetch entirely - cold load requests only the target cut
921
+ * for that cell. Far gaps keep the shell.
922
+ */
923
+ private substituteCoverage;
924
+ /** Issues pending classic-path chunk wants in group-priority order. */
925
+ private flushClassicFetches;
926
+ /**
927
+ * A camera turn must not wait for all eight old visible requests to finish.
928
+ * Only classic requests carry a precise rank; page-table work retains its
929
+ * own scheduler and is never cancelled here.
930
+ */
931
+ private preemptClassicFetches;
932
+ /**
933
+ * Loads the always-resident environment tile once, on the first update after
934
+ * it is wanted. The tile has no LOD ladder and no manifest count, so it is
935
+ * appended whole (measuring its splat count at decode) and thereafter toggled
936
+ * by flipping its pool range active - never scheduled, refetched, or evicted.
937
+ */
938
+ private updateEnvironment;
939
+ /**
940
+ * Page-table reschedule (`foveationMode: 'pagetable'`): posts the camera to the
941
+ * worker, which owns the cache + traversal + pager and replies asynchronously
942
+ * with a paging plan. Coalesced to one outstanding request so the main thread
943
+ * never blocks. Also drives chunk fetching, in priority order.
944
+ */
945
+ private reschedulePageTable;
946
+ /**
947
+ * Applies a paging plan from the worker to the slab - fast memcpy writes only,
948
+ * no traversal or gather on the main thread - then fetches the chunks the
949
+ * frontier wants next, and reschedules again if chunks are still streaming.
950
+ */
951
+ private applyFrontierPlan;
952
+ /** Forwards a decoded chunk's arrays to the worker (buffers transferred) so the
953
+ * worker's cache/traversal/gather can use it. */
954
+ private forwardChunkToWorker;
955
+ /** Parallel chunk fetches. HTTP/2 multiplexes them; on HTTP/1.1 the browser's
956
+ * per-host cap simply queues. Same cap for classic and pagetable so near
957
+ * detail is not structurally starved on the non-pagetable path. */
958
+ private get maxInflight();
959
+ /**
960
+ * Whether this mesh may run its speculative background sweep. A mesh with no
961
+ * weight is hidden or suspended; a mesh with no `fetchWeight` at all is a
962
+ * host that never asked for arbitration, and keeps the old behaviour.
963
+ */
964
+ /**
965
+ * Sets this mesh's share of the scene's fetch bandwidth, as
966
+ * {@link StreamedSplatMeshOptions.fetchWeight} does at load.
967
+ *
968
+ * The weight normally closes over the mesh itself (`() =>
969
+ * governor.weightOf(mesh)`), which a host cannot express until `load`
970
+ * resolves - hence a setter as well as an option. Pass `undefined` to go back
971
+ * to unarbitrated sweeping.
972
+ */
973
+ setFetchWeight(weight: (() => number) | undefined): void;
974
+ private sweepAllowed;
975
+ /** Aborts in-flight fetches of one kind; their slots return through `finally`. */
976
+ private abortFetches;
977
+ private requestChunk;
978
+ /**
979
+ * Stores a decoded chunk while keeping {@link cacheBytesTotal} in step. The
980
+ * counter replaces a full-cache re-sum on every reschedule; every mutation
981
+ * of {@link cache} (this method, eviction, dispose's clear) maintains it.
982
+ */
983
+ private cacheChunk;
984
+ private evictChunks;
985
+ }