openvibers/openvibe.games Commits

Games M6: the low quality tier loads textures at most 512 px (a quarter of the GPU memory on weak phones) (#69)

OP OpenVibers committed 30 days ago · d782f6c088 · 2 files, +42 −4

M6 on phones: the low quality tier loads textures at most 512 px on a side.

Babylon's WebGL loader scales any image above `caps.maxTextureSize` down as it uploads it. `applyEngineQuality` now sets that cap from the tier: 512 at low, the device's own limit otherwise. The device limit is read once per engine, so moving back to a higher tier restores it. The 1024 px terrain textures (11 of them, about 5.6 MB of GPU memory each with mipmaps) take a quarter of that on the devices the low tier is for: phones with little memory.

Textures already loaded keep their size, so a tier change applies to later loads. WebGPU's loader does not read the cap; WebGPU devices are rarely on the low tier.

Tests: `quality.test.ts` covers the low tier's cap, restoring the device limit at high, and never raising a device's smaller limit (27/27). Typecheck and eslint are clean. Browser: the play page on the forced low tier (`openvibe.quality=low`, SwiftShader) rendered with no page errors, and the hotbar icon was drawn.

Changes

ddiff --git a/apps/client/src/render/quality.test.ts b/apps/client/src/render/quality.test.ts
iindex a2c178c..0cab3e2 100644
---- a/apps/client/src/render/quality.test.ts
++++ b/apps/client/src/render/quality.test.ts
@@@ -213,7 +213,12 @@ describe('qualityForTier', () => {
it('turns antialiasing off below high and shadows off at low', () => {
expect(qualityForTier('high')).toMatchObject({ antialias: true, shadows: true })
expect(qualityForTier('medium')).toMatchObject({ antialias: false, shadows: true })
-- expect(qualityForTier('low')).toMatchObject({ antialias: false, shadows: false })
++ expect(qualityForTier('low')).toMatchObject({
++ antialias: false,
++ shadows: false,
++ maxTextureSize: 512,
++ })
++ expect(qualityForTier('high').maxTextureSize).toBeNull()
})
})
@@@ -249,6 +254,25 @@ describe('applying settings', () => {
expect(setHardwareScalingLevel).toHaveBeenCalledWith(1.25)
})
++ it('caps texture size at the low tier and gives the device limit back above it', () => {
++ const caps = { maxTextureSize: 4096 }
++ const engine = {
++ setHardwareScalingLevel: vi.fn(),
++ getCaps: () => caps,
++ } as unknown as AbstractEngine
++ applyEngineQuality(engine, qualityForTier('low'))
++ expect(caps.maxTextureSize).toBe(512)
++ applyEngineQuality(engine, qualityForTier('high'))
++ expect(caps.maxTextureSize).toBe(4096)
++ const small = { maxTextureSize: 256 }
++ const weak = {
++ setHardwareScalingLevel: vi.fn(),
++ getCaps: () => small,
++ } as unknown as AbstractEngine
++ applyEngineQuality(weak, qualityForTier('low'))
++ expect(small.maxTextureSize).toBe(256)
++ })
++
it('toggles the scene shadow switch', () => {
const scene = { shadowsEnabled: true }
applySceneQuality(scene, qualityForTier('low'))
ddiff --git a/apps/client/src/render/quality.ts b/apps/client/src/render/quality.ts
iindex ee7fd37..98867b5 100644
---- a/apps/client/src/render/quality.ts
++++ b/apps/client/src/render/quality.ts
@@@ -33,6 +33,12 @@ export interface QualitySettings {
hardwareScalingLevel: number
antialias: boolean
shadows: boolean
++ /**
++ * The largest texture side the tier loads (null = whatever the GPU allows). WebGL's loader scales a bigger image
++ * down as it uploads it, so the low tier's 1024 px terrain textures take a quarter of the memory. Textures already
++ * loaded keep their size; WebGPU's loader ignores the cap.
++ */
++ maxTextureSize: number | null
}
/**
@@@ -41,9 +47,9 @@ export interface QualitySettings {
* scene's authored lighting) but drops AA and renders at 80% resolution.
*/
const QUALITY_SETTINGS: Record<QualityTier, Omit<QualitySettings, 'tier'>> = {
-- high: { hardwareScalingLevel: 1, antialias: true, shadows: true },
-- medium: { hardwareScalingLevel: 1.25, antialias: false, shadows: true },
-- low: { hardwareScalingLevel: 1.5, antialias: false, shadows: false },
++ high: { hardwareScalingLevel: 1, antialias: true, shadows: true, maxTextureSize: null },
++ medium: { hardwareScalingLevel: 1.25, antialias: false, shadows: true, maxTextureSize: null },
++ low: { hardwareScalingLevel: 1.5, antialias: false, shadows: false, maxTextureSize: 512 },
}
/** The full renderer settings for a tier. Pure — safe to unit-test. */
@@@ -167,8 +173,16 @@ export function resolveStartupQuality(
*/
export function applyEngineQuality(engine: AbstractEngine, settings: QualitySettings): void {
engine.setHardwareScalingLevel(settings.hardwareScalingLevel)
++ const caps = engine.getCaps?.()
++ if (!caps) return
++ // The device's own limit, read once: a later tier change can raise the cap back to it.
++ if (!deviceTextureLimit.has(engine)) deviceTextureLimit.set(engine, caps.maxTextureSize)
++ const device = deviceTextureLimit.get(engine)!
++ caps.maxTextureSize = settings.maxTextureSize ? Math.min(device, settings.maxTextureSize) : device
}
++const deviceTextureLimit = new WeakMap<AbstractEngine, number>()
++
/** Shadow maps are a Scene concern, not an engine one. */
export function applySceneQuality(
scene: { shadowsEnabled: boolean },