diff --git a/src/display_context.ts b/src/display_context.ts index d25d9e9c2..6854f7f37 100644 --- a/src/display_context.ts +++ b/src/display_context.ts @@ -587,7 +587,7 @@ export class DisplayContext extends RefCounted implements FrameNumberCounter { this.updateStarted.dispatch(); const gl = this.gl; const ext = this.framerateMonitor.getTimingExtension(gl); - const query = this.framerateMonitor.startFrameTimeQuery(gl, ext); + this.framerateMonitor.startFrameTimeQuery(gl, ext, this.frameNumber); this.ensureBoundsUpdated(); this.gl.clearColor(0.0, 0.0, 0.0, 0.0); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); @@ -611,7 +611,7 @@ export class DisplayContext extends RefCounted implements FrameNumberCounter { gl.clear(gl.COLOR_BUFFER_BIT); this.gl.colorMask(true, true, true, true); this.updateFinished.dispatch(); - this.framerateMonitor.endFrameTimeQuery(gl, ext, query); + this.framerateMonitor.endLastTimeQuery(gl, ext); this.framerateMonitor.grabAnyFinishedQueryResults(gl); } diff --git a/src/util/framerate.ts b/src/util/framerate.ts index 05f34bec2..35eb36108 100644 --- a/src/util/framerate.ts +++ b/src/util/framerate.ts @@ -20,10 +20,22 @@ export enum FrameTimingMethod { MAX = 2, } +interface QueryInfo { + glQuery: WebGLQuery; + frameNumber: number; + wasStarted: boolean; + wasEnded: boolean; +} + +interface FrameDeltaInfo { + frameDelta: number; + frameNumber: number; +} + export class FramerateMonitor { - private timeElapsedQueries: (WebGLQuery | null)[] = []; + private timeElapsedQueries: QueryInfo[] = []; private warnedAboutMissingExtension = false; - private storedTimeDeltas: number[] = []; + private storedTimeDeltas: FrameDeltaInfo[] = []; constructor( private numStoredTimes: number = 10, @@ -48,61 +60,107 @@ export class FramerateMonitor { return ext; } - startFrameTimeQuery(gl: WebGL2RenderingContext, ext: any) { + getOldestQueryIndexByFrameNumber() { + if (this.timeElapsedQueries.length === 0) { + return undefined; + } + let oldestQueryIndex = 0; + for (let i = 1; i < this.timeElapsedQueries.length; i++) { + const oldestQuery = this.timeElapsedQueries[oldestQueryIndex]; + if (this.timeElapsedQueries[i].frameNumber < oldestQuery.frameNumber) { + oldestQueryIndex = i; + } + } + return oldestQueryIndex; + } + + startFrameTimeQuery( + gl: WebGL2RenderingContext, + ext: any, + frameNumber: number, + ) { if (ext === null) { return null; } const query = gl.createQuery(); - if (query !== null) { + const currentQuery = + this.timeElapsedQueries[this.timeElapsedQueries.length - 1]; + if (query !== null && currentQuery !== query) { gl.beginQuery(ext.TIME_ELAPSED_EXT, query); + if (this.timeElapsedQueries.length >= this.queryPoolSize) { + const oldestQueryIndex = this.getOldestQueryIndexByFrameNumber(); + if (oldestQueryIndex !== undefined) { + const oldestQuery = this.timeElapsedQueries.splice( + oldestQueryIndex, + 1, + )[0]; + gl.deleteQuery(oldestQuery.glQuery); + } + } + const queryInfo: QueryInfo = { + glQuery: query, + frameNumber: frameNumber, + wasStarted: true, + wasEnded: false, + }; + this.timeElapsedQueries.push(queryInfo); } return query; } - endFrameTimeQuery( - gl: WebGL2RenderingContext, - ext: any, - query: WebGLQuery | null, - ) { - if (ext !== null && query !== null) { - gl.endQuery(ext.TIME_ELAPSED_EXT); - } - if (this.timeElapsedQueries.length >= this.queryPoolSize) { - const oldestQuery = this.timeElapsedQueries.shift(); - if (oldestQuery !== null && oldestQuery !== undefined) { - gl.deleteQuery(oldestQuery); + endLastTimeQuery(gl: WebGL2RenderingContext, ext: any) { + if (ext !== null) { + const currentQuery = + this.timeElapsedQueries[this.timeElapsedQueries.length - 1]; + if (!currentQuery.wasEnded && currentQuery.wasStarted) { + gl.endQuery(ext.TIME_ELAPSED_EXT); + currentQuery.wasEnded = true; } } - this.timeElapsedQueries.push(query); } grabAnyFinishedQueryResults(gl: WebGL2RenderingContext) { const deletedQueryIndices: number[] = []; for (let i = 0; i < this.timeElapsedQueries.length; i++) { const query = this.timeElapsedQueries[i]; - if (query !== null) { + // Error checking: if the query was not started or ended, just delete it. + // This can happen from errors in the rendering + if (!query.wasEnded || !query.wasStarted) { + gl.deleteQuery(query.glQuery); + deletedQueryIndices.push(i); + } else { const available = gl.getQueryParameter( - query, + query.glQuery, gl.QUERY_RESULT_AVAILABLE, ); - if (available) { - const result = gl.getQueryParameter(query, gl.QUERY_RESULT) / 1e6; - this.storedTimeDeltas.push(result); - gl.deleteQuery(query); + // If the result is null, then something went wrong and we should just delete the query. + if (available === null) { + gl.deleteQuery(query.glQuery); + deletedQueryIndices.push(i); + } else if (available) { + const result = + gl.getQueryParameter(query.glQuery, gl.QUERY_RESULT) / 1e6; + this.storedTimeDeltas.push({ + frameDelta: result, + frameNumber: query.frameNumber, + }); + gl.deleteQuery(query.glQuery); deletedQueryIndices.push(i); } } } - for (let i = deletedQueryIndices.length - 1; i >= 0; i--) { - this.timeElapsedQueries.splice(i, 1); - } + this.timeElapsedQueries = this.timeElapsedQueries.filter( + (_, i) => !deletedQueryIndices.includes(i), + ); if (this.storedTimeDeltas.length > this.numStoredTimes) { this.storedTimeDeltas = this.storedTimeDeltas.slice(-this.numStoredTimes); } } getLastFrameTimesInMs(numberOfFrames: number = 10) { - return this.storedTimeDeltas.slice(-numberOfFrames); + return this.storedTimeDeltas + .slice(-numberOfFrames) + .map((frameDeltaInfo) => frameDeltaInfo.frameDelta); } getQueries() {