minecraft-renderer 0.1.39 → 0.1.41

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 (57) hide show
  1. package/dist/mesher.js +8 -8
  2. package/dist/mesher.js.map +4 -4
  3. package/dist/mesherWasm.js +94 -94
  4. package/dist/minecraft-renderer.js +57 -57
  5. package/dist/minecraft-renderer.js.meta.json +1 -1
  6. package/dist/threeWorker.js +66 -66
  7. package/package.json +3 -4
  8. package/src/bundler/bundlePrepare.ts +56 -0
  9. package/src/graphicsBackend/appViewer.ts +10 -0
  10. package/src/graphicsBackend/config.ts +5 -1
  11. package/src/graphicsBackend/preloadWorkers.ts +187 -0
  12. package/src/lib/worldrendererCommon.ts +26 -2
  13. package/src/{mesher → mesher-legacy}/mesher.ts +14 -4
  14. package/src/{mesher → mesher-legacy}/test/mesherTester.ts +2 -2
  15. package/src/{mesher → mesher-legacy}/test/run/test-js.ts +1 -1
  16. package/src/{mesher → mesher-legacy}/test/test-perf.ts +1 -1
  17. package/src/{mesher → mesher-legacy}/test/tests.test.ts +1 -1
  18. package/src/{mesher → mesher-shared}/shared.ts +2 -0
  19. package/src/playground/allEntitiesDebug.ts +1 -1
  20. package/src/three/chunkMeshManager.ts +1 -1
  21. package/src/three/entities.ts +19 -6
  22. package/src/three/entity/EntityMesh.ts +123 -140
  23. package/src/three/graphicsBackendBase.ts +13 -0
  24. package/src/three/holdingBlock.ts +1 -1
  25. package/src/three/holdingBlockLegacy.ts +1 -1
  26. package/src/three/modules/sciFiWorldReveal.ts +1 -1
  27. package/src/three/worldRendererThree.ts +2 -2
  28. package/src/wasm-mesher/README.md +90 -0
  29. package/src/{wasm-lib → wasm-mesher/bridge}/convertChunk.ts +2 -2
  30. package/src/{wasm-lib → wasm-mesher/bridge}/render-from-wasm.ts +4 -4
  31. package/src/wasm-mesher/runtime-build/wasm_mesher.d.ts +210 -0
  32. package/src/wasm-mesher/runtime-build/wasm_mesher.js +881 -0
  33. package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm +0 -0
  34. package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm.d.ts +24 -0
  35. package/src/{mesher/test → wasm-mesher/tests}/heightmapParity.test.ts +4 -4
  36. package/src/{mesher/test → wasm-mesher/tests}/mesherWasmConversionCache.test.ts +2 -2
  37. package/src/{mesher/test → wasm-mesher/tests}/splitColumnWasmOutput.test.ts +1 -1
  38. package/src/wasm-mesher/worker/mesherWasm.ts +1247 -0
  39. package/src/{mesher → wasm-mesher/worker}/mesherWasmConversionCache.ts +1 -1
  40. package/src/worldView/types.ts +90 -0
  41. package/src/mesher/mesherWasm.ts +0 -696
  42. package/wasm/wasm_mesher.d.ts +0 -46
  43. package/wasm/wasm_mesher.js +0 -443
  44. package/wasm/wasm_mesher_bg.wasm +0 -0
  45. package/wasm/wasm_mesher_bg.wasm.d.ts +0 -9
  46. /package/src/{mesher → mesher-legacy}/test/a.ts +0 -0
  47. /package/src/{mesher → mesher-legacy}/test/playground.ts +0 -0
  48. /package/src/{mesher → mesher-legacy}/test/run/chunk.ts +0 -0
  49. /package/src/{mesher → mesher-legacy}/test/snapshotUtils.ts +0 -0
  50. /package/src/{mesher → mesher-shared}/blockEntityMetadata.ts +0 -0
  51. /package/src/{mesher → mesher-shared}/computeHeightmap.ts +0 -0
  52. /package/src/{mesher → mesher-shared}/models.ts +0 -0
  53. /package/src/{mesher → mesher-shared}/modelsGeometryCommon.ts +0 -0
  54. /package/src/{mesher → mesher-shared}/standaloneRenderer.ts +0 -0
  55. /package/src/{mesher → mesher-shared}/world.ts +0 -0
  56. /package/src/{mesher → mesher-shared}/worldConstants.ts +0 -0
  57. /package/src/{mesher → wasm-mesher/worker}/mesherWasmRequestTracker.ts +0 -0
