@voluma/vlam 0.1.0 → 0.2.1

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 +68 -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,156 @@
1
+ /**
2
+ * Cross-mesh decoded-chunk cache arbitration.
3
+ *
4
+ * Every {@link StreamedSplatMesh} caps its own decoded-chunk cache, and in
5
+ * `foveationMode: 'pagetable'` that cap is `min(2 GiB, this capture's decoded
6
+ * size)` - a number sized for *one* streamed scene. A scene of streamed markers
7
+ * therefore has no ceiling at all: thirteen markers plus a main are thirteen
8
+ * plus one independent caps, and because each is sized to its own capture, a
9
+ * desktop that fits them never evicts. The background sweep then runs to
10
+ * completion against every one of them, pulling every capture in the scene into
11
+ * RAM and keeping it there.
12
+ *
13
+ * This is the missing ceiling, at whole-mesh granularity: one budget shared by
14
+ * every streamed mesh in a scene (the same sharing model as `SplatPool` and
15
+ * {@link ChunkFetchScheduler}), splitting a scene total by the same
16
+ * camera-projected weight a `CameraBudgetGovernor` already computes for drawing.
17
+ *
18
+ * It bounds **retention**, not prefetching. The sweep still runs and still warms
19
+ * the cache; it simply stops at an allowance the whole scene agreed on rather
20
+ * than at the size of each capture. A mesh the camera approaches gets a larger
21
+ * allowance and resumes sweeping; one it leaves gives bytes back.
22
+ *
23
+ * The budget brokers **bytes**, never chunks: it does not know about
24
+ * `ChunkLoader`, chunk ids or workers, which is what keeps the eviction policy
25
+ * where it belongs - inside the frontier worker, which is the only place that
26
+ * knows what the current cut still needs.
27
+ */
28
+ /** A streamed mesh, as the cache budget sees it. */
29
+ export interface ChunkCacheClient {
30
+ /**
31
+ * This mesh's camera-projected weight - larger means more of the cache.
32
+ *
33
+ * Read on demand rather than pushed, so the budget never holds a stale
34
+ * weight; a `CameraBudgetGovernor.weightOf` call is the intended source, and
35
+ * is the same one {@link ChunkFetchScheduler} reads. Zero (hidden or
36
+ * suspended) still keeps `perMeshFloorBytes`: a mesh whose coarse base has
37
+ * been evicted draws nothing at all when the camera comes back to it.
38
+ */
39
+ weight(): number;
40
+ /**
41
+ * The most this mesh could ever put to use - for a page-table mesh,
42
+ * `min(PAGETABLE_CACHE_FLOOR_BYTES, estimateSceneDecodedBytes(scene))`.
43
+ *
44
+ * Bytes above it are handed to siblings that can use them, the way
45
+ * `BudgetGovernor` waterfills past a member's `maxBudget`. Without it a scene
46
+ * of one large capture and a dozen small markers would reserve most of the
47
+ * envelope for markers that cannot fill it.
48
+ */
49
+ readonly ceilingBytes: number;
50
+ /**
51
+ * This mesh's allowance moved. The mesh forwards it to its frontier worker;
52
+ * nothing is dropped synchronously, because only the worker's own eviction
53
+ * pass knows which chunks the current cut still needs.
54
+ */
55
+ onAllowanceChanged(bytes: number): void;
56
+ }
57
+ /** Options for {@link ChunkCacheBudget}. */
58
+ export interface ChunkCacheBudgetOptions {
59
+ /**
60
+ * Decoded-chunk bytes every registered mesh may hold **in total**.
61
+ *
62
+ * This is the number that decides whether the budget helps at all. Too low
63
+ * and the focused mesh re-fetches chunks it just evicted; too high and it is
64
+ * inert, because each mesh's own `ceilingBytes` becomes binding again. Size
65
+ * it against the tab's heap, not against any one capture.
66
+ */
67
+ totalBytes: number;
68
+ /**
69
+ * Bytes each mesh keeps regardless of weight. Default 32 MiB, matching
70
+ * `resolveCpuCacheBytes`'s own minimum.
71
+ *
72
+ * Without a floor a far mesh evicts its own coarse base and re-fetches it on
73
+ * the next reschedule, forever - the same thrash {@link
74
+ * ChunkFetchScheduler.perMeshFloor} exists to prevent on the network side.
75
+ */
76
+ perMeshFloorBytes?: number;
77
+ /**
78
+ * Minimum ms between weight-driven reallocations. Default 250, matching the
79
+ * idle reschedule interval - there is no point re-splitting faster than the
80
+ * meshes can act on it. Membership changes bypass it.
81
+ */
82
+ minIntervalMs?: number;
83
+ /**
84
+ * Relative allowance change below which a mesh is not notified. Default 0.15.
85
+ *
86
+ * Every notification is a worker post and, indirectly, an eviction pass. A
87
+ * camera drifting slowly would otherwise repost a 1% different number every
88
+ * quarter second for no change in behaviour.
89
+ */
90
+ deadband?: number;
91
+ }
92
+ /**
93
+ * A mesh's registration. Opaque to callers: hold it from {@link
94
+ * ChunkCacheBudget.register} and pass it back to read the allowance or
95
+ * unregister.
96
+ */
97
+ export interface ChunkCacheHandle {
98
+ readonly client: ChunkCacheClient;
99
+ }
100
+ /** See the module comment. Construct one per scene and pass it to every mesh. */
101
+ export declare class ChunkCacheBudget {
102
+ private readonly entries;
103
+ private readonly perMeshFloorBytes;
104
+ private readonly minIntervalMs;
105
+ private readonly deadband;
106
+ private totalBytesValue;
107
+ private lastAllocationMs;
108
+ private disposed;
109
+ constructor(options: ChunkCacheBudgetOptions);
110
+ /** Bytes shared across every registered mesh. */
111
+ get totalBytes(): number;
112
+ /**
113
+ * Resizes the scene envelope and re-splits immediately. For a host reacting
114
+ * to a quality change or a device-memory signal.
115
+ */
116
+ setTotalBytes(bytes: number): void;
117
+ /** Meshes currently registered. */
118
+ get clientCount(): number;
119
+ /**
120
+ * Adds a mesh and re-splits at once, so the caller can read its opening
121
+ * allowance out of {@link allowanceFor} before posting worker init.
122
+ */
123
+ register(client: ChunkCacheClient): ChunkCacheHandle;
124
+ /** Drops a mesh and hands its bytes back to the siblings. */
125
+ unregister(handle: ChunkCacheHandle): void;
126
+ /** This mesh's current allowance in bytes, or 0 once unregistered. */
127
+ allowanceFor(handle: ChunkCacheHandle): number;
128
+ /**
129
+ * Re-splits after the camera moved. Call it once per frame, right beside
130
+ * {@link ChunkFetchScheduler.weightsChanged} - the two read the same weights,
131
+ * so cache and network follow the same measure.
132
+ *
133
+ * Rate-limited and dead-banded; calling it every frame is the intended use.
134
+ */
135
+ weightsChanged(now?: number): void;
136
+ /**
137
+ * Releases every registration. Meshes are not disposed - the budget is shared
138
+ * and does not own them, exactly as a shared `SplatPool` does not own its
139
+ * meshes.
140
+ */
141
+ dispose(): void;
142
+ /**
143
+ * Cap-aware waterfill: weight-proportional shares, floored so every mesh can
144
+ * hold its coarse base, clamped at each mesh's `ceilingBytes`, and the surplus
145
+ * from clamped meshes redistributed over the rest until it settles.
146
+ *
147
+ * The invariant callers depend on is `Σ allowance <= totalBytes`. It holds in
148
+ * every branch, including the degenerate one where the floors alone exceed
149
+ * the envelope - there the floors are scaled down proportionally rather than
150
+ * silently overcommitting, because a budget that can be exceeded by
151
+ * registering more meshes is the bug this class exists to fix.
152
+ */
153
+ private allocate;
154
+ /** Suppresses reposts for changes too small to alter any mesh's behaviour. */
155
+ private movedEnough;
156
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Cross-mesh chunk-fetch arbitration.
3
+ *
4
+ * Every {@link StreamedSplatMesh} owns its own `ChunkLoader` worker and issues
5
+ * chunk requests toward its own in-flight cap, so a scene of streamed meshes
6
+ * fetches with no shared ordering: a marker the camera is pointed at competes
7
+ * for bandwidth and connection slots against a dozen distant ones, each of
8
+ * which is equally entitled to its own cap. Spark does not have this problem
9
+ * structurally - one global traversal orders every fetch want
10
+ * biggest-on-screen-first through one pager, so its network order *is* its
11
+ * visual priority.
12
+ *
13
+ * This is that ordering at whole-mesh granularity: one scheduler shared by
14
+ * every streamed mesh in a scene (the same sharing model as `SplatPool`),
15
+ * handing out a bounded number of fetch slots in proportion to the same
16
+ * camera-projected weight a `CameraBudgetGovernor` already computes for
17
+ * drawing. Within a mesh the existing order still applies - a frontier plan's
18
+ * `touched` list is already sorted biggest-on-screen first - so the two
19
+ * granularities compose.
20
+ *
21
+ * The scheduler brokers **slots**, never fetches: it does not know about
22
+ * `ChunkLoader`, URLs or workers, which is what keeps a future scene-level
23
+ * shared loader an independent change.
24
+ */
25
+ /**
26
+ * Why a mesh wants a slot. The kind is the mesh's own statement of intent, and
27
+ * decides what a weightless (hidden, or suspended) mesh may still do:
28
+ *
29
+ * - `priority` - detail the frontier asked for and does not have. The chunks
30
+ * actually on screen.
31
+ * - `base` - the camera-directed coarse base, and the pinned coverage every
32
+ * deferred swap substitutes from. A far mesh must keep trickling these or it
33
+ * has nothing to draw at all.
34
+ * - `sweep` - the background file-order sweep that pulls a whole capture into
35
+ * the worker cache so later camera moves are served from RAM. Pure
36
+ * pre-warming: valuable when a mesh is on screen, and the first thing to give
37
+ * up when it is not.
38
+ */
39
+ export type ChunkFetchKind = 'priority' | 'base' | 'sweep';
40
+ /** A streamed mesh, as the scheduler sees it. */
41
+ export interface ChunkFetchClient {
42
+ /**
43
+ * This mesh's camera-projected weight - larger means more of the pipe.
44
+ * Zero means hidden or suspended: such a mesh keeps its floor for `priority`
45
+ * and `base` work but is denied `sweep` entirely.
46
+ *
47
+ * Read on demand rather than pushed, so the scheduler never holds a stale
48
+ * weight; a `CameraBudgetGovernor.weightOf` call is the intended source.
49
+ */
50
+ weight(): number;
51
+ /**
52
+ * A slot may now be free. The mesh should re-run its reschedule, which
53
+ * re-issues whatever it still wants. The scheduler deliberately keeps no
54
+ * queue of pending requests: the reschedule paths are already idempotent
55
+ * re-issuers, so a poke carries strictly more current information than a
56
+ * request recorded when the camera was somewhere else.
57
+ */
58
+ onSlotAvailable(): void;
59
+ /**
60
+ * Abort in-flight fetches of this kind - the mesh's weight dropped to zero
61
+ * while it held slots. Aborted fetches release their slots through the normal
62
+ * path, so this is how a focus change hands the pipe over immediately rather
63
+ * than after the far meshes' current requests drain.
64
+ */
65
+ shedFetches(kind: ChunkFetchKind): void;
66
+ }
67
+ /** Options for {@link ChunkFetchScheduler}. */
68
+ export interface ChunkFetchSchedulerOptions {
69
+ /**
70
+ * Total chunk fetches in flight across every registered mesh. Default 16.
71
+ *
72
+ * This is the number that decides whether the scheduler helps at all. Too low
73
+ * and a single mesh scene streams slower than it does today; too high and
74
+ * there is nothing to arbitrate, because the browser's own queueing (six per
75
+ * origin on HTTP/1.1; a much larger multiplexed window on HTTP/2) becomes the
76
+ * real scheduler again - and it orders by request time, not by what is on
77
+ * screen. Tune it from a measured concurrency high-water mark, not by feel.
78
+ */
79
+ maxGlobalInflight?: number;
80
+ /**
81
+ * Slots every registered mesh may hold regardless of weight. Default 1.
82
+ *
83
+ * Without a floor a scene's far meshes stop fetching entirely while the
84
+ * focused one streams, and a marker with no coarse coverage draws nothing -
85
+ * so the floor is what turns "far meshes wait" into "far meshes trickle".
86
+ *
87
+ * One slot is always granted regardless of this setting: a mesh entitled to
88
+ * zero would never fetch and never settle. Set this above 1 to widen every
89
+ * mesh's guaranteed trickle.
90
+ */
91
+ perMeshFloor?: number;
92
+ }
93
+ /**
94
+ * A mesh's registration. Opaque to callers: hold it from {@link
95
+ * ChunkFetchScheduler.register} and pass it back to acquire, release and
96
+ * unregister.
97
+ */
98
+ export interface ChunkFetchHandle {
99
+ readonly client: ChunkFetchClient;
100
+ }
101
+ /** See the module comment. Construct one per scene and pass it to every mesh. */
102
+ export declare class ChunkFetchScheduler {
103
+ private readonly entries;
104
+ private readonly maxGlobalInflight;
105
+ private readonly perMeshFloor;
106
+ private inflightCount;
107
+ private disposed;
108
+ constructor(options?: ChunkFetchSchedulerOptions);
109
+ /** Total slots granted and not yet released. */
110
+ get inflight(): number;
111
+ /** Meshes currently registered. */
112
+ get clientCount(): number;
113
+ register(client: ChunkFetchClient): ChunkFetchHandle;
114
+ /**
115
+ * Drops a mesh. Slots it still holds are released here: a disposing mesh
116
+ * aborts its fetches, and those aborts land after the handle is gone.
117
+ */
118
+ unregister(handle: ChunkFetchHandle): void;
119
+ /**
120
+ * Grants a fetch slot, or denies and remembers the demand.
121
+ *
122
+ * A denial is not a failure and must not feed a mesh's retry backoff - the
123
+ * mesh simply did not fetch this tick, and will be woken (or reschedule on
124
+ * its own within the idle interval) to ask again.
125
+ */
126
+ tryAcquire(handle: ChunkFetchHandle, kind: ChunkFetchKind): boolean;
127
+ /**
128
+ * Returns a slot. Must be called exactly once for every granted acquire -
129
+ * on success, on failure **and** on abort - or the pipe leaks capacity until
130
+ * the scene is torn down.
131
+ */
132
+ release(handle: ChunkFetchHandle): void;
133
+ /**
134
+ * Re-examines weights after the camera moved. Call it once per frame, right
135
+ * after the budget governor's own update.
136
+ *
137
+ * Meshes that just lost all weight shed their pre-warming sweeps, so a focus
138
+ * change frees the pipe now rather than when a dozen far requests happen to
139
+ * finish.
140
+ */
141
+ weightsChanged(): void;
142
+ /**
143
+ * Releases every registration. Meshes are not disposed - the scheduler is
144
+ * shared and does not own them, exactly as a shared `SplatPool` does not own
145
+ * its meshes.
146
+ */
147
+ dispose(): void;
148
+ /**
149
+ * The most slots this mesh may hold: its weight-proportional share of the
150
+ * *whole* pipe, floored so it can always trickle and capped so its siblings
151
+ * can always reach their own floors.
152
+ *
153
+ * This is a ceiling, not a reservation. The global cap does the real
154
+ * limiting, so a heavy mesh can use capacity its idle siblings are leaving on
155
+ * the table, while a far mesh's ceiling stays at its floor however early it
156
+ * asks - which is what keeps the first mesh to reschedule from taking the
157
+ * pipe and holding it. Dividing the *remainder* after every participant's
158
+ * floor instead would invert the whole point on a large scene: thirteen
159
+ * markers and a main at floor 1 consume a 16-slot pipe entirely, leaving the
160
+ * focused marker a smaller share than the far ones it is competing with.
161
+ *
162
+ * The denominator counts every mesh that plausibly wants the pipe - all
163
+ * registered meshes except those both weightless and idle, plus the caller.
164
+ */
165
+ private shareFor;
166
+ /** Pokes the heaviest mesh that was denied since its last grant. */
167
+ private wake;
168
+ }
@@ -0,0 +1,85 @@
1
+ import type { SplatData } from './splat-data';
2
+ import type { RadChunkRangeRequest } from './load-worker-protocol';
3
+ import type { LccChunkParams } from './formats/lcc/parse-lcc';
4
+ import { type SplatFileLoadOptions, type SplatFormat, type SplatInputOptions } from './loading';
5
+ /**
6
+ * Client for the persistent scene/chunk loading workers. Multiple requests
7
+ * may be in flight; each can be cancelled through an `AbortSignal`, which
8
+ * aborts the worker-side fetches (useful when the camera has moved on
9
+ * before a chunk arrived).
10
+ *
11
+ * There are two workers behind this one class. The streaming worker is
12
+ * constructed eagerly and handles everything the LOD paths need; the one-shot
13
+ * worker ({@link ONE_SHOT_FORMATS}) is constructed the first time a `.spz`,
14
+ * `.splat` or `.ksplat` is requested. Both decode off the main thread - the
15
+ * split exists only to keep the one-shot parsers out of the published entry.
16
+ *
17
+ * @example
18
+ * const loader = new ChunkLoader();
19
+ * const chunk = await loader.load('/scene/0_0/', { kind: 'directory' });
20
+ */
21
+ export declare class ChunkLoader {
22
+ private readonly streaming;
23
+ /** The in-flight construction; `null` until a one-shot format is requested. */
24
+ private oneShot;
25
+ /** The constructed one-shot client, so `dispose` does not have to await. */
26
+ private oneShotClient;
27
+ private disposed;
28
+ /**
29
+ * Fetches and decodes one scene file or chunk directory.
30
+ *
31
+ * @param url - Scene URL, absolute or relative to the page.
32
+ * @param options.kind - 'file' (default) selects a parser by extension;
33
+ * 'directory' decodes an unbundled SOG chunk directory.
34
+ * @param options.format - Explicit parser; `lcc-bin` additionally needs
35
+ * `options.lcc` to say which byte range of the file to fetch.
36
+ * @param options.signal - Abort signal for cancellation; the returned
37
+ * promise then rejects with a `DOMException` named `AbortError`.
38
+ * @param options.lcc - Byte-range plumbing for `lcc-bin`. This path exists
39
+ * for {@link StreamedSplatMesh} and is **not** part of the stable surface;
40
+ * the type is nameable as `LccChunkParams` on `@voluma/vlam/formats/lcc`.
41
+ * @param options.rad - Byte-range plumbing for `rad-chunk`, on the same
42
+ * footing; the type is `RadChunkRangeRequest` on `@voluma/vlam/formats/rad`.
43
+ * @throws Rejects only with {@link SplatLoadError} (resolve, fetch, decode or
44
+ * worker failures - `error.phase` says which; a worker that cannot start at
45
+ * all, e.g. under a CSP that blocks `blob:` workers, reports
46
+ * `phase: 'worker'`) or an `Error` named `AbortError` (cancellation or
47
+ * {@link dispose}).
48
+ */
49
+ load(url: string, options?: SplatInputOptions & {
50
+ kind?: 'file' | 'directory';
51
+ format?: SplatFormat | 'lcc-bin' | 'rad-chunk';
52
+ lcc?: LccChunkParams;
53
+ rad?: RadChunkRangeRequest;
54
+ sog?: {
55
+ packShBands: 1 | 2 | 3;
56
+ };
57
+ files?: Readonly<Record<string, string>>;
58
+ }): Promise<SplatData>;
59
+ /**
60
+ * Decodes a local file (from a file input or a drop) in the worker - the
61
+ * bytes are read worker-side, so a multi-million-splat decode still never
62
+ * blocks the main thread.
63
+ *
64
+ * Only self-contained files are supported: `.ply`, `.spz`, `.splat`,
65
+ * `.ksplat` and bundled `.sog`. An unbundled SOG directory needs sibling
66
+ * fetches and so requires {@link load} with `kind: 'directory'`.
67
+ *
68
+ * @param file - The file; its name selects the parser unless `format` is given.
69
+ * @throws Rejects only with {@link SplatLoadError} (unknown extension, read
70
+ * or decode failures) or an `Error` named `AbortError` (cancellation or
71
+ * {@link dispose}).
72
+ */
73
+ loadFile(file: File, options?: SplatFileLoadOptions): Promise<SplatData>;
74
+ /** Routes one load to the worker that owns its format. */
75
+ private request;
76
+ /**
77
+ * The worker for `format`, constructing the one-shot worker on first need.
78
+ *
79
+ * The in-flight promise is cached, not just the resolved client, so two
80
+ * concurrent `.spz` loads share one worker instead of racing to build two.
81
+ */
82
+ private clientFor;
83
+ /** Terminates both workers and rejects all in-flight requests. */
84
+ dispose(): void;
85
+ }
@@ -0,0 +1,131 @@
1
+ import * as THREE from 'three/webgpu';
2
+ import type { SplatSorter } from './sorter';
3
+ import type { uniformArray } from 'three/tsl';
4
+ /**
5
+ * Optional per-source world transform for a unified {@link SplatScene} pool.
6
+ * When present, each splat's center is transformed to world space by its
7
+ * source's matrix (selected by a per-splat id read from `sourceIdTexture`)
8
+ * before its depth is computed, so clouds with different world transforms sort
9
+ * against each other correctly. Absent for an ordinary single-transform mesh.
10
+ */
11
+ export interface PerSourceSortTransform {
12
+ /** RedFormat float texture holding each pool slot's source id (the channel). */
13
+ sourceIdTexture: THREE.DataTexture;
14
+ /** Shared array of source-matrix columns; see `SourceMatrixArray`. */
15
+ columns: ReturnType<typeof uniformArray>;
16
+ /** CPU mirror of the source-id channel, used by the WebGL2 worker sorter. */
17
+ sourceIds: Float32Array;
18
+ /** Column-major source matrices, used by the WebGL2 worker sorter. */
19
+ matrices: Float32Array;
20
+ }
21
+ /**
22
+ * GPU depth sorter: a single-pass counting sort over 2²² (~4M) depth
23
+ * buckets, running entirely in TSL compute passes on the WebGPU backend.
24
+ *
25
+ * 1. clear - zero the histogram
26
+ * 2. histogram - depth per active splat → bucket id; count per bucket
27
+ * 3. scanBlocks - exclusive prefix sum within each 256-bucket block
28
+ * 4. scanBlockSums - parallel exclusive scans over groups of block totals
29
+ * 5. scanSuperSums - exclusive scan over the remaining super-block totals
30
+ * 6. addBlockOffsets - combine both block-sum scan levels
31
+ * 7. addOffsets - combine block and bucket offsets
32
+ * 8. scatter - order[offset[bucket]++] = pool index (atomic)
33
+ *
34
+ * Why ~4M buckets, not 64K: on a large scene, 16-bit buckets made a
35
+ * near-camera bucket span thousands of overlapping grass/leaf splats whose
36
+ * arbitrary intra-bucket order reshuffled as the camera moved - visible
37
+ * popping. Each depth bucket is now a sub-splat-width slice, so splats that
38
+ * share a bucket are effectively coplanar and their (still arbitrary) order
39
+ * cannot be seen. (A multi-pass radix would give exact order but needs a
40
+ * *stable* scatter, which the parallel `atomicAdd` scatter is not - hence a
41
+ * single wide pass instead.)
42
+ *
43
+ * Dynamic capacity: the sorter walks the mesh's `sourceIndex` buffer (pool
44
+ * indices of the active splats) and sorts only the first `activeCount`
45
+ * entries, so ranges can be appended and removed without rebuilding
46
+ * pipelines - the per-splat passes are dispatched with a dynamic invocation
47
+ * count and guarded by an `activeCount` uniform.
48
+ *
49
+ * Portable by construction: cross-workgroup communication only happens
50
+ * through the implicit synchronization WebGPU guarantees between dispatches
51
+ * - no spin-waits or scheduling assumptions.
52
+ *
53
+ * The depth range for bucket quantization comes from the mesh's bounding
54
+ * sphere transformed to view space, so no GPU min/max reduction is needed.
55
+ */
56
+ export declare class ComputeSorter implements SplatSorter {
57
+ readonly kind: "counting";
58
+ private static readonly BUCKET_COUNT;
59
+ private static readonly BLOCK_SIZE;
60
+ /**
61
+ * Smallest bucket count a sort will dispatch. Below this the fixed passes
62
+ * are already cheap, and the floor keeps every block-scan index exact.
63
+ */
64
+ private static readonly MIN_BUCKET_COUNT;
65
+ private readonly renderer;
66
+ private readonly clearPass;
67
+ private readonly histogramPass;
68
+ private readonly scanBlocksPass;
69
+ private readonly scanBlockSumsPass;
70
+ private readonly addBlockSumOffsetsPass;
71
+ private readonly addOffsetsPass;
72
+ private readonly scatterPass;
73
+ /** All stages in dependency order, submitted as one WebGPU compute pass. */
74
+ private readonly sortPasses;
75
+ /** Sorter-owned working buffers, released on {@link dispose}. */
76
+ private readonly workingAttributes;
77
+ /** Frees the JS mirrors three keeps behind the GPU-only working buffers. */
78
+ private readonly mirrors;
79
+ /** Set by {@link dispose}; makes a second dispose a no-op. */
80
+ private disposed;
81
+ /** Row 2 of the model-view matrix; view-space z = row2 · (position, 1). */
82
+ private readonly viewRow2;
83
+ private readonly depthMin;
84
+ private readonly depthScale;
85
+ /** Active splat count; a float compares exactly for counts < 2²⁴. */
86
+ private readonly activeCount;
87
+ /** Highest bucket index this sort uses; see {@link effectiveBucketCount}. */
88
+ private readonly bucketMax;
89
+ private readonly viewCenter;
90
+ constructor(options: {
91
+ renderer: THREE.WebGPURenderer;
92
+ /** Pool capacity in splats; all buffers are sized once from this. */
93
+ capacity: number;
94
+ /** RGBA32F texture holding splat centers (xyz), pool-indexed. */
95
+ centersTexture?: THREE.DataTexture;
96
+ /** Width of the centers texture, to map pool index → texel. */
97
+ dataTextureWidth?: number;
98
+ /** Work-buffer centers (xyzw per splat), as written by a gather pass. */
99
+ centersBuffer?: THREE.StorageBufferAttribute;
100
+ /** The float `splatIndex` buffer the render material reads. */
101
+ splatIndexAttribute: THREE.StorageInstancedBufferAttribute;
102
+ /** Pool indices of the active splats (first `activeCount` entries). */
103
+ sourceIndexAttribute: THREE.StorageBufferAttribute;
104
+ /** Per-source world transform for a unified pool; omit for a single mesh. */
105
+ perSource?: PerSourceSortTransform;
106
+ });
107
+ /**
108
+ * Buckets to actually use for `activeCount` splats, rounded up to a power of
109
+ * two so block indexing stays exact.
110
+ *
111
+ * The histogram is allocated once for the pool's worst case, but clearing and
112
+ * scanning all 2²² buckets costs the same whether one splat is resident or
113
+ * four million - a fixed per-sort bill that dominates on a mobile GPU when
114
+ * the pool is only partly filled (a streaming scene ramping up, or any small
115
+ * scene). Sizing the dispatch to the live splat count keeps the design's
116
+ * ~1-bucket-per-splat depth resolution, so ordering is unaffected, while
117
+ * skipping work on buckets no splat can land in.
118
+ */
119
+ private effectiveBucketCount;
120
+ sort(modelView: THREE.Matrix4, activeCount: number, bounds: THREE.Sphere): boolean;
121
+ dispose(): void;
122
+ }
123
+ /**
124
+ * Frees the GPU buffers of storage attributes that never sat in a geometry.
125
+ * The renderer has no public API for this, so it reaches for the renderer's
126
+ * attribute map directly; a harmless no-op if the internal shape changes, and
127
+ * idempotent (the map's `delete` ignores unknown entries). Shared by the
128
+ * sorters and by owners of mesh-level storage attributes (`sourceIndex`, the
129
+ * unified work buffer) on dispose.
130
+ */
131
+ export declare function releaseRendererAttributes(renderer: THREE.WebGPURenderer, attributes: readonly THREE.BufferAttribute[]): void;
@@ -0,0 +1,99 @@
1
+ /**
2
+ * One-call WebGPU renderer construction for VLAM! scenes.
3
+ *
4
+ * Every host needs the same four things right - adapter, device, raised limits
5
+ * and the platform-dependent `powerPreference` - and getting any of them wrong
6
+ * fails *late*: past ~8.4 M unified splats, or with MSAA silently dropped.
7
+ * {@link createSplatRenderer} is that block, once.
8
+ *
9
+ * The primitives it composes ({@link recommendedWebGpuRequiredLimits},
10
+ * {@link webGpuPowerPreferenceOptions}) stay exported for hosts that own device
11
+ * creation themselves - an XR session sharing a device, say.
12
+ */
13
+ import { WebGPURenderer, type WebGPURendererParameters } from 'three/webgpu';
14
+ import { type WebGpuPowerPreference } from './webgpu-limits';
15
+ /** The device handle `createSplatRenderer` hands to `WebGPURenderer`. */
16
+ export interface SplatRendererGpuDevice {
17
+ readonly features: ReadonlySet<string>;
18
+ }
19
+ /** The adapter surface `createSplatRenderer` reads limits and features from. */
20
+ export interface SplatRendererGpuAdapter {
21
+ readonly features: ReadonlySet<string>;
22
+ readonly limits: {
23
+ readonly maxStorageBufferBindingSize: number;
24
+ readonly maxBufferSize: number;
25
+ readonly maxTextureDimension2D?: number;
26
+ };
27
+ requestDevice(descriptor?: {
28
+ requiredFeatures?: readonly string[];
29
+ requiredLimits?: Record<string, number>;
30
+ }): Promise<SplatRendererGpuDevice>;
31
+ }
32
+ /** The `navigator.gpu` surface `createSplatRenderer` probes. */
33
+ export interface SplatRendererGpu {
34
+ requestAdapter(options?: {
35
+ powerPreference?: WebGpuPowerPreference;
36
+ }): Promise<SplatRendererGpuAdapter | null>;
37
+ }
38
+ /**
39
+ * Options for {@link createSplatRenderer}.
40
+ *
41
+ * Everything `THREE.WebGPURenderer` accepts passes straight through -
42
+ * `antialias`, `forceWebGL`, `trackTimestamp`, `canvas`, `alpha`, `samples`,
43
+ * `getFallback`, … - except the three members the helper owns (`device`,
44
+ * `requiredLimits`, and `powerPreference`, which is widened here to allow
45
+ * `null`).
46
+ */
47
+ export interface CreateSplatRendererOptions extends Omit<WebGPURendererParameters, 'device' | 'requiredLimits' | 'powerPreference'> {
48
+ /**
49
+ * Adapter/renderer power hint. Omitted automatically on Windows, where Chrome
50
+ * ignores it and warns ([crbug.com/369219127](https://crbug.com/369219127)).
51
+ * Pass `null` to never send it.
52
+ *
53
+ * @defaultValue `'high-performance'`
54
+ */
55
+ powerPreference?: WebGpuPowerPreference | null;
56
+ /**
57
+ * Throw instead of degrading to three's WebGL2 backend. Adapter and device
58
+ * failures rethrow their original error, unwrapped.
59
+ *
60
+ * @defaultValue `false`
61
+ */
62
+ requireWebGpu?: boolean;
63
+ /**
64
+ * The WebGPU entry point to probe. Defaults to `navigator.gpu`; pass `null`
65
+ * to skip the probe entirely (tests, non-DOM hosts).
66
+ */
67
+ gpu?: SplatRendererGpu | null;
68
+ }
69
+ /**
70
+ * Creates a `THREE.WebGPURenderer` configured for splat rendering: the
71
+ * adapter's advertised buffer/texture maxima as `requiredLimits`, every adapter
72
+ * feature as `requiredFeatures`, and `powerPreference` only where the platform
73
+ * honours it.
74
+ *
75
+ * Both raised values matter late rather than loudly. Without the limits, large
76
+ * unified or streamed scenes throw past ~8.4 M splats. Without
77
+ * `core-features-and-limits` among the requested features, three treats the
78
+ * backend as compatibility-mode and **silently drops MSAA** - which is why the
79
+ * device is requested here rather than left to `WebGPUBackend.init`. Owning the
80
+ * request also keeps the real failure: three's `getFallback` hook logs a flat
81
+ * "WebGPU is not available" and drops the cause.
82
+ *
83
+ * Degrades to three's WebGL2 backend exactly as an unconfigured
84
+ * `new THREE.WebGPURenderer()` would, warning through
85
+ * {@link setVlamLogHandler} when the cause was a fixable failure rather than a
86
+ * browser without WebGPU. Pass `requireWebGpu: true` to throw instead.
87
+ *
88
+ * Does **not** call `setSize`, `renderer.init()`, or append the canvas - those
89
+ * stay in host code - and does not touch `outputColorSpace`, whose three.js
90
+ * default (sRGB) is already what the splat shader expects.
91
+ *
92
+ * @example
93
+ * const renderer = await createSplatRenderer();
94
+ * renderer.setSize(innerWidth, innerHeight);
95
+ * document.body.appendChild(renderer.domElement);
96
+ *
97
+ * @throws {TypeError} if `requireWebGpu` and `forceWebGL` are both set.
98
+ */
99
+ export declare function createSplatRenderer(options?: CreateSplatRendererOptions): Promise<WebGPURenderer>;