-
Notifications
You must be signed in to change notification settings - Fork 1.1k
webgl perf #4138
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
webgl perf #4138
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,7 @@ | |
| */ | ||
|
|
||
| import type { Config } from "../../../core/configuration/Config"; | ||
| import { getEffectiveDpr } from "../../utilities/Dpr"; | ||
| import type { | ||
| AttackRingInput, | ||
| BonusEvent, | ||
|
|
@@ -158,6 +159,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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. GPU query resources need cleanup in dispose(). The query objects allocated at line 226 should be deleted when the renderer is disposed. Currently, ♻️ Add cleanup for GPU timer queriesIn the 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 |
||
|
|
||
| // Hit-testing references | ||
| private lastUnits: Map<number, UnitState> = new Map(); | ||
| private lastStructures: Map<number, UnitState> = new Map(); | ||
|
|
@@ -208,6 +219,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; | ||
|
|
@@ -522,7 +545,7 @@ export class GPURenderer { | |
| // --------------------------------------------------------------------------- | ||
|
|
||
| resize(cssWidth: number, cssHeight: number): void { | ||
| const dpr = window.devicePixelRatio || 1; | ||
| const dpr = getEffectiveDpr(); | ||
| this.canvas.width = Math.round(cssWidth * dpr); | ||
| this.canvas.height = Math.round(cssHeight * dpr); | ||
| this.camera.resize(cssWidth, cssHeight); | ||
|
|
@@ -1166,9 +1189,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); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| // Cap the effective devicePixelRatio used for the WebGL drawing buffer and | ||
| // any pixel-aware HUD math. Above 1.5×, low-end integrated GPU compositors | ||
| // (notably older Chromebooks) can't swap the drawing buffer within a vsync | ||
| // interval and frame pacing collapses. All callers must agree on this value | ||
| // or camera ↔ screen coordinate math will desync. | ||
| const MAX_RENDER_DPR = 1.5; | ||
|
|
||
| // BISECT: sub-native render scale to test whether fill rate is the bottleneck | ||
| // on low-end Chromebooks. 0.5 = 1/4 the pixels. Visibly softer. | ||
| const RENDER_SCALE = 0.5; | ||
|
|
||
| export function getEffectiveDpr(): number { | ||
| return Math.min(window.devicePixelRatio || 1, MAX_RENDER_DPR) * RENDER_SCALE; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use the rounded backing-store scale for camera math.
resize()roundscanvasW/canvasH, but the conversion methods recompute a fresh DPR. WhencssSize * dprlands on a half-pixel, those values no longer match, soscreenToWorld()andworldToScreen()drift from each other and clicks stop lining up exactly with what was rendered. Cache the actual scale fromresize()and reuse it here. As per coding guidelines,Camera.tsshould handle world↔screen math.♻️ One simple way to keep the math exact
export class Camera { @@ private canvasW = 1; private canvasH = 1; + private scaleX = 1; + private scaleY = 1; @@ resize(cssWidth: number, cssHeight: number): void { const dpr = getEffectiveDpr(); this.canvasW = Math.round(cssWidth * dpr); this.canvasH = Math.round(cssHeight * dpr); + this.scaleX = cssWidth > 0 ? this.canvasW / cssWidth : 1; + this.scaleY = cssHeight > 0 ? this.canvasH / cssHeight : 1; if (this.needsInitialFit) { this.fitMap(); } @@ screenToWorld(screenX: number, screenY: number): { x: number; y: number } { - const dpr = getEffectiveDpr(); - const ndcX = ((screenX * dpr) / this.canvasW) * 2 - 1; - const ndcY = -(((screenY * dpr) / this.canvasH) * 2 - 1); + const ndcX = ((screenX * this.scaleX) / this.canvasW) * 2 - 1; + const ndcY = -(((screenY * this.scaleY) / this.canvasH) * 2 - 1); @@ worldToScreen(worldX: number, worldY: number): { x: number; y: number } { - const dpr = getEffectiveDpr(); return { - x: (this.zoom * (worldX - this.offsetX)) / dpr + this.canvasW / (2 * dpr), - y: (this.zoom * (worldY - this.offsetY)) / dpr + this.canvasH / (2 * dpr), + x: + (this.zoom * (worldX - this.offsetX)) / this.scaleX + + this.canvasW / (2 * this.scaleX), + y: + (this.zoom * (worldY - this.offsetY)) / this.scaleY + + this.canvasH / (2 * this.scaleY), }; }Also applies to: 167-184
🤖 Prompt for AI Agents