Skip to content

Commit

Permalink
UnloadTilesPlugin: Add time delay and bytes target (#880)
Browse files Browse the repository at this point in the history
* Add lrucache and delay to UnloadTilesPlugin

* Update field

* Fixes

* README update
  • Loading branch information
gkjohnson authored Dec 20, 2024
1 parent f826b6f commit 9f0bc99
Show file tree
Hide file tree
Showing 3 changed files with 184 additions and 14 deletions.
154 changes: 141 additions & 13 deletions example/src/plugins/UnloadTilesPlugin.js
Original file line number Diff line number Diff line change
@@ -1,38 +1,108 @@
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;

}

init( tiles ) {

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 );

}

Expand All @@ -46,6 +116,7 @@ export class UnloadTilesPlugin {
} );

tiles.addEventListener( 'tile-visibility-change', this._onVisibilityChangeCallback );
tiles.addEventListener( 'update-before', this._onUpdateBefore );

}

Expand Down Expand Up @@ -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 );

} );

}

Expand Down
19 changes: 19 additions & 0 deletions src/plugins/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
```
25 changes: 24 additions & 1 deletion src/utilities/LRUCache.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -114,6 +115,12 @@ class LRUCache {

}

has( item ) {

return this.itemSet.has( item );

}

remove( item ) {

const usedSet = this.usedSet;
Expand Down Expand Up @@ -204,6 +211,17 @@ class LRUCache {

}

markUnused( item ) {

const usedSet = this.usedSet;
if ( usedSet.has( item ) ) {

usedSet.delete( item );

}

}

markAllUnused() {

this.usedSet.clear();
Expand Down Expand Up @@ -372,7 +390,12 @@ class LRUCache {

this.scheduled = false;
this.unloadUnusedContent();
this.markUnusedQueued = true;

if ( this.autoMarkUnused ) {

this.markUnusedQueued = true;

}

} );

Expand Down

0 comments on commit 9f0bc99

Please sign in to comment.