Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/client/ClientGameRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,8 +380,15 @@ function mountWebGLFrameLoop(
// TransformHandler, pushes it to WebGL, and synchronously invokes the
// renderer's captured frame callback (which draws). One RAF = one
// synchronized camera-update + WebGL render.
const driveFrame = (): void => {
let lastRafTime = performance.now();
const driveFrame = (now: number): void => {
const dt = now - lastRafTime;
lastRafTime = now;
if (dt > 20) console.log(`[rAF gap] ${dt.toFixed(1)}ms`);
const cpuStart = performance.now();
syncCamera();
const cpuMs = performance.now() - cpuStart;
if (cpuMs > 10) console.log(`[CPU frame] ${cpuMs.toFixed(1)}ms`);
requestAnimationFrame(driveFrame);
};
requestAnimationFrame(driveFrame);
Expand Down
54 changes: 54 additions & 0 deletions src/client/render/gl/Renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,16 @@ export class GPURenderer {
onFrame: ((ms: number) => void) | null = null;
afterRender: ((canvas: HTMLCanvasElement) => void) | null = null;

// Deferred GPU timer
private gpuTimerExt: {
TIME_ELAPSED_EXT: number;
GPU_DISJOINT_EXT: number;
} | null = null;
private gpuQueries: (WebGLQuery | null)[] = [];
private gpuQueryIssued: boolean[] = [];
private gpuQueryIdx = 0;
private static readonly GPU_QUERY_DEPTH = 3;
Comment on lines +162 to +170
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

GPU query resources need cleanup in dispose().

The query objects allocated at line 226 should be deleted when the renderer is disposed. Currently, dispose() (line 1370) doesn't call gl.deleteQuery() for the timer queries, creating a small resource leak.

♻️ Add cleanup for GPU timer queries

In the dispose() method around line 1410, add:

     this.gl.deleteFramebuffer(this.sceneTarget.fbo);
     this.gl.deleteTexture(this.sceneTarget.tex);
+    for (const q of this.gpuQueries) {
+      if (q) this.gl.deleteQuery(q);
+    }
     this.lastUnits = new Map();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/render/gl/Renderer.ts` around lines 161 - 169, dispose() currently
doesn't free the WebGLQuery objects stored for GPU timing (gpuQueries /
gpuQueryIssued / gpuQueryIdx / gpuTimerExt), causing a resource leak; update the
Renderer.dispose() method to loop over this.gpuQueries (up to
Renderer.GPU_QUERY_DEPTH), call this.gl.deleteQuery(query) for each non-null
entry, clear/reset the arrays (gpuQueries = [], gpuQueryIssued = [], gpuQueryIdx
= 0) and set gpuTimerExt = null so all GPU timer resources are released.


// Hit-testing references
private lastUnits: Map<number, UnitState> = new Map();
private lastStructures: Map<number, UnitState> = new Map();
Expand Down Expand Up @@ -208,6 +218,18 @@ export class GPURenderer {
if (!floatExt)
console.warn("EXT_color_buffer_float not available — palette may fail");

this.gpuTimerExt = gl.getExtension(
"EXT_disjoint_timer_query_webgl2",
) as typeof this.gpuTimerExt;
if (this.gpuTimerExt) {
for (let i = 0; i < GPURenderer.GPU_QUERY_DEPTH; i++) {
this.gpuQueries.push(gl.createQuery());
this.gpuQueryIssued.push(false);
}
} else {
console.warn("EXT_disjoint_timer_query_webgl2 not available");
}

const mapW = header.mapWidth;
const mapH = header.mapHeight;
this.mapW = mapW;
Expand Down Expand Up @@ -1166,9 +1188,41 @@ export class GPURenderer {
draw(): void {
const now = performance.now();
this.trackFps(now);

// Deferred GPU timer: read result issued GPU_QUERY_DEPTH frames ago
// (by now the GPU has finished it, so the read is non-blocking).
const gl = this.gl;
const ext = this.gpuTimerExt;
const slot = this.gpuQueryIdx;
if (ext) {
const prevQuery = this.gpuQueries[slot];
if (prevQuery && this.gpuQueryIssued[slot]) {
const available = gl.getQueryParameter(
prevQuery,
gl.QUERY_RESULT_AVAILABLE,
);
const disjoint = gl.getParameter(ext.GPU_DISJOINT_EXT);
if (available && !disjoint) {
const ns = gl.getQueryParameter(prevQuery, gl.QUERY_RESULT) as number;
const ms = ns / 1e6;
if (ms > 12) console.log(`[GPU] ${ms.toFixed(1)}ms`);
}
}
if (prevQuery) {
gl.beginQuery(ext.TIME_ELAPSED_EXT, prevQuery);
this.gpuQueryIssued[slot] = true;
}
}

this.uploadTextures();
this.computeTextures();
this.renderFrame();

if (ext && this.gpuQueries[slot]) {
gl.endQuery(ext.TIME_ELAPSED_EXT);
this.gpuQueryIdx = (slot + 1) % GPURenderer.GPU_QUERY_DEPTH;
}

if (this.onFrame) this.onFrame(performance.now() - now);
if (this.afterRender) this.afterRender(this.canvas);
}
Expand Down
Loading