minecraft-renderer 0.1.79 → 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 +27 -27
  6. package/dist/minecraft-renderer.js.meta.json +1 -1
  7. package/dist/threeWorker.js +133 -133
  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 +239 -282
  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 +167 -100
  126. package/src/three/globalLegacyBuffer.ts +257 -166
  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 +10 -16
  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 +34 -34
  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 +300 -41
  170. package/src/three/tests/legacyMultiDraw.test.ts +8 -29
  171. package/src/three/tests/pr3PerFrameCpuCuts.test.ts +106 -29
  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 +117 -129
  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
@@ -71,10 +74,7 @@ export class GlobalBlockBuffer {
71
74
  private debugOverlay = false
72
75
  private layoutVersion = 0
73
76
 
74
- constructor (
75
- material: THREE.ShaderMaterial,
76
- scene: THREE.Object3D,
77
- ) {
77
+ constructor(material: THREE.ShaderMaterial, scene: THREE.Object3D) {
78
78
  this.capacityFaces = INITIAL_CAPACITY_FACES
79
79
  this.w0 = new Uint32Array(this.capacityFaces)
80
80
  this.w1 = new Uint32Array(this.capacityFaces)
@@ -119,14 +119,12 @@ export class GlobalBlockBuffer {
119
119
  this.multiDrawCaps,
120
120
  this.visibleSpans,
121
121
  this._drawScratch,
122
- this.multiDrawCaps.tier === 'C'
123
- ? (g, spans) => this.drawTierCSpans(g, spans, renderer, shaderMaterial)
124
- : undefined,
122
+ this.multiDrawCaps.tier === 'C' ? (g, spans) => this.drawTierCSpans(g, spans, renderer, shaderMaterial) : undefined
125
123
  )
126
124
  }
127
125
  }
128
126
 
129
- setDebugOverlay (enabled: boolean): void {
127
+ setDebugOverlay(enabled: boolean): void {
130
128
  this.debugOverlay = enabled
131
129
  }
132
130
 
@@ -135,69 +133,117 @@ export class GlobalBlockBuffer {
135
133
  * setDrawRange(0,0) skips bindingStates.setup in r0.184 — use 6 verts + instanceCount=0
136
134
  * so program/VAO stay bound while renderInstances no-ops at primcount===0.
137
135
  */
138
- suppressThreeDraw (): void {
136
+ suppressThreeDraw(): void {
139
137
  const geometry = this.mesh.geometry
140
138
  geometry.setDrawRange(0, VERTICES_PER_FACE)
141
139
  geometry.instanceCount = 0
142
140
  }
143
141
 
144
- setVisibleSpans (spans: CubeDrawSpan[]): void {
142
+ setVisibleSpans(spans: CubeDrawSpan[]): void {
145
143
  this.visibleSpans = spans
146
144
  }
147
145
 
148
- getVisibleSpans (): readonly CubeDrawSpan[] {
146
+ getVisibleSpans(): readonly CubeDrawSpan[] {
149
147
  return this.visibleSpans
150
148
  }
151
149
 
152
- forEachSectionSlot (cb: (key: string, slot: { start: number, count: number }) => void): void {
150
+ forEachSectionSlot(cb: (key: string, slot: { start: number; count: number }) => void): void {
153
151
  for (const [key, slot] of this.sectionSlots) {
154
152
  cb(key, slot)
155
153
  }
156
154
  }
157
155
 
158
- getSectionDrawStart (sectionKey: string): number | undefined {
156
+ getSectionDrawStart(sectionKey: string): number | undefined {
159
157
  const slot = this.sectionSlots.get(sectionKey)
160
158
  if (!slot) return undefined
161
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
162
163
  return slot.start
163
164
  }
164
165
 
165
- 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 {
166
197
  return this.highWatermark
167
198
  }
168
199
 
169
- getCapacityFaces (): number {
200
+ getCapacityFaces(): number {
170
201
  return this.capacityFaces
171
202
  }
172
203
 
173
- getSectionCount (): number {
204
+ getSectionCount(): number {
174
205
  return this.sectionSlots.size
175
206
  }
176
207
 
177
- getMemoryBytes (): number {
208
+ getMemoryBytes(): number {
178
209
  return this.capacityFaces * SHADER_CUBE_BYTES_PER_FACE
179
210
  }
180
211
 
181
- getUsedMemoryBytes (): number {
212
+ getUsedMemoryBytes(): number {
182
213
  return this.highWatermark * SHADER_CUBE_BYTES_PER_FACE
183
214
  }
184
215
 
185
- hasPendingUploads (): boolean {
216
+ hasPendingUploads(): boolean {
186
217
  return this.pendingRanges.length > 0
187
218
  }
188
219
 
189
- getPendingMove (): PendingMove | null {
220
+ getPendingMove(): PendingMove | null {
190
221
  return this.pendingMove
191
222
  }
192
223
 
193
- addSection (sectionKey: string, words: Uint32Array, faceCount: number): void {
224
+ addSection(sectionKey: string, words: Uint32Array, faceCount: number): void {
194
225
  if (faceCount <= 0) {
195
226
  this.removeSection(sectionKey)
196
227
  return
197
228
  }
198
229
 
199
- if (this.sectionSlots.has(sectionKey)) {
200
- 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
+ }
201
247
  }
202
248
 
203
249
  if (faceCount > this.capacityFaces) {
@@ -224,30 +270,33 @@ export class GlobalBlockBuffer {
224
270
  }
225
271
 
226
272
  this.sectionSlots.set(sectionKey, slot)
273
+ if (isRemesh && previousSlot) {
274
+ this.pendingReplace.set(sectionKey, { oldStart: previousSlot.start, oldCount: previousSlot.count })
275
+ }
227
276
  this.markDirty(slot.start, slot.start + faceCount - 1)
228
277
  this.mesh.geometry.instanceCount = this.highWatermark
229
278
  this.layoutVersion++
230
279
  }
231
280
 
232
- getLayoutVersion (): number {
281
+ getLayoutVersion(): number {
233
282
  return this.layoutVersion
234
283
  }
235
284
 
236
- hasSection (sectionKey: string): boolean {
285
+ hasSection(sectionKey: string): boolean {
237
286
  return this.sectionSlots.has(sectionKey)
238
287
  }
239
288
 
240
- getSectionSlot (sectionKey: string): { start: number, count: number } | undefined {
289
+ getSectionSlot(sectionKey: string): { start: number; count: number } | undefined {
241
290
  return this.sectionSlots.get(sectionKey)
242
291
  }
243
292
 
244
293
  /** Fetch fresh each raycast — growCapacity reallocates the backing array. */
245
- getW0 (): Uint32Array {
294
+ getW0(): Uint32Array {
246
295
  return this.w0
247
296
  }
248
297
 
249
298
  /** Copy live GPU words and remove the section (sci-fi reveal hide / restore). */
250
- takeSectionData (sectionKey: string): GlobalBlockBufferShaderData | undefined {
299
+ takeSectionData(sectionKey: string): GlobalBlockBufferShaderData | undefined {
251
300
  const slot = this.sectionSlots.get(sectionKey)
252
301
  if (!slot) return undefined
253
302
 
@@ -265,20 +314,19 @@ export class GlobalBlockBuffer {
265
314
  return { words, count: slot.count }
266
315
  }
267
316
 
268
- removeSection (sectionKey: string): void {
317
+ removeSection(sectionKey: string): void {
269
318
  const slot = this.sectionSlots.get(sectionKey)
270
319
  if (!slot) return
271
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
+
272
327
  if (this.pendingMove?.key === sectionKey) {
273
328
  const { oldStart, count } = this.pendingMove
274
- for (let i = oldStart; i < oldStart + count; i++) {
275
- this.w0[i] = 0
276
- this.w1[i] = 0
277
- this.w2[i] = EMPTY_W2
278
- this.w3[i] = 0
279
- }
280
- this.markDirty(oldStart, oldStart + count - 1)
281
- this.insertFreeSlot({ start: oldStart, count })
329
+ this.zeroAndFreeSlot(oldStart, count)
282
330
  this.pendingMove = null
283
331
  }
284
332
 
@@ -298,7 +346,7 @@ export class GlobalBlockBuffer {
298
346
  }
299
347
 
300
348
  /** One interior-hole move per frame when fragmentation exceeds threshold; deferred shrink. */
301
- compactStep (): void {
349
+ compactStep(): void {
302
350
  if (this.pendingMove) {
303
351
  const { newStart, count } = this.pendingMove
304
352
  if (this.rangeFullyUploaded(newStart, newStart + count - 1)) {
@@ -307,6 +355,14 @@ export class GlobalBlockBuffer {
307
355
  return
308
356
  }
309
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
+
310
366
  if (this.highWatermark === 0) return
311
367
  const interiorFree = this.interiorFreeFaces()
312
368
  if (interiorFree / this.highWatermark <= FRAGMENTATION_THRESHOLD) return
@@ -328,7 +384,7 @@ export class GlobalBlockBuffer {
328
384
  this.layoutVersion++
329
385
  }
330
386
 
331
- uploadDirtyRange (): void {
387
+ uploadDirtyRange(): void {
332
388
  const r = this.pendingRanges[0]
333
389
  if (!r) return
334
390
 
@@ -343,11 +399,15 @@ export class GlobalBlockBuffer {
343
399
  attr.needsUpdate = true
344
400
  }
345
401
 
346
- if (offset + count > r.end) this.pendingRanges.shift()
347
- 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++
348
408
  }
349
409
 
350
- setCameraOrigin (renderOrigin: RenderOrigin, x: number, y: number, z: number): void {
410
+ setCameraOrigin(renderOrigin: RenderOrigin, x: number, y: number, z: number): void {
351
411
  const { originDelta, cameraOriginFrac } = computeCameraRelativeUniforms(renderOrigin, x, y, z)
352
412
  const sectionOriginRel = computeSectionOriginRel(renderOrigin)
353
413
  const u = this.mesh.material.uniforms.u_originDelta
@@ -364,12 +424,14 @@ export class GlobalBlockBuffer {
364
424
  }
365
425
  }
366
426
 
367
- reset (): void {
427
+ reset(): void {
368
428
  this.sectionSlots.clear()
369
429
  this.freeList.length = 0
370
430
  this.highWatermark = 0
371
431
  this.pendingRanges.length = 0
372
432
  this.pendingMove = null
433
+ this.pendingReplace.clear()
434
+ this.uploadEpoch = 0
373
435
  this.visibleSpans = []
374
436
  this.w0.fill(0)
375
437
  this.w1.fill(0)
@@ -379,14 +441,14 @@ export class GlobalBlockBuffer {
379
441
  this.invalidateTierCVao()
380
442
  }
381
443
 
382
- dispose (): void {
444
+ dispose(): void {
383
445
  this.invalidateTierCVao()
384
446
  this.mesh.parent?.remove(this.mesh)
385
447
  this.mesh.geometry.dispose()
386
448
  this.reset()
387
449
  }
388
450
 
389
- private invalidateTierCVao (): void {
451
+ private invalidateTierCVao(): void {
390
452
  if (this.tierCVao && this.tierCGl) {
391
453
  this.tierCGl.deleteVertexArray(this.tierCVao)
392
454
  }
@@ -394,12 +456,7 @@ export class GlobalBlockBuffer {
394
456
  this.tierCAttrs = null
395
457
  }
396
458
 
397
- private drawTierCSpans (
398
- gl: WebGL2RenderingContext,
399
- spans: readonly CubeDrawSpan[],
400
- renderer: THREE.WebGLRenderer,
401
- material: THREE.ShaderMaterial,
402
- ): void {
459
+ private drawTierCSpans(gl: WebGL2RenderingContext, spans: readonly CubeDrawSpan[], renderer: THREE.WebGLRenderer, material: THREE.ShaderMaterial): void {
403
460
  this.ensureTierCVao(gl, renderer, material)
404
461
  if (!this.tierCVao || !this.tierCAttrs) return
405
462
 
@@ -420,11 +477,7 @@ export class GlobalBlockBuffer {
420
477
  gl.bindVertexArray(null)
421
478
  }
422
479
 
423
- private ensureTierCVao (
424
- gl: WebGL2RenderingContext,
425
- renderer: THREE.WebGLRenderer,
426
- material: THREE.ShaderMaterial,
427
- ): void {
480
+ private ensureTierCVao(gl: WebGL2RenderingContext, renderer: THREE.WebGLRenderer, material: THREE.ShaderMaterial): void {
428
481
  this.tierCGl = gl
429
482
  const internals = renderer as WebGLRendererInternals
430
483
  const materialProps = internals.properties.get(material) as { currentProgram: { program: WebGLProgram } }
@@ -474,19 +527,19 @@ export class GlobalBlockBuffer {
474
527
  w0: { loc: w0Loc, buffer: w0Buf },
475
528
  w1: { loc: w1Loc, buffer: w1Buf },
476
529
  w2: { loc: w2Loc, buffer: w2Buf },
477
- w3: { loc: w3Loc, buffer: w3Buf },
530
+ w3: { loc: w3Loc, buffer: w3Buf }
478
531
  }
479
532
  }
480
533
 
481
- private markDirty (start: number, end: number): void {
534
+ private markDirty(start: number, end: number): void {
482
535
  this.pendingRanges.push({ start, end })
483
536
  this.pendingRanges.sort((a, b) => a.start - b.start)
484
537
  this.mergePendingRanges()
485
538
  }
486
539
 
487
- private mergePendingRanges (): void {
540
+ private mergePendingRanges(): void {
488
541
  if (this.pendingRanges.length < 2) return
489
- const merged: Array<{ start: number, end: number }> = []
542
+ const merged: Array<{ start: number; end: number }> = []
490
543
  let cur = this.pendingRanges[0]!
491
544
  for (let i = 1; i < this.pendingRanges.length; i++) {
492
545
  const next = this.pendingRanges[i]!
@@ -501,7 +554,7 @@ export class GlobalBlockBuffer {
501
554
  this.pendingRanges = merged
502
555
  }
503
556
 
504
- private takeFreeSlot (count: number): { start: number, count: number } | undefined {
557
+ private takeFreeSlot(count: number): { start: number; count: number } | undefined {
505
558
  for (let i = 0; i < this.freeList.length; i++) {
506
559
  const slot = this.freeList[i]!
507
560
  if (slot.count >= count) {
@@ -515,15 +568,15 @@ export class GlobalBlockBuffer {
515
568
  return undefined
516
569
  }
517
570
 
518
- private insertFreeSlot (slot: { start: number, count: number }): void {
571
+ private insertFreeSlot(slot: { start: number; count: number }): void {
519
572
  this.freeList.push(slot)
520
573
  this.freeList.sort((a, b) => a.start - b.start)
521
574
  this.mergeFreeList()
522
575
  }
523
576
 
524
- private mergeFreeList (): void {
577
+ private mergeFreeList(): void {
525
578
  if (this.freeList.length < 2) return
526
- const merged: Array<{ start: number, count: number }> = []
579
+ const merged: Array<{ start: number; count: number }> = []
527
580
  let cur = this.freeList[0]!
528
581
  for (let i = 1; i < this.freeList.length; i++) {
529
582
  const next = this.freeList[i]!
@@ -538,7 +591,7 @@ export class GlobalBlockBuffer {
538
591
  this.freeList = merged
539
592
  }
540
593
 
541
- private interiorFreeFaces (): number {
594
+ private interiorFreeFaces(): number {
542
595
  let total = 0
543
596
  for (const slot of this.freeList) {
544
597
  if (slot.start < this.highWatermark) total += slot.count
@@ -546,8 +599,8 @@ export class GlobalBlockBuffer {
546
599
  return total
547
600
  }
548
601
 
549
- private findMovableSection (maxCount: number): { key: string, start: number, count: number } | undefined {
550
- 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 }> = []
551
604
  for (const [key, slot] of this.sectionSlots) {
552
605
  sections.push({ key, start: slot.start, count: slot.count })
553
606
  }
@@ -555,7 +608,7 @@ export class GlobalBlockBuffer {
555
608
 
556
609
  sections.sort((a, b) => {
557
610
  if (b.start !== a.start) return b.start - a.start
558
- return (b.start + b.count) - (a.start + a.count)
611
+ return b.start + b.count - (a.start + a.count)
559
612
  })
560
613
 
561
614
  const tailmost = sections[0]!
@@ -563,9 +616,7 @@ export class GlobalBlockBuffer {
563
616
  return tailmost
564
617
  }
565
618
 
566
- const candidates = sections
567
- .filter(s => s.count <= maxCount)
568
- .sort((a, b) => b.count - a.count)
619
+ const candidates = sections.filter(s => s.count <= maxCount).sort((a, b) => b.count - a.count)
569
620
 
570
621
  for (const s of candidates) {
571
622
  if (this.findLowestInteriorHole(s.start, s.count)) return s
@@ -573,10 +624,7 @@ export class GlobalBlockBuffer {
573
624
  return undefined
574
625
  }
575
626
 
576
- private findLowestInteriorHole (
577
- sectionStart: number,
578
- count: number,
579
- ): { start: number, count: number, index: number } | undefined {
627
+ private findLowestInteriorHole(sectionStart: number, count: number): { start: number; count: number; index: number } | undefined {
580
628
  for (let i = 0; i < this.freeList.length; i++) {
581
629
  const slot = this.freeList[i]!
582
630
  if (slot.start < sectionStart && slot.count >= count) {
@@ -586,7 +634,7 @@ export class GlobalBlockBuffer {
586
634
  return undefined
587
635
  }
588
636
 
589
- private reserveFreeSlotAt (index: number, count: number): { start: number, count: number } {
637
+ private reserveFreeSlotAt(index: number, count: number): { start: number; count: number } {
590
638
  const slot = this.freeList[index]!
591
639
  this.freeList.splice(index, 1)
592
640
  if (slot.count === count) return { start: slot.start, count }
@@ -595,41 +643,57 @@ export class GlobalBlockBuffer {
595
643
  return used
596
644
  }
597
645
 
598
- private copySectionRange (oldStart: number, newStart: number, count: number): void {
646
+ private copySectionRange(oldStart: number, newStart: number, count: number): void {
599
647
  this.w0.copyWithin(newStart, oldStart, oldStart + count)
600
648
  this.w1.copyWithin(newStart, oldStart, oldStart + count)
601
649
  this.w2.copyWithin(newStart, oldStart, oldStart + count)
602
650
  this.w3.copyWithin(newStart, oldStart, oldStart + count)
603
651
  }
604
652
 
605
- private rangeFullyUploaded (start: number, end: number): boolean {
653
+ private rangeFullyUploaded(start: number, end: number): boolean {
606
654
  for (const r of this.pendingRanges) {
607
655
  if (r.start <= end && r.end >= start) return false
608
656
  }
609
657
  return true
610
658
  }
611
659
 
612
- private finalizePendingMove (): void {
613
- const move = this.pendingMove
614
- if (!move) return
615
-
616
- const { oldStart, count } = move
617
- 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++) {
618
662
  this.w0[i] = 0
619
663
  this.w1[i] = 0
620
664
  this.w2[i] = EMPTY_W2
621
665
  this.w3[i] = 0
622
666
  }
623
- 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)
624
689
  this.shrinkHighWatermark()
625
- if (oldStart < this.highWatermark) {
626
- this.markDirty(oldStart, oldStart + count - 1)
627
- }
628
690
  this.mesh.geometry.instanceCount = this.highWatermark
629
691
  this.pendingMove = null
692
+ this.layoutVersion++
693
+ this.uploadEpoch++
630
694
  }
631
695
 
632
- private shrinkHighWatermark (): void {
696
+ private shrinkHighWatermark(): void {
633
697
  while (this.highWatermark > 0) {
634
698
  const tail = this.highWatermark - 1
635
699
  const free = this.freeList.find(s => s.start <= tail && s.start + s.count > tail)
@@ -640,9 +704,12 @@ export class GlobalBlockBuffer {
640
704
  }
641
705
  }
642
706
 
643
- private growCapacity (minFaces: number): void {
707
+ private growCapacity(minFaces: number): void {
644
708
  // Moved CPU data at newStart survives nw*.set(); pendingRanges cleared below anyway.
645
709
  if (this.pendingMove) this.finalizePendingMove()
710
+ for (const key of [...this.pendingReplace.keys()]) {
711
+ this.finalizePendingReplace(key)
712
+ }
646
713
 
647
714
  console.warn('[globalBlockBuffer] growing faces', this.capacityFaces, '->', '(need', minFaces, ')')
648
715
  let newCap = this.capacityFaces
@@ -671,7 +738,7 @@ export class GlobalBlockBuffer {
671
738
  if (prev) {
672
739
  geometry.deleteAttribute(name)
673
740
  if ('dispose' in prev && typeof (prev as { dispose?: () => void }).dispose === 'function') {
674
- (prev as { dispose: () => void }).dispose()
741
+ ;(prev as { dispose: () => void }).dispose()
675
742
  }
676
743
  }
677
744
  const attr = new THREE.InstancedBufferAttribute(arr, 1)