From a2e9404a70ea240f35ca82c5e0ffb6d193465006 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Thu, 5 Jun 2025 20:22:58 +0300 Subject: [PATCH] feat: Simple but effective renderer perf debug features (#347) --- renderer/viewer/lib/worldrendererCommon.ts | 18 +++ renderer/viewer/three/worldrendererThree.ts | 36 +++++- src/controls.ts | 6 + src/optionsGuiScheme.tsx | 3 + src/optionsStorage.ts | 1 + src/react/RendererDebugMenu.tsx | 115 ++++++++++++++++++++ src/react/button.module.css | 1 + src/react/rendererDebugMenu.module.css | 34 ++++++ src/react/rendererDebugMenu.module.css.d.ts | 9 ++ src/reactUi.tsx | 2 + src/watchOptions.ts | 1 + 11 files changed, 225 insertions(+), 1 deletion(-) create mode 100644 src/react/RendererDebugMenu.tsx create mode 100644 src/react/rendererDebugMenu.module.css create mode 100644 src/react/rendererDebugMenu.module.css.d.ts diff --git a/renderer/viewer/lib/worldrendererCommon.ts b/renderer/viewer/lib/worldrendererCommon.ts index 3f517beb..2b37742c 100644 --- a/renderer/viewer/lib/worldrendererCommon.ts +++ b/renderer/viewer/lib/worldrendererCommon.ts @@ -8,6 +8,7 @@ import { ItemsRenderer } from 'mc-assets/dist/itemsRenderer' import { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider' import { generateSpiralMatrix } from 'flying-squid/dist/utils' import { subscribeKey } from 'valtio/utils' +import { proxy } from 'valtio' import { dynamicMcDataFiles } from '../../buildMesherConfig.mjs' import { toMajorVersion } from '../../../src/utils' import { ResourcesManager } from '../../../src/resourcesManager' @@ -49,6 +50,7 @@ export const defaultWorldRendererConfig = { fetchPlayerSkins: true, highlightBlockColor: 'blue', foreground: true, + enableDebugOverlay: false, _experimentalSmoothChunkLoading: true, _renderByChunks: false } @@ -60,6 +62,17 @@ export abstract class WorldRendererCommon worldReadyPromise = this.worldReadyResolvers.promise timeOfTheDay = 0 worldSizeParams = { minY: 0, worldHeight: 256 } + reactiveDebugParams = proxy({ + stopRendering: false, + chunksRenderAboveOverride: undefined as number | undefined, + chunksRenderAboveEnabled: false, + chunksRenderBelowOverride: undefined as number | undefined, + chunksRenderBelowEnabled: false, + chunksRenderDistanceOverride: undefined as number | undefined, + chunksRenderDistanceEnabled: false, + disableEntities: false, + // disableParticles: false + }) active = false @@ -315,6 +328,11 @@ export abstract class WorldRendererCommon subscribeKey(this.worldRendererConfig, key, callback) } + onReactiveDebugUpdated(key: T, callback: (value: typeof this.reactiveDebugParams[T]) => void) { + callback(this.reactiveDebugParams[key]) + subscribeKey(this.reactiveDebugParams, key, callback) + } + watchReactivePlayerState () { this.onReactiveValueUpdated('backgroundColor', (value) => { this.changeBackgroundColor(value) diff --git a/renderer/viewer/three/worldrendererThree.ts b/renderer/viewer/three/worldrendererThree.ts index d2d54998..a8e08068 100644 --- a/renderer/viewer/three/worldrendererThree.ts +++ b/renderer/viewer/three/worldrendererThree.ts @@ -455,7 +455,39 @@ export class WorldRendererThree extends WorldRendererCommon { this.cameraShake.setBaseRotation(pitch, yaw) } + debugChunksVisibilityOverride () { + const { chunksRenderAboveOverride, chunksRenderBelowOverride, chunksRenderDistanceOverride, chunksRenderAboveEnabled, chunksRenderBelowEnabled, chunksRenderDistanceEnabled } = this.reactiveDebugParams + + const baseY = this.cameraSectionPos.y * 16 + + if ( + this.displayOptions.inWorldRenderingConfig.enableDebugOverlay && + chunksRenderAboveOverride !== undefined || + chunksRenderBelowOverride !== undefined || + chunksRenderDistanceOverride !== undefined + ) { + for (const [key, object] of Object.entries(this.sectionObjects)) { + const [x, y, z] = key.split(',').map(Number) + const isVisible = + // eslint-disable-next-line no-constant-binary-expression, sonarjs/no-redundant-boolean + (chunksRenderAboveEnabled && chunksRenderAboveOverride !== undefined) ? y <= (baseY + chunksRenderAboveOverride) : true && + // eslint-disable-next-line @stylistic/indent-binary-ops, no-constant-binary-expression, sonarjs/no-redundant-boolean + (chunksRenderBelowEnabled && chunksRenderBelowOverride !== undefined) ? y >= (baseY - chunksRenderBelowOverride) : true && + // eslint-disable-next-line @stylistic/indent-binary-ops + (chunksRenderDistanceEnabled && chunksRenderDistanceOverride !== undefined) ? Math.abs(y - baseY) <= chunksRenderDistanceOverride : true + + object.visible = isVisible + } + } else { + for (const object of Object.values(this.sectionObjects)) { + object.visible = true + } + } + } + render (sizeChanged = false) { + if (this.reactiveDebugParams.stopRendering) return + this.debugChunksVisibilityOverride() const start = performance.now() this.lastRendered = performance.now() this.cursorBlock.render() @@ -468,7 +500,9 @@ export class WorldRendererThree extends WorldRendererCommon { this.camera.updateProjectionMatrix() } - this.entities.render() + if (!this.reactiveDebugParams.disableEntities) { + this.entities.render() + } // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style const cam = this.cameraGroupVr instanceof THREE.Group ? this.cameraGroupVr.children.find(child => child instanceof THREE.PerspectiveCamera) as THREE.PerspectiveCamera : this.camera diff --git a/src/controls.ts b/src/controls.ts index e270750e..f32cbda6 100644 --- a/src/controls.ts +++ b/src/controls.ts @@ -678,6 +678,12 @@ export const f3Keybinds: Array<{ }, mobileTitle: 'Show Chunks Debug', }, + { + action () { + showModal({ reactType: 'renderer-debug' }) + }, + mobileTitle: 'Renderer Debug Menu', + }, { key: 'KeyY', async action () { diff --git a/src/optionsGuiScheme.tsx b/src/optionsGuiScheme.tsx index 7f4c34ab..c74a72d8 100644 --- a/src/optionsGuiScheme.tsx +++ b/src/optionsGuiScheme.tsx @@ -109,6 +109,9 @@ export const guiOptionsScheme: { 'none' ], }, + rendererPerfDebugOverlay: { + text: 'Performance Debug', + } }, { custom () { diff --git a/src/optionsStorage.ts b/src/optionsStorage.ts index a0c995a0..88e86817 100644 --- a/src/optionsStorage.ts +++ b/src/optionsStorage.ts @@ -106,6 +106,7 @@ const defaultOptions = { vrSupport: true, // doesn't directly affect the VR mode, should only disable the button which is annoying to android users vrPageGameRendering: false, renderDebug: 'basic' as 'none' | 'advanced' | 'basic', + rendererPerfDebugOverlay: false, // advanced bot options autoRespawn: false, diff --git a/src/react/RendererDebugMenu.tsx b/src/react/RendererDebugMenu.tsx new file mode 100644 index 00000000..2e6030c1 --- /dev/null +++ b/src/react/RendererDebugMenu.tsx @@ -0,0 +1,115 @@ +import { WorldRendererCommon } from 'renderer/viewer/lib/worldrendererCommon' +import { useState } from 'react' +import { useSnapshot } from 'valtio' +import { options } from '../optionsStorage' +import Screen from './Screen' +import Button from './Button' +import Slider from './Slider' +import styles from './rendererDebugMenu.module.css' + +export default () => { + const worldRenderer = window.world as WorldRendererCommon + const { reactiveDebugParams } = worldRenderer + const { chunksRenderAboveEnabled, chunksRenderBelowEnabled, chunksRenderDistanceEnabled, chunksRenderAboveOverride, chunksRenderBelowOverride, chunksRenderDistanceOverride, stopRendering, disableEntities } = useSnapshot(reactiveDebugParams) + + const { rendererPerfDebugOverlay } = useSnapshot(options) + + // Helper to round values to nearest step + const roundToStep = (value: number, step: number) => Math.round(value / step) * step + + if (!rendererPerfDebugOverlay) return null + + return
+
+

Rendering Controls

+
+ +
+

Chunks Render Settings

+
+
+ +
+
+ + {/*
+
*/} +
+
+} diff --git a/src/react/button.module.css b/src/react/button.module.css index 677a1b44..e940c224 100644 --- a/src/react/button.module.css +++ b/src/react/button.module.css @@ -4,6 +4,7 @@ position: relative; width: 200px; min-height: calc(20px * var(--scale)); + max-height: calc(20px * var(--scale)); font-family: minecraft, mojangles, monospace; font-size: 10px; color: white; diff --git a/src/react/rendererDebugMenu.module.css b/src/react/rendererDebugMenu.module.css new file mode 100644 index 00000000..6c49e04c --- /dev/null +++ b/src/react/rendererDebugMenu.module.css @@ -0,0 +1,34 @@ +.container { + display: flex; + flex-direction: row; + justify-content: space-between; + gap: 20px; + padding: 10px; + height: 100%; + padding-top: env(safe-area-inset-top, 10px); + padding-left: env(safe-area-inset-left, 10px); + padding-right: env(safe-area-inset-right, 10px); + padding-bottom: env(safe-area-inset-bottom, 10px); +} + +.column { + display: flex; + flex-direction: column; + gap: 10px; + min-width: 200px; +} + +.column h3 { + margin: 0; + padding: 0; + font-size: 16px; + color: white; + text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.8); +} + +.sliderGroup { + display: flex; + flex-direction: column; + gap: 5px; + margin-bottom: 10px; +} diff --git a/src/react/rendererDebugMenu.module.css.d.ts b/src/react/rendererDebugMenu.module.css.d.ts new file mode 100644 index 00000000..0f8f2163 --- /dev/null +++ b/src/react/rendererDebugMenu.module.css.d.ts @@ -0,0 +1,9 @@ +// This file is automatically generated. +// Please do not change this file! +interface CssExports { + column: string; + container: string; + sliderGroup: string; +} +declare const cssExports: CssExports; +export default cssExports; diff --git a/src/reactUi.tsx b/src/reactUi.tsx index 6405b27e..e67facea 100644 --- a/src/reactUi.tsx +++ b/src/reactUi.tsx @@ -61,6 +61,7 @@ import ControDebug from './react/ControDebug' import ChunksDebug from './react/ChunksDebug' import ChunksDebugScreen from './react/ChunksDebugScreen' import DebugResponseTimeIndicator from './react/debugs/DebugResponseTimeIndicator' +import RendererDebugMenu from './react/RendererDebugMenu' import CreditsAboutModal from './react/CreditsAboutModal' import GlobalOverlayHints from './react/GlobalOverlayHints' @@ -167,6 +168,7 @@ const InGameUi = () => { {!disabledUiParts.includes('bossbars') && displayBossBars && } + diff --git a/src/watchOptions.ts b/src/watchOptions.ts index 903d7da8..478da4fb 100644 --- a/src/watchOptions.ts +++ b/src/watchOptions.ts @@ -83,6 +83,7 @@ export const watchOptionsAfterViewerInit = () => { watchValue(options, o => { appViewer.inWorldRenderingConfig.vrSupport = o.vrSupport appViewer.inWorldRenderingConfig.vrPageGameRendering = o.vrPageGameRendering + appViewer.inWorldRenderingConfig.enableDebugOverlay = o.rendererPerfDebugOverlay }) watchValue(options, (o, isChanged) => {