minecraft-renderer 0.1.25 → 0.1.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecraft-renderer",
3
- "version": "0.1.25",
3
+ "version": "0.1.27",
4
4
  "description": "The most Modular Minecraft world renderer with Three.js WebGL backend",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -119,7 +119,7 @@ export interface GraphicsBackend {
119
119
  updateCamera(pos: Vec3 | null, yaw: number, pitch: number): void
120
120
  soundSystem?: any
121
121
  backendMethods?: any
122
- getDebugOverlay?(): { entitiesString?: string, right?: string }
122
+ getDebugOverlay?(): { entitiesString?: string, left?: Record<string, string>, right?: Record<string, string> }
123
123
  }
124
124
 
125
125
  /** Graphics backend loader function type */
@@ -108,6 +108,13 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
108
108
  protocolCustomBlocks = new Map<string, CustomBlockModels>()
109
109
  private heightmapDebounceTimers = new Map<string, ReturnType<typeof setTimeout>>()
110
110
 
111
+ // Geometry throttle: first dirty per section is instant, subsequent within window are grouped
112
+ private sectionDirtyCount = new Map<string, number>()
113
+ private sectionDirtyTimers = new Map<string, ReturnType<typeof setTimeout>>()
114
+ private sectionDirtyPendingArgs = new Map<string, { pos: Vec3; value: boolean; useChangeWorker: boolean }>()
115
+ private static readonly GEOMETRY_THROTTLE_THRESHOLD = 1
116
+ private static readonly GEOMETRY_THROTTLE_DELAY = 100 // ms
117
+
111
118
  blockStateModelInfo = new Map<string, BlockStateModelInfo>()
112
119
 
113
120
  abstract outputFormat: 'threeJs' | 'webgpu'
@@ -682,6 +689,15 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
682
689
  clearTimeout(pendingTimer)
683
690
  this.heightmapDebounceTimers.delete(debounceKey)
684
691
  }
692
+ // Cancel any pending geometry throttle timers for sections in this chunk
693
+ for (const [key, timer] of this.sectionDirtyTimers) {
694
+ if (key.startsWith(`${x},`) && key.endsWith(`,${z}`)) {
695
+ clearTimeout(timer)
696
+ this.sectionDirtyTimers.delete(key)
697
+ this.sectionDirtyCount.delete(key)
698
+ this.sectionDirtyPendingArgs.delete(key)
699
+ }
700
+ }
685
701
  for (const worker of this.workers) {
686
702
  worker.postMessage({ type: 'unloadChunk', x, z })
687
703
  }
@@ -955,14 +971,65 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
955
971
  if (!this.forceCallFromMesherReplayer && this.mesherLogReader) return
956
972
 
957
973
  if (this.viewDistance === -1) throw new Error('viewDistance not set')
958
- this.reactiveState.world.mesherWork = true
974
+
959
975
  const distance = this.getDistance(pos)
960
976
  // todo shouldnt we check loadedChunks instead?
961
977
  if (!this.workers.length || distance[0] > this.viewDistance || distance[1] > this.viewDistance) return
978
+
979
+ // When unloading chunks (value=false) — always immediate, no throttle
980
+ if (!value) {
981
+ this._dispatchDirtyImmediate(pos, value, useChangeWorker)
982
+ return
983
+ }
984
+
962
985
  const CHUNK_SIZE = 16
963
986
  const sectionHeight = this.getSectionHeight()
964
987
  const key = `${Math.floor(pos.x / CHUNK_SIZE) * CHUNK_SIZE},${Math.floor(pos.y / sectionHeight) * sectionHeight},${Math.floor(pos.z / CHUNK_SIZE) * CHUNK_SIZE}`
