diff --git a/src/client/graphics/GameRenderer.ts b/src/client/graphics/GameRenderer.ts index 4b000e8a9..8e336b311 100644 --- a/src/client/graphics/GameRenderer.ts +++ b/src/client/graphics/GameRenderer.ts @@ -372,7 +372,7 @@ export class GameRenderer { if (this.backlogTurns > 0) { const BASE_FPS = 60; - const MIN_FPS = 10; + const MIN_FPS = 30; const BACKLOG_MAX_TURNS = 50; const scale = Math.min(1, this.backlogTurns / BACKLOG_MAX_TURNS); @@ -426,8 +426,11 @@ export class GameRenderer { const layerStart = FrameProfiler.start(); layer.renderLayer?.(this.context); - const name = layer.constructor?.name ?? "UnknownLayer"; - FrameProfiler.end(name, layerStart); + const profileName = + (layer as any).profileName?.() ?? + layer.constructor?.name ?? + "UnknownLayer"; + FrameProfiler.end(profileName, layerStart); } handleTransformState(false, isTransformActive); // Ensure context is clean after rendering this.transformHandler.resetChanged(); diff --git a/src/client/graphics/layers/Layer.ts b/src/client/graphics/layers/Layer.ts index 239937435..16bd67f50 100644 --- a/src/client/graphics/layers/Layer.ts +++ b/src/client/graphics/layers/Layer.ts @@ -4,4 +4,5 @@ export interface Layer { renderLayer?: (context: CanvasRenderingContext2D) => void; shouldTransform?: () => boolean; redraw?: () => void; + profileName?: () => string; } diff --git a/src/client/graphics/layers/TerritoryLayer.ts b/src/client/graphics/layers/TerritoryLayer.ts index 9de266173..7b7140f41 100644 --- a/src/client/graphics/layers/TerritoryLayer.ts +++ b/src/client/graphics/layers/TerritoryLayer.ts @@ -33,6 +33,10 @@ import { import { TerritoryWebGLRenderer } from "./TerritoryWebGLRenderer"; export class TerritoryLayer implements Layer { + profileName(): string { + return "TerritoryLayer:renderLayer"; + } + private userSettings: UserSettings; private borderAnimTime = 0; @@ -92,6 +96,7 @@ export class TerritoryLayer implements Layer { } tick() { + const tickProfile = FrameProfiler.start(); if (this.game.inSpawnPhase()) { this.spawnHighlight(); } @@ -188,6 +193,7 @@ export class TerritoryLayer implements Layer { if (currentMyPlayer !== this.lastMyPlayerSmallId) { this.redraw(); } + FrameProfiler.end("TerritoryLayer:tick", tickProfile); } private spawnHighlight() { @@ -537,10 +543,17 @@ export class TerritoryLayer implements Layer { return; } let numToRender = Math.floor(this.tileToRenderQueue.size() / 10); - if (numToRender === 0 || this.game.inSpawnPhase()) { + if ( + numToRender === 0 || + this.game.inSpawnPhase() || + this.territoryRenderer.isWebGL() + ) { numToRender = this.tileToRenderQueue.size(); } + const useNeighborPaint = !(this.territoryRenderer?.isWebGL() ?? false); + const neighborsToPaint: TileRef[] = []; + const mainSpan = FrameProfiler.start(); while (numToRender > 0) { numToRender--; @@ -551,9 +564,24 @@ export class TerritoryLayer implements Layer { const tile = entry.tile; this.paintTerritory(tile); - for (const neighbor of this.game.neighbors(tile)) { + + if (useNeighborPaint) { + for (const neighbor of this.game.neighbors(tile)) { + neighborsToPaint.push(neighbor); + } + } + } + FrameProfiler.end("TerritoryLayer:renderTerritory.mainPaint", mainSpan); + + if (useNeighborPaint && neighborsToPaint.length > 0) { + const neighborSpan = FrameProfiler.start(); + for (const neighbor of neighborsToPaint) { this.paintTerritory(neighbor, true); //this is a misuse of the _Border parameter, making it a maybe stale border } + FrameProfiler.end( + "TerritoryLayer:renderTerritory.neighborPaint", + neighborSpan, + ); } } diff --git a/src/client/graphics/layers/TerritoryRenderers.ts b/src/client/graphics/layers/TerritoryRenderers.ts index 99276b3a4..5626d3059 100644 --- a/src/client/graphics/layers/TerritoryRenderers.ts +++ b/src/client/graphics/layers/TerritoryRenderers.ts @@ -110,7 +110,7 @@ export class CanvasTerritoryRenderer implements TerritoryRendererStrategy { } else { this.clearTile(tile); } - FrameProfiler.end("TerritoryLayer:paintTerritory.cpu", cpuStart); + FrameProfiler.end("CanvasTerritoryRenderer:paintTile", cpuStart); return; } @@ -142,7 +142,7 @@ export class CanvasTerritoryRenderer implements TerritoryRendererStrategy { 150, ); } - FrameProfiler.end("TerritoryLayer:paintTerritory.cpu", cpuStart); + FrameProfiler.end("CanvasTerritoryRenderer:paintTile", cpuStart); } render( @@ -165,7 +165,7 @@ export class CanvasTerritoryRenderer implements TerritoryRendererStrategy { width, height, ); - FrameProfiler.end("TerritoryLayer:putImageData", putImageStart); + FrameProfiler.end("CanvasTerritoryRenderer:putImageData", putImageStart); } const drawCanvasStart = FrameProfiler.start(); @@ -176,7 +176,7 @@ export class CanvasTerritoryRenderer implements TerritoryRendererStrategy { this.game.width(), this.game.height(), ); - FrameProfiler.end("TerritoryLayer:drawCanvas", drawCanvasStart); + FrameProfiler.end("CanvasTerritoryRenderer:drawCanvas", drawCanvasStart); } setAlternativeView(enabled: boolean): void { @@ -304,7 +304,7 @@ export class WebglTerritoryRenderer implements TerritoryRendererStrategy { ): void { const webglRenderStart = FrameProfiler.start(); this.renderer.render(); - FrameProfiler.end("TerritoryLayer:territoryWebGL.render", webglRenderStart); + FrameProfiler.end("WebglTerritoryRenderer:render", webglRenderStart); const drawCanvasStart = FrameProfiler.start(); context.drawImage( @@ -314,10 +314,7 @@ export class WebglTerritoryRenderer implements TerritoryRendererStrategy { this.game.width(), this.game.height(), ); - FrameProfiler.end( - "TerritoryLayer:territoryWebGL.drawImage", - drawCanvasStart, - ); + FrameProfiler.end("WebglTerritoryRenderer:drawImage", drawCanvasStart); } setAlternativeView(enabled: boolean): void { diff --git a/src/client/graphics/layers/TerritoryWebGLRenderer.ts b/src/client/graphics/layers/TerritoryWebGLRenderer.ts index 691c6d806..91a967660 100644 --- a/src/client/graphics/layers/TerritoryWebGLRenderer.ts +++ b/src/client/graphics/layers/TerritoryWebGLRenderer.ts @@ -481,10 +481,13 @@ export class TerritoryWebGLRenderer { } const gl = this.gl; - const uploadSpan = FrameProfiler.start(); + const uploadStateSpan = FrameProfiler.start(); this.uploadStateTexture(); + FrameProfiler.end("TerritoryWebGLRenderer:uploadState", uploadStateSpan); + + const uploadBorderSpan = FrameProfiler.start(); this.uploadBorderTexture(); - FrameProfiler.end("TerritoryWebGLRenderer:uploadState", uploadSpan); + FrameProfiler.end("TerritoryWebGLRenderer:uploadBorder", uploadBorderSpan); const renderSpan = FrameProfiler.start(); gl.viewport(0, 0, this.canvas.width, this.canvas.height); @@ -524,12 +527,16 @@ export class TerritoryWebGLRenderer { FrameProfiler.end("TerritoryWebGLRenderer:draw", renderSpan); } - private uploadStateTexture() { - if (!this.gl || !this.stateTexture) return; + private uploadStateTexture(): { rows: number; bytes: number } { + if (!this.gl || !this.stateTexture) return { rows: 0, bytes: 0 }; const gl = this.gl; gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, this.stateTexture); + const bytesPerPixel = Uint16Array.BYTES_PER_ELEMENT; + let rowsUploaded = 0; + let bytesUploaded = 0; + if (this.needsFullUpload) { gl.texImage2D( gl.TEXTURE_2D, @@ -544,11 +551,13 @@ export class TerritoryWebGLRenderer { ); this.needsFullUpload = false; this.dirtyRows.clear(); - return; + rowsUploaded = this.canvas.height; + bytesUploaded = this.canvas.width * this.canvas.height * bytesPerPixel; + return { rows: rowsUploaded, bytes: bytesUploaded }; } if (this.dirtyRows.size === 0) { - return; + return { rows: 0, bytes: 0 }; } for (const [y, span] of this.dirtyRows) { @@ -566,16 +575,23 @@ export class TerritoryWebGLRenderer { gl.UNSIGNED_SHORT, rowSlice, ); + rowsUploaded++; + bytesUploaded += width * bytesPerPixel; } this.dirtyRows.clear(); + return { rows: rowsUploaded, bytes: bytesUploaded }; } - private uploadBorderTexture() { - if (!this.gl || !this.borderColorTexture) return; + private uploadBorderTexture(): { rows: number; bytes: number } { + if (!this.gl || !this.borderColorTexture) return { rows: 0, bytes: 0 }; const gl = this.gl; gl.activeTexture(gl.TEXTURE3); gl.bindTexture(gl.TEXTURE_2D, this.borderColorTexture); + const bytesPerPixel = Uint8Array.BYTES_PER_ELEMENT * 4; // RGBA8 + let rowsUploaded = 0; + let bytesUploaded = 0; + if (this.borderNeedsFullUpload) { gl.texImage2D( gl.TEXTURE_2D, @@ -590,11 +606,13 @@ export class TerritoryWebGLRenderer { ); this.borderNeedsFullUpload = false; this.borderDirtyRows.clear(); - return; + rowsUploaded = this.canvas.height; + bytesUploaded = this.canvas.width * this.canvas.height * bytesPerPixel; + return { rows: rowsUploaded, bytes: bytesUploaded }; } if (this.borderDirtyRows.size === 0) { - return; + return { rows: 0, bytes: 0 }; } for (const [y, span] of this.borderDirtyRows) { @@ -615,8 +633,27 @@ export class TerritoryWebGLRenderer { gl.UNSIGNED_BYTE, rowSlice, ); + rowsUploaded++; + bytesUploaded += width * bytesPerPixel; } this.borderDirtyRows.clear(); + return { rows: rowsUploaded, bytes: bytesUploaded }; + } + + private labelUpload( + base: string, + metrics: { rows: number; bytes: number }, + ): string { + if (metrics.rows === 0 || metrics.bytes === 0) { + return `${base} (skip)`; + } + const rowBucket = + metrics.rows >= this.canvas.height + ? "full" + : `${Math.ceil(metrics.rows / 50) * 50}`; + const kb = Math.max(1, Math.round(metrics.bytes / 1024)); + const kbBucket = kb > 1024 ? `${Math.round(kb / 1024)}MB` : `${kb}KB`; + return `${base} rows:${rowBucket} bytes:${kbBucket}`; } private uploadPalette() {