From 362fcb7009af8e0d46688438dfaae9346aafb687 Mon Sep 17 00:00:00 2001 From: Jonathan Chemla Date: Wed, 13 Aug 2025 11:26:27 +0200 Subject: [PATCH 1/4] WIP per-tile fade on new tileset loaded --- debug/test-rastertile-fade.html | 100 +++++++++++++++++++++++++++ src/render/draw_raster.ts | 19 ++++- src/render/program/raster_program.ts | 39 ++++------- src/shaders/raster.fragment.glsl | 17 +++++ src/source/raster_tile_source.ts | 43 ++++++++++-- src/source/tile.ts | 4 ++ 6 files changed, 188 insertions(+), 34 deletions(-) create mode 100644 debug/test-rastertile-fade.html diff --git a/debug/test-rastertile-fade.html b/debug/test-rastertile-fade.html new file mode 100644 index 00000000000..c7b487ae27e --- /dev/null +++ b/debug/test-rastertile-fade.html @@ -0,0 +1,100 @@ + + + + Mapbox-Raster-Reprojection + + + + + + + + + + + +
+ + + + + + + + diff --git a/src/render/draw_raster.ts b/src/render/draw_raster.ts index c4b35983a52..d1760d2fce7 100644 --- a/src/render/draw_raster.ts +++ b/src/render/draw_raster.ts @@ -172,6 +172,22 @@ function drawRaster(painter: Painter, sourceCache: SourceCache, layer: RasterSty context.activeTexture.set(gl.TEXTURE0); texture.bind(textureFilter, gl.CLAMP_TO_EDGE); + // ICONEM + const elapsed = performance.now() - (tile.fadeStartTime || 0); + const fadeDuration = 500; // ms + const fadeMix = Math.min(elapsed / fadeDuration, 1.0); + if (fadeMix >= 1.0) { + tile.previousTexture = null; + } + if (tile.previousTexture) { + + context.activeTexture.set(gl.TEXTURE2); + tile.previousTexture.bind(textureFilter, gl.CLAMP_TO_EDGE); + } else { + context.activeTexture.set(gl.TEXTURE2); + texture.bind(textureFilter, gl.CLAMP_TO_EDGE); // same as new + } + context.activeTexture.set(gl.TEXTURE1); if (parentTile) { @@ -248,7 +264,8 @@ function drawRaster(painter: Painter, sourceCache: SourceCache, layer: RasterSty rasterConfig.range, tileSize, buffer, - emissiveStrength + emissiveStrength, + fadeMix ); const affectedByFog = painter.isTileAffectedByFog(coord); diff --git a/src/render/program/raster_program.ts b/src/render/program/raster_program.ts index eed4281f0bc..b207303fbe1 100644 --- a/src/render/program/raster_program.ts +++ b/src/render/program/raster_program.ts @@ -43,6 +43,8 @@ export type RasterUniformsType = { ['u_texture_offset']: Uniform2f; ['u_texture_res']: Uniform2f; ['u_emissive_strength']: Uniform1f; + ['u_fade_mix']: Uniform1f; + ['u_previous_tile']: Uniform1i; }; export type RasterDefinesType = 'RASTER_COLOR' | 'RENDER_CUTOFF' | 'RASTER_ARRAY' | 'RASTER_ARRAY_LINEAR'; @@ -74,34 +76,16 @@ const rasterUniforms = (context: Context): RasterUniformsType => ({ 'u_color_ramp': new Uniform1i(context), 'u_texture_offset': new Uniform2f(context), 'u_texture_res': new Uniform2f(context), - 'u_emissive_strength': new Uniform1f(context) + 'u_emissive_strength': new Uniform1f(context), + 'u_fade_mix': new Uniform1f(context), + 'u_previous_tile': new Uniform1i(context), }); const rasterUniformValues = ( - matrix: Float32Array, - normalizeMatrix: Float32Array, - globeMatrix: Float32Array, - mercMatrix: Float32Array, - gridMatrix: Float32Array, - parentTL: [number, number], - zoomTransition: number, - mercatorCenter: [number, number], - cutoffParams: [number, number, number, number], - parentScaleBy: number, - fade: { - mix: number; - opacity: number; - }, - layer: RasterStyleLayer, - perspectiveTransform: [number, number], - elevation: number, - colorRampUnit: number, - colorMix: [number, number, number, number], - colorOffset: number, - colorRange: [number, number], - tileSize: number, - buffer: number, - emissiveStrength: number, +matrix: Float32Array, normalizeMatrix: Float32Array, globeMatrix: Float32Array, mercMatrix: Float32Array, gridMatrix: Float32Array, parentTL: [number, number], zoomTransition: number, mercatorCenter: [number, number], cutoffParams: [number, number, number, number], parentScaleBy: number, fade: { + mix: number; + opacity: number; +}, layer: RasterStyleLayer, perspectiveTransform: [number, number], elevation: number, colorRampUnit: number, colorMix: [number, number, number, number], colorOffset: number, colorRange: [number, number], tileSize: number, buffer: number, emissiveStrength: number, fadeMix: number, ): UniformValues => ({ 'u_matrix': matrix, 'u_normalize_matrix': normalizeMatrix, @@ -136,7 +120,9 @@ const rasterUniformValues = ( tileSize / (tileSize + 2 * buffer) ], 'u_texture_res': [tileSize + 2 * buffer, tileSize + 2 * buffer], - 'u_emissive_strength': emissiveStrength + 'u_emissive_strength': emissiveStrength, + 'u_fade_mix': fadeMix, + 'u_previous_tile': 2, }); const rasterPoleUniformValues = ( @@ -178,6 +164,7 @@ const rasterPoleUniformValues = ( 1, 0, emissiveStrength, + 1 )); function spinWeights(angle: number): [number, number, number] { diff --git a/src/shaders/raster.fragment.glsl b/src/shaders/raster.fragment.glsl index 16a6e1cc1b0..2ee00f6e026 100644 --- a/src/shaders/raster.fragment.glsl +++ b/src/shaders/raster.fragment.glsl @@ -37,6 +37,10 @@ uniform highp float u_colorization_offset; uniform vec2 u_texture_res; #endif +// ICONEM +uniform sampler2D u_previous_tile; +uniform float u_fade_mix; + void main() { vec4 color0, color1, color; @@ -64,6 +68,10 @@ void main() { if (value.y > 0.0) value.x /= value.y; #else color = mix(texture(u_image0, v_pos0), texture(u_image1, v_pos1), u_fade_t); + + // ICONEM + color = mix(texture(u_previous_tile, v_pos0), color, u_fade_mix); + value = vec2(u_colorization_offset + dot(color.rgb, u_colorization_mix.rgb), color.a); #endif @@ -82,6 +90,15 @@ void main() { if (color0.a > 0.0) color0.rgb /= color0.a; if (color1.a > 0.0) color1.rgb /= color1.a; color = mix(color0, color1, u_fade_t); + + // ICONEM + vec4 previousColor = texture(u_previous_tile, v_pos0); + color = mix(previousColor, color, u_fade_mix); + // float c = 1. - u_fade_mix; + // color = vec4(c, c, c, 1.); + // color = previousColor; + // ICONEM + #endif color.a *= u_opacity; diff --git a/src/source/raster_tile_source.ts b/src/source/raster_tile_source.ts index 8ed50a481d4..c41ffa07824 100644 --- a/src/source/raster_tile_source.ts +++ b/src/source/raster_tile_source.ts @@ -223,6 +223,20 @@ class RasterTileSource extends Evented implements IS if (!data) return callback(null); if (this.map._refreshExpiredTiles) tile.setExpiryData({cacheControl, expires}); + + // ICONEM + if (tile.texture && tile.texture instanceof Texture) { + tile.previousTexture = tile.texture; + tile.fadeStartTime = performance.now(); + tile.isFading = true; + } + + tile.setTexture(data, this.map.painter); + + // ICONEM + tile.previousTexture = tile.texture || null; + const textureStartTime = Date.now(); + tile.fadeEndTime = textureStartTime + 500; // 300ms fade tile.setTexture(data, this.map.painter); tile.state = 'loaded'; @@ -242,18 +256,33 @@ class RasterTileSource extends Evented implements IS unloadTile(tile: Tile, callback?: Callback) { // Cache the tile texture to avoid re-allocating Textures if they'll just be reloaded if (tile.texture && tile.texture instanceof Texture) { - // Clean everything else up owned by the tile, but preserve the texture. - // Destroy first to prevent racing with the texture cache being popped. - tile.destroy(true); - - // Save the texture to the cache - if (tile.texture && tile.texture instanceof Texture) { - this.map.painter.saveTileTexture(tile.texture); + // // Clean everything else up owned by the tile, but preserve the texture. + // // Destroy first to prevent racing with the texture cache being popped. + // tile.destroy(true); + + // // Save the texture to the cache + // if (tile.texture && tile.texture instanceof Texture) { + // this.map.painter.saveTileTexture(tile.texture); + // } + + // ICONEM + // Preserve as previous texture if fading + if (tile.fadeEndTime && Date.now() < tile.fadeEndTime) { + tile.previousTexture = tile.texture; + } else { + tile.texture.destroy(); } } else { tile.destroy(); } + // ICONEM + if (tile.previousTexture && tile.previousTexture instanceof Texture) { + tile.previousTexture.destroy(); + } + tile.previousTexture = null; + tile.fadeEndTime = undefined; + if (callback) callback(); } diff --git a/src/source/tile.ts b/src/source/tile.ts index 6ebd69b2979..d178621194c 100644 --- a/src/source/tile.ts +++ b/src/source/tile.ts @@ -185,6 +185,10 @@ class Tile { worldview: string | undefined; + // ICONEM + fadeStartTime: number | undefined; + previousTexture: Texture | null | undefined | UserManagedTexture; + /** * @param {OverscaledTileID} tileID * @param size From e430df7d6f991a385f4825833f554de07d7dd9a3 Mon Sep 17 00:00:00 2001 From: Jonathan Chemla Date: Wed, 13 Aug 2025 12:36:13 +0200 Subject: [PATCH 2/4] Use perTileFadeEndTime and u_per_tile_fade_mix Reworked the implementation to look more like the existing parent/child fading procedure, so passing `u_per_tile_fade_mix / perTileFadeMix` as a uniform computing and storing per-tile the `perTileFadeEndTime` instead of start --- src/render/draw_raster.ts | 28 +++++++++++++++++----------- src/render/program/raster_program.ts | 13 +++++++------ src/shaders/raster.fragment.glsl | 8 ++++---- src/source/raster_tile_source.ts | 22 +++++++++++----------- src/source/tile.ts | 2 +- 5 files changed, 40 insertions(+), 33 deletions(-) diff --git a/src/render/draw_raster.ts b/src/render/draw_raster.ts index d1760d2fce7..f7a5f2bc06a 100644 --- a/src/render/draw_raster.ts +++ b/src/render/draw_raster.ts @@ -23,6 +23,7 @@ import {mercatorXfromLng, mercatorYfromLat} from '../geo/mercator_coordinate'; import {COLOR_MIX_FACTOR} from '../style/style_layer/raster_style_layer'; import RasterArrayTile from '../source/raster_array_tile'; import RasterArrayTileSource from '../source/raster_array_tile_source'; +import browser from '../util/browser'; import type Transform from '../geo/transform'; import type {OverscaledTileID} from '../source/tile_id'; @@ -40,6 +41,7 @@ import type {CrossTileID, VariableOffset} from '../symbol/placement'; export default drawRaster; const RASTER_COLOR_TEXTURE_UNIT = 2; +const PREVIOUS_TILE_TEXTURE_UNIT = 3; type RasterConfig = { defines: DynamicDefinesType[]; @@ -173,20 +175,23 @@ function drawRaster(painter: Painter, sourceCache: SourceCache, layer: RasterSty texture.bind(textureFilter, gl.CLAMP_TO_EDGE); // ICONEM - const elapsed = performance.now() - (tile.fadeStartTime || 0); - const fadeDuration = 500; // ms - const fadeMix = Math.min(elapsed / fadeDuration, 1.0); - if (fadeMix >= 1.0) { + const perTileFadeMix = (browser.now() - tile.timeAdded) / rasterFadeDuration; // can use same raster-fade-duration layer paint property + if (perTileFadeMix >= 1.0) { + // tile.previousTexture?.destroy(); tile.previousTexture = null; - } - if (tile.previousTexture) { - - context.activeTexture.set(gl.TEXTURE2); - tile.previousTexture.bind(textureFilter, gl.CLAMP_TO_EDGE); } else { - context.activeTexture.set(gl.TEXTURE2); + // if (tile.previousTexture && perTileFadeMix < 1.0) { + // context.activeTexture.set(gl.TEXTURE3); + context.activeTexture.set(gl.TEXTURE0 + PREVIOUS_TILE_TEXTURE_UNIT); + tile.previousTexture.bind(textureFilter, gl.CLAMP_TO_EDGE); + } + /* + else { + // context.activeTexture.set(gl.TEXTURE3); + context.activeTexture.set(gl.TEXTURE0 + PREVIOUS_TILE_TEXTURE_UNIT); texture.bind(textureFilter, gl.CLAMP_TO_EDGE); // same as new } + */ context.activeTexture.set(gl.TEXTURE1); @@ -265,7 +270,8 @@ function drawRaster(painter: Painter, sourceCache: SourceCache, layer: RasterSty tileSize, buffer, emissiveStrength, - fadeMix + perTileFadeMix, + (tile.previousTexture && perTileFadeMix < 1.0) ? PREVIOUS_TILE_TEXTURE_UNIT : 0 ); const affectedByFog = painter.isTileAffectedByFog(coord); diff --git a/src/render/program/raster_program.ts b/src/render/program/raster_program.ts index b207303fbe1..042b6546631 100644 --- a/src/render/program/raster_program.ts +++ b/src/render/program/raster_program.ts @@ -43,7 +43,7 @@ export type RasterUniformsType = { ['u_texture_offset']: Uniform2f; ['u_texture_res']: Uniform2f; ['u_emissive_strength']: Uniform1f; - ['u_fade_mix']: Uniform1f; + ['u_per_tile_fade_mix']: Uniform1f; ['u_previous_tile']: Uniform1i; }; @@ -77,7 +77,7 @@ const rasterUniforms = (context: Context): RasterUniformsType => ({ 'u_texture_offset': new Uniform2f(context), 'u_texture_res': new Uniform2f(context), 'u_emissive_strength': new Uniform1f(context), - 'u_fade_mix': new Uniform1f(context), + 'u_per_tile_fade_mix': new Uniform1f(context), 'u_previous_tile': new Uniform1i(context), }); @@ -85,7 +85,7 @@ const rasterUniformValues = ( matrix: Float32Array, normalizeMatrix: Float32Array, globeMatrix: Float32Array, mercMatrix: Float32Array, gridMatrix: Float32Array, parentTL: [number, number], zoomTransition: number, mercatorCenter: [number, number], cutoffParams: [number, number, number, number], parentScaleBy: number, fade: { mix: number; opacity: number; -}, layer: RasterStyleLayer, perspectiveTransform: [number, number], elevation: number, colorRampUnit: number, colorMix: [number, number, number, number], colorOffset: number, colorRange: [number, number], tileSize: number, buffer: number, emissiveStrength: number, fadeMix: number, +}, layer: RasterStyleLayer, perspectiveTransform: [number, number], elevation: number, colorRampUnit: number, colorMix: [number, number, number, number], colorOffset: number, colorRange: [number, number], tileSize: number, buffer: number, emissiveStrength: number, perTileFadeMix: number, previousTileTextureUnit: number, ): UniformValues => ({ 'u_matrix': matrix, 'u_normalize_matrix': normalizeMatrix, @@ -121,8 +121,8 @@ matrix: Float32Array, normalizeMatrix: Float32Array, globeMatrix: Float32Array, ], 'u_texture_res': [tileSize + 2 * buffer, tileSize + 2 * buffer], 'u_emissive_strength': emissiveStrength, - 'u_fade_mix': fadeMix, - 'u_previous_tile': 2, + 'u_per_tile_fade_mix': perTileFadeMix, + 'u_previous_tile': previousTileTextureUnit, }); const rasterPoleUniformValues = ( @@ -164,7 +164,8 @@ const rasterPoleUniformValues = ( 1, 0, emissiveStrength, - 1 + 1, // perTileperTileFadeMix by default + 3, // previousTileTextureUnit )); function spinWeights(angle: number): [number, number, number] { diff --git a/src/shaders/raster.fragment.glsl b/src/shaders/raster.fragment.glsl index 2ee00f6e026..ee6e0983bf8 100644 --- a/src/shaders/raster.fragment.glsl +++ b/src/shaders/raster.fragment.glsl @@ -39,7 +39,7 @@ uniform vec2 u_texture_res; // ICONEM uniform sampler2D u_previous_tile; -uniform float u_fade_mix; +uniform float u_per_tile_fade_mix; void main() { @@ -70,7 +70,7 @@ void main() { color = mix(texture(u_image0, v_pos0), texture(u_image1, v_pos1), u_fade_t); // ICONEM - color = mix(texture(u_previous_tile, v_pos0), color, u_fade_mix); + color = mix(texture(u_previous_tile, v_pos0), color, u_per_tile_fade_mix); value = vec2(u_colorization_offset + dot(color.rgb, u_colorization_mix.rgb), color.a); #endif @@ -93,8 +93,8 @@ void main() { // ICONEM vec4 previousColor = texture(u_previous_tile, v_pos0); - color = mix(previousColor, color, u_fade_mix); - // float c = 1. - u_fade_mix; + color = mix(previousColor, color, u_per_tile_fade_mix); + // float c = 1. - u_per_tile_fade_mix; // color = vec4(c, c, c, 1.); // color = previousColor; // ICONEM diff --git a/src/source/raster_tile_source.ts b/src/source/raster_tile_source.ts index c41ffa07824..1509eca7c73 100644 --- a/src/source/raster_tile_source.ts +++ b/src/source/raster_tile_source.ts @@ -226,18 +226,13 @@ class RasterTileSource extends Evented implements IS // ICONEM if (tile.texture && tile.texture instanceof Texture) { - tile.previousTexture = tile.texture; - tile.fadeStartTime = performance.now(); - tile.isFading = true; + tile.previousTexture = tile.texture || null; + const perTileFadeDuration = 500; + tile.perTileFadeEndTime = browser.now() + perTileFadeDuration; // 300ms fade } tile.setTexture(data, this.map.painter); - // ICONEM - tile.previousTexture = tile.texture || null; - const textureStartTime = Date.now(); - tile.fadeEndTime = textureStartTime + 500; // 300ms fade - tile.setTexture(data, this.map.painter); tile.state = 'loaded'; cacheEntryPossiblyAdded(this.dispatcher); @@ -250,6 +245,12 @@ class RasterTileSource extends Evented implements IS tile.request.cancel(); delete tile.request; } + + if (tile.previousTexture && tile.previousTexture instanceof Texture) { + tile.previousTexture.destroy(); + } + tile.previousTexture = null; + if (callback) callback(); } @@ -267,7 +268,7 @@ class RasterTileSource extends Evented implements IS // ICONEM // Preserve as previous texture if fading - if (tile.fadeEndTime && Date.now() < tile.fadeEndTime) { + if (tile.perTileFadeEndTime && browser.now() < tile.perTileFadeEndTime) { tile.previousTexture = tile.texture; } else { tile.texture.destroy(); @@ -276,12 +277,11 @@ class RasterTileSource extends Evented implements IS tile.destroy(); } - // ICONEM + // ICONEM Ensure previous fade state cannot leak across lifecycles if (tile.previousTexture && tile.previousTexture instanceof Texture) { tile.previousTexture.destroy(); } tile.previousTexture = null; - tile.fadeEndTime = undefined; if (callback) callback(); } diff --git a/src/source/tile.ts b/src/source/tile.ts index d178621194c..085dfdc1f68 100644 --- a/src/source/tile.ts +++ b/src/source/tile.ts @@ -186,7 +186,7 @@ class Tile { worldview: string | undefined; // ICONEM - fadeStartTime: number | undefined; + perTileFadeEndTime: number | undefined; previousTexture: Texture | null | undefined | UserManagedTexture; /** From a2920c26306317d421d15efa29d7fbcb4ef5aa02 Mon Sep 17 00:00:00 2001 From: Jonathan Chemla Date: Wed, 13 Aug 2025 15:14:34 +0200 Subject: [PATCH 3/4] cleaning and testing, still not working replace perTileFadeEndTime by perTileFadeStartTime to test Some wip to try debugging why still tiles flicker Showing tile bounds, it appears tiles are actually marked for expiration --- debug/test-rastertile-fade.html | 2 +- src/render/draw_raster.ts | 37 +++++++++++++++--------------- src/shaders/raster.fragment.glsl | 26 +++++++++++++++++---- src/source/raster_tile_source.ts | 39 ++++++++++++++++---------------- src/source/tile.ts | 2 +- 5 files changed, 62 insertions(+), 44 deletions(-) diff --git a/debug/test-rastertile-fade.html b/debug/test-rastertile-fade.html index c7b487ae27e..e9e5c684b94 100644 --- a/debug/test-rastertile-fade.html +++ b/debug/test-rastertile-fade.html @@ -70,7 +70,7 @@ source: "custom-source", paint: { "raster-opacity": 1, - "raster-fade-duration": 0, + "raster-fade-duration": 500, "raster-opacity-transition": { duration: 0 }, }, }); diff --git a/src/render/draw_raster.ts b/src/render/draw_raster.ts index f7a5f2bc06a..1c470a7750a 100644 --- a/src/render/draw_raster.ts +++ b/src/render/draw_raster.ts @@ -174,25 +174,6 @@ function drawRaster(painter: Painter, sourceCache: SourceCache, layer: RasterSty context.activeTexture.set(gl.TEXTURE0); texture.bind(textureFilter, gl.CLAMP_TO_EDGE); - // ICONEM - const perTileFadeMix = (browser.now() - tile.timeAdded) / rasterFadeDuration; // can use same raster-fade-duration layer paint property - if (perTileFadeMix >= 1.0) { - // tile.previousTexture?.destroy(); - tile.previousTexture = null; - } else { - // if (tile.previousTexture && perTileFadeMix < 1.0) { - // context.activeTexture.set(gl.TEXTURE3); - context.activeTexture.set(gl.TEXTURE0 + PREVIOUS_TILE_TEXTURE_UNIT); - tile.previousTexture.bind(textureFilter, gl.CLAMP_TO_EDGE); - } - /* - else { - // context.activeTexture.set(gl.TEXTURE3); - context.activeTexture.set(gl.TEXTURE0 + PREVIOUS_TILE_TEXTURE_UNIT); - texture.bind(textureFilter, gl.CLAMP_TO_EDGE); // same as new - } - */ - context.activeTexture.set(gl.TEXTURE1); if (parentTile) { @@ -206,6 +187,24 @@ function drawRaster(painter: Painter, sourceCache: SourceCache, layer: RasterSty texture.bind(textureFilter, gl.CLAMP_TO_EDGE); } + // --- ICONEM: bind previous tile texture (if any) on unit 3 + let perTileFadeMix = 0.5; + const perTileFadeDuration = 1000; // because was probably initialLoad, rasterFadeDuration = 0 + if (tile.previousTexture) { + const now = browser.now(); + // If we missed setting start, treat as fully shown + const t0 = tile.perTileFadeStartTime ? tile.perTileFadeStartTime : now; + // const t0 = tile.perTileFadeEndTime ? tile.perTileFadeEndTime - perTileFadeDuration : now; + perTileFadeMix = Math.max(0, Math.min(1, (now - t0) / perTileFadeDuration)); + context.activeTexture.set(gl.TEXTURE0 + PREVIOUS_TILE_TEXTURE_UNIT); + tile.previousTexture.bind(textureFilter, gl.CLAMP_TO_EDGE); + // Optional micro-GC once the fade is done + if (perTileFadeMix >= 1 && tile.previousTexture instanceof Texture) { + tile.previousTexture.destroy(); + tile.previousTexture = null; + } + } + // Enable trilinear filtering on tiles only beyond 20 degrees pitch, // to prevent it from compromising image crispness on flat or low tilted maps. if ('useMipmap' in texture && context.extTextureFilterAnisotropic && painter.transform.pitch > 20) { diff --git a/src/shaders/raster.fragment.glsl b/src/shaders/raster.fragment.glsl index ee6e0983bf8..5cc1207aafd 100644 --- a/src/shaders/raster.fragment.glsl +++ b/src/shaders/raster.fragment.glsl @@ -39,7 +39,7 @@ uniform vec2 u_texture_res; // ICONEM uniform sampler2D u_previous_tile; -uniform float u_per_tile_fade_mix; +uniform float u_per_tile_fade_mix; // 0: show previous, 1: show current void main() { @@ -70,7 +70,7 @@ void main() { color = mix(texture(u_image0, v_pos0), texture(u_image1, v_pos1), u_fade_t); // ICONEM - color = mix(texture(u_previous_tile, v_pos0), color, u_per_tile_fade_mix); + // color = mix(texture(u_previous_tile, v_pos0), color, u_per_tile_fade_mix); value = vec2(u_colorization_offset + dot(color.rgb, u_colorization_mix.rgb), color.a); #endif @@ -94,14 +94,32 @@ void main() { // ICONEM vec4 previousColor = texture(u_previous_tile, v_pos0); color = mix(previousColor, color, u_per_tile_fade_mix); - // float c = 1. - u_per_tile_fade_mix; - // color = vec4(c, c, c, 1.); + // float c = color0.a; + // float c = u_per_tile_fade_mix; + // color = vec4(c, 0.,0., 1.); // color = previousColor; + + // 1 Per-tile previous/current crossfade + // color = texture(u_image0, v_pos0); + // if (u_per_tile_fade_mix < 1.0) { + // // vec4 prevColor = texture(u_previous_tile, v_pos0); + // vec4 previousColor = vec4(1., 0., 0., 1.); + // color = mix(previousColor, color, clamp(u_per_tile_fade_mix, 0.0, 1.0)); + // } + // 2 Existing parent/child crossfade + // parentColor = texture(u_image1, v_pos1); + // color = mix(parentColor, color, u_fade_t); + // ICONEM #endif color.a *= u_opacity; + + // ICONEM + color.a = 1.; + + #ifdef GLOBE_POLES color.a *= 1.0 - smoothstep(0.0, 0.05, u_zoom_transition); #endif diff --git a/src/source/raster_tile_source.ts b/src/source/raster_tile_source.ts index 1509eca7c73..1fdd32e4869 100644 --- a/src/source/raster_tile_source.ts +++ b/src/source/raster_tile_source.ts @@ -224,11 +224,17 @@ class RasterTileSource extends Evented implements IS if (this.map._refreshExpiredTiles) tile.setExpiryData({cacheControl, expires}); - // ICONEM - if (tile.texture && tile.texture instanceof Texture) { - tile.previousTexture = tile.texture || null; - const perTileFadeDuration = 500; - tile.perTileFadeEndTime = browser.now() + perTileFadeDuration; // 300ms fade + // --- ICONEM: capture previous GL texture for per-tile fade + if (tile.texture) { + tile.previousTexture = tile.texture; + // Important: prevent setTexture from destroying it implicitly + // by clearing the reference before creating the new one. + // (setTexture will only delete an existing texture if it's still attached) + // @ts-expect-error intentional temporary clear to avoid auto-destroy + tile.texture = null; + // const perTileFadeDuration = 500; + // tile.perTileFadeEndTime = browser.now() + perTileFadeDuration; // 300ms fade + tile.perTileFadeStartTime = browser.now(); // 300ms fade } tile.setTexture(data, this.map.painter); @@ -259,29 +265,24 @@ class RasterTileSource extends Evented implements IS if (tile.texture && tile.texture instanceof Texture) { // // Clean everything else up owned by the tile, but preserve the texture. // // Destroy first to prevent racing with the texture cache being popped. - // tile.destroy(true); + tile.destroy(true); // // Save the texture to the cache - // if (tile.texture && tile.texture instanceof Texture) { - // this.map.painter.saveTileTexture(tile.texture); - // } - - // ICONEM - // Preserve as previous texture if fading - if (tile.perTileFadeEndTime && browser.now() < tile.perTileFadeEndTime) { - tile.previousTexture = tile.texture; - } else { - tile.texture.destroy(); + if (tile.texture && tile.texture instanceof Texture) { + this.map.painter.saveTileTexture(tile.texture); } } else { tile.destroy(); } // ICONEM Ensure previous fade state cannot leak across lifecycles - if (tile.previousTexture && tile.previousTexture instanceof Texture) { - tile.previousTexture.destroy(); + if (tile.previousTexture) { + if (tile.previousTexture instanceof Texture) { + tile.previousTexture.destroy(); + } + tile.previousTexture = null; } - tile.previousTexture = null; + tile.perTileFadeStartTime = undefined; if (callback) callback(); } diff --git a/src/source/tile.ts b/src/source/tile.ts index 085dfdc1f68..f4985d63be3 100644 --- a/src/source/tile.ts +++ b/src/source/tile.ts @@ -186,7 +186,7 @@ class Tile { worldview: string | undefined; // ICONEM - perTileFadeEndTime: number | undefined; + perTileFadeStartTime: number | undefined; previousTexture: Texture | null | undefined | UserManagedTexture; /** From ac153bd702a4f93fc5426cf4d66fc9c75fc5398b Mon Sep 17 00:00:00 2001 From: Jonathan Chemla Date: Mon, 18 Aug 2025 15:35:55 +0200 Subject: [PATCH 4/4] Final experiments --- debug/test-rastertile-fade.html | 3 +- src/source/raster_tile_source.ts | 48 +++++++++++++++++++++----------- src/source/source_cache.ts | 32 +++++++++++++++++++++ src/source/tile.ts | 1 + 4 files changed, 67 insertions(+), 17 deletions(-) diff --git a/debug/test-rastertile-fade.html b/debug/test-rastertile-fade.html index e9e5c684b94..8a47c3e9182 100644 --- a/debug/test-rastertile-fade.html +++ b/debug/test-rastertile-fade.html @@ -88,7 +88,8 @@ let source = map.getSource("custom-source"); let new_url = `https://gibs-b.earthdata.nasa.gov/wmts/epsg3857/all/MODIS_Aqua_CorrectedReflectance_TrueColor/default/${year}-08-02/GoogleMapsCompatible_Level9/{z}/{y}/{x}.jpg`; //source?.setUrl(new_url); // not working - source?.setTiles([new_url]); + const clearSource= false + source?.setTiles([new_url], clearSource); } let intervalId; diff --git a/src/source/raster_tile_source.ts b/src/source/raster_tile_source.ts index 1fdd32e4869..b7d7370a428 100644 --- a/src/source/raster_tile_source.ts +++ b/src/source/raster_tile_source.ts @@ -139,19 +139,28 @@ class RasterTileSource extends Evented implements IS /** * Reloads the source data and re-renders the map. * + * @param {boolean} clearSource Optional param to load new source without clearing the tile cache (source from style). * @example * map.getSource('source-id').reload(); */ - reload() { + reload(clearSource: boolean = true) { this.cancelTileJSONRequest(); - const fqid = makeFQID(this.id, this.scope); - this.load(() => this.map.style.clearSource(fqid)); + if (clearSource) { + // Legacy path: full clear + const fqid = makeFQID(this.id, this.scope); + this.load(() => this.map.style.clearSource(fqid)); + } else { + // Soft path: keep SourceCache tiles + // Note the param could be passed as object like so opts?: {preserveTiles?: boolean} + this.load(); + } } /** * Sets the source `tiles` property and re-renders the map. * * @param {string[]} tiles An array of one or more tile source URLs, as in the TileJSON spec. + * @param {boolean} clearSource Optional param to load new source without clearing the tile cache (source from style). * @returns {RasterTileSource} Returns itself to allow for method chaining. * @example * map.addSource('source-id', { @@ -163,9 +172,10 @@ class RasterTileSource extends Evented implements IS * // Set the endpoint associated with a raster tile source. * map.getSource('source-id').setTiles(['https://another_end_point.net/{z}/{x}/{y}.png']); */ - setTiles(tiles: Array): this { + setTiles(tiles: Array, clearSource: boolean = true): this { this._options.tiles = tiles; - this.reload(); + // Soft reload: do NOT clear the source cache. + this.reload(clearSource); return this; } @@ -224,17 +234,23 @@ class RasterTileSource extends Evented implements IS if (this.map._refreshExpiredTiles) tile.setExpiryData({cacheControl, expires}); - // --- ICONEM: capture previous GL texture for per-tile fade - if (tile.texture) { - tile.previousTexture = tile.texture; - // Important: prevent setTexture from destroying it implicitly - // by clearing the reference before creating the new one. - // (setTexture will only delete an existing texture if it's still attached) - // @ts-expect-error intentional temporary clear to avoid auto-destroy - tile.texture = null; - // const perTileFadeDuration = 500; - // tile.perTileFadeEndTime = browser.now() + perTileFadeDuration; // 300ms fade - tile.perTileFadeStartTime = browser.now(); // 300ms fade + // // --- ICONEM: capture previous GL texture for per-tile fade + // if (tile.texture) { + // tile.previousTexture = tile.texture; + // // Important: prevent setTexture from destroying it implicitly + // // by clearing the reference before creating the new one. + // // (setTexture will only delete an existing texture if it's still attached) + // // @ts-expect-error intentional temporary clear to avoid auto-destroy + // tile.texture = null; + // // const perTileFadeDuration = 500; + // // tile.perTileFadeEndTime = browser.now() + perTileFadeDuration; // 300ms fade + // tile.perTileFadeStartTime = browser.now(); // 300ms fade + // } + const perTileFadeDuration = 5000; + const oldTile = this._tiles[tile.tileID.key]; + + if (oldTile && perTileFadeDuration > 0) { + oldTile.fadeRetainUntil = Date.now() + perTileFadeDuration; } tile.setTexture(data, this.map.painter); diff --git a/src/source/source_cache.ts b/src/source/source_cache.ts index 52c561f207b..34e6cb04165 100644 --- a/src/source/source_cache.ts +++ b/src/source/source_cache.ts @@ -421,6 +421,19 @@ class SourceCache extends Evented { } } + /** + * Find a loaded previousTile of the given tile + * @private + */ + findPreviousTile(tileID: OverscaledTileID): Tile | null | undefined { + const key = String(tileID.key); + const tile = this._tiles[key]; + if (tile && tile.previousTexture) { + return tile; + } + return undefined; + } + _getLoadedTile(tileID: OverscaledTileID): Tile | null | undefined { const tile = this._tiles[tileID.key]; if (tile && tile.hasData()) { @@ -619,6 +632,25 @@ class SourceCache extends Evented { // parent or child tiles that are *already* loaded. const retain = this._updateRetainedTiles(idealTileIDs); + // ICONEM: Retain old tiles for crossfade between old/new TMS + if (this._tiles) { + console.log('cache', this._tiles); + for (const id in this._tiles) { + const tile = this._tiles[id]; + console.log('yo', tile); + + // Old texture, not already in retain, and has a fade in progress + retain[id] = tile.tileID; + // if (!retain[id] && tile.texture && tile.previousTexture) { + // console.log('match'); + // const fadeDuration = 2000; + // if (tile.perTileFadeStartTime && Date.now() < tile.perTileFadeStartTime + fadeDuration) { + // retain[id] = tile.tileID; // keep this tile alive + // } + // } + } + } + if (isRasterType(this._source.type) && idealTileIDs.length !== 0) { const parentsForFading: Partial> = {}; const fadingTiles: Record = {}; diff --git a/src/source/tile.ts b/src/source/tile.ts index f4985d63be3..39068366b5b 100644 --- a/src/source/tile.ts +++ b/src/source/tile.ts @@ -188,6 +188,7 @@ class Tile { // ICONEM perTileFadeStartTime: number | undefined; previousTexture: Texture | null | undefined | UserManagedTexture; + fadeRetainUntil?: number; /** * @param {OverscaledTileID} tileID