diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 808ca0ed1..9bf59575e 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -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); diff --git a/src/client/render/gl/Renderer.ts b/src/client/render/gl/Renderer.ts index 41110b3ed..5cbbd23b1 100644 --- a/src/client/render/gl/Renderer.ts +++ b/src/client/render/gl/Renderer.ts @@ -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; + // Hit-testing references private lastUnits: Map = new Map(); private lastStructures: Map = new Map(); @@ -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; @@ -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); }