minecraft-renderer 0.1.107 → 0.1.108

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.
Files changed (90) hide show
  1. package/dist/lightOwnerWorker.js +9 -0
  2. package/dist/lightOwnerWorker.js.map +7 -0
  3. package/dist/mesher.js +1 -1
  4. package/dist/mesher.js.map +2 -2
  5. package/dist/mesherWasm.js +222 -222
  6. package/dist/minecraft-renderer.js +80 -80
  7. package/dist/minecraft-renderer.js.meta.json +1 -1
  8. package/dist/threeWorker.js +513 -513
  9. package/package.json +1 -1
  10. package/src/bundler/bundlePrepare.test.ts +25 -0
  11. package/src/bundler/bundlePrepare.ts +1 -1
  12. package/src/graphicsBackend/appViewer.ts +15 -0
  13. package/src/graphicsBackend/config.ts +5 -0
  14. package/src/graphicsBackend/rendererDefaultOptions.test.ts +51 -0
  15. package/src/graphicsBackend/rendererDefaultOptions.ts +13 -1
  16. package/src/graphicsBackend/rendererOptionsSync.ts +3 -3
  17. package/src/lib/blockEntityLightRegistry.ts +4 -0
  18. package/src/lib/worldrendererCommon.lightOwner.test.ts +916 -0
  19. package/src/lib/worldrendererCommon.removeColumn.test.ts +16 -0
  20. package/src/lib/worldrendererCommon.ts +329 -67
  21. package/src/lighting/cache/rendererLightCache.test.ts +158 -0
  22. package/src/lighting/cache/rendererLightCache.ts +248 -0
  23. package/src/lighting/coordinator/clientLightCoordinator.test.ts +102 -0
  24. package/src/lighting/coordinator/clientLightCoordinator.ts +574 -0
  25. package/src/lighting/debug/clientLightProbe.test.ts +24 -0
  26. package/src/lighting/debug/clientLightProbe.ts +20 -0
  27. package/src/lighting/debug/clientLightTrace.test.ts +271 -0
  28. package/src/lighting/debug/clientLightTrace.ts +335 -0
  29. package/src/lighting/entity/entityLightController.test.ts +216 -0
  30. package/src/lighting/entity/entityLightController.ts +322 -0
  31. package/src/lighting/owner/clientLightOwner.test.ts +467 -0
  32. package/src/lighting/owner/clientLightOwner.ts +523 -0
  33. package/src/lighting/owner/lightOwnerHost.test.ts +78 -0
  34. package/src/lighting/owner/lightOwnerHost.ts +202 -0
  35. package/src/lighting/owner/lightOwnerWorker.ts +154 -0
  36. package/src/lighting/owner/updateLight1171OwnerPath.test.ts +68 -0
  37. package/src/lighting/owner/updateLightToOwnerEvents.ts +49 -0
  38. package/src/lighting/tables/lightTables1171.generated.json +10 -0
  39. package/src/lighting/tables/lightTables1171.test.ts +168 -0
  40. package/src/lighting/tables/lightTables1171.ts +59 -0
  41. package/src/lighting/versioning/clientLightVersions.test.ts +241 -0
  42. package/src/lighting/versioning/clientLightVersions.ts +203 -0
  43. package/src/lighting/versioning/pendingSectionFlush.test.ts +214 -0
  44. package/src/lighting/versioning/pendingSectionFlush.ts +123 -0
  45. package/src/mesher-shared/lightNibblePack.ts +26 -0
  46. package/src/mesher-shared/mcDataWorkerShim.cjs +9 -7
  47. package/src/mesher-shared/modelsGeometryCommon.test.ts +6 -1
  48. package/src/mesher-shared/shared.ts +48 -1
  49. package/src/three/chunkMeshManager.ts +26 -0
  50. package/src/three/entities.ts +77 -11
  51. package/src/three/entity/EntityMesh.ts +1 -0
  52. package/src/three/entity/legWalkAnimation.test.ts +11 -2
  53. package/src/three/entity/legWalkAnimation.ts +1 -9
  54. package/src/three/globalBlockBuffer.ts +94 -2
  55. package/src/three/globalLegacyBuffer.ts +94 -2
  56. package/src/three/graphicsBackendBase.ts +2 -9
  57. package/src/three/tests/globalBlockBuffer.test.ts +98 -0
  58. package/src/three/tests/globalLegacyBuffer.test.ts +108 -0
  59. package/src/three/worldRendererThree.ts +75 -34
  60. package/src/wasm-mesher/bridge/render-from-wasm.ts +46 -1
  61. package/src/wasm-mesher/runtime-build/wasm_mesher.d.ts +31 -5
  62. package/src/wasm-mesher/runtime-build/wasm_mesher.js +180 -5
  63. package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm +0 -0
  64. package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm.d.ts +11 -0
  65. package/src/wasm-mesher/tests/mesherLightDependencies.test.ts +83 -0
  66. package/src/wasm-mesher/tests/mesherWasmColumnTickSelect.test.ts +39 -0
  67. package/src/wasm-mesher/tests/mesherWasmConversionCache.test.ts +30 -1
  68. package/src/wasm-mesher/tests/mesherWasmTickParseCache.test.ts +73 -0
  69. package/src/wasm-mesher/tests/mesherWasmTickSchedule.test.ts +51 -0
  70. package/src/wasm-mesher/tests/mesherWasmTopologyPostCache.test.ts +78 -0
  71. package/src/wasm-mesher/worker/clientLightDarkPublication.probe.test.ts +32 -0
  72. package/src/wasm-mesher/worker/mesherLightDependencies.ts +43 -0
  73. package/src/wasm-mesher/worker/mesherWasm.ownerLightSync.test.ts +198 -0
  74. package/src/wasm-mesher/worker/mesherWasm.ts +506 -96
  75. package/src/wasm-mesher/worker/mesherWasmChunkBuffer.ts +1 -0
  76. package/src/wasm-mesher/worker/mesherWasmColumnTickSelect.ts +47 -0
  77. package/src/wasm-mesher/worker/mesherWasmConversionCache.ts +16 -5
  78. package/src/wasm-mesher/worker/mesherWasmLightDirty.test.ts +17 -1
  79. package/src/wasm-mesher/worker/mesherWasmLightDirty.ts +10 -0
  80. package/src/wasm-mesher/worker/mesherWasmLightMerge.test.ts +208 -0
  81. package/src/wasm-mesher/worker/mesherWasmLightMerge.ts +228 -0
  82. package/src/wasm-mesher/worker/mesherWasmOwnerLight.test.ts +161 -0
  83. package/src/wasm-mesher/worker/mesherWasmOwnerLight.ts +112 -0
  84. package/src/wasm-mesher/worker/mesherWasmTickParseCache.ts +45 -0
  85. package/src/wasm-mesher/worker/mesherWasmTickSchedule.ts +44 -0
  86. package/src/wasm-mesher/worker/mesherWasmTopologyPostCache.ts +63 -0
  87. package/src/worldView/types.ts +1 -0
  88. package/src/worldView/worldView.geometryReload.test.ts +68 -0
  89. package/src/worldView/worldView.spiral.test.ts +1 -1
  90. package/src/worldView/worldView.ts +46 -1
