mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 08:20:50 +00:00
Cap renderer device-pixel-ratio at 2 (#4339)
## What
Routes every renderer call site that read `window.devicePixelRatio`
through a single `renderDpr()` helper that caps the value at **2**.
```ts
export function renderDpr(): number {
return Math.min(window.devicePixelRatio || 2, 2);
}
```
## Why
On very high-DPI displays (DPR 3, common on phones) the WebGL backing
store was sized at 3× CSS pixels — ~9× the fragment work of 1× — for a
marginal visual gain over 2×. Capping at 2 keeps retina (DPR 2)
pixel-perfect while clamping the 3× case.
## How it stays correct
DPR isn't just the canvas size — it's one coordinate system shared by:
- the canvas backing-store size (`Renderer.resize`)
- the camera's screen↔world math (`Camera.resize` / `screenToWorld` /
`worldToScreen`)
- the camera zoom scale (`ClientGameRunner.syncCamera`)
- the constant-CSS-pixel-size world text (`WorldTextPass`)
These must all use the same DPR value or pointer hit-testing and text
sizing drift. Routing them through one helper guarantees that. The
diagnostics reporter (`Diagnostic.ts`) is intentionally left reading the
real hardware DPR, since its job is to report the actual device.
## Test
- `tsc --noEmit` clean for all touched files (one pre-existing unrelated
`marked` types error remains on `main`).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -72,6 +72,7 @@ import {
|
||||
deepAssign,
|
||||
MapRenderer,
|
||||
preloadAtlasData,
|
||||
renderDpr,
|
||||
type RenderSettings,
|
||||
} from "./render/gl";
|
||||
import { ALL_UNIT_TYPES, UnitState } from "./render/types";
|
||||
@@ -352,7 +353,7 @@ function mountWebGLFrameLoop(
|
||||
|
||||
const syncCamera = (): void => {
|
||||
const scale = transformHandler.scale;
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const dpr = renderDpr();
|
||||
const centerX =
|
||||
transformHandler.offsetX +
|
||||
mapWidth / 2 +
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
* ty = -offsetY * sy
|
||||
*/
|
||||
|
||||
import { renderDpr } from "./utils/Dpr";
|
||||
|
||||
const MIN_ZOOM = 0.2;
|
||||
const MAX_ZOOM = 20;
|
||||
const DBLCLICK_MIN_ZOOM = 0.7;
|
||||
@@ -44,7 +46,7 @@ export class Camera {
|
||||
|
||||
/** Update canvas pixel dimensions. Triggers initial fitMap on first call. */
|
||||
resize(cssWidth: number, cssHeight: number): void {
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const dpr = renderDpr();
|
||||
this.canvasW = Math.round(cssWidth * dpr);
|
||||
this.canvasH = Math.round(cssHeight * dpr);
|
||||
if (this.needsInitialFit) {
|
||||
@@ -163,7 +165,7 @@ export class Camera {
|
||||
|
||||
/** Convert screen pixel position to world coordinates. */
|
||||
screenToWorld(screenX: number, screenY: number): { x: number; y: number } {
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const dpr = renderDpr();
|
||||
const ndcX = ((screenX * dpr) / this.canvasW) * 2 - 1;
|
||||
const ndcY = -(((screenY * dpr) / this.canvasH) * 2 - 1);
|
||||
const sx = (this.zoom * 2) / this.canvasW;
|
||||
@@ -176,7 +178,7 @@ export class Camera {
|
||||
|
||||
/** Convert world coordinates to screen pixel position (CSS pixels). */
|
||||
worldToScreen(worldX: number, worldY: number): { x: number; y: number } {
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const dpr = renderDpr();
|
||||
return {
|
||||
x: (this.zoom * (worldX - this.offsetX)) / dpr + this.canvasW / (2 * dpr),
|
||||
y: (this.zoom * (worldY - this.offsetY)) / dpr + this.canvasH / (2 * dpr),
|
||||
|
||||
@@ -60,6 +60,7 @@ import { WorldTextPass } from "./passes/WorldTextPass";
|
||||
import type { RenderSettings } from "./RenderSettings";
|
||||
import { AffiliationPalette } from "./utils/Affiliation";
|
||||
import { getPaletteSize, hexToRgb } from "./utils/ColorUtils";
|
||||
import { renderDpr } from "./utils/Dpr";
|
||||
import {
|
||||
createTexture2D,
|
||||
toScreen,
|
||||
@@ -569,7 +570,7 @@ export class GPURenderer {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
resize(cssWidth: number, cssHeight: number): void {
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const dpr = renderDpr();
|
||||
this.canvas.width = Math.round(cssWidth * dpr);
|
||||
this.canvas.height = Math.round(cssHeight * dpr);
|
||||
this.camera.resize(cssWidth, cssHeight);
|
||||
|
||||
@@ -11,6 +11,7 @@ export { createRenderSettings, dumpSettings } from "./RenderSettings";
|
||||
export type { RenderSettings } from "./RenderSettings";
|
||||
export { deepAssign, deepDiff } from "./SettingsUtils";
|
||||
export { buildTerrainRGBA, getPaletteSize } from "./utils/ColorUtils";
|
||||
export { renderDpr } from "./utils/Dpr";
|
||||
export { buildNukeTrajectory, samRange } from "./utils/NukeTrajectory";
|
||||
export type { SAMInfo } from "./utils/NukeTrajectory";
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import type { Config } from "../../../../core/configuration/Config";
|
||||
import type { BonusEvent, ConquestFx } from "../../types";
|
||||
import type { RenderSettings } from "../RenderSettings";
|
||||
import { renderDpr } from "../utils/Dpr";
|
||||
import { createProgram } from "../utils/GlUtils";
|
||||
import type { GlyphTables } from "./name-pass/AtlasData";
|
||||
import { buildGlyphTables, parseAtlasData } from "./name-pass/AtlasData";
|
||||
@@ -399,7 +400,7 @@ export class WorldTextPass {
|
||||
let count = 0;
|
||||
// canvasW in Camera is cssWidth*dpr, so `zoom` is device-px-per-world-unit.
|
||||
// Multiply screen-relative scales by dpr to keep a constant CSS-pixel size.
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const dpr = renderDpr();
|
||||
|
||||
for (const popup of this.active) {
|
||||
const elapsed = now - popup.startMs;
|
||||
@@ -541,7 +542,7 @@ export class WorldTextPass {
|
||||
gl.useProgram(this.program);
|
||||
gl.uniformMatrix3fv(this.uCamera, false, cameraMatrix);
|
||||
gl.uniform1f(this.uZoom, zoom);
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const dpr = renderDpr();
|
||||
gl.uniform1f(
|
||||
this.uMinScreenScale,
|
||||
this.settings.bonusPopup.minScreenScale * dpr,
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Device-pixel-ratio used by the WebGL renderer for its backing store and all
|
||||
* screen↔world math. Capped at 2 to avoid rendering at 3x on very high-DPI
|
||||
* (mobile) displays, which costs ~9x the fragment work of 1x for a marginal
|
||||
* visual gain over 2x.
|
||||
*
|
||||
* Every renderer call site that previously read `window.devicePixelRatio`
|
||||
* must go through this so the canvas size, camera math, and text scaling stay
|
||||
* on the same coordinate system.
|
||||
*/
|
||||
export function renderDpr(): number {
|
||||
return Math.min(window.devicePixelRatio || 2, 2);
|
||||
}
|
||||
Reference in New Issue
Block a user