From 1b18cfc7534c3ef2fb3d369a928d1c06b1377066 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Fri, 20 Dec 2024 11:25:45 +0900 Subject: [PATCH 1/4] Add lrucache and delay to UnloadTilesPlugin --- example/src/plugins/UnloadTilesPlugin.js | 152 +++++++++++++++++++++-- src/utilities/LRUCache.js | 26 +++- 2 files changed, 163 insertions(+), 15 deletions(-) diff --git a/example/src/plugins/UnloadTilesPlugin.js b/example/src/plugins/UnloadTilesPlugin.js index ec0f11ebc..5c04e7109 100644 --- a/example/src/plugins/UnloadTilesPlugin.js +++ b/example/src/plugins/UnloadTilesPlugin.js @@ -1,19 +1,56 @@ -import { estimateBytesUsed } from '../../../src/three/utilities.js'; +import { LRUCache } from '3d-tiles-renderer'; // Plugin that disposes tiles on unload to remove them from the GPU, saving memory // TODO: // - abstract the "tile visible" callback so fade tiles can call it when tiles are _actually_ marked as non-visible -// - add a memory unload function to the tiles renderer that can be called and reacted to by any plugin including BatchedMesh, -// though this may prevent different options. Something like a subfunction that "disposeTile" calls without full disposal. export class UnloadTilesPlugin { - constructor() { + set delay( v ) { + + this.deferCallbacks.delay = v; + + } + + get delay() { + + return this.deferCallbacks.delay; + + } + + set bytesTarget( v ) { + + this.lruCache.minBytesSize = v; + + } + + get bytesTarget() { + + return this.lruCache.minBytesSize; + + } + + get estimatedGpuBytes() { + + return this.lruCache.cachedBytes; + + } + + constructor( options ) { + + const { + delay = 0, + bytesTarget = 0, + } = options; this.name = 'UNLOAD_TILES_PLUGIN'; this.tiles = null; - this.estimatedGpuBytes = 0; + this.lruCache = new LRUCache(); + this.deferCallbacks = new DeferCallbackManager(); + + this.delay = delay; + this.bytesTarget = bytesTarget; } @@ -21,18 +58,49 @@ export class UnloadTilesPlugin { this.tiles = tiles; - this._onVisibilityChangeCallback = ( { scene, visible, tile } ) => { + const { lruCache, deferCallbacks } = this; + deferCallbacks.callback = tile => { - if ( scene ) { + lruCache.markUnused( tile ); + lruCache.scheduleUnload( false ); - const size = estimateBytesUsed( scene ); - this.estimatedGpuBytes += visible ? size : - size; + }; - if ( ! visible ) { + const unloadCallback = tile => { - tiles.invokeOnePlugin( plugin => plugin.unloadTileFromGPU && plugin.unloadTileFromGPU( scene, tile ) ); + const scene = tile.cached.scene; + const visible = tiles.visibleTiles.get( tile ); - } + if ( ! visible ) { + + tiles.invokeOnePlugin( plugin => plugin.unloadTileFromGPU && plugin.unloadTileFromGPU( scene, tile ) ); + + } + + + }; + + this._onUpdateBefore = () => { + + // update lruCache in "update" in case the callback values change + lruCache.unloadPriorityCallback = tiles.lruCache.unloadPriorityCallback; + lruCache.computeMemoryUsageCallback = tiles.lruCache.computeMemoryUsageCallback; + lruCache.maxSize = Infinity; + lruCache.maxBytesSize = Infinity; + + }; + + this._onVisibilityChangeCallback = ( { tile, visible } ) => { + + if ( visible ) { + + lruCache.add( tile, unloadCallback ); + lruCache.markUnused( tile ); + deferCallbacks.cancel( tile ); + + } else { + + deferCallbacks.run( tile ); } @@ -46,6 +114,7 @@ export class UnloadTilesPlugin { } ); tiles.addEventListener( 'tile-visibility-change', this._onVisibilityChangeCallback ); + tiles.addEventListener( 'update-before', this._onUpdateBefore ); } @@ -88,7 +157,64 @@ export class UnloadTilesPlugin { dispose() { this.tiles.removeEventListener( 'tile-visibility-change', this._onVisibilityChangeCallback ); - this.estimatedGpuBytes = 0; + this.tiles.removeEventListener( 'update-before', this._onUpdateBefore ); + this.deferCallbacks.cancelAll(); + + } + +} + +// Manager for running callbacks after a certain amount of time +class DeferCallbackManager { + + constructor( callback = () => {} ) { + + this.map = new Map(); + this.callback = callback; + this.delay = 0; + + } + + run( tile ) { + + const { map, delay } = this; + if ( map.has( tile ) ) { + + throw new Error( 'DeferCallbackManager: Callback already initialized.' ); + + } + + if ( delay === 0 ) { + + this.callback( tile ); + + } else { + + map.set( tile, setTimeout( () => this.callback( tile ), delay ) ); + + } + + } + + cancel( tile ) { + + const { map } = this; + if ( map.has( tile ) ) { + + clearTimeout( map.get( tile ) ); + map.delete( tile ); + + } + + } + + cancelAll() { + + this.map.forEach( ( value, tile ) => { + + this.cancel( tile ); + + } ); } diff --git a/src/utilities/LRUCache.js b/src/utilities/LRUCache.js index 4030d29ae..ecb8123fc 100644 --- a/src/utilities/LRUCache.js +++ b/src/utilities/LRUCache.js @@ -114,6 +114,12 @@ class LRUCache { } + has( item ) { + + return this.itemSet.has( item ); + + } + remove( item ) { const usedSet = this.usedSet; @@ -204,6 +210,17 @@ class LRUCache { } + markUnused( item ) { + + const usedSet = this.usedSet; + if ( usedSet.has( item ) ) { + + usedSet.delete( item ); + + } + + } + markAllUnused() { this.usedSet.clear(); @@ -363,7 +380,7 @@ class LRUCache { } - scheduleUnload() { + scheduleUnload( markUnused = true ) { if ( ! this.scheduled ) { @@ -372,7 +389,12 @@ class LRUCache { this.scheduled = false; this.unloadUnusedContent(); - this.markUnusedQueued = true; + + if ( markUnused ) { + + this.markUnusedQueued = true; + + } } ); From ed45e85034569dd40f3a17555a32288fd2f7c37e Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Fri, 20 Dec 2024 11:26:28 +0900 Subject: [PATCH 2/4] Update field --- example/src/plugins/UnloadTilesPlugin.js | 1 + 1 file changed, 1 insertion(+) diff --git a/example/src/plugins/UnloadTilesPlugin.js b/example/src/plugins/UnloadTilesPlugin.js index 5c04e7109..bcdd77b75 100644 --- a/example/src/plugins/UnloadTilesPlugin.js +++ b/example/src/plugins/UnloadTilesPlugin.js @@ -85,6 +85,7 @@ export class UnloadTilesPlugin { // update lruCache in "update" in case the callback values change lruCache.unloadPriorityCallback = tiles.lruCache.unloadPriorityCallback; lruCache.computeMemoryUsageCallback = tiles.lruCache.computeMemoryUsageCallback; + lruCache.minSize = Infinity; lruCache.maxSize = Infinity; lruCache.maxBytesSize = Infinity; From 5c1f0713a53ef9c609260ba889681358aa79611e Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Fri, 20 Dec 2024 11:37:44 +0900 Subject: [PATCH 3/4] Fixes --- example/src/plugins/UnloadTilesPlugin.js | 6 ++++-- src/utilities/LRUCache.js | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/example/src/plugins/UnloadTilesPlugin.js b/example/src/plugins/UnloadTilesPlugin.js index bcdd77b75..fdc116343 100644 --- a/example/src/plugins/UnloadTilesPlugin.js +++ b/example/src/plugins/UnloadTilesPlugin.js @@ -69,7 +69,7 @@ export class UnloadTilesPlugin { const unloadCallback = tile => { const scene = tile.cached.scene; - const visible = tiles.visibleTiles.get( tile ); + const visible = tiles.visibleTiles.has( tile ); if ( ! visible ) { @@ -88,6 +88,8 @@ export class UnloadTilesPlugin { lruCache.minSize = Infinity; lruCache.maxSize = Infinity; lruCache.maxBytesSize = Infinity; + lruCache.unloadPercent = 1; + lruCache.autoMarkUnused = false; }; @@ -96,7 +98,7 @@ export class UnloadTilesPlugin { if ( visible ) { lruCache.add( tile, unloadCallback ); - lruCache.markUnused( tile ); + lruCache.markUsed( tile ); deferCallbacks.cancel( tile ); } else { diff --git a/src/utilities/LRUCache.js b/src/utilities/LRUCache.js index ecb8123fc..878e05233 100644 --- a/src/utilities/LRUCache.js +++ b/src/utilities/LRUCache.js @@ -40,6 +40,7 @@ class LRUCache { this.minBytesSize = 0.3 * GIGABYTE_BYTES; this.maxBytesSize = 0.4 * GIGABYTE_BYTES; this.unloadPercent = 0.05; + this.autoMarkUnused = true; // "itemSet" doubles as both the list of the full set of items currently // stored in the cache (keys) as well as a map to the time the item was last @@ -380,7 +381,7 @@ class LRUCache { } - scheduleUnload( markUnused = true ) { + scheduleUnload() { if ( ! this.scheduled ) { @@ -390,7 +391,7 @@ class LRUCache { this.scheduled = false; this.unloadUnusedContent(); - if ( markUnused ) { + if ( this.autoMarkUnused ) { this.markUnusedQueued = true; From 4276ee5a0347b166f67a819fec6945abc437c951 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Fri, 20 Dec 2024 11:49:17 +0900 Subject: [PATCH 4/4] README update --- example/src/plugins/UnloadTilesPlugin.js | 1 - src/plugins/README.md | 19 +++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/example/src/plugins/UnloadTilesPlugin.js b/example/src/plugins/UnloadTilesPlugin.js index fdc116343..3abb56e84 100644 --- a/example/src/plugins/UnloadTilesPlugin.js +++ b/example/src/plugins/UnloadTilesPlugin.js @@ -77,7 +77,6 @@ export class UnloadTilesPlugin { } - }; this._onUpdateBefore = () => { diff --git a/src/plugins/README.md b/src/plugins/README.md index 8bc8d46fc..d9e18efed 100644 --- a/src/plugins/README.md +++ b/src/plugins/README.md @@ -594,3 +594,22 @@ estimatedGPUBytes : number ``` The number of bytes that are actually uploaded to the GPU for rendering compared to `lruCache.cachedBytes` which reports the amount of texture and geometry buffer bytes actually downloaded. + +### .constructor + +```js +constructor( options : Object ) +``` + +Available options are as follows: + +```js +{ + // The amount of time to wait in milliseconds before unloading tile content from the GPU. This option can be + // used to account for cases where the user is moving the camera and tiles are coming in and out of frame. + delay: 0, + + // The amount of bytes to unload to. + bytesTarget: 0, +} +```