diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index ed7c271f8..3de721c35 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -236,7 +236,7 @@ function mountWebGLDebugRenderer( transformHandler: import("./graphics/TransformHandler").TransformHandler, gameView: GameView, eventBus: EventBus, -): { builder: WebGLFrameBuilder; syncCamera: () => void } { +): { builder: WebGLFrameBuilder } { const gameMap = terrainMap.gameMap; const mapWidth = gameMap.width(); const mapHeight = gameMap.height(); @@ -368,7 +368,17 @@ function mountWebGLDebugRenderer( view.setSelectedUnits(e.unit ? [e.unit.id()] : []); }); - return { builder: new WebGLFrameBuilder(view), syncCamera }; + // Self-driving RAF: syncCamera reads the latest camera state from + // 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 => { + syncCamera(); + requestAnimationFrame(driveFrame); + }; + requestAnimationFrame(driveFrame); + + return { builder: new WebGLFrameBuilder(view) }; } async function createClientGame( @@ -412,23 +422,34 @@ async function createClientGame( lobbyConfig.gameStartInfo.players, ); - const canvas = createCanvas(); + // Transparent fullscreen overlay used purely as the pointer-event / + // bounding-rect target for InputHandler + TransformHandler. The actual + // map drawing happens on the WebGL canvas created in mountWebGLDebugRenderer. + const inputOverlay = document.createElement("div"); + inputOverlay.id = "game-input-overlay"; + inputOverlay.style.position = "fixed"; + inputOverlay.style.left = "0"; + inputOverlay.style.top = "0"; + inputOverlay.style.width = "100%"; + inputOverlay.style.height = "100%"; + inputOverlay.style.touchAction = "none"; + document.body.appendChild(inputOverlay); + const soundManager = new SoundManager(eventBus, userSettings); try { const gameRenderer = createRenderer( - canvas, + inputOverlay, gameView, eventBus, lobbyConfig.playerRole, ); - const { builder: webglBuilder, syncCamera } = mountWebGLDebugRenderer( + const { builder: webglBuilder } = mountWebGLDebugRenderer( gameMap, gameRenderer.transformHandler, gameView, eventBus, ); - gameRenderer.onPreRender = syncCamera; console.log( `creating private game got difficulty: ${lobbyConfig.gameStartInfo.config.difficulty}`, @@ -439,7 +460,7 @@ async function createClientGame( clientID, eventBus, gameRenderer, - new InputHandler(gameView, gameRenderer.uiState, canvas, eventBus), + new InputHandler(gameView, gameRenderer.uiState, inputOverlay, eventBus), transport, worker, gameView, diff --git a/src/client/InputHandler.ts b/src/client/InputHandler.ts index a26ae5470..8a30e150e 100644 --- a/src/client/InputHandler.ts +++ b/src/client/InputHandler.ts @@ -246,7 +246,7 @@ export class InputHandler { constructor( private gameView: GameView, public uiState: UIState, - private canvas: HTMLCanvasElement, + private canvas: HTMLElement, private eventBus: EventBus, ) {} diff --git a/src/client/graphics/GameRenderer.ts b/src/client/graphics/GameRenderer.ts index 4580d4cbe..f3e3f2a3b 100644 --- a/src/client/graphics/GameRenderer.ts +++ b/src/client/graphics/GameRenderer.ts @@ -37,12 +37,12 @@ import { UnitDisplay } from "./layers/UnitDisplay"; import { WinModal } from "./layers/WinModal"; export function createRenderer( - canvas: HTMLCanvasElement, + inputEl: HTMLElement, game: GameView, eventBus: EventBus, playerRole: string | null, ): GameRenderer { - const transformHandler = new TransformHandler(game, eventBus, canvas); + const transformHandler = new TransformHandler(game, eventBus, inputEl); const userSettings = new UserSettings(); const uiState: UIState = { @@ -298,9 +298,7 @@ export function createRenderer( ]; return new GameRenderer( - game, eventBus, - canvas, transformHandler, uiState, layers, @@ -309,56 +307,26 @@ export function createRenderer( } export class GameRenderer { - private context: CanvasRenderingContext2D; private layerTickState = new Map(); - private renderFramesSinceLastTick: number = 0; - private renderLayerDurationsSinceLastTick: Record = {}; - public onPreRender: (() => void) | null = null; constructor( - private game: GameView, private eventBus: EventBus, - private canvas: HTMLCanvasElement, public transformHandler: TransformHandler, public uiState: UIState, private layers: Layer[], private performanceOverlay: PerformanceOverlay, - ) { - const context = canvas.getContext("2d", { alpha: true }); - if (context === null) throw new Error("2d context not supported"); - this.context = context; - } + ) {} initialize() { this.eventBus.on(RedrawGraphicsEvent, () => this.redraw()); this.layers.forEach((l) => l.init?.()); - // only append the canvas if it's not already in the document to avoid reparenting side-effects - if (!document.body.contains(this.canvas)) { - document.body.appendChild(this.canvas); - } - - window.addEventListener("resize", () => this.resizeCanvas()); - this.resizeCanvas(); + window.addEventListener("resize", () => + this.transformHandler.updateCanvasBoundingRect(), + ); //show whole map on startup this.transformHandler.centerAll(0.9); - - let rafId = requestAnimationFrame(() => this.renderGame()); - this.canvas.addEventListener("contextlost", () => { - cancelAnimationFrame(rafId); - }); - this.canvas.addEventListener("contextrestored", () => { - this.redraw(); - rafId = requestAnimationFrame(() => this.renderGame()); - }); - } - - resizeCanvas() { - this.canvas.width = window.innerWidth; - this.canvas.height = window.innerHeight; - this.transformHandler.updateCanvasBoundingRect(); - //this.redraw() } redraw() { @@ -369,86 +337,10 @@ export class GameRenderer { }); } - renderGame() { - const shouldProfileFrame = FrameProfiler.isEnabled(); - if (shouldProfileFrame) { - FrameProfiler.clear(); - } - const start = performance.now(); - this.onPreRender?.(); - this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); - - const handleTransformState = ( - needsTransform: boolean, - active: boolean, - ): boolean => { - if (needsTransform && !active) { - this.context.save(); - this.transformHandler.handleTransform(this.context); - return true; - } else if (!needsTransform && active) { - this.context.restore(); - return false; - } - return active; - }; - - let isTransformActive = false; - - for (const layer of this.layers) { - const needsTransform = layer.shouldTransform?.() ?? false; - isTransformActive = handleTransformState( - needsTransform, - isTransformActive, - ); - - if (shouldProfileFrame) { - const layerStart = FrameProfiler.start(); - layer.renderLayer?.(this.context); - FrameProfiler.end( - layer.constructor?.name ?? "UnknownLayer", - layerStart, - ); - } else { - layer.renderLayer?.(this.context); - } - } - handleTransformState(false, isTransformActive); // Ensure context is clean after rendering - this.transformHandler.resetChanged(); - - requestAnimationFrame(() => this.renderGame()); - const duration = performance.now() - start; - - if (shouldProfileFrame) { - const layerDurations = FrameProfiler.consume(); - this.renderFramesSinceLastTick++; - for (const [name, ms] of Object.entries(layerDurations)) { - this.renderLayerDurationsSinceLastTick[name] = - (this.renderLayerDurationsSinceLastTick[name] ?? 0) + ms; - } - this.performanceOverlay.updateFrameMetrics(duration, layerDurations); - } - - if (duration > 50) { - console.warn( - `tick ${this.game.ticks()} took ${duration}ms to render frame`, - ); - } - } - tick() { const nowMs = performance.now(); const shouldProfileTick = FrameProfiler.isEnabled(); - if (shouldProfileTick) { - this.performanceOverlay.updateRenderPerTickMetrics( - this.renderFramesSinceLastTick, - this.renderLayerDurationsSinceLastTick, - ); - this.renderFramesSinceLastTick = 0; - this.renderLayerDurationsSinceLastTick = {}; - } - const tickLayerDurations: Record = {}; for (const layer of this.layers) { @@ -482,9 +374,4 @@ export class GameRenderer { this.performanceOverlay.updateTickLayerMetrics(tickLayerDurations); } } - - resize(width: number, height: number): void { - this.canvas.width = Math.ceil(width / window.devicePixelRatio); - this.canvas.height = Math.ceil(height / window.devicePixelRatio); - } } diff --git a/src/client/graphics/TransformHandler.ts b/src/client/graphics/TransformHandler.ts index 94e9535c9..5e69a6901 100644 --- a/src/client/graphics/TransformHandler.ts +++ b/src/client/graphics/TransformHandler.ts @@ -40,7 +40,7 @@ export class TransformHandler { constructor( private game: GameView, private eventBus: EventBus, - private canvas: HTMLCanvasElement, + private canvas: HTMLElement, ) { this._boundingRect = this.canvas.getBoundingClientRect(); this.eventBus.on(ZoomEvent, (e) => this.onZoom(e));