From c87de3e8f9d65626e1cde3ce57ae7b8f51130abc Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Mon, 3 Jun 2024 16:06:32 -0700 Subject: [PATCH] IR-1439-Scene-Render-Quality-Dropping-Unexpectedly-with-Automatic-Render-Settings (#10246) * Start of reworking render quality change parameters * wip * Smoothing fix * Single source of truth for quality tier, csm update frustrum quality flag * Override performance tier with user settings, render scale as performance setting * Store any value that can be serialized * Accumulator instead of timeout * Reactive cameraOcclusion in model component * Measure logic behind automatic flag * Better tracking of GPU status * enabled flag * remove unused variables * remove unused variable --------- Co-authored-by: HexaField --- packages/client-core/i18n/en/user.json | 2 +- .../components/UserMenu/menus/SettingMenu.tsx | 4 +- .../src/scene/components/ModelComponent.tsx | 17 +- .../hyperflux/functions/StateFunctions.ts | 6 +- .../src/renderer/PerformanceState.test.tsx | 14 +- .../spatial/src/renderer/PerformanceState.ts | 254 ++++++++++++------ .../spatial/src/renderer/RendererState.ts | 3 + .../src/renderer/WebGLRendererSystem.tsx | 68 ++--- .../spatial/src/resources/ResourceState.ts | 33 ++- 9 files changed, 239 insertions(+), 162 deletions(-) diff --git a/packages/client-core/i18n/en/user.json b/packages/client-core/i18n/en/user.json index 45392f3125..2b4480edc3 100755 --- a/packages/client-core/i18n/en/user.json +++ b/packages/client-core/i18n/en/user.json @@ -163,7 +163,7 @@ "lbl-left-control-scheme": "Left Control Scheme", "lbl-right-control-scheme": "Right Control Scheme", "lbl-preferred-hand": "Preferred Hand", - "lbl-resolution": "Resolution", + "lbl-quality": "Quality Preset", "lbl-shadow": "Shadows", "lbl-pp": "Post Processing", "lbl-pbr": "Full PBR", diff --git a/packages/client-core/src/user/components/UserMenu/menus/SettingMenu.tsx b/packages/client-core/src/user/components/UserMenu/menus/SettingMenu.tsx index 31c1c2ea3b..0cf3e59959 100755 --- a/packages/client-core/src/user/components/UserMenu/menus/SettingMenu.tsx +++ b/packages/client-core/src/user/components/UserMenu/menus/SettingMenu.tsx @@ -455,9 +455,9 @@ const SettingMenu = ({ isPopover }: Props): JSX.Element => { <> } - label={t('user:usermenu.setting.lbl-resolution')} + label={t('user:usermenu.setting.lbl-quality')} max={5} - min={1} + min={0} step={1} value={rendererState.qualityLevel.value} sx={{ mt: 4 }} diff --git a/packages/engine/src/scene/components/ModelComponent.tsx b/packages/engine/src/scene/components/ModelComponent.tsx index eb36ed548e..14f3147f78 100644 --- a/packages/engine/src/scene/components/ModelComponent.tsx +++ b/packages/engine/src/scene/components/ModelComponent.tsx @@ -41,10 +41,7 @@ import { CameraComponent } from '@etherealengine/spatial/src/camera/components/C import { RendererComponent } from '@etherealengine/spatial/src/renderer/WebGLRendererSystem' import { GroupComponent, addObjectToGroup } from '@etherealengine/spatial/src/renderer/components/GroupComponent' import { MeshComponent } from '@etherealengine/spatial/src/renderer/components/MeshComponent' -import { - ObjectLayerComponents, - ObjectLayerMaskComponent -} from '@etherealengine/spatial/src/renderer/components/ObjectLayerComponent' +import { ObjectLayerMaskComponent } from '@etherealengine/spatial/src/renderer/components/ObjectLayerComponent' import { ObjectLayers } from '@etherealengine/spatial/src/renderer/constants/ObjectLayers' import { EntityTreeComponent, @@ -118,14 +115,10 @@ function ModelReactor() { const [gltf, error] = useGLTF(modelComponent.src.value, entity) useEffect(() => { - const occlusion = - !!modelComponent?.cameraOcclusion?.value || hasComponent(entity, ObjectLayerComponents[ObjectLayers.Camera]) - if (!occlusion) return - ObjectLayerMaskComponent.enableLayer(entity, ObjectLayers.Camera) - return () => { - ObjectLayerMaskComponent.disableLayer(entity, ObjectLayers.Camera) - } - }, [modelComponent?.cameraOcclusion?.value]) + const occlusion = modelComponent.cameraOcclusion.value + if (!occlusion) ObjectLayerMaskComponent.disableLayer(entity, ObjectLayers.Camera) + else ObjectLayerMaskComponent.enableLayer(entity, ObjectLayers.Camera) + }, [modelComponent.cameraOcclusion]) useEffect(() => { if (!error) return diff --git a/packages/hyperflux/functions/StateFunctions.ts b/packages/hyperflux/functions/StateFunctions.ts index e997e4e774..7cac7d3210 100644 --- a/packages/hyperflux/functions/StateFunctions.ts +++ b/packages/hyperflux/functions/StateFunctions.ts @@ -147,8 +147,10 @@ export function syncStateWithLocalStorage( onSet: (state, desc) => { for (const key of keys) { const storageKey = `${stateNamespaceKey}.${rootState.identifier}.${key}` - if (!rootState[key] || !rootState[key].get(NO_PROXY)) localStorage.removeItem(storageKey) - else localStorage.setItem(storageKey, JSON.stringify(rootState[key].get(NO_PROXY))) + const value = rootState[key]?.get(NO_PROXY) + // We should still store flags that have been set to false or null + if (value === undefined) localStorage.removeItem(storageKey) + else localStorage.setItem(storageKey, JSON.stringify(value)) } } } as any diff --git a/packages/spatial/src/renderer/PerformanceState.test.tsx b/packages/spatial/src/renderer/PerformanceState.test.tsx index 821fd42d97..5db219f3a4 100644 --- a/packages/spatial/src/renderer/PerformanceState.test.tsx +++ b/packages/spatial/src/renderer/PerformanceState.test.tsx @@ -83,12 +83,11 @@ describe('PerformanceState', () => { mockRenderer, () => { const performanceState = getState(PerformanceState) - const budgets = performanceState.budgets - assert(budgets.max3DTextureSize === 1000) - assert(budgets.maxBufferSize === 54000000000) - assert(budgets.maxIndices === 8000) - assert(budgets.maxTextureSize === 2000) - assert(budgets.maxVerticies === 10000) + assert(performanceState.max3DTextureSize === 1000) + assert(performanceState.maxBufferSize === 54000000000) + assert(performanceState.maxIndices === 8000) + assert(performanceState.maxTextureSize === 2000) + assert(performanceState.maxVerticies === 10000) done() }, { renderer: 'nvidia corporation, nvidia geforce rtx 3070/pcie/sse2, ' } @@ -176,8 +175,9 @@ describe('PerformanceState', () => { const { rerender, unmount } = render() const clock = sinon.useFakeTimers() act(async () => { + // Decrementing performance state twice consecutively should only have one reactive change with the value off by 1 instead of 2 + PerformanceManager.decrementGPUPerformance() PerformanceManager.decrementGPUPerformance() - PerformanceManager.incrementGPUPerformance() clock.tick(3000) rerender() clock.restore() diff --git a/packages/spatial/src/renderer/PerformanceState.ts b/packages/spatial/src/renderer/PerformanceState.ts index 700614c386..29719998ea 100644 --- a/packages/spatial/src/renderer/PerformanceState.ts +++ b/packages/spatial/src/renderer/PerformanceState.ts @@ -27,9 +27,9 @@ import { GetGPUTier, getGPUTier } from 'detect-gpu' import { debounce } from 'lodash' import { SMAAPreset } from 'postprocessing' import { useEffect } from 'react' -import { Camera, Scene } from 'three' +import { Camera, MathUtils, Scene } from 'three' -import { ECSState } from '@etherealengine/ecs' +import { defineSystem, ECSState, PresentationSystemGroup } from '@etherealengine/ecs' import { profile } from '@etherealengine/ecs/src/Timer' import { defineState, getMutableState, getState, State, useMutableState } from '@etherealengine/hyperflux' import { EngineRenderer, RenderSettingsState } from '@etherealengine/spatial/src/renderer/WebGLRendererSystem' @@ -38,30 +38,75 @@ import { EngineState } from '../EngineState' import { RendererState } from './RendererState' type PerformanceTier = 0 | 1 | 2 | 3 | 4 | 5 +type TargetFPS = 30 | 60 +const maxPerformanceTier = 5 +const maxPerformanceOffset = 12 const tieredSettings = { [0]: { - engine: { useShadows: false, shadowMapResolution: 0, usePostProcessing: false, forceBasicMaterials: true }, + engine: { + useShadows: false, + shadowMapResolution: 0, + usePostProcessing: false, + forceBasicMaterials: true, + updateCSMFrustums: false, + renderScale: 0.75 + }, render: { smaaPreset: SMAAPreset.LOW } }, [1]: { - engine: { useShadows: false, shadowMapResolution: 0, usePostProcessing: false, forceBasicMaterials: false }, + engine: { + useShadows: false, + shadowMapResolution: 0, + usePostProcessing: false, + forceBasicMaterials: false, + updateCSMFrustums: true, + renderScale: 1 + }, render: { smaaPreset: SMAAPreset.LOW } }, [2]: { - engine: { useShadows: true, shadowMapResolution: 256, usePostProcessing: false, forceBasicMaterials: false }, + engine: { + useShadows: true, + shadowMapResolution: 256, + usePostProcessing: false, + forceBasicMaterials: false, + updateCSMFrustums: true, + renderScale: 1 + }, render: { smaaPreset: SMAAPreset.LOW } }, [3]: { - engine: { useShadows: true, shadowMapResolution: 512, usePostProcessing: false, forceBasicMaterials: false }, + engine: { + useShadows: true, + shadowMapResolution: 512, + usePostProcessing: false, + forceBasicMaterials: false, + updateCSMFrustums: true, + renderScale: 1 + }, render: { smaaPreset: SMAAPreset.MEDIUM } }, [4]: { - engine: { useShadows: true, shadowMapResolution: 1024, usePostProcessing: true, forceBasicMaterials: false }, + engine: { + useShadows: true, + shadowMapResolution: 1024, + usePostProcessing: true, + forceBasicMaterials: false, + updateCSMFrustums: true, + renderScale: 1 + }, render: { smaaPreset: SMAAPreset.HIGH } }, [5]: { - engine: { useShadows: true, shadowMapResolution: 2048, usePostProcessing: true, forceBasicMaterials: false }, + engine: { + useShadows: true, + shadowMapResolution: 2048, + usePostProcessing: true, + forceBasicMaterials: false, + updateCSMFrustums: true, + renderScale: 1 + }, render: { smaaPreset: SMAAPreset.ULTRA } } } as { @@ -76,7 +121,7 @@ type ExponentialMovingAverage = { multiplier: number } -const createExponentialMovingAverage = (timePeriods = 10, startingMean = 16): ExponentialMovingAverage => { +const createExponentialMovingAverage = (timePeriods: number, startingMean: number): ExponentialMovingAverage => { return { mean: startingMean, multiplier: 2 / (timePeriods + 1) @@ -84,57 +129,71 @@ const createExponentialMovingAverage = (timePeriods = 10, startingMean = 16): Ex } const updateExponentialMovingAverage = (average: State, newValue: number) => { - const meanIncrement = average.multiplier.value * (newValue - average.mean.value) - const newMean = average.mean.value + meanIncrement - average.mean.set(newMean) + const multiplier = average.multiplier.value + const prev = average.mean.value + const ema = newValue * multiplier + prev * (1 - multiplier) + average.mean.set(ema) } export const PerformanceState = defineState({ name: 'PerformanceState', + initial: () => ({ - isMobileGPU: false as boolean | undefined, + enabled: true, + + isMobileGPU: false, gpuTier: 3 as PerformanceTier, cpuTier: 3 as PerformanceTier, supportWebGL2: true, + targetFPS: 60 as TargetFPS, + + gpu: 'unknown', + device: 'unknown', + + maxTextureSize: 0, + max3DTextureSize: 0, + maxBufferSize: 0, + maxIndices: 0, + maxVerticies: 0, + renderContext: null! as WebGL2RenderingContext, // The lower the performance the higher the offset gpuPerformanceOffset: 0, cpuPerformanceOffset: 0, - // Render timings and constants - // 180 = 3 * 60 = 3 seconds @ 60fps - // 35 = 28fps - // 18 = 55fps - averageRenderTime: createExponentialMovingAverage(180 as const), - maxRenderTime: 35 as const, - minRenderTime: 18 as const, + averageFrameTime: createExponentialMovingAverage(180, (1 / 60) * 1000), + averageRenderTime: createExponentialMovingAverage(180, (1 / 60) * 1000), + averageSystemTime: createExponentialMovingAverage(180, (1 / 60) * 1000), - // System timings and constants - averageSystemTime: createExponentialMovingAverage(180 as const), - maxSystemTime: 35 as const, - minSystemTime: 18 as const, - - gpu: 'unknown', - device: 'unknown', - budgets: { - maxTextureSize: 0, - max3DTextureSize: 0, - maxBufferSize: 0, - maxIndices: 0, - maxVerticies: 0 - } + performanceSmoothingCycles: 300 as const, + performanceSmoothingAccum: 0 }), + reactor: () => { const performanceState = useMutableState(PerformanceState) const renderSettings = useMutableState(RenderSettingsState) const engineSettings = useMutableState(RendererState) - const ecsState = useMutableState(ECSState) - const isEditing = getState(EngineState).isEditing + const engineState = useMutableState(EngineState) + + const recreateEMA = () => { + const targetFPS = performanceState.targetFPS.value + const timePeriods = targetFPS * 3 + const startingMean = (1 / targetFPS) * 1000 + performanceState.merge({ + averageFrameTime: createExponentialMovingAverage(timePeriods, startingMean), + averageRenderTime: createExponentialMovingAverage(timePeriods, startingMean), + averageSystemTime: createExponentialMovingAverage(timePeriods, startingMean) + }) + } useEffect(() => { - if (isEditing) return + recreateEMA() + }, [performanceState.targetFPS]) + + useEffect(() => { + if (engineState.isEditing.value) return const performanceTier = performanceState.gpuTier.value const settings = tieredSettings[performanceTier] @@ -143,56 +202,85 @@ export const PerformanceState = defineState({ }, [performanceState.gpuTier]) useEffect(() => { - if (isEditing) return + recreateEMA() + performanceState.performanceSmoothingAccum.set(0) + }, [performanceState.gpuPerformanceOffset, performanceState.cpuPerformanceOffset]) + + useEffect(() => { + performanceState.enabled.set(!engineState.isEditing.value && engineSettings.automatic.value) + }, [engineState.isEditing, engineSettings.automatic]) + } +}) - const { averageRenderTime, maxRenderTime, minRenderTime, averageSystemTime, maxSystemTime, minSystemTime } = - performanceState.value +export const PerformanceSystem = defineSystem({ + uuid: 'ee.engine.PerformanceSystem', + insert: { after: PresentationSystemGroup }, - const renderMean = averageRenderTime.mean - if (renderMean > maxRenderTime) { - decrementGPUPerformance() - } else if (renderMean < minRenderTime) { - incrementGPUPerformance() - } + execute: () => { + const performanceState = getState(PerformanceState) + if (!performanceState.enabled) return - const systemMean = averageSystemTime.mean - if (systemMean > maxSystemTime) { - decrementGPUPerformance() - } else if (renderMean < minSystemTime) { - incrementGPUPerformance() + { + const { performanceSmoothingAccum, performanceSmoothingCycles } = performanceState + const performanceStateMut = getMutableState(PerformanceState) + const ecsState = getState(ECSState) + + updateExponentialMovingAverage(performanceStateMut.averageSystemTime, ecsState.lastSystemExecutionDuration) + updateExponentialMovingAverage(performanceStateMut.averageFrameTime, ecsState.deltaSeconds * 1000) + + if (performanceSmoothingAccum < performanceSmoothingCycles) { + performanceStateMut.performanceSmoothingAccum.set(performanceSmoothingAccum + 1) + return } - }, [performanceState.averageRenderTime]) + } - useEffect(() => { - if (isEditing) return + const { averageFrameTime, averageRenderTime, averageSystemTime, targetFPS } = performanceState + const maxDelta = 1000 / (targetFPS / 2 - 2) + const minDelta = 1000 / (targetFPS - 5) - const lastDuration = ecsState.lastSystemExecutionDuration.value - updateExponentialMovingAverage(performanceState.averageSystemTime, lastDuration) - }, [ecsState.lastSystemExecutionDuration]) + const frameMean = averageFrameTime.mean + const renderMean = averageRenderTime.mean + const systemMean = averageSystemTime.mean + + // Frame time is below target + if (frameMean > maxDelta) { + const maxRatio = 2.5 + const gpuRatio = frameMean / renderMean + const systemRatio = frameMean / systemMean + + // Check if we are GPU bound + if (gpuRatio < maxRatio) { + decrementGPUPerformance() + // Check if we are system bound + } else if (systemRatio < maxRatio) { + decrementCPUPerformance() + // We are CPU bound by something other than systems + } else { + decrementCPUPerformance() + } + } else if (frameMean < minDelta) { + incrementGPUPerformance() + incrementCPUPerformance() + } } }) -const timeBeforeCheck = 3 -let timeAccum = 0 let checkingRenderTime = false /** * API to get GPU timings, with fallback if WebGL extension is not available (Not available on WebGL1 devices and Safari) * Will only run if not already running and the number of elapsed seconds since it last ran is greater than timeBeforeCheck * * @param renderer EngineRenderer - * @param dt delta time * @returns Function to call after you call your render function */ -const profileGPURender = (dt: number): (() => void) => { - timeAccum += dt - if (checkingRenderTime || timeAccum < timeBeforeCheck) return () => {} - checkingRenderTime = true +const profileGPURender = (): (() => void) => { + if (checkingRenderTime || !getState(PerformanceState).enabled) return () => {} + checkingRenderTime = true return timeRenderFrameGPU((renderTime) => { checkingRenderTime = false - timeAccum = 0 const performanceState = getMutableState(PerformanceState) - updateExponentialMovingAverage(performanceState.averageRenderTime, Math.min(renderTime, 50)) + updateExponentialMovingAverage(performanceState.averageRenderTime, renderTime) }) } @@ -239,7 +327,6 @@ const timeRenderFrameGPU = (callback: (number) => void = () => {}): (() => void) gl.deleteQuery(startQuery) gl.deleteQuery(endQuery) checkingRenderTime = false - timeAccum = 0 } else { requestAnimationFrame(poll) } @@ -281,7 +368,12 @@ const updatePerformanceState = (tierState: State, tier: number, offsetSt const debounceTime = 1000 const updateStateTierAndOffset = debounce( (tierState: State, tier: number, offsetState: State, offset: number) => { - updatePerformanceState(tierState, tier, offsetState, offset) + updatePerformanceState( + tierState, + MathUtils.clamp(tier, 0, maxPerformanceTier), + offsetState, + MathUtils.clamp(offset, 0, maxPerformanceOffset) + ) }, debounceTime, { trailing: true, maxWait: debounceTime * 2 } @@ -291,20 +383,19 @@ const incrementGPUPerformance = () => { const performanceState = getMutableState(PerformanceState) updateStateTierAndOffset( performanceState.gpuTier, - Math.min(performanceState.gpuTier.value + 1, 5), + performanceState.gpuTier.value + 1, performanceState.gpuPerformanceOffset, - Math.max(performanceState.gpuPerformanceOffset.value - 1, 0) + performanceState.gpuPerformanceOffset.value - 1 ) } -const maxOffset = 12 const decrementGPUPerformance = () => { const performanceState = getMutableState(PerformanceState) updateStateTierAndOffset( performanceState.gpuTier, - Math.max(performanceState.gpuTier.value - 1, 0), + performanceState.gpuTier.value - 1, performanceState.gpuPerformanceOffset, - Math.min(performanceState.gpuPerformanceOffset.value + 1, maxOffset) + performanceState.gpuPerformanceOffset.value + 1 ) } @@ -312,9 +403,9 @@ const incrementCPUPerformance = () => { const performanceState = getMutableState(PerformanceState) updateStateTierAndOffset( performanceState.cpuTier, - Math.min(performanceState.cpuTier.value + 1, 5), + performanceState.cpuTier.value + 1, performanceState.cpuPerformanceOffset, - Math.max(performanceState.cpuPerformanceOffset.value - 1, 0) + performanceState.cpuPerformanceOffset.value - 1 ) } @@ -322,9 +413,9 @@ const decrementCPUPerformance = () => { const performanceState = getMutableState(PerformanceState) updateStateTierAndOffset( performanceState.cpuTier, - Math.max(performanceState.cpuTier.value - 1, 0), + performanceState.cpuTier.value - 1, performanceState.cpuPerformanceOffset, - Math.min(performanceState.cpuPerformanceOffset.value + 1, maxOffset) + performanceState.cpuPerformanceOffset.value + 1 ) } @@ -342,7 +433,7 @@ const buildPerformanceState = async ( override }) let tier = gpuTier.tier - performanceState.isMobileGPU.set(gpuTier.isMobile) + performanceState.isMobileGPU.set(!!gpuTier.isMobile) if (gpuTier.gpu) performanceState.gpu.set(gpuTier.gpu) if (gpuTier.device) performanceState.device.set(gpuTier.device) @@ -350,7 +441,7 @@ const buildPerformanceState = async ( performanceState.supportWebGL2.set(renderer.supportWebGL2) performanceState.renderContext.set(gl) const max3DTextureSize = gl.getParameter(gl.MAX_3D_TEXTURE_SIZE) - performanceState.budgets.set({ + performanceState.merge({ maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE), max3DTextureSize: max3DTextureSize, maxBufferSize: @@ -363,7 +454,10 @@ const buildPerformanceState = async ( maxVerticies: gl.getParameter(gl.MAX_ELEMENTS_VERTICES) * 2 }) - if (gpuTier.isMobile) tier = Math.max(tier - 2, 0) + if (gpuTier.isMobile) { + performanceState.targetFPS.set(30) + tier = Math.max(tier - 2, 0) + } performanceState.gpuTier.set(tier as PerformanceTier) onFinished() diff --git a/packages/spatial/src/renderer/RendererState.ts b/packages/spatial/src/renderer/RendererState.ts index a752ee2958..d65b552793 100644 --- a/packages/spatial/src/renderer/RendererState.ts +++ b/packages/spatial/src/renderer/RendererState.ts @@ -37,6 +37,9 @@ export const RendererState = defineState({ // usePBR: true, usePostProcessing: isIPhone ? false : true, useShadows: isIPhone ? false : true, + updateCSMFrustums: true, + /** Resoulion scale. **Default** value is 1. */ + renderScale: 1, physicsDebug: false, bvhDebug: false, avatarDebug: false, diff --git a/packages/spatial/src/renderer/WebGLRendererSystem.tsx b/packages/spatial/src/renderer/WebGLRendererSystem.tsx index de727166fb..d01a323872 100644 --- a/packages/spatial/src/renderer/WebGLRendererSystem.tsx +++ b/packages/spatial/src/renderer/WebGLRendererSystem.tsx @@ -57,8 +57,6 @@ import { import { defineState, getMutableState, getState, useMutableState } from '@etherealengine/hyperflux' import { CameraComponent } from '../camera/components/CameraComponent' -import { ExponentialMovingAverage } from '../common/classes/ExponentialAverageCurve' -import { EngineState } from '../EngineState' import { getNestedChildren } from '../transform/components/EntityTree' import { createWebXRManager, WebXRManager } from '../xr/WebXRManager' import { XRLightProbeState } from '../xr/XRLightProbeSystem' @@ -76,7 +74,7 @@ import { RenderModes } from './constants/RenderModes' import { CSM } from './csm/CSM' import CSMHelper from './csm/CSMHelper' import { changeRenderMode } from './functions/changeRenderMode' -import { PerformanceManager } from './PerformanceState' +import { PerformanceManager, PerformanceState } from './PerformanceState' import { RendererState } from './RendererState' import WebGL from './THREE.WebGL' @@ -108,15 +106,6 @@ export class EngineRenderer { /** Is resize needed? */ needsResize: boolean - /** Maximum Quality level of the rendered. **Default** value is 5. */ - maxQualityLevel = 5 - /** point at which we downgrade quality level (large delta) */ - maxRenderDelta = 1000 / 28 // 28 fps = 35 ms (on some devices, rAF updates at 30fps, e.g., Low Power Mode) - /** point at which we upgrade quality level (small delta) */ - minRenderDelta = 1000 / 55 // 55 fps = 18 ms - /** Resoulion scale. **Default** value is 1. */ - scaleFactor = 1 - renderPass: RenderPass normalPass: NormalPass renderContext: WebGLRenderingContext | WebGL2RenderingContext @@ -124,13 +113,7 @@ export class EngineRenderer { supportWebGL2: boolean canvas: HTMLCanvasElement - averageTimePeriods = 3 * 60 // 3 seconds @ 60fps - /** init ExponentialMovingAverage */ - movingAverage = new ExponentialMovingAverage(this.averageTimePeriods) - renderer: WebGLRenderer = null! - /** used to optimize proxified threejs objects during render time, see loadGLTFModel and https://github.com/EtherealEngine/etherealengine/issues/9308 */ - rendering = false effectComposer: EffectComposer = null! /** @todo deprecate and replace with engine implementation */ xrManager: WebXRManager = null! @@ -213,31 +196,6 @@ export class EngineRenderer { } } -/** - * Change the quality of the renderer. - */ -const changeQualityLevel = (renderer: EngineRenderer) => { - const time = Date.now() - const delta = time - lastRenderTime - lastRenderTime = time - - const { qualityLevel } = getState(RendererState) - let newQualityLevel = qualityLevel - - renderer.movingAverage.update(Math.min(delta, 50)) - const averageDelta = renderer.movingAverage.mean - - if (averageDelta > renderer.maxRenderDelta && newQualityLevel > 1) { - newQualityLevel-- - } else if (averageDelta < renderer.minRenderDelta && newQualityLevel < renderer.maxQualityLevel) { - newQualityLevel++ - } - - if (newQualityLevel !== qualityLevel) { - getMutableState(RendererState).qualityLevel.set(newQualityLevel) - } -} - /** * Executes the system. Called each frame by default from the Engine.instance. * @param delta Time since last frame. @@ -256,12 +214,9 @@ export const render = ( const state = getState(RendererState) - const engineState = getState(EngineState) - if (!engineState.isEditor && state.automatic) changeQualityLevel(renderer) - if (renderer.needsResize) { const curPixelRatio = renderer.renderer.getPixelRatio() - const scaledPixelRatio = window.devicePixelRatio * renderer.scaleFactor + const scaledPixelRatio = window.devicePixelRatio * state.renderScale if (curPixelRatio !== scaledPixelRatio) renderer.renderer.setPixelRatio(scaledPixelRatio) @@ -273,7 +228,7 @@ export const render = ( camera.updateProjectionMatrix() } - state.qualityLevel > 0 && renderer.csm?.updateFrustums() + state.updateCSMFrustums && renderer.csm?.updateFrustums() if (renderer.effectComposer) { renderer.effectComposer.setSize(width, height, true) @@ -340,7 +295,7 @@ export const getSceneParameters = (entities: Entity[]) => { const execute = () => { const deltaSeconds = getState(ECSState).deltaSeconds - const onRenderEnd = PerformanceManager.profileGPURender(deltaSeconds) + const onRenderEnd = PerformanceManager.profileGPURender() for (const entity of rendererQuery()) { const camera = getComponent(entity, CameraComponent) const renderer = getComponent(entity, RendererComponent) @@ -372,10 +327,19 @@ const rendererReactor = () => { const engineRendererSettings = useMutableState(RendererState) useEffect(() => { - renderer.scaleFactor.set(engineRendererSettings.qualityLevel.value / renderer.maxQualityLevel.value) - renderer.renderer.value.setPixelRatio(window.devicePixelRatio * renderer.scaleFactor.value) + if (engineRendererSettings.automatic.value) return + + const qualityLevel = engineRendererSettings.qualityLevel.value + getMutableState(PerformanceState).merge({ + gpuTier: qualityLevel, + cpuTier: qualityLevel + } as any) + }, [engineRendererSettings.qualityLevel, engineRendererSettings.automatic]) + + useEffect(() => { + renderer.renderer.value.setPixelRatio(window.devicePixelRatio * engineRendererSettings.renderScale.value) renderer.needsResize.set(true) - }, [engineRendererSettings.qualityLevel]) + }, [engineRendererSettings.renderScale]) useEffect(() => { changeRenderMode() diff --git a/packages/spatial/src/resources/ResourceState.ts b/packages/spatial/src/resources/ResourceState.ts index 6f2259d2b3..9e5d029c39 100644 --- a/packages/spatial/src/resources/ResourceState.ts +++ b/packages/spatial/src/resources/ResourceState.ts @@ -23,7 +23,7 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -import { Cache, CompressedTexture, Material, Mesh, Object3D, Scene, SkinnedMesh, Texture } from 'three' +import { BufferAttribute, Cache, CompressedTexture, Material, Mesh, Object3D, Scene, SkinnedMesh, Texture } from 'three' import { Engine, Entity, getOptionalComponent, UndefinedEntity } from '@etherealengine/ecs' import { defineState, getMutableState, getState, NO_PROXY, none, State } from '@etherealengine/hyperflux' @@ -80,6 +80,7 @@ export type ResourceAssetType = type BaseMetadata = { size?: number + onGPU?: boolean } type GLTFMetadata = { @@ -89,7 +90,6 @@ type GLTFMetadata = { type TexutreMetadata = { textureWidth: number - onGPU: boolean } & BaseMetadata type Metadata = GLTFMetadata | TexutreMetadata | BaseMetadata @@ -170,8 +170,8 @@ const getRendererInfo = () => { const checkBudgets = () => { const resourceState = getState(ResourceState) const performanceState = getState(PerformanceState) - const maxVerts = performanceState.budgets.maxVerticies - const maxBuffer = performanceState.budgets.maxBufferSize + const maxVerts = performanceState.maxVerticies + const maxBuffer = performanceState.maxBufferSize const currVerts = resourceState.totalVertexCount const currBuff = resourceState.totalBufferCount if (currVerts > maxVerts) @@ -281,11 +281,32 @@ const resourceCallbacks = { onStart: (resource: State) => {}, onLoad: (response: Geometry, resource: State, resourceState: State) => { // Estimated geometry size + const attributeKeys = Object.keys(response.attributes) + let needsUploaded = response.index ? attributeKeys.length + 1 : attributeKeys.length let size = 0 - for (const name in response.attributes) { - const attr = response.getAttribute(name) + + const checkUploaded = () => { + if (needsUploaded == 0 && resource && resource.value) resource.metadata.merge({ onGPU: true }) + } + + response.index?.onUpload(() => { + needsUploaded -= 1 + checkUploaded() + }) + + for (const name of attributeKeys) { + const attr = response.getAttribute(name) as BufferAttribute size += attr.count * attr.itemSize * attr.array.BYTES_PER_ELEMENT + if (typeof attr.onUpload === 'function') { + attr.onUpload(() => { + needsUploaded -= 1 + checkUploaded() + }) + } else { + needsUploaded -= 1 + } } + checkUploaded() const indices = response.getIndex() if (indices) {