streaming-gltf 1.0.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.
- package/LICENSE +21 -0
- package/README.md +90 -0
- package/examples/local-progressive/batched-far-tier.js +296 -0
- package/examples/local-progressive/buffer-pool.js +182 -0
- package/examples/local-progressive/deferred-load-queue.js +253 -0
- package/examples/local-progressive/draw-call-batching.js +615 -0
- package/examples/local-progressive/draw-call-sorter.js +146 -0
- package/examples/local-progressive/frustum-cache.js +104 -0
- package/examples/local-progressive/lod-unload-manager.js +162 -0
- package/examples/local-progressive/lod-worker.js +297 -0
- package/examples/local-progressive/material-pool.js +241 -0
- package/examples/local-progressive/model-pool.js +2961 -0
- package/examples/local-progressive/multi-draw-optimizer.js +347 -0
- package/examples/local-progressive/multi-draw-utils.js +199 -0
- package/examples/local-progressive/stress.js +655 -0
- package/examples/local-progressive/vertex-compression.js +128 -0
- package/index.js +23 -0
- package/package.json +48 -0
- package/tools/bake-all.mjs +126 -0
- package/tools/bake-progressive.mjs +663 -0
- package/tools/bake-streaming.mjs +453 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
// DeferredLoadQueue — priority-based async LOD loading with concurrent throttling.
|
|
2
|
+
//
|
|
3
|
+
// Responsibilities:
|
|
4
|
+
// - Queue LOD load requests with distance-based priority scoring.
|
|
5
|
+
// - Limit concurrent asset fetches to a configurable max (typically 2).
|
|
6
|
+
// - Track pending and loaded LODs per asset to avoid re-queuing.
|
|
7
|
+
// - Expose queue stats for HUD display (pending count, load time, etc).
|
|
8
|
+
//
|
|
9
|
+
// Design:
|
|
10
|
+
// - Each request is { asset, lod, priority, entity, timestamp }.
|
|
11
|
+
// - Priority = negative distance (closer = higher priority).
|
|
12
|
+
// - Two heaps: PENDING (sorted by priority) and IN_FLIGHT (tracks active loads).
|
|
13
|
+
// - Throttle: when a load completes, pop the next item from PENDING.
|
|
14
|
+
|
|
15
|
+
export class DeferredLoadQueue {
|
|
16
|
+
constructor(maxConcurrent = 2, maxQueueSize = 50, requestTimeoutMs = 5000) {
|
|
17
|
+
this.maxConcurrent = maxConcurrent;
|
|
18
|
+
this.maxQueueSize = maxQueueSize;
|
|
19
|
+
this.requestTimeoutMs = requestTimeoutMs;
|
|
20
|
+
this._inFlight = 0;
|
|
21
|
+
this._pending = []; // min-heap of { asset, meshDescIdx, lodIdx, priority, entity, timestamp, abortController }
|
|
22
|
+
this._pending_set = new Set(); // dedup key: `assetUrl:meshDescIdx:lodIdx`
|
|
23
|
+
this._loading = new Map(); // meshDescIdx:lodIdx -> Promise
|
|
24
|
+
this._loadedLods = new Map(); // assetUrl -> Set of loaded lodIdxs per meshDescIdx
|
|
25
|
+
this._timeoutHandles = new Map(); // key -> timeoutId
|
|
26
|
+
this._stats = {
|
|
27
|
+
queued: 0,
|
|
28
|
+
inFlight: 0,
|
|
29
|
+
totalLoaded: 0,
|
|
30
|
+
avgLoadTimeMs: 0,
|
|
31
|
+
loadTimes: [], // rolling window of recent load times
|
|
32
|
+
dropped: 0, // dropped due to timeout or queue overflow
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Enqueue a LOD for loading. Returns true if queued, false if already loaded or pending.
|
|
37
|
+
queueLoad(asset, meshDescIdx, lodIdx, priority = 0, entity = null) {
|
|
38
|
+
if (!asset || meshDescIdx == null || lodIdx == null) return false;
|
|
39
|
+
|
|
40
|
+
const key = `${asset.url}:${meshDescIdx}:${lodIdx}`;
|
|
41
|
+
|
|
42
|
+
// Check if already loaded
|
|
43
|
+
const assetLoads = this._loadedLods.get(asset.url);
|
|
44
|
+
if (assetLoads?.has(key)) return false;
|
|
45
|
+
|
|
46
|
+
// Check if already in pending queue
|
|
47
|
+
if (this._pending_set.has(key)) return false;
|
|
48
|
+
|
|
49
|
+
// Check if actively loading
|
|
50
|
+
if (this._loading.has(key)) return false;
|
|
51
|
+
|
|
52
|
+
// If queue is too large, drop oldest low-priority requests
|
|
53
|
+
if (this._pending.length >= this.maxQueueSize) {
|
|
54
|
+
const dropped = this._pending.pop(); // remove lowest priority (heap tail)
|
|
55
|
+
this._pending_set.delete(dropped.key);
|
|
56
|
+
clearTimeout(this._timeoutHandles.get(dropped.key));
|
|
57
|
+
this._timeoutHandles.delete(dropped.key);
|
|
58
|
+
this._stats.dropped++;
|
|
59
|
+
console.warn(`[deferred-queue] Queue size exceeded ${this.maxQueueSize}, dropped lowest-priority LOD: ${dropped.key}`);
|
|
60
|
+
if (this._pending.length > 0) this._bubbleDown(0); // maintain heap after removal
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Add to pending queue
|
|
64
|
+
const item = {
|
|
65
|
+
asset,
|
|
66
|
+
meshDescIdx,
|
|
67
|
+
lodIdx,
|
|
68
|
+
priority, // negative distance (closer = higher priority)
|
|
69
|
+
entity,
|
|
70
|
+
timestamp: performance.now(),
|
|
71
|
+
key,
|
|
72
|
+
};
|
|
73
|
+
this._pending.push(item);
|
|
74
|
+
this._pending_set.add(key);
|
|
75
|
+
this._stats.queued = this._pending.length;
|
|
76
|
+
|
|
77
|
+
// Re-heapify to maintain min-heap property (higher priority at root)
|
|
78
|
+
this._bubbleUp(this._pending.length - 1);
|
|
79
|
+
|
|
80
|
+
// Set timeout to drop stale request
|
|
81
|
+
const timeoutId = setTimeout(() => {
|
|
82
|
+
this._removeRequest(key);
|
|
83
|
+
console.warn(`[deferred-queue] LOD request timed out after ${this.requestTimeoutMs}ms: ${key}`);
|
|
84
|
+
}, this.requestTimeoutMs);
|
|
85
|
+
this._timeoutHandles.set(key, timeoutId);
|
|
86
|
+
|
|
87
|
+
// Try to load immediately if under concurrent limit
|
|
88
|
+
this._processNext();
|
|
89
|
+
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Remove a request from the queue (called by timeout or cleanup)
|
|
94
|
+
_removeRequest(key) {
|
|
95
|
+
if (!this._pending_set.has(key)) return;
|
|
96
|
+
|
|
97
|
+
this._pending_set.delete(key);
|
|
98
|
+
this._stats.dropped++;
|
|
99
|
+
|
|
100
|
+
// Find and remove from heap
|
|
101
|
+
const idx = this._pending.findIndex(item => item.key === key);
|
|
102
|
+
if (idx >= 0) {
|
|
103
|
+
this._pending.splice(idx, 1);
|
|
104
|
+
if (idx < this._pending.length) this._bubbleDown(idx);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
clearTimeout(this._timeoutHandles.get(key));
|
|
108
|
+
this._timeoutHandles.delete(key);
|
|
109
|
+
this._stats.queued = this._pending.length;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Process next pending load if under concurrent limit
|
|
113
|
+
_processNext() {
|
|
114
|
+
if (this._inFlight >= this.maxConcurrent || !this._pending.length) return;
|
|
115
|
+
|
|
116
|
+
const item = this._popHighestPriority();
|
|
117
|
+
if (!item) return;
|
|
118
|
+
|
|
119
|
+
this._inFlight++;
|
|
120
|
+
this._stats.inFlight = this._inFlight;
|
|
121
|
+
const tLoad0 = performance.now();
|
|
122
|
+
const key = item.key;
|
|
123
|
+
|
|
124
|
+
// Clear timeout since load is starting
|
|
125
|
+
clearTimeout(this._timeoutHandles.get(key));
|
|
126
|
+
this._timeoutHandles.delete(key);
|
|
127
|
+
|
|
128
|
+
// Track this load in flight
|
|
129
|
+
const promise = item.asset.ensureMeshLod(item.meshDescIdx, item.lodIdx)
|
|
130
|
+
.then((geo) => {
|
|
131
|
+
const tLoad1 = performance.now();
|
|
132
|
+
const loadTime = tLoad1 - item.timestamp;
|
|
133
|
+
|
|
134
|
+
// Record load time for stats
|
|
135
|
+
this._stats.loadTimes.push(loadTime);
|
|
136
|
+
if (this._stats.loadTimes.length > 20) this._stats.loadTimes.shift(); // rolling window
|
|
137
|
+
this._stats.avgLoadTimeMs = this._stats.loadTimes.reduce((a, b) => a + b, 0) / this._stats.loadTimes.length;
|
|
138
|
+
this._stats.totalLoaded++;
|
|
139
|
+
|
|
140
|
+
// Mark as loaded
|
|
141
|
+
if (!this._loadedLods.has(item.asset.url)) {
|
|
142
|
+
this._loadedLods.set(item.asset.url, new Set());
|
|
143
|
+
}
|
|
144
|
+
this._loadedLods.get(item.asset.url).add(key);
|
|
145
|
+
|
|
146
|
+
return geo;
|
|
147
|
+
})
|
|
148
|
+
.finally(() => {
|
|
149
|
+
this._inFlight--;
|
|
150
|
+
this._stats.inFlight = this._inFlight;
|
|
151
|
+
this._loading.delete(key);
|
|
152
|
+
// Process next pending load
|
|
153
|
+
this._processNext();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
this._loading.set(key, promise);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Min-heap: bubble up (used when inserting)
|
|
160
|
+
_bubbleUp(idx) {
|
|
161
|
+
if (idx <= 0) return;
|
|
162
|
+
const parent = Math.floor((idx - 1) / 2);
|
|
163
|
+
if (this._pending[idx].priority > this._pending[parent].priority) {
|
|
164
|
+
[this._pending[idx], this._pending[parent]] = [this._pending[parent], this._pending[idx]];
|
|
165
|
+
this._bubbleUp(parent);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Min-heap: bubble down (used when removing root)
|
|
170
|
+
_bubbleDown(idx) {
|
|
171
|
+
const left = 2 * idx + 1;
|
|
172
|
+
const right = 2 * idx + 2;
|
|
173
|
+
let smallest = idx;
|
|
174
|
+
|
|
175
|
+
if (left < this._pending.length && this._pending[left].priority > this._pending[smallest].priority) {
|
|
176
|
+
smallest = left;
|
|
177
|
+
}
|
|
178
|
+
if (right < this._pending.length && this._pending[right].priority > this._pending[smallest].priority) {
|
|
179
|
+
smallest = right;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (smallest !== idx) {
|
|
183
|
+
[this._pending[idx], this._pending[smallest]] = [this._pending[smallest], this._pending[idx]];
|
|
184
|
+
this._bubbleDown(smallest);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Extract and return the highest-priority item
|
|
189
|
+
_popHighestPriority() {
|
|
190
|
+
if (!this._pending.length) return null;
|
|
191
|
+
|
|
192
|
+
const root = this._pending[0];
|
|
193
|
+
this._pending_set.delete(root.key);
|
|
194
|
+
this._pending.splice(0, 1);
|
|
195
|
+
if (this._pending.length > 0) this._bubbleDown(0);
|
|
196
|
+
this._stats.queued = this._pending.length;
|
|
197
|
+
|
|
198
|
+
return root;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Get the list of loaded LODs for an asset
|
|
202
|
+
getLoadedLods(asset) {
|
|
203
|
+
return this._loadedLods.get(asset.url) || new Set();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Check if a specific LOD is loaded
|
|
207
|
+
isLodLoaded(assetUrl, meshDescIdx, lodIdx) {
|
|
208
|
+
const key = `${assetUrl}:${meshDescIdx}:${lodIdx}`;
|
|
209
|
+
return this._loadedLods.get(assetUrl)?.has(key) ?? false;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Unload a LOD from memory (called by unload manager)
|
|
213
|
+
unloadLod(asset, meshDescIdx, lodIdx) {
|
|
214
|
+
const key = `${asset.url}:${meshDescIdx}:${lodIdx}`;
|
|
215
|
+
const loads = this._loadedLods.get(asset.url);
|
|
216
|
+
if (loads) {
|
|
217
|
+
loads.delete(key);
|
|
218
|
+
}
|
|
219
|
+
// Geometry disposal is handled by the Asset itself via evictMeshLod
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Get current queue stats
|
|
223
|
+
getStats() {
|
|
224
|
+
return {
|
|
225
|
+
queued: this._pending.length,
|
|
226
|
+
inFlight: this._inFlight,
|
|
227
|
+
totalLoaded: this._stats.totalLoaded,
|
|
228
|
+
avgLoadTimeMs: this._stats.avgLoadTimeMs.toFixed(1),
|
|
229
|
+
concurrency: this._inFlight,
|
|
230
|
+
maxConcurrency: this.maxConcurrent,
|
|
231
|
+
dropped: this._stats.dropped,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Update priorities for pending items based on new entity positions
|
|
236
|
+
// Called when entity moves or camera changes to re-sort the queue
|
|
237
|
+
updatePriorities(entities) {
|
|
238
|
+
if (!this._pending.length) return;
|
|
239
|
+
|
|
240
|
+
// Recalculate priorities based on current entity positions
|
|
241
|
+
for (const item of this._pending) {
|
|
242
|
+
if (item.entity) {
|
|
243
|
+
const dist = item.entity._currentDistance ?? Infinity;
|
|
244
|
+
item.priority = -dist; // negative distance (closer = higher priority)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Re-heapify entire array (simpler than selective bubbling)
|
|
249
|
+
for (let i = Math.floor(this._pending.length / 2) - 1; i >= 0; i--) {
|
|
250
|
+
this._bubbleDown(i);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|