@needle-tools/gltf-progressive 3.4.0-next.a9f46ea → 3.4.0-next.ed30751

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/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, isMobileDevice, 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
@@ -309,6 +310,16 @@ export class NEEDLE_progressive {
309
310
  }
310
311
  return Promise.resolve(null);
311
312
  }
313
+ /**
314
+ * Set the maximum number of concurrent loading tasks for LOD resources. This limits how many LOD resources (meshes or textures) can be loaded at the same time to prevent overloading the network or GPU. If the limit is reached, additional loading requests will be queued and processed as previous ones finish.
315
+ * @default 50 on desktop, 20 on mobile devices
316
+ */
317
+ static set maxConcurrentLoadingTasks(value) {
318
+ NEEDLE_progressive.queue.maxConcurrent = value;
319
+ }
320
+ static get maxConcurrentLoadingTasks() {
321
+ return NEEDLE_progressive.queue.maxConcurrent;
322
+ }
312
323
  // #region INTERNAL
313
324
  static assignTextureLODForSlot(current, level, material, slot) {
314
325
  if (current?.isTexture !== true) {
@@ -334,29 +345,37 @@ export class NEEDLE_progressive {
334
345
  if (assignedLOD && assignedLOD?.level < level) {
335
346
  if (debug === "verbose")
336
347
  console.warn("Assigned texture level is already higher: ", assignedLOD.level, level, material, assigned, tex);
348
+ // Dispose the newly loaded texture since we're not using it
349
+ // (the assigned texture is higher quality, so we reject the new one)
350
+ // Note: We dispose directly here (not via untrackTextureUsage) because this texture
351
+ // was never tracked/used - it was rejected immediately upon loading
352
+ if (tex && tex !== assigned) {
353
+ if (debug || debugGC) {
354
+ console.log(`[gltf-progressive] Disposing rejected lower-quality texture LOD ${level} (assigned is ${assignedLOD.level})`, tex.uuid);
355
+ }
356
+ tex.dispose();
357
+ }
337
358
  return null;
338
359
  }
339
360
  // assigned.dispose();
340
361
  }
341
- // Since we're switching LOD level for the texture based on distance we can avoid uploading all the mipmaps
342
- if (reduceMipmaps && tex.mipmaps) {
343
- const prevCount = tex.mipmaps.length;
344
- tex.mipmaps.length = Math.min(tex.mipmaps.length, 3);
345
- if (prevCount !== tex.mipmaps.length) {
346
- if (debug)
347
- console.debug(`Reduced mipmap count from ${prevCount} to ${tex.mipmaps.length} for ${tex.uuid}: ${tex.image?.width}x${tex.image?.height}.`);
362
+ // Track reference count for new texture
363
+ this.trackTextureUsage(tex);
364
+ // Untrack the old texture (may dispose if ref count hits 0)
365
+ // This prevents accumulation of GPU VRAM while waiting for garbage collection
366
+ if (assigned && assigned !== tex) {
367
+ const wasDisposed = this.untrackTextureUsage(assigned);
368
+ if (wasDisposed && (debug || debugGC)) {
369
+ const assignedLOD = this.getAssignedLODInformation(assigned);
370
+ console.log(`[gltf-progressive] Disposed old texture LOD ${assignedLOD?.level ?? '?'} → ${level} for ${material.name || material.type}.${slot}`, assigned.uuid);
348
371
  }
349
372
  }
350
373
  material[slot] = tex;
351
374
  }
352
- // check if the old texture is still used by other objects
353
- // if not we dispose it...
354
- // this could also be handled elsewhere and not be done immediately
355
- // const users = getResourceUserCount(current);
356
- // if (!users) {
357
- // if (debug) console.log("Progressive: Dispose texture", current.name, current.source.data, current.uuid);
358
- // current?.dispose();
359
- // }
375
+ // Note: We use reference counting above to track texture usage across multiple materials.
376
+ // When the reference count hits zero, GPU memory (VRAM) is freed immediately via gl.deleteTexture(),
377
+ // not waiting for JavaScript garbage collection which may take seconds/minutes.
378
+ // This handles cases where the same texture is shared across multiple materials/objects.
360
379
  }
361
380
  // this.onProgressiveLoadEnd(info);
362
381
  return tex;
@@ -531,6 +550,7 @@ export class NEEDLE_progressive {
531
550
  * Dispose cached resources to free memory.
532
551
  * Call this when a model is removed from the scene to allow garbage collection of its LOD resources.
533
552
  * Calls three.js `.dispose()` on cached Textures and BufferGeometries to free GPU memory.
553
+ * Also clears reference counts for disposed textures.
534
554
  * @param guid Optional GUID to dispose resources for a specific model. If omitted, all cached resources are cleared.
535
555
  */
536
556
  static dispose(guid) {
@@ -542,7 +562,9 @@ export class NEEDLE_progressive {
542
562
  const lowres = lowresRef.deref();
543
563
  if (lowres) {
544
564
  if (lowres.isTexture) {
545
- lowres.dispose();
565
+ const tex = lowres;
566
+ this.textureRefCounts.delete(tex.uuid); // Clear ref count
567
+ tex.dispose();
546
568
  }
547
569
  else if (Array.isArray(lowres)) {
548
570
  for (const geo of lowres)
@@ -552,20 +574,24 @@ export class NEEDLE_progressive {
552
574
  this.lowresCache.delete(guid);
553
575
  }
554
576
  // Dispose previously loaded LOD entries
555
- for (const [key, entry] of this.previouslyLoaded) {
577
+ for (const [key, entry] of this.cache) {
556
578
  if (key.includes(guid)) {
557
579
  this._disposeCacheEntry(entry);
558
- this.previouslyLoaded.delete(key);
580
+ this.cache.delete(key);
559
581
  }
560
582
  }
561
583
  }
562
584
  else {
585
+ // Invalidate requests waiting for a queue slot as well as active loads.
586
+ this.cacheGeneration++;
563
587
  this.lodInfos.clear();
564
588
  for (const [, entryRef] of this.lowresCache) {
565
589
  const entry = entryRef.deref();
566
590
  if (entry) {
567
591
  if (entry.isTexture) {
568
- entry.dispose();
592
+ const tex = entry;
593
+ this.textureRefCounts.delete(tex.uuid); // Clear ref count
594
+ tex.dispose();
569
595
  }
570
596
  else if (Array.isArray(entry)) {
571
597
  for (const geo of entry)
@@ -574,10 +600,12 @@ export class NEEDLE_progressive {
574
600
  }
575
601
  }
576
602
  this.lowresCache.clear();
577
- for (const [, entry] of this.previouslyLoaded) {
603
+ for (const [, entry] of this.cache) {
578
604
  this._disposeCacheEntry(entry);
579
605
  }
580
- this.previouslyLoaded.clear();
606
+ this.cache.clear();
607
+ // Clear all texture reference counts when disposing everything
608
+ this.textureRefCounts.clear();
581
609
  }
582
610
  }
583
611
  /** Dispose a single cache entry's three.js resource(s) to free GPU memory. */
@@ -585,11 +613,18 @@ export class NEEDLE_progressive {
585
613
  if (entry instanceof WeakRef) {
586
614
  // Single resource — deref and dispose if still alive
587
615
  const resource = entry.deref();
588
- resource?.dispose();
616
+ if (resource) {
617
+ // Clear ref count for textures
618
+ if (resource.isTexture) {
619
+ this.textureRefCounts.delete(resource.uuid);
620
+ }
621
+ resource.dispose();
622
+ }
589
623
  }
590
624
  else {
591
625
  // Promise — may be in-flight or already resolved.
592
626
  // Attach disposal to run after resolution.
627
+ this.disposedRequests.add(entry);
593
628
  entry.then(resource => {
594
629
  if (resource) {
595
630
  if (Array.isArray(resource)) {
@@ -597,6 +632,10 @@ export class NEEDLE_progressive {
597
632
  geo.dispose();
598
633
  }
599
634
  else {
635
+ // Clear ref count for textures
636
+ if (resource.isTexture) {
637
+ this.textureRefCounts.delete(resource.uuid);
638
+ }
600
639
  resource.dispose();
601
640
  }
602
641
  }
@@ -606,30 +645,90 @@ export class NEEDLE_progressive {
606
645
  /** A map of key = asset uuid and value = LOD information */
607
646
  static lodInfos = new Map();
608
647
  /** cache of already loaded mesh lods. Uses WeakRef for single resources to allow garbage collection when unused. */
609
- static previouslyLoaded = new Map();
648
+ static cache = new Map();
610
649
  /** this contains the geometry/textures that were originally loaded. Uses WeakRef to allow garbage collection when unused. */
611
650
  static lowresCache = new Map();
651
+ /** Reference counting for textures to track usage across multiple materials/objects */
652
+ static textureRefCounts = new Map();
612
653
  /**
613
654
  * FinalizationRegistry to automatically clean up `previouslyLoaded` cache entries
614
655
  * when their associated three.js resources are garbage collected by the browser.
615
656
  * The held value is the cache key string used in `previouslyLoaded`.
616
657
  */
617
658
  static _resourceRegistry = new FinalizationRegistry((cacheKey) => {
618
- const entry = NEEDLE_progressive.previouslyLoaded.get(cacheKey);
619
- console.debug(`[gltf-progressive] FinalizationRegistry cleanup: Resource GC'd for ${cacheKey}.`);
659
+ const entry = NEEDLE_progressive.cache.get(cacheKey);
660
+ if (debug || debugGC)
661
+ console.debug(`[gltf-progressive] Memory: Resource GC'd\n${cacheKey}`);
620
662
  // Only delete if the entry is still a WeakRef and the resource is gone
621
663
  if (entry instanceof WeakRef) {
622
664
  const derefed = entry.deref();
623
665
  if (!derefed) {
624
- NEEDLE_progressive.previouslyLoaded.delete(cacheKey);
625
- if (debug)
626
- console.log(`[gltf-progressive] Cache entry auto-cleaned (GC'd): ${cacheKey}`);
666
+ NEEDLE_progressive.cache.delete(cacheKey);
667
+ if (debug || debugGC)
668
+ console.log(`[gltf-progressive] Cache entry deleted (GC)`);
627
669
  }
628
670
  }
629
671
  });
672
+ /**
673
+ * Track texture usage by incrementing reference count
674
+ */
675
+ static trackTextureUsage(texture) {
676
+ const uuid = texture.uuid;
677
+ const count = this.textureRefCounts.get(uuid) || 0;
678
+ this.textureRefCounts.set(uuid, count + 1);
679
+ if (debug === "verbose") {
680
+ console.log(`[gltf-progressive] Track texture ${uuid}, refCount: ${count} → ${count + 1}`);
681
+ }
682
+ }
683
+ /**
684
+ * Untrack texture usage by decrementing reference count.
685
+ * Automatically disposes the texture when reference count reaches zero.
686
+ * @returns true if the texture was disposed, false otherwise
687
+ */
688
+ static untrackTextureUsage(texture) {
689
+ const uuid = texture.uuid;
690
+ const count = this.textureRefCounts.get(uuid);
691
+ if (!count) {
692
+ // Texture wasn't tracked, dispose immediately (safe fallback)
693
+ if (debug === "verbose" || debugGC) {
694
+ logDebugInfo(`[gltf-progressive] Memory: Untrack untracked texture (dispose immediately)`, 0);
695
+ }
696
+ texture.dispose();
697
+ return true;
698
+ }
699
+ const newCount = count - 1;
700
+ if (newCount <= 0) {
701
+ this.textureRefCounts.delete(uuid);
702
+ if (debug || debugGC) {
703
+ logDebugInfo(`[gltf-progressive] Memory: Dispose texture`, newCount);
704
+ }
705
+ texture.dispose();
706
+ return true;
707
+ }
708
+ else {
709
+ this.textureRefCounts.set(uuid, newCount);
710
+ if (debug === "verbose") {
711
+ logDebugInfo(`[gltf-progressive] Memory: Untrack texture`, newCount);
712
+ }
713
+ return false;
714
+ }
715
+ function logDebugInfo(prefix, newCount) {
716
+ let width = texture.image?.width || texture.source?.data?.width || 0;
717
+ let height = texture.image?.height || texture.source?.data?.height || 0;
718
+ const textureSize = width && height ? `${width}x${height}` : "N/A";
719
+ let memorySize = "N/A";
720
+ if (width && height) {
721
+ memorySize = `~${(determineTextureMemoryInBytes(texture) / (1024 * 1024)).toFixed(2)} MB`;
722
+ }
723
+ console.log(`${prefix} — ${texture.name} ${textureSize} (${memorySize}), refCount: ${count} → ${newCount}\n${uuid}`);
724
+ }
725
+ }
630
726
  static workers = [];
727
+ static cacheGeneration = 0;
728
+ static disposedRequests = new WeakSet();
631
729
  static _workersIndex = 0;
632
730
  static async getOrLoadLOD(current, level) {
731
+ const generation = this.cacheGeneration;
633
732
  const debugverbose = debug == "verbose";
634
733
  /** this key is used to lookup the LOD information */
635
734
  const LOD = this.getAssignedLODInformation(current);
@@ -699,8 +798,10 @@ export class NEEDLE_progressive {
699
798
  // check if the requested file has already been loaded
700
799
  const KEY = lod_url + "_" + lodInfo.guid;
701
800
  const slot = await this.queue.slot(lod_url);
801
+ if (generation !== this.cacheGeneration)
802
+ return null;
702
803
  // check if the requested file is currently being loaded or was previously loaded
703
- const existing = this.previouslyLoaded.get(KEY);
804
+ const existing = this.cache.get(KEY);
704
805
  if (existing !== undefined) {
705
806
  if (debugverbose)
706
807
  console.log(`LOD ${level} was already loading/loaded: ${KEY}`);
@@ -728,7 +829,7 @@ export class NEEDLE_progressive {
728
829
  }
729
830
  }
730
831
  // Resource was garbage collected or disposed — remove stale entry and re-load
731
- this.previouslyLoaded.delete(KEY);
832
+ this.cache.delete(KEY);
732
833
  if (debug)
733
834
  console.log(`[gltf-progressive] Re-loading GC'd/disposed resource: ${KEY}`);
734
835
  }
@@ -738,6 +839,8 @@ export class NEEDLE_progressive {
738
839
  console.error(`Error loading LOD ${level} from ${lod_url}\n`, err);
739
840
  return null;
740
841
  });
842
+ if (generation !== this.cacheGeneration || this.disposedRequests.has(existing))
843
+ return null;
741
844
  let resouceIsDisposed = false;
742
845
  if (res == null) {
743
846
  // if the resource is null the last loading result didnt succeed (maybe because the url doesnt exist)
@@ -751,7 +854,7 @@ export class NEEDLE_progressive {
751
854
  // if it has been disposed we need to load it again
752
855
  else {
753
856
  resouceIsDisposed = true;
754
- this.previouslyLoaded.delete(KEY);
857
+ this.cache.delete(KEY);
755
858
  }
756
859
  }
757
860
  else if (res instanceof BufferGeometry && current instanceof BufferGeometry) {
@@ -760,7 +863,7 @@ export class NEEDLE_progressive {
760
863
  }
761
864
  else {
762
865
  resouceIsDisposed = true;
763
- this.previouslyLoaded.delete(KEY);
866
+ this.cache.delete(KEY);
764
867
  }
765
868
  }
766
869
  if (!resouceIsDisposed) {
@@ -912,30 +1015,36 @@ export class NEEDLE_progressive {
912
1015
  // we could not find a texture or mesh with the given guid
913
1016
  return resolve(null);
914
1017
  });
915
- this.previouslyLoaded.set(KEY, request);
1018
+ this.cache.set(KEY, request);
916
1019
  slot.use(request);
917
1020
  const res = await request;
1021
+ // dispose() cleans up removed pending entries when they resolve. Do not
1022
+ // return those resources or overwrite a newer request for this key.
1023
+ if (generation !== this.cacheGeneration || this.cache.get(KEY) !== request)
1024
+ return null;
918
1025
  // Optimize cache entry: replace loading promise with lightweight reference.
919
1026
  // This releases closure variables captured during the loading function.
920
1027
  if (res != null) {
921
- if (Array.isArray(res)) {
1028
+ if (res instanceof Texture) {
1029
+ // For Texture resources, use WeakRef to allow garbage collection.
1030
+ // The FinalizationRegistry will auto-clean this entry when the resource is GC'd.
1031
+ this.cache.set(KEY, new WeakRef(res));
1032
+ NEEDLE_progressive._resourceRegistry.register(res, KEY);
1033
+ }
1034
+ else if (Array.isArray(res)) {
922
1035
  // For BufferGeometry[] (multi-primitive meshes), use a resolved promise.
923
- // WeakRef can't be used here because callers only extract individual elements
924
- // from the array, so the array object itself would be GC'd immediately.
925
- this.previouslyLoaded.set(KEY, Promise.resolve(res));
1036
+ // This keeps geometries in memory as they should not be GC'd (mesh LODs stay cached).
1037
+ this.cache.set(KEY, Promise.resolve(res));
926
1038
  }
927
1039
  else {
928
- // For single resources (Texture or BufferGeometry), use WeakRef to allow
929
- // garbage collection when the resource is no longer referenced by the scene.
930
- // The FinalizationRegistry will auto-clean this entry when the resource is GC'd.
931
- this.previouslyLoaded.set(KEY, new WeakRef(res));
932
- NEEDLE_progressive._resourceRegistry.register(res, KEY);
1040
+ // For single BufferGeometry, keep in memory (don't use WeakRef)
1041
+ this.cache.set(KEY, Promise.resolve(res));
933
1042
  }
934
1043
  }
935
1044
  else {
936
1045
  // Failed load — replace with clean resolved promise to release loading closure.
937
1046
  // Keeping the entry prevents retrying (existing behavior).
938
- this.previouslyLoaded.set(KEY, Promise.resolve(null));
1047
+ this.cache.set(KEY, Promise.resolve(null));
939
1048
  }
940
1049
  return res;
941
1050
  }
@@ -945,6 +1054,10 @@ export class NEEDLE_progressive {
945
1054
  console.log("Load texture from uri: " + lod_url);
946
1055
  const loader = new TextureLoader();
947
1056
  const tex = await loader.loadAsync(lod_url);
1057
+ if (generation !== this.cacheGeneration) {
1058
+ tex?.dispose();
1059
+ return null;
1060
+ }
948
1061
  if (tex) {
949
1062
  tex.guid = lodInfo.guid;
950
1063
  tex.flipY = false;
@@ -961,8 +1074,7 @@ export class NEEDLE_progressive {
961
1074
  }
962
1075
  return null;
963
1076
  }
964
- static maxConcurrent = 50;
965
- static queue = new PromiseQueue(NEEDLE_progressive.maxConcurrent, { debug: debug != false });
1077
+ static queue = new PromiseQueue(isMobileDevice() ? 20 : 50, { debug: debug != false });
966
1078
  static assignLODInformation(url, res, key, level, index) {
967
1079
  if (!res)
968
1080
  return;
@@ -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;
@@ -18,11 +19,11 @@ export type SlotReturnValue<T = any> = {
18
19
  * Use the `slot` method to request a slot for a promise with a specific key. The returned promise resolves to an object with a `use` method that can be called to add the promise to the queue.
19
20
  */
20
21
  export declare class PromiseQueue<T = any> {
21
- readonly maxConcurrent: number;
22
+ maxConcurrent: number;
22
23
  private readonly _running;
23
24
  private readonly _queue;
24
25
  debug: boolean;
25
- constructor(maxConcurrent?: number, opts?: {
26
+ constructor(maxConcurrent: number, opts?: {
26
27
  debug?: boolean;
27
28
  });
28
29
  private tick;
@@ -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;
@@ -71,7 +71,7 @@ export class PromiseQueue {
71
71
  _running = new Map();
72
72
  _queue = [];
73
73
  debug = false;
74
- constructor(maxConcurrent = 100, opts = {}) {
74
+ constructor(maxConcurrent, opts = {}) {
75
75
  this.maxConcurrent = maxConcurrent;
76
76
  this.debug = opts.debug ?? false;
77
77
  window.requestAnimationFrame(this.tick);
@@ -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
@@ -1,4 +1,4 @@
1
1
  // replaced at build time
2
- export const version = "3.4.0-beta";
2
+ export const version = "3.4.0-beta.2";
3
3
  globalThis["GLTF_PROGRESSIVE_VERSION"] = version;
4
4
  console.debug(`[gltf-progressive] version ${version || "-"}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@needle-tools/gltf-progressive",
3
- "version": "3.4.0-next.a9f46ea",
3
+ "version": "3.4.0-next.ed30751",
4
4
  "description": "three.js support for loading glTF or GLB files that contain progressive loading data",
5
5
  "homepage": "https://needle.tools",
6
6
  "author": {