@needle-tools/gltf-progressive 3.4.0-beta → 3.4.0-beta.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/gltf-progressive.js +635 -584
- package/gltf-progressive.min.js +9 -8
- package/gltf-progressive.umd.cjs +8 -7
- package/lib/extension.d.ts +13 -0
- package/lib/extension.js +105 -23
- package/lib/utils.internal.d.ts +6 -0
- package/lib/utils.internal.js +92 -0
- package/lib/version.js +1 -1
- package/package.json +1 -1
package/lib/extension.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BufferGeometry, Mesh, Texture, TextureLoader } from "three";
|
|
2
2
|
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
|
3
3
|
import { addDracoAndKTX2Loaders } from "./loaders.js";
|
|
4
|
-
import { getParam, PromiseQueue, resolveUrl } from "./utils.internal.js";
|
|
4
|
+
import { determineTextureMemoryInBytes, getParam, PromiseQueue, resolveUrl } from "./utils.internal.js";
|
|
5
5
|
import { getRaycastMesh, registerRaycastMesh } from "./utils.js";
|
|
6
6
|
// All of this has to be removed
|
|
7
7
|
// import { getRaycastMesh, setRaycastMesh } from "../../engine_physics.js";
|
|
@@ -11,6 +11,7 @@ import { debug } from "./lods.debug.js";
|
|
|
11
11
|
import { getWorker } from "./worker/loader.mainthread.js";
|
|
12
12
|
const useWorker = getParam("gltf-progressive-worker");
|
|
13
13
|
const reduceMipmaps = getParam("gltf-progressive-reduce-mipmaps");
|
|
14
|
+
const debugGC = getParam("gltf-progressive-gc");
|
|
14
15
|
const $progressiveTextureExtension = Symbol("needle-progressive-texture");
|
|
15
16
|
export const EXTENSION_NAME = "NEEDLE_progressive";
|
|
16
17
|
// #region EXT
|
|
@@ -334,29 +335,37 @@ export class NEEDLE_progressive {
|
|
|
334
335
|
if (assignedLOD && assignedLOD?.level < level) {
|
|
335
336
|
if (debug === "verbose")
|
|
336
337
|
console.warn("Assigned texture level is already higher: ", assignedLOD.level, level, material, assigned, tex);
|
|
338
|
+
// Dispose the newly loaded texture since we're not using it
|
|
339
|
+
// (the assigned texture is higher quality, so we reject the new one)
|
|
340
|
+
// Note: We dispose directly here (not via untrackTextureUsage) because this texture
|
|
341
|
+
// was never tracked/used - it was rejected immediately upon loading
|
|
342
|
+
if (tex && tex !== assigned) {
|
|
343
|
+
if (debug || debugGC) {
|
|
344
|
+
console.log(`[gltf-progressive] Disposing rejected lower-quality texture LOD ${level} (assigned is ${assignedLOD.level})`, tex.uuid);
|
|
345
|
+
}
|
|
346
|
+
tex.dispose();
|
|
347
|
+
}
|
|
337
348
|
return null;
|
|
338
349
|
}
|
|
339
350
|
// assigned.dispose();
|
|
340
351
|
}
|
|
341
|
-
//
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
352
|
+
// Track reference count for new texture
|
|
353
|
+
this.trackTextureUsage(tex);
|
|
354
|
+
// Untrack the old texture (may dispose if ref count hits 0)
|
|
355
|
+
// This prevents accumulation of GPU VRAM while waiting for garbage collection
|
|
356
|
+
if (assigned && assigned !== tex) {
|
|
357
|
+
const wasDisposed = this.untrackTextureUsage(assigned);
|
|
358
|
+
if (wasDisposed && (debug || debugGC)) {
|
|
359
|
+
const assignedLOD = this.getAssignedLODInformation(assigned);
|
|
360
|
+
console.log(`[gltf-progressive] Disposed old texture LOD ${assignedLOD?.level ?? '?'} → ${level} for ${material.name || material.type}.${slot}`, assigned.uuid);
|
|
348
361
|
}
|
|
349
362
|
}
|
|
350
363
|
material[slot] = tex;
|
|
351
364
|
}
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
// if (!users) {
|
|
357
|
-
// if (debug) console.log("Progressive: Dispose texture", current.name, current.source.data, current.uuid);
|
|
358
|
-
// current?.dispose();
|
|
359
|
-
// }
|
|
365
|
+
// Note: We use reference counting above to track texture usage across multiple materials.
|
|
366
|
+
// When the reference count hits zero, GPU memory (VRAM) is freed immediately via gl.deleteTexture(),
|
|
367
|
+
// not waiting for JavaScript garbage collection which may take seconds/minutes.
|
|
368
|
+
// This handles cases where the same texture is shared across multiple materials/objects.
|
|
360
369
|
}
|
|
361
370
|
// this.onProgressiveLoadEnd(info);
|
|
362
371
|
return tex;
|
|
@@ -531,6 +540,7 @@ export class NEEDLE_progressive {
|
|
|
531
540
|
* Dispose cached resources to free memory.
|
|
532
541
|
* Call this when a model is removed from the scene to allow garbage collection of its LOD resources.
|
|
533
542
|
* Calls three.js `.dispose()` on cached Textures and BufferGeometries to free GPU memory.
|
|
543
|
+
* Also clears reference counts for disposed textures.
|
|
534
544
|
* @param guid Optional GUID to dispose resources for a specific model. If omitted, all cached resources are cleared.
|
|
535
545
|
*/
|
|
536
546
|
static dispose(guid) {
|
|
@@ -542,7 +552,9 @@ export class NEEDLE_progressive {
|
|
|
542
552
|
const lowres = lowresRef.deref();
|
|
543
553
|
if (lowres) {
|
|
544
554
|
if (lowres.isTexture) {
|
|
545
|
-
lowres
|
|
555
|
+
const tex = lowres;
|
|
556
|
+
this.textureRefCounts.delete(tex.uuid); // Clear ref count
|
|
557
|
+
tex.dispose();
|
|
546
558
|
}
|
|
547
559
|
else if (Array.isArray(lowres)) {
|
|
548
560
|
for (const geo of lowres)
|
|
@@ -565,7 +577,9 @@ export class NEEDLE_progressive {
|
|
|
565
577
|
const entry = entryRef.deref();
|
|
566
578
|
if (entry) {
|
|
567
579
|
if (entry.isTexture) {
|
|
568
|
-
entry
|
|
580
|
+
const tex = entry;
|
|
581
|
+
this.textureRefCounts.delete(tex.uuid); // Clear ref count
|
|
582
|
+
tex.dispose();
|
|
569
583
|
}
|
|
570
584
|
else if (Array.isArray(entry)) {
|
|
571
585
|
for (const geo of entry)
|
|
@@ -578,6 +592,8 @@ export class NEEDLE_progressive {
|
|
|
578
592
|
this._disposeCacheEntry(entry);
|
|
579
593
|
}
|
|
580
594
|
this.cache.clear();
|
|
595
|
+
// Clear all texture reference counts when disposing everything
|
|
596
|
+
this.textureRefCounts.clear();
|
|
581
597
|
}
|
|
582
598
|
}
|
|
583
599
|
/** Dispose a single cache entry's three.js resource(s) to free GPU memory. */
|
|
@@ -585,7 +601,13 @@ export class NEEDLE_progressive {
|
|
|
585
601
|
if (entry instanceof WeakRef) {
|
|
586
602
|
// Single resource — deref and dispose if still alive
|
|
587
603
|
const resource = entry.deref();
|
|
588
|
-
resource
|
|
604
|
+
if (resource) {
|
|
605
|
+
// Clear ref count for textures
|
|
606
|
+
if (resource.isTexture) {
|
|
607
|
+
this.textureRefCounts.delete(resource.uuid);
|
|
608
|
+
}
|
|
609
|
+
resource.dispose();
|
|
610
|
+
}
|
|
589
611
|
}
|
|
590
612
|
else {
|
|
591
613
|
// Promise — may be in-flight or already resolved.
|
|
@@ -597,6 +619,10 @@ export class NEEDLE_progressive {
|
|
|
597
619
|
geo.dispose();
|
|
598
620
|
}
|
|
599
621
|
else {
|
|
622
|
+
// Clear ref count for textures
|
|
623
|
+
if (resource.isTexture) {
|
|
624
|
+
this.textureRefCounts.delete(resource.uuid);
|
|
625
|
+
}
|
|
600
626
|
resource.dispose();
|
|
601
627
|
}
|
|
602
628
|
}
|
|
@@ -609,6 +635,8 @@ export class NEEDLE_progressive {
|
|
|
609
635
|
static cache = new Map();
|
|
610
636
|
/** this contains the geometry/textures that were originally loaded. Uses WeakRef to allow garbage collection when unused. */
|
|
611
637
|
static lowresCache = new Map();
|
|
638
|
+
/** Reference counting for textures to track usage across multiple materials/objects */
|
|
639
|
+
static textureRefCounts = new Map();
|
|
612
640
|
/**
|
|
613
641
|
* FinalizationRegistry to automatically clean up `previouslyLoaded` cache entries
|
|
614
642
|
* when their associated three.js resources are garbage collected by the browser.
|
|
@@ -616,18 +644,72 @@ export class NEEDLE_progressive {
|
|
|
616
644
|
*/
|
|
617
645
|
static _resourceRegistry = new FinalizationRegistry((cacheKey) => {
|
|
618
646
|
const entry = NEEDLE_progressive.cache.get(cacheKey);
|
|
619
|
-
if (debug)
|
|
620
|
-
console.debug(`[gltf-progressive] Resource GC'd\n${cacheKey}`);
|
|
647
|
+
if (debug || debugGC)
|
|
648
|
+
console.debug(`[gltf-progressive] Memory: Resource GC'd\n${cacheKey}`);
|
|
621
649
|
// Only delete if the entry is still a WeakRef and the resource is gone
|
|
622
650
|
if (entry instanceof WeakRef) {
|
|
623
651
|
const derefed = entry.deref();
|
|
624
652
|
if (!derefed) {
|
|
625
653
|
NEEDLE_progressive.cache.delete(cacheKey);
|
|
626
|
-
if (debug)
|
|
627
|
-
console.log(`[gltf-progressive] Cache entry
|
|
654
|
+
if (debug || debugGC)
|
|
655
|
+
console.log(`[gltf-progressive] ↪ Cache entry deleted (GC)`);
|
|
628
656
|
}
|
|
629
657
|
}
|
|
630
658
|
});
|
|
659
|
+
/**
|
|
660
|
+
* Track texture usage by incrementing reference count
|
|
661
|
+
*/
|
|
662
|
+
static trackTextureUsage(texture) {
|
|
663
|
+
const uuid = texture.uuid;
|
|
664
|
+
const count = this.textureRefCounts.get(uuid) || 0;
|
|
665
|
+
this.textureRefCounts.set(uuid, count + 1);
|
|
666
|
+
if (debug === "verbose") {
|
|
667
|
+
console.log(`[gltf-progressive] Track texture ${uuid}, refCount: ${count} → ${count + 1}`);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Untrack texture usage by decrementing reference count.
|
|
672
|
+
* Automatically disposes the texture when reference count reaches zero.
|
|
673
|
+
* @returns true if the texture was disposed, false otherwise
|
|
674
|
+
*/
|
|
675
|
+
static untrackTextureUsage(texture) {
|
|
676
|
+
const uuid = texture.uuid;
|
|
677
|
+
const count = this.textureRefCounts.get(uuid);
|
|
678
|
+
if (!count) {
|
|
679
|
+
// Texture wasn't tracked, dispose immediately (safe fallback)
|
|
680
|
+
if (debug === "verbose" || debugGC) {
|
|
681
|
+
logDebugInfo(`[gltf-progressive] Memory: Untrack untracked texture (dispose immediately)`, 0);
|
|
682
|
+
}
|
|
683
|
+
texture.dispose();
|
|
684
|
+
return true;
|
|
685
|
+
}
|
|
686
|
+
const newCount = count - 1;
|
|
687
|
+
if (newCount <= 0) {
|
|
688
|
+
this.textureRefCounts.delete(uuid);
|
|
689
|
+
if (debug || debugGC) {
|
|
690
|
+
logDebugInfo(`[gltf-progressive] Memory: Dispose texture`, newCount);
|
|
691
|
+
}
|
|
692
|
+
texture.dispose();
|
|
693
|
+
return true;
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
this.textureRefCounts.set(uuid, newCount);
|
|
697
|
+
if (debug === "verbose") {
|
|
698
|
+
logDebugInfo(`[gltf-progressive] Memory: Untrack texture`, newCount);
|
|
699
|
+
}
|
|
700
|
+
return false;
|
|
701
|
+
}
|
|
702
|
+
function logDebugInfo(prefix, newCount) {
|
|
703
|
+
let width = texture.image?.width || texture.source?.data?.width || 0;
|
|
704
|
+
let height = texture.image?.height || texture.source?.data?.height || 0;
|
|
705
|
+
const textureSize = width && height ? `${width}x${height}` : "N/A";
|
|
706
|
+
let memorySize = "N/A";
|
|
707
|
+
if (width && height) {
|
|
708
|
+
memorySize = `~${(determineTextureMemoryInBytes(texture) / (1024 * 1024)).toFixed(2)} MB`;
|
|
709
|
+
}
|
|
710
|
+
console.log(`${prefix} — ${texture.name} ${textureSize} (${memorySize}), refCount: ${count} → ${newCount}\n${uuid}`);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
631
713
|
static workers = [];
|
|
632
714
|
static _workersIndex = 0;
|
|
633
715
|
static async getOrLoadLOD(current, level) {
|
package/lib/utils.internal.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Texture } from "three";
|
|
1
2
|
export declare function isDebugMode(): string | boolean;
|
|
2
3
|
export declare function getParam(name: string): boolean | string;
|
|
3
4
|
export declare function resolveUrl(source: string | undefined, uri: string): string;
|
|
@@ -33,3 +34,8 @@ export declare class PromiseQueue<T = any> {
|
|
|
33
34
|
private add;
|
|
34
35
|
private internalUpdate;
|
|
35
36
|
}
|
|
37
|
+
export declare function determineTextureMemoryInBytes(texture: Texture): number;
|
|
38
|
+
/**
|
|
39
|
+
* Detect the GPU memory of the current device. This is a very rough estimate based on the renderer information, and may not be accurate. It returns the estimated memory in MB, or `undefined` if it cannot be detected.
|
|
40
|
+
*/
|
|
41
|
+
export declare function detectGPUMemory(): number | undefined;
|
package/lib/utils.internal.js
CHANGED
|
@@ -115,3 +115,95 @@ export class PromiseQueue {
|
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
|
+
// #region Texture Memory
|
|
119
|
+
export function determineTextureMemoryInBytes(texture) {
|
|
120
|
+
const width = texture.image?.width ?? 0;
|
|
121
|
+
const height = texture.image?.height ?? 0;
|
|
122
|
+
const depth = texture.image?.depth ?? 1;
|
|
123
|
+
const mipLevels = Math.floor(Math.log2(Math.max(width, height, depth))) + 1;
|
|
124
|
+
const bytesPerPixel = getBytesPerPixel(texture);
|
|
125
|
+
const totalBytes = (width * height * depth * bytesPerPixel * (1 - Math.pow(0.25, mipLevels))) / (1 - 0.25);
|
|
126
|
+
return totalBytes;
|
|
127
|
+
}
|
|
128
|
+
function getBytesPerPixel(texture) {
|
|
129
|
+
// Determine channel count from format
|
|
130
|
+
let channels = 4; // Default RGBA
|
|
131
|
+
const format = texture.format;
|
|
132
|
+
if (format === 1024)
|
|
133
|
+
channels = 1; // RedFormat
|
|
134
|
+
else if (format === 1025)
|
|
135
|
+
channels = 1; // RedIntegerFormat
|
|
136
|
+
else if (format === 1026)
|
|
137
|
+
channels = 2; // RGFormat
|
|
138
|
+
else if (format === 1027)
|
|
139
|
+
channels = 2; // RGIntegerFormat
|
|
140
|
+
else if (format === 1022)
|
|
141
|
+
channels = 3; // RGBFormat
|
|
142
|
+
else if (format === 1029)
|
|
143
|
+
channels = 3; // RGBIntegerFormat
|
|
144
|
+
else if (format === 1023)
|
|
145
|
+
channels = 4; // RGBAFormat
|
|
146
|
+
else if (format === 1033)
|
|
147
|
+
channels = 4; // RGBAIntegerFormat
|
|
148
|
+
// Determine bytes per channel from type
|
|
149
|
+
let bytesPerChannel = 1; // UnsignedByteType default
|
|
150
|
+
const type = texture.type;
|
|
151
|
+
if (type === 1009)
|
|
152
|
+
bytesPerChannel = 1; // UnsignedByteType
|
|
153
|
+
else if (type === 1010)
|
|
154
|
+
bytesPerChannel = 1; // ByteType
|
|
155
|
+
else if (type === 1011)
|
|
156
|
+
bytesPerChannel = 2; // ShortType
|
|
157
|
+
else if (type === 1012)
|
|
158
|
+
bytesPerChannel = 2; // UnsignedShortType
|
|
159
|
+
else if (type === 1013)
|
|
160
|
+
bytesPerChannel = 4; // IntType
|
|
161
|
+
else if (type === 1014)
|
|
162
|
+
bytesPerChannel = 4; // UnsignedIntType
|
|
163
|
+
else if (type === 1015)
|
|
164
|
+
bytesPerChannel = 4; // FloatType
|
|
165
|
+
else if (type === 1016)
|
|
166
|
+
bytesPerChannel = 2; // HalfFloatType
|
|
167
|
+
const bytesPerPixel = channels * bytesPerChannel;
|
|
168
|
+
return bytesPerPixel;
|
|
169
|
+
}
|
|
170
|
+
// #region GPU
|
|
171
|
+
let rendererInfo;
|
|
172
|
+
/**
|
|
173
|
+
* Detect the GPU memory of the current device. This is a very rough estimate based on the renderer information, and may not be accurate. It returns the estimated memory in MB, or `undefined` if it cannot be detected.
|
|
174
|
+
*/
|
|
175
|
+
export function detectGPUMemory() {
|
|
176
|
+
if (rendererInfo !== undefined) {
|
|
177
|
+
return rendererInfo?.estimatedMemory;
|
|
178
|
+
}
|
|
179
|
+
const canvas = document.createElement('canvas');
|
|
180
|
+
const powerPreference = "high-performance";
|
|
181
|
+
const gl = canvas.getContext('webgl', { powerPreference }) || canvas.getContext('experimental-webgl', { powerPreference });
|
|
182
|
+
if (!gl) {
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
if ("getExtension" in gl) {
|
|
186
|
+
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
|
|
187
|
+
if (debugInfo) {
|
|
188
|
+
const vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
|
|
189
|
+
const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
|
|
190
|
+
// Estimate memory based on renderer information (this is a very rough estimate)
|
|
191
|
+
let estimatedMemory = 512;
|
|
192
|
+
if (/NVIDIA/i.test(renderer)) {
|
|
193
|
+
estimatedMemory = 2048;
|
|
194
|
+
}
|
|
195
|
+
else if (/AMD/i.test(renderer)) {
|
|
196
|
+
estimatedMemory = 1024;
|
|
197
|
+
}
|
|
198
|
+
else if (/Intel/i.test(renderer)) {
|
|
199
|
+
estimatedMemory = 512;
|
|
200
|
+
}
|
|
201
|
+
rendererInfo = { vendor, renderer, estimatedMemory };
|
|
202
|
+
return estimatedMemory;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
rendererInfo = null;
|
|
207
|
+
}
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
package/lib/version.js
CHANGED
package/package.json
CHANGED