From a5c569ab5d866a315363cd9d1d0909e1ebd897f1 Mon Sep 17 00:00:00 2001 From: Aiosa <469130@mail.muni.cz> Date: Thu, 19 Dec 2024 16:11:53 +0100 Subject: [PATCH 01/11] fix: reference to the cpuData on webgldrawer --- src/drawerbase.js | 2 +- src/tilecache.js | 4 ++-- src/webgldrawer.js | 12 +++++++----- test/modules/tilecache.js | 4 ++-- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/drawerbase.js b/src/drawerbase.js index 3355b45c..d84531f7 100644 --- a/src/drawerbase.js +++ b/src/drawerbase.js @@ -161,7 +161,7 @@ OpenSeadragon.DrawerBase = class DrawerBase{ $.console.warn("Attempt to draw tile %s when not cached!", tile); return undefined; } - const dataCache = cache.getDataForRendering(this, tile); + const dataCache = cache.getCacheForRendering(this, tile); return dataCache && dataCache.data; } diff --git a/src/tilecache.js b/src/tilecache.js index f23d5e26..a66a32f5 100644 --- a/src/tilecache.js +++ b/src/tilecache.js @@ -176,7 +176,7 @@ * Returns undefined if the data is not ready for rendering. * @private */ - getDataForRendering(drawer, tileToDraw) { + getCacheForRendering(drawer, tileToDraw) { const supportedTypes = drawer.getSupportedDataFormats(), keepInternalCopy = drawer.options.usePrivateCache; if (this.loaded && supportedTypes.includes(this.type)) { @@ -264,7 +264,7 @@ const conversionPath = $.convertor.getConversionPath(this.type, supportedTypes); if (!conversionPath) { - $.console.error(`[getDataForRendering] Conversion ${this.type} ---> ${supportedTypes} cannot be done!`); + $.console.error(`[getCacheForRendering] Conversion ${this.type} ---> ${supportedTypes} cannot be done!`); return $.Promise.resolve(this); } const newInternalCache = new $.SimpleCacheRecord(); diff --git a/src/webgldrawer.js b/src/webgldrawer.js index eaa26623..02be1db0 100644 --- a/src/webgldrawer.js +++ b/src/webgldrawer.js @@ -100,9 +100,9 @@ this._setupCanvases(); this._setupRenderer(); + this._setupCallCount = 1; this._supportedFormats = this._setupTextureHandlers(); this._requiredFormats = this._supportedFormats; - this._setupCallCount = 1; this.context = this._outputContext; // API required by tests } @@ -249,7 +249,7 @@ } const dataCache = cache.getDataForRendering(this, tile); // Use CPU Data for the drawer instead - return dataCache && dataCache.cpuData; + return dataCache && dataCache.data.cpuData.getContext("2d"); }; } @@ -950,7 +950,7 @@ return { texture: texture, position: position, - cpuData: data // Reference to the outer cache data, used to draw if webgl canont be used + cpuData: data }; }; const tex2DCompatibleDestructor = textureInfo => { @@ -967,10 +967,12 @@ // We should be OK uploading any of these types. The complexity is selected to be O(3n), should be // more than linear pass over pixels $.convertor.learn("context2d", c2dTexType, (t, d) => tex2DCompatibleLoader(t, d.canvas), 1, 3); - $.convertor.learn("image", imageTexType, tex2DCompatibleLoader, 1, 3); + // TODO: lost support for image + // $.convertor.learn("image", imageTexType, tex2DCompatibleLoader, 1, 3); $.convertor.learnDestroy(c2dTexType, tex2DCompatibleDestructor); - $.convertor.learnDestroy(imageTexType, tex2DCompatibleDestructor); + // TODO + // $.convertor.learnDestroy(imageTexType, tex2DCompatibleDestructor); return [c2dTexType, imageTexType]; } diff --git a/test/modules/tilecache.js b/test/modules/tilecache.js index accbdec5..6f56c938 100644 --- a/test/modules/tilecache.js +++ b/test/modules/tilecache.js @@ -341,7 +341,7 @@ const cache = tile.getCache(); test.equal(cache.type, T_A, "Cache data was not affected, the drawer uses internal cache."); - const internalCache = cache.getDataForRendering(drawer, tile); + const internalCache = cache.getCacheForRendering(drawer, tile); test.equal(internalCache.type, T_C, "Conversion A->C ready, since there is no way to get to T_E."); test.ok(internalCache.loaded, "Internal cache ready."); } @@ -424,7 +424,7 @@ test.equal(cache.type, T_C, "Main Cache Updated (suite 1)"); test.equal(cache.data, previousTestValue, "Main Cache Updated (suite 1)"); - const internalCache = cache.getDataForRendering(drawer, tile); + const internalCache = cache.getCacheForRendering(drawer, tile); test.equal(T_C, internalCache.type, "Conversion A->C ready, since there is no way to get to T_E."); test.ok(internalCache.loaded, "Internal cache ready."); } From 4c2b0715af4d9e5a7f62127f0dc25047a3aab130 Mon Sep 17 00:00:00 2001 From: Aiosa <469130@mail.muni.cz> Date: Thu, 19 Dec 2024 16:20:50 +0100 Subject: [PATCH 02/11] fix: typo --- src/webgldrawer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webgldrawer.js b/src/webgldrawer.js index 02be1db0..1ae21e80 100644 --- a/src/webgldrawer.js +++ b/src/webgldrawer.js @@ -247,7 +247,7 @@ $.console.warn("Attempt to draw tile %s when not cached!", tile); return undefined; } - const dataCache = cache.getDataForRendering(this, tile); + const dataCache = cache.getCacheForRendering(this, tile); // Use CPU Data for the drawer instead return dataCache && dataCache.data.cpuData.getContext("2d"); }; From 6315662078823fe3035f128ef0c58f4eb1a2dbda Mon Sep 17 00:00:00 2001 From: Aiosa <469130@mail.muni.cz> Date: Tue, 7 Jan 2025 19:56:21 +0100 Subject: [PATCH 03/11] DrawerBase API for internal cache. --- src/drawerbase.js | 30 ++++- src/tilecache.js | 297 ++++++++++++++++++++++----------------------- src/webgldrawer.js | 169 +++++++++++--------------- src/world.js | 11 +- 4 files changed, 250 insertions(+), 257 deletions(-) diff --git a/src/drawerbase.js b/src/drawerbase.js index d84531f7..c3138655 100644 --- a/src/drawerbase.js +++ b/src/drawerbase.js @@ -97,7 +97,8 @@ OpenSeadragon.DrawerBase = class DrawerBase{ */ get defaultOptions() { return { - usePrivateCache: false + usePrivateCache: false, + preloadCache: true, }; } @@ -161,7 +162,7 @@ OpenSeadragon.DrawerBase = class DrawerBase{ $.console.warn("Attempt to draw tile %s when not cached!", tile); return undefined; } - const dataCache = cache.getCacheForRendering(this, tile); + const dataCache = cache.getDataForRendering(this, tile); return dataCache && dataCache.data; } @@ -240,6 +241,31 @@ OpenSeadragon.DrawerBase = class DrawerBase{ $.console.warn('[drawer].clear() is deprecated. The drawer is responsible for clearing itself as needed before drawing tiles.'); } + /** + * If options.usePrivateCache is true, this method MUST RETURN the private cache content + * @param {OpenSeadragon.CacheRecord} cache + * @param {OpenSeadragon.Tile} tile + * @return any + */ + dataCreate(cache, tile) {} + + /** + * It is possible to perform any necessary cleanup on internal cache, necessary if you + * need to clean up some memory (e.g. destroy canvas by setting with & height to 0). + * @param {*} data object returned by dataCreate(...) + */ + dataFree(data) {} + + /** + * Call to invalidate internal cache. It will be rebuilt. With synchronous converions, + * it will be rebuilt immediatelly. With asynchronous, it will be rebuilt once invalidation + * routine happens, e.g. you should call also requestInvalidate() if you need to happen + * it as soon as possible. + */ + setDataNeedsRefresh() { + this._dataNeedsRefresh = $.now(); + } + // Private functions /** diff --git a/src/tilecache.js b/src/tilecache.js index a66a32f5..105a24b0 100644 --- a/src/tilecache.js +++ b/src/tilecache.js @@ -79,7 +79,7 @@ */ await() { if (!this._promise) { //if not cache loaded, do not fail - return $.Promise.resolve(); + return $.Promise.resolve(this._data); } return this._promise; } @@ -171,83 +171,133 @@ * @param {OpenSeadragon.Tile} tileToDraw reference to the tile that is in the process of drawing and * for which we request the data; if we attempt to draw such tile while main cache target is destroyed, * attempt to reset the tile state to force system to re-download it again - * @returns {OpenSeadragon.CacheRecord|OpenSeadragon.SimpleCacheRecord|undefined} desired data if available, + * @returns {OpenSeadragon.CacheRecord|OpenSeadragon.InternalCacheRecord|undefined} desired data if available, * wrapped in the cache container. This data is guaranteed to be loaded & in the type supported by the drawer. * Returns undefined if the data is not ready for rendering. * @private */ - getCacheForRendering(drawer, tileToDraw) { - const supportedTypes = drawer.getSupportedDataFormats(), - keepInternalCopy = drawer.options.usePrivateCache; - if (this.loaded && supportedTypes.includes(this.type)) { - return this; - } + getDataForRendering(drawer, tileToDraw) { + const keepInternalCopy = drawer.options.usePrivateCache; + if (!this.loaded) { + $.console.error("Attempt to draw tile when not loaded main cache!"); + return undefined; + } if (this._destroyed) { $.console.error("Attempt to draw tile with destroyed main cache!"); tileToDraw._unload(); // try to restore the state so that the tile is later on fetched again return undefined; } - let internalCache = this[DRAWER_INTERNAL_CACHE]; - internalCache = internalCache && internalCache[drawer.getId()]; - if (keepInternalCopy && !internalCache) { - $.console.warn("Attempt to render cache that is not prepared for current drawer " + - "supported format: the preparation should've happened after tile processing has finished.", - this, tileToDraw); - - this.prepareForRendering(drawer.getId(), supportedTypes, keepInternalCopy) - .then(() => this._triggerNeedsDraw()); + const supportedTypes = drawer.getSupportedDataFormats(); + if (!supportedTypes.includes(this.type)) { + $.console.error("Attempt to draw tile with unsupported target drawer type!"); + this.prepareForRendering(drawer); return undefined; } - if (internalCache) { - internalCache.withTileReference(this._tRef); - } else { - internalCache = this; + // If we support internal cache + if (keepInternalCopy && !drawer.options.preloadCache) { + // let sync preparation handle data + if (!drawer.options.preloadCache) { + return this.prepareInternalCacheSync(drawer); + } + // or check that it was properly initiated before returning + const internalCache = this._getInternalCacheRef(drawer); + if (!internalCache || !internalCache.loaded) { + $.console.error("Attempt to draw tile with internal cache non-ready state!"); + return undefined; + } + return internalCache; } - // Cache in the process of loading, no-op - if (!internalCache.loaded) { - $.console.warn("Attempt to render cache that is not prepared for current drawer: " + - "internal cache still loading: this should be awaited.", - this, tileToDraw); - this._triggerNeedsDraw(); - return undefined; - } - - if (!supportedTypes.includes(internalCache.type)) { - let logReference = this[DRAWER_INTERNAL_CACHE]; - logReference = logReference ? Object.entries(logReference) : this; - $.console.warn("Attempt to render cache that is not prepared for current drawer " + - "supported format: the preparation should've happened after tile processing has finished.", - logReference, tileToDraw); - - internalCache.transformTo(supportedTypes.length > 1 ? supportedTypes : supportedTypes[0]) - .then(() => this._triggerNeedsDraw()); - return undefined; // type is NOT compatible - } - return internalCache; + // If no internal cache support, we are ready - just return self reference + return this; } /** * Should not be called if cache type is already among supported types * @private - * @param drawerId - * @param supportedTypes - * @param keepInternalCopy if a drawer requests internal copy, it means it can only use - * given cache for itself, cannot be shared -> initialize privately - * @return {OpenSeadragon.Promise | null} - * reference to the cache processed for drawer rendering requirements, or null on error + * @param {OpenSeadragon.DrawerBase} drawer + * @return {OpenSeadragon.Promise<*>} reference to the data, + * or null if not data yet loaded/ready (usually due to error) */ - prepareForRendering(drawerId, supportedTypes, keepInternalCopy = true) { - // if not internal copy and we have no data, or we are ready to render, exit - if (!this.loaded || supportedTypes.includes(this.type)) { - return $.Promise.resolve(this); + prepareForRendering(drawer) { + const supportedTypes = drawer.getRequiredDataFormats(); + + if (drawer.options.usePrivateCache && drawer.options.preloadCache) { + return this.prepareInternalCacheAsync(drawer).then(_ => { + // if not internal copy and we have no data, or we are ready to render, exit + if (!this.loaded || supportedTypes.includes(this.type)) { + return this.await(); + } + + return this.transformTo(supportedTypes); + }); } - if (!keepInternalCopy) { - return this.transformTo(supportedTypes); + if (!this.loaded || supportedTypes.includes(this.type)) { + return this.await(); + } + return this.transformTo(supportedTypes); + } + + /** + * Must not be called if drawer.options.usePrivateCache == false. Called inside prepareForRenderine + * by cache itself if preloadCache == true (supports async behavior). + * + * @private + * @param {OpenSeadragon.DrawerBase} drawer + * @return {OpenSeadragon.Promise<*>} reference to the data wrapped in a promise, + * or null if not data yet loaded/ready (usually due to error) + */ + prepareInternalCacheAsync(drawer) { + let internalCache = this._getInternalCacheRef(drawer); + if (this._checkInternalCacheUpToDate(internalCache, drawer)) { + return internalCache.await(); + } + + // Force reset + if (internalCache && !internalCache.loaded) { + internalCache.await().then(() => internalCache.destroy()); + } + + $.console.assert(this._tRef, "Data Create called from invalidation routine needs tile reference!"); + const transformedData = drawer.dataCreate(this, this._tRef); + $.console.assert(transformedData !== undefined, "[DrawerBase.dataCreate] must return a value if usePrivateCache is enabled!"); + internalCache = this[DRAWER_INTERNAL_CACHE][drawer.getId()] = new $.InternalCacheRecord(transformedData, (data) => drawer.dataFree(data)); + return internalCache.await(); + } + + /** + * Must not be called if drawer.options.usePrivateCache == false. Called inside getDataForRendering + * by cache itself if preloadCache == false (without support for async behavior). + * @private + * @param {OpenSeadragon.DrawerBase} drawer + * @return {OpenSeadragon.InternalCacheRecord} reference to the cache + */ + prepareInternalCacheSync(drawer) { + let internalCache = this._getInternalCacheRef(drawer); + if (this._checkInternalCacheUpToDate(internalCache, drawer)) { + return internalCache; + } + + // Force reset + if (internalCache) { + internalCache.destroy(); + } + + $.console.assert(this._tRef, "Data Create called from drawing loop needs tile reference!"); + const transformedData = drawer.dataCreate(this, this._tRef); + $.console.assert(transformedData !== undefined, "[DrawerBase.dataCreate] must return a value if usePrivateCache is enabled!"); + internalCache = this[DRAWER_INTERNAL_CACHE][drawer.getId()] = new $.InternalCacheRecord(transformedData, (data) => drawer.dataFree(data)); + return internalCache; + } + + _getInternalCacheRef(drawer) { + const options = drawer.options; + if (!options.usePrivateCache) { + return $.Promise.reject("[CacheRecord.prepareInternalCacheSync] must not be called when usePrivateCache is false."); } // we can get here only if we want to render incompatible type @@ -255,32 +305,13 @@ if (!internalCache) { internalCache = this[DRAWER_INTERNAL_CACHE] = {}; } + return internalCache[drawer.getId()]; + } - internalCache = internalCache[drawerId]; - if (internalCache && supportedTypes.includes(internalCache.type)) { - // already done - return $.Promise.resolve(this); - } - - const conversionPath = $.convertor.getConversionPath(this.type, supportedTypes); - if (!conversionPath) { - $.console.error(`[getCacheForRendering] Conversion ${this.type} ---> ${supportedTypes} cannot be done!`); - return $.Promise.resolve(this); - } - const newInternalCache = new $.SimpleCacheRecord(); - - newInternalCache.withTileReference(this._tRef); - const selectedFormat = conversionPath[conversionPath.length - 1].target.value; - return $.convertor.convert(this._tRef, this.data, this.type, selectedFormat).then(data => { - newInternalCache.setDataAs(data, selectedFormat); // synchronous, SimpleCacheRecord call - - // if existed, delete - if (internalCache) { - internalCache.destroy(); - } - this[DRAWER_INTERNAL_CACHE][drawerId] = newInternalCache; - return newInternalCache; - }); + _checkInternalCacheUpToDate(internalCache, drawer) { + // We respect existing records, unless they are outdated. Invalidation routine by its nature + // destroys internal cache, therefore we do not need to check if internal cache is consistent with its parent. + return internalCache && internalCache.loaded && internalCache.tstamp >= drawer._dataNeedsRefresh; } /** @@ -623,7 +654,7 @@ }; /** - * @class SimpleCacheRecord + * @class InternalCacheRecord * @memberof OpenSeadragon * @classdesc Simple cache record without robust support for async access. Meant for internal use only. * @@ -635,12 +666,22 @@ * It also does not record tiles nor allows cache/tile sharing. * @private */ - $.SimpleCacheRecord = class { - constructor(preferredTypes) { - this._data = null; - this._type = null; - this.loaded = false; - this.format = Array.isArray(preferredTypes) ? preferredTypes : null; + $.InternalCacheRecord = class { + constructor(data, onDestroy) { + this.tstamp = $.now(); + this._ondestroy = onDestroy; + + if (data instanceof $.Promise) { + this._promise = data; + data.then(data => { + this.loaded = true; + this._data = data; + }); + } else { + this._promise = null; + this.loaded = true; + this._data = data; + } } /** @@ -656,86 +697,42 @@ * @returns {string} */ get type() { - return this._type; + return "__internal_cache__"; + } + + /** + * Await ongoing process so that we get cache ready on callback. + * @returns {OpenSeadragon.Promise} + */ + await() { + if (!this._promise) { //if not cache loaded, do not fail + return $.Promise.resolve(this._data); + } + return this._promise; } /** * Must be called before transformTo or setDataAs. To keep * compatible api with CacheRecord where tile refs are known. * @param {OpenSeadragon.Tile} referenceTile reference tile for conversion - * @return {OpenSeadragon.SimpleCacheRecord} self reference for builder pattern + * @return {OpenSeadragon.InternalCacheRecord} self reference for builder pattern */ withTileReference(referenceTile) { this._temporaryTileRef = referenceTile; return this; } - /** - * Transform cache to desired type and get the data after conversion. - * Does nothing if the type equals to the current type. Asynchronous. - * @param {string|string[]} type if array provided, the system will - * try to optimize for the best type to convert to. - * @returns {OpenSeadragon.Promise} - */ - transformTo(type) { - $.console.assert(this._temporaryTileRef, "SimpleCacheRecord needs tile reference set before update operation!"); - const convertor = $.convertor, - conversionPath = convertor.getConversionPath(this._type, type); - if (!conversionPath) { - $.console.error(`[SimpleCacheRecord.transformTo] Conversion ${this._type} ---> ${type} cannot be done!`); - return $.Promise.resolve(); //no-op - } - - const stepCount = conversionPath.length, - _this = this, - convert = (x, i) => { - if (i >= stepCount) { - _this._data = x; - _this.loaded = true; - _this._temporaryTileRef = null; - return $.Promise.resolve(x); - } - let edge = conversionPath[i]; - try { - // no test for y - less robust approach - let y = edge.transform(this._temporaryTileRef, x); - convertor.destroy(x, edge.origin.value); - const result = $.type(y) === "promise" ? y : $.Promise.resolve(y); - return result.then(res => convert(res, i + 1)); - } catch (e) { - _this.loaded = false; - _this._temporaryTileRef = null; - throw e; - } - }; - - this.loaded = false; - // Read target type from the conversion path: [edge.target] = Vertex, its value=type - this._type = conversionPath[stepCount - 1].target.value; - const promise = convert(this._data, 0); - this._data = undefined; - return promise; - } - /** * Free all the data and call data destructors if defined. */ destroy() { - $.convertor.destroy(this._data, this._type); - this._data = null; - this._type = null; - } - - /** - * Safely overwrite the cache data and return the old data - * @private - */ - setDataAs(data, type) { - // no check for state, users must ensure compatibility manually - $.convertor.destroy(this._data, this._type); - this._type = type; - this._data = data; - this.loaded = true; + if (this.loaded) { + if (this._ondestroy) { + this._ondestroy(this._data); + } + this._data = null; + this.loaded = false; + } } }; diff --git a/src/webgldrawer.js b/src/webgldrawer.js index 1ae21e80..7e3a5fd1 100644 --- a/src/webgldrawer.js +++ b/src/webgldrawer.js @@ -100,10 +100,7 @@ this._setupCanvases(); this._setupRenderer(); - this._setupCallCount = 1; - this._supportedFormats = this._setupTextureHandlers(); - this._requiredFormats = this._supportedFormats; - + this._supportedFormats = ["context2d", "image"]; this.context = this._outputContext; // API required by tests } @@ -247,9 +244,9 @@ $.console.warn("Attempt to draw tile %s when not cached!", tile); return undefined; } - const dataCache = cache.getCacheForRendering(this, tile); + const dataCache = cache.getDataForRendering(this, tile); // Use CPU Data for the drawer instead - return dataCache && dataCache.data.cpuData.getContext("2d"); + return dataCache && dataCache.cpuData; }; } @@ -480,10 +477,6 @@ } - getRequiredDataFormats() { - return this._requiredFormats; - } - // Public API required by all Drawer implementations /** * Sets whether image smoothing is enabled or disabled @@ -492,15 +485,9 @@ setImageSmoothingEnabled(enabled){ if( this._imageSmoothingEnabled !== enabled ){ this._imageSmoothingEnabled = enabled; - - // Todo consider removing old type handlers if _supportedFormats had already types defined, - // and remove support for rendering old types... - const newFormats = this._setupTextureHandlers(); // re-sets the type to enforce re-initialization - this._supportedFormats.push(...newFormats); - this._requiredFormats = newFormats; - return this.viewer.requestInvalidate(); + this.setDataNeedsRefresh(); + this.viewer.forceRedraw(); } - return $.Promise.resolve(); } /** @@ -883,97 +870,83 @@ this.viewer.addHandler("resize", this._resizeHandler); } - _setupTextureHandlers() { - const tex2DCompatibleLoader = (tile, data) => { - let tiledImage = tile.tiledImage; - let gl = this._gl; - let texture; - let position; + dataCreate(cache, tile) { + let tiledImage = tile.tiledImage; + let gl = this._gl; + let texture; + let position; - if (!tiledImage.isTainted()) { - if((data instanceof CanvasRenderingContext2D) && $.isCanvasTainted(data.canvas)){ - tiledImage.setTainted(true); - $.console.warn('WebGL cannot be used to draw this TiledImage because it has tainted data. Does crossOriginPolicy need to be set?'); - this._raiseDrawerErrorEvent(tiledImage, 'Tainted data cannot be used by the WebGLDrawer. Falling back to CanvasDrawer for this TiledImage.'); + const data = cache.data; + + if (!tiledImage.isTainted()) { + if((data instanceof CanvasRenderingContext2D) && $.isCanvasTainted(data.canvas)){ + tiledImage.setTainted(true); + $.console.warn('WebGL cannot be used to draw this TiledImage because it has tainted data. Does crossOriginPolicy need to be set?'); + this._raiseDrawerErrorEvent(tiledImage, 'Tainted data cannot be used by the WebGLDrawer. Falling back to CanvasDrawer for this TiledImage.'); + } else { + let sourceWidthFraction, sourceHeightFraction; + if (tile.sourceBounds) { + sourceWidthFraction = Math.min(tile.sourceBounds.width, data.width) / data.width; + sourceHeightFraction = Math.min(tile.sourceBounds.height, data.height) / data.height; } else { - let sourceWidthFraction, sourceHeightFraction; - if (tile.sourceBounds) { - sourceWidthFraction = Math.min(tile.sourceBounds.width, data.width) / data.width; - sourceHeightFraction = Math.min(tile.sourceBounds.height, data.height) / data.height; - } else { - sourceWidthFraction = 1; - sourceHeightFraction = 1; - } + sourceWidthFraction = 1; + sourceHeightFraction = 1; + } - // create a gl Texture for this tile and bind the canvas with the image data - texture = gl.createTexture(); - let overlap = tiledImage.source.tileOverlap; - if( overlap > 0){ - // calculate the normalized position of the rect to actually draw - // discarding overlap. - let overlapFraction = this._calculateOverlapFraction(tile, tiledImage); + // create a gl Texture for this tile and bind the canvas with the image data + texture = gl.createTexture(); + let overlap = tiledImage.source.tileOverlap; + if( overlap > 0){ + // calculate the normalized position of the rect to actually draw + // discarding overlap. + let overlapFraction = this._calculateOverlapFraction(tile, tiledImage); - let left = (tile.x === 0 ? 0 : overlapFraction.x) * sourceWidthFraction; - let top = (tile.y === 0 ? 0 : overlapFraction.y) * sourceHeightFraction; - let right = (tile.isRightMost ? 1 : 1 - overlapFraction.x) * sourceWidthFraction; - let bottom = (tile.isBottomMost ? 1 : 1 - overlapFraction.y) * sourceHeightFraction; - position = this._makeQuadVertexBuffer(left, right, top, bottom); - } else if (sourceWidthFraction === 1 && sourceHeightFraction === 1) { - // no overlap and no padding: this texture can use the unit quad as its position data - position = this._unitQuad; - } else { - position = this._makeQuadVertexBuffer(0, sourceWidthFraction, 0, sourceHeightFraction); - } + let left = (tile.x === 0 ? 0 : overlapFraction.x) * sourceWidthFraction; + let top = (tile.y === 0 ? 0 : overlapFraction.y) * sourceHeightFraction; + let right = (tile.isRightMost ? 1 : 1 - overlapFraction.x) * sourceWidthFraction; + let bottom = (tile.isBottomMost ? 1 : 1 - overlapFraction.y) * sourceHeightFraction; + position = this._makeQuadVertexBuffer(left, right, top, bottom); + } else if (sourceWidthFraction === 1 && sourceHeightFraction === 1) { + // no overlap and no padding: this texture can use the unit quad as its position data + position = this._unitQuad; + } else { + position = this._makeQuadVertexBuffer(0, sourceWidthFraction, 0, sourceHeightFraction); + } - gl.activeTexture(gl.TEXTURE0); - gl.bindTexture(gl.TEXTURE_2D, texture); - // Set the parameters so we can render any size image. - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, this._textureFilter()); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, this._textureFilter()); + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, texture); + // Set the parameters so we can render any size image. + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, this._textureFilter()); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, this._textureFilter()); - try { - // This depends on gl.TEXTURE_2D being bound to the texture - // associated with this canvas before calling this function - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, data); - } catch (e){ - // Todo a bit dirty re-use of the tainted flag, but makes the code more stable - tiledImage.setTainted(true); - $.console.error('Error uploading image data to WebGL. Falling back to canvas renderer.', e); - this._raiseDrawerErrorEvent(tiledImage, 'Unknown error when uploading texture. Falling back to CanvasDrawer for this TiledImage.'); - } + try { + // This depends on gl.TEXTURE_2D being bound to the texture + // associated with this canvas before calling this function + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, data); + } catch (e){ + // Todo a bit dirty re-use of the tainted flag, but makes the code more stable + tiledImage.setTainted(true); + $.console.error('Error uploading image data to WebGL. Falling back to canvas renderer.', e); + this._raiseDrawerErrorEvent(tiledImage, 'Unknown error when uploading texture. Falling back to CanvasDrawer for this TiledImage.'); } } + } - // TextureInfo stored in the cache - return { - texture: texture, - position: position, - cpuData: data - }; - }; - const tex2DCompatibleDestructor = textureInfo => { - if (textureInfo) { - this._gl.deleteTexture(textureInfo.texture); - } + // TextureInfo stored in the cache + return { + texture: texture, + position: position, + cpuData: data // Reference to the outer cache data, used to draw if webgl cannot be used }; + } - const thisType = `${this.getId()}_${this._setupCallCount++}_TEX_2D`; - // Differentiate type also based on type used to upload data: we can support bidirectional conversion. - const c2dTexType = thisType + ":context2d", - imageTexType = thisType + ":image"; - - // We should be OK uploading any of these types. The complexity is selected to be O(3n), should be - // more than linear pass over pixels - $.convertor.learn("context2d", c2dTexType, (t, d) => tex2DCompatibleLoader(t, d.canvas), 1, 3); - // TODO: lost support for image - // $.convertor.learn("image", imageTexType, tex2DCompatibleLoader, 1, 3); - - $.convertor.learnDestroy(c2dTexType, tex2DCompatibleDestructor); - // TODO - // $.convertor.learnDestroy(imageTexType, tex2DCompatibleDestructor); - return [c2dTexType, imageTexType]; + dataFree(data) { + if (data && data.texture) { + this._gl.deleteTexture(data.texture); + data.texture = null; + } } // private diff --git a/src/world.js b/src/world.js index 3530cf05..6ab55742 100644 --- a/src/world.js +++ b/src/world.js @@ -310,9 +310,7 @@ $.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.W // We call the event on the parent viewer window no matter what const eventTarget = this.viewer.viewer || this.viewer; // However, we must pick the correct drawer reference (navigator VS viewer) - const supportedFormats = this.viewer.drawer.getRequiredDataFormats(); - const keepInternalCacheCopy = this.viewer.drawer.options.usePrivateCache; - const drawerId = this.viewer.drawer.getId(); + const drawer = this.viewer.drawer; const jobList = tileList.map(tile => { const tiledImage = tile.tiledImage; @@ -360,7 +358,6 @@ $.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.W tiledImage._tileCache.restoreTilesThatShareOriginalCache(tile, tile.getCache(tile.originalCacheKey), true); } }; - /** * @event tile-invalidated * @memberof OpenSeadragon.Viewer @@ -391,7 +388,7 @@ $.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.W }).then(_ => { if (originalCache.__invStamp === tStamp && (tile.loaded || tile.loading)) { if (workingCache) { - return workingCache.prepareForRendering(drawerId, supportedFormats, keepInternalCacheCopy).then(c => { + return workingCache.prepareForRendering(drawer).then(c => { if (c && originalCache.__invStamp === tStamp) { atomicCacheSwap(); originalCache.__invStamp = null; @@ -402,7 +399,7 @@ $.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.W // If we requested restore, perform now if (restoreTiles) { const freshOriginalCacheRef = tile.getCache(tile.originalCacheKey); - return freshOriginalCacheRef.prepareForRendering(drawerId, supportedFormats, keepInternalCacheCopy).then((c) => { + return freshOriginalCacheRef.prepareForRendering(drawer).then((c) => { if (c && originalCache.__invStamp === tStamp) { atomicCacheSwap(); originalCache.__invStamp = null; @@ -412,7 +409,7 @@ $.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.W // Preventive call to ensure we stay compatible const freshMainCacheRef = tile.getCache(); - return freshMainCacheRef.prepareForRendering(drawerId, supportedFormats, keepInternalCacheCopy).then(() => { + return freshMainCacheRef.prepareForRendering(drawer).then(() => { atomicCacheSwap(); originalCache.__invStamp = null; }); From 426700b1c6af95f61254b272c98f4ca5ca627af4 Mon Sep 17 00:00:00 2001 From: Aiosa <469130@mail.muni.cz> Date: Tue, 7 Jan 2025 19:57:37 +0100 Subject: [PATCH 04/11] Temporarily disable tests - discussing design. --- test/modules/tilecache.js | 386 +++++++++++++++++++------------------- 1 file changed, 193 insertions(+), 193 deletions(-) diff --git a/test/modules/tilecache.js b/test/modules/tilecache.js index 6f56c938..9f965866 100644 --- a/test/modules/tilecache.js +++ b/test/modules/tilecache.js @@ -314,200 +314,200 @@ }); //Tile API and cache interaction - QUnit.test('Tile: basic rendering & test setup', function(test) { - const done = test.async(); + // QUnit.test('Tile: basic rendering & test setup', function(test) { + // const done = test.async(); + // + // const tileCache = viewer.tileCache; + // const drawer = viewer.drawer; + // + // let testTileCalled = false; + // drawer.testEvents.addHandler('test-tile', e => { + // testTileCalled = true; + // test.ok(e.dataToDraw, "Tile data is ready to be drawn"); + // }); + // + // viewer.addHandler('open', async () => { + // await viewer.waitForFinishedJobsForTest(); + // await sleep(1); // necessary to make space for a draw call + // + // test.ok(viewer.world.getItemAt(0).source instanceof OpenSeadragon.EmptyTestT_ATileSource, "Tests are done with empty test source type T_A."); + // test.ok(viewer.world.getItemAt(1).source instanceof OpenSeadragon.EmptyTestT_ATileSource, "Tests are done with empty test source type T_A."); + // test.ok(testTileCalled, "Drawer tested at least one tile."); + // + // test.ok(typeAtoB > 1, "At least one conversion was triggered."); + // test.equal(typeAtoB, typeBtoC, "A->B = B->C, since we need to move all data to T_C for the drawer."); + // + // for (let tile of tileCache._tilesLoaded) { + // const cache = tile.getCache(); + // test.equal(cache.type, T_A, "Cache data was not affected, the drawer uses internal cache."); + // + // const internalCache = cache.getCacheForRendering(drawer, tile); + // test.equal(internalCache.type, T_C, "Conversion A->C ready, since there is no way to get to T_E."); + // test.ok(internalCache.loaded, "Internal cache ready."); + // } + // + // done(); + // }); + // viewer.open([ + // {isTestSource: true}, + // {isTestSource: true}, + // ]); + // }); - const tileCache = viewer.tileCache; - const drawer = viewer.drawer; - - let testTileCalled = false; - drawer.testEvents.addHandler('test-tile', e => { - testTileCalled = true; - test.ok(e.dataToDraw, "Tile data is ready to be drawn"); - }); - - viewer.addHandler('open', async () => { - await viewer.waitForFinishedJobsForTest(); - await sleep(1); // necessary to make space for a draw call - - test.ok(viewer.world.getItemAt(0).source instanceof OpenSeadragon.EmptyTestT_ATileSource, "Tests are done with empty test source type T_A."); - test.ok(viewer.world.getItemAt(1).source instanceof OpenSeadragon.EmptyTestT_ATileSource, "Tests are done with empty test source type T_A."); - test.ok(testTileCalled, "Drawer tested at least one tile."); - - test.ok(typeAtoB > 1, "At least one conversion was triggered."); - test.equal(typeAtoB, typeBtoC, "A->B = B->C, since we need to move all data to T_C for the drawer."); - - for (let tile of tileCache._tilesLoaded) { - const cache = tile.getCache(); - test.equal(cache.type, T_A, "Cache data was not affected, the drawer uses internal cache."); - - const internalCache = cache.getCacheForRendering(drawer, tile); - test.equal(internalCache.type, T_C, "Conversion A->C ready, since there is no way to get to T_E."); - test.ok(internalCache.loaded, "Internal cache ready."); - } - - done(); - }); - viewer.open([ - {isTestSource: true}, - {isTestSource: true}, - ]); - }); - - QUnit.test('Tile & Invalidation API: basic conversion & preprocessing', function(test) { - const done = test.async(); - - const tileCache = viewer.tileCache; - const drawer = viewer.drawer; - - let testTileCalled = false; - - let _currentTestVal = undefined; - let previousTestValue = undefined; - drawer.testEvents.addHandler('test-tile', e => { - test.ok(e.dataToDraw, "Tile data is ready to be drawn"); - if (_currentTestVal !== undefined) { - testTileCalled = true; - test.equal(e.dataToDraw, _currentTestVal, "Value is correct on the drawn data."); - } - }); - - function testDrawingRoutine(value) { - _currentTestVal = value; - viewer.world.needsDraw(); - viewer.world.draw(); - previousTestValue = value; - _currentTestVal = undefined; - } - - viewer.addHandler('open', async () => { - await viewer.waitForFinishedJobsForTest(); - await sleep(1); // necessary to make space for a draw call - - // Test simple data set -> creates main cache - - let testHandler = async e => { - // data comes in as T_A - test.equal(typeDtoA, 0, "No conversion needed to get type A."); - test.equal(typeCtoA, 0, "No conversion needed to get type A."); - - const data = await e.getData(T_A); - test.equal(data, 1, "Copy: creation of a working cache."); - e.tile.__TEST_PROCESSED = true; - - // Test value 2 since we set T_C no need to convert - await e.setData(2, T_C); - test.notOk(e.outdated(), "Event is still valid."); - }; - - viewer.addHandler('tile-invalidated', testHandler); - await viewer.world.requestInvalidate(true); - await sleep(1); // necessary to make space for internal updates - testDrawingRoutine(2); - - //test for each level only single cache was processed - const processedLevels = {}; - for (let tile of tileCache._tilesLoaded) { - const level = tile.level; - - if (tile.__TEST_PROCESSED) { - test.ok(!processedLevels[level], "Only single tile processed per level."); - processedLevels[level] = true; - delete tile.__TEST_PROCESSED; - } - - const origCache = tile.getCache(tile.originalCacheKey); - test.equal(origCache.type, T_A, "Original cache data was not affected, the drawer uses internal cache."); - test.equal(origCache.data, 0, "Original cache data was not affected, the drawer uses internal cache."); - - const cache = tile.getCache(); - test.equal(cache.type, T_C, "Main Cache Updated (suite 1)"); - test.equal(cache.data, previousTestValue, "Main Cache Updated (suite 1)"); - - const internalCache = cache.getCacheForRendering(drawer, tile); - test.equal(T_C, internalCache.type, "Conversion A->C ready, since there is no way to get to T_E."); - test.ok(internalCache.loaded, "Internal cache ready."); - } - - // Test that basic scenario with reset data false starts from the main cache data of previous round - const modificationConstant = 50; - viewer.removeHandler('tile-invalidated', testHandler); - testHandler = async e => { - const data = await e.getData(T_B); - test.equal(data, previousTestValue + 2, "C -> A -> B conversion happened."); - await e.setData(data + modificationConstant, T_B); - console.log(data + modificationConstant); - test.notOk(e.outdated(), "Event is still valid."); - }; - console.log(previousTestValue, modificationConstant) - - viewer.addHandler('tile-invalidated', testHandler); - await viewer.world.requestInvalidate(false); - await sleep(1); // necessary to make space for a draw call - // We set data as TB - there is T_C -> T_A -> T_B -> T_C conversion round - let newValue = previousTestValue + modificationConstant + 3; - testDrawingRoutine(newValue); - - newValue--; // intenrla cache performed +1 conversion, but here we have main cache with one step less - for (let tile of tileCache._tilesLoaded) { - const cache = tile.getCache(); - test.equal(cache.type, T_B, "Main Cache Updated (suite 2)."); - test.equal(cache.data, newValue, "Main Cache Updated (suite 2)."); - } - - // Now test whether data reset works, value 1 -> copy perfomed due to internal cache cration - viewer.removeHandler('tile-invalidated', testHandler); - testHandler = async e => { - const data = await e.getData(T_B); - test.equal(data, 1, "Copy: creation of a working cache."); - await e.setData(-8, T_E); - e.resetData(); - }; - viewer.addHandler('tile-invalidated', testHandler); - await viewer.world.requestInvalidate(true); - await sleep(1); // necessary to make space for a draw call - testDrawingRoutine(2); // Value +2 rendering from original data - - for (let tile of tileCache._tilesLoaded) { - const origCache = tile.getCache(tile.originalCacheKey); - test.ok(tile.getCache() === origCache, "Main cache is now original cache."); - } - - // Now force main cache creation that differs - viewer.removeHandler('tile-invalidated', testHandler); - testHandler = async e => { - await e.setData(41, T_B); - }; - viewer.addHandler('tile-invalidated', testHandler); - await viewer.world.requestInvalidate(true); - - // Now test whether data reset works, even with non-original data - viewer.removeHandler('tile-invalidated', testHandler); - testHandler = async e => { - const data = await e.getData(T_B); - test.equal(data, 42, "Copy: 41 + 1."); - await e.setData(data, T_E); - e.resetData(); - }; - viewer.addHandler('tile-invalidated', testHandler); - await viewer.world.requestInvalidate(false); - await sleep(1); // necessary to make space for a draw call - testDrawingRoutine(42); - - for (let tile of tileCache._tilesLoaded) { - const origCache = tile.getCache(tile.originalCacheKey); - test.equal(origCache.type, T_A, "Original cache data was not affected, the drawer uses main cache even after refresh."); - test.equal(origCache.data, 0, "Original cache data was not affected, the drawer uses main cache even after refresh."); - } - - test.ok(testTileCalled, "Drawer tested at least one tile."); - done(); - }); - viewer.open([ - {isTestSource: true}, - {isTestSource: true}, - ]); - }); + // QUnit.test('Tile & Invalidation API: basic conversion & preprocessing', function(test) { + // const done = test.async(); + // + // const tileCache = viewer.tileCache; + // const drawer = viewer.drawer; + // + // let testTileCalled = false; + // + // let _currentTestVal = undefined; + // let previousTestValue = undefined; + // drawer.testEvents.addHandler('test-tile', e => { + // test.ok(e.dataToDraw, "Tile data is ready to be drawn"); + // if (_currentTestVal !== undefined) { + // testTileCalled = true; + // test.equal(e.dataToDraw, _currentTestVal, "Value is correct on the drawn data."); + // } + // }); + // + // function testDrawingRoutine(value) { + // _currentTestVal = value; + // viewer.world.needsDraw(); + // viewer.world.draw(); + // previousTestValue = value; + // _currentTestVal = undefined; + // } + // + // viewer.addHandler('open', async () => { + // await viewer.waitForFinishedJobsForTest(); + // await sleep(1); // necessary to make space for a draw call + // + // // Test simple data set -> creates main cache + // + // let testHandler = async e => { + // // data comes in as T_A + // test.equal(typeDtoA, 0, "No conversion needed to get type A."); + // test.equal(typeCtoA, 0, "No conversion needed to get type A."); + // + // const data = await e.getData(T_A); + // test.equal(data, 1, "Copy: creation of a working cache."); + // e.tile.__TEST_PROCESSED = true; + // + // // Test value 2 since we set T_C no need to convert + // await e.setData(2, T_C); + // test.notOk(e.outdated(), "Event is still valid."); + // }; + // + // viewer.addHandler('tile-invalidated', testHandler); + // await viewer.world.requestInvalidate(true); + // await sleep(1); // necessary to make space for internal updates + // testDrawingRoutine(2); + // + // //test for each level only single cache was processed + // const processedLevels = {}; + // for (let tile of tileCache._tilesLoaded) { + // const level = tile.level; + // + // if (tile.__TEST_PROCESSED) { + // test.ok(!processedLevels[level], "Only single tile processed per level."); + // processedLevels[level] = true; + // delete tile.__TEST_PROCESSED; + // } + // + // const origCache = tile.getCache(tile.originalCacheKey); + // test.equal(origCache.type, T_A, "Original cache data was not affected, the drawer uses internal cache."); + // test.equal(origCache.data, 0, "Original cache data was not affected, the drawer uses internal cache."); + // + // const cache = tile.getCache(); + // test.equal(cache.type, T_C, "Main Cache Updated (suite 1)"); + // test.equal(cache.data, previousTestValue, "Main Cache Updated (suite 1)"); + // + // const internalCache = cache.getCacheForRendering(drawer, tile); + // test.equal(T_C, internalCache.type, "Conversion A->C ready, since there is no way to get to T_E."); + // test.ok(internalCache.loaded, "Internal cache ready."); + // } + // + // // Test that basic scenario with reset data false starts from the main cache data of previous round + // const modificationConstant = 50; + // viewer.removeHandler('tile-invalidated', testHandler); + // testHandler = async e => { + // const data = await e.getData(T_B); + // test.equal(data, previousTestValue + 2, "C -> A -> B conversion happened."); + // await e.setData(data + modificationConstant, T_B); + // console.log(data + modificationConstant); + // test.notOk(e.outdated(), "Event is still valid."); + // }; + // console.log(previousTestValue, modificationConstant) + // + // viewer.addHandler('tile-invalidated', testHandler); + // await viewer.world.requestInvalidate(false); + // await sleep(1); // necessary to make space for a draw call + // // We set data as TB - there is T_C -> T_A -> T_B -> T_C conversion round + // let newValue = previousTestValue + modificationConstant + 3; + // testDrawingRoutine(newValue); + // + // newValue--; // intenrla cache performed +1 conversion, but here we have main cache with one step less + // for (let tile of tileCache._tilesLoaded) { + // const cache = tile.getCache(); + // test.equal(cache.type, T_B, "Main Cache Updated (suite 2)."); + // test.equal(cache.data, newValue, "Main Cache Updated (suite 2)."); + // } + // + // // Now test whether data reset works, value 1 -> copy perfomed due to internal cache cration + // viewer.removeHandler('tile-invalidated', testHandler); + // testHandler = async e => { + // const data = await e.getData(T_B); + // test.equal(data, 1, "Copy: creation of a working cache."); + // await e.setData(-8, T_E); + // e.resetData(); + // }; + // viewer.addHandler('tile-invalidated', testHandler); + // await viewer.world.requestInvalidate(true); + // await sleep(1); // necessary to make space for a draw call + // testDrawingRoutine(2); // Value +2 rendering from original data + // + // for (let tile of tileCache._tilesLoaded) { + // const origCache = tile.getCache(tile.originalCacheKey); + // test.ok(tile.getCache() === origCache, "Main cache is now original cache."); + // } + // + // // Now force main cache creation that differs + // viewer.removeHandler('tile-invalidated', testHandler); + // testHandler = async e => { + // await e.setData(41, T_B); + // }; + // viewer.addHandler('tile-invalidated', testHandler); + // await viewer.world.requestInvalidate(true); + // + // // Now test whether data reset works, even with non-original data + // viewer.removeHandler('tile-invalidated', testHandler); + // testHandler = async e => { + // const data = await e.getData(T_B); + // test.equal(data, 42, "Copy: 41 + 1."); + // await e.setData(data, T_E); + // e.resetData(); + // }; + // viewer.addHandler('tile-invalidated', testHandler); + // await viewer.world.requestInvalidate(false); + // await sleep(1); // necessary to make space for a draw call + // testDrawingRoutine(42); + // + // for (let tile of tileCache._tilesLoaded) { + // const origCache = tile.getCache(tile.originalCacheKey); + // test.equal(origCache.type, T_A, "Original cache data was not affected, the drawer uses main cache even after refresh."); + // test.equal(origCache.data, 0, "Original cache data was not affected, the drawer uses main cache even after refresh."); + // } + // + // test.ok(testTileCalled, "Drawer tested at least one tile."); + // done(); + // }); + // viewer.open([ + // {isTestSource: true}, + // {isTestSource: true}, + // ]); + // }); //Tile API and cache interaction QUnit.test('Tile API Cache Interaction', function(test) { From cb06a5c0fb4ba4f8c487808e886e48e7bd9a2cfc Mon Sep 17 00:00:00 2001 From: Aiosa <469130@mail.muni.cz> Date: Fri, 10 Jan 2025 14:46:52 +0100 Subject: [PATCH 05/11] Debug fallback to canvas drawer, small bugfixes, add support for drawer cleanup in cache. --- src/drawerbase.js | 18 ++++++++++++++++- src/tile.js | 2 -- src/tilecache.js | 33 ++++++++++++++++++++++++++----- src/webgldrawer.js | 49 ++++++++++++++++++++++++++-------------------- 4 files changed, 73 insertions(+), 29 deletions(-) diff --git a/src/drawerbase.js b/src/drawerbase.js index c3138655..08142944 100644 --- a/src/drawerbase.js +++ b/src/drawerbase.js @@ -38,7 +38,14 @@ * @typedef BaseDrawerOptions * @memberOf OpenSeadragon * @property {boolean} [usePrivateCache=false] specify whether the drawer should use - * detached (=internal) cache object in case it has to perform type conversion + * detached (=internal) cache object in case it has to perform custom type conversion atop + * what cache performs. In that case, drawer must implement dataCreate() which gets data in one + * of formats the drawer declares as supported. This method must return object to be used during drawing. + * You should probably implement also dataFree() to provide cleanup logics. + * + * @property {boolean} [preloadCache=true] + * When dataCreate is used, it can be applied offline (asynchronously) during data processing = preloading, + * or just in time before rendering (if necessary). Preloading supports */ const OpenSeadragon = $; // (re)alias back to OpenSeadragon for JSDoc @@ -86,6 +93,7 @@ OpenSeadragon.DrawerBase = class DrawerBase{ this.container.appendChild( this.canvas ); this._checkInterfaceImplementation(); + this.setDataNeedsRefresh(); // initializes stamp } /** @@ -208,6 +216,14 @@ OpenSeadragon.DrawerBase = class DrawerBase{ $.console.error('Drawer.destroy must be implemented by child class'); } + /** + * Destroy internal cache. Should be called within destroy() when + * + */ + destroyInternalCache() { + this.viewer.tileCache.clearDrawerInternalCache(this); + } + /** * @param {TiledImage} tiledImage the tiled image that is calling the function * @returns {Boolean} Whether this drawer requires enforcing minimum tile overlap to avoid showing seams. diff --git a/src/tile.js b/src/tile.js index b04dbc61..473207af 100644 --- a/src/tile.js +++ b/src/tile.js @@ -795,8 +795,6 @@ $.Tile.prototype = { this.tiledImage = null; this._caches = {}; this._cacheSize = 0; - this.element = null; - this.imgElement = null; this.loaded = false; this.loading = false; this._cKey = this._ocKey; diff --git a/src/tilecache.js b/src/tilecache.js index 105a24b0..9578cc91 100644 --- a/src/tilecache.js +++ b/src/tilecache.js @@ -197,7 +197,7 @@ } // If we support internal cache - if (keepInternalCopy && !drawer.options.preloadCache) { + if (keepInternalCopy) { // let sync preparation handle data if (!drawer.options.preloadCache) { return this.prepareInternalCacheSync(drawer); @@ -361,15 +361,24 @@ /** * If cache ceases to be the primary one, free data + * @param {string} drawerId if undefined, all caches are freed, else only target one * @private */ - destroyInternalCache() { + destroyInternalCache(drawerId = undefined) { const internal = this[DRAWER_INTERNAL_CACHE]; if (internal) { - for (let iCache in internal) { - internal[iCache].destroy(); + if (drawerId) { + const cache = internal[drawerId]; + if (cache) { + cache.destroy(); + delete internal[drawerId]; + } + } else { + for (let iCache in internal) { + internal[iCache].destroy(); + } + delete this[DRAWER_INTERNAL_CACHE]; } - delete this[DRAWER_INTERNAL_CACHE]; } } @@ -1152,6 +1161,20 @@ this._cachesLoadedCount = 0; } + /** + * Clean up internal drawer data for a given drawer + * @param {OpenSeadragon.DrawerBase} drawer + */ + clearDrawerInternalCache(drawer) { + const drawerId = drawer.getId(); + for (let zombie in this._zombiesLoaded) { + this._zombiesLoaded[zombie].destroyInternalCache(drawerId); + } + for (let cache in this._tilesLoaded) { + this._tilesLoaded[cache].destroyInternalCache(drawerId); + } + } + /** * Returns reference to all tiles loaded by a particular * tiled image item diff --git a/src/webgldrawer.js b/src/webgldrawer.js index 7e3a5fd1..06fa12a5 100644 --- a/src/webgldrawer.js +++ b/src/webgldrawer.js @@ -107,7 +107,8 @@ get defaultOptions() { return { // use detached cache: our type conversion will not collide (and does not have to preserve CPU data ref) - usePrivateCache: true + usePrivateCache: true, + preloadCache: false, }; } @@ -168,6 +169,8 @@ this.viewer.drawer = null; } + this.destroyInternalCache(); + // set our destroyed flag to true this._destroyed = true; } @@ -238,16 +241,7 @@ this._backupCanvasDrawer = this.viewer.requestDrawer('canvas', {mainDrawer: false}); this._backupCanvasDrawer.canvas.style.setProperty('visibility', 'hidden'); this._backupCanvasDrawer.getSupportedDataFormats = () => this._supportedFormats; - this._backupCanvasDrawer.getDataToDraw = (tile) => { - const cache = tile.getCache(tile.cacheKey); - if (!cache) { - $.console.warn("Attempt to draw tile %s when not cached!", tile); - return undefined; - } - const dataCache = cache.getDataForRendering(this, tile); - // Use CPU Data for the drawer instead - return dataCache && dataCache.cpuData; - }; + this._backupCanvasDrawer.getDataToDraw = this.getDataToDraw.bind(this); } return this._backupCanvasDrawer; @@ -386,7 +380,7 @@ let numTilesToDraw = indexInDrawArray + 1; const textureInfo = this.getDataToDraw(tile); - if (textureInfo) { + if (textureInfo && textureInfo.texture) { this._getTileData(tile, tiledImage, textureInfo, overallMatrix, indexInDrawArray, texturePositionArray, textureDataArray, matrixArray, opacityArray); } else { // console.log('No tile info', tile); @@ -824,7 +818,6 @@ //bind the frame buffer to the new texture gl.bindFramebuffer(gl.FRAMEBUFFER, this._glFrameBuffer); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this._renderToTexture, 0); - } // private @@ -876,13 +869,14 @@ let texture; let position; - const data = cache.data; + let data = cache.data; if (!tiledImage.isTainted()) { if((data instanceof CanvasRenderingContext2D) && $.isCanvasTainted(data.canvas)){ tiledImage.setTainted(true); $.console.warn('WebGL cannot be used to draw this TiledImage because it has tainted data. Does crossOriginPolicy need to be set?'); this._raiseDrawerErrorEvent(tiledImage, 'Tainted data cannot be used by the WebGLDrawer. Falling back to CanvasDrawer for this TiledImage.'); + this.setDataNeedsRefresh(); } else { let sourceWidthFraction, sourceHeightFraction; if (tile.sourceBounds) { @@ -925,21 +919,34 @@ // This depends on gl.TEXTURE_2D being bound to the texture // associated with this canvas before calling this function gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, data); + // TextureInfo stored in the cache + return { + texture: texture, + position: position, + }; } catch (e){ // Todo a bit dirty re-use of the tainted flag, but makes the code more stable tiledImage.setTainted(true); $.console.error('Error uploading image data to WebGL. Falling back to canvas renderer.', e); this._raiseDrawerErrorEvent(tiledImage, 'Unknown error when uploading texture. Falling back to CanvasDrawer for this TiledImage.'); + this.setDataNeedsRefresh(); } } } - - // TextureInfo stored in the cache - return { - texture: texture, - position: position, - cpuData: data // Reference to the outer cache data, used to draw if webgl cannot be used - }; + if (data instanceof Image) { + const canvas = document.createElement( 'canvas' ); + canvas.width = data.width; + canvas.height = data.height; + const context = canvas.getContext('2d', { willReadFrequently: true }); + context.drawImage( data, 0, 0 ); + data = context; + $.console.log("FONCSCNSO"); + } + if (data instanceof CanvasRenderingContext2D) { + return data; + } + $.console.error("Unsupported data used for WebGL Drawer - probably a bug!"); + return {}; } dataFree(data) { From 226a44c4982c8d7229f4093fedaa7a07707beb41 Mon Sep 17 00:00:00 2001 From: Aiosa <469130@mail.muni.cz> Date: Fri, 10 Jan 2025 14:57:41 +0100 Subject: [PATCH 06/11] Fix docs. --- src/drawerbase.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/drawerbase.js b/src/drawerbase.js index 08142944..d7511668 100644 --- a/src/drawerbase.js +++ b/src/drawerbase.js @@ -218,7 +218,8 @@ OpenSeadragon.DrawerBase = class DrawerBase{ /** * Destroy internal cache. Should be called within destroy() when - * + * usePrivateCache is set to true. Ensures cleanup of anything created + * by dataCreate(...). */ destroyInternalCache() { this.viewer.tileCache.clearDrawerInternalCache(this); From 03b7c5b9a6190fdcdbb8e2402f576076158e4e13 Mon Sep 17 00:00:00 2001 From: Aiosa <469130@mail.muni.cz> Date: Fri, 10 Jan 2025 22:05:16 +0100 Subject: [PATCH 07/11] Update tests, fix async cache bug - first convert to supported format, then process. Record drawer ID in the internal cache type. --- src/tilecache.js | 48 ++-- test/modules/tilecache.js | 520 ++++++++++++++++++++++---------------- 2 files changed, 329 insertions(+), 239 deletions(-) diff --git a/src/tilecache.js b/src/tilecache.js index 9578cc91..12060295 100644 --- a/src/tilecache.js +++ b/src/tilecache.js @@ -225,21 +225,17 @@ prepareForRendering(drawer) { const supportedTypes = drawer.getRequiredDataFormats(); - if (drawer.options.usePrivateCache && drawer.options.preloadCache) { - return this.prepareInternalCacheAsync(drawer).then(_ => { - // if not internal copy and we have no data, or we are ready to render, exit - if (!this.loaded || supportedTypes.includes(this.type)) { - return this.await(); - } - - return this.transformTo(supportedTypes); - }); - } - + let selfPromise; if (!this.loaded || supportedTypes.includes(this.type)) { - return this.await(); + selfPromise = this.await(); + } else { + selfPromise = this.transformTo(supportedTypes); } - return this.transformTo(supportedTypes); + + if (drawer.options.usePrivateCache && drawer.options.preloadCache) { + return selfPromise.then(_ => this.prepareInternalCacheAsync(drawer)); + } + return selfPromise; } /** @@ -265,7 +261,9 @@ $.console.assert(this._tRef, "Data Create called from invalidation routine needs tile reference!"); const transformedData = drawer.dataCreate(this, this._tRef); $.console.assert(transformedData !== undefined, "[DrawerBase.dataCreate] must return a value if usePrivateCache is enabled!"); - internalCache = this[DRAWER_INTERNAL_CACHE][drawer.getId()] = new $.InternalCacheRecord(transformedData, (data) => drawer.dataFree(data)); + const drawerID = drawer.getId(); + internalCache = this[DRAWER_INTERNAL_CACHE][drawerID] = new $.InternalCacheRecord(transformedData, + drawerID, (data) => drawer.dataFree(data)); return internalCache.await(); } @@ -290,7 +288,10 @@ $.console.assert(this._tRef, "Data Create called from drawing loop needs tile reference!"); const transformedData = drawer.dataCreate(this, this._tRef); $.console.assert(transformedData !== undefined, "[DrawerBase.dataCreate] must return a value if usePrivateCache is enabled!"); - internalCache = this[DRAWER_INTERNAL_CACHE][drawer.getId()] = new $.InternalCacheRecord(transformedData, (data) => drawer.dataFree(data)); + + const drawerID = drawer.getId(); + internalCache = this[DRAWER_INTERNAL_CACHE][drawerID] = new $.InternalCacheRecord(transformedData, + drawerID, (data) => drawer.dataFree(data)); return internalCache; } @@ -676,9 +677,10 @@ * @private */ $.InternalCacheRecord = class { - constructor(data, onDestroy) { + constructor(data, type, onDestroy) { this.tstamp = $.now(); this._ondestroy = onDestroy; + this._type = type; if (data instanceof $.Promise) { this._promise = data; @@ -706,7 +708,7 @@ * @returns {string} */ get type() { - return "__internal_cache__"; + return this._type; } /** @@ -1167,11 +1169,15 @@ */ clearDrawerInternalCache(drawer) { const drawerId = drawer.getId(); - for (let zombie in this._zombiesLoaded) { - this._zombiesLoaded[zombie].destroyInternalCache(drawerId); + for (let zombie of this._zombiesLoaded) { + if (zombie) { + zombie.destroyInternalCache(drawerId); + } } - for (let cache in this._tilesLoaded) { - this._tilesLoaded[cache].destroyInternalCache(drawerId); + for (let cache of this._cachesLoaded) { + if (cache) { + cache.destroyInternalCache(drawerId); + } } } diff --git a/test/modules/tilecache.js b/test/modules/tilecache.js index 9f965866..73b06aa0 100644 --- a/test/modules/tilecache.js +++ b/test/modules/tilecache.js @@ -92,21 +92,6 @@ this.testEvents = new OpenSeadragon.EventSource(); } - getType() { - return "test-cache-drawer"; - } - - // Make test use private cache - get defaultOptions() { - return { - usePrivateCache: true - }; - } - - getSupportedDataFormats() { - return [T_C, T_E]; - } - static isSupported() { return true; } @@ -128,12 +113,16 @@ } } + dataFree(data) { + this.testEvents.raiseEvent('free-data'); + } + canRotate() { return true; } destroy() { - //noop + this.destroyInternalCache(); } setImageSmoothingEnabled(imageSmoothingEnabled){ @@ -149,6 +138,60 @@ } } + OpenSeadragon.SyncInternalCacheDrawer = class extends OpenSeadragon.TestCacheDrawer { + + getType() { + return "test-cache-drawer-sync"; + } + + getSupportedDataFormats() { + return [T_C, T_E]; + } + + // Make test use private cache + get defaultOptions() { + return { + usePrivateCache: true, + preloadCache: false, + }; + } + + dataCreate(cache, tile) { + this.testEvents.raiseEvent('create-data'); + return cache.data; + } + } + + OpenSeadragon.AsnycInternalCacheDrawer = class extends OpenSeadragon.TestCacheDrawer { + + getType() { + return "test-cache-drawer-async"; + } + + getSupportedDataFormats() { + return [T_A]; + } + + // Make test use private cache + get defaultOptions() { + return { + usePrivateCache: true, + preloadCache: true, + }; + } + + dataCreate(cache, tile) { + this.testEvents.raiseEvent('create-data'); + return cache.getDataAs(T_C, true); + } + + dataFree(data) { + super.dataFree(data); + // Be nice and truly destroy the data copy + OpenSeadragon.convertor.destroy(data, T_C); + } + } + OpenSeadragon.EmptyTestT_ATileSource = class extends OpenSeadragon.TileSource { supports( data, url ){ @@ -184,14 +227,6 @@ $('
').appendTo("#qunit-fixture"); testLog.reset(); - - viewer = OpenSeadragon({ - id: 'example', - prefixUrl: '/build/openseadragon/images/', - maxImageCacheCount: 200, //should be enough to fit test inside the cache - springStiffness: 100, // Faster animation = faster tests - drawer: 'test-cache-drawer', - }); OpenSeadragon.ImageLoader.prototype.addJob = originalJob; // Reset counters @@ -313,201 +348,234 @@ done(); }); - //Tile API and cache interaction - // QUnit.test('Tile: basic rendering & test setup', function(test) { - // const done = test.async(); - // - // const tileCache = viewer.tileCache; - // const drawer = viewer.drawer; - // - // let testTileCalled = false; - // drawer.testEvents.addHandler('test-tile', e => { - // testTileCalled = true; - // test.ok(e.dataToDraw, "Tile data is ready to be drawn"); - // }); - // - // viewer.addHandler('open', async () => { - // await viewer.waitForFinishedJobsForTest(); - // await sleep(1); // necessary to make space for a draw call - // - // test.ok(viewer.world.getItemAt(0).source instanceof OpenSeadragon.EmptyTestT_ATileSource, "Tests are done with empty test source type T_A."); - // test.ok(viewer.world.getItemAt(1).source instanceof OpenSeadragon.EmptyTestT_ATileSource, "Tests are done with empty test source type T_A."); - // test.ok(testTileCalled, "Drawer tested at least one tile."); - // - // test.ok(typeAtoB > 1, "At least one conversion was triggered."); - // test.equal(typeAtoB, typeBtoC, "A->B = B->C, since we need to move all data to T_C for the drawer."); - // - // for (let tile of tileCache._tilesLoaded) { - // const cache = tile.getCache(); - // test.equal(cache.type, T_A, "Cache data was not affected, the drawer uses internal cache."); - // - // const internalCache = cache.getCacheForRendering(drawer, tile); - // test.equal(internalCache.type, T_C, "Conversion A->C ready, since there is no way to get to T_E."); - // test.ok(internalCache.loaded, "Internal cache ready."); - // } - // - // done(); - // }); - // viewer.open([ - // {isTestSource: true}, - // {isTestSource: true}, - // ]); - // }); + // Tile API and cache interaction + QUnit.test('Tile: basic rendering & test setup (sync drawer)', function(test) { + const done = test.async(); - // QUnit.test('Tile & Invalidation API: basic conversion & preprocessing', function(test) { - // const done = test.async(); - // - // const tileCache = viewer.tileCache; - // const drawer = viewer.drawer; - // - // let testTileCalled = false; - // - // let _currentTestVal = undefined; - // let previousTestValue = undefined; - // drawer.testEvents.addHandler('test-tile', e => { - // test.ok(e.dataToDraw, "Tile data is ready to be drawn"); - // if (_currentTestVal !== undefined) { - // testTileCalled = true; - // test.equal(e.dataToDraw, _currentTestVal, "Value is correct on the drawn data."); - // } - // }); - // - // function testDrawingRoutine(value) { - // _currentTestVal = value; - // viewer.world.needsDraw(); - // viewer.world.draw(); - // previousTestValue = value; - // _currentTestVal = undefined; - // } - // - // viewer.addHandler('open', async () => { - // await viewer.waitForFinishedJobsForTest(); - // await sleep(1); // necessary to make space for a draw call - // - // // Test simple data set -> creates main cache - // - // let testHandler = async e => { - // // data comes in as T_A - // test.equal(typeDtoA, 0, "No conversion needed to get type A."); - // test.equal(typeCtoA, 0, "No conversion needed to get type A."); - // - // const data = await e.getData(T_A); - // test.equal(data, 1, "Copy: creation of a working cache."); - // e.tile.__TEST_PROCESSED = true; - // - // // Test value 2 since we set T_C no need to convert - // await e.setData(2, T_C); - // test.notOk(e.outdated(), "Event is still valid."); - // }; - // - // viewer.addHandler('tile-invalidated', testHandler); - // await viewer.world.requestInvalidate(true); - // await sleep(1); // necessary to make space for internal updates - // testDrawingRoutine(2); - // - // //test for each level only single cache was processed - // const processedLevels = {}; - // for (let tile of tileCache._tilesLoaded) { - // const level = tile.level; - // - // if (tile.__TEST_PROCESSED) { - // test.ok(!processedLevels[level], "Only single tile processed per level."); - // processedLevels[level] = true; - // delete tile.__TEST_PROCESSED; - // } - // - // const origCache = tile.getCache(tile.originalCacheKey); - // test.equal(origCache.type, T_A, "Original cache data was not affected, the drawer uses internal cache."); - // test.equal(origCache.data, 0, "Original cache data was not affected, the drawer uses internal cache."); - // - // const cache = tile.getCache(); - // test.equal(cache.type, T_C, "Main Cache Updated (suite 1)"); - // test.equal(cache.data, previousTestValue, "Main Cache Updated (suite 1)"); - // - // const internalCache = cache.getCacheForRendering(drawer, tile); - // test.equal(T_C, internalCache.type, "Conversion A->C ready, since there is no way to get to T_E."); - // test.ok(internalCache.loaded, "Internal cache ready."); - // } - // - // // Test that basic scenario with reset data false starts from the main cache data of previous round - // const modificationConstant = 50; - // viewer.removeHandler('tile-invalidated', testHandler); - // testHandler = async e => { - // const data = await e.getData(T_B); - // test.equal(data, previousTestValue + 2, "C -> A -> B conversion happened."); - // await e.setData(data + modificationConstant, T_B); - // console.log(data + modificationConstant); - // test.notOk(e.outdated(), "Event is still valid."); - // }; - // console.log(previousTestValue, modificationConstant) - // - // viewer.addHandler('tile-invalidated', testHandler); - // await viewer.world.requestInvalidate(false); - // await sleep(1); // necessary to make space for a draw call - // // We set data as TB - there is T_C -> T_A -> T_B -> T_C conversion round - // let newValue = previousTestValue + modificationConstant + 3; - // testDrawingRoutine(newValue); - // - // newValue--; // intenrla cache performed +1 conversion, but here we have main cache with one step less - // for (let tile of tileCache._tilesLoaded) { - // const cache = tile.getCache(); - // test.equal(cache.type, T_B, "Main Cache Updated (suite 2)."); - // test.equal(cache.data, newValue, "Main Cache Updated (suite 2)."); - // } - // - // // Now test whether data reset works, value 1 -> copy perfomed due to internal cache cration - // viewer.removeHandler('tile-invalidated', testHandler); - // testHandler = async e => { - // const data = await e.getData(T_B); - // test.equal(data, 1, "Copy: creation of a working cache."); - // await e.setData(-8, T_E); - // e.resetData(); - // }; - // viewer.addHandler('tile-invalidated', testHandler); - // await viewer.world.requestInvalidate(true); - // await sleep(1); // necessary to make space for a draw call - // testDrawingRoutine(2); // Value +2 rendering from original data - // - // for (let tile of tileCache._tilesLoaded) { - // const origCache = tile.getCache(tile.originalCacheKey); - // test.ok(tile.getCache() === origCache, "Main cache is now original cache."); - // } - // - // // Now force main cache creation that differs - // viewer.removeHandler('tile-invalidated', testHandler); - // testHandler = async e => { - // await e.setData(41, T_B); - // }; - // viewer.addHandler('tile-invalidated', testHandler); - // await viewer.world.requestInvalidate(true); - // - // // Now test whether data reset works, even with non-original data - // viewer.removeHandler('tile-invalidated', testHandler); - // testHandler = async e => { - // const data = await e.getData(T_B); - // test.equal(data, 42, "Copy: 41 + 1."); - // await e.setData(data, T_E); - // e.resetData(); - // }; - // viewer.addHandler('tile-invalidated', testHandler); - // await viewer.world.requestInvalidate(false); - // await sleep(1); // necessary to make space for a draw call - // testDrawingRoutine(42); - // - // for (let tile of tileCache._tilesLoaded) { - // const origCache = tile.getCache(tile.originalCacheKey); - // test.equal(origCache.type, T_A, "Original cache data was not affected, the drawer uses main cache even after refresh."); - // test.equal(origCache.data, 0, "Original cache data was not affected, the drawer uses main cache even after refresh."); - // } - // - // test.ok(testTileCalled, "Drawer tested at least one tile."); - // done(); - // }); - // viewer.open([ - // {isTestSource: true}, - // {isTestSource: true}, - // ]); - // }); + viewer = OpenSeadragon({ + id: 'example', + prefixUrl: '/build/openseadragon/images/', + maxImageCacheCount: 200, //should be enough to fit test inside the cache + springStiffness: 100, // Faster animation = faster tests + drawer: 'test-cache-drawer-sync', + }); + + const tileCache = viewer.tileCache; + const drawer = viewer.drawer; + + let testTileCalled = false; + let countFreeCalled = 0; + let countCreateCalled = 0; + drawer.testEvents.addHandler('test-tile', e => { + testTileCalled = true; + test.ok(e.dataToDraw, "Tile data is ready to be drawn"); + }); + drawer.testEvents.addHandler('create-data', e => { + countCreateCalled++; + }); + drawer.testEvents.addHandler('free-data', e => { + countFreeCalled++; + }); + + viewer.addHandler('open', async () => { + await viewer.waitForFinishedJobsForTest(); + await sleep(1); // necessary to make space for a draw call + + test.ok(viewer.world.getItemAt(0).source instanceof OpenSeadragon.EmptyTestT_ATileSource, "Tests are done with empty test source type T_A."); + test.ok(viewer.world.getItemAt(1).source instanceof OpenSeadragon.EmptyTestT_ATileSource, "Tests are done with empty test source type T_A."); + test.ok(testTileCalled, "Drawer tested at least one tile."); + + test.ok(typeAtoB > 1, "At least one conversion was triggered."); + test.equal(typeAtoB, typeBtoC, "A->B = B->C, since we need to move all data to T_C for the drawer."); + + for (let tile of tileCache._tilesLoaded) { + const cache = tile.getCache(); + test.equal(cache.type, T_C, "Cache data was affected, the drawer supports only T_C since there is no way to get to T_E."); + + const internalCache = cache.getDataForRendering(drawer, tile); + test.equal(internalCache.type, viewer.drawer.getId(), "Sync conversion routine means T_C is also internal since dataCreate only creates data. However, internal cache keeps type of the drawer ID."); + test.ok(internalCache.loaded, "Internal cache ready."); + } + + test.ok(countCreateCalled > 0, "Internal cache creation called."); + viewer.drawer.destroyInternalCache(); + test.equal(countCreateCalled, countFreeCalled, "Free called as many times as create."); + + done(); + }); + viewer.open([ + {isTestSource: true}, + {isTestSource: true}, + ]); + }); + + QUnit.test('Tile & Invalidation API: basic conversion & preprocessing', function(test) { + const done = test.async(); + + viewer = OpenSeadragon({ + id: 'example', + prefixUrl: '/build/openseadragon/images/', + maxImageCacheCount: 200, //should be enough to fit test inside the cache + springStiffness: 100, // Faster animation = faster tests + drawer: 'test-cache-drawer-async', + }); + const tileCache = viewer.tileCache; + const drawer = viewer.drawer; + + let testTileCalled = false; + + let _currentTestVal = undefined; + let previousTestValue = undefined; + drawer.testEvents.addHandler('test-tile', e => { + test.ok(e.dataToDraw, "Tile data is ready to be drawn"); + if (_currentTestVal !== undefined) { + testTileCalled = true; + test.equal(e.dataToDraw, _currentTestVal, "Value is correct on the drawn data."); + } + }); + + function testDrawingRoutine(value) { + _currentTestVal = value; + viewer.world.needsDraw(); + viewer.world.draw(); + _currentTestVal = undefined; + } + + viewer.addHandler('open', async () => { + await viewer.waitForFinishedJobsForTest(); + await sleep(1); // necessary to make space for a draw call + + // Test simple data set -> creates main cache + + let testHandler = async e => { + // data comes in as T_A + test.equal(typeDtoA, 0, "No conversion needed to get type A."); + test.equal(typeCtoA, 0, "No conversion needed to get type A."); + + const data = await e.getData(T_A); + test.equal(data, 1, "Copy: creation of a working cache."); + e.tile.__TEST_PROCESSED = true; + + // Test value 2 since we set T_C no need to convert + await e.setData(2, T_C); + test.notOk(e.outdated(), "Event is still valid."); + }; + + viewer.addHandler('tile-invalidated', testHandler); + await viewer.world.requestInvalidate(true); + + //test for each level only single cache was processed + const processedLevels = {}; + for (let tile of tileCache._tilesLoaded) { + const level = tile.level; + + if (tile.__TEST_PROCESSED) { + test.ok(!processedLevels[level], "Only single tile processed per level."); + processedLevels[level] = true; + delete tile.__TEST_PROCESSED; + } + + const origCache = tile.getCache(tile.originalCacheKey); + test.equal(origCache.type, T_A, "Original cache data was not affected, the drawer uses internal cache."); + test.equal(origCache.data, 0, "Original cache data was not affected, the drawer uses internal cache."); + + const cache = tile.getCache(); + test.equal(cache.type, T_A, "Main Cache Converted T_C -> T_A (drawer supports type A) (suite 1)"); + test.equal(cache.data, 3, "Conversion step increases plugin-stored value 2 to 3"); + + const internalCache = cache.getDataForRendering(drawer, tile); + test.equal(internalCache.type, viewer.drawer.getId(), "Internal cache has type of the drawer ID."); + test.ok(internalCache.loaded, "Internal cache ready."); + } + // Internal cache will have value 5: main cache is 3, type is T_A, + testDrawingRoutine(5); // internal cache transforms to T_C: two steps, TA->TB->TC 3+2 + + // Test that basic scenario with reset data false starts from the main cache data of previous round + const modificationConstant = 50; + viewer.removeHandler('tile-invalidated', testHandler); + testHandler = async e => { + const data = await e.getData(T_B); + test.equal(data, 4, "A -> B conversion happened, we started from value 3 in the main cache."); + await e.setData(data + modificationConstant, T_B); + test.notOk(e.outdated(), "Event is still valid."); + }; + + viewer.addHandler('tile-invalidated', testHandler); + await viewer.world.requestInvalidate(false); + + // We set data as TB - there is required T_A: T_B -> T_C -> T_A conversion round on the main cache + let newValue = modificationConstant + 4 + 2; + // and there is still requirement of T_C on internal data, +2 steps + testDrawingRoutine(newValue + 2); + + for (let tile of tileCache._tilesLoaded) { + const cache = tile.getCache(); + test.equal(cache.type, T_A, "Main Cache Updated (suite 2)."); + test.equal(cache.data, newValue, "Main Cache Updated (suite 2)."); + } + + // Now test whether data reset works, value 1 -> copy perfomed due to internal cache cration + viewer.removeHandler('tile-invalidated', testHandler); + testHandler = async e => { + const data = await e.getData(T_B); + test.equal(data, 1, "Copy: creation of a working cache."); + await e.setData(-8, T_E); + e.resetData(); + }; + viewer.addHandler('tile-invalidated', testHandler); + await viewer.world.requestInvalidate(true); + await sleep(1); // necessary to make space for a draw call + testDrawingRoutine(2); // Value +2 rendering from original data + + for (let tile of tileCache._tilesLoaded) { + const origCache = tile.getCache(tile.originalCacheKey); + test.ok(tile.getCache() === origCache, "Main cache is now original cache."); + } + + // Now force main cache creation that differs + viewer.removeHandler('tile-invalidated', testHandler); + testHandler = async e => { + await e.setData(41, T_B); + }; + viewer.addHandler('tile-invalidated', testHandler); + await viewer.world.requestInvalidate(true); + + // Now test whether data reset works, even with non-original data + viewer.removeHandler('tile-invalidated', testHandler); + testHandler = async e => { + const data = await e.getData(T_B); + test.equal(data, 44, "Copy: 41 +2 (previous request invalidate ends at T_A) + 1 (we request type B)."); + await e.setData(data, T_E); // there is no way to convert T_E -> T_A, this would throw an error + e.resetData(); // reset data will revert to original cache + }; + viewer.addHandler('tile-invalidated', testHandler); + + // The data will be 45 since no change has been made: + // last main cache set was 41 T_B, supported T_A = +2 + // and internal requirement T_C = +2 + const checkNotCalled = e => { + test.ok(false, "Create data must not be called when there is no change!"); + }; + drawer.testEvents.addHandler('create-data', checkNotCalled); + + await viewer.world.requestInvalidate(false); + testDrawingRoutine(45); + + for (let tile of tileCache._tilesLoaded) { + const origCache = tile.getCache(tile.originalCacheKey); + test.equal(origCache.type, T_A, "Original cache data was not affected, the drawer uses main cache even after refresh."); + test.equal(origCache.data, 0, "Original cache data was not affected, the drawer uses main cache even after refresh."); + } + + test.ok(testTileCalled, "Drawer tested at least one tile."); + viewer.destroy(); + done(); + }); + viewer.open([ + {isTestSource: true}, + {isTestSource: true}, + ]); + }); //Tile API and cache interaction QUnit.test('Tile API Cache Interaction', function(test) { @@ -632,6 +700,14 @@ QUnit.test('Zombie Cache', function(test) { const done = test.async(); + viewer = OpenSeadragon({ + id: 'example', + prefixUrl: '/build/openseadragon/images/', + maxImageCacheCount: 200, //should be enough to fit test inside the cache + springStiffness: 100, // Faster animation = faster tests + drawer: 'test-cache-drawer-sync', + }); + //test jobs by coverage: fail if cached coverage not fully re-stored without jobs let jobCounter = 0, coverage = undefined; OpenSeadragon.ImageLoader.prototype.addJob = function (options) { @@ -711,6 +787,14 @@ QUnit.test('Zombie Cache Replace Item', function(test) { const done = test.async(); + viewer = OpenSeadragon({ + id: 'example', + prefixUrl: '/build/openseadragon/images/', + maxImageCacheCount: 200, //should be enough to fit test inside the cache + springStiffness: 100, // Faster animation = faster tests + drawer: 'test-cache-drawer-sync', + }); + let jobCounter = 0, coverage = undefined; OpenSeadragon.ImageLoader.prototype.addJob = function (options) { jobCounter++; From 1286fa454972a39811a85378cd1017d53c98e4eb Mon Sep 17 00:00:00 2001 From: Aiosa <469130@mail.muni.cz> Date: Fri, 10 Jan 2025 22:13:16 +0100 Subject: [PATCH 08/11] Test drawers collide with drawer render testing - remove after cache tests. --- test/modules/tilecache.js | 276 +++++++++++++++++++------------------- 1 file changed, 141 insertions(+), 135 deletions(-) diff --git a/test/modules/tilecache.js b/test/modules/tilecache.js index 73b06aa0..6bb481cd 100644 --- a/test/modules/tilecache.js +++ b/test/modules/tilecache.js @@ -86,141 +86,6 @@ destroyE++; }); - OpenSeadragon.TestCacheDrawer = class extends OpenSeadragon.DrawerBase { - constructor(opts) { - super(opts); - this.testEvents = new OpenSeadragon.EventSource(); - } - - static isSupported() { - return true; - } - - _createDrawingElement() { - return document.createElement("div"); - } - - draw(tiledImages) { - for (let image of tiledImages) { - const tilesDoDraw = image.getTilesToDraw().map(info => info.tile); - for (let tile of tilesDoDraw) { - const data = this.getDataToDraw(tile); - this.testEvents.raiseEvent('test-tile', { - tile: tile, - dataToDraw: data, - }); - } - } - } - - dataFree(data) { - this.testEvents.raiseEvent('free-data'); - } - - canRotate() { - return true; - } - - destroy() { - this.destroyInternalCache(); - } - - setImageSmoothingEnabled(imageSmoothingEnabled){ - //noop - } - - drawDebuggingRect(rect) { - //noop - } - - clear(){ - //noop - } - } - - OpenSeadragon.SyncInternalCacheDrawer = class extends OpenSeadragon.TestCacheDrawer { - - getType() { - return "test-cache-drawer-sync"; - } - - getSupportedDataFormats() { - return [T_C, T_E]; - } - - // Make test use private cache - get defaultOptions() { - return { - usePrivateCache: true, - preloadCache: false, - }; - } - - dataCreate(cache, tile) { - this.testEvents.raiseEvent('create-data'); - return cache.data; - } - } - - OpenSeadragon.AsnycInternalCacheDrawer = class extends OpenSeadragon.TestCacheDrawer { - - getType() { - return "test-cache-drawer-async"; - } - - getSupportedDataFormats() { - return [T_A]; - } - - // Make test use private cache - get defaultOptions() { - return { - usePrivateCache: true, - preloadCache: true, - }; - } - - dataCreate(cache, tile) { - this.testEvents.raiseEvent('create-data'); - return cache.getDataAs(T_C, true); - } - - dataFree(data) { - super.dataFree(data); - // Be nice and truly destroy the data copy - OpenSeadragon.convertor.destroy(data, T_C); - } - } - - OpenSeadragon.EmptyTestT_ATileSource = class extends OpenSeadragon.TileSource { - - supports( data, url ){ - return data && data.isTestSource; - } - - configure( data, url, postData ){ - return { - width: 512, /* width *required */ - height: 512, /* height *required */ - tileSize: 128, /* tileSize *required */ - tileOverlap: 0, /* tileOverlap *required */ - minLevel: 0, /* minLevel */ - maxLevel: 3, /* maxLevel */ - tilesUrl: "", /* tilesUrl */ - fileFormat: "", /* fileFormat */ - displayRects: null /* displayRects */ - } - } - - getTileUrl(level, x, y) { - return String(level); //treat each tile on level same to introduce cache overlaps - } - - downloadTileStart(context) { - context.finish(0, null, T_A); - } - } - // ---------- QUnit.module('TileCache', { beforeEach: function () { @@ -233,12 +98,153 @@ typeAtoB = 0, typeBtoC = 0, typeCtoA = 0, typeDtoA = 0, typeCtoE = 0; copyA = 0, copyB = 0, copyC = 0, copyD = 0, copyE = 0; destroyA = 0, destroyB = 0, destroyC = 0, destroyD = 0, destroyE = 0; + + OpenSeadragon.TestCacheDrawer = class extends OpenSeadragon.DrawerBase { + constructor(opts) { + super(opts); + this.testEvents = new OpenSeadragon.EventSource(); + } + + static isSupported() { + return true; + } + + _createDrawingElement() { + return document.createElement("div"); + } + + draw(tiledImages) { + for (let image of tiledImages) { + const tilesDoDraw = image.getTilesToDraw().map(info => info.tile); + for (let tile of tilesDoDraw) { + const data = this.getDataToDraw(tile); + this.testEvents.raiseEvent('test-tile', { + tile: tile, + dataToDraw: data, + }); + } + } + } + + dataFree(data) { + this.testEvents.raiseEvent('free-data'); + } + + canRotate() { + return true; + } + + destroy() { + this.destroyInternalCache(); + } + + setImageSmoothingEnabled(imageSmoothingEnabled){ + //noop + } + + drawDebuggingRect(rect) { + //noop + } + + clear(){ + //noop + } + } + + OpenSeadragon.SyncInternalCacheDrawer = class extends OpenSeadragon.TestCacheDrawer { + + getType() { + return "test-cache-drawer-sync"; + } + + getSupportedDataFormats() { + return [T_C, T_E]; + } + + // Make test use private cache + get defaultOptions() { + return { + usePrivateCache: true, + preloadCache: false, + }; + } + + dataCreate(cache, tile) { + this.testEvents.raiseEvent('create-data'); + return cache.data; + } + } + + OpenSeadragon.AsnycInternalCacheDrawer = class extends OpenSeadragon.TestCacheDrawer { + + getType() { + return "test-cache-drawer-async"; + } + + getSupportedDataFormats() { + return [T_A]; + } + + // Make test use private cache + get defaultOptions() { + return { + usePrivateCache: true, + preloadCache: true, + }; + } + + dataCreate(cache, tile) { + this.testEvents.raiseEvent('create-data'); + return cache.getDataAs(T_C, true); + } + + dataFree(data) { + super.dataFree(data); + // Be nice and truly destroy the data copy + OpenSeadragon.convertor.destroy(data, T_C); + } + } + + OpenSeadragon.EmptyTestT_ATileSource = class extends OpenSeadragon.TileSource { + + supports( data, url ){ + return data && data.isTestSource; + } + + configure( data, url, postData ){ + return { + width: 512, /* width *required */ + height: 512, /* height *required */ + tileSize: 128, /* tileSize *required */ + tileOverlap: 0, /* tileOverlap *required */ + minLevel: 0, /* minLevel */ + maxLevel: 3, /* maxLevel */ + tilesUrl: "", /* tilesUrl */ + fileFormat: "", /* fileFormat */ + displayRects: null /* displayRects */ + } + } + + getTileUrl(level, x, y) { + return String(level); //treat each tile on level same to introduce cache overlaps + } + + downloadTileStart(context) { + context.finish(0, null, T_A); + } + } }, afterEach: function () { if (viewer && viewer.close) { viewer.close(); } + // Some tests test all drawers - remove test drawers to avoid collision with other tests + OpenSeadragon.EmptyTestT_ATileSource = null; + OpenSeadragon.AsnycInternalCacheDrawer = null; + OpenSeadragon.SyncInternalCacheDrawer = null; + OpenSeadragon.TestCacheDrawer = null; + viewer = null; } }); From 6e81bedc39388571a8df26a0b49313a5d62f34f3 Mon Sep 17 00:00:00 2001 From: Aiosa <469130@mail.muni.cz> Date: Tue, 14 Jan 2025 12:14:28 +0100 Subject: [PATCH 09/11] Improve docs, change order of calls in prepareForRendering for better code stability. --- src/tilecache.js | 60 ++++++++++++++++++++++++++++++++++++---------- src/webgldrawer.js | 1 - 2 files changed, 47 insertions(+), 14 deletions(-) diff --git a/src/tilecache.js b/src/tilecache.js index 12060295..00410a35 100644 --- a/src/tilecache.js +++ b/src/tilecache.js @@ -162,9 +162,12 @@ } /** - * Access of the data by drawers, synchronous function. Should always access a valid main cache, e.g. - * cache swap performed on working cache (replaceCache()) must be synchronous such that cache is always - * ready to render, and swaps atomically between render calls. + * Access of the data by drawers, synchronous function. Should always access a valid main cache. + * This is ensured by invalidation routine that executes data modification on a copy record, and + * then synchronously swaps records (main caches) to the new data between render calls. + * + * If a drawer decides to have internal cache with synchronous behavior, it is (if necessary) + * performed during this phase. * * @param {OpenSeadragon.DrawerBase} drawer drawer reference which requests the data: the drawer * defines the supported formats this cache should return **synchronously** @@ -179,6 +182,7 @@ getDataForRendering(drawer, tileToDraw) { const keepInternalCopy = drawer.options.usePrivateCache; + // Test cache state if (!this.loaded) { $.console.error("Attempt to draw tile when not loaded main cache!"); return undefined; @@ -189,6 +193,9 @@ return undefined; } + // Ensure cache in a format suitable for the current drawer. If not it is an error, prepareForRendering + // should be called at the end of invalidation routine instead. Since the processing is async, we are + // unable to provide the rendering data immediatelly - return. const supportedTypes = drawer.getSupportedDataFormats(); if (!supportedTypes.includes(this.type)) { $.console.error("Attempt to draw tile with unsupported target drawer type!"); @@ -198,11 +205,11 @@ // If we support internal cache if (keepInternalCopy) { - // let sync preparation handle data + // let sync preparation handle data if no preloading desired if (!drawer.options.preloadCache) { return this.prepareInternalCacheSync(drawer); } - // or check that it was properly initiated before returning + // or check internal cache state before returning const internalCache = this._getInternalCacheRef(drawer); if (!internalCache || !internalCache.loaded) { $.console.error("Attempt to draw tile with internal cache non-ready state!"); @@ -211,12 +218,16 @@ return internalCache; } - // If no internal cache support, we are ready - just return self reference + // else just return self reference return this; } /** - * Should not be called if cache type is already among supported types + * Preparation for rendering ensures the CacheRecord is in a format supported by the current + * drawer. Furthermore, if internal cache is to be used by a drawer with preloading enabled, + * it happens in this step. + * + * Note: Should not be called if cache type is already among supported types. * @private * @param {OpenSeadragon.DrawerBase} drawer * @return {OpenSeadragon.Promise<*>} reference to the data, @@ -225,13 +236,20 @@ prepareForRendering(drawer) { const supportedTypes = drawer.getRequiredDataFormats(); - let selfPromise; - if (!this.loaded || supportedTypes.includes(this.type)) { - selfPromise = this.await(); - } else { - selfPromise = this.transformTo(supportedTypes); + // If not loaded, await until ready and try again + if (!this.loaded) { + return this.await().then(_ => this.prepareForRendering(drawer)); } + let selfPromise; + // If not in one of required types, transform + if (!supportedTypes.includes(this.type)) { + selfPromise = this.transformTo(supportedTypes); + } else { + selfPromise = this.await(); + } + + // If internal cache wanted and preloading enabled, convert now if (drawer.options.usePrivateCache && drawer.options.preloadCache) { return selfPromise.then(_ => this.prepareInternalCacheAsync(drawer)); } @@ -239,6 +257,8 @@ } /** + * Internal cache is defined by a Drawer. Async preparation happens as the last step in the + * invalidation routine. * Must not be called if drawer.options.usePrivateCache == false. Called inside prepareForRenderine * by cache itself if preloadCache == true (supports async behavior). * @@ -268,6 +288,7 @@ } /** + * Internal cache is defined by a Drawer. Sync preparation happens directly before rendering. * Must not be called if drawer.options.usePrivateCache == false. Called inside getDataForRendering * by cache itself if preloadCache == false (without support for async behavior). * @private @@ -295,10 +316,17 @@ return internalCache; } + /** + * Get an internal cache reference for given drawer + * @param {OpenSeadragon.DrawerBase} drawer + * @return {OpenSeadragon.InternalCacheRecord|undefined} + * @private + */ _getInternalCacheRef(drawer) { const options = drawer.options; if (!options.usePrivateCache) { - return $.Promise.reject("[CacheRecord.prepareInternalCacheSync] must not be called when usePrivateCache is false."); + $.console.error("[CacheRecord.prepareInternalCacheSync] must not be called when usePrivateCache is false."); + return undefined; } // we can get here only if we want to render incompatible type @@ -309,6 +337,12 @@ return internalCache[drawer.getId()]; } + /** + * @param {OpenSeadragon.InternalCacheRecord} internalCache + * @param {OpenSeadragon.DrawerBase} drawer + * @return {boolean} false if the internal cache is outdated + * @private + */ _checkInternalCacheUpToDate(internalCache, drawer) { // We respect existing records, unless they are outdated. Invalidation routine by its nature // destroys internal cache, therefore we do not need to check if internal cache is consistent with its parent. diff --git a/src/webgldrawer.js b/src/webgldrawer.js index 06fa12a5..3dfff944 100644 --- a/src/webgldrawer.js +++ b/src/webgldrawer.js @@ -940,7 +940,6 @@ const context = canvas.getContext('2d', { willReadFrequently: true }); context.drawImage( data, 0, 0 ); data = context; - $.console.log("FONCSCNSO"); } if (data instanceof CanvasRenderingContext2D) { return data; From 71c3c8437a6afc53e60937ca2e3e11d57dc50cf9 Mon Sep 17 00:00:00 2001 From: Aiosa <469130@mail.muni.cz> Date: Tue, 14 Jan 2025 12:20:39 +0100 Subject: [PATCH 10/11] Rename new drawer base api to reflect that it deals with internal cache. --- src/drawerbase.js | 18 +++++++++--------- src/tilecache.js | 12 ++++++------ src/webgldrawer.js | 10 +++++----- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/drawerbase.js b/src/drawerbase.js index d7511668..01646c90 100644 --- a/src/drawerbase.js +++ b/src/drawerbase.js @@ -39,12 +39,12 @@ * @memberOf OpenSeadragon * @property {boolean} [usePrivateCache=false] specify whether the drawer should use * detached (=internal) cache object in case it has to perform custom type conversion atop - * what cache performs. In that case, drawer must implement dataCreate() which gets data in one + * what cache performs. In that case, drawer must implement internalCacheCreate() which gets data in one * of formats the drawer declares as supported. This method must return object to be used during drawing. - * You should probably implement also dataFree() to provide cleanup logics. + * You should probably implement also internalCacheFree() to provide cleanup logics. * * @property {boolean} [preloadCache=true] - * When dataCreate is used, it can be applied offline (asynchronously) during data processing = preloading, + * When internalCacheCreate is used, it can be applied offline (asynchronously) during data processing = preloading, * or just in time before rendering (if necessary). Preloading supports */ @@ -93,7 +93,7 @@ OpenSeadragon.DrawerBase = class DrawerBase{ this.container.appendChild( this.canvas ); this._checkInterfaceImplementation(); - this.setDataNeedsRefresh(); // initializes stamp + this.setInternalCacheNeedsRefresh(); // initializes stamp } /** @@ -219,7 +219,7 @@ OpenSeadragon.DrawerBase = class DrawerBase{ /** * Destroy internal cache. Should be called within destroy() when * usePrivateCache is set to true. Ensures cleanup of anything created - * by dataCreate(...). + * by internalCacheCreate(...). */ destroyInternalCache() { this.viewer.tileCache.clearDrawerInternalCache(this); @@ -264,14 +264,14 @@ OpenSeadragon.DrawerBase = class DrawerBase{ * @param {OpenSeadragon.Tile} tile * @return any */ - dataCreate(cache, tile) {} + internalCacheCreate(cache, tile) {} /** * It is possible to perform any necessary cleanup on internal cache, necessary if you * need to clean up some memory (e.g. destroy canvas by setting with & height to 0). - * @param {*} data object returned by dataCreate(...) + * @param {*} data object returned by internalCacheCreate(...) */ - dataFree(data) {} + internalCacheFree(data) {} /** * Call to invalidate internal cache. It will be rebuilt. With synchronous converions, @@ -279,7 +279,7 @@ OpenSeadragon.DrawerBase = class DrawerBase{ * routine happens, e.g. you should call also requestInvalidate() if you need to happen * it as soon as possible. */ - setDataNeedsRefresh() { + setInternalCacheNeedsRefresh() { this._dataNeedsRefresh = $.now(); } diff --git a/src/tilecache.js b/src/tilecache.js index 00410a35..2f8b9335 100644 --- a/src/tilecache.js +++ b/src/tilecache.js @@ -279,11 +279,11 @@ } $.console.assert(this._tRef, "Data Create called from invalidation routine needs tile reference!"); - const transformedData = drawer.dataCreate(this, this._tRef); - $.console.assert(transformedData !== undefined, "[DrawerBase.dataCreate] must return a value if usePrivateCache is enabled!"); + const transformedData = drawer.internalCacheCreate(this, this._tRef); + $.console.assert(transformedData !== undefined, "[DrawerBase.internalCacheCreate] must return a value if usePrivateCache is enabled!"); const drawerID = drawer.getId(); internalCache = this[DRAWER_INTERNAL_CACHE][drawerID] = new $.InternalCacheRecord(transformedData, - drawerID, (data) => drawer.dataFree(data)); + drawerID, (data) => drawer.internalCacheFree(data)); return internalCache.await(); } @@ -307,12 +307,12 @@ } $.console.assert(this._tRef, "Data Create called from drawing loop needs tile reference!"); - const transformedData = drawer.dataCreate(this, this._tRef); - $.console.assert(transformedData !== undefined, "[DrawerBase.dataCreate] must return a value if usePrivateCache is enabled!"); + const transformedData = drawer.internalCacheCreate(this, this._tRef); + $.console.assert(transformedData !== undefined, "[DrawerBase.internalCacheCreate] must return a value if usePrivateCache is enabled!"); const drawerID = drawer.getId(); internalCache = this[DRAWER_INTERNAL_CACHE][drawerID] = new $.InternalCacheRecord(transformedData, - drawerID, (data) => drawer.dataFree(data)); + drawerID, (data) => drawer.internalCacheFree(data)); return internalCache; } diff --git a/src/webgldrawer.js b/src/webgldrawer.js index 3dfff944..98769180 100644 --- a/src/webgldrawer.js +++ b/src/webgldrawer.js @@ -479,7 +479,7 @@ setImageSmoothingEnabled(enabled){ if( this._imageSmoothingEnabled !== enabled ){ this._imageSmoothingEnabled = enabled; - this.setDataNeedsRefresh(); + this.setInternalCacheNeedsRefresh(); this.viewer.forceRedraw(); } } @@ -863,7 +863,7 @@ this.viewer.addHandler("resize", this._resizeHandler); } - dataCreate(cache, tile) { + internalCacheCreate(cache, tile) { let tiledImage = tile.tiledImage; let gl = this._gl; let texture; @@ -876,7 +876,7 @@ tiledImage.setTainted(true); $.console.warn('WebGL cannot be used to draw this TiledImage because it has tainted data. Does crossOriginPolicy need to be set?'); this._raiseDrawerErrorEvent(tiledImage, 'Tainted data cannot be used by the WebGLDrawer. Falling back to CanvasDrawer for this TiledImage.'); - this.setDataNeedsRefresh(); + this.setInternalCacheNeedsRefresh(); } else { let sourceWidthFraction, sourceHeightFraction; if (tile.sourceBounds) { @@ -929,7 +929,7 @@ tiledImage.setTainted(true); $.console.error('Error uploading image data to WebGL. Falling back to canvas renderer.', e); this._raiseDrawerErrorEvent(tiledImage, 'Unknown error when uploading texture. Falling back to CanvasDrawer for this TiledImage.'); - this.setDataNeedsRefresh(); + this.setInternalCacheNeedsRefresh(); } } } @@ -948,7 +948,7 @@ return {}; } - dataFree(data) { + internalCacheFree(data) { if (data && data.texture) { this._gl.deleteTexture(data.texture); data.texture = null; From 994f0e25a4439273646ca86c0ad37f1754e58a83 Mon Sep 17 00:00:00 2001 From: Aiosa <469130@mail.muni.cz> Date: Tue, 14 Jan 2025 12:26:28 +0100 Subject: [PATCH 11/11] Fix renaming also in tests. --- test/modules/tilecache.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/modules/tilecache.js b/test/modules/tilecache.js index 6bb481cd..483d3cac 100644 --- a/test/modules/tilecache.js +++ b/test/modules/tilecache.js @@ -126,7 +126,7 @@ } } - dataFree(data) { + internalCacheFree(data) { this.testEvents.raiseEvent('free-data'); } @@ -169,7 +169,7 @@ }; } - dataCreate(cache, tile) { + internalCacheCreate(cache, tile) { this.testEvents.raiseEvent('create-data'); return cache.data; } @@ -193,13 +193,13 @@ }; } - dataCreate(cache, tile) { + internalCacheCreate(cache, tile) { this.testEvents.raiseEvent('create-data'); return cache.getDataAs(T_C, true); } - dataFree(data) { - super.dataFree(data); + internalCacheFree(data) { + super.internalCacheFree(data); // Be nice and truly destroy the data copy OpenSeadragon.convertor.destroy(data, T_C); }