diff --git a/CHANGES.md b/CHANGES.md index fc914d1a426..564a1b50f09 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,17 @@ # Change Log +### 1.123 - 2024-11-01 + +#### @cesium/engine + +##### Additions :tada: + +- Use rendering rate to adjust zoom rate so zoom speed appears consistent on high or low refresh rate browsers. Use `zoomFactor` to adjust zoomRate. Added `averageFramesPerSecond` and `averageFrameRateWindow` to `FrameRateMonitor` class for getting the frame rendering rate. + +##### Fixes :wrench: + +##### Deprecated :hourglass_flowing_sand: + ### 1.122 - 2024-10-01 #### @cesium/engine diff --git a/packages/engine/Source/Scene/FrameRateMonitor.js b/packages/engine/Source/Scene/FrameRateMonitor.js index 3e7f3ae5053..516478b8d77 100644 --- a/packages/engine/Source/Scene/FrameRateMonitor.js +++ b/packages/engine/Source/Scene/FrameRateMonitor.js @@ -17,7 +17,8 @@ import TimeConstants from "../Core/TimeConstants.js"; * * @param {object} [options] Object with the following properties: * @param {Scene} options.scene The Scene instance for which to monitor performance. - * @param {number} [options.samplingWindow=5.0] The length of the sliding window over which to compute the average frame rate, in seconds. + * @param {number} [options.samplingWindow=5.0] The length of the sliding window over which to compute the average frame rate, in seconds, + * for the purpose of calculating if the frame rate is lower or higher than the minimum frame rate threshold. * @param {number} [options.quietPeriod=2.0] The length of time to wait at startup and each time the page becomes visible (i.e. when the user * switches back to the tab) before starting to measure performance, in seconds. * @param {number} [options.warmupPeriod=5.0] The length of the warmup period, in seconds. During the warmup period, a separate @@ -28,6 +29,8 @@ import TimeConstants from "../Core/TimeConstants.js"; * @param {number} [options.minimumFrameRateAfterWarmup=8] The minimum frames-per-second that are required for acceptable performance after * the end of the warmup period. If the frame rate averages less than this during any samplingWindow after the warmupPeriod, the * lowFrameRate event will be raised and the page will redirect to the redirectOnLowFrameRateUrl, if any. + * @param {number} [options.averageFrameRateWindow=10] The length of the sliding window over which to compute the average frame rate, in + * number of frames, for the purpose of calculating and displaying averageFramesPerSecond. */ function FrameRateMonitor(options) { //>>includeStart('debug', pragmas.debug); @@ -39,7 +42,8 @@ function FrameRateMonitor(options) { this._scene = options.scene; /** - * Gets or sets the length of the sliding window over which to compute the average frame rate, in seconds. + * Gets or sets the length of the sliding window over which to compute the average frame rate, in seconds, for the + * purpose of calculating if the frame rate is lower or higher than the minimum frame rate threshold. * @type {number} */ this.samplingWindow = defaultValue( @@ -47,6 +51,16 @@ function FrameRateMonitor(options) { FrameRateMonitor.defaultSettings.samplingWindow, ); + /** + * Gets or sets the length of the sliding window over which to compute the average frame rate, in number of frames, + * for the purpose of calculating and displaying averageFramesPerSecond. + * @type {number} + */ + this.averageFrameRateWindow = defaultValue( + options.averageFrameRateWindow, + FrameRateMonitor.defaultSettings.averageFrameRateWindow, + ); + /** * Gets or sets the length of time to wait at startup and each time the page becomes visible (i.e. when the user * switches back to the tab) before starting to measure performance, in seconds. @@ -93,6 +107,7 @@ function FrameRateMonitor(options) { this._nominalFrameRate = new Event(); this._frameTimes = []; + this._averageFrameRateTimes = []; this._needsQuietPeriod = true; this._quietPeriodEndTime = 0.0; this._warmupPeriodEndTime = 0.0; @@ -165,6 +180,7 @@ FrameRateMonitor.defaultSettings = { warmupPeriod: 5.0, minimumFrameRateDuringWarmup: 4, minimumFrameRateAfterWarmup: 8, + averageFrameRateWindow: 10, }; /** @@ -233,6 +249,9 @@ Object.defineProperties(FrameRateMonitor.prototype, { /** * Gets the most recently computed average frames-per-second over the last samplingWindow. + * The samplingWindow begins after the expiration of the quietPeriod, and restarts + * each time the minimumFrameRateDuringWarmup or minimumFrameRateAfterWarmup threshold + * is crossed, or pause is invoked. * This property may be undefined if the frame rate has not been computed. * @memberof FrameRateMonitor.prototype * @type {number} @@ -242,6 +261,31 @@ Object.defineProperties(FrameRateMonitor.prototype, { return this._lastFramesPerSecond; }, }, + + /** + * Gets the most recently computed average frames-per-second over the last number of frames specified + * in averageFrameRateWindow. + * This property may be undefined when the scene first loads until averageFrameRateWindow + * has passed. After averageFrameRateWindow has passed the property will be continuously + * available, the limitations specified for lastFramesPerSecond do not apply to it. + * @memberof FrameRateMonitor.prototype + * @type {number|undefined} + */ + averageFramesPerSecond: { + get: function () { + if (this._averageFrameRateTimes.length !== this.averageFrameRateWindow) { + return undefined; + } + const windowSize = this.averageFrameRateWindow - 1; + const averageTimePerFrame = + (this._averageFrameRateTimes[0] - + this._averageFrameRateTimes[windowSize]) / + windowSize; + const fps = + 1.0 / (averageTimePerFrame * TimeConstants.SECONDS_PER_MILLISECOND); + return fps; + }, + }, }); /** @@ -310,12 +354,17 @@ FrameRateMonitor.prototype.destroy = function () { }; function update(monitor, time) { + const timeStamp = getTimestamp(); + + monitor._averageFrameRateTimes.unshift(timeStamp); + if (monitor._averageFrameRateTimes.length > monitor.averageFrameRateWindow) { + monitor._averageFrameRateTimes.pop(); + } + if (monitor._pauseCount > 0) { return; } - const timeStamp = getTimestamp(); - if (monitor._needsQuietPeriod) { monitor._needsQuietPeriod = false; monitor._frameTimes.length = 0; diff --git a/packages/engine/Source/Scene/ScreenSpaceCameraController.js b/packages/engine/Source/Scene/ScreenSpaceCameraController.js index 54e7aee3e11..8785a84f7d4 100644 --- a/packages/engine/Source/Scene/ScreenSpaceCameraController.js +++ b/packages/engine/Source/Scene/ScreenSpaceCameraController.js @@ -25,6 +25,7 @@ import MapMode2D from "./MapMode2D.js"; import SceneMode from "./SceneMode.js"; import SceneTransforms from "./SceneTransforms.js"; import TweenCollection from "./TweenCollection.js"; +import FrameRateMonitor from "./FrameRateMonitor.js"; /** * Modifies the camera position and orientation based on mouse input to a canvas. @@ -145,6 +146,8 @@ function ScreenSpaceCameraController(scene) { */ this.zoomFactor = 5.0; + this.frameRateMonitor = FrameRateMonitor.fromScene(scene); + /** * The input that allows the user to pan around the map. This only applies in 2D and Columbus view modes. *

