minecraft-renderer 0.1.78 → 0.1.80

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 (200) hide show
  1. package/README.md +29 -25
  2. package/dist/mesher.js +36 -36
  3. package/dist/mesher.js.map +4 -4
  4. package/dist/mesherWasm.js +217 -217
  5. package/dist/minecraft-renderer.js +32 -32
  6. package/dist/minecraft-renderer.js.meta.json +1 -1
  7. package/dist/threeWorker.js +415 -415
  8. package/package.json +5 -2
  9. package/src/bundler/bundlePrepare.ts +3 -8
  10. package/src/graphicsBackend/appViewer.ts +10 -7
  11. package/src/graphicsBackend/config.ts +1 -5
  12. package/src/graphicsBackend/playerState.ts +35 -34
  13. package/src/graphicsBackend/preloadWorkers.ts +43 -41
  14. package/src/graphicsBackend/rendererDefaultOptions.ts +68 -70
  15. package/src/graphicsBackend/rendererOptionsSync.ts +24 -50
  16. package/src/graphicsBackend/types.ts +2 -2
  17. package/src/index.ts +6 -27
  18. package/src/lib/DebugGui.ts +34 -31
  19. package/src/lib/animationController.ts +9 -9
  20. package/src/lib/bakeLegacyLight.ts +1 -6
  21. package/src/lib/bindAbortableListener.test.ts +32 -12
  22. package/src/lib/bindAbortableListener.ts +14 -6
  23. package/src/lib/blockEntityLightRegistry.ts +11 -20
  24. package/src/lib/blockEntityLighting.test.ts +1 -6
  25. package/src/lib/blockEntityLighting.ts +5 -5
  26. package/src/lib/buildSharedConfig.mjs +4 -4
  27. package/src/lib/buildWorkerMcDataIndexes.test.ts +4 -4
  28. package/src/lib/buildWorkerMcDataIndexes.ts +11 -19
  29. package/src/lib/cameraBobbing.ts +1 -1
  30. package/src/lib/canvas2DOverlay.example.ts +13 -33
  31. package/src/lib/canvas2DOverlay.quickstart.ts +24 -54
  32. package/src/lib/canvas2DOverlay.ts +28 -47
  33. package/src/lib/cleanupDecorator.ts +2 -2
  34. package/src/lib/createPlayerObject.test.ts +2 -7
  35. package/src/lib/createPlayerObject.ts +18 -25
  36. package/src/lib/frameTimingCollector.ts +2 -5
  37. package/src/lib/guiRenderer.ts +57 -42
  38. package/src/lib/items.ts +15 -9
  39. package/src/lib/mesherlogReader.ts +5 -6
  40. package/src/lib/simpleUtils.ts +5 -5
  41. package/src/lib/skyLight.ts +3 -3
  42. package/src/lib/smoothSwitcher.ts +11 -18
  43. package/src/lib/ui/newStats.ts +10 -12
  44. package/src/lib/utils/proxy.ts +7 -1
  45. package/src/lib/utils/skins.ts +4 -4
  46. package/src/lib/utils.ts +1 -1
  47. package/src/lib/workerMessageSanitize.ts +5 -12
  48. package/src/lib/workerProxy.ts +52 -42
  49. package/src/lib/workerSyncOps.test.ts +58 -41
  50. package/src/lib/worldrendererCommon.reconfigure.test.ts +24 -21
  51. package/src/lib/worldrendererCommon.removeColumn.test.ts +11 -13
  52. package/src/lib/worldrendererCommon.ts +338 -211
  53. package/src/mesher-legacy/mesher.ts +9 -4
  54. package/src/mesher-legacy/test/a.ts +1 -1
  55. package/src/mesher-legacy/test/mesherTester.ts +2 -2
  56. package/src/mesher-legacy/test/playground.ts +1 -1
  57. package/src/mesher-legacy/test/run/test-js.ts +1 -1
  58. package/src/mesher-legacy/test/snapshotUtils.ts +3 -8
  59. package/src/mesher-legacy/test/test-perf.ts +1 -1
  60. package/src/mesher-legacy/test/tests.test.ts +2 -2
  61. package/src/mesher-shared/blockEntityMetadata.test.ts +7 -5
  62. package/src/mesher-shared/blockEntityMetadata.ts +28 -19
  63. package/src/mesher-shared/blockPropertiesForMeshing.ts +2 -2
  64. package/src/mesher-shared/computeHeightmap.ts +2 -4
  65. package/src/mesher-shared/exportedGeometryTypes.ts +3 -3
  66. package/src/mesher-shared/faceOcclusion.ts +324 -0
  67. package/src/mesher-shared/models.ts +80 -131
  68. package/src/mesher-shared/modelsGeometryCommon.ts +30 -15
  69. package/src/mesher-shared/shared.ts +46 -46
  70. package/src/mesher-shared/standaloneRenderer.ts +40 -44
  71. package/src/mesher-shared/tests/faceOcclusion.test.ts +179 -0
  72. package/src/mesher-shared/tests/liquidQuadInvariant.test.ts +12 -8
  73. package/src/mesher-shared/vertexShading.ts +2 -11
  74. package/src/mesher-shared/world.ts +19 -19
  75. package/src/mesher-shared/worldLighting.test.ts +2 -2
  76. package/src/performanceMonitor/PerformanceMonitor.ts +4 -13
  77. package/src/performanceMonitor/formatPerformanceFactorsDebug.ts +2 -2
  78. package/src/performanceMonitor/index.ts +1 -5
  79. package/src/performanceMonitor/types.ts +1 -1
  80. package/src/playerState/playerState.ts +55 -50
  81. package/src/playerState/types.ts +2 -8
  82. package/src/playground/allEntitiesDebug.ts +19 -16
  83. package/src/playground/baseScene.ts +29 -32
  84. package/src/playground/mobileControls.tsx +133 -129
  85. package/src/playground/playground.html +76 -73
  86. package/src/playground/playground.ts +14 -1
  87. package/src/playground/playgroundUi.tsx +100 -74
  88. package/src/playground/reactUtils.ts +3 -7
  89. package/src/playground/scenes/entities.ts +10 -7
  90. package/src/playground/scenes/frequentUpdates.ts +5 -5
  91. package/src/playground/scenes/geometryExport.ts +6 -20
  92. package/src/playground/scenes/highPosEntityUpdates.ts +2 -10
  93. package/src/playground/scenes/index.ts +1 -1
  94. package/src/playground/scenes/lightingStarfield.ts +18 -12
  95. package/src/playground/scenes/main.ts +15 -11
  96. package/src/playground/scenes/partialBlockCulling.ts +31 -0
  97. package/src/playground/scenes/railsCobweb.ts +1 -1
  98. package/src/playground/scenes/rotationIssue.ts +1 -1
  99. package/src/playground/scenes/slabsOptimization.ts +1 -2
  100. package/src/playground/scenes/transparencyIssue.ts +1 -1
  101. package/src/playground/shared.ts +2 -2
  102. package/src/resourcesManager/resourcesManager.ts +29 -54
  103. package/src/resourcesManager/resourcesManager.worker.test.ts +6 -10
  104. package/src/shims/minecraftData.ts +70 -24
  105. package/src/sign-renderer/index.html +10 -12
  106. package/src/sign-renderer/index.ts +29 -29
  107. package/src/sign-renderer/playground.ts +22 -15
  108. package/src/sign-renderer/tests.test.ts +16 -13
  109. package/src/sign-renderer/vite.config.ts +2 -2
  110. package/src/three/appShared.ts +46 -25
  111. package/src/three/bannerRenderer.ts +33 -61
  112. package/src/three/cameraShake.ts +15 -6
  113. package/src/three/chunkMeshManager.ts +329 -301
  114. package/src/three/cinimaticScript.ts +26 -15
  115. package/src/three/cubeDrawSpans.ts +27 -37
  116. package/src/three/cubeMultiDraw.ts +9 -22
  117. package/src/three/documentRenderer.ts +15 -18
  118. package/src/three/entities.ts +175 -150
  119. package/src/three/entity/EntityMesh.ts +114 -108
  120. package/src/three/entity/animations.js +3 -3
  121. package/src/three/entity/armorModels.ts +15 -15
  122. package/src/three/entity/gltfAnimationUtils.ts +2 -5
  123. package/src/three/fireworks.ts +34 -32
  124. package/src/three/fireworksRenderer.ts +32 -29
  125. package/src/three/globalBlockBuffer.ts +174 -99
  126. package/src/three/globalLegacyBuffer.ts +465 -136
  127. package/src/three/graphicsBackendBase.ts +19 -25
  128. package/src/three/graphicsBackendOffThread.ts +12 -15
  129. package/src/three/hand.ts +21 -21
  130. package/src/three/handLegacy.ts +3 -13
  131. package/src/three/holdingBlock.ts +116 -79
  132. package/src/three/holdingBlockFactory.ts +1 -4
  133. package/src/three/holdingBlockItemIdentity.test.ts +25 -17
  134. package/src/three/holdingBlockItemIdentity.ts +9 -6
  135. package/src/three/holdingBlockLegacy.ts +113 -132
  136. package/src/three/itemMesh.ts +20 -29
  137. package/src/three/legacyMultiDraw.ts +93 -0
  138. package/src/three/legacySectionCull.ts +8 -14
  139. package/src/three/menuBackground/activeView.ts +1 -4
  140. package/src/three/menuBackground/assetUrl.ts +1 -1
  141. package/src/three/menuBackground/classic.ts +4 -12
  142. package/src/three/menuBackground/gpuPreference.ts +1 -3
  143. package/src/three/menuBackground/index.ts +2 -11
  144. package/src/three/menuBackground/renderer.ts +1 -5
  145. package/src/three/menuBackground/types.ts +1 -4
  146. package/src/three/menuBackground/v2.ts +196 -96
  147. package/src/three/menuBackground/v2Meta.ts +3 -3
  148. package/src/three/menuBackground/worldBlocks.ts +37 -35
  149. package/src/three/modules/blockBreakParticles.ts +59 -28
  150. package/src/three/modules/cameraBobbing.ts +3 -6
  151. package/src/three/modules/index.ts +1 -1
  152. package/src/three/modules/rain.ts +10 -23
  153. package/src/three/modules/sciFiWorldReveal.ts +11 -25
  154. package/src/three/modules/starfield.ts +7 -13
  155. package/src/three/renderSlot.ts +28 -16
  156. package/src/three/rendererModuleSystem.ts +1 -3
  157. package/src/three/sceneOrigin.ts +96 -33
  158. package/src/three/sectionRaycastAabb.ts +30 -18
  159. package/src/three/shaderCubeMesh.ts +6 -17
  160. package/src/three/shaders/cubeBlockShader.ts +85 -96
  161. package/src/three/shaders/legacyBlockShader.ts +21 -43
  162. package/src/three/shaders/textureIndexMapping.ts +89 -95
  163. package/src/three/shaders/tintPalette.ts +156 -170
  164. package/src/three/signTextureCache.ts +9 -12
  165. package/src/three/skyboxRenderer.ts +62 -70
  166. package/src/three/tests/chunkMeshManagerLegacy.test.ts +133 -25
  167. package/src/three/tests/cubeDrawSpans.test.ts +55 -25
  168. package/src/three/tests/globalBlockBuffer.test.ts +217 -0
  169. package/src/three/tests/globalLegacyBuffer.test.ts +460 -58
  170. package/src/three/tests/legacyMultiDraw.test.ts +88 -0
  171. package/src/three/tests/pr3PerFrameCpuCuts.test.ts +370 -0
  172. package/src/three/tests/signTextureCache.test.ts +4 -4
  173. package/src/three/threeJsMedia.ts +55 -72
  174. package/src/three/threeJsParticles.ts +32 -41
  175. package/src/three/threeJsSound.ts +3 -3
  176. package/src/three/threeJsUtils.ts +3 -3
  177. package/src/three/threeWorker.ts +2 -2
  178. package/src/three/waypointSprite.ts +55 -82
  179. package/src/three/waypoints.ts +20 -23
  180. package/src/three/world/cursorBlock.ts +25 -12
  181. package/src/three/world/vr.ts +11 -17
  182. package/src/three/worldGeometryExport.ts +15 -34
  183. package/src/three/worldRendererThree.ts +131 -133
  184. package/src/wasm-mesher/bridge/convertChunk.ts +3 -9
  185. package/src/wasm-mesher/bridge/render-from-wasm.ts +204 -229
  186. package/src/wasm-mesher/bridge/shaderCubeBridge.ts +26 -67
  187. package/src/wasm-mesher/tests/cullingRegression.test.ts +307 -0
  188. package/src/wasm-mesher/tests/heightmapParity.test.ts +8 -18
  189. package/src/wasm-mesher/tests/mesherWasmConversionCache.test.ts +2 -2
  190. package/src/wasm-mesher/tests/sectionRaycastAabb.test.ts +17 -4
  191. package/src/wasm-mesher/tests/shaderCubeInstances.test.ts +103 -141
  192. package/src/wasm-mesher/tests/splitColumnWasmOutput.test.ts +14 -22
  193. package/src/wasm-mesher/worker/mesherWasm.ts +150 -141
  194. package/src/wasm-mesher/worker/mesherWasmConversionCache.ts +2 -8
  195. package/src/wasm-mesher/worker/mesherWasmLightDirty.test.ts +1 -3
  196. package/src/wasm-mesher/worker/mesherWasmLightDirty.ts +1 -5
  197. package/src/wasm-mesher/worker/mesherWasmRequestTracker.ts +42 -42
  198. package/src/worldView/worldView.spiral.test.ts +1 -1
  199. package/src/worldView/worldView.ts +34 -39
  200. package/src/worldView/worldViewWorkerBridge.test.ts +4 -4
