Skip to content

Commit e53bd50

Browse files
authored
ImageOverlayPlugin: Add failure handling (#1594)
* Add failure handling * Clean up * Update * Updates * Final fix * Update signatures
1 parent cb0e7ae commit e53bd50

8 files changed

Lines changed: 150 additions & 56 deletions

File tree

src/three/plugins/API.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1195,6 +1195,17 @@ deleteOverlay( overlay: ImageOverlay ): void
11951195
Removes the given overlay from the plugin.
11961196
11971197
1198+
### .resetFailedOverlays
1199+
1200+
```js
1201+
resetFailedOverlays(): void
1202+
```
1203+
1204+
Retries any overlay texture fetches that previously failed. Successfully loaded textures
1205+
are applied to their tiles without requiring a geometry reload. Pairs with the `load-error`
1206+
event, which fires on the `TilesRenderer` when an overlay texture fetch fails.
1207+
1208+
11981209
## LoadRegionPlugin
11991210
12001211
Plugin that restricts tile loading and traversal to one or more geometric regions

src/three/plugins/images/ImageOverlayPlugin.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export class ImageOverlayPlugin {
1515
addOverlay( overlay: ImageOverlay, order?: number ): void;
1616
setOverlayOrder( overlay: ImageOverlay, order?: number ): void;
1717
deleteOverlay( overlay: ImageOverlay ): void;
18+
resetFailedOverlays(): void;
1819

1920
}
2021

src/three/plugins/images/ImageOverlayPlugin.js

Lines changed: 94 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1005,6 +1005,7 @@ export class ImageOverlayPlugin {
10051005
range: null,
10061006
target: null,
10071007
meshInfo: new Map(),
1008+
failed: false,
10081009
};
10091010

10101011
overlayInfo
@@ -1046,7 +1047,7 @@ export class ImageOverlayPlugin {
10461047

10471048
}
10481049

1049-
const { tiles, overlayInfo, tileControllers, processQueue } = this;
1050+
const { tiles, overlayInfo, tileControllers } = this;
10501051
const { ellipsoid } = tiles;
10511052
const { controller, tileInfo } = overlayInfo.get( overlay );
10521053
const tileController = tileControllers.get( tile );
@@ -1133,51 +1134,117 @@ export class ImageOverlayPlugin {
11331134

11341135
// if the image projection is outside the 0, 1 uvw range or there are no textures to draw in
11351136
// the tiled image set the don't allocate a texture for it.
1136-
let target = null;
11371137
if ( heightInRange && overlay.hasContent( range ) ) {
11381138

1139-
target = await processQueue
1140-
.add( { tile, overlay }, async () => {
1139+
await this._fetchTileOverlayTexture( tile, overlay, info );
11411140

1142-
// check if the overlay has been disposed since starting this function
1143-
if ( controller.signal.aborted || tileController.signal.aborted ) {
1141+
}
11441142

1145-
return null;
1143+
meshes.forEach( ( mesh, i ) => {
11461144

1147-
}
1145+
const array = new Float32Array( uvs[ i ] );
1146+
const attribute = new BufferAttribute( array, 3 );
1147+
info.meshInfo.set( mesh, { attribute } );
11481148

1149-
// Get the texture from the overlay
1150-
const regionTarget = await overlay.getTexture( range );
1149+
} );
11511150

1152-
// check if the overlay has been disposed since starting this function
1153-
if ( controller.signal.aborted || tileController.signal.aborted ) {
1151+
}
11541152

1155-
return null;
1153+
// Queues an overlay texture fetch for the given tile, writing the result into info.target.
1154+
// Never throws — failures mark info.failed and dispatch a load-error event instead.
1155+
async _fetchTileOverlayTexture( tile, overlay, info ) {
11561156

1157-
}
1157+
const { tiles, overlayInfo, tileControllers, processQueue } = this;
1158+
const { controller } = overlayInfo.get( overlay );
1159+
const tileController = tileControllers.get( tile );
1160+
const { range } = info;
11581161

1159-
return regionTarget;
1162+
info.target = await processQueue
1163+
.add( { tile, overlay }, async () => {
11601164

1161-
} )
1162-
.catch( err => {
1165+
// check if the overlay has been disposed since starting this function
1166+
if ( controller.signal.aborted || tileController.signal.aborted ) {
11631167

1164-
if ( ! ( err instanceof PriorityQueueItemRemovedError ) ) {
1168+
return null;
11651169

1166-
throw err;
1170+
}
11671171

1168-
}
1172+
// Get the texture from the overlay
1173+
const regionTarget = await overlay.getTexture( range );
11691174

1170-
} );
1175+
// check if the overlay has been disposed since starting this function
1176+
if ( controller.signal.aborted || tileController.signal.aborted ) {
11711177

1172-
}
1178+
return null;
11731179

1174-
info.target = target;
1180+
}
11751181

1176-
meshes.forEach( ( mesh, i ) => {
1182+
return regionTarget;
11771183

1178-
const array = new Float32Array( uvs[ i ] );
1179-
const attribute = new BufferAttribute( array, 3 );
1180-
info.meshInfo.set( mesh, { attribute } );
1184+
} )
1185+
.catch( err => {
1186+
1187+
if ( err instanceof PriorityQueueItemRemovedError ) {
1188+
1189+
return null;
1190+
1191+
}
1192+
1193+
info.failed = true;
1194+
tiles.dispatchEvent( { type: 'load-error', tile, overlay, error: err } );
1195+
return null;
1196+
1197+
} );
1198+
1199+
}
1200+
1201+
/**
1202+
* Retries any overlay texture fetches that previously failed. Successfully loaded textures
1203+
* are applied to their tiles without requiring a geometry reload. Pairs with the `load-error`
1204+
* event, which fires on the `TilesRenderer` when an overlay texture fetch fails.
1205+
*/
1206+
resetFailedOverlays() {
1207+
1208+
const { processedTiles, overlayInfo, overlays } = this;
1209+
const failed = [];
1210+
1211+
// Release all failed entries synchronously so their DataCache disposal
1212+
// microtasks are queued before we re-lock below.
1213+
processedTiles.forEach( tile => {
1214+
1215+
overlays.forEach( overlay => {
1216+
1217+
const { tileInfo } = overlayInfo.get( overlay );
1218+
const info = tileInfo.get( tile );
1219+
if ( ! info.failed ) {
1220+
1221+
return;
1222+
1223+
}
1224+
1225+
info.failed = false;
1226+
overlay.releaseTexture( info.range );
1227+
failed.push( { tile, overlay, info } );
1228+
1229+
} );
1230+
1231+
} );
1232+
1233+
// Defer to the next frame so all disposal microtasks — including nested sub-cache
1234+
// cleanup — have fully drained before re-locking.
1235+
requestAnimationFrame( () => {
1236+
1237+
failed.forEach( ( { tile, overlay, info } ) => {
1238+
1239+
overlay.lockTexture( info.range );
1240+
this._fetchTileOverlayTexture( tile, overlay, info )
1241+
.then( () => {
1242+
1243+
this._updateLayers( tile );
1244+
1245+
} );
1246+
1247+
} );
11811248

11821249
} );
11831250

src/three/plugins/images/sources/GeoJSONImageSource.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,11 @@ export class GeoJSONImageSource extends RegionImageSource {
118118

119119
disposeItem( texture ) {
120120

121-
texture.dispose();
121+
if ( texture ) {
122+
123+
texture.dispose();
124+
125+
}
122126

123127
}
124128

src/three/plugins/images/sources/MVTImageSource.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -206,21 +206,23 @@ export class MVTImageSource extends RegionImageSource {
206206
tex.colorSpace = SRGBColorSpace;
207207
tex.generateMipmaps = false;
208208
tex.needsUpdate = true;
209-
tex._regionArgs = [ minX, minY, maxX, maxY, level ];
210209
return tex;
211210

212211
}
213212

214-
disposeItem( texture ) {
213+
disposeItem( texture, [ minX, minY, maxX, maxY, level ] ) {
215214

216-
const [ minX, minY, maxX, maxY, level ] = texture._regionArgs;
217215
forEachTileInBounds( [ minX, minY, maxX, maxY ], level, this._contentCache.tiling, ( tx, ty, tl ) => {
218216

219217
this._contentCache.release( tx, ty, tl );
220218

221219
} );
222220

223-
texture.dispose();
221+
if ( texture ) {
222+
223+
texture.dispose();
224+
225+
}
224226

225227
}
226228

src/three/plugins/images/sources/RegionImageSource.js

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,6 @@ export class TiledRegionImageSource extends RegionImageSource {
4747
this.tiledImageSource = tiledImageSource;
4848
this.tileComposer = new TiledTextureComposer();
4949
this.resolution = 256;
50-
this.IS_DIRECT_TILE = Symbol( 'IS_DIRECT_TILE' );
51-
this.LOCK_TOKENS = Symbol( 'LOCK_TOKENS' );
5250

5351
}
5452

@@ -68,10 +66,9 @@ export class TiledRegionImageSource extends RegionImageSource {
6866

6967
async fetchItem( [ minX, minY, maxX, maxY, level ], signal ) {
7068

71-
const { tiledImageSource, tileComposer, IS_DIRECT_TILE, LOCK_TOKENS } = this;
69+
const { tiledImageSource, tileComposer } = this;
7270
const range = [ minX, minY, maxX, maxY ];
7371
const tiling = tiledImageSource.tiling;
74-
const tokens = [ ...range, level ];
7572

7673
// lock tiles for the requested level
7774
await this._markImages( range, level, false );
@@ -95,14 +92,11 @@ export class TiledRegionImageSource extends RegionImageSource {
9592
if ( singleTileBounds !== null ) {
9693

9794
// Clone rather than returning the texture directly so each region cache entry owns
98-
// a distinct object. Returning the shared texture would cause symbol properties
99-
// to be overwritten or deleted by concurrent cache entries during race conditions,
100-
// (create, delete, create) leading to errors on disposal.
95+
// a distinct object. Returning the shared texture would cause concurrent cache entries
96+
// to alias the same object, leading to errors on disposal.
10197
// Cloning shares the underlying Source so no extra GPU memory is used.
10298
const [ tx, ty, tl ] = singleTileBounds;
10399
const clone = tiledImageSource.get( tx, ty, tl ).clone();
104-
clone[ IS_DIRECT_TILE ] = true;
105-
clone[ LOCK_TOKENS ] = tokens;
106100

107101
return clone;
108102

@@ -118,7 +112,6 @@ export class TiledRegionImageSource extends RegionImageSource {
118112
const target = new CanvasTexture( canvas );
119113
target.colorSpace = SRGBColorSpace;
120114
target.generateMipmaps = false;
121-
target[ LOCK_TOKENS ] = tokens;
122115

123116
// TODO: we could draw the parent tile data here if it's available just to make sure we
124117
// have something to display but the texture is not usable until it returns. Though it
@@ -142,14 +135,13 @@ export class TiledRegionImageSource extends RegionImageSource {
142135

143136
}
144137

145-
disposeItem( target ) {
138+
disposeItem( target, [ minX, minY, maxX, maxY, level ] ) {
146139

147-
const { IS_DIRECT_TILE, LOCK_TOKENS } = this;
148-
const [ minX, minY, maxX, maxY, level ] = target[ LOCK_TOKENS ];
140+
if ( target ) {
149141

150-
target.dispose();
151-
delete target[ IS_DIRECT_TILE ];
152-
delete target[ LOCK_TOKENS ];
142+
target.dispose();
143+
144+
}
153145

154146
// Unlock the component tiles using the stored tokens
155147
this._markImages( [ minX, minY, maxX, maxY ], level, true );

src/three/plugins/images/sources/TiledImageSource.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ export class TiledImageSource extends DataCache {
6969
// dispose of the item that was fetched
7070
disposeItem( texture ) {
7171

72+
if ( ! texture ) {
73+
74+
return;
75+
76+
}
77+
7278
texture.dispose();
7379
if ( texture.image instanceof ImageBitmap ) {
7480

src/three/plugins/images/utils/DataCache.js

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ export class DataCache {
1818
}
1919

2020
// overridable
21-
fetchItem() {}
22-
disposeItem() {}
21+
fetchItem( keys, signal ) {}
22+
// called with null if the fetch failed
23+
disposeItem( item, keys ) {}
2324
getMemoryUsage( item ) {
2425

2526
return 0;
@@ -207,17 +208,27 @@ export class DataCache {
207208
if ( result instanceof Promise ) {
208209

209210
// "disposeItem" will throw potentially if fetch, etc are cancelled using the abort signal
210-
result.then( item => {
211+
result
212+
.then( item => {
211213

212-
this.disposeItem( item );
213-
this.count --;
214-
this.cachedBytes -= info.bytes;
214+
this.disposeItem( item, info.args );
215215

216-
} ).catch( () => {} );
216+
} )
217+
.catch( () => {
218+
219+
this.disposeItem( null, info.args );
220+
221+
} )
222+
.finally( () => {
223+
224+
this.count --;
225+
this.cachedBytes -= info.bytes;
226+
227+
} );
217228

218229
} else {
219230

220-
this.disposeItem( result );
231+
this.disposeItem( result, info.args );
221232
this.count --;
222233
this.cachedBytes -= info.bytes;
223234

0 commit comments

Comments
 (0)