Skip to content

Commit

Permalink
fix(framerate tracking): better handling of query failures (#652)
Browse files Browse the repository at this point in the history
* fix: correct deletion of old queries

* feat(framerate): framerate fixes when application errors occur

* feat: store the frame number with the delta

* refactor: easier management of the query pool
  • Loading branch information
seankmartin authored Oct 22, 2024
1 parent 41ac40a commit c110447
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 29 deletions.
4 changes: 2 additions & 2 deletions src/display_context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
}

Expand Down
112 changes: 85 additions & 27 deletions src/util/framerate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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() {
Expand Down

0 comments on commit c110447

Please sign in to comment.