@@ -7,7 +7,7 @@ import {
7
7
  drawCubeSpans,
8
8
  logMultiDrawTierOnce,
9
9
  type CubeMultiDrawScratch,
10
- type MultiDrawCaps,
10
+ type MultiDrawCaps
11
11
  } from './cubeMultiDraw'
12
12
  import { VERTICES_PER_FACE, computeSectionOriginRel } from './shaders/cubeBlockShader'
13
13
  import { computeCameraRelativeUniforms, type RenderOrigin } from './shaders/legacyBlockShader'
@@ -19,7 +19,7 @@ type WebGLRendererInternals = THREE.WebGLRenderer & {
19
19
  }
20
20
  }
21
21
 
22
- type TierCAttrBinding = { loc: number, buffer: WebGLBuffer }
22
+ type TierCAttrBinding = { loc: number; buffer: WebGLBuffer }
23
23
  type TierCAttrState = {
24
24
  w0: TierCAttrBinding
25
25
  w1: TierCAttrBinding
@@ -29,16 +29,17 @@ type TierCAttrState = {
29
29
 
30
30
  // Linear growth (NOT doubling) to keep iOS allocation spikes bounded to one increment.
31
31
  // Reference: prismarine-web-client PR #90 (webgl) and #120 (webgpu) both grow by +1M faces.
32
- const INITIAL_CAPACITY_FACES = 512_000 // ~8 MB up front (4 words × 4 B), well under 1M
33
- const GROWTH_INCREMENT_FACES = 1_000_000 // +16 MB per growth step instead of doubling
34
- const MAX_UPLOAD_FACES_PER_FRAME = 15_000 // face-indexed budget (chunksStorage uses 10k blocks)
32
+ const INITIAL_CAPACITY_FACES = 512_000 // ~8 MB up front (4 words × 4 B), well under 1M
33
+ const GROWTH_INCREMENT_FACES = 1_000_000 // +16 MB per growth step instead of doubling
34
+ const MAX_UPLOAD_FACES_PER_FRAME = 15_000 // face-indexed budget (chunksStorage uses 10k blocks)
35
35
  const FRAGMENTATION_THRESHOLD = 0.25
36
36
  const EMPTY_W2 = packWord2Empty()
37
37
 
38
38
  /** CPU bytes per instanced cube face (a_w0..a_w3). */
39
39
  export const SHADER_CUBE_BYTES_PER_FACE = 16
40
40
 
41
- type PendingMove = { key: string, oldStart: number, newStart: number, count: number }
41
+ type PendingMove = { key: string; oldStart: number; newStart: number; count: number }
42
+ type PendingReplace = { oldStart: number; oldCount: number }
42
43
 
43
44
  export type GlobalBlockBufferShaderData = {
44
45
  words: Uint32Array
@@ -57,11 +58,13 @@ export class GlobalBlockBuffer {
57
58
  private w1: Uint32Array
58
59
  private w2: Uint32Array
59
60
  private w3: Uint32Array
60
- private readonly sectionSlots = new Map<string, { start: number, count: number }>()
61
- private freeList: Array<{ start: number, count: number }> = []
61
+ private readonly sectionSlots = new Map<string, { start: number; count: number }>()
62
+ private freeList: Array<{ start: number; count: number }> = []
62
63
  private highWatermark = 0
63
- private pendingRanges: Array<{ start: number, end: number }> = []
64
+ private pendingRanges: Array<{ start: number; end: number }> = []
64
65
  private pendingMove: PendingMove | null = null
66
+ private readonly pendingReplace = new Map<string, PendingReplace>()
67
+ private uploadEpoch = 0
65
68
  private visibleSpans: CubeDrawSpan[] = []
66
69
  private readonly _drawScratch: CubeMultiDrawScratch = createCubeMultiDrawScratch()
67
70
  private multiDrawCaps: MultiDrawCaps | null = null
@@ -69,11 +72,9 @@ export class GlobalBlockBuffer {
69
72
  private tierCAttrs: TierCAttrState | null = null
70
73
  private tierCGl: WebGL2RenderingContext | null = null
71
74
  private debugOverlay = false
75
+ private layoutVersion = 0
72
76
 
73
- constructor (
74
- material: THREE.ShaderMaterial,
75
- scene: THREE.Object3D,
76
- ) {
77
+ constructor(material: THREE.ShaderMaterial, scene: THREE.Object3D) {
77
78
  this.capacityFaces = INITIAL_CAPACITY_FACES
78
79
  this.w0 = new Uint32Array(this.capacityFaces)
79
80
  this.w1 = new Uint32Array(this.capacityFaces)
@@ -118,14 +119,12 @@ export class GlobalBlockBuffer {
118
119
  this.multiDrawCaps,
119
120
  this.visibleSpans,
120
121
  this._drawScratch,
121
- this.multiDrawCaps.tier === 'C'
122
- ? (g, spans) => this.drawTierCSpans(g, spans, renderer, shaderMaterial)
123
- : undefined,
122
+ this.multiDrawCaps.tier === 'C' ? (g, spans) => this.drawTierCSpans(g, spans, renderer, shaderMaterial) : undefined
124
123
  )
125
124
  }
126
125
  }
127
126
 
128
- setDebugOverlay (enabled: boolean): void {
127
+ setDebugOverlay(enabled: boolean): void {
129
128
  this.debugOverlay = enabled
130
129
  }
131
130
 
@@ -134,69 +133,117 @@ export class GlobalBlockBuffer {
134
133
  * setDrawRange(0,0) skips bindingStates.setup in r0.184 — use 6 verts + instanceCount=0
135
134
  * so program/VAO stay bound while renderInstances no-ops at primcount===0.
136
135
  */
137
- suppressThreeDraw (): void {
136
+ suppressThreeDraw(): void {
138
137
  const geometry = this.mesh.geometry
139
138
  geometry.setDrawRange(0, VERTICES_PER_FACE)
140
139
  geometry.instanceCount = 0
141
140
  }
142
141
 
143
- setVisibleSpans (spans: CubeDrawSpan[]): void {
142
+ setVisibleSpans(spans: CubeDrawSpan[]): void {
144
143
  this.visibleSpans = spans
145
144
  }
146
145
 
147
- getVisibleSpans (): readonly CubeDrawSpan[] {
146
+ getVisibleSpans(): readonly CubeDrawSpan[] {
148
147
  return this.visibleSpans
149
148
  }
150
149
 
151
- forEachSectionSlot (cb: (key: string, slot: { start: number, count: number }) => void): void {
150
+ forEachSectionSlot(cb: (key: string, slot: { start: number; count: number }) => void): void {
152
151
  for (const [key, slot] of this.sectionSlots) {
153
152
  cb(key, slot)
154
153
  }
155
154
  }
156
155
 
157
- getSectionDrawStart (sectionKey: string): number | undefined {
156
+ getSectionDrawStart(sectionKey: string): number | undefined {
158
157
  const slot = this.sectionSlots.get(sectionKey)
159
158
  if (!slot) return undefined
160
159
  if (this.pendingMove?.key === sectionKey) return this.pendingMove.oldStart
160
+ const replace = this.pendingReplace.get(sectionKey)
161
+ if (replace) return replace.oldStart
162
+ if (!this.rangeFullyUploaded(slot.start, slot.start + slot.count - 1)) return undefined
161
163
  return slot.start
162
164
  }
163
165
 
164
- getHighWatermark (): number {
166
+ getSectionDrawCount(sectionKey: string): number | undefined {
167
+ const slot = this.sectionSlots.get(sectionKey)
168
+ if (!slot) return undefined
169
+ if (this.pendingMove?.key === sectionKey) return this.pendingMove.count
170
+ const replace = this.pendingReplace.get(sectionKey)
171
+ if (replace) return replace.oldCount
172
+ if (!this.rangeFullyUploaded(slot.start, slot.start + slot.count - 1)) return undefined
173
+ return slot.count
174
+ }
175
+
176
+ getUploadEpoch(): number {
177
+ return this.uploadEpoch
178
+ }
179
+
180
+ hasPendingReplace(): boolean {
181
+ return this.pendingReplace.size > 0
182
+ }
183
+
184
+ canUseFullDrawShortcut(): boolean {
185
+ return this.pendingRanges.length === 0 && this.interiorFreeFaces() === 0 && this.pendingMove === null && this.pendingReplace.size === 0
186
+ }
187
+
188
+ isRangeFullyUploaded(start: number, end: number): boolean {
189
+ return this.rangeFullyUploaded(start, end)
190
+ }
191
+
192
+ getPendingDirtyRanges(): ReadonlyArray<{ start: number; end: number }> {
193
+ return this.pendingRanges
194
+ }
195
+
196
+ getHighWatermark(): number {
165
197
  return this.highWatermark
166
198
  }
167
199
 
168
- getCapacityFaces (): number {
200
+ getCapacityFaces(): number {
169
201
  return this.capacityFaces
170
202
  }
171
203
 
172
- getSectionCount (): number {
204
+ getSectionCount(): number {
173
205
  return this.sectionSlots.size
174
206
  }
175
207
 
176
- getMemoryBytes (): number {
208
+ getMemoryBytes(): number {
177
209
  return this.capacityFaces * SHADER_CUBE_BYTES_PER_FACE
178
210
  }
179
211
 
180
- getUsedMemoryBytes (): number {
212
+ getUsedMemoryBytes(): number {
181
213
  return this.highWatermark * SHADER_CUBE_BYTES_PER_FACE
182
214
  }
183
215
 
184
- hasPendingUploads (): boolean {
216
+ hasPendingUploads(): boolean {
185
217
  return this.pendingRanges.length > 0
186
218
  }
187
219
 
188
- getPendingMove (): PendingMove | null {
220
+ getPendingMove(): PendingMove | null {
189
221
  return this.pendingMove
190
222
  }
191
223
 
192
- addSection (sectionKey: string, words: Uint32Array, faceCount: number): void {
224
+ addSection(sectionKey: string, words: Uint32Array, faceCount: number): void {
193
225
  if (faceCount <= 0) {
194
226
  this.removeSection(sectionKey)
195
227
  return
196
228
  }
197
229
 
198
- if (this.sectionSlots.has(sectionKey)) {
199
- this.removeSection(sectionKey)
230
+ const isRemesh = this.sectionSlots.has(sectionKey)
231
+ let previousSlot: { start: number; count: number } | undefined
232
+ if (isRemesh) {
233
+ const currentSlot = this.sectionSlots.get(sectionKey)!
234
+ const inflightReplace = this.pendingReplace.get(sectionKey)
235
+ const inflightMove = this.pendingMove?.key === sectionKey ? this.pendingMove : undefined
236
+ if (inflightReplace) {
237
+ this.zeroAndFreeSlot(currentSlot.start, currentSlot.count)
238
+ previousSlot = { start: inflightReplace.oldStart, count: inflightReplace.oldCount }
239
+ this.pendingReplace.delete(sectionKey)
240
+ } else if (inflightMove) {
241
+ this.zeroAndFreeSlot(currentSlot.start, currentSlot.count)
242
+ previousSlot = { start: inflightMove.oldStart, count: inflightMove.count }
243
+ this.pendingMove = null
244
+ } else {
245
+ previousSlot = currentSlot
246
+ }
200
247
  }
201
248
 
202
249
  if (faceCount > this.capacityFaces) {
@@ -223,25 +270,33 @@ export class GlobalBlockBuffer {
223
270
  }
224
271
 
225
272
  this.sectionSlots.set(sectionKey, slot)
273
+ if (isRemesh && previousSlot) {
274
+ this.pendingReplace.set(sectionKey, { oldStart: previousSlot.start, oldCount: previousSlot.count })
275
+ }
226
276
  this.markDirty(slot.start, slot.start + faceCount - 1)
227
277
  this.mesh.geometry.instanceCount = this.highWatermark
278
+ this.layoutVersion++
279
+ }
280
+
281
+ getLayoutVersion(): number {
282
+ return this.layoutVersion
228
283
  }
229
284
 
230
- hasSection (sectionKey: string): boolean {
285
+ hasSection(sectionKey: string): boolean {
231
286
  return this.sectionSlots.has(sectionKey)
232
287
  }
233
288
 
234
- getSectionSlot (sectionKey: string): { start: number, count: number } | undefined {
289
+ getSectionSlot(sectionKey: string): { start: number; count: number } | undefined {
235
290
  return this.sectionSlots.get(sectionKey)
236
291
  }
237
292
 
238
293
  /** Fetch fresh each raycast — growCapacity reallocates the backing array. */
239
- getW0 (): Uint32Array {
294
+ getW0(): Uint32Array {
240
295
  return this.w0
241
296
  }
242
297
 
243
298
  /** Copy live GPU words and remove the section (sci-fi reveal hide / restore). */
244
- takeSectionData (sectionKey: string): GlobalBlockBufferShaderData | undefined {
299
+ takeSectionData(sectionKey: string): GlobalBlockBufferShaderData | undefined {
245
300
  const slot = this.sectionSlots.get(sectionKey)
246
301
  if (!slot) return undefined
247
302
 
@@ -259,20 +314,19 @@ export class GlobalBlockBuffer {
259
314
  return { words, count: slot.count }
260
315
  }
261
316
 
262
- removeSection (sectionKey: string): void {
317
+ removeSection(sectionKey: string): void {
263
318
  const slot = this.sectionSlots.get(sectionKey)
264
319
  if (!slot) return
265
320
 
321
+ if (this.pendingReplace.has(sectionKey)) {
322
+ const pr = this.pendingReplace.get(sectionKey)!
323
+ this.zeroAndFreeSlot(pr.oldStart, pr.oldCount)
324
+ this.pendingReplace.delete(sectionKey)
325
+ }
326
+
266
327
  if (this.pendingMove?.key === sectionKey) {
267
328
  const { oldStart, count } = this.pendingMove
268
- for (let i = oldStart; i < oldStart + count; i++) {
269
- this.w0[i] = 0
270
- this.w1[i] = 0
271
- this.w2[i] = EMPTY_W2
272
- this.w3[i] = 0
273
- }
274
- this.markDirty(oldStart, oldStart + count - 1)
275
- this.insertFreeSlot({ start: oldStart, count })
329
+ this.zeroAndFreeSlot(oldStart, count)
276
330
  this.pendingMove = null
277
331
  }
278
332
 
@@ -288,10 +342,11 @@ export class GlobalBlockBuffer {
288
342
  this.insertFreeSlot(slot)
289
343
  this.shrinkHighWatermark()
290
344
  this.mesh.geometry.instanceCount = this.highWatermark
345
+ this.layoutVersion++
291
346
  }
292
347
 
293
348
  /** One interior-hole move per frame when fragmentation exceeds threshold; deferred shrink. */
294
- compactStep (): void {
349
+ compactStep(): void {
295
350
  if (this.pendingMove) {
296
351
  const { newStart, count } = this.pendingMove
297
352
  if (this.rangeFullyUploaded(newStart, newStart + count - 1)) {
@@ -300,6 +355,14 @@ export class GlobalBlockBuffer {
300
355
  return
301
356
  }
302
357
 
358
+ for (const key of [...this.pendingReplace.keys()]) {
359
+ const slot = this.sectionSlots.get(key)
360
+ if (!slot) continue
361
+ if (this.rangeFullyUploaded(slot.start, slot.start + slot.count - 1)) {
362
+ this.finalizePendingReplace(key)
363
+ }
364
+ }
365
+
303
366
  if (this.highWatermark === 0) return
304
367
  const interiorFree = this.interiorFreeFaces()
305
368
  if (interiorFree / this.highWatermark <= FRAGMENTATION_THRESHOLD) return
@@ -318,9 +381,10 @@ export class GlobalBlockBuffer {
318
381
  this.sectionSlots.set(section.key, { start: newStart, count: section.count })
319
382
  this.markDirty(newStart, newStart + section.count - 1)
320
383
  this.pendingMove = { key: section.key, oldStart, newStart, count: section.count }
384
+ this.layoutVersion++
321
385
  }
322
386
 
323
- uploadDirtyRange (): void {
387
+ uploadDirtyRange(): void {
324
388
  const r = this.pendingRanges[0]
325
389
  if (!r) return
326
390
 
@@ -335,11 +399,15 @@ export class GlobalBlockBuffer {
335
399
  attr.needsUpdate = true
336
400
  }
337
401
 
338
- if (offset + count > r.end) this.pendingRanges.shift()
339
- else r.start = offset + count
402
+ if (offset + count > r.end) {
403
+ this.pendingRanges.shift()
404
+ } else {
405
+ r.start = offset + count
406
+ }
407
+ this.uploadEpoch++
340
408
  }
341
409
 
342
- setCameraOrigin (renderOrigin: RenderOrigin, x: number, y: number, z: number): void {
410
+ setCameraOrigin(renderOrigin: RenderOrigin, x: number, y: number, z: number): void {
343
411
  const { originDelta, cameraOriginFrac } = computeCameraRelativeUniforms(renderOrigin, x, y, z)
344
412
  const sectionOriginRel = computeSectionOriginRel(renderOrigin)
345
413
  const u = this.mesh.material.uniforms.u_originDelta
@@ -356,12 +424,14 @@ export class GlobalBlockBuffer {
356
424
  }
357
425
  }
358
426
 
359
- reset (): void {
427
+ reset(): void {
360
428
  this.sectionSlots.clear()
361
429
  this.freeList.length = 0
362
430
  this.highWatermark = 0
363
431
  this.pendingRanges.length = 0
364
432
  this.pendingMove = null
433
+ this.pendingReplace.clear()
434
+ this.uploadEpoch = 0
365
435
  this.visibleSpans = []
366
436
  this.w0.fill(0)
367
437
  this.w1.fill(0)
@@ -371,14 +441,14 @@ export class GlobalBlockBuffer {
371
441
  this.invalidateTierCVao()
372
442
  }
373
443
 
374
- dispose (): void {
444
+ dispose(): void {
375
445
  this.invalidateTierCVao()
376
446
  this.mesh.parent?.remove(this.mesh)
377
447
  this.mesh.geometry.dispose()
378
448
  this.reset()
379
449
  }
380
450
 
381
- private invalidateTierCVao (): void {
451
+ private invalidateTierCVao(): void {
382
452
  if (this.tierCVao && this.tierCGl) {
383
453
  this.tierCGl.deleteVertexArray(this.tierCVao)
384
454
  }
@@ -386,12 +456,7 @@ export class GlobalBlockBuffer {
386
456
  this.tierCAttrs = null
387
457
  }
388
458
 
389
- private drawTierCSpans (
390
- gl: WebGL2RenderingContext,
391
- spans: readonly CubeDrawSpan[],
392
- renderer: THREE.WebGLRenderer,
393
- material: THREE.ShaderMaterial,
394
- ): void {
459
+ private drawTierCSpans(gl: WebGL2RenderingContext, spans: readonly CubeDrawSpan[], renderer: THREE.WebGLRenderer, material: THREE.ShaderMaterial): void {
395
460
  this.ensureTierCVao(gl, renderer, material)
396
461
  if (!this.tierCVao || !this.tierCAttrs) return
397
462
 
@@ -412,11 +477,7 @@ export class GlobalBlockBuffer {
412
477
  gl.bindVertexArray(null)
413
478
  }
414
479
 
415
- private ensureTierCVao (
416
- gl: WebGL2RenderingContext,
417
- renderer: THREE.WebGLRenderer,
418
- material: THREE.ShaderMaterial,
419
- ): void {
480
+ private ensureTierCVao(gl: WebGL2RenderingContext, renderer: THREE.WebGLRenderer, material: THREE.ShaderMaterial): void {
420
481
  this.tierCGl = gl
421
482
  const internals = renderer as WebGLRendererInternals
422
483
  const materialProps = internals.properties.get(material) as { currentProgram: { program: WebGLProgram } }
@@ -466,19 +527,19 @@ export class GlobalBlockBuffer {
466
527
  w0: { loc: w0Loc, buffer: w0Buf },
467
528
  w1: { loc: w1Loc, buffer: w1Buf },
468
529
  w2: { loc: w2Loc, buffer: w2Buf },
469
- w3: { loc: w3Loc, buffer: w3Buf },
530
+ w3: { loc: w3Loc, buffer: w3Buf }
470
531
  }
471
532
  }
472
533
 
473
- private markDirty (start: number, end: number): void {
534
+ private markDirty(start: number, end: number): void {
474
535
  this.pendingRanges.push({ start, end })
475
536
  this.pendingRanges.sort((a, b) => a.start - b.start)
476
537
  this.mergePendingRanges()
477
538
  }
478
539
 
479
- private mergePendingRanges (): void {
540
+ private mergePendingRanges(): void {
480
541
  if (this.pendingRanges.length < 2) return
481
- const merged: Array<{ start: number, end: number }> = []
542
+ const merged: Array<{ start: number; end: number }> = []
482
543
  let cur = this.pendingRanges[0]!
483
544
  for (let i = 1; i < this.pendingRanges.length; i++) {
484
545
  const next = this.pendingRanges[i]!
@@ -493,7 +554,7 @@ export class GlobalBlockBuffer {
493
554
  this.pendingRanges = merged
494
555
  }
495
556
 
496
- private takeFreeSlot (count: number): { start: number, count: number } | undefined {
557
+ private takeFreeSlot(count: number): { start: number; count: number } | undefined {
497
558
  for (let i = 0; i < this.freeList.length; i++) {
498
559
  const slot = this.freeList[i]!
499
560
  if (slot.count >= count) {
@@ -507,15 +568,15 @@ export class GlobalBlockBuffer {
507
568
  return undefined
508
569
  }
509
570
 
510
- private insertFreeSlot (slot: { start: number, count: number }): void {
571
+ private insertFreeSlot(slot: { start: number; count: number }): void {
511
572
  this.freeList.push(slot)
512
573
  this.freeList.sort((a, b) => a.start - b.start)
513
574
  this.mergeFreeList()
514
575
  }
515
576
 
516
- private mergeFreeList (): void {
577
+ private mergeFreeList(): void {
517
578
  if (this.freeList.length < 2) return
518
- const merged: Array<{ start: number, count: number }> = []
579
+ const merged: Array<{ start: number; count: number }> = []
519
580
  let cur = this.freeList[0]!
520
581
  for (let i = 1; i < this.freeList.length; i++) {
521
582
  const next = this.freeList[i]!
@@ -530,7 +591,7 @@ export class GlobalBlockBuffer {
530
591
  this.freeList = merged
531
592
  }
532
593
 
533
- private interiorFreeFaces (): number {
594
+ private interiorFreeFaces(): number {
534
595
  let total = 0
535
596
  for (const slot of this.freeList) {
536
597
  if (slot.start < this.highWatermark) total += slot.count
@@ -538,8 +599,8 @@ export class GlobalBlockBuffer {
538
599
  return total
539
600
  }
540
601
 
541
- private findMovableSection (maxCount: number): { key: string, start: number, count: number } | undefined {
542
- const sections: Array<{ key: string, start: number, count: number }> = []
602
+ private findMovableSection(maxCount: number): { key: string; start: number; count: number } | undefined {
603
+ const sections: Array<{ key: string; start: number; count: number }> = []
543
604
  for (const [key, slot] of this.sectionSlots) {
544
605
  sections.push({ key, start: slot.start, count: slot.count })
545
606
  }
@@ -547,7 +608,7 @@ export class GlobalBlockBuffer {
547
608
 
548
609
  sections.sort((a, b) => {
549
610
  if (b.start !== a.start) return b.start - a.start
550
- return (b.start + b.count) - (a.start + a.count)
611
+ return b.start + b.count - (a.start + a.count)
551
612
  })
552
613
 
553
614
  const tailmost = sections[0]!
@@ -555,9 +616,7 @@ export class GlobalBlockBuffer {
555
616
  return tailmost
556
617
  }
557
618
 
558
- const candidates = sections
559
- .filter(s => s.count <= maxCount)
560
- .sort((a, b) => b.count - a.count)
619
+ const candidates = sections.filter(s => s.count <= maxCount).sort((a, b) => b.count - a.count)
561
620
 
562
621
  for (const s of candidates) {
563
622
  if (this.findLowestInteriorHole(s.start, s.count)) return s
@@ -565,10 +624,7 @@ export class GlobalBlockBuffer {
565
624
  return undefined
566
625
  }
567
626
 
568
- private findLowestInteriorHole (
569
- sectionStart: number,
570
- count: number,
571
- ): { start: number, count: number, index: number } | undefined {
627
+ private findLowestInteriorHole(sectionStart: number, count: number): { start: number; count: number; index: number } | undefined {
572
628
  for (let i = 0; i < this.freeList.length; i++) {
573
629
  const slot = this.freeList[i]!
574
630
  if (slot.start < sectionStart && slot.count >= count) {
@@ -578,7 +634,7 @@ export class GlobalBlockBuffer {
578
634
  return undefined
579
635
  }
580
636
 
581
- private reserveFreeSlotAt (index: number, count: number): { start: number, count: number } {
637
+ private reserveFreeSlotAt(index: number, count: number): { start: number; count: number } {
582
638
  const slot = this.freeList[index]!
583
639
  this.freeList.splice(index, 1)
584
640
  if (slot.count === count) return { start: slot.start, count }
@@ -587,41 +643,57 @@ export class GlobalBlockBuffer {
587
643
  return used
588
644
  }
589
645
 
590
- private copySectionRange (oldStart: number, newStart: number, count: number): void {
646
+ private copySectionRange(oldStart: number, newStart: number, count: number): void {
591
647
  this.w0.copyWithin(newStart, oldStart, oldStart + count)
592
648
  this.w1.copyWithin(newStart, oldStart, oldStart + count)
593
649
  this.w2.copyWithin(newStart, oldStart, oldStart + count)
594
650
  this.w3.copyWithin(newStart, oldStart, oldStart + count)
595
651
  }
596
652
 
597
- private rangeFullyUploaded (start: number, end: number): boolean {
653
+ private rangeFullyUploaded(start: number, end: number): boolean {
598
654
  for (const r of this.pendingRanges) {
599
655
  if (r.start <= end && r.end >= start) return false
600
656
  }
601
657
  return true
602
658
  }
603
659
 
604
- private finalizePendingMove (): void {
605
- const move = this.pendingMove
606
- if (!move) return
607
-
608
- const { oldStart, count } = move
609
- for (let i = oldStart; i < oldStart + count; i++) {
660
+ private zeroAndFreeSlot(start: number, count: number): void {
661
+ for (let i = start; i < start + count; i++) {
610
662
  this.w0[i] = 0
611
663
  this.w1[i] = 0
612
664
  this.w2[i] = EMPTY_W2
613
665
  this.w3[i] = 0
614
666
  }
615
- this.insertFreeSlot({ start: oldStart, count })
667
+ this.markDirty(start, start + count - 1)
668
+ this.insertFreeSlot({ start, count })
669
+ }
670
+
671
+ private finalizePendingReplace(key: string): void {
672
+ const pr = this.pendingReplace.get(key)
673
+ if (!pr) return
674
+
675
+ this.zeroAndFreeSlot(pr.oldStart, pr.oldCount)
676
+ this.pendingReplace.delete(key)
677
+ this.shrinkHighWatermark()
678
+ this.mesh.geometry.instanceCount = this.highWatermark
679
+ this.layoutVersion++
680
+ this.uploadEpoch++
681
+ }
682
+
683
+ private finalizePendingMove(): void {
684
+ const move = this.pendingMove
685
+ if (!move) return
686
+
687
+ const { oldStart, count } = move
688
+ this.zeroAndFreeSlot(oldStart, count)
616
689
  this.shrinkHighWatermark()
617
- if (oldStart < this.highWatermark) {
618
- this.markDirty(oldStart, oldStart + count - 1)
619
- }
620
690
  this.mesh.geometry.instanceCount = this.highWatermark
621
691
  this.pendingMove = null
692
+ this.layoutVersion++
693
+ this.uploadEpoch++
622
694
  }
623
695
 
624
- private shrinkHighWatermark (): void {
696
+ private shrinkHighWatermark(): void {
625
697
  while (this.highWatermark > 0) {
626
698
  const tail = this.highWatermark - 1
627
699
  const free = this.freeList.find(s => s.start <= tail && s.start + s.count > tail)
@@ -632,9 +704,12 @@ export class GlobalBlockBuffer {
632
704
  }
633
705
  }
634
706
 
635
- private growCapacity (minFaces: number): void {
707
+ private growCapacity(minFaces: number): void {
636
708
  // Moved CPU data at newStart survives nw*.set(); pendingRanges cleared below anyway.
637
709
  if (this.pendingMove) this.finalizePendingMove()
710
+ for (const key of [...this.pendingReplace.keys()]) {
711
+ this.finalizePendingReplace(key)
712
+ }
638
713
 
639
714
  console.warn('[globalBlockBuffer] growing faces', this.capacityFaces, '->', '(need', minFaces, ')')
640
715
  let newCap = this.capacityFaces
@@ -663,7 +738,7 @@ export class GlobalBlockBuffer {
663
738
  if (prev) {
664
739
  geometry.deleteAttribute(name)
665
740
  if ('dispose' in prev && typeof (prev as { dispose?: () => void }).dispose === 'function') {
666
- (prev as { dispose: () => void }).dispose()
741
+ ;(prev as { dispose: () => void }).dispose()
667
742
  }
668
743
  }
669
744
  const attr = new THREE.InstancedBufferAttribute(arr, 1)