diff --git a/packages/engine/Source/Core/VectorPipeline.js b/packages/engine/Source/Core/VectorPipeline.js index 52523e47d58..3c91d90c1d3 100644 --- a/packages/engine/Source/Core/VectorPipeline.js +++ b/packages/engine/Source/Core/VectorPipeline.js @@ -39,6 +39,8 @@ const scratchSegmentEnd = new Cartesian2(); * @typedef {object} VectorTileData * * @property {boolean} show Whether this vector data should be rendered. + * @property {number} [changeCount] Provider change count this data was last + * validated against, managed by VectorProvider. * * Stage 1: Collect vector segments intersecting tile. * @property {number[][]} [segments] diff --git a/packages/engine/Source/Core/VectorProvider.js b/packages/engine/Source/Core/VectorProvider.js index 258203a52ce..915a1212651 100644 --- a/packages/engine/Source/Core/VectorProvider.js +++ b/packages/engine/Source/Core/VectorProvider.js @@ -71,6 +71,21 @@ class VectorProvider { * @private */ this._dirtyRectangles = []; + + /** + * Total number of dirty regions ever recorded. Tile data is stamped with + * this value when validated, so a tile that missed regions cleared while + * it was not rendered can be detected and re-baked. + * @private + */ + this._changeCount = 0; + + /** + * Value of {@link VectorProvider#_changeCount} at the last + * {@link VectorProvider#makeClean}. + * @private + */ + this._changeCountAtClean = 0; } /** @type {TilingScheme} */ @@ -160,6 +175,7 @@ class VectorProvider { new Rectangle(), ); this._dirtyRectangles.push(collectionRectangle); + this._changeCount++; } /** @@ -175,7 +191,7 @@ class VectorProvider { const width = Rectangle.computeWidth(tileRectangle); /** @type {VectorTileData} */ - const result = { show: true }; + const result = { show: true, changeCount: this._changeCount }; for (const collection of this._collections) { const collectionRectangle = Rectangle.fromBoundingSphere( @@ -228,7 +244,9 @@ class VectorProvider { * Re-bakes a tile's vector data if the tile overlaps a region changed since * the last {@link VectorProvider#makeClean}, releasing the previous data. * Returns the current data unchanged when the tile is outside every changed - * region. + * region. A tile whose data predates regions already cleared — because the + * tile was not rendered while they were consumed — is re-baked + * conservatively. * * @param {number} x * @param {number} y @@ -238,9 +256,22 @@ class VectorProvider { * @returns {VectorTileData|undefined} */ updateTileData(x, y, level, context, currentData) { - const dirtyRectangles = this._dirtyRectangles; - const tilingScheme = this._tilingScheme; - if (!intersectRectangles(x, y, level, dirtyRectangles, tilingScheme)) { + const validated = + defined(currentData) && + currentData.changeCount >= this._changeCountAtClean; + + if ( + validated && + !intersectRectangles( + x, + y, + level, + this._dirtyRectangles, + this._tilingScheme, + ) + ) { + // Now validated against every recorded change. + currentData.changeCount = this._changeCount; return currentData; } @@ -257,6 +288,7 @@ class VectorProvider { */ makeClean() { this._dirtyRectangles.length = 0; + this._changeCountAtClean = this._changeCount; } /** diff --git a/packages/engine/Specs/Core/VectorProviderSpec.js b/packages/engine/Specs/Core/VectorProviderSpec.js index c58b7af7b2e..f0260c58125 100644 --- a/packages/engine/Specs/Core/VectorProviderSpec.js +++ b/packages/engine/Specs/Core/VectorProviderSpec.js @@ -43,9 +43,9 @@ describe("Core/VectorProvider", function () { it("returns hidden vector data with no collections", function () { const provider = new VectorProvider({ tilingScheme }); const xy = tilingScheme.positionToTileXY(lineMidpoint, level); - expect(provider.requestTileData(xy.x, xy.y, level, context)).toEqual({ - show: false, - }); + expect(provider.requestTileData(xy.x, xy.y, level, context)).toEqual( + jasmine.objectContaining({ show: false }), + ); }); it("returns packed lookup data for a tile overlapping a polyline", function () { @@ -103,9 +103,9 @@ describe("Core/VectorProvider", function () { provider.add(createPolylineCollection()); const xy = tilingScheme.positionToTileXY(farPoint, level); - expect(provider.requestTileData(xy.x, xy.y, level, context)).toEqual({ - show: false, - }); + expect(provider.requestTileData(xy.x, xy.y, level, context)).toEqual( + jasmine.objectContaining({ show: false }), + ); }); it("stops returning data after a collection is removed", function () { @@ -115,9 +115,9 @@ describe("Core/VectorProvider", function () { provider.remove(collection); const xy = tilingScheme.positionToTileXY(lineMidpoint, level); - expect(provider.requestTileData(xy.x, xy.y, level, context)).toEqual({ - show: false, - }); + expect(provider.requestTileData(xy.x, xy.y, level, context)).toEqual( + jasmine.objectContaining({ show: false }), + ); }); it("keeps existing tile data when no dirty regions are recorded", function () { @@ -156,6 +156,27 @@ describe("Core/VectorProvider", function () { expect(updated.show).toBe(true); }); + it("re-bakes a tile that missed dirty regions consumed while it was not rendered", function () { + const provider = new VectorProvider({ tilingScheme }); + const collection = createPolylineCollection(); + provider.add(collection); + + const xy = tilingScheme.positionToTileXY(lineMidpoint, level); + const data = provider.requestTileData(xy.x, xy.y, level, context); + provider.makeClean(); + + // The collection is removed while the tile is outside the rendered set; + // another frame's pass consumes and clears the dirty region. + provider.remove(collection); + provider.update(); + provider.makeClean(); + + // When the tile re-enters the rendered set it must not keep stale data. + const updated = provider.updateTileData(xy.x, xy.y, level, context, data); + expect(updated).not.toBe(data); + expect(updated.show).toBe(false); + }); + it("records and clears a dirty rectangle for a collection with a local region", function () { const provider = new VectorProvider({ tilingScheme }); const collection = new BufferPolylineCollection({