@@ -34,6 +34,25 @@ import { PlayerStateReactive } from '../playerState/playerState'
34
34
  import { IndexedData } from 'minecraft-data'
35
35
  import { WorldRendererConfig } from '../graphicsBackend/config'
36
36
  import { CameraCollisionBlockCache } from '../three/cameraCollisionBlockCache'
37
+ import { RendererLightCache } from '../lighting/cache/rendererLightCache'
38
+ import { ClientLightCoordinator, type ClientLightHost } from '../lighting/coordinator/clientLightCoordinator'
39
+ import {
40
+ bumpClientLightTraceSessionEpoch,
41
+ ingestRemoteClientLightTraceMessage,
42
+ isClientLightTraceEnabled,
43
+ isClientLightTraceMessage,
44
+ maybeEnableClientLightTraceFromLocation,
45
+ recordClientLightTrace,
46
+ setClientLightTraceConfigListener
47
+ } from '../lighting/debug/clientLightTrace'
48
+ import {
49
+ LIGHT_OWNER_WORKER_SCRIPT,
50
+ shouldAcceptMeshGeometry,
51
+ shouldSpawnClientLightOwner,
52
+ skyLightEnabledFromRendererState
53
+ } from '../lighting/owner/clientLightOwner'
54
+ import { shouldDispatchCoveringRemesh, validateMeshResultVersions } from '../lighting/versioning/clientLightVersions'
55
+ import type { FeedChunkPacketPayload } from '../worldView/types'
37
56
 
38
57
  function mod(x, n) {
39
58
  return ((x % n) + n) % n
@@ -47,6 +66,8 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
47
66
  worldSizeParams = { minY: 0, worldHeight: 256 }
48
67
  /** Block columns for third-person voxel DDA (renderer thread). */
49
68
  cameraCollisionBlockCache: CameraCollisionBlockCache
69
+ /** Packed block/sky light for entity sampling (renderer thread). */
70
+ rendererLightCache: RendererLightCache
50
71
  reactiveDebugParams = proxy({
51
72
  stopRendering: false,
52
73
  chunksRenderAboveOverride: undefined as number | undefined,
@@ -86,6 +107,8 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
86
107
  }>
87
108
  customTexturesDataUrl = undefined as string | undefined
88
109
  workers: any[] = []
110
+ /** Client light owner session and the version bookkeeping that keeps owner-lit meshes consistent. */
111
+ readonly lightCoordinator = new ClientLightCoordinator(this.createLightHost())
89
112
  viewerChunkPosition?: Vec3
90
113
  // Last viewer chunk-grid coords for which `onViewerChunkPositionChanged`
91
114
  // fired — throttles the hook to chunk-grid changes.
@@ -125,6 +148,8 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
125
148
  // Cumulative cache hit/miss counters (column-mode conversion cache).
126
149
  workersPreCacheHitsTotal = 0
127
150
  workersPreCacheMissesTotal = 0
151
+ workersPreWasmParseHitsTotal = 0
152
+ workersPreWasmParseMissesTotal = 0
128
153
  private static readonly PHASE_PERF_LOG_INTERVAL = 64
129
154
  geometryReceiveCount = {} as Record<number, number>
130
155
  allLoadedIn: undefined | number
@@ -154,7 +179,8 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
154
179
  // Geometry throttle: first dirty per section is instant, subsequent within window are grouped
155
180
  private sectionDirtyCount = new Map<string, number>()
156
181
  private sectionDirtyTimers = new Map<string, ReturnType<typeof setTimeout>>()
157
- private sectionDirtyPendingArgs = new Map<string, { pos: Vec3; value: boolean; useChangeWorker: boolean }>()
182
+ private sectionDirtyPendingArgs = new Map<string, { pos: Vec3; value: boolean; useChangeWorker: boolean; urgent?: boolean }>()
183
+ private sectionDirtyLightMeta = new Map<string, { lightPublicationVersion: number; worldGeneration?: number }>()
158
184
  private static readonly GEOMETRY_THROTTLE_THRESHOLD = 1
159
185
  private static readonly GEOMETRY_THROTTLE_DELAY = 100 // ms
160
186
 
@@ -224,11 +250,17 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
224
250
  public initOptions: GraphicsInitOptions
225
251
  ) {
226
252
  this.cameraCollisionBlockCache = new CameraCollisionBlockCache(this.displayOptions.version)
253
+ this.rendererLightCache = new RendererLightCache(this.displayOptions.version)
227
254
  this.snapshotInitialValues()
228
255
  this.worldRendererConfig = displayOptions.inWorldRenderingConfig
229
256
  this.playerStateReactive = displayOptions.playerStateReactive!
230
257
  this.playerStateUtils = getPlayerStateUtils(this.playerStateReactive)
231
258
  this.reactiveState = displayOptions.rendererState!
259
+ maybeEnableClientLightTraceFromLocation()
260
+ if (isClientLightTraceEnabled()) bumpClientLightTraceSessionEpoch()
261
+ setClientLightTraceConfigListener(() => {
262
+ this.lightCoordinator.syncTraceConfig()
263
+ })
232
264
  // this.mesherLogReader = new MesherLogReader(this)
233
265
  this.renderUpdateEmitter.on('update', () => {
234
266
  const loadedChunks = Object.keys(this.finishedChunks).length
@@ -355,6 +387,81 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
355
387
  for (let i = 0; i < numWorkers; i++) {
356
388
  this.workers.push(this.createMesherWorker())
357
389
  }
390
+ this.lightCoordinator.maybeSpawn()
391
+ this.lightCoordinator.syncTraceConfig()
392
+ }
393
+
394
+ setClientLightProbe(points: Array<[number, number, number]>) {
395
+ this.lightCoordinator.setProbe(points)
396
+ }
397
+
398
+ hasClientLightOwner(): boolean {
399
+ return this.lightCoordinator.hasOwner()
400
+ }
401
+
402
+ getClientLightOwnerWorker(): Worker | null {
403
+ return this.lightCoordinator.getWorker()
404
+ }
405
+
406
+ getClientLightOwnerFailureReason(): string | null {
407
+ return this.lightCoordinator.getFailureReason()
408
+ }
409
+
410
+ columnIncarnationFor(columnKey: string): number {
411
+ return this.lightCoordinator.columnIncarnationFor(columnKey)
412
+ }
413
+
414
+ private createLightHost(): ClientLightHost {
415
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
416
+ const renderer = this
417
+ return {
418
+ get version() {
419
+ return renderer.version
420
+ },
421
+ get workers() {
422
+ return renderer.workers
423
+ },
424
+ get rendererLightCache() {
425
+ return renderer.rendererLightCache
426
+ },
427
+ get worldRendererConfig() {
428
+ return renderer.worldRendererConfig
429
+ },
430
+ get worldSizeParams() {
431
+ return renderer.worldSizeParams
432
+ },
433
+ get playerStateReactive() {
434
+ return renderer.playerStateReactive
435
+ },
436
+ get sectionsWaiting() {
437
+ return renderer.sectionsWaiting
438
+ },
439
+ get loadedChunks() {
440
+ return renderer.loadedChunks
441
+ },
442
+ get sectionDirtyLightMeta() {
443
+ return renderer.sectionDirtyLightMeta
444
+ },
445
+ getSectionHeight: () => renderer.getSectionHeight(),
446
+ getWorkerNumber: (pos, updateAction) => renderer.getWorkerNumber(pos, updateAction),
447
+ setSectionDirty: (pos, value, useChangeWorker) => renderer.setSectionDirty(pos, value, useChangeWorker),
448
+ dispatchDirtyImmediate: (pos, value, useChangeWorker) => renderer._dispatchDirtyImmediate(pos, value, useChangeWorker),
449
+ createLightOwnerWorker: onMessage => initMesherWorker(onMessage, LIGHT_OWNER_WORKER_SCRIPT)
450
+ }
451
+ }
452
+
453
+ feedChunkPacket(payload: FeedChunkPacketPayload) {
454
+ const { kind, ...rest } = payload
455
+ const message = { type: kind, ...rest }
456
+ const isLightPacket = kind === 'setUpdateLightV17' || kind === 'setUpdateLightV16'
457
+ const ownerSession = this.lightCoordinator.session
458
+ if (ownerSession && isLightPacket) {
459
+ ownerSession.pushRawUpdateLight(kind, rest as Record<string, unknown>)
460
+ if (ownerSession.isReady) return
461
+ }
462
+ for (const worker of this.workers) {
463
+ worker.postMessage(message)
464
+ }
358
465
  }
359
466
 
360
467
  private syncMesherPoolSnapshot() {
@@ -402,6 +509,8 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
402
509
  this.sectionDirtyTimers.clear()
403
510
  this.sectionDirtyCount.clear()
404
511
  this.sectionDirtyPendingArgs.clear()
512
+ this.sectionDirtyLightMeta.clear()
513
+ this.lightCoordinator.clearPendingMesherState()
405
514
  this.reactiveState.world.mesherWork = false
406
515
  }
407
516
 
@@ -424,6 +533,9 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
424
533
  if (!this.active) return
425
534
 
426
535
  this.clearMesherPendingState()
536
+ // Worker topology revisions belong to the previous mesher lifetime.
537
+ // Reloaded columns replay owner snapshots through identical ingest.
538
+ this.lightCoordinator.forgetRequiredTopology()
427
539
  this.terminateAllMesherWorkers()
428
540
  this.initWorkers()
429
541
  await this.bootstrapMesherWorkers()
@@ -490,6 +602,15 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
490
602
  this.changeCardinalLight(value)
491
603
  })
492
604
  )
605
+ this.valtioUnsubs.push(
606
+ this.onReactivePlayerStateUpdated(
607
+ 'lightingDisabled',
608
+ value => {
609
+ this.lightCoordinator.session?.setSkyLightEnabled(skyLightEnabledFromRendererState({ lightingDisabled: value }))
610
+ },
611
+ false
612
+ )
613
+ )
493
614
  }
494
615
 
495
616
  watchReactiveConfig() {
@@ -498,6 +619,16 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
498
619
  setSkinsConfig({ apiEnabled: value })
499
620
  })
500
621
  )
622
+ this.valtioUnsubs.push(
623
+ this.onReactiveConfigUpdated(
624
+ 'enableClientLightOwner',
625
+ enabled => {
626
+ if (enabled) this.lightCoordinator.maybeSpawn()
627
+ else this.lightCoordinator.terminate()
628
+ },
629
+ false
630
+ )
631
+ )
501
632
  }
502
633
 
503
634
  async processMessageQueue(source: string) {
@@ -549,15 +680,49 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
549
680
  }
550
681
 
551
682
  handleMessage(rawData: any) {
552
- const data = rawData as MesherMainEvent
683
+ const data = rawData as MesherMainEvent & {
684
+ worldGeneration?: number
685
+ lightPublicationVersion?: number
686
+ clientLightRequestId?: number
687
+ clientLightEditSeq?: number
688
+ clientLightSessionEpoch?: number
689
+ }
553
690
  if (!this.active) return
691
+ if (isClientLightTraceMessage(rawData)) {
692
+ ingestRemoteClientLightTraceMessage(rawData)
693
+ return
694
+ }
554
695
  this.mesherLogReader?.workerMessageReceived(data.type, data)
555
- if (data.type !== 'geometry' || !this.debugStopGeometryUpdate) {
696
+ const staleOwnerGeometry = this.lightCoordinator.rejectStaleOwnerGeometry(data)
697
+ if (staleOwnerGeometry && data.type === 'geometry' && isClientLightTraceEnabled()) {
698
+ recordClientLightTrace({
699
+ phase: 'reject',
700
+ sectionKey: data.key,
701
+ requestId: data.clientLightRequestId,
702
+ editSeq: data.clientLightEditSeq,
703
+ sessionEpoch: data.clientLightSessionEpoch,
704
+ lightVersion: data.lightPublicationVersion,
705
+ worldGeneration: data.worldGeneration
706
+ })
707
+ }
708
+ if ((data.type !== 'geometry' || !this.debugStopGeometryUpdate) && !staleOwnerGeometry) {
556
709
  const start = performance.now()
557
710
  this.handleWorkerMessage(data as WorkerReceive)
558
711
  this.workerCustomHandleTime += performance.now() - start
559
712
  }
560
713
  if (data.type === 'geometry') {
714
+ if (!staleOwnerGeometry && isClientLightTraceEnabled()) {
715
+ recordClientLightTrace({
716
+ phase: 'receive',
717
+ sectionKey: data.key,
718
+ requestId: data.clientLightRequestId,
719
+ editSeq: data.clientLightEditSeq,
720
+ sessionEpoch: data.clientLightSessionEpoch,
721
+ lightVersion: data.lightPublicationVersion,
722
+ worldGeneration: data.worldGeneration,
723
+ workerIndex: data.workerIndex
724
+ })
725
+ }
561
726
  this.logWorkerWork(() => `-> ${data.workerIndex} geometry ${data.key} ${JSON.stringify({ dataSize: JSON.stringify(data).length })}`)
562
727
  this.geometryReceiveCount[data.workerIndex] ??= 0
563
728
  this.geometryReceiveCount[data.workerIndex]++
@@ -567,6 +732,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
567
732
  if (data.type === 'sectionFinished') {
568
733
  // on after load & unload section
569
734
  this.logWorkerWork(`<- ${data.workerIndex} sectionFinished ${data.key} ${JSON.stringify({ processTime: data.processTime })}`)
735
+ const rejectedFinish = this.lightCoordinator.consumeRejectedFinish(data.key)
570
736
  if (!this.sectionsWaiting.has(data.key)) {
571
737
  console.debug(`sectionFinished for non-outstanding section ${data.key} (viewDistance=${this.viewDistance})`)
572
738
  return
@@ -574,7 +740,14 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
574
740
  this.sectionsWaiting.set(data.key, this.sectionsWaiting.get(data.key)! - 1)
575
741
  if (this.sectionsWaiting.get(data.key) === 0) {
576
742
  this.sectionsWaiting.delete(data.key)
577
- this.finishedSections[data.key] = true
743
+ if (!rejectedFinish) {
744
+ this.lightCoordinator.forgetCovering(data.key)
745
+ this.finishedSections[data.key] = true
746
+ }
747
+ }
748
+ if (rejectedFinish) {
749
+ this.lightCoordinator.maybeDispatchCoveringRemesh(data.key)
750
+ if (!this.finishedSections[data.key]) return
578
751
  }
579
752
 
580
753
  const chunkCoords = data.key.split(',').map(Number)
@@ -648,6 +821,8 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
648
821
  this.workersPreOtherAverageTime = (this.workersPreOtherAverageTime * (n - 1) + po) / n
649
822
  this.workersPreCacheHitsTotal += pch
650
823
  this.workersPreCacheMissesTotal += pcm
824
+ if (typeof data.preWasmParseHits === 'number') this.workersPreWasmParseHitsTotal += data.preWasmParseHits
825
+ if (typeof data.preWasmParseMisses === 'number') this.workersPreWasmParseMissesTotal += data.preWasmParseMisses
651
826
  if (this.worldRendererConfig.debugWasmPerf && n % WorldRendererCommon.PHASE_PERF_LOG_INTERVAL === 0) {
652
827
  const total = this.workersPreAverageTime + this.workersWasmAverageTime + this.workersPostAverageTime
653
828
  const prePct = total > 0 ? (this.workersPreAverageTime / total) * 100 : 0
@@ -662,9 +837,11 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
662
837
  const nbrPerAvg = nbrCnt > 0 ? this.workersPreNeighborConvertAverageTime / nbrCnt : 0
663
838
  const cacheTotal = this.workersPreCacheHitsTotal + this.workersPreCacheMissesTotal
664
839
  const cacheHitPct = cacheTotal > 0 ? (this.workersPreCacheHitsTotal / cacheTotal) * 100 : 0
840
+ const wasmParseTotal = this.workersPreWasmParseHitsTotal + this.workersPreWasmParseMissesTotal
841
+ const wasmParseHitPct = wasmParseTotal > 0 ? (this.workersPreWasmParseHitsTotal / wasmParseTotal) * 100 : 0
665
842
  // eslint-disable-next-line no-console
666
843
  console.log(
667
- `[wasm-mesher perf] n=${n} pre=${this.workersPreAverageTime.toFixed(2)}ms (${prePct.toFixed(1)}%) wasm=${this.workersWasmAverageTime.toFixed(2)}ms (${wasmPct.toFixed(1)}%) post=${this.workersPostAverageTime.toFixed(2)}ms (${postPct.toFixed(1)}%) | pre.targetConvert=${this.workersPreTargetConvertAverageTime.toFixed(2)}ms (${tgtPct.toFixed(1)}%) pre.neighborConvert=${this.workersPreNeighborConvertAverageTime.toFixed(2)}ms (${nbrPct.toFixed(1)}%) [n̄=${nbrCnt.toFixed(2)}, per-nbr=${nbrPerAvg.toFixed(2)}ms] pre.typedArrayBuild=${this.workersPreTypedArrayBuildAverageTime.toFixed(2)}ms (${tabPct.toFixed(1)}%) pre.other=${this.workersPreOtherAverageTime.toFixed(2)}ms (${othPct.toFixed(1)}%) | pre.cache hits=${this.workersPreCacheHitsTotal} misses=${this.workersPreCacheMissesTotal} (${cacheHitPct.toFixed(1)}% hit)`
844
+ `[wasm-mesher perf] n=${n} pre=${this.workersPreAverageTime.toFixed(2)}ms (${prePct.toFixed(1)}%) wasm=${this.workersWasmAverageTime.toFixed(2)}ms (${wasmPct.toFixed(1)}%) post=${this.workersPostAverageTime.toFixed(2)}ms (${postPct.toFixed(1)}%) | pre.targetConvert=${this.workersPreTargetConvertAverageTime.toFixed(2)}ms (${tgtPct.toFixed(1)}%) pre.neighborConvert=${this.workersPreNeighborConvertAverageTime.toFixed(2)}ms (${nbrPct.toFixed(1)}%) [n̄=${nbrCnt.toFixed(2)}, per-nbr=${nbrPerAvg.toFixed(2)}ms] pre.typedArrayBuild=${this.workersPreTypedArrayBuildAverageTime.toFixed(2)}ms (${tabPct.toFixed(1)}%) pre.other=${this.workersPreOtherAverageTime.toFixed(2)}ms (${othPct.toFixed(1)}%) | pre.cache hits=${this.workersPreCacheHitsTotal} misses=${this.workersPreCacheMissesTotal} (${cacheHitPct.toFixed(1)}% hit) | pre.wasmParse hits=${this.workersPreWasmParseHitsTotal} misses=${this.workersPreWasmParseMissesTotal} (${wasmParseHitPct.toFixed(1)}% hit)`
668
845
  )
669
846
  }
670
847
  }
@@ -776,11 +953,13 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
776
953
  abstract updateShowChunksBorder(value: boolean): void
777
954
 
778
955
  resetWorld() {
956
+ this.lightCoordinator.interactiveSections.clear()
779
957
  // destroy workers
780
958
  for (const worker of this.workers) {
781
959
  worker.terminate()
782
960
  }
783
961
  this.workers = []
962
+ this.lightCoordinator.terminate()
784
963
  }
785
964
 
786
965
  async resetWorkers() {
@@ -889,7 +1068,8 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
889
1068
  x,
890
1069
  z,
891
1070
  chunk,
892
- customBlockModels: customBlockModels || undefined
1071
+ customBlockModels: customBlockModels || undefined,
1072
+ isLightUpdate
893
1073
  })
894
1074
  }
895
1075
  // WASM mesher pushes heightmaps from `processColumnTick` after each
@@ -910,6 +1090,15 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
910
1090
  )
911
1091
  this.mesherLogReader?.chunkReceived(x, z, chunk.length)
912
1092
  this.cameraCollisionBlockCache.ingestColumn(x, z, chunk)
1093
+ this.rendererLightCache.ingestColumn(x, z, chunk)
1094
+ this.lightCoordinator.session?.ingestColumn({
1095
+ chunkX: x,
1096
+ chunkZ: z,
1097
+ chunkJson: chunk,
1098
+ version: this.version,
1099
+ worldMinY: this.worldSizeParams.minY,
1100
+ worldHeight: this.worldSizeParams.worldHeight
1101
+ })
913
1102
  const sectionHeight = this.getSectionHeight()
914
1103
  const CHUNK_SIZE = 16
915
1104
 
@@ -935,7 +1124,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
935
1124
  this.checkAllFinished()
936
1125
  }
937
1126
 
938
- removeColumn(x, z) {
1127
+ removeColumn(x, z, opts?: { preserveClientLight?: boolean }) {
939
1128
  delete this.loadedChunks[`${x},${z}`]
940
1129
  // Cancel any pending heightmap debounce for this chunk
941
1130
  const debounceKey = `${x},${z}`
@@ -951,6 +1140,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
951
1140
  this.sectionDirtyTimers.delete(key)
952
1141
  this.sectionDirtyCount.delete(key)
953
1142
  this.sectionDirtyPendingArgs.delete(key)
1143
+ this.sectionDirtyLightMeta.delete(key)
954
1144
  }
955
1145
  }
956
1146
  for (let i = 0; i < this.workers.length; i++) {
@@ -975,10 +1165,13 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
975
1165
  )
976
1166
  }
977
1167
  this.sectionsWaiting.delete(sectionKey)
1168
+ this.lightCoordinator.forgetInteractiveSection(sectionKey)
978
1169
  delete this.finishedSections[sectionKey]
979
1170
  }
980
1171
  this.highestBlocksByChunks.delete(`${x},${z}`)
981
1172
  this.cameraCollisionBlockCache.removeColumn(x, z)
1173
+ this.rendererLightCache.removeColumn(x, z)
1174
+ this.lightCoordinator.onColumnRemoved(x, z, opts)
982
1175
  const heightmapKey = `${Math.floor(x / 16)},${Math.floor(z / 16)}`
983
1176
  delete this.reactiveState.world.heightmaps[heightmapKey]
984
1177
 
@@ -1056,15 +1249,19 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
1056
1249
  ({ x, z, chunk, worldConfig, isLightUpdate }) => {
1057
1250
  this.worldSizeParams = worldConfig
1058
1251
  this.cameraCollisionBlockCache.setWorldBounds(worldConfig.minY, worldConfig.worldHeight)
1252
+ this.rendererLightCache.setWorldBounds(worldConfig.minY, worldConfig.worldHeight)
1059
1253
  this.queuedChunks.add(`${x},${z}`)
1060
1254
  const args = [x, z, chunk, isLightUpdate]
1255
+ const incarnation = this.columnIncarnationFor(`${x},${z}`)
1061
1256
  if (!currentLoadChunkBatch) {
1062
1257
  // add a setting to use debounce instead
1063
1258
  currentLoadChunkBatch = {
1064
1259
  data: [],
1065
1260
  timeout: setTimeout(() => {
1066
- for (const args of currentLoadChunkBatch!.data) {
1067
- this.queuedChunks.delete(`${args[0]},${args[1]}`)
1261
+ for (const { args, incarnation } of currentLoadChunkBatch!.data) {
1262
+ const key = `${args[0]},${args[1]}`
1263
+ this.queuedChunks.delete(key)
1264
+ if (this.columnIncarnationFor(key) !== incarnation) continue
1068
1265
  this.addColumn(...(args as Parameters<typeof this.addColumn>))
1069
1266
  }
1070
1267
  for (const fn of this.queuedFunctions) {
@@ -1075,7 +1272,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
1075
1272
  }, this.worldRendererConfig.addChunksBatchWaitTime)
1076
1273
  }
1077
1274
  }
1078
- currentLoadChunkBatch.data.push(args)
1275
+ currentLoadChunkBatch.data.push({ args, incarnation })
1079
1276
  },
1080
1277
  signal
1081
1278
  )
@@ -1092,8 +1289,8 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
1092
1289
  bindAbortableListener(
1093
1290
  worldEmitter,
1094
1291
  'unloadChunk',
1095
- ({ x, z }) => {
1096
- this.removeColumn(x, z)
1292
+ ({ x, z, preserveClientLight }) => {
1293
+ this.removeColumn(x, z, { preserveClientLight })
1097
1294
  },
1098
1295
  signal
1099
1296
  )
@@ -1160,6 +1357,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
1160
1357
  'onWorldSwitch',
1161
1358
  () => {
1162
1359
  this.cameraCollisionBlockCache.clear()
1360
+ this.rendererLightCache.clear()
1163
1361
  for (const fn of this.onWorldSwitched) {
1164
1362
  try {
1165
1363
  fn()
@@ -1262,43 +1460,49 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
1262
1460
  if (stateId !== undefined) {
1263
1461
  this.cameraCollisionBlockCache.setBlockStateId(pos.x, pos.y, pos.z, stateId)
1264
1462
  }
1265
- this.setSectionDirty(pos, true, true)
1266
- if (this.neighborChunkUpdates) {
1267
- const CHUNK_SIZE = 16
1268
- const sectionHeight = this.getSectionHeight()
1269
- if ((pos.x & 15) === 0) this.setSectionDirty(pos.offset(-CHUNK_SIZE, 0, 0), true, true)
1270
- if ((pos.x & 15) === 15) this.setSectionDirty(pos.offset(CHUNK_SIZE, 0, 0), true, true)
1271
- if ((pos.y & (sectionHeight - 1)) === 0) this.setSectionDirty(pos.offset(0, -sectionHeight, 0), true, true)
1272
- if ((pos.y & (sectionHeight - 1)) === sectionHeight - 1) this.setSectionDirty(pos.offset(0, sectionHeight, 0), true, true)
1273
- if ((pos.z & 15) === 0) this.setSectionDirty(pos.offset(0, 0, -CHUNK_SIZE), true, true)
1274
- if ((pos.z & 15) === 15) this.setSectionDirty(pos.offset(0, 0, CHUNK_SIZE), true, true)
1275
-
1276
- if (needAoRecalculation) {
1277
- // top view neighbors
1278
- if ((pos.x & 15) === 0 && (pos.z & 15) === 0) this.setSectionDirty(pos.offset(-CHUNK_SIZE, 0, -CHUNK_SIZE), true, true)
1279
- if ((pos.x & 15) === 15 && (pos.z & 15) === 0) this.setSectionDirty(pos.offset(CHUNK_SIZE, 0, -CHUNK_SIZE), true, true)
1280
- if ((pos.x & 15) === 0 && (pos.z & 15) === 15) this.setSectionDirty(pos.offset(-CHUNK_SIZE, 0, CHUNK_SIZE), true, true)
1281
- if ((pos.x & 15) === 15 && (pos.z & 15) === 15) this.setSectionDirty(pos.offset(CHUNK_SIZE, 0, CHUNK_SIZE), true, true)
1282
-
1283
- // side view neighbors (but ignore updates above)
1284
- // z view neighbors
1285
- if ((pos.x & 15) === 0 && (pos.y & (sectionHeight - 1)) === 0) this.setSectionDirty(pos.offset(-CHUNK_SIZE, -sectionHeight, 0), true, true)
1286
- if ((pos.x & 15) === 15 && (pos.y & (sectionHeight - 1)) === 0) this.setSectionDirty(pos.offset(CHUNK_SIZE, -sectionHeight, 0), true, true)
1287
-
1288
- // x view neighbors
1289
- if ((pos.z & 15) === 0 && (pos.y & (sectionHeight - 1)) === 0) this.setSectionDirty(pos.offset(0, -sectionHeight, -CHUNK_SIZE), true, true)
1290
- if ((pos.z & 15) === 15 && (pos.y & (sectionHeight - 1)) === 0) this.setSectionDirty(pos.offset(0, -sectionHeight, CHUNK_SIZE), true, true)
1291
-
1292
- // x & z neighbors
1293
- if ((pos.y & (sectionHeight - 1)) === 0 && (pos.x & 15) === 0 && (pos.z & 15) === 0)
1294
- this.setSectionDirty(pos.offset(-CHUNK_SIZE, -sectionHeight, -CHUNK_SIZE), true, true)
1295
- if ((pos.y & (sectionHeight - 1)) === 0 && (pos.x & 15) === 15 && (pos.z & 15) === 0)
1296
- this.setSectionDirty(pos.offset(CHUNK_SIZE, -sectionHeight, -CHUNK_SIZE), true, true)
1297
- if ((pos.y & (sectionHeight - 1)) === 0 && (pos.x & 15) === 0 && (pos.z & 15) === 15)
1298
- this.setSectionDirty(pos.offset(-CHUNK_SIZE, -sectionHeight, CHUNK_SIZE), true, true)
1299
- if ((pos.y & (sectionHeight - 1)) === 0 && (pos.x & 15) === 15 && (pos.z & 15) === 15)
1300
- this.setSectionDirty(pos.offset(CHUNK_SIZE, -sectionHeight, CHUNK_SIZE), true, true)
1463
+ this.lightCoordinator.noteBlockChange(pos, stateId, this.neighborChunkUpdates)
1464
+ this.lightCoordinator.nextDirtyUrgent = true
1465
+ try {
1466
+ this.setSectionDirty(pos, true, true)
1467
+ if (this.neighborChunkUpdates) {
1468
+ const CHUNK_SIZE = 16
1469
+ const sectionHeight = this.getSectionHeight()
1470
+ if ((pos.x & 15) === 0) this.setSectionDirty(pos.offset(-CHUNK_SIZE, 0, 0), true, true)
1471
+ if ((pos.x & 15) === 15) this.setSectionDirty(pos.offset(CHUNK_SIZE, 0, 0), true, true)
1472
+ if ((pos.y & (sectionHeight - 1)) === 0) this.setSectionDirty(pos.offset(0, -sectionHeight, 0), true, true)
1473
+ if ((pos.y & (sectionHeight - 1)) === sectionHeight - 1) this.setSectionDirty(pos.offset(0, sectionHeight, 0), true, true)
1474
+ if ((pos.z & 15) === 0) this.setSectionDirty(pos.offset(0, 0, -CHUNK_SIZE), true, true)
1475
+ if ((pos.z & 15) === 15) this.setSectionDirty(pos.offset(0, 0, CHUNK_SIZE), true, true)
1476
+
1477
+ if (needAoRecalculation) {
1478
+ // top view neighbors
1479
+ if ((pos.x & 15) === 0 && (pos.z & 15) === 0) this.setSectionDirty(pos.offset(-CHUNK_SIZE, 0, -CHUNK_SIZE), true, true)
1480
+ if ((pos.x & 15) === 15 && (pos.z & 15) === 0) this.setSectionDirty(pos.offset(CHUNK_SIZE, 0, -CHUNK_SIZE), true, true)
1481
+ if ((pos.x & 15) === 0 && (pos.z & 15) === 15) this.setSectionDirty(pos.offset(-CHUNK_SIZE, 0, CHUNK_SIZE), true, true)
1482
+ if ((pos.x & 15) === 15 && (pos.z & 15) === 15) this.setSectionDirty(pos.offset(CHUNK_SIZE, 0, CHUNK_SIZE), true, true)
1483
+
1484
+ // side view neighbors (but ignore updates above)
1485
+ // z view neighbors
1486
+ if ((pos.x & 15) === 0 && (pos.y & (sectionHeight - 1)) === 0) this.setSectionDirty(pos.offset(-CHUNK_SIZE, -sectionHeight, 0), true, true)
1487
+ if ((pos.x & 15) === 15 && (pos.y & (sectionHeight - 1)) === 0) this.setSectionDirty(pos.offset(CHUNK_SIZE, -sectionHeight, 0), true, true)
1488
+
1489
+ // x view neighbors
1490
+ if ((pos.z & 15) === 0 && (pos.y & (sectionHeight - 1)) === 0) this.setSectionDirty(pos.offset(0, -sectionHeight, -CHUNK_SIZE), true, true)
1491
+ if ((pos.z & 15) === 15 && (pos.y & (sectionHeight - 1)) === 0) this.setSectionDirty(pos.offset(0, -sectionHeight, CHUNK_SIZE), true, true)
1492
+
1493
+ // x & z neighbors
1494
+ if ((pos.y & (sectionHeight - 1)) === 0 && (pos.x & 15) === 0 && (pos.z & 15) === 0)
1495
+ this.setSectionDirty(pos.offset(-CHUNK_SIZE, -sectionHeight, -CHUNK_SIZE), true, true)
1496
+ if ((pos.y & (sectionHeight - 1)) === 0 && (pos.x & 15) === 15 && (pos.z & 15) === 0)
1497
+ this.setSectionDirty(pos.offset(CHUNK_SIZE, -sectionHeight, -CHUNK_SIZE), true, true)
1498
+ if ((pos.y & (sectionHeight - 1)) === 0 && (pos.x & 15) === 0 && (pos.z & 15) === 15)
1499
+ this.setSectionDirty(pos.offset(-CHUNK_SIZE, -sectionHeight, CHUNK_SIZE), true, true)
1500
+ if ((pos.y & (sectionHeight - 1)) === 0 && (pos.x & 15) === 15 && (pos.z & 15) === 15)
1501
+ this.setSectionDirty(pos.offset(CHUNK_SIZE, -sectionHeight, CHUNK_SIZE), true, true)
1502
+ }
1301
1503
  }
1504
+ } finally {
1505
+ this.lightCoordinator.nextDirtyUrgent = false
1302
1506
  }
1303
1507
  }
1304
1508
 
@@ -1389,6 +1593,28 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
1389
1593
  const sectionHeight = this.getSectionHeight()
1390
1594
  const key = `${Math.floor(pos.x / CHUNK_SIZE) * CHUNK_SIZE},${Math.floor(pos.y / sectionHeight) * sectionHeight},${Math.floor(pos.z / CHUNK_SIZE) * CHUNK_SIZE}`
1391
1595
 
1596
+ this.sectionDirtyLightMeta ??= new Map()
1597
+ if (this.lightCoordinator.nextDirtyLightMeta.lightPublicationVersion != null) {
1598
+ const prev = this.sectionDirtyLightMeta.get(key)
1599
+ const incomingVersion = this.lightCoordinator.nextDirtyLightMeta.lightPublicationVersion
1600
+ if (!prev || incomingVersion >= prev.lightPublicationVersion) {
1601
+ this.sectionDirtyLightMeta.set(key, {
1602
+ lightPublicationVersion: incomingVersion,
1603
+ worldGeneration: this.lightCoordinator.nextDirtyLightMeta.worldGeneration
1604
+ })
1605
+ }
1606
+ }
1607
+
1608
+ if (this.lightCoordinator.nextDirtyLightMeta.lightPublicationVersion != null || this.lightCoordinator.nextDirtyUrgent) {
1609
+ this._dispatchDirtyImmediate(pos, value, useChangeWorker)
1610
+ const pendingTimer = this.sectionDirtyTimers.get(key)
1611
+ if (pendingTimer) clearTimeout(pendingTimer)
1612
+ this.sectionDirtyTimers.delete(key)
1613
+ this.sectionDirtyCount.delete(key)
1614
+ this.sectionDirtyPendingArgs.delete(key)
1615
+ return
1616
+ }
1617
+
1392
1618
  const currentCount = (this.sectionDirtyCount.get(key) ?? 0) + 1
1393
1619
  this.sectionDirtyCount.set(key, currentCount)
1394
1620
 
@@ -1406,14 +1632,19 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
1406
1632
  this.sectionDirtyTimers.delete(key)
1407
1633
  this.sectionDirtyPendingArgs.delete(key)
1408
1634
  if (args) {
1409
- this._dispatchDirtyImmediate(args.pos, args.value, args.useChangeWorker)
1635
+ this.dispatchPendingDirty(args)
1410
1636
  }
1411
1637
  }, WorldRendererCommon.GEOMETRY_THROTTLE_DELAY)
1412
1638
  )
1413
1639
  }
1414
1640
  } else {
1415
1641
  // Subsequent requests — throttle: store latest args, existing timer will dispatch
1416
- this.sectionDirtyPendingArgs.set(key, { pos, value, useChangeWorker })
1642
+ this.sectionDirtyPendingArgs.set(key, {
1643
+ pos,
1644
+ value,
1645
+ useChangeWorker,
1646
+ urgent: this.lightCoordinator.nextDirtyUrgent || this.sectionDirtyPendingArgs.get(key)?.urgent
1647
+ })
1417
1648
 
1418
1649
  if (!this.sectionDirtyTimers.has(key)) {
1419
1650
  this.sectionDirtyTimers.set(
@@ -1424,7 +1655,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
1424
1655
  this.sectionDirtyTimers.delete(key)
1425
1656
  this.sectionDirtyPendingArgs.delete(key)
1426
1657
  if (args) {
1427
- this._dispatchDirtyImmediate(args.pos, args.value, args.useChangeWorker)
1658
+ this.dispatchPendingDirty(args)
1428
1659
  }
1429
1660
  }, WorldRendererCommon.GEOMETRY_THROTTLE_DELAY)
1430
1661
  )
@@ -1432,6 +1663,12 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
1432
1663
  }
1433
1664
  }
1434
1665
 
1666
+ private dispatchPendingDirty(args: { pos: Vec3; value: boolean; useChangeWorker: boolean; urgent?: boolean }) {
1667
+ this.lightCoordinator.withDirtyUrgency(!!args.urgent, () => {
1668
+ this._dispatchDirtyImmediate(args.pos, args.value, args.useChangeWorker)
1669
+ })
1670
+ }
1671
+
1435
1672
  /** Dispatch dirty message to worker without throttle (original logic) */
1436
1673
  private _dispatchDirtyImmediate(pos: Vec3, value: boolean, useChangeWorker: boolean) {
1437
1674
  this.reactiveState.world.mesherWork = true
@@ -1445,25 +1682,47 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
1445
1682
  // is always dispatched to the same worker
1446
1683
  const hash = this.getWorkerNumber(pos, useChangeWorker && (this.mesherLogger.active || this.worldRendererConfig.dedicatedChangeWorker))
1447
1684
  this.sectionsWaiting.set(key, (this.sectionsWaiting.get(key) ?? 0) + 1)
1448
- if (this.forceCallFromMesherReplayer) {
1449
- this.workers[hash].postMessage({
1450
- type: 'dirty',
1451
- x: pos.x,
1452
- y: pos.y,
1453
- z: pos.z,
1454
- value,
1455
- config: this.getMesherConfig()
1685
+ this.sectionDirtyLightMeta ??= new Map()
1686
+ const { lightMeta, ownerVersions, traceIds } = this.lightCoordinator.describeDirty(pos, key)
1687
+ const dirtyMessage = {
1688
+ type: 'dirty' as const,
1689
+ x: pos.x,
1690
+ y: pos.y,
1691
+ z: pos.z,
1692
+ value,
1693
+ config: this.getMesherConfig(),
1694
+ ...(lightMeta?.lightPublicationVersion != null
1695
+ ? {
1696
+ lightPublicationVersion: lightMeta.lightPublicationVersion,
1697
+ worldGeneration: lightMeta.worldGeneration
1698
+ }
1699
+ : {}),
1700
+ ...ownerVersions,
1701
+ ...(this.lightCoordinator.nextDirtyUrgent ? { urgent: true as const } : {}),
1702
+ ...(traceIds
1703
+ ? {
1704
+ clientLightRequestId: traceIds.requestId,
1705
+ clientLightEditSeq: traceIds.editSeq,
1706
+ clientLightSessionEpoch: traceIds.sessionEpoch
1707
+ }
1708
+ : {})
1709
+ }
1710
+ if (traceIds) {
1711
+ recordClientLightTrace({
1712
+ phase: 'mesherEnqueue',
1713
+ sectionKey: key,
1714
+ requestId: traceIds.requestId,
1715
+ editSeq: traceIds.editSeq,
1716
+ sessionEpoch: traceIds.sessionEpoch,
1717
+ lightVersion: lightMeta?.lightPublicationVersion,
1718
+ worldGeneration: lightMeta?.worldGeneration
1456
1719
  })
1720
+ }
1721
+ if (this.forceCallFromMesherReplayer) {
1722
+ this.workers[hash].postMessage(dirtyMessage)
1457
1723
  } else {
1458
1724
  this.toWorkerMessagesQueue[hash] ??= []
1459
- this.toWorkerMessagesQueue[hash].push({
1460
- type: 'dirty',
1461
- x: pos.x,
1462
- y: pos.y,
1463
- z: pos.z,
1464
- value,
1465
- config: this.getMesherConfig()
1466
- })
1725
+ this.toWorkerMessagesQueue[hash].push(dirtyMessage)
1467
1726
  this.dispatchMessages()
1468
1727
  }
1469
1728
  }
@@ -1544,12 +1803,15 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
1544
1803
  this.sectionDirtyTimers.clear()
1545
1804
  this.sectionDirtyCount.clear()
1546
1805
  this.sectionDirtyPendingArgs.clear()
1806
+ this.sectionDirtyLightMeta.clear()
1547
1807
 
1808
+ setClientLightTraceConfigListener(undefined)
1548
1809
  // Stop all workers
1549
1810
  for (const worker of this.workers) {
1550
1811
  worker.terminate()
1551
1812
  }
1552
1813
  this.workers = []
1814
+ this.lightCoordinator.terminate()
1553
1815
 
1554
1816
  // Stop and destroy sound system
1555
1817
  if (this.soundSystem) {