From 013880b486437cc392fb1e5873d19d682a82a2b6 Mon Sep 17 00:00:00 2001 From: RicardoDeZoete Date: Thu, 5 Mar 2026 09:21:08 +0100 Subject: [PATCH] Improve adaptive render resolution for high-DPI displays. Cap effective internal pixel ratio by viewport budget and centralize resolution application across init, resize, and quality updates for smoother fullscreen performance. Made-with: Cursor --- client/src/core/Renderer.ts | 46 +++++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/client/src/core/Renderer.ts b/client/src/core/Renderer.ts index bc742730..ca3e3c8d 100644 --- a/client/src/core/Renderer.ts +++ b/client/src/core/Renderer.ts @@ -41,6 +41,11 @@ import type { NetworkManagerEventPayload } from '../network/NetworkManager'; import { type ClientSettingsEventPayload, ClientSettingsEventType } from '../settings/SettingsManager'; const MISSING_SKYBOX_TEXTURE_PATH = '/textures/missing-skybox'; +// Cap internal render target pixel count to avoid severe fullscreen slowdowns on +// high-DPI displays (e.g. Retina). Windowed mode remains sharper because viewport +// area is smaller and usually falls below this budget. +const MAX_RENDER_TARGET_PIXELS = 2560 * 1440; +const MIN_RENDER_PIXEL_RATIO = 0.5; // Working variables const color = new Color(); @@ -203,6 +208,34 @@ export default class Renderer { public get viewDistance(): number { return Math.min(this._game.settingsManager.qualityPerfTradeoff.viewDistance.distance, this._fogFar); } public get webGLRenderer(): WebGLRenderer { return this._renderer; } + private _getViewportSize(): { width: number; height: number } { + return { + width: Math.max(1, document.documentElement.clientWidth), + height: Math.max(1, document.documentElement.clientHeight), + }; + } + + private _calculateEffectivePixelRatio(): number { + const resolutionMultiplier = this._game.settingsManager.qualityPerfTradeoff.resolution.multiplier; + const requestedPixelRatio = window.devicePixelRatio * resolutionMultiplier; + const { width, height } = this._getViewportSize(); + const viewportPixelCount = width * height; + const maxPixelRatioForBudget = Math.sqrt(MAX_RENDER_TARGET_PIXELS / viewportPixelCount); + + return Math.max( + MIN_RENDER_PIXEL_RATIO, + Math.min(requestedPixelRatio, maxPixelRatioForBudget), + ); + } + + private _applyRenderResolution(): void { + const { width, height } = this._getViewportSize(); + this._renderer.setPixelRatio(this._calculateEffectivePixelRatio()); + this._renderer.setSize(width, height); + this._sceneUiRenderer.setSize(width, height); + this._resizePostProcessing(); + } + private _setupPostProcessing(): void { this._effectComposer.addPass(this._renderPass); this._effectComposer.addPass(this._outlinePass); @@ -400,9 +433,7 @@ export default class Renderer { // document.documentElement.clientHeight are used instead. However, it needs to be // verified whether this solution works correctly on other platforms as well. this._game.camera.onWindowResize(); - this._renderer.setSize(document.documentElement.clientWidth, document.documentElement.clientHeight); - this._sceneUiRenderer.setSize(document.documentElement.clientWidth, document.documentElement.clientHeight); - this._resizePostProcessing(); + this._applyRenderResolution(); } private _onWorldPacket = (payload: NetworkManagerEventPayload.IWorldPacket): void => { @@ -490,13 +521,9 @@ export default class Renderer { } private _onClientSettingsUpdate = (_payload: ClientSettingsEventPayload.IUpdate): void => { - const { resolution } = this._game.settingsManager.qualityPerfTradeoff; - - this._renderer.setPixelRatio(window.devicePixelRatio * resolution.multiplier); - + this._applyRenderResolution(); this._clampTargetFogNearAndFar(); this._setupFog(); - this._resizePostProcessing(); }; private _setupEventListeners(): void { @@ -589,8 +616,7 @@ export default class Renderer { } private _setupRenderer(): void { - this._renderer.setSize(document.documentElement.clientWidth, document.documentElement.clientHeight); - this._renderer.setPixelRatio(window.devicePixelRatio * this._game.settingsManager.qualityPerfTradeoff.resolution.multiplier); + this._applyRenderResolution(); this._renderer.info.autoReset = false; this._renderer.localClippingEnabled = false; // Be explicit about output space; this is cheap and avoids surprises across Three.js versions.