diff --git a/example/src/plugins/UnloadTilesPlugin.js b/example/src/plugins/UnloadTilesPlugin.js index ec0f11eb..3abb56e8 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,51 @@ 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.has( 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.minSize = Infinity; + lruCache.maxSize = Infinity; + lruCache.maxBytesSize = Infinity; + lruCache.unloadPercent = 1; + lruCache.autoMarkUnused = false; + + }; + + this._onVisibilityChangeCallback = ( { tile, visible } ) => { + + if ( visible ) { + + lruCache.add( tile, unloadCallback ); + lruCache.markUsed( tile ); + deferCallbacks.cancel( tile ); + + } else { + + deferCallbacks.run( tile ); } @@ -46,6 +116,7 @@ export class UnloadTilesPlugin { } ); tiles.addEventListener( 'tile-visibility-change', this._onVisibilityChangeCallback ); + tiles.addEventListener( 'update-before', this._onUpdateBefore ); } @@ -88,7 +159,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/plugins/README.md b/src/plugins/README.md index 8bc8d46f..d9e18efe 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, +} +``` diff --git a/src/utilities/LRUCache.js b/src/utilities/LRUCache.js index 4030d29a..878e0523 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 @@ -114,6 +115,12 @@ class LRUCache { } + has( item ) { + + return this.itemSet.has( item ); + + } + remove( item ) { const usedSet = this.usedSet; @@ -204,6 +211,17 @@ class LRUCache { } + markUnused( item ) { + + const usedSet = this.usedSet; + if ( usedSet.has( item ) ) { + + usedSet.delete( item ); + + } + + } + markAllUnused() { this.usedSet.clear(); @@ -372,7 +390,12 @@ class LRUCache { this.scheduled = false; this.unloadUnusedContent(); - this.markUnusedQueued = true; + + if ( this.autoMarkUnused ) { + + this.markUnusedQueued = true; + + } } );