diff --git a/CHANGES.md b/CHANGES.md index e774050a7dbb..f722649f8b60 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -18,6 +18,7 @@ - Improved performance when removing primitives. [#3018](https://github.com/CesiumGS/cesium/pull/3018) - Improved performance of terrain Quadtree handling of custom data [#12907](https://github.com/CesiumGS/cesium/pull/12907) - Fixed picking of `GroundPrimitive` with multiple `PolygonGeometry` instances selecting the wrong instance. [#12978](https://github.com/CesiumGS/cesium/pull/12978) +- Fix render issues when updating Billboards with syncronous textures with `requestRenderMode=true`. [#12543](https://github.com/CesiumGS/cesium/issues/12543) ## 1.134.1 - 2025-10-10 diff --git a/packages/engine/Source/DataSources/BillboardVisualizer.js b/packages/engine/Source/DataSources/BillboardVisualizer.js index f01ca3220cf1..ab768ad889e7 100644 --- a/packages/engine/Source/DataSources/BillboardVisualizer.js +++ b/packages/engine/Source/DataSources/BillboardVisualizer.js @@ -77,7 +77,8 @@ function BillboardVisualizer(entityCluster, entityCollection) { * Entity counterpart at the given time. * * @param {JulianDate} time The time to update to. - * @returns {boolean} This function always returns true. + * @returns {boolean} True if the visualizer successfully updated to the provided time, + * false if the visualizer is waiting for asynchronous work to be completed. */ BillboardVisualizer.prototype.update = function (time) { //>>includeStart('debug', pragmas.debug); @@ -89,6 +90,7 @@ BillboardVisualizer.prototype.update = function (time) { const items = this._items.values; const cluster = this._cluster; + let isUpdated = true; for (let i = 0, len = items.length; i < len; i++) { const item = items[i]; const entity = item.entity; @@ -226,6 +228,7 @@ BillboardVisualizer.prototype.update = function (time) { time, defaultSplitDirection, ); + isUpdated = billboard.ready && isUpdated; const subRegion = Property.getValueOrUndefined( billboardGraphics._imageSubRegion, @@ -236,7 +239,7 @@ BillboardVisualizer.prototype.update = function (time) { billboard.setImageSubRegion(billboard.image, subRegion); } } - return true; + return isUpdated; }; /** diff --git a/packages/engine/Source/DataSources/DataSourceDisplay.js b/packages/engine/Source/DataSources/DataSourceDisplay.js index eb540fee3d28..3f80f08101c1 100644 --- a/packages/engine/Source/DataSources/DataSourceDisplay.js +++ b/packages/engine/Source/DataSources/DataSourceDisplay.js @@ -115,6 +115,7 @@ function DataSourceDisplay(options) { this._removeDataSourceCollectionListener = removeDataSourceCollectionListener; this._ready = false; + this._prevUpdateResult = this._ready; } const ExtraVisualizers = []; @@ -295,7 +296,7 @@ DataSourceDisplay.prototype.update = function (time) { return false; } - let result = true; + let updateResult = true; let i; let x; @@ -306,33 +307,33 @@ DataSourceDisplay.prototype.update = function (time) { for (i = 0; i < length; i++) { const dataSource = dataSources.get(i); if (defined(dataSource.update)) { - result = dataSource.update(time) && result; + updateResult = dataSource.update(time) && updateResult; } visualizers = dataSource._visualizers; vLength = visualizers.length; for (x = 0; x < vLength; x++) { - result = visualizers[x].update(time) && result; + updateResult = visualizers[x].update(time) && updateResult; } } visualizers = this._defaultDataSource._visualizers; vLength = visualizers.length; for (x = 0; x < vLength; x++) { - result = visualizers[x].update(time) && result; + updateResult = visualizers[x].update(time) && updateResult; } - // Request a rendering of the scene when the data source - // becomes 'ready' for the first time - if (!this._ready && result) { + // Trigger an event when all of the data sources finish updating + if (!this._prevUpdateResult && updateResult) { this._scene.requestRender(); } + this._prevUpdateResult = updateResult; // once the DataSourceDisplay is ready it should stay ready to prevent // entities from breaking updates when they become "un-ready" - this._ready = this._ready || result; + this._ready = this._ready || updateResult; - return result; + return updateResult; }; DataSourceDisplay.prototype._postRender = function () { diff --git a/packages/engine/Source/Renderer/TextureAtlas.js b/packages/engine/Source/Renderer/TextureAtlas.js index 9fa62b646e3e..9fd891d050bf 100644 --- a/packages/engine/Source/Renderer/TextureAtlas.js +++ b/packages/engine/Source/Renderer/TextureAtlas.js @@ -623,7 +623,14 @@ TextureAtlas.prototype.update = function (context) { return this._processImageQueue(context); }; -async function resolveImage(image, id) { +/** + * Gets an image from various possible input types. + * @private + * @param {HTMLImageElement|HTMLCanvasElement|string|Resource|Promise|TextureAtlas.CreateImageCallback} image An image or canvas to add to the texture atlas + * @param {string} id An identifier to detect whether the image already exists in the atlas. + * @returns {TexturePacker.PackableObject | Promise} The image or a Promise that resolves to it. + */ +function getImage(image, id) { if (typeof image === "function") { image = image(id); } @@ -660,21 +667,23 @@ TextureAtlas.prototype.addImage = function (id, image) { const index = this._nextIndex++; this._indexById.set(id, index); - - const resolveAndAddImage = async () => { - image = await resolveImage(image, id); - //>>includeStart('debug', pragmas.debug); - Check.defined("image", image); - //>>includeEnd('debug'); - + image = getImage(image, id); + + const resolveAndAddImage = async (index, image) => { + // The initial part of an async function runs synchronously up to the first await + // If the image is synchronous, skip the await: add and process the image the current frame. + if (image instanceof Promise) { + // Effectively return a promise chain. The image promise will resolve and be processed on a later frame + image = await image; + } if (this.isDestroyed() || !defined(image)) { return -1; } return this._addImage(index, image); }; + promise = resolveAndAddImage(index, image); - promise = resolveAndAddImage(); this._indexPromiseById.set(id, promise); return promise; }; diff --git a/packages/engine/Source/Scene/Scene.js b/packages/engine/Source/Scene/Scene.js index e2aac5ab9a17..d55ab6628a35 100644 --- a/packages/engine/Source/Scene/Scene.js +++ b/packages/engine/Source/Scene/Scene.js @@ -84,9 +84,7 @@ import getMetadataProperty from "./getMetadataProperty.js"; const requestRenderAfterFrame = function (scene) { return function () { - scene.frameState.afterRender.push(function () { - scene.requestRender(); - }); + scene.frameState.afterRender.push(() => true); }; }; diff --git a/packages/engine/Specs/Renderer/TextureAtlasSpec.js b/packages/engine/Specs/Renderer/TextureAtlasSpec.js index 51d07bb7a3e0..342515ab4c72 100644 --- a/packages/engine/Specs/Renderer/TextureAtlasSpec.js +++ b/packages/engine/Specs/Renderer/TextureAtlasSpec.js @@ -114,13 +114,14 @@ describe("Scene/TextureAtlas", function () { }).toThrowDeveloperError(); }); - it("add image throws if a promise returns undefined", async function () { + it("add image returns -1 if a promise returns undefined", async function () { atlas = new TextureAtlas(); const promise = atlas.addImage(greenGuid, Promise.resolve()); expect(atlas.numberOfImages).toEqual(1); - await expectAsync(promise).toBeRejectedWithDeveloperError(); + const index = await promise; + expect(index).toEqual(-1); }); it("add image rejects if a promised image rejects", async function () { @@ -1297,11 +1298,23 @@ describe("Scene/TextureAtlas", function () { expect(guid1).not.toEqual(guid2); }); - it("destroys successfully while image is queued", async function () { + it("Syncronous texture resolves immediately", async function () { atlas = new TextureAtlas(); const promise = atlas.addImage(greenGuid, greenImage); + atlas.update(scene.frameState.context); + const index = await promise; + + expect(index).toEqual(0); + atlas = undefined; + }); + + it("destroys successfully while image promise is queued", async function () { + atlas = new TextureAtlas(); + + const promise = atlas.addImage(greenGuid, Promise.resolve(greenImage)); + atlas.update(scene.frameState.context); const texture = atlas.texture;