diff --git a/src/tile.js b/src/tile.js index c24dcddc..4e1350cb 100644 --- a/src/tile.js +++ b/src/tile.js @@ -268,6 +268,18 @@ $.Tile = function(level, x, y, bounds, exists, url, context2D, loadWithAjax, aja * @private */ this.processing = false; + /** + * Remembers last processing time of the tile, 1 if the tile has just been loaded. + * @private + */ + this.lastProcess = 0; + /** + * Transforming flag, exempt the tile from any processing since it is being transformed to a drawer-compatible + * format. This process cannot be paused and the tile cannot be touched during the process. Used externally. + * @member {Boolean|Number} + * @private + */ + this.transforming = false; }; /** @lends OpenSeadragon.Tile.prototype */ @@ -603,15 +615,14 @@ $.Tile.prototype = { tileAllowNotLoaded: _allowTileNotLoaded }); this.cacheKey = newCacheKey; - return; - } - // If we requested restore, perform now - if (requestedRestore) { + } else if (requestedRestore) { + // If we requested restore, perform now this.tiledImage._tileCache.restoreTilesThatShareOriginalCache( this, this.getCache(this.originalCacheKey), this.__restoreRequestedFree ); } - // Else no work to be done + // If transforming was set, we finished: drawer transform always finishes with updateRenderTarget() + this.transforming = false; }, /** diff --git a/src/tilecache.js b/src/tilecache.js index 288cc0c1..2b36735f 100644 --- a/src/tilecache.js +++ b/src/tilecache.js @@ -163,15 +163,14 @@ /** * @private - * Access of the data by drawers, synchronous function. - * - * When drawers access data, they can choose to access this data as internal copy + * Access of the data by drawers, synchronous function. Should always access a valid main cache, e.g. + * cache swap should be atomic. * * @param {OpenSeadragon.DrawerBase} drawer - * until 'setData' is called + * @param {OpenSeadragon.Tile} tileDrawn * @returns {any|undefined} desired data if available, undefined if conversion must be done */ - getDataForRendering(drawer) { + getDataForRendering(drawer, tileDrawn) { const supportedTypes = drawer.getSupportedDataFormats(), keepInternalCopy = drawer.options.usePrivateCache; if (this.loaded && supportedTypes.includes(this.type)) { @@ -180,6 +179,7 @@ if (this._destroyed) { $.console.error("Attempt to draw tile with destroyed main cache!"); + tileDrawn._unload(); // try to restore the state so that the tile is later on fetched again return undefined; } diff --git a/src/tiledimage.js b/src/tiledimage.js index 0421cb97..798fae43 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -2146,11 +2146,13 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag }).then(_ => { tile.loading = false; tile.loaded = true; + tile.lastProcess = 1; resolver(tile); }); } else { tile.loading = false; tile.loaded = true; + tile.lastProcess = 1; resolver(tile); } } diff --git a/src/world.js b/src/world.js index f903b322..c865fcf6 100644 --- a/src/world.js +++ b/src/world.js @@ -249,11 +249,12 @@ $.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.W // const promise = this.requestTileInvalidateEvent(priorityTiles, tStamp, restoreTiles); // return promise.then(() => this.requestTileInvalidateEvent(this.viewer.tileCache.getLoadedTilesFor(null), tStamp, restoreTiles)); - // - //return this.requestTileInvalidateEvent(this.viewer.tileCache.getLoadedTilesFor(null), tStamp, restoreTiles); + // Tile-first retrieval fires computation on tiles that share cache, which are filtered out by processing property + return this.requestTileInvalidateEvent(this.viewer.tileCache.getLoadedTilesFor(null), tStamp, restoreTiles); - // Try go cache-first order, ensuring all tiles that do have cache entry get processed - return this.requestTileInvalidateEvent(new Set(Object.values(this.viewer.tileCache._cachesLoaded).map(c => !c._destroyed && c._tiles[0])), tStamp, restoreTiles); + // Cache-first update tile retrieval is nicer since there might be many tiles sharing + // return this.requestTileInvalidateEvent(new Set(Object.values(this.viewer.tileCache._cachesLoaded) + // .map(c => !c._destroyed && c._tiles[0])), tStamp, restoreTiles); }, /** @@ -268,23 +269,25 @@ $.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.W * @return {OpenSeadragon.Promise} */ requestTileInvalidateEvent: function(tilesToProcess, tStamp, restoreTiles = true) { - const tileList = []; + const tileList = [], + markedTiles = []; for (const tile of tilesToProcess) { - //todo if tiles are processing then it does not update them to the latest stage - // but if we do allow it, then a collision on processing occurs - swap in middle of rendering, we need - // to check what is the latest processing stamp and if it is bigger than current process finish we need to abort - if (!tile || !tile.loaded || tile.processing) { /* || tile.processing*/ + // We allow re-execution on tiles that are in process but have too low processing timestamp, + // which must be solved by ensuring subsequent data calls in the suddenly outdated processing + // pipeline take no effect. + // TODO: cross writes on tile when processing cause memory errors - either ensure + // tile makes NOOP for any execution that comes with older stamp, or prevent update routine + // to happen simultanously + if (!tile || !tile.loaded || (tile.processing && tile.processing <= tStamp) || tile.transforming) { continue; } + // TODO: consider locking on the original cache, which should be read only + // or lock the main cache, and compare with tile.processing tstamp const tileCache = tile.getCache(); - if (tileCache._updateStamp >= tStamp) { - continue; - } - tileCache._updateStamp = tStamp; - for (let t of tileCache._tiles) { - // Mark all as processing + // Mark all related tiles as processing and cache the references to unmark later on t.processing = tStamp; + markedTiles.push(t); } tileList.push(tile); } @@ -307,23 +310,23 @@ $.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.W tile: tile, tiledImage: tile.tiledImage, }).then(() => { - // asynchronous finisher - return tile.updateRenderTargetWithDataTransform(drawerId, supportedFormats, keepInternalCacheCopy); + if (tile.processing === tStamp) { + // asynchronous finisher + tile.transforming = tStamp; + return tile.updateRenderTargetWithDataTransform(drawerId, supportedFormats, keepInternalCacheCopy); + } + return null; }).catch(e => { $.console.error("Update routine error:", e); }); }); return $.Promise.all(jobList).then(() => { - for (let tile of tileList) { - // pass update stamp on the new cache object to avoid needless updates - const newCache = tile.getCache(); - if (newCache) { - newCache._updateStamp = tStamp; - tile.processing = false; - } + for (let tile of markedTiles) { + tile.lastProcess = tile.processing; + tile.processing = false; + tile.transforming = false; } - this.draw(); }); },