965
- // if (this.sectionsOutstanding.has(key)) return
988
+
989
+ const currentCount = (this.sectionDirtyCount.get(key) ?? 0) + 1
990
+ this.sectionDirtyCount.set(key, currentCount)
991
+
992
+ if (currentCount <= WorldRendererCommon.GEOMETRY_THROTTLE_THRESHOLD) {
993
+ // First request in window — dispatch immediately for instant feedback
994
+ this._dispatchDirtyImmediate(pos, value, useChangeWorker)
995
+
996
+ // Schedule trailing dispatch after throttle window
997
+ if (!this.sectionDirtyTimers.has(key)) {
998
+ this.sectionDirtyTimers.set(key, setTimeout(() => {
999
+ const args = this.sectionDirtyPendingArgs.get(key)
1000
+ this.sectionDirtyCount.delete(key)
1001
+ this.sectionDirtyTimers.delete(key)
1002
+ this.sectionDirtyPendingArgs.delete(key)
1003
+ if (args) {
1004
+ this._dispatchDirtyImmediate(args.pos, args.value, args.useChangeWorker)
1005
+ }
1006
+ }, WorldRendererCommon.GEOMETRY_THROTTLE_DELAY))
1007
+ }
1008
+ } else {
1009
+ // Subsequent requests — throttle: store latest args, existing timer will dispatch
1010
+ this.sectionDirtyPendingArgs.set(key, { pos, value, useChangeWorker })
1011
+
1012
+ if (!this.sectionDirtyTimers.has(key)) {
1013
+ this.sectionDirtyTimers.set(key, setTimeout(() => {
1014
+ const args = this.sectionDirtyPendingArgs.get(key)
1015
+ this.sectionDirtyCount.delete(key)
1016
+ this.sectionDirtyTimers.delete(key)
1017
+ this.sectionDirtyPendingArgs.delete(key)
1018
+ if (args) {
1019
+ this._dispatchDirtyImmediate(args.pos, args.value, args.useChangeWorker)
1020
+ }
1021
+ }, WorldRendererCommon.GEOMETRY_THROTTLE_DELAY))
1022
+ }
1023
+ }
1024
+ }
1025
+
1026
+ /** Dispatch dirty message to worker without throttle (original logic) */
1027
+ private _dispatchDirtyImmediate(pos: Vec3, value: boolean, useChangeWorker: boolean) {
1028
+ this.reactiveState.world.mesherWork = true
1029
+ const CHUNK_SIZE = 16
1030
+ const sectionHeight = this.getSectionHeight()
1031
+ const key = `${Math.floor(pos.x / CHUNK_SIZE) * CHUNK_SIZE},${Math.floor(pos.y / sectionHeight) * sectionHeight},${Math.floor(pos.z / CHUNK_SIZE) * CHUNK_SIZE}`
1032
+
966
1033
  this.renderUpdateEmitter.emit('dirty', pos, value)
967
1034
  // Dispatch sections to workers based on position
968
1035
  // This guarantees uniformity accross workers and that a given section
@@ -981,7 +1048,6 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
981
1048
  } else {
982
1049
  this.toWorkerMessagesQueue[hash] ??= []
983
1050
  this.toWorkerMessagesQueue[hash].push({
984
- // this.workers[hash].postMessage({
985
1051
  type: 'dirty',
986
1052
  x: pos.x,
987
1053
  y: pos.y,
@@ -1054,6 +1120,14 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
1054
1120
  }
1055
1121
  this.heightmapDebounceTimers.clear()
1056
1122
 
1123
+ // Cancel all pending geometry throttle timers
1124
+ for (const timer of this.sectionDirtyTimers.values()) {
1125
+ clearTimeout(timer)
1126
+ }
1127
+ this.sectionDirtyTimers.clear()
1128
+ this.sectionDirtyCount.clear()
1129
+ this.sectionDirtyPendingArgs.clear()
1130
+
1057
1131
  // Stop all workers
1058
1132
  for (const worker of this.workers) {
1059
1133
  worker.terminate()
@@ -215,6 +215,11 @@ export const createGraphicsBackendBase = () => {
215
215
  get entitiesString() {
216
216
  return worldRenderer?.entities.getDebugString()
217
217
  },
218
+ get left() {
219
+ return {
220
+ 'Geo Memory': worldRenderer?.worldBlockGeometry.getEstimatedMemoryUsage().readable ?? '-'
221
+ }
222
+ },
218
223
  }),
219
224
  updateCamera(pos: Vec3 | null, yaw: number, pitch: number) {
220
225
  // Mark camera update event for frame timing visualization