@@ -573,7 +576,16 @@ function handleZoom( const maxHeight = object.maximumZoomDistance; const minDistance = distanceMeasure - minHeight; - let zoomRate = zoomFactor * minDistance; + + let fpsMultiplier = 1.0; + const fps = object.frameRateMonitor.averageFramesPerSecond; + // we set the ideal browser refresh rate to 60hz + if (defined(fps) && fps > 0) { + fpsMultiplier = 60.0 / fps; + } + + let zoomRate = zoomFactor * minDistance * fpsMultiplier; + zoomRate = CesiumMath.clamp( zoomRate, object._minimumZoomRate, diff --git a/packages/engine/Specs/Scene/FrameRateMonitorSpec.js b/packages/engine/Specs/Scene/FrameRateMonitorSpec.js index 3de2f8a5b8d..67f61b4d1c1 100644 --- a/packages/engine/Specs/Scene/FrameRateMonitorSpec.js +++ b/packages/engine/Specs/Scene/FrameRateMonitorSpec.js @@ -58,6 +58,7 @@ describe( monitor = new FrameRateMonitor({ scene: scene, samplingWindow: 3.0, + averageFrameRateWindow: 20, quietPeriod: 1.0, warmupPeriod: 6.0, minimumFrameRateDuringWarmup: 1, @@ -65,6 +66,7 @@ describe( }); expect(monitor.samplingWindow).toBe(3.0); + expect(monitor.averageFrameRateWindow).toBe(20); expect(monitor.quietPeriod).toBe(1.0); expect(monitor.warmupPeriod).toBe(6.0); expect(monitor.minimumFrameRateDuringWarmup).toBe(1); @@ -251,6 +253,43 @@ describe( expect(monitor.lastFramesPerSecond).toBeGreaterThanOrEqual(10); expect(nominalListener).toHaveBeenCalled(); }); + + it("returns averageFrameRate, irrespective of quiet period and pauses", function () { + monitor = new FrameRateMonitor({ + scene: scene, + quietPeriod: 3, + warmupPeriod: 1, + samplingWindow: 1, + averageFrameRateWindow: 20, + minimumFrameRateDuringWarmup: 10, + minimumFrameRateAfterWarmup: 10, + }); + + // Rendering once starts the quiet period + scene.render(); + + // Render 10 frames at 100 millisecond interval + let numFramesRendered = 0; + while (numFramesRendered < 10) { + scene.render(); + spinWait(100); + numFramesRendered++; + } + + monitor.pause(); + + // Render 15 more frames at 100 millisecond interval + while (numFramesRendered < 25) { + scene.render(); + spinWait(100); + numFramesRendered++; + } + + monitor.unpause(); + + expect(getTimestamp()).toBeLessThan(monitor._quietPeriodEndTime); + expect(monitor.averageFramesPerSecond).toBeCloseTo(10, 1); + }); }, "WebGL", ); diff --git a/packages/engine/Specs/Scene/ScreenSpaceCameraControllerSpec.js b/packages/engine/Specs/Scene/ScreenSpaceCameraControllerSpec.js index 593ca54b10f..28da0f1e570 100644 --- a/packages/engine/Specs/Scene/ScreenSpaceCameraControllerSpec.js +++ b/packages/engine/Specs/Scene/ScreenSpaceCameraControllerSpec.js @@ -22,6 +22,7 @@ import { import createCamera from "../../../../Specs/createCamera.js"; import createCanvas from "../../../../Specs/createCanvas.js"; import DomEventSimulator from "../../../../Specs/DomEventSimulator.js"; +import Event from "../../Source/Core/Event.js"; describe("Scene/ScreenSpaceCameraController", function () { let usePointerEvents; @@ -41,6 +42,7 @@ describe("Scene/ScreenSpaceCameraController", function () { this.screenSpaceCameraController = undefined; this.cameraUnderground = false; this.globeHeight = 0.0; + this.preUpdate = new Event(); } function MockGlobe(ellipsoid) {