@@ -1,696 +0,0 @@
1
- //@ts-nocheck
2
- import { Vec3 } from 'vec3'
3
- import { convertChunkToWasm } from '../wasm-lib/convertChunk'
4
- import { extractColumnHeightmap, splitColumnWasmOutputToSections } from '../wasm-lib/render-from-wasm'
5
- import { setBlockStatesData as setMesherData } from './models'
6
- import { defaultMesherConfig, type MesherGeometryOutput, SECTION_HEIGHT } from './shared'
7
- import { worldColumnKey, World } from './world'
8
- import { handleGetHeightmap, EMPTY_COLUMN_HEIGHTMAP_SENTINEL } from './computeHeightmap'
9
- import { collectBlockEntityMetadata, type SignMeta, type HeadMeta, type BannerMeta } from './blockEntityMetadata'
10
- import { SectionRequestTracker } from './mesherWasmRequestTracker'
11
- import {
12
- CONVERSION_CACHE_LIMIT,
13
- clearConversionCache,
14
- getOrConvertColumn,
15
- invalidateConversion,
16
- setConversionCacheLimit,
17
- } from './mesherWasmConversionCache'
18
-
19
- let wasm: typeof import('../../wasm/wasm_mesher.js') | null = null
20
- let wasmInitialized = false
21
-
22
- async function initWasm() {
23
- if (wasmInitialized) return
24
- try {
25
- wasmInitialized = true
26
- wasm = await import('../../wasm/wasm_mesher.js')
27
- await wasm.default('/wasm_mesher_bg.wasm') as any
28
-
29
- // const result = await testChunkShared(wasm)
30
- // console.log('result', result)
31
- } catch (err) {
32
- console.error('Failed to initialize WASM mesher:', err)
33
- wasmInitialized = true // Don't try to initialize again
34
- // Don't throw - allow worker to continue without WASM (will fail on first use)
35
- }
36
- }
37
-
38
- globalThis.structuredClone ??= (value) => JSON.parse(JSON.stringify(value))
39
-
40
- if (globalThis.module && module.require) {
41
- // If we are in a node environment, we need to fake some env variables
42
- const r = module.require
43
- const { parentPort } = r('worker_threads')
44
- global.self = parentPort
45
- global.postMessage = (value, transferList) => { parentPort.postMessage(value, transferList) }
46
- global.performance = r('perf_hooks').performance
47
- }
48
-
49
- let workerIndex = 0
50
- let config = defaultMesherConfig
51
- let version = '1.16.5'
52
- let world: World // chunkKey -> chunk data
53
- let dirtySections = new Map<string, number>()
54
- // Kept in sync with `dirtySections` so column mode can filter outgoing
55
- // geometry/sectionFinished events to only the section keys requested by the
56
- // main thread, even though a full-column WASM call may generate more data.
57
- const requestTracker = new SectionRequestTracker()
58
- let allDataReady = false
59
-
60
- function sectionKey(x: number, y: number, z: number) {
61
- return `${x},${y},${z}`
62
- }
63
-
64
- const batchMessagesLimit = 100
65
-
66
- let queuedMessages: any[] = []
67
- let queueWaiting = false
68
- const postMessage = (data: any, transferList: any[] = []) => {
69
- queuedMessages.push({ data, transferList })
70
- if (queuedMessages.length > batchMessagesLimit) {
71
- drainQueue(0, batchMessagesLimit)
72
- }
73
- if (queueWaiting) return
74
- queueWaiting = true
75
- setTimeout(() => {
76
- queueWaiting = false
77
- drainQueue(0, queuedMessages.length)
78
- })
79
- }
80
-
81
- function drainQueue(from: number, to: number) {
82
- const messages = queuedMessages.slice(from, to)
83
- global.postMessage(messages.map(m => m.data), messages.flatMap(m => m.transferList) as unknown as string)
84
- queuedMessages = queuedMessages.slice(to)
85
- }
86
-
87
- // Single emit point for `sectionFinished`. Consumes one pending request from
88
- // `requestTracker` and posts via the existing batched `postMessage` queue.
89
- //
90
- // Column-mode is the ONLY WASM path now: an emit for a non-requested key is a
91
- // contract violation (`WorldRendererCommon` would throw on the main thread)
92
- // and we surface it via `console.warn` so it shows up in dev/CI without
93
- // killing the worker.
94
- const emitSectionFinished = (payload: { type: 'sectionFinished', key: string } & Record<string, any>) => {
95
- const consumed = requestTracker.consumeOne(payload.key)
96
- if (!consumed) {
97
- console.warn(`[WASM Mesher] sectionFinished for non-requested key ${payload.key} (column-mode contract violation)`)
98
- }
99
- postMessage(payload)
100
- }
101
-
102
- let hadDirty = false
103
- function setSectionDirty(pos: Vec3, value = true) {
104
- if (hadDirty) return
105
-
106
- // hadDirty = true
107
- const x = Math.floor(pos.x / 16) * 16
108
- const sectionHeight = getSectionHeight()
109
- const y = Math.floor(pos.y / sectionHeight) * sectionHeight
110
- const z = Math.floor(pos.z / 16) * 16
111
- const key = sectionKey(x, y, z)
112
- if (!value) {
113
- dirtySections.delete(key)
114
- // The main thread waits for a sectionFinished response to dirty=false too.
115
- // Record + consume it so request accounting stays balanced.
116
- requestTracker.addRequest(key)
117
- emitSectionFinished({ type: 'sectionFinished', key, workerIndex })
118
- return
119
- }
120
-
121
- // Check if we have the chunk for this section
122
- const chunk = world?.getColumn(x, z)
123
- if (chunk?.getSection(pos)) {
124
- dirtySections.set(key, (dirtySections.get(key) || 0) + 1)
125
- requestTracker.addRequest(key)
126
- } else {
127
- // Missing chunks still owe the main thread a sectionFinished response.
128
- requestTracker.addRequest(key)
129
- emitSectionFinished({ type: 'sectionFinished', key, workerIndex })
130
- }
131
- }
132
-
133
- const softCleanup = () => {
134
- world = new World(world.config.version)
135
- globalThis.world = world
136
- }
137
-
138
- const handleMessage = async (data: any) => {
139
- const globalVar: any = globalThis
140
-
141
- if (data.type === 'mcData') {
142
- globalVar.mcData = data.mcData
143
- globalVar.loadedData = data.mcData
144
- }
145
-
146
- if (data.config) {
147
- config = { ...config, ...data.config }
148
- version = config.version || version
149
- world ??= new World(version)
150
- world.config = { ...world.config, ...data.config }
151
- globalThis.world = world
152
- globalThis.Vec3 = Vec3
153
- setConversionCacheLimit(config.disableConversionCache ? 0 : CONVERSION_CACHE_LIMIT)
154
- }
155
-
156
- switch (data.type) {
157
- case 'mesherData': {
158
- setMesherData(data.blockstatesModels, data.blocksAtlas, data.config.outputFormat === 'webgpu')
159
- ;(globalThis as any).__wasmBlockModelCache = new Map()
160
- // Conservative: blockstates/version/world config may have changed.
161
- clearConversionCache()
162
-
163
- await initWasm()
164
- allDataReady = true
165
- workerIndex = data.workerIndex
166
- break
167
- }
168
- case 'dirty': {
169
- const loc = new Vec3(data.x, data.y, data.z)
170
- setSectionDirty(loc, data.value)
171
- break
172
- }
173
- case 'chunk': {
174
- // Invalidate BEFORE replacing the column reference so a stale entry
175
- // can never outlive the old chunk object.
176
- invalidateConversion(data.x, data.z)
177
- if (!world) break
178
- world.addColumn(data.x, data.z, data.chunk)
179
- if (data.customBlockModels) {
180
- const chunkKey = `${data.x},${data.z}`
181
- world.customBlockModels.set(chunkKey, data.customBlockModels)
182
- }
183
- // Safety-net heightmap push for fully empty columns. With WASM
184
- // mesher as the sole path, the main thread no longer requests
185
- // `getHeightmap` on chunk load — heightmaps come from
186
- // `processColumnTick`. But a fully empty column (no sections, or
187
- // all sections missing) never enters that path because
188
- // `setSectionDirty` short-circuits when `chunk.getSection(pos)` is
189
- // falsy, so `processColumnTick` never sees it. Without this push
190
- // downstream consumers (e.g. `rain.ts`) would have no heightmap
191
- // entry for such columns. We send a cheap sentinel-filled
192
- // `Int16Array(256).fill(-32768)` — no JS heightmap scan — only when
193
- // we detect zero sections; non-empty columns get their real
194
- // heightmap from the next `processColumnTick`.
195
- const sectionH = SECTION_HEIGHT
196
- const minY = config?.worldMinY ?? 0
197
- const maxY = config?.worldMaxY ?? 256
198
- const column = world.getColumn(data.x, data.z)
199
- let hasAnySection = false
200
- for (let y = minY; y < maxY; y += sectionH) {
201
- if (column?.getSection?.(new Vec3(0, y, 0))) {
202
- hasAnySection = true
203
- break
204
- }
205
- }
206
- if (!hasAnySection) {
207
- const emptyHeightmap = new Int16Array(256).fill(EMPTY_COLUMN_HEIGHTMAP_SENTINEL)
208
- postMessage(
209
- { type: 'heightmap', key: `${data.x >> 4},${data.z >> 4}`, heightmap: emptyHeightmap },
210
- [emptyHeightmap.buffer]
211
- )
212
- }
213
- break
214
- }
215
- case 'unloadChunk': {
216
- invalidateConversion(data.x, data.z)
217
- if (!world) break
218
- world.removeColumn(data.x, data.z)
219
- world.customBlockModels.delete(`${data.x},${data.z}`)
220
- if (Object.keys(world.columns).length === 0) softCleanup()
221
- break
222
- }
223
- case 'blockUpdate': {
224
- const loc = new Vec3(data.pos.x, data.pos.y, data.pos.z).floored()
225
- if (data.stateId !== undefined && data.stateId !== null) {
226
- world?.setBlockStateId(loc, data.stateId)
227
- }
228
-
229
- const chunkX = Math.floor(loc.x / 16) * 16
230
- const chunkZ = Math.floor(loc.z / 16) * 16
231
- // In-place mutation preserves chunk identity; explicit invalidation
232
- // is required so the next tick recomputes from current block state.
233
- invalidateConversion(chunkX, chunkZ)
234
- const chunkKey = `${chunkX},${chunkZ}`
235
- if (data.customBlockModels) {
236
- world?.customBlockModels.set(chunkKey, data.customBlockModels)
237
- }
238
- break
239
- }
240
- case 'reset': {
241
- world = undefined as any
242
- dirtySections.clear()
243
- requestTracker.clear()
244
- clearConversionCache()
245
- globalVar.mcData = null
246
- globalVar.loadedData = null
247
- allDataReady = false
248
- break
249
- }
250
- case 'getHeightmap': {
251
- // Fallback path. With WASM column mesher as the sole path, the main
252
- // thread should be receiving heightmaps as `'heightmap'` push messages
253
- // posted by `processColumnTick`. This handler stays as a safety net for
254
- // cases where the WASM heightmap could not be extracted (length mismatch
255
- // or missing field) — see the `extractColumnHeightmap` warn below.
256
- console.warn(`[WASM Mesher] explicit getHeightmap request for ${data.x},${data.z} — push from processColumnTick missed?`)
257
- if (!world) {
258
- const emptyHeightmap = new Int16Array(256).fill(EMPTY_COLUMN_HEIGHTMAP_SENTINEL)
259
- postMessage({ type: 'heightmap', key: `${Math.floor(data.x / 16)},${Math.floor(data.z / 16)}`, heightmap: emptyHeightmap })
260
- break
261
- }
262
- const { key, heightmap } = handleGetHeightmap(world, data.x, data.z)
263
- postMessage({ type: 'heightmap', key, heightmap }, [heightmap.buffer])
264
-
265
- break
266
- }
267
- // Note: getCustomBlockModel not implemented in WASM version
268
- // as it requires World class functionality
269
- }
270
- }
271
-
272
- // eslint-disable-next-line no-restricted-globals -- TODO
273
- self.onmessage = ({ data }) => {
274
- if (Array.isArray(data)) {
275
- // eslint-disable-next-line unicorn/no-array-for-each
276
- data.forEach(handleMessage)
277
- return
278
- }
279
-
280
- handleMessage(data)
281
- }
282
-
283
- // Section height is always 16 in column mode (the only WASM path).
284
- const getSectionHeight = () => SECTION_HEIGHT
285
-
286
-
287
- // 3x3 X/Z neighbor set for column meshing. Y-agnostic because full-column
288
- // meshing converts the entire world Y range in one go.
289
- function collectChunksForColumn(x: number, z: number) {
290
- const result = [] as Array<{ x: number, z: number, chunk: any }>
291
- const target = world.getColumn(x, z)
292
- if (target) result.push({ x, z, chunk: target })
293
- const offsets = [-16, 0, 16]
294
- for (const dx of offsets) {
295
- for (const dz of offsets) {
296
- if (dx === 0 && dz === 0) continue
297
- const nx = x + dx
298
- const nz = z + dz
299
- const c = world.getColumn(nx, nz)
300
- if (c) result.push({ x: nx, z: nz, chunk: c })
301
- }
302
- }
303
- return result
304
- }
305
-
306
- function makeEmptyColumnGeometry(sx: number, sy: number, sz: number, sectionHeight: number, hadErrors: boolean): MesherGeometryOutput {
307
- return {
308
- sectionYNumber: (sy - (config?.worldMinY || 0)) >> 4,
309
- chunkKey: worldColumnKey(sx, sz),
310
- sectionStartY: sy,
311
- sectionEndY: sy + sectionHeight,
312
- sectionStartX: sx,
313
- sectionEndX: sx + 16,
314
- sectionStartZ: sz,
315
- sectionEndZ: sz + 16,
316
- sx: sx + 8,
317
- sy: sy + 8,
318
- sz: sz + 8,
319
- positions: new Float32Array(0),
320
- normals: new Float32Array(0),
321
- colors: new Float32Array(0),
322
- uvs: new Float32Array(0),
323
- indices: new Uint32Array(0),
324
- indicesCount: 0,
325
- using32Array: false,
326
- tiles: {},
327
- heads: {},
328
- signs: {},
329
- banners: {},
330
- hadErrors,
331
- blocksCount: 0,
332
- }
333
- }
334
-
335
- // Full-column meshing path — the sole WASM mesh path.
336
- // It groups dirty section keys by chunk column, runs one WASM call per column
337
- // over the full Y range, then splits the column output back into per-section
338
- // geometries. Only requested section keys are emitted back to the main thread.
339
- function processColumnTick() {
340
- const worldMinY = config?.worldMinY ?? 0
341
- const worldMaxY = config?.worldMaxY ?? 256
342
- const columnHeight = worldMaxY - worldMinY
343
- const sectionHeight = SECTION_HEIGHT
344
-
345
- // Group dirty sections by chunk column (`${x},${z}` in world block
346
- // coords — the same units used by section keys). This guarantees a
347
- // single WASM call per column per tick even when multiple section keys
348
- // of the same column are dirty.
349
- const groups = new Map<string, { x: number, z: number, sections: Array<{ key: string, x: number, y: number, z: number, count: number }> }>()
350
- for (const [key, count] of dirtySections) {
351
- const [sx, sy, sz] = key.split(',').map(v => parseInt(v, 10))
352
- const colKey = `${sx},${sz}`
353
- let g = groups.get(colKey)
354
- if (!g) {
355
- g = { x: sx, z: sz, sections: [] }
356
- groups.set(colKey, g)
357
- }
358
- g.sections.push({ key, x: sx, y: sy, z: sz, count })
359
- }
360
- dirtySections.clear()
361
-
362
- for (const group of groups.values()) {
363
- const { x, z, sections } = group
364
- const targetChunk = world.getColumn(x, z)
365
-
366
- let exportedMap: Map<string, { exported: import('../three/worldGeometryExport').ExportedSection, blocksCount: number }> | null = null
367
- let processTime = 0
368
- let prePhase = 0
369
- let wasmPhase = 0
370
- let postPhase = 0
371
- let preTargetConvert = 0
372
- let preNeighborConvert = 0
373
- let preNeighborCount = 0
374
- let preTypedArrayBuild = 0
375
- let preOther = 0
376
- let preCacheHits = 0
377
- let preCacheMisses = 0
378
- let hadError = false
379
- // Outer-scope timestamps so we can finalize `processTime` and
380
- // `postPhase` AFTER the per-section emit loop runs (the loop builds
381
- // typed arrays, walks block-entity metadata, and calls postMessage —
382
- // all of which are part of the worker's real cost and must be
383
- // attributed to the column).
384
- let columnStart = 0
385
- let postStart = 0
386
-
387
- if (targetChunk && wasm) {
388
- columnStart = performance.now()
389
- const start = columnStart
390
- const t0 = start
391
- try {
392
- const chunksToUse = collectChunksForColumn(x, z)
393
- const chunkCount = chunksToUse.length
394
-
395
- const conversions = chunksToUse.map(({ x: cx, z: cz, chunk }) => {
396
- const cs = performance.now()
397
- const { result: conv, hit } = getOrConvertColumn(
398
- cx,
399
- cz,
400
- chunk,
401
- version,
402
- worldMinY,
403
- worldMaxY,
404
- () => convertChunkToWasm(
405
- chunk,
406
- version,
407
- cx,
408
- cz,
409
- worldMinY,
410
- worldMaxY
411
- // No sectionY/sectionHeight => full column conversion.
412
- ),
413
- chunk
414
- )
415
- const ce = performance.now()
416
- if (hit) preCacheHits++
417
- else preCacheMisses++
418
- if (cx === x && cz === z) {
419
- preTargetConvert += ce - cs
420
- } else {
421
- preNeighborConvert += ce - cs
422
- preNeighborCount++
423
- }
424
- return conv
425
- })
426
-
427
- const {
428
- invisibleBlocks,
429
- transparentBlocks,
430
- noAoBlocks,
431
- cullIdenticalBlocks,
432
- occludingBlocks,
433
- } = conversions[0]
434
-
435
- let wasmResult: any
436
- let t1: number
437
- if (chunkCount === 1 || !(wasm as any).generate_geometry_multi) {
438
- // Single-chunk path: no discrete typed-array build/copy step
439
- // (the per-chunk arrays from convertChunkToWasm are passed
440
- // straight through). preTypedArrayBuild stays 0.
441
- const { blockStates, blockLight, skyLight, biomesArray } = conversions[0]
442
- t1 = performance.now()
443
- wasmResult = wasm.generate_geometry(
444
- x, worldMinY, z, columnHeight,
445
- worldMinY, worldMaxY,
446
- worldMinY,
447
- blockStates, blockLight, skyLight, biomesArray,
448
- invisibleBlocks, transparentBlocks, noAoBlocks, cullIdenticalBlocks, occludingBlocks,
449
- config?.enableLighting !== false,
450
- config?.smoothLighting !== false,
451
- config?.skyLight || 15
452
- )
453
- } else {
454
- const tBuildStart = performance.now()
455
- const perChunkLen = conversions[0].blockStates.length
456
- const xs = new Int32Array(chunkCount)
457
- const zs = new Int32Array(chunkCount)
458
- const blockStatesAll = new Uint16Array(perChunkLen * chunkCount)
459
- const blockLightAll = new Uint8Array(perChunkLen * chunkCount)
460
- const skyLightAll = new Uint8Array(perChunkLen * chunkCount)
461
- const biomesAll = new Uint8Array(perChunkLen * chunkCount)
462
-
463
- for (let i = 0; i < chunkCount; i++) {
464
- const c = conversions[i]
465
- xs[i] = chunksToUse[i].x
466
- zs[i] = chunksToUse[i].z
467
- blockStatesAll.set(c.blockStates, perChunkLen * i)
468
- blockLightAll.set(c.blockLight, perChunkLen * i)
469
- skyLightAll.set(c.skyLight, perChunkLen * i)
470
- biomesAll.set(c.biomesArray, perChunkLen * i)
471
- }
472
- preTypedArrayBuild = performance.now() - tBuildStart
473
-
474
- t1 = performance.now()
475
- wasmResult = (wasm as any).generate_geometry_multi(
476
- x, worldMinY, z, columnHeight,
477
- worldMinY, worldMaxY,
478
- worldMinY,
479
- xs, zs,
480
- blockStatesAll, blockLightAll, skyLightAll, biomesAll,
481
- invisibleBlocks, transparentBlocks, noAoBlocks, cullIdenticalBlocks, occludingBlocks,
482
- config?.enableLighting !== false,
483
- config?.smoothLighting !== false,
484
- config?.skyLight || 15
485
- )
486
- }
487
-
488
- const t2 = performance.now()
489
- postStart = t2
490
-
491
- // Split full-column output back into per-section ExportedSection
492
- // entries — only for the section keys the main thread actually
493
- // requested. Sections in the column that were NOT requested are
494
- // intentionally skipped (the request tracker would warn if we
495
- // emitted sectionFinished for them).
496
- const requestedSectionKeys = sections.map(s => ({ x: s.x, y: s.y, z: s.z }))
497
- exportedMap = splitColumnWasmOutputToSections(
498
- wasmResult,
499
- requestedSectionKeys,
500
- { version, world, sectionHeight }
501
- )
502
-
503
- // Push heightmap from the WASM column output. With column meshing as
504
- // the only WASM path, the main thread does not request heightmaps
505
- // explicitly anymore — the worker is the source of truth and pushes
506
- // a `'heightmap'` message every column tick. Key shape matches the
507
- // legacy `handleGetHeightmap` contract: `${chunkX>>4},${chunkZ>>4}`.
508
- const heightmapKey = `${x >> 4},${z >> 4}`
509
- const wasmHeightmap = extractColumnHeightmap(wasmResult)
510
- if (wasmHeightmap) {
511
- postMessage({ type: 'heightmap', key: heightmapKey, heightmap: wasmHeightmap }, [wasmHeightmap.buffer])
512
- } else {
513
- console.warn(`[WASM Mesher] heightmap extraction returned null for column ${x},${z}, falling back to JS computeHeightmap`)
514
- const fallback = handleGetHeightmap(world, x, z)
515
- postMessage({ type: 'heightmap', key: fallback.key, heightmap: fallback.heightmap }, [fallback.heightmap.buffer])
516
- }
517
-
518
- prePhase = t1 - t0
519
- wasmPhase = t2 - t1
520
- preOther = Math.max(0, prePhase - (preTargetConvert + preNeighborConvert + preTypedArrayBuild))
521
- // NOTE: `postPhase` and `processTime` are finalized AFTER the
522
- // per-section emit loop below — see the `Finalize column phase
523
- // numbers` block.
524
- } catch (err) {
525
- console.error(`[WASM Mesher] Error processing column ${x},${z}:`, err)
526
- hadError = true
527
- }
528
- }
529
-
530
- // Emit geometry + sectionFinished for each requested section. Column-
531
- // level perf metrics are attributed to the first sectionFinished of
532
- // the first requested section (others get zeros) so totals don't
533
- // double-count.
534
- //
535
- // Coherent chunk appearance: column mode relies on the existing
536
- // `_renderByChunks` / `chunkFinished` contract on the main thread.
537
- // ChunkMeshManager batches sections per column and reveals them
538
- // atomically once `WorldRendererCommon` sees the last
539
- // `sectionFinished` for the column. No dedicated `columnFinished`
540
- // worker message is needed.
541
- // Pass 1: build geometry + postMessage for each requested section.
542
- // We collect finished keys here and emit `sectionFinished` only in
543
- // Pass 2 below, after `postPhase` / `processTime` have been
544
- // finalized — otherwise the totals attached to the first event
545
- // would miss the typed-array allocation, block-entity walk, and
546
- // postMessage cost of every section in this column.
547
- const finished: Array<{ key: string, count: number }> = []
548
- for (const s of sections) {
549
- const { key, x: sx, y: sy, z: sz, count } = s
550
-
551
- if (exportedMap && !hadError) {
552
- const entry = exportedMap.get(key)
553
- const exported = entry?.exported
554
- const sectionBlocksCount = entry?.blocksCount ?? 0
555
- // Block entity metadata still needs a per-section world walk
556
- // (signs/heads/banners), matching the legacy per-section path.
557
- const signs: Record<string, SignMeta> = {}
558
- const heads: Record<string, HeadMeta> = {}
559
- const banners: Record<string, BannerMeta> = {}
560
- const beTarget = { signs, heads, banners }
561
- const beOpts = { disableBlockEntityTextures: world.config.disableBlockEntityTextures }
562
- const cursor = new Vec3(0, 0, 0)
563
- for (cursor.y = sy; cursor.y < sy + sectionHeight; cursor.y++) {
564
- for (cursor.z = sz; cursor.z < sz + 16; cursor.z++) {
565
- for (cursor.x = sx; cursor.x < sx + 16; cursor.x++) {
566
- const b = world.getBlock(cursor)
567
- if (!b) continue
568
- collectBlockEntityMetadata(b, cursor.x, cursor.y, cursor.z, beTarget, beOpts)
569
- }
570
- }
571
- }
572
-
573
- let geometry: MesherGeometryOutput
574
- let transferable: any[] = []
575
- if (exported && exported.geometry.indices.length > 0) {
576
- const maxIndex = exported.geometry.indices.length > 0
577
- ? Math.max(...exported.geometry.indices)
578
- : 0
579
- const using32Array = maxIndex > 65535
580
- geometry = {
581
- sectionYNumber: (sy - (config?.worldMinY || 0)) >> 4,
582
- chunkKey: worldColumnKey(sx, sz),
583
- sectionStartY: sy,
584
- sectionEndY: sy + sectionHeight,
585
- sectionStartX: sx,
586
- sectionEndX: sx + 16,
587
- sectionStartZ: sz,
588
- sectionEndZ: sz + 16,
589
- sx: sx + 8,
590
- sy: sy + 8,
591
- sz: sz + 8,
592
- positions: new Float32Array(exported.geometry.positions),
593
- normals: new Float32Array(exported.geometry.normals),
594
- colors: new Float32Array(exported.geometry.colors),
595
- uvs: new Float32Array(exported.geometry.uvs),
596
- indices: using32Array
597
- ? new Uint32Array(exported.geometry.indices)
598
- : new Uint16Array(exported.geometry.indices),
599
- indicesCount: exported.geometry.indices.length,
600
- using32Array,
601
- tiles: {},
602
- heads,
603
- signs,
604
- banners,
605
- hadErrors: false,
606
- // Per-section block bucket size from the column split. The
607
- // field is informational (used by `chunkMeshManager` for the
608
- // `B:` debug overlay stat) and matches the per-section path's
609
- // semantics: number of blocks that contributed faces to this
610
- // section's geometry.
611
- blocksCount: sectionBlocksCount,
612
- }
613
- transferable = [
614
- geometry.positions?.buffer,
615
- geometry.normals?.buffer,
616
- geometry.colors?.buffer,
617
- geometry.uvs?.buffer,
618
- //@ts-ignore
619
- geometry.indices?.buffer,
620
- ].filter(Boolean)
621
- } else {
622
- geometry = makeEmptyColumnGeometry(sx, sy, sz, sectionHeight, false)
623
- // Still attach block entity metadata so the main thread sees
624
- // signs/heads/banners even for empty-mesh sections.
625
- geometry.signs = signs
626
- geometry.heads = heads
627
- geometry.banners = banners
628
- }
629
- postMessage({ type: 'geometry', key, geometry, workerIndex }, transferable)
630
- } else if (hadError) {
631
- const errorGeometry = makeEmptyColumnGeometry(sx, sy, sz, sectionHeight, true)
632
- postMessage({ type: 'geometry', key, geometry: errorGeometry, workerIndex })
633
- }
634
- // No targetChunk and no error: skip geometry message (mirrors
635
- // legacy behavior for sections whose chunk has been unloaded
636
- // mid-tick) but still emit sectionFinished below so the main
637
- // thread's sectionsWaiting counter unblocks.
638
- finished.push({ key, count })
639
- }
640
-
641
- // Finalize column phase numbers — now they include split + per-
642
- // section typed-array build + block-entity walk + geometry
643
- // postMessage cost.
644
- if (columnStart > 0 && !hadError) {
645
- const tEnd = performance.now()
646
- if (postStart > 0) postPhase = tEnd - postStart
647
- processTime = tEnd - columnStart
648
- }
649
-
650
- // Pass 2: emit sectionFinished events. Column-level perf metrics
651
- // are attributed to the first emitted sectionFinished (others get
652
- // zeros) so totals don't double-count.
653
- let attributed = false
654
- for (const { key, count } of finished) {
655
- for (let i = 0; i < count; i++) {
656
- emitSectionFinished({
657
- type: 'sectionFinished',
658
- key,
659
- workerIndex,
660
- processTime: !attributed ? processTime : 0,
661
- pre: !attributed ? prePhase : 0,
662
- wasm: !attributed ? wasmPhase : 0,
663
- post: !attributed ? postPhase : 0,
664
- preTargetConvert: !attributed ? preTargetConvert : 0,
665
- preNeighborConvert: !attributed ? preNeighborConvert : 0,
666
- preNeighborCount: !attributed ? preNeighborCount : 0,
667
- preTypedArrayBuild: !attributed ? preTypedArrayBuild : 0,
668
- preOther: !attributed ? preOther : 0,
669
- preCacheHits: !attributed ? preCacheHits : 0,
670
- preCacheMisses: !attributed ? preCacheMisses : 0,
671
- })
672
- attributed = true
673
- }
674
- }
675
- }
676
- }
677
-
678
- setInterval(async () => {
679
- if (!allDataReady) return
680
-
681
- // Ensure WASM is initialized
682
- if (!wasmInitialized) {
683
- await initWasm()
684
- if (!wasmInitialized) return // Still not initialized, skip this cycle
685
- }
686
-
687
- if (dirtySections.size === 0) return
688
-
689
- try {
690
- processColumnTick()
691
- } catch (err) {
692
- console.error('[WASM Mesher] processColumnTick failed:', err)
693
- // Swallow to avoid breaking the setInterval; individual columns
694
- // already have their own try/catch.
695
- }
696
- }, 50)