@needle-tools/gltf-progressive 3.3.5 → 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/NEEDLE_progressive/README.md +45 -47
- package/gltf-progressive.js +697 -569
- package/gltf-progressive.min.js +9 -7
- package/gltf-progressive.umd.cjs +9 -7
- package/lib/extension.d.ts +31 -3
- package/lib/extension.js +301 -56
- 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;
|
|
@@ -493,7 +502,7 @@ export class NEEDLE_progressive {
|
|
|
493
502
|
const key = ext.guid;
|
|
494
503
|
NEEDLE_progressive.assignLODInformation(url, tex, key, level, index);
|
|
495
504
|
NEEDLE_progressive.lodInfos.set(key, ext);
|
|
496
|
-
NEEDLE_progressive.lowresCache.set(key, tex);
|
|
505
|
+
NEEDLE_progressive.lowresCache.set(key, new WeakRef(tex));
|
|
497
506
|
};
|
|
498
507
|
/**
|
|
499
508
|
* Register a mesh with LOD information
|
|
@@ -511,12 +520,15 @@ export class NEEDLE_progressive {
|
|
|
511
520
|
console.log("> Progressive: register mesh " + mesh.name, { index, uuid: mesh.uuid }, ext, mesh);
|
|
512
521
|
NEEDLE_progressive.assignLODInformation(url, geometry, key, level, index);
|
|
513
522
|
NEEDLE_progressive.lodInfos.set(key, ext);
|
|
514
|
-
|
|
515
|
-
|
|
523
|
+
const existingRef = NEEDLE_progressive.lowresCache.get(key);
|
|
524
|
+
let existing = existingRef?.deref();
|
|
525
|
+
if (existing) {
|
|
516
526
|
existing.push(mesh.geometry);
|
|
517
|
-
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
518
529
|
existing = [mesh.geometry];
|
|
519
|
-
|
|
530
|
+
}
|
|
531
|
+
NEEDLE_progressive.lowresCache.set(key, new WeakRef(existing));
|
|
520
532
|
if (level > 0 && !getRaycastMesh(mesh)) {
|
|
521
533
|
registerRaycastMesh(mesh, geometry);
|
|
522
534
|
}
|
|
@@ -524,12 +536,180 @@ export class NEEDLE_progressive {
|
|
|
524
536
|
plugin.onRegisteredNewMesh?.(mesh, ext);
|
|
525
537
|
}
|
|
526
538
|
};
|
|
539
|
+
/**
|
|
540
|
+
* Dispose cached resources to free memory.
|
|
541
|
+
* Call this when a model is removed from the scene to allow garbage collection of its LOD resources.
|
|
542
|
+
* Calls three.js `.dispose()` on cached Textures and BufferGeometries to free GPU memory.
|
|
543
|
+
* Also clears reference counts for disposed textures.
|
|
544
|
+
* @param guid Optional GUID to dispose resources for a specific model. If omitted, all cached resources are cleared.
|
|
545
|
+
*/
|
|
546
|
+
static dispose(guid) {
|
|
547
|
+
if (guid) {
|
|
548
|
+
this.lodInfos.delete(guid);
|
|
549
|
+
// Dispose lowres cache entries (original proxy resources)
|
|
550
|
+
const lowresRef = this.lowresCache.get(guid);
|
|
551
|
+
if (lowresRef) {
|
|
552
|
+
const lowres = lowresRef.deref();
|
|
553
|
+
if (lowres) {
|
|
554
|
+
if (lowres.isTexture) {
|
|
555
|
+
const tex = lowres;
|
|
556
|
+
this.textureRefCounts.delete(tex.uuid); // Clear ref count
|
|
557
|
+
tex.dispose();
|
|
558
|
+
}
|
|
559
|
+
else if (Array.isArray(lowres)) {
|
|
560
|
+
for (const geo of lowres)
|
|
561
|
+
geo.dispose();
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
this.lowresCache.delete(guid);
|
|
565
|
+
}
|
|
566
|
+
// Dispose previously loaded LOD entries
|
|
567
|
+
for (const [key, entry] of this.cache) {
|
|
568
|
+
if (key.includes(guid)) {
|
|
569
|
+
this._disposeCacheEntry(entry);
|
|
570
|
+
this.cache.delete(key);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
this.lodInfos.clear();
|
|
576
|
+
for (const [, entryRef] of this.lowresCache) {
|
|
577
|
+
const entry = entryRef.deref();
|
|
578
|
+
if (entry) {
|
|
579
|
+
if (entry.isTexture) {
|
|
580
|
+
const tex = entry;
|
|
581
|
+
this.textureRefCounts.delete(tex.uuid); // Clear ref count
|
|
582
|
+
tex.dispose();
|
|
583
|
+
}
|
|
584
|
+
else if (Array.isArray(entry)) {
|
|
585
|
+
for (const geo of entry)
|
|
586
|
+
geo.dispose();
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
this.lowresCache.clear();
|
|
591
|
+
for (const [, entry] of this.cache) {
|
|
592
|
+
this._disposeCacheEntry(entry);
|
|
593
|
+
}
|
|
594
|
+
this.cache.clear();
|
|
595
|
+
// Clear all texture reference counts when disposing everything
|
|
596
|
+
this.textureRefCounts.clear();
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
/** Dispose a single cache entry's three.js resource(s) to free GPU memory. */
|
|
600
|
+
static _disposeCacheEntry(entry) {
|
|
601
|
+
if (entry instanceof WeakRef) {
|
|
602
|
+
// Single resource — deref and dispose if still alive
|
|
603
|
+
const resource = entry.deref();
|
|
604
|
+
if (resource) {
|
|
605
|
+
// Clear ref count for textures
|
|
606
|
+
if (resource.isTexture) {
|
|
607
|
+
this.textureRefCounts.delete(resource.uuid);
|
|
608
|
+
}
|
|
609
|
+
resource.dispose();
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
else {
|
|
613
|
+
// Promise — may be in-flight or already resolved.
|
|
614
|
+
// Attach disposal to run after resolution.
|
|
615
|
+
entry.then(resource => {
|
|
616
|
+
if (resource) {
|
|
617
|
+
if (Array.isArray(resource)) {
|
|
618
|
+
for (const geo of resource)
|
|
619
|
+
geo.dispose();
|
|
620
|
+
}
|
|
621
|
+
else {
|
|
622
|
+
// Clear ref count for textures
|
|
623
|
+
if (resource.isTexture) {
|
|
624
|
+
this.textureRefCounts.delete(resource.uuid);
|
|
625
|
+
}
|
|
626
|
+
resource.dispose();
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}).catch(() => { });
|
|
630
|
+
}
|
|
631
|
+
}
|
|
527
632
|
/** A map of key = asset uuid and value = LOD information */
|
|
528
633
|
static lodInfos = new Map();
|
|
529
|
-
/** cache of already loaded mesh lods */
|
|
530
|
-
static
|
|
531
|
-
/** this contains the geometry/textures that were originally loaded */
|
|
634
|
+
/** cache of already loaded mesh lods. Uses WeakRef for single resources to allow garbage collection when unused. */
|
|
635
|
+
static cache = new Map();
|
|
636
|
+
/** this contains the geometry/textures that were originally loaded. Uses WeakRef to allow garbage collection when unused. */
|
|
532
637
|
static lowresCache = new Map();
|
|
638
|
+
/** Reference counting for textures to track usage across multiple materials/objects */
|
|
639
|
+
static textureRefCounts = new Map();
|
|
640
|
+
/**
|
|
641
|
+
* FinalizationRegistry to automatically clean up `previouslyLoaded` cache entries
|
|
642
|
+
* when their associated three.js resources are garbage collected by the browser.
|
|
643
|
+
* The held value is the cache key string used in `previouslyLoaded`.
|
|
644
|
+
*/
|
|
645
|
+
static _resourceRegistry = new FinalizationRegistry((cacheKey) => {
|
|
646
|
+
const entry = NEEDLE_progressive.cache.get(cacheKey);
|
|
647
|
+
if (debug || debugGC)
|
|
648
|
+
console.debug(`[gltf-progressive] Memory: Resource GC'd\n${cacheKey}`);
|
|
649
|
+
// Only delete if the entry is still a WeakRef and the resource is gone
|
|
650
|
+
if (entry instanceof WeakRef) {
|
|
651
|
+
const derefed = entry.deref();
|
|
652
|
+
if (!derefed) {
|
|
653
|
+
NEEDLE_progressive.cache.delete(cacheKey);
|
|
654
|
+
if (debug || debugGC)
|
|
655
|
+
console.log(`[gltf-progressive] ↪ Cache entry deleted (GC)`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
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
|
+
}
|
|
533
713
|
static workers = [];
|
|
534
714
|
static _workersIndex = 0;
|
|
535
715
|
static async getOrLoadLOD(current, level) {
|
|
@@ -567,8 +747,18 @@ export class NEEDLE_progressive {
|
|
|
567
747
|
useLowRes = true;
|
|
568
748
|
}
|
|
569
749
|
if (useLowRes) {
|
|
570
|
-
const
|
|
571
|
-
|
|
750
|
+
const lowresRef = this.lowresCache.get(LODKEY);
|
|
751
|
+
if (lowresRef) {
|
|
752
|
+
const lowres = lowresRef.deref();
|
|
753
|
+
if (lowres)
|
|
754
|
+
return lowres;
|
|
755
|
+
// Resource was GC'd, remove stale entry
|
|
756
|
+
this.lowresCache.delete(LODKEY);
|
|
757
|
+
if (debug)
|
|
758
|
+
console.log(`[gltf-progressive] Lowres cache entry was GC'd: ${LODKEY}`);
|
|
759
|
+
}
|
|
760
|
+
// Fallback to current if lowres was GC'd
|
|
761
|
+
return null;
|
|
572
762
|
}
|
|
573
763
|
}
|
|
574
764
|
/** the unresolved LOD url */
|
|
@@ -592,42 +782,73 @@ export class NEEDLE_progressive {
|
|
|
592
782
|
// check if the requested file has already been loaded
|
|
593
783
|
const KEY = lod_url + "_" + lodInfo.guid;
|
|
594
784
|
const slot = await this.queue.slot(lod_url);
|
|
595
|
-
// check if the requested file is currently being loaded
|
|
596
|
-
const existing = this.
|
|
785
|
+
// check if the requested file is currently being loaded or was previously loaded
|
|
786
|
+
const existing = this.cache.get(KEY);
|
|
597
787
|
if (existing !== undefined) {
|
|
598
788
|
if (debugverbose)
|
|
599
789
|
console.log(`LOD ${level} was already loading/loaded: ${KEY}`);
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
790
|
+
if (existing instanceof WeakRef) {
|
|
791
|
+
// Previously resolved resource — check if still alive in memory
|
|
792
|
+
const derefed = existing.deref();
|
|
793
|
+
if (derefed) {
|
|
794
|
+
let res = derefed;
|
|
795
|
+
let resourceIsDisposed = false;
|
|
796
|
+
if (res instanceof Texture && current instanceof Texture) {
|
|
797
|
+
if (res.image?.data || res.source?.data) {
|
|
798
|
+
res = this.copySettings(current, res);
|
|
799
|
+
}
|
|
800
|
+
else {
|
|
801
|
+
resourceIsDisposed = true;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
else if (res instanceof BufferGeometry && current instanceof BufferGeometry) {
|
|
805
|
+
if (!res.attributes.position?.array) {
|
|
806
|
+
resourceIsDisposed = true;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
if (!resourceIsDisposed) {
|
|
810
|
+
return res;
|
|
811
|
+
}
|
|
618
812
|
}
|
|
813
|
+
// Resource was garbage collected or disposed — remove stale entry and re-load
|
|
814
|
+
this.cache.delete(KEY);
|
|
815
|
+
if (debug)
|
|
816
|
+
console.log(`[gltf-progressive] Re-loading GC'd/disposed resource: ${KEY}`);
|
|
619
817
|
}
|
|
620
|
-
else
|
|
621
|
-
|
|
622
|
-
|
|
818
|
+
else {
|
|
819
|
+
// Promise — loading in progress or previously completed
|
|
820
|
+
let res = await existing.catch(err => {
|
|
821
|
+
console.error(`Error loading LOD ${level} from ${lod_url}\n`, err);
|
|
822
|
+
return null;
|
|
823
|
+
});
|
|
824
|
+
let resouceIsDisposed = false;
|
|
825
|
+
if (res == null) {
|
|
826
|
+
// if the resource is null the last loading result didnt succeed (maybe because the url doesnt exist)
|
|
827
|
+
// in which case we don't attempt to load it again
|
|
623
828
|
}
|
|
624
|
-
else {
|
|
625
|
-
|
|
626
|
-
|
|
829
|
+
else if (res instanceof Texture && current instanceof Texture) {
|
|
830
|
+
// check if the texture has been disposed or not
|
|
831
|
+
if (res.image?.data || res.source?.data) {
|
|
832
|
+
res = this.copySettings(current, res);
|
|
833
|
+
}
|
|
834
|
+
// if it has been disposed we need to load it again
|
|
835
|
+
else {
|
|
836
|
+
resouceIsDisposed = true;
|
|
837
|
+
this.cache.delete(KEY);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
else if (res instanceof BufferGeometry && current instanceof BufferGeometry) {
|
|
841
|
+
if (res.attributes.position?.array) {
|
|
842
|
+
// the geometry is OK
|
|
843
|
+
}
|
|
844
|
+
else {
|
|
845
|
+
resouceIsDisposed = true;
|
|
846
|
+
this.cache.delete(KEY);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
if (!resouceIsDisposed) {
|
|
850
|
+
return res;
|
|
627
851
|
}
|
|
628
|
-
}
|
|
629
|
-
if (!resouceIsDisposed) {
|
|
630
|
-
return res;
|
|
631
852
|
}
|
|
632
853
|
}
|
|
633
854
|
// #region loading
|
|
@@ -774,9 +995,33 @@ export class NEEDLE_progressive {
|
|
|
774
995
|
// we could not find a texture or mesh with the given guid
|
|
775
996
|
return resolve(null);
|
|
776
997
|
});
|
|
777
|
-
this.
|
|
998
|
+
this.cache.set(KEY, request);
|
|
778
999
|
slot.use(request);
|
|
779
1000
|
const res = await request;
|
|
1001
|
+
// Optimize cache entry: replace loading promise with lightweight reference.
|
|
1002
|
+
// This releases closure variables captured during the loading function.
|
|
1003
|
+
if (res != null) {
|
|
1004
|
+
if (res instanceof Texture) {
|
|
1005
|
+
// For Texture resources, use WeakRef to allow garbage collection.
|
|
1006
|
+
// The FinalizationRegistry will auto-clean this entry when the resource is GC'd.
|
|
1007
|
+
this.cache.set(KEY, new WeakRef(res));
|
|
1008
|
+
NEEDLE_progressive._resourceRegistry.register(res, KEY);
|
|
1009
|
+
}
|
|
1010
|
+
else if (Array.isArray(res)) {
|
|
1011
|
+
// For BufferGeometry[] (multi-primitive meshes), use a resolved promise.
|
|
1012
|
+
// This keeps geometries in memory as they should not be GC'd (mesh LODs stay cached).
|
|
1013
|
+
this.cache.set(KEY, Promise.resolve(res));
|
|
1014
|
+
}
|
|
1015
|
+
else {
|
|
1016
|
+
// For single BufferGeometry, keep in memory (don't use WeakRef)
|
|
1017
|
+
this.cache.set(KEY, Promise.resolve(res));
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
else {
|
|
1021
|
+
// Failed load — replace with clean resolved promise to release loading closure.
|
|
1022
|
+
// Keeping the entry prevents retrying (existing behavior).
|
|
1023
|
+
this.cache.set(KEY, Promise.resolve(null));
|
|
1024
|
+
}
|
|
780
1025
|
return res;
|
|
781
1026
|
}
|
|
782
1027
|
else {
|
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