diff --git a/CHANGES.md b/CHANGES.md index 98047d22162c..702cb45e9f5a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,6 +17,7 @@ ##### Fixes :wrench: - Fix label rendering bug in WebGL1 contexts. [#12301](https://github.com/CesiumGS/cesium/pull/12301) +- Fixed lag or crashes when loading many models in the same frame. [#12320](https://github.com/CesiumGS/cesium/pull/12320) - Updated WMS example URL in UrlTemplateImageryProvider documentation to use an active service. [#12323](https://github.com/CesiumGS/cesium/pull/12323) - Fix point cloud filtering performance on certain hardware [#12317](https://github.com/CesiumGS/cesium/pull/12317) diff --git a/packages/engine/Source/Renderer/ContextLimits.js b/packages/engine/Source/Renderer/ContextLimits.js index b6c5e5731f13..c435971762ac 100644 --- a/packages/engine/Source/Renderer/ContextLimits.js +++ b/packages/engine/Source/Renderer/ContextLimits.js @@ -44,7 +44,7 @@ Object.defineProperties(ContextLimits, { }, /** - * The approximate maximum cube mape width and height supported by this WebGL implementation. + * The approximate maximum cube map width and height supported by this WebGL implementation. * The minimum is 16, but most desktop and laptop implementations will support much larger sizes like 8,192. * @memberof ContextLimits * @type {number} diff --git a/packages/engine/Source/Scene/DynamicEnvironmentMapManager.js b/packages/engine/Source/Scene/DynamicEnvironmentMapManager.js index 40fd2560f1be..f3897e2ff0c3 100644 --- a/packages/engine/Source/Scene/DynamicEnvironmentMapManager.js +++ b/packages/engine/Source/Scene/DynamicEnvironmentMapManager.js @@ -12,6 +12,7 @@ import PixelFormat from "../Core/PixelFormat.js"; import SceneMode from "./SceneMode.js"; import Transforms from "../Core/Transforms.js"; import ComputeCommand from "../Renderer/ComputeCommand.js"; +import ContextLimits from "../Renderer/ContextLimits.js"; import CubeMap from "../Renderer/CubeMap.js"; import Framebuffer from "../Renderer/Framebuffer.js"; import Texture from "../Renderer/Texture.js"; @@ -79,7 +80,11 @@ function DynamicEnvironmentMapManager(options) { options = defaultValue(options, defaultValue.EMPTY_OBJECT); - const mipmapLevels = defaultValue(options.mipmapLevels, 10); + const mipmapLevels = Math.min( + defaultValue(options.mipmapLevels, 10), + Math.log2(ContextLimits.maximumCubeMapSize), + ); + this._mipmapLevels = mipmapLevels; this._radianceMapComputeCommands = new Array(6); this._convolutionComputeCommands = new Array((mipmapLevels - 1) * 6); @@ -295,6 +300,63 @@ Object.defineProperties(DynamicEnvironmentMapManager.prototype, { }, }); +// Internally manage a queue of commands across all instances to prevent too many commands from being added in a single frame and using too much memory at once. +DynamicEnvironmentMapManager._maximumComputeCommandCount = 8; // This value is updated once a context is created. +DynamicEnvironmentMapManager._activeComputeCommandCount = 0; +DynamicEnvironmentMapManager._nextFrameCommandQueue = []; +/** + * Add a command to the queue. If possible, it will be added to the list of commands for the next frame. Otherwise, it will be added to a backlog + * and attempted next frame. + * @private + * @param {ComputeCommand} command The created command + * @param {FrameState} frameState The current frame state + */ +DynamicEnvironmentMapManager._queueCommand = (command, frameState) => { + if ( + DynamicEnvironmentMapManager._activeComputeCommandCount >= + DynamicEnvironmentMapManager._maximumComputeCommandCount + ) { + // Command will instead be scheduled next frame + DynamicEnvironmentMapManager._nextFrameCommandQueue.push(command); + return; + } + + frameState.commandList.push(command); + DynamicEnvironmentMapManager._activeComputeCommandCount++; +}; +/** + * If there are any backlogged commands, queue up as many as possible for the next frame. + * @private + * @param {FrameState} frameState The current frame state + */ +DynamicEnvironmentMapManager._updateCommandQueue = (frameState) => { + DynamicEnvironmentMapManager._maximumComputeCommandCount = Math.log2( + ContextLimits.maximumCubeMapSize, + ); // Scale relative to GPU resources available + + if ( + DynamicEnvironmentMapManager._nextFrameCommandQueue.length > 0 && + DynamicEnvironmentMapManager._activeComputeCommandCount < + DynamicEnvironmentMapManager._maximumComputeCommandCount + ) { + let command = DynamicEnvironmentMapManager._nextFrameCommandQueue.shift(); + while ( + defined(command) && + DynamicEnvironmentMapManager._activeComputeCommandCount < + DynamicEnvironmentMapManager._maximumComputeCommandCount + ) { + if (command.canceled) { + command = DynamicEnvironmentMapManager._nextFrameCommandQueue.shift(); + continue; + } + + frameState.commandList.push(command); + DynamicEnvironmentMapManager._activeComputeCommandCount++; + command = DynamicEnvironmentMapManager._nextFrameCommandQueue.shift(); + } + } +}; + /** * Sets the owner for the input DynamicEnvironmentMapManager if there wasn't another owner. * Destroys the owner's previous DynamicEnvironmentMapManager if setting is successful. @@ -334,15 +396,25 @@ DynamicEnvironmentMapManager.setOwner = function ( DynamicEnvironmentMapManager.prototype.reset = function () { let length = this._radianceMapComputeCommands.length; for (let i = 0; i < length; ++i) { + if (defined(this._radianceMapComputeCommands[i])) { + this._radianceMapComputeCommands[i].canceled = true; + DynamicEnvironmentMapManager._activeComputeCommandCount--; + } this._radianceMapComputeCommands[i] = undefined; } length = this._convolutionComputeCommands.length; for (let i = 0; i < length; ++i) { + if (defined(this._convolutionComputeCommands[i])) { + this._convolutionComputeCommands[i].canceled = true; + DynamicEnvironmentMapManager._activeComputeCommandCount--; + } this._convolutionComputeCommands[i] = undefined; } if (defined(this._irradianceComputeCommand)) { + this._irradianceComputeCommand.canceled = true; + DynamicEnvironmentMapManager._activeComputeCommandCount--; this._irradianceComputeCommand = undefined; } @@ -520,14 +592,17 @@ function updateRadianceMap(manager, frameState) { framebuffer._unBind(); framebuffer.destroy(); + DynamicEnvironmentMapManager._activeComputeCommandCount--; + if (!commands.some(defined)) { manager._convolutionsCommandsDirty = true; manager._shouldRegenerateShaders = true; } }, }); - frameState.commandList.push(command); + manager._radianceMapComputeCommands[i] = command; + DynamicEnvironmentMapManager._queueCommand(command, frameState); i++; } manager._radianceCommandsDirty = false; @@ -554,7 +629,7 @@ function updateSpecularMaps(manager, frameState) { const getPostExecute = (index, texture, face, level) => () => { // Copy output texture to corresponding face and mipmap level const commands = manager._convolutionComputeCommands; - if (!defined(commands[index])) { + if (!defined(commands[index]) || commands[index].canceled) { // This command was cancelled return; } @@ -562,6 +637,7 @@ function updateSpecularMaps(manager, frameState) { radianceCubeMap.copyFace(frameState, texture, face, level); facesCopied++; + DynamicEnvironmentMapManager._activeComputeCommandCount--; // All faces and levels have been copied if (facesCopied === manager._specularMapTextures.length) { @@ -611,7 +687,7 @@ function updateSpecularMaps(manager, frameState) { owner: manager, uniformMap: { u_roughness: () => level / (mipmapLevels - 1), - u_radianceTexture: () => radianceCubeMap, + u_radianceTexture: () => radianceCubeMap ?? context.defaultTexture, u_faceDirection: () => { return CubeMap.getDirection(face, scratchCartesian); }, @@ -619,7 +695,7 @@ function updateSpecularMaps(manager, frameState) { postExecute: getPostExecute(index, texture, face, level), }); manager._convolutionComputeCommands[index] = command; - frameState.commandList.push(command); + DynamicEnvironmentMapManager._queueCommand(command, frameState); ++index; } @@ -664,7 +740,7 @@ function updateIrradianceResources(manager, frameState) { fragmentShaderSource: fs, outputTexture: texture, uniformMap: { - u_radianceMap: () => manager._radianceCubeMap, + u_radianceMap: () => manager._radianceCubeMap ?? context.defaultTexture, }, postExecute: () => { if (!defined(manager._irradianceComputeCommand)) { @@ -674,10 +750,12 @@ function updateIrradianceResources(manager, frameState) { manager._irradianceTextureDirty = false; manager._irradianceComputeCommand = undefined; manager._sphericalHarmonicCoefficientsDirty = true; + + DynamicEnvironmentMapManager._activeComputeCommandCount--; }, }); manager._irradianceComputeCommand = command; - frameState.commandList.push(command); + DynamicEnvironmentMapManager._queueCommand(command, frameState); manager._irradianceTextureDirty = true; } @@ -744,6 +822,8 @@ DynamicEnvironmentMapManager.prototype.update = function (frameState) { return; } + DynamicEnvironmentMapManager._updateCommandQueue(frameState); + const dynamicLighting = frameState.atmosphere.dynamicLighting; const regenerateEnvironmentMap = atmosphereNeedsUpdate(this, frameState) || diff --git a/packages/engine/Specs/Scene/DynamicEnvironmentMapManagerSpec.js b/packages/engine/Specs/Scene/DynamicEnvironmentMapManagerSpec.js index de82af7a9478..0783774b7482 100644 --- a/packages/engine/Specs/Scene/DynamicEnvironmentMapManagerSpec.js +++ b/packages/engine/Specs/Scene/DynamicEnvironmentMapManagerSpec.js @@ -2,6 +2,7 @@ import { Cartesian3, Cartographic, Color, + ContextLimits, CubeMap, DynamicAtmosphereLightingType, DynamicEnvironmentMapManager, @@ -70,16 +71,20 @@ describe("Scene/DynamicEnvironmentMapManager", function () { () => { const time = JulianDate.fromIso8601("2024-08-30T10:45:00Z"); - let scene; + let scene, orginalMaximumCubeMapSize; beforeAll(() => { scene = createScene({ skyBox: false, }); + orginalMaximumCubeMapSize = ContextLimits.maximumCubeMapSize; + // To keep tests fast, don't throttle + ContextLimits._maximumCubeMapSize = Number.POSITIVE_INFINITY; }); afterAll(() => { scene.destroyForSpecs(); + ContextLimits._maximumCubeMapSize = orginalMaximumCubeMapSize; }); afterEach(() => {