Skip to content

Commit

Permalink
fix: update changed blocks quicker in the world by using a dedicated …
Browse files Browse the repository at this point in the history
…mesher thread reserved for blocks updates only
  • Loading branch information
zardoy committed Dec 12, 2024
1 parent 689bebd commit a4c86d7
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 98 deletions.
2 changes: 1 addition & 1 deletion prismarine-viewer/viewer/lib/mesher/mesher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ setInterval(() => {
const geometry = getSectionGeometry(x, y, z, world)
const transferable = [geometry.positions?.buffer, geometry.normals?.buffer, geometry.colors?.buffer, geometry.uvs?.buffer].filter(Boolean)
//@ts-expect-error
postMessage({ type: 'geometry', key, geometry }, transferable)
postMessage({ type: 'geometry', key, geometry, workerIndex }, transferable)
processTime = performance.now() - start
} else {
// console.info('[mesher] Missing section', x, y, z)
Expand Down
52 changes: 39 additions & 13 deletions prismarine-viewer/viewer/lib/worldrendererCommon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ import itemsAtlasLatest from 'mc-assets/dist/itemsAtlasLatest.png'
import itemsAtlasLegacy from 'mc-assets/dist/itemsAtlasLegacy.png'
import { AtlasParser } from 'mc-assets'
import TypedEmitter from 'typed-emitter'
import { LineMaterial } from 'three-stdlib'
import { dynamicMcDataFiles } from '../../buildMesherConfig.mjs'
import { toMajorVersion } from '../../../src/utils'
import { buildCleanupDecorator } from './cleanupDecorator'
import { defaultMesherConfig, HighestBlockInfo, MesherGeometryOutput } from './mesher/shared'
import { chunkPos } from './simpleUtils'
import { HandItemBlock } from './holdingBlock'
import { updateStatText } from './ui/newStats'
import { WorldRendererThree } from './worldrendererThree'

function mod (x, n) {
return ((x % n) + n) % n
Expand All @@ -38,8 +40,14 @@ type CustomTexturesData = {
}

export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any> {
// todo
@worldCleanup()
threejsCursorLineMaterial: LineMaterial
@worldCleanup()
cursorBlock = null as Vec3 | null
isPlayground = false
displayStats = true
@worldCleanup()
worldConfig = { minY: 0, worldHeight: 256 }
// todo need to cleanup
material = new THREE.MeshLambertMaterial({ vertexColors: true, transparent: true, alphaTest: 0.1 })
Expand Down Expand Up @@ -72,9 +80,13 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
@worldCleanup()
currentTextureImage = undefined as any
workers: any[] = []
@worldCleanup()
viewerPosition?: Vec3
lastCamUpdate = 0
droppedFpsPercentage = 0
@worldCleanup()
initialChunkLoadWasStartedIn: number | undefined
@worldCleanup()
initialChunksLoad = true
enableChunksLoadDelay = false
texturesVersion?: string
Expand Down Expand Up @@ -102,6 +114,10 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
workersProcessAverageTime = 0
workersProcessAverageTimeCount = 0
maxWorkersProcessTime = 0
geometryReceiveCount = {}
allLoadedIn: undefined | number
rendererDevice = '...'

edgeChunks = {} as Record<string, boolean>
lastAddChunk = null as null | {
timeout: any
Expand Down Expand Up @@ -129,7 +145,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>

initWorkers (numWorkers = this.config.numWorkers) {
// init workers
for (let i = 0; i < numWorkers; i++) {
for (let i = 0; i < numWorkers + 1; i++) {
// Node environment needs an absolute path, but browser needs the url of the file
const workerName = 'mesher.js'
// eslint-disable-next-line node/no-path-concat
Expand All @@ -140,6 +156,8 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
if (!this.active) return
this.handleWorkerMessage(data)
if (data.type === 'geometry') {
this.geometryReceiveCount[data.workerIndex] ??= 0
this.geometryReceiveCount[data.workerIndex]++
const geometry = data.geometry as MesherGeometryOutput
for (const key in geometry.highestBlocks) {
const highest = geometry.highestBlocks[key]
Expand Down Expand Up @@ -336,16 +354,17 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
return Math.floor(Math.max(this.worldConfig.minY, this.mesherConfig.clipWorldBelowY ?? -Infinity) / 16) * 16
}

updateDownloadedChunksText () {
updateStatText('downloaded-chunks', `${Object.keys(this.loadedChunks).length}/${this.chunksLength} chunks D`)
updateChunksStatsText () {
updateStatText('downloaded-chunks', `${Object.keys(this.loadedChunks).length}/${this.chunksLength} chunks D (${this.workers.length}:${this.workersProcessAverageTime.toFixed(0)}ms/${this.allLoadedIn?.toFixed(1) ?? '-'}s)`)
}

addColumn (x: number, z: number, chunk: any, isLightUpdate: boolean) {
if (!this.active) return
if (this.workers.length === 0) throw new Error('workers not initialized yet')
this.initialChunksLoad = false
this.initialChunkLoadWasStartedIn ??= Date.now()
this.loadedChunks[`${x},${z}`] = true
this.updateDownloadedChunksText()
this.updateChunksStatsText()
for (const worker of this.workers) {
// todo optimize
worker.postMessage({ type: 'chunk', x, z, chunk })
Expand Down Expand Up @@ -394,21 +413,26 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
for (const worker of this.workers) {
worker.postMessage({ type: 'blockUpdate', pos, stateId })
}
this.setSectionDirty(pos)
this.setSectionDirty(pos, true, true)
if (this.neighborChunkUpdates) {
if ((pos.x & 15) === 0) this.setSectionDirty(pos.offset(-16, 0, 0))
if ((pos.x & 15) === 15) this.setSectionDirty(pos.offset(16, 0, 0))
if ((pos.y & 15) === 0) this.setSectionDirty(pos.offset(0, -16, 0))
if ((pos.y & 15) === 15) this.setSectionDirty(pos.offset(0, 16, 0))
if ((pos.z & 15) === 0) this.setSectionDirty(pos.offset(0, 0, -16))
if ((pos.z & 15) === 15) this.setSectionDirty(pos.offset(0, 0, 16))
if ((pos.x & 15) === 0) this.setSectionDirty(pos.offset(-16, 0, 0), true, true)
if ((pos.x & 15) === 15) this.setSectionDirty(pos.offset(16, 0, 0), true, true)
if ((pos.y & 15) === 0) this.setSectionDirty(pos.offset(0, -16, 0), true, true)
if ((pos.y & 15) === 15) this.setSectionDirty(pos.offset(0, 16, 0), true, true)
if ((pos.z & 15) === 0) this.setSectionDirty(pos.offset(0, 0, -16), true, true)
if ((pos.z & 15) === 15) this.setSectionDirty(pos.offset(0, 0, 16), true, true)
}
}

queueAwaited = false
messagesQueue = {} as { [workerIndex: string]: any[] }

setSectionDirty (pos: Vec3, value = true) { // value false is used for unloading chunks
getWorkerNumber (pos: Vec3) {
const hash = mod(Math.floor(pos.x / 16) + Math.floor(pos.y / 16) + Math.floor(pos.z / 16), this.workers.length - 1)
return hash + 1
}

setSectionDirty (pos: Vec3, value = true, useChangeWorker = false) { // value false is used for unloading chunks
if (this.viewDistance === -1) throw new Error('viewDistance not set')
this.allChunksFinished = false
const distance = this.getDistance(pos)
Expand All @@ -419,7 +443,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
// Dispatch sections to workers based on position
// This guarantees uniformity accross workers and that a given section
// is always dispatched to the same worker
const hash = mod(Math.floor(pos.x / 16) + Math.floor(pos.y / 16) + Math.floor(pos.z / 16), this.workers.length)
const hash = useChangeWorker ? 0 : this.getWorkerNumber(pos)
this.sectionsWaiting.set(key, (this.sectionsWaiting.get(key) ?? 0) + 1)
this.messagesQueue[hash] ??= []
this.messagesQueue[hash].push({
Expand Down Expand Up @@ -487,4 +511,6 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
destroy () {
console.warn('world destroy is not implemented')
}

abstract setHighlightCursorBlock (block: typeof this.cursorBlock, shapePositions?: Array<{ position; width; height; depth }>): void
}
55 changes: 48 additions & 7 deletions prismarine-viewer/viewer/lib/worldrendererThree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Vec3 } from 'vec3'
import nbt from 'prismarine-nbt'
import PrismarineChatLoader from 'prismarine-chat'
import * as tweenJs from '@tweenjs/tween.js'
import { BloomPass, RenderPass, UnrealBloomPass, EffectComposer, WaterPass, GlitchPass } from 'three-stdlib'
import { BloomPass, RenderPass, UnrealBloomPass, EffectComposer, WaterPass, GlitchPass, LineSegmentsGeometry, Wireframe, LineMaterial } from 'three-stdlib'
import worldBlockProvider from 'mc-assets/dist/worldBlockProvider'
import { renderSign } from '../sign-renderer'
import { chunkPos, sectionPos } from './simpleUtils'
Expand All @@ -14,6 +14,7 @@ import { addNewStat } from './ui/newStats'
import { MesherGeometryOutput } from './mesher/shared'

export class WorldRendererThree extends WorldRendererCommon {
interactionLines: null | { blockPos; mesh } = null
outputFormat = 'threeJs' as const
blockEntities = {}
sectionObjects: Record<string, THREE.Object3D> = {}
Expand All @@ -22,6 +23,7 @@ export class WorldRendererThree extends WorldRendererCommon {
starField: StarField
cameraSectionPos: Vec3 = new Vec3(0, 0, 0)
holdingBlock: HoldingBlock
rendererDevice = '...'

get tilesRendered () {
return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj as any).tilesCount, 0)
Expand All @@ -33,6 +35,7 @@ export class WorldRendererThree extends WorldRendererCommon {

constructor (public scene: THREE.Scene, public renderer: THREE.WebGLRenderer, public config: WorldRendererConfig) {
super(config)
this.rendererDevice = String(WorldRendererThree.getRendererInfo(this.renderer))
this.starField = new StarField(scene)
this.holdingBlock = new HoldingBlock(this.scene)

Expand All @@ -54,10 +57,6 @@ export class WorldRendererThree extends WorldRendererCommon {
void this.holdingBlock.initHandObject(this.material, this.blockstatesModels, this.blocksAtlases, item)
}

changeBackgroundColor (color: [number, number, number]): void {
this.scene.background = new THREE.Color(color[0], color[1], color[2])
}

changeHandSwingingState (isAnimationPlaying: boolean) {
if (isAnimationPlaying) {
this.holdingBlock.startSwing()
Expand All @@ -66,6 +65,10 @@ export class WorldRendererThree extends WorldRendererCommon {
}
}

changeBackgroundColor (color: [number, number, number]): void {
this.scene.background = new THREE.Color(color[0], color[1], color[2])
}

timeUpdated (newTime: number): void {
const nightTime = 13_500
const morningStart = 23_000
Expand Down Expand Up @@ -365,9 +368,47 @@ export class WorldRendererThree extends WorldRendererCommon {
}
}

setSectionDirty (pos, value = true) {
setSectionDirty (...args: Parameters<WorldRendererCommon['setSectionDirty']>) {
const [pos] = args
this.cleanChunkTextures(pos.x, pos.z) // todo don't do this!
super.setSectionDirty(pos, value)
super.setSectionDirty(...args)
}

setHighlightCursorBlock (blockPos: typeof this.cursorBlock, shapePositions?: Array<{ position: any; width: any; height: any; depth: any; }>): void {
this.cursorBlock = blockPos
if (blockPos && this.interactionLines && blockPos.equals(this.interactionLines.blockPos)) {
return
}
if (this.interactionLines !== null) {
this.scene.remove(this.interactionLines.mesh)
this.interactionLines = null
}
if (blockPos === null) {
return
}

const group = new THREE.Group()
for (const { position, width, height, depth } of shapePositions ?? []) {
const scale = [1.0001 * width, 1.0001 * height, 1.0001 * depth] as const
const geometry = new THREE.BoxGeometry(...scale)
const lines = new LineSegmentsGeometry().fromEdgesGeometry(new THREE.EdgesGeometry(geometry))
const wireframe = new Wireframe(lines, this.threejsCursorLineMaterial)
const pos = blockPos.plus(position)
wireframe.position.set(pos.x, pos.y, pos.z)
wireframe.computeLineDistances()
group.add(wireframe)
}
this.scene.add(group)
this.interactionLines = { blockPos, mesh: group }
}

static getRendererInfo (renderer: THREE.WebGLRenderer) {
try {
const gl = renderer.getContext()
return `${gl.getParameter(gl.getExtension('WEBGL_debug_renderer_info')!.UNMASKED_RENDERER_WEBGL)} powered by three.js r{THREE.REVISION}`
} catch (err) {
console.warn('Failed to get renderer info', err)
}
}
}

Expand Down
1 change: 0 additions & 1 deletion src/optionsGuiScheme.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ export const guiOptionsScheme: {
tooltip: 'Additional distance to keep the chunks loading before unloading them by marking them as too far',
},
handDisplay: {},
neighborChunkUpdates: {},
renderDebug: {
values: [
'advanced',
Expand Down
15 changes: 4 additions & 11 deletions src/react/DebugOverlay.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useEffect, useRef, useMemo, useState } from 'react'
import * as THREE from 'three'
import type { Block } from 'prismarine-block'
import { getFixedFilesize } from '../downloadAndOpenFile'
import { options } from '../optionsStorage'
import worldInteractions from '../worldInteractions'
import styles from './DebugOverlay.module.css'

export default () => {
Expand Down Expand Up @@ -35,10 +35,10 @@ export default () => {
const [day, setDay] = useState(0)
const [entitiesCount, setEntitiesCount] = useState(0)
const [dimension, setDimension] = useState('')
const [cursorBlock, setCursorBlock] = useState<typeof worldInteractions.cursorBlock>(null)
const [rendererDevice, setRendererDevice] = useState('')
const [cursorBlock, setCursorBlock] = useState<Block | null>(null)
const minecraftYaw = useRef(0)
const minecraftQuad = useRef(0)
const { rendererDevice } = viewer.world

const quadsDescription = [
'north (towards negative Z)',
Expand Down Expand Up @@ -118,13 +118,6 @@ export default () => {
managePackets('sent', name, data)
})

try {
const gl = window.renderer.getContext()
setRendererDevice(gl.getParameter(gl.getExtension('WEBGL_debug_renderer_info')!.UNMASKED_RENDERER_WEBGL))
} catch (err) {
console.warn(err)
}

return () => {
document.removeEventListener('keydown', handleF3)
clearInterval(packetsUpdateInterval)
Expand Down Expand Up @@ -159,7 +152,7 @@ export default () => {
</div>

<div className={styles['debug-right-side']}>
<p>Renderer: {rendererDevice} powered by three.js r{THREE.REVISION}</p>
<p>Renderer: {rendererDevice}</p>
<div className={styles.empty} />
{cursorBlock ? (<>
<p>{cursorBlock.name}</p>
Expand Down
Loading

0 comments on commit a4c86d7

Please sign in to comment.