Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UnloadTilesPlugin: Add time delay and bytes target #880

Merged
merged 4 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading