minecraft-renderer 0.1.106 → 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 (98) 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 +105 -76
  7. package/dist/minecraft-renderer.js.meta.json +1 -1
  8. package/dist/threeWorker.js +537 -508
  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 +8 -0
  14. package/src/graphicsBackend/rendererDefaultOptions.test.ts +51 -0
  15. package/src/graphicsBackend/rendererDefaultOptions.ts +18 -1
  16. package/src/graphicsBackend/rendererOptionsSync.ts +4 -3
  17. package/src/lib/blockEntityLightRegistry.ts +4 -0
  18. package/src/lib/skyLight.ts +17 -0
  19. package/src/lib/worldrendererCommon.lightOwner.test.ts +916 -0
  20. package/src/lib/worldrendererCommon.removeColumn.test.ts +16 -0
  21. package/src/lib/worldrendererCommon.ts +340 -67
  22. package/src/lighting/cache/rendererLightCache.test.ts +158 -0
  23. package/src/lighting/cache/rendererLightCache.ts +248 -0
  24. package/src/lighting/coordinator/clientLightCoordinator.test.ts +102 -0
  25. package/src/lighting/coordinator/clientLightCoordinator.ts +574 -0
  26. package/src/lighting/debug/clientLightProbe.test.ts +24 -0
  27. package/src/lighting/debug/clientLightProbe.ts +20 -0
  28. package/src/lighting/debug/clientLightTrace.test.ts +271 -0
  29. package/src/lighting/debug/clientLightTrace.ts +335 -0
  30. package/src/lighting/entity/entityLightController.test.ts +216 -0
  31. package/src/lighting/entity/entityLightController.ts +322 -0
  32. package/src/lighting/owner/clientLightOwner.test.ts +467 -0
  33. package/src/lighting/owner/clientLightOwner.ts +523 -0
  34. package/src/lighting/owner/lightOwnerHost.test.ts +78 -0
  35. package/src/lighting/owner/lightOwnerHost.ts +202 -0
  36. package/src/lighting/owner/lightOwnerWorker.ts +154 -0
  37. package/src/lighting/owner/updateLight1171OwnerPath.test.ts +68 -0
  38. package/src/lighting/owner/updateLightToOwnerEvents.ts +49 -0
  39. package/src/lighting/tables/lightTables1171.generated.json +10 -0
  40. package/src/lighting/tables/lightTables1171.test.ts +168 -0
  41. package/src/lighting/tables/lightTables1171.ts +59 -0
  42. package/src/lighting/versioning/clientLightVersions.test.ts +241 -0
  43. package/src/lighting/versioning/clientLightVersions.ts +203 -0
  44. package/src/lighting/versioning/pendingSectionFlush.test.ts +214 -0
  45. package/src/lighting/versioning/pendingSectionFlush.ts +123 -0
  46. package/src/mesher-shared/lightNibblePack.ts +26 -0
  47. package/src/mesher-shared/mcDataWorkerShim.cjs +9 -7
  48. package/src/mesher-shared/modelsGeometryCommon.test.ts +6 -1
  49. package/src/mesher-shared/shared.ts +48 -1
  50. package/src/three/chunkMeshManager.ts +26 -0
  51. package/src/three/entities.ts +97 -8
  52. package/src/three/entity/EntityMesh.ts +1 -0
  53. package/src/three/entity/legWalkAnimation.test.ts +154 -0
  54. package/src/three/entity/legWalkAnimation.ts +193 -0
  55. package/src/three/globalBlockBuffer.ts +94 -2
  56. package/src/three/globalLegacyBuffer.ts +94 -2
  57. package/src/three/graphicsBackendBase.ts +2 -9
  58. package/src/three/modules/clouds.ts +191 -0
  59. package/src/three/modules/index.ts +4 -0
  60. package/src/three/modules/sunMoon.ts +151 -0
  61. package/src/three/skyboxRenderer.ts +15 -8
  62. package/src/three/tests/clouds.test.ts +59 -0
  63. package/src/three/tests/globalBlockBuffer.test.ts +98 -0
  64. package/src/three/tests/globalLegacyBuffer.test.ts +108 -0
  65. package/src/three/tests/waterFog.test.ts +25 -0
  66. package/src/three/waterFog.ts +28 -0
  67. package/src/three/worldRendererThree.ts +84 -39
  68. package/src/wasm-mesher/bridge/render-from-wasm.ts +46 -1
  69. package/src/wasm-mesher/runtime-build/wasm_mesher.d.ts +31 -5
  70. package/src/wasm-mesher/runtime-build/wasm_mesher.js +180 -5
  71. package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm +0 -0
  72. package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm.d.ts +11 -0
  73. package/src/wasm-mesher/tests/mesherLightDependencies.test.ts +83 -0
  74. package/src/wasm-mesher/tests/mesherWasmColumnTickSelect.test.ts +39 -0
  75. package/src/wasm-mesher/tests/mesherWasmConversionCache.test.ts +30 -1
  76. package/src/wasm-mesher/tests/mesherWasmTickParseCache.test.ts +73 -0
  77. package/src/wasm-mesher/tests/mesherWasmTickSchedule.test.ts +51 -0
  78. package/src/wasm-mesher/tests/mesherWasmTopologyPostCache.test.ts +78 -0
  79. package/src/wasm-mesher/worker/clientLightDarkPublication.probe.test.ts +32 -0
  80. package/src/wasm-mesher/worker/mesherLightDependencies.ts +43 -0
  81. package/src/wasm-mesher/worker/mesherWasm.ownerLightSync.test.ts +198 -0
  82. package/src/wasm-mesher/worker/mesherWasm.ts +506 -96
  83. package/src/wasm-mesher/worker/mesherWasmChunkBuffer.ts +1 -0
  84. package/src/wasm-mesher/worker/mesherWasmColumnTickSelect.ts +47 -0
  85. package/src/wasm-mesher/worker/mesherWasmConversionCache.ts +16 -5
  86. package/src/wasm-mesher/worker/mesherWasmLightDirty.test.ts +17 -1
  87. package/src/wasm-mesher/worker/mesherWasmLightDirty.ts +10 -0
  88. package/src/wasm-mesher/worker/mesherWasmLightMerge.test.ts +208 -0
  89. package/src/wasm-mesher/worker/mesherWasmLightMerge.ts +228 -0
  90. package/src/wasm-mesher/worker/mesherWasmOwnerLight.test.ts +161 -0
  91. package/src/wasm-mesher/worker/mesherWasmOwnerLight.ts +112 -0
  92. package/src/wasm-mesher/worker/mesherWasmTickParseCache.ts +45 -0
  93. package/src/wasm-mesher/worker/mesherWasmTickSchedule.ts +44 -0
  94. package/src/wasm-mesher/worker/mesherWasmTopologyPostCache.ts +63 -0
  95. package/src/worldView/types.ts +3 -0
  96. package/src/worldView/worldView.geometryReload.test.ts +68 -0
  97. package/src/worldView/worldView.spiral.test.ts +1 -1
  98. package/src/worldView/worldView.ts +49 -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
  }
@@ -727,6 +904,8 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
727
904
 
728
905
  timeUpdated?(newTime: number): void
729
906
 
907
+ moonPhaseUpdated?(phase: number): void
908
+
730
909
  /** Called when day-cycle sky-light bucket changes; Three.js overrides to remesh. */
731
910
  protected onDayCycleSkyLightChanged?(_skyLight: number): void
732
911
 
@@ -774,11 +953,13 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
774
953
  abstract updateShowChunksBorder(value: boolean): void
775
954
 
776
955
  resetWorld() {
956
+ this.lightCoordinator.interactiveSections.clear()
777
957
  // destroy workers
778
958
  for (const worker of this.workers) {
779
959
  worker.terminate()
780
960
  }
781
961
  this.workers = []
962
+ this.lightCoordinator.terminate()
782
963
  }
783
964
 
784
965
  async resetWorkers() {
@@ -887,7 +1068,8 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
887
1068
  x,
888
1069
  z,
889
1070
  chunk,
890
- customBlockModels: customBlockModels || undefined
1071
+ customBlockModels: customBlockModels || undefined,
1072
+ isLightUpdate
891
1073
  })
892
1074
  }
893
1075
  // WASM mesher pushes heightmaps from `processColumnTick` after each
@@ -908,6 +1090,15 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
908
1090
  )
909
1091
  this.mesherLogReader?.chunkReceived(x, z, chunk.length)
910
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
+ })
911
1102
  const sectionHeight = this.getSectionHeight()
912
1103
  const CHUNK_SIZE = 16
913
1104
 
@@ -933,7 +1124,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
933
1124
  this.checkAllFinished()
934
1125
  }
935
1126
 
936
- removeColumn(x, z) {
1127
+ removeColumn(x, z, opts?: { preserveClientLight?: boolean }) {
937
1128
  delete this.loadedChunks[`${x},${z}`]
938
1129
  // Cancel any pending heightmap debounce for this chunk
939
1130
  const debounceKey = `${x},${z}`
@@ -949,6 +1140,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
949
1140
  this.sectionDirtyTimers.delete(key)
950
1141
  this.sectionDirtyCount.delete(key)
951
1142
  this.sectionDirtyPendingArgs.delete(key)
1143
+ this.sectionDirtyLightMeta.delete(key)
952
1144
  }
953
1145
  }
954
1146
  for (let i = 0; i < this.workers.length; i++) {
@@ -973,10 +1165,13 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
973
1165
  )
974
1166
  }
975
1167
  this.sectionsWaiting.delete(sectionKey)
1168
+ this.lightCoordinator.forgetInteractiveSection(sectionKey)
976
1169
  delete this.finishedSections[sectionKey]
977
1170
  }
978
1171
  this.highestBlocksByChunks.delete(`${x},${z}`)
979
1172
  this.cameraCollisionBlockCache.removeColumn(x, z)
1173
+ this.rendererLightCache.removeColumn(x, z)
1174
+ this.lightCoordinator.onColumnRemoved(x, z, opts)
980
1175
  const heightmapKey = `${Math.floor(x / 16)},${Math.floor(z / 16)}`
981
1176
  delete this.reactiveState.world.heightmaps[heightmapKey]
982
1177
 
@@ -1054,15 +1249,19 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
1054
1249
  ({ x, z, chunk, worldConfig, isLightUpdate }) => {
1055
1250
  this.worldSizeParams = worldConfig
1056
1251
  this.cameraCollisionBlockCache.setWorldBounds(worldConfig.minY, worldConfig.worldHeight)
1252
+ this.rendererLightCache.setWorldBounds(worldConfig.minY, worldConfig.worldHeight)
1057
1253
  this.queuedChunks.add(`${x},${z}`)
1058
1254
  const args = [x, z, chunk, isLightUpdate]
1255
+ const incarnation = this.columnIncarnationFor(`${x},${z}`)
1059
1256
  if (!currentLoadChunkBatch) {
1060
1257
  // add a setting to use debounce instead
1061
1258
  currentLoadChunkBatch = {
1062
1259
  data: [],
1063
1260
  timeout: setTimeout(() => {
1064
- for (const args of currentLoadChunkBatch!.data) {
1065
- 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
1066
1265
  this.addColumn(...(args as Parameters<typeof this.addColumn>))
1067
1266
  }
1068
1267
  for (const fn of this.queuedFunctions) {
@@ -1073,7 +1272,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
1073
1272
  }, this.worldRendererConfig.addChunksBatchWaitTime)
1074
1273
  }
1075
1274
  }
1076
- currentLoadChunkBatch.data.push(args)
1275
+ currentLoadChunkBatch.data.push({ args, incarnation })
1077
1276
  },
1078
1277
  signal
1079
1278
  )
@@ -1090,8 +1289,8 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
1090
1289
  bindAbortableListener(
1091
1290
  worldEmitter,
1092
1291
  'unloadChunk',
1093
- ({ x, z }) => {
1094
- this.removeColumn(x, z)
1292
+ ({ x, z, preserveClientLight }) => {
1293
+ this.removeColumn(x, z, { preserveClientLight })
1095
1294
  },
1096
1295
  signal
1097
1296
  )
@@ -1158,6 +1357,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
1158
1357
  'onWorldSwitch',
1159
1358
  () => {
1160
1359
  this.cameraCollisionBlockCache.clear()
1360
+ this.rendererLightCache.clear()
1161
1361
  for (const fn of this.onWorldSwitched) {
1162
1362
  try {
1163
1363
  fn()
@@ -1194,6 +1394,15 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
1194
1394
  signal
1195
1395
  )
1196
1396
 
1397
+ bindAbortableListener(
1398
+ worldEmitter,
1399
+ 'moonPhase',
1400
+ phase => {
1401
+ this.moonPhaseUpdated?.(phase)
1402
+ },
1403
+ signal
1404
+ )
1405
+
1197
1406
  bindAbortableListener(
1198
1407
  worldEmitter,
1199
1408
  'biomeUpdate',
@@ -1251,43 +1460,49 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
1251
1460
  if (stateId !== undefined) {
1252
1461
  this.cameraCollisionBlockCache.setBlockStateId(pos.x, pos.y, pos.z, stateId)
1253
1462
  }
1254
- this.setSectionDirty(pos, true, true)
1255
- if (this.neighborChunkUpdates) {
1256
- const CHUNK_SIZE = 16
1257
- const sectionHeight = this.getSectionHeight()
1258
- if ((pos.x & 15) === 0) this.setSectionDirty(pos.offset(-CHUNK_SIZE, 0, 0), true, true)
1259
- if ((pos.x & 15) === 15) this.setSectionDirty(pos.offset(CHUNK_SIZE, 0, 0), true, true)
1260
- if ((pos.y & (sectionHeight - 1)) === 0) this.setSectionDirty(pos.offset(0, -sectionHeight, 0), true, true)
1261
- if ((pos.y & (sectionHeight - 1)) === sectionHeight - 1) this.setSectionDirty(pos.offset(0, sectionHeight, 0), true, true)
1262
- if ((pos.z & 15) === 0) this.setSectionDirty(pos.offset(0, 0, -CHUNK_SIZE), true, true)
1263
- if ((pos.z & 15) === 15) this.setSectionDirty(pos.offset(0, 0, CHUNK_SIZE), true, true)
1264
-
1265
- if (needAoRecalculation) {
1266
- // top view neighbors
1267
- if ((pos.x & 15) === 0 && (pos.z & 15) === 0) this.setSectionDirty(pos.offset(-CHUNK_SIZE, 0, -CHUNK_SIZE), true, true)
1268
- if ((pos.x & 15) === 15 && (pos.z & 15) === 0) this.setSectionDirty(pos.offset(CHUNK_SIZE, 0, -CHUNK_SIZE), true, true)
1269
- if ((pos.x & 15) === 0 && (pos.z & 15) === 15) this.setSectionDirty(pos.offset(-CHUNK_SIZE, 0, CHUNK_SIZE), true, true)
1270
- if ((pos.x & 15) === 15 && (pos.z & 15) === 15) this.setSectionDirty(pos.offset(CHUNK_SIZE, 0, CHUNK_SIZE), true, true)
1271
-
1272
- // side view neighbors (but ignore updates above)
1273
- // z view neighbors
1274
- if ((pos.x & 15) === 0 && (pos.y & (sectionHeight - 1)) === 0) this.setSectionDirty(pos.offset(-CHUNK_SIZE, -sectionHeight, 0), true, true)
1275
- if ((pos.x & 15) === 15 && (pos.y & (sectionHeight - 1)) === 0) this.setSectionDirty(pos.offset(CHUNK_SIZE, -sectionHeight, 0), true, true)
1276
-
1277
- // x view neighbors
1278
- if ((pos.z & 15) === 0 && (pos.y & (sectionHeight - 1)) === 0) this.setSectionDirty(pos.offset(0, -sectionHeight, -CHUNK_SIZE), true, true)
1279
- if ((pos.z & 15) === 15 && (pos.y & (sectionHeight - 1)) === 0) this.setSectionDirty(pos.offset(0, -sectionHeight, CHUNK_SIZE), true, true)
1280
-
1281
- // x & z neighbors
1282
- if ((pos.y & (sectionHeight - 1)) === 0 && (pos.x & 15) === 0 && (pos.z & 15) === 0)
1283
- this.setSectionDirty(pos.offset(-CHUNK_SIZE, -sectionHeight, -CHUNK_SIZE), true, true)
1284
- if ((pos.y & (sectionHeight - 1)) === 0 && (pos.x & 15) === 15 && (pos.z & 15) === 0)
1285
- this.setSectionDirty(pos.offset(CHUNK_SIZE, -sectionHeight, -CHUNK_SIZE), true, true)
1286
- if ((pos.y & (sectionHeight - 1)) === 0 && (pos.x & 15) === 0 && (pos.z & 15) === 15)
1287
- this.setSectionDirty(pos.offset(-CHUNK_SIZE, -sectionHeight, CHUNK_SIZE), true, true)
1288
- if ((pos.y & (sectionHeight - 1)) === 0 && (pos.x & 15) === 15 && (pos.z & 15) === 15)
1289
- 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
+ }
1290
1503
  }
1504
+ } finally {
1505
+ this.lightCoordinator.nextDirtyUrgent = false
1291
1506
  }
1292
1507
  }
1293
1508
 
@@ -1378,6 +1593,28 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
1378
1593
  const sectionHeight = this.getSectionHeight()
1379
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}`
1380
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
+
1381
1618
  const currentCount = (this.sectionDirtyCount.get(key) ?? 0) + 1
1382
1619
  this.sectionDirtyCount.set(key, currentCount)
1383
1620
 
@@ -1395,14 +1632,19 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
1395
1632
  this.sectionDirtyTimers.delete(key)
1396
1633
  this.sectionDirtyPendingArgs.delete(key)
1397
1634
  if (args) {
1398
- this._dispatchDirtyImmediate(args.pos, args.value, args.useChangeWorker)
1635
+ this.dispatchPendingDirty(args)
1399
1636
  }
1400
1637
  }, WorldRendererCommon.GEOMETRY_THROTTLE_DELAY)
1401
1638
  )
1402
1639
  }
1403
1640
  } else {
1404
1641
  // Subsequent requests — throttle: store latest args, existing timer will dispatch
1405
- 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
+ })
1406
1648
 
1407
1649
  if (!this.sectionDirtyTimers.has(key)) {
1408
1650
  this.sectionDirtyTimers.set(
@@ -1413,7 +1655,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
1413
1655
  this.sectionDirtyTimers.delete(key)
1414
1656
  this.sectionDirtyPendingArgs.delete(key)
1415
1657
  if (args) {
1416
- this._dispatchDirtyImmediate(args.pos, args.value, args.useChangeWorker)
1658
+ this.dispatchPendingDirty(args)
1417
1659
  }
1418
1660
  }, WorldRendererCommon.GEOMETRY_THROTTLE_DELAY)
1419
1661
  )
@@ -1421,6 +1663,12 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
1421
1663
  }
1422
1664
  }
1423
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
+
1424
1672
  /** Dispatch dirty message to worker without throttle (original logic) */
1425
1673
  private _dispatchDirtyImmediate(pos: Vec3, value: boolean, useChangeWorker: boolean) {
1426
1674
  this.reactiveState.world.mesherWork = true
@@ -1434,25 +1682,47 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
1434
1682
  // is always dispatched to the same worker
1435
1683
  const hash = this.getWorkerNumber(pos, useChangeWorker && (this.mesherLogger.active || this.worldRendererConfig.dedicatedChangeWorker))
1436
1684
  this.sectionsWaiting.set(key, (this.sectionsWaiting.get(key) ?? 0) + 1)
1437
- if (this.forceCallFromMesherReplayer) {
1438
- this.workers[hash].postMessage({
1439
- type: 'dirty',
1440
- x: pos.x,
1441
- y: pos.y,
1442
- z: pos.z,
1443
- value,
1444
- 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
1445
1719
  })
1720
+ }
1721
+ if (this.forceCallFromMesherReplayer) {
1722
+ this.workers[hash].postMessage(dirtyMessage)
1446
1723
  } else {
1447
1724
  this.toWorkerMessagesQueue[hash] ??= []
1448
- this.toWorkerMessagesQueue[hash].push({
1449
- type: 'dirty',
1450
- x: pos.x,
1451
- y: pos.y,
1452
- z: pos.z,
1453
- value,
1454
- config: this.getMesherConfig()
1455
- })
1725
+ this.toWorkerMessagesQueue[hash].push(dirtyMessage)
1456
1726
  this.dispatchMessages()
1457
1727
  }
1458
1728
  }
@@ -1533,12 +1803,15 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
1533
1803
  this.sectionDirtyTimers.clear()
1534
1804
  this.sectionDirtyCount.clear()
1535
1805
  this.sectionDirtyPendingArgs.clear()
1806
+ this.sectionDirtyLightMeta.clear()
1536
1807
 
1808
+ setClientLightTraceConfigListener(undefined)
1537
1809
  // Stop all workers
1538
1810
  for (const worker of this.workers) {
1539
1811
  worker.terminate()
1540
1812
  }
1541
1813
  this.workers = []
1814
+ this.lightCoordinator.terminate()
1542
1815
 
1543
1816
  // Stop and destroy sound system
1544
1817
  if (this.soundSystem) {