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.
- package/dist/mesher.js +8 -8
- package/dist/mesher.js.map +4 -4
- package/dist/mesherWasm.js +94 -94
- package/dist/minecraft-renderer.js +57 -57
- package/dist/minecraft-renderer.js.meta.json +1 -1
- package/dist/threeWorker.js +66 -66
- package/package.json +3 -4
- package/src/bundler/bundlePrepare.ts +56 -0
- package/src/graphicsBackend/appViewer.ts +10 -0
- package/src/graphicsBackend/config.ts +5 -1
- package/src/graphicsBackend/preloadWorkers.ts +187 -0
- package/src/lib/worldrendererCommon.ts +26 -2
- package/src/{mesher → mesher-legacy}/mesher.ts +14 -4
- package/src/{mesher → mesher-legacy}/test/mesherTester.ts +2 -2
- package/src/{mesher → mesher-legacy}/test/run/test-js.ts +1 -1
- package/src/{mesher → mesher-legacy}/test/test-perf.ts +1 -1
- package/src/{mesher → mesher-legacy}/test/tests.test.ts +1 -1
- package/src/{mesher → mesher-shared}/shared.ts +2 -0
- package/src/playground/allEntitiesDebug.ts +1 -1
- package/src/three/chunkMeshManager.ts +1 -1
- package/src/three/entities.ts +19 -6
- package/src/three/entity/EntityMesh.ts +123 -140
- package/src/three/graphicsBackendBase.ts +13 -0
- package/src/three/holdingBlock.ts +1 -1
- package/src/three/holdingBlockLegacy.ts +1 -1
- package/src/three/modules/sciFiWorldReveal.ts +1 -1
- package/src/three/worldRendererThree.ts +2 -2
- package/src/wasm-mesher/README.md +90 -0
- package/src/{wasm-lib → wasm-mesher/bridge}/convertChunk.ts +2 -2
- package/src/{wasm-lib → wasm-mesher/bridge}/render-from-wasm.ts +4 -4
- package/src/wasm-mesher/runtime-build/wasm_mesher.d.ts +210 -0
- package/src/wasm-mesher/runtime-build/wasm_mesher.js +881 -0
- package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm +0 -0
- package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm.d.ts +24 -0
- package/src/{mesher/test → wasm-mesher/tests}/heightmapParity.test.ts +4 -4
- package/src/{mesher/test → wasm-mesher/tests}/mesherWasmConversionCache.test.ts +2 -2
- package/src/{mesher/test → wasm-mesher/tests}/splitColumnWasmOutput.test.ts +1 -1
- package/src/wasm-mesher/worker/mesherWasm.ts +1247 -0
- package/src/{mesher → wasm-mesher/worker}/mesherWasmConversionCache.ts +1 -1
- package/src/worldView/types.ts +90 -0
- package/src/mesher/mesherWasm.ts +0 -696
- package/wasm/wasm_mesher.d.ts +0 -46
- package/wasm/wasm_mesher.js +0 -443
- package/wasm/wasm_mesher_bg.wasm +0 -0
- package/wasm/wasm_mesher_bg.wasm.d.ts +0 -9
- /package/src/{mesher → mesher-legacy}/test/a.ts +0 -0
- /package/src/{mesher → mesher-legacy}/test/playground.ts +0 -0
- /package/src/{mesher → mesher-legacy}/test/run/chunk.ts +0 -0
- /package/src/{mesher → mesher-legacy}/test/snapshotUtils.ts +0 -0
- /package/src/{mesher → mesher-shared}/blockEntityMetadata.ts +0 -0
- /package/src/{mesher → mesher-shared}/computeHeightmap.ts +0 -0
- /package/src/{mesher → mesher-shared}/models.ts +0 -0
- /package/src/{mesher → mesher-shared}/modelsGeometryCommon.ts +0 -0
- /package/src/{mesher → mesher-shared}/standaloneRenderer.ts +0 -0
- /package/src/{mesher → mesher-shared}/world.ts +0 -0
- /package/src/{mesher → mesher-shared}/worldConstants.ts +0 -0
- /package/src/{mesher → wasm-mesher/worker}/mesherWasmRequestTracker.ts +0 -0
|
@@ -0,0 +1,1247 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { Vec3 } from 'vec3'
|
|
3
|
+
import { convertChunkToWasm, getBlockMeta, type ChunkConversionResult } from '../bridge/convertChunk'
|
|
4
|
+
import { extractColumnHeightmap, splitColumnWasmOutputToSections } from '../bridge/render-from-wasm'
|
|
5
|
+
import { setBlockStatesData as setMesherData } from '../../mesher-shared/models'
|
|
6
|
+
import { defaultMesherConfig, type MesherGeometryOutput, SECTION_HEIGHT } from '../../mesher-shared/shared'
|
|
7
|
+
import { worldColumnKey, World } from '../../mesher-shared/world'
|
|
8
|
+
import { handleGetHeightmap, EMPTY_COLUMN_HEIGHTMAP_SENTINEL } from '../../mesher-shared/computeHeightmap'
|
|
9
|
+
import { collectBlockEntityMetadata, type SignMeta, type HeadMeta, type BannerMeta } from '../../mesher-shared/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('../runtime-build/wasm_mesher.js') | null = null
|
|
20
|
+
let wasmInitialized = false
|
|
21
|
+
|
|
22
|
+
// Pending raw `update_light` packets that arrived before WASM finished
|
|
23
|
+
// loading. Parsed and drained once `initWasm` resolves. Without this queue
|
|
24
|
+
// the very first batch of chunks (~40-50 in our smoke test) lose their
|
|
25
|
+
// real lighting and fall back to fill(15) — which makes shadowed areas
|
|
26
|
+
// (under trees, cliff edges) look brighter than vanilla, and after the
|
|
27
|
+
// renderer interpolates with neighbour chunks that DO have real light,
|
|
28
|
+
// the seams look like "local night".
|
|
29
|
+
const pendingUpdateLightV17: Array<{ rawPacket: Uint8Array, numSections: number }> = []
|
|
30
|
+
// Separate v16 pending queue so 1.16 update_light packets that arrive
|
|
31
|
+
// before WASM is initialised land in the v16 light cache (not v17) on
|
|
32
|
+
// drain — the mesh hot path looks them up per protocol family.
|
|
33
|
+
const pendingUpdateLightV16: Array<{ rawPacket: Uint8Array }> = []
|
|
34
|
+
|
|
35
|
+
function processUpdateLightV17 (rawPacket: Uint8Array, numSections: number): void {
|
|
36
|
+
if (!wasm || !(wasm as any).parseUpdateLightV17) {
|
|
37
|
+
pendingUpdateLightV17.push({ rawPacket, numSections })
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const parsed: any = (wasm as any).parseUpdateLightV17(rawPacket, numSections)
|
|
42
|
+
const x = (parsed.x as number) * 16
|
|
43
|
+
const z = (parsed.z as number) * 16
|
|
44
|
+
updateLightV17Cache.set(rawCacheKey(x, z), {
|
|
45
|
+
skyLight: parsed.skyLight as Uint8Array,
|
|
46
|
+
blockLight: parsed.blockLight as Uint8Array,
|
|
47
|
+
})
|
|
48
|
+
invalidateConversion(x, z)
|
|
49
|
+
} catch (err) {
|
|
50
|
+
console.warn('[WASM Mesher] parseUpdateLightV17 failed:', err)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 1.16 update_light shares the wire format (and the WASM parser) with
|
|
55
|
+
// 1.17, but we keep a SEPARATE result cache to keep the two protocol
|
|
56
|
+
// families fully isolated — the mesh tick picks v16 vs v17 by which raw
|
|
57
|
+
// chunk cache has the entry, and crossing the streams could mismatch a
|
|
58
|
+
// stale 1.17 column with 1.16 light or vice versa during version switches.
|
|
59
|
+
function processUpdateLightV16 (rawPacket: Uint8Array): void {
|
|
60
|
+
if (!wasm || !(wasm as any).parseUpdateLightV17) {
|
|
61
|
+
pendingUpdateLightV16.push({ rawPacket })
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const parsed: any = (wasm as any).parseUpdateLightV17(rawPacket, 16)
|
|
66
|
+
const x = (parsed.x as number) * 16
|
|
67
|
+
const z = (parsed.z as number) * 16
|
|
68
|
+
updateLightV16Cache.set(rawCacheKey(x, z), {
|
|
69
|
+
skyLight: parsed.skyLight as Uint8Array,
|
|
70
|
+
blockLight: parsed.blockLight as Uint8Array,
|
|
71
|
+
})
|
|
72
|
+
invalidateConversion(x, z)
|
|
73
|
+
} catch (err) {
|
|
74
|
+
console.warn('[WASM Mesher] parseUpdateLightV17 (v16) failed:', err)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function initWasm() {
|
|
79
|
+
if (wasmInitialized) return
|
|
80
|
+
try {
|
|
81
|
+
wasmInitialized = true
|
|
82
|
+
wasm = await import('../runtime-build/wasm_mesher.js')
|
|
83
|
+
await wasm.default('/wasm_mesher_bg.wasm') as any
|
|
84
|
+
|
|
85
|
+
if (pendingUpdateLightV17.length > 0) {
|
|
86
|
+
console.log('[WASM Mesher] draining', pendingUpdateLightV17.length, 'pending update_light v17 packets')
|
|
87
|
+
const queue = pendingUpdateLightV17.splice(0, pendingUpdateLightV17.length)
|
|
88
|
+
for (const item of queue) processUpdateLightV17(item.rawPacket, item.numSections)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (pendingUpdateLightV16.length > 0) {
|
|
92
|
+
console.log('[WASM Mesher] draining', pendingUpdateLightV16.length, 'pending update_light v16 packets')
|
|
93
|
+
const queue = pendingUpdateLightV16.splice(0, pendingUpdateLightV16.length)
|
|
94
|
+
for (const item of queue) processUpdateLightV16(item.rawPacket)
|
|
95
|
+
}
|
|
96
|
+
} catch (err) {
|
|
97
|
+
console.error('Failed to initialize WASM mesher:', err)
|
|
98
|
+
wasmInitialized = true // Don't try to initialize again
|
|
99
|
+
// Don't throw - allow worker to continue without WASM (will fail on first use)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
globalThis.structuredClone ??= (value) => JSON.parse(JSON.stringify(value))
|
|
104
|
+
|
|
105
|
+
if (globalThis.module && module.require) {
|
|
106
|
+
// If we are in a node environment, we need to fake some env variables
|
|
107
|
+
const r = module.require
|
|
108
|
+
const { parentPort } = r('worker_threads')
|
|
109
|
+
global.self = parentPort
|
|
110
|
+
global.postMessage = (value, transferList) => { parentPort.postMessage(value, transferList) }
|
|
111
|
+
global.performance = r('perf_hooks').performance
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let workerIndex = 0
|
|
115
|
+
let config = defaultMesherConfig
|
|
116
|
+
let version = '1.16.5'
|
|
117
|
+
let world: World // chunkKey -> chunk data
|
|
118
|
+
let dirtySections = new Map<string, number>()
|
|
119
|
+
// Kept in sync with `dirtySections` so column mode can filter outgoing
|
|
120
|
+
// geometry/sectionFinished events to only the section keys requested by the
|
|
121
|
+
// main thread, even though a full-column WASM call may generate more data.
|
|
122
|
+
const requestTracker = new SectionRequestTracker()
|
|
123
|
+
let allDataReady = false
|
|
124
|
+
|
|
125
|
+
function sectionKey(x: number, y: number, z: number) {
|
|
126
|
+
return `${x},${y},${z}`
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const batchMessagesLimit = 100
|
|
130
|
+
|
|
131
|
+
let queuedMessages: any[] = []
|
|
132
|
+
let queueWaiting = false
|
|
133
|
+
const postMessage = (data: any, transferList: any[] = []) => {
|
|
134
|
+
queuedMessages.push({ data, transferList })
|
|
135
|
+
if (queuedMessages.length > batchMessagesLimit) {
|
|
136
|
+
drainQueue(0, batchMessagesLimit)
|
|
137
|
+
}
|
|
138
|
+
if (queueWaiting) return
|
|
139
|
+
queueWaiting = true
|
|
140
|
+
setTimeout(() => {
|
|
141
|
+
queueWaiting = false
|
|
142
|
+
drainQueue(0, queuedMessages.length)
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function drainQueue(from: number, to: number) {
|
|
147
|
+
const messages = queuedMessages.slice(from, to)
|
|
148
|
+
global.postMessage(messages.map(m => m.data), messages.flatMap(m => m.transferList) as unknown as string)
|
|
149
|
+
queuedMessages = queuedMessages.slice(to)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Single emit point for `sectionFinished`. Consumes one pending request from
|
|
153
|
+
// `requestTracker` and posts via the existing batched `postMessage` queue.
|
|
154
|
+
//
|
|
155
|
+
// Column-mode is the ONLY WASM path now: an emit for a non-requested key is a
|
|
156
|
+
// contract violation (`WorldRendererCommon` would throw on the main thread)
|
|
157
|
+
// and we surface it via `console.warn` so it shows up in dev/CI without
|
|
158
|
+
// killing the worker.
|
|
159
|
+
const emitSectionFinished = (payload: { type: 'sectionFinished', key: string } & Record<string, any>) => {
|
|
160
|
+
const consumed = requestTracker.consumeOne(payload.key)
|
|
161
|
+
if (!consumed) {
|
|
162
|
+
console.warn(`[WASM Mesher] sectionFinished for non-requested key ${payload.key} (column-mode contract violation)`)
|
|
163
|
+
}
|
|
164
|
+
postMessage(payload)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let hadDirty = false
|
|
168
|
+
function setSectionDirty(pos: Vec3, value = true) {
|
|
169
|
+
if (hadDirty) return
|
|
170
|
+
|
|
171
|
+
// hadDirty = true
|
|
172
|
+
const x = Math.floor(pos.x / 16) * 16
|
|
173
|
+
const sectionHeight = getSectionHeight()
|
|
174
|
+
const y = Math.floor(pos.y / sectionHeight) * sectionHeight
|
|
175
|
+
const z = Math.floor(pos.z / 16) * 16
|
|
176
|
+
const key = sectionKey(x, y, z)
|
|
177
|
+
if (!value) {
|
|
178
|
+
dirtySections.delete(key)
|
|
179
|
+
// The main thread waits for a sectionFinished response to dirty=false too.
|
|
180
|
+
// Record + consume it so request accounting stays balanced.
|
|
181
|
+
requestTracker.addRequest(key)
|
|
182
|
+
emitSectionFinished({ type: 'sectionFinished', key, workerIndex })
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Check if we have the chunk for this section
|
|
187
|
+
const chunk = world?.getColumn(x, z)
|
|
188
|
+
if (chunk?.getSection(pos)) {
|
|
189
|
+
dirtySections.set(key, (dirtySections.get(key) || 0) + 1)
|
|
190
|
+
requestTracker.addRequest(key)
|
|
191
|
+
} else {
|
|
192
|
+
// Missing chunks still owe the main thread a sectionFinished response.
|
|
193
|
+
requestTracker.addRequest(key)
|
|
194
|
+
emitSectionFinished({ type: 'sectionFinished', key, workerIndex })
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const softCleanup = () => {
|
|
199
|
+
world = new World(world.config.version)
|
|
200
|
+
globalThis.world = world
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Stage 3 (issue-15-wasm): cache of raw `map_chunk` packets keyed by
|
|
204
|
+
// `"x,z"`. Populated from the main thread (`setRawMapChunk` message), used
|
|
205
|
+
// to bypass the JS hot loop `convertChunkToWasm` for protocol >= 757
|
|
206
|
+
// (1.18+). Block updates / chunk unload invalidate entries so we fall back
|
|
207
|
+
// to the legacy column-walk path until the next `map_chunk` arrives.
|
|
208
|
+
interface RawMapChunkEntry {
|
|
209
|
+
rawPacket: Uint8Array
|
|
210
|
+
protocol: number
|
|
211
|
+
numSections: number
|
|
212
|
+
}
|
|
213
|
+
const rawMapChunkCache = new Map<string, RawMapChunkEntry>()
|
|
214
|
+
const rawCacheKey = (x: number, z: number) => `${x},${z}`
|
|
215
|
+
|
|
216
|
+
// 1.17 path: pre-extracted section bytes + bit mask (mineflayer already
|
|
217
|
+
// did the cheap top-level packet parsing on the main thread).
|
|
218
|
+
interface ParsedV17Entry {
|
|
219
|
+
protocol: number
|
|
220
|
+
numSections: number
|
|
221
|
+
maxBitsPerBlock: number
|
|
222
|
+
chunkData: Uint8Array
|
|
223
|
+
bitMapLoHi: Uint32Array
|
|
224
|
+
biomes?: Int32Array
|
|
225
|
+
}
|
|
226
|
+
const parsedV17Cache = new Map<string, ParsedV17Entry>()
|
|
227
|
+
|
|
228
|
+
// 1.17 light arrives in a separate `update_light` packet. We parse it via
|
|
229
|
+
// WASM (`parseUpdateLightV17`) and cache per-block arrays keyed by the
|
|
230
|
+
// chunk origin — the next mesh tick of that column merges them in instead
|
|
231
|
+
// of the sky=15/block=0 fallback. May arrive before or after `map_chunk`.
|
|
232
|
+
interface UpdateLightV17Entry {
|
|
233
|
+
skyLight: Uint8Array
|
|
234
|
+
blockLight: Uint8Array
|
|
235
|
+
}
|
|
236
|
+
const updateLightV17Cache = new Map<string, UpdateLightV17Entry>()
|
|
237
|
+
|
|
238
|
+
// 1.16 path: pre-extracted section bytes + bit mask. Bit mask in 1.16
|
|
239
|
+
// is a varint that fits in i32 (only 16 sections), so we accept it as a
|
|
240
|
+
// single number and widen to a [lo,hi]=[bitMap,0] u32 pair when calling
|
|
241
|
+
// the shared `parseChunkSectionsV16V17` parser. Held separately from the
|
|
242
|
+
// v17 cache to keep the two protocol families isolated during version
|
|
243
|
+
// switches; mesh tick picks v16 vs v17 by which cache holds the entry.
|
|
244
|
+
interface ParsedV16Entry {
|
|
245
|
+
protocol: number
|
|
246
|
+
chunkData: Uint8Array
|
|
247
|
+
bitMap: number
|
|
248
|
+
biomes: Int32Array
|
|
249
|
+
}
|
|
250
|
+
const parsedV16Cache = new Map<string, ParsedV16Entry>()
|
|
251
|
+
|
|
252
|
+
// 1.16 sky/block light cache — same shape as the v17 entry, populated by
|
|
253
|
+
// `processUpdateLightV16` (which calls the shared `parseUpdateLightV17`
|
|
254
|
+
// WASM export). Separate map for the same isolation reasons as
|
|
255
|
+
// `parsedV16Cache` above.
|
|
256
|
+
const updateLightV16Cache = new Map<string, UpdateLightV17Entry>()
|
|
257
|
+
|
|
258
|
+
// Mirrors `convertChunkToWasm`'s output (same layout: x + z*16 + y*256,
|
|
259
|
+
// y outer) so it can be dropped straight into `generate_geometry`.
|
|
260
|
+
const convertRawMapChunkToWasm = (
|
|
261
|
+
raw: RawMapChunkEntry,
|
|
262
|
+
version: string
|
|
263
|
+
): ChunkConversionResult | null => {
|
|
264
|
+
if (!wasm || !(wasm as any).parseMapChunkV18Plus) return null
|
|
265
|
+
// 1.18 introduced the new chunk format; on earlier protocols the packet
|
|
266
|
+
// shape differs and our parser would throw. Fall back to the JS path.
|
|
267
|
+
if (raw.protocol < 757) return null
|
|
268
|
+
// max_bits_per_block / max_bits_per_biome match the parity-tested defaults
|
|
269
|
+
// in `wasm-mesher/src/parser_v18plus.rs` (see Stage 2 fixtures).
|
|
270
|
+
const MAX_BITS_PER_BLOCK = 8
|
|
271
|
+
const MAX_BITS_PER_BIOME = 3
|
|
272
|
+
let parsed: any
|
|
273
|
+
try {
|
|
274
|
+
parsed = (wasm as any).parseMapChunkV18Plus(
|
|
275
|
+
raw.rawPacket,
|
|
276
|
+
raw.numSections,
|
|
277
|
+
MAX_BITS_PER_BLOCK,
|
|
278
|
+
MAX_BITS_PER_BIOME,
|
|
279
|
+
raw.protocol
|
|
280
|
+
)
|
|
281
|
+
} catch (err) {
|
|
282
|
+
console.warn('[WASM Mesher] parseMapChunkV18Plus failed, falling back:', err)
|
|
283
|
+
return null
|
|
284
|
+
}
|
|
285
|
+
const meta = getBlockMeta(version)
|
|
286
|
+
const blockStates: Uint16Array = parsed.blockStates
|
|
287
|
+
let blockCount = 0
|
|
288
|
+
for (let i = 0; i < blockStates.length; i++) {
|
|
289
|
+
if (blockStates[i] !== 0) blockCount++
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
blockStates,
|
|
293
|
+
blockLight: parsed.blockLight,
|
|
294
|
+
skyLight: parsed.skyLight,
|
|
295
|
+
biomesArray: parsed.biomes,
|
|
296
|
+
invisibleBlocks: meta.invisibleBlocks,
|
|
297
|
+
transparentBlocks: meta.transparentBlocks,
|
|
298
|
+
noAoBlocks: meta.noAoBlocks,
|
|
299
|
+
cullIdenticalBlocks: meta.cullIdenticalBlocks,
|
|
300
|
+
occludingBlocks: meta.occludingBlocks,
|
|
301
|
+
blockCount,
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// 1.17 conversion: WASM now returns blockStates **and** per-block biomes
|
|
306
|
+
// (expanded from the 4×4×4 cell layout). Light comes from the paired
|
|
307
|
+
// `update_light` cache when available; otherwise we fall back to full
|
|
308
|
+
// daylight (sky=15) and no block light so geometry stays visible.
|
|
309
|
+
const convertParsedV17ToWasm = (
|
|
310
|
+
entry: ParsedV17Entry,
|
|
311
|
+
lightEntry: UpdateLightV17Entry | undefined,
|
|
312
|
+
version: string
|
|
313
|
+
): ChunkConversionResult | null => {
|
|
314
|
+
if (!wasm || !(wasm as any).parseChunkSectionsV16V17) return null
|
|
315
|
+
// Empty `Int32Array` signals "no biomes captured" — WASM falls back to
|
|
316
|
+
// `default_biome` for every block. Plains (id 1) matches the JS path.
|
|
317
|
+
const biomesCells = entry.biomes ?? new Int32Array(0)
|
|
318
|
+
const DEFAULT_BIOME = 1
|
|
319
|
+
let parsed: any
|
|
320
|
+
try {
|
|
321
|
+
parsed = (wasm as any).parseChunkSectionsV16V17(
|
|
322
|
+
entry.chunkData,
|
|
323
|
+
entry.bitMapLoHi,
|
|
324
|
+
entry.numSections,
|
|
325
|
+
entry.maxBitsPerBlock,
|
|
326
|
+
biomesCells,
|
|
327
|
+
DEFAULT_BIOME,
|
|
328
|
+
)
|
|
329
|
+
} catch (err) {
|
|
330
|
+
console.warn('[WASM Mesher] parseChunkSectionsV16V17 failed, falling back:', err)
|
|
331
|
+
return null
|
|
332
|
+
}
|
|
333
|
+
const blockStates: Uint16Array = parsed.blockStates
|
|
334
|
+
const totalBlocks = blockStates.length
|
|
335
|
+
let blockLight: Uint8Array
|
|
336
|
+
let skyLight: Uint8Array
|
|
337
|
+
if (lightEntry && lightEntry.skyLight.length === totalBlocks) {
|
|
338
|
+
skyLight = lightEntry.skyLight
|
|
339
|
+
blockLight = lightEntry.blockLight
|
|
340
|
+
} else {
|
|
341
|
+
blockLight = new Uint8Array(totalBlocks)
|
|
342
|
+
skyLight = new Uint8Array(totalBlocks)
|
|
343
|
+
skyLight.fill(15)
|
|
344
|
+
}
|
|
345
|
+
const biomesArray: Uint8Array = parsed.biomes
|
|
346
|
+
let blockCount = 0
|
|
347
|
+
for (let i = 0; i < totalBlocks; i++) {
|
|
348
|
+
if (blockStates[i] !== 0) blockCount++
|
|
349
|
+
}
|
|
350
|
+
const meta = getBlockMeta(version)
|
|
351
|
+
return {
|
|
352
|
+
blockStates,
|
|
353
|
+
blockLight,
|
|
354
|
+
skyLight,
|
|
355
|
+
biomesArray,
|
|
356
|
+
invisibleBlocks: meta.invisibleBlocks,
|
|
357
|
+
transparentBlocks: meta.transparentBlocks,
|
|
358
|
+
noAoBlocks: meta.noAoBlocks,
|
|
359
|
+
cullIdenticalBlocks: meta.cullIdenticalBlocks,
|
|
360
|
+
occludingBlocks: meta.occludingBlocks,
|
|
361
|
+
blockCount,
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// 1.16 conversion: shares the WASM parser with 1.17 (chunk-section wire
|
|
366
|
+
// format is identical between 1.16.x and 1.17). The fixed parameters
|
|
367
|
+
// (16 sections, max_bits_per_block=14) match the prismarine-chunk@1.16
|
|
368
|
+
// defaults — anything else means a non-vanilla server we don't support
|
|
369
|
+
// on the fast path, in which case we return null and fall back to the
|
|
370
|
+
// JS column-walk via `convertChunkToWasm`.
|
|
371
|
+
const convertParsedV16ToWasm = (
|
|
372
|
+
entry: ParsedV16Entry,
|
|
373
|
+
lightEntry: UpdateLightV17Entry | undefined,
|
|
374
|
+
version: string
|
|
375
|
+
): ChunkConversionResult | null => {
|
|
376
|
+
if (!wasm || !(wasm as any).parseChunkSectionsV16V17) return null
|
|
377
|
+
const NUM_SECTIONS = 16
|
|
378
|
+
const MAX_BITS_PER_BLOCK = 15
|
|
379
|
+
const DEFAULT_BIOME = 1
|
|
380
|
+
// 1.16 bit mask is a varint that fits in i32 (only 16 sections used);
|
|
381
|
+
// the WASM parser still expects [lo,hi] u32 pairs (one pair per long),
|
|
382
|
+
// so widen the single number to [bitMap, 0].
|
|
383
|
+
const bitMapLoHi = new Uint32Array([entry.bitMap >>> 0, 0])
|
|
384
|
+
const biomesCells = entry.biomes ?? new Int32Array(0)
|
|
385
|
+
let parsed: any
|
|
386
|
+
try {
|
|
387
|
+
parsed = (wasm as any).parseChunkSectionsV16V17(
|
|
388
|
+
entry.chunkData,
|
|
389
|
+
bitMapLoHi,
|
|
390
|
+
NUM_SECTIONS,
|
|
391
|
+
MAX_BITS_PER_BLOCK,
|
|
392
|
+
biomesCells,
|
|
393
|
+
DEFAULT_BIOME,
|
|
394
|
+
)
|
|
395
|
+
} catch (err) {
|
|
396
|
+
console.warn('[WASM Mesher] parseChunkSectionsV16V17 (v16) failed, falling back:', err)
|
|
397
|
+
return null
|
|
398
|
+
}
|
|
399
|
+
const blockStates: Uint16Array = parsed.blockStates
|
|
400
|
+
const totalBlocks = blockStates.length
|
|
401
|
+
let blockLight: Uint8Array
|
|
402
|
+
let skyLight: Uint8Array
|
|
403
|
+
if (lightEntry && lightEntry.skyLight.length === totalBlocks) {
|
|
404
|
+
skyLight = lightEntry.skyLight
|
|
405
|
+
blockLight = lightEntry.blockLight
|
|
406
|
+
} else {
|
|
407
|
+
blockLight = new Uint8Array(totalBlocks)
|
|
408
|
+
skyLight = new Uint8Array(totalBlocks)
|
|
409
|
+
skyLight.fill(15)
|
|
410
|
+
}
|
|
411
|
+
const biomesArray: Uint8Array = parsed.biomes
|
|
412
|
+
let blockCount = 0
|
|
413
|
+
for (let i = 0; i < totalBlocks; i++) {
|
|
414
|
+
if (blockStates[i] !== 0) blockCount++
|
|
415
|
+
}
|
|
416
|
+
const meta = getBlockMeta(version)
|
|
417
|
+
return {
|
|
418
|
+
blockStates,
|
|
419
|
+
blockLight,
|
|
420
|
+
skyLight,
|
|
421
|
+
biomesArray,
|
|
422
|
+
invisibleBlocks: meta.invisibleBlocks,
|
|
423
|
+
transparentBlocks: meta.transparentBlocks,
|
|
424
|
+
noAoBlocks: meta.noAoBlocks,
|
|
425
|
+
cullIdenticalBlocks: meta.cullIdenticalBlocks,
|
|
426
|
+
occludingBlocks: meta.occludingBlocks,
|
|
427
|
+
blockCount,
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ---------------------------------------------------------------------------
|
|
432
|
+
// Fused parse+mesh helpers (single WASM call, no typed arrays in JS heap)
|
|
433
|
+
// ---------------------------------------------------------------------------
|
|
434
|
+
|
|
435
|
+
const MAX_BITS_PER_BLOCK = 8
|
|
436
|
+
const MAX_BITS_PER_BIOME = 3
|
|
437
|
+
|
|
438
|
+
/// Fused single-column mesh for 1.18+ raw map_chunk.
|
|
439
|
+
/// Returns the GeometryOutput directly (or null on failure → fallback).
|
|
440
|
+
const meshColumnFromRawV18Plus = (
|
|
441
|
+
raw: RawMapChunkEntry,
|
|
442
|
+
x: number,
|
|
443
|
+
z: number,
|
|
444
|
+
worldMinY: number,
|
|
445
|
+
worldMaxY: number,
|
|
446
|
+
meta: ReturnType<typeof getBlockMeta>
|
|
447
|
+
): any | null => {
|
|
448
|
+
if (!wasm || !(wasm as any).generateGeometryFromMapChunkV18Plus) return null
|
|
449
|
+
if (raw.protocol < 757) return null
|
|
450
|
+
const columnHeight = worldMaxY - worldMinY
|
|
451
|
+
try {
|
|
452
|
+
return (wasm as any).generateGeometryFromMapChunkV18Plus(
|
|
453
|
+
raw.rawPacket,
|
|
454
|
+
raw.numSections,
|
|
455
|
+
MAX_BITS_PER_BLOCK,
|
|
456
|
+
MAX_BITS_PER_BIOME,
|
|
457
|
+
raw.protocol,
|
|
458
|
+
x, worldMinY, z, columnHeight,
|
|
459
|
+
worldMinY, worldMaxY,
|
|
460
|
+
worldMinY,
|
|
461
|
+
meta.invisibleBlocks,
|
|
462
|
+
meta.transparentBlocks,
|
|
463
|
+
meta.noAoBlocks,
|
|
464
|
+
meta.cullIdenticalBlocks,
|
|
465
|
+
meta.occludingBlocks,
|
|
466
|
+
config?.enableLighting !== false,
|
|
467
|
+
config?.smoothLighting !== false,
|
|
468
|
+
config?.skyLight || 15
|
|
469
|
+
)
|
|
470
|
+
} catch (err) {
|
|
471
|
+
console.warn('[WASM Mesher] generateGeometryFromMapChunkV18Plus failed, falling back:', err)
|
|
472
|
+
return null
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/// Fused single-column mesh for 1.16 / 1.17 pre-parsed chunk sections.
|
|
477
|
+
const meshColumnFromParsedV16V17 = (
|
|
478
|
+
chunkData: Uint8Array,
|
|
479
|
+
bitMapLoHi: Uint32Array,
|
|
480
|
+
numSections: number,
|
|
481
|
+
maxBitsPerBlock: number,
|
|
482
|
+
biomesCells: Int32Array | undefined,
|
|
483
|
+
defaultBiome: number,
|
|
484
|
+
skyLight: Uint8Array | null,
|
|
485
|
+
blockLight: Uint8Array | null,
|
|
486
|
+
x: number,
|
|
487
|
+
z: number,
|
|
488
|
+
worldMinY: number,
|
|
489
|
+
worldMaxY: number,
|
|
490
|
+
meta: ReturnType<typeof getBlockMeta>
|
|
491
|
+
): any | null => {
|
|
492
|
+
if (!wasm || !(wasm as any).generateGeometryFromParsedV16V17) return null
|
|
493
|
+
const columnHeight = worldMaxY - worldMinY
|
|
494
|
+
try {
|
|
495
|
+
return (wasm as any).generateGeometryFromParsedV16V17(
|
|
496
|
+
chunkData,
|
|
497
|
+
bitMapLoHi,
|
|
498
|
+
numSections,
|
|
499
|
+
maxBitsPerBlock,
|
|
500
|
+
biomesCells ?? new Int32Array(0),
|
|
501
|
+
defaultBiome,
|
|
502
|
+
skyLight ?? new Uint8Array(0),
|
|
503
|
+
blockLight ?? new Uint8Array(0),
|
|
504
|
+
x, worldMinY, z, columnHeight,
|
|
505
|
+
worldMinY, worldMaxY,
|
|
506
|
+
worldMinY,
|
|
507
|
+
meta.invisibleBlocks,
|
|
508
|
+
meta.transparentBlocks,
|
|
509
|
+
meta.noAoBlocks,
|
|
510
|
+
meta.cullIdenticalBlocks,
|
|
511
|
+
meta.occludingBlocks,
|
|
512
|
+
config?.enableLighting !== false,
|
|
513
|
+
config?.smoothLighting !== false,
|
|
514
|
+
config?.skyLight || 15
|
|
515
|
+
)
|
|
516
|
+
} catch (err) {
|
|
517
|
+
console.warn('[WASM Mesher] generateGeometryFromParsedV16V17 failed, falling back:', err)
|
|
518
|
+
return null
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const handleMessage = async (data: any) => {
|
|
523
|
+
const globalVar: any = globalThis
|
|
524
|
+
|
|
525
|
+
if (data.type === 'mcData') {
|
|
526
|
+
globalVar.mcData = data.mcData
|
|
527
|
+
globalVar.loadedData = data.mcData
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (data.config) {
|
|
531
|
+
config = { ...config, ...data.config }
|
|
532
|
+
version = config.version || version
|
|
533
|
+
world ??= new World(version)
|
|
534
|
+
world.config = { ...world.config, ...data.config }
|
|
535
|
+
globalThis.world = world
|
|
536
|
+
globalThis.Vec3 = Vec3
|
|
537
|
+
setConversionCacheLimit(config.disableConversionCache ? 0 : CONVERSION_CACHE_LIMIT)
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
switch (data.type) {
|
|
541
|
+
case 'mesherData': {
|
|
542
|
+
setMesherData(data.blockstatesModels, data.blocksAtlas, data.config.outputFormat === 'webgpu')
|
|
543
|
+
;(globalThis as any).__wasmBlockModelCache = new Map()
|
|
544
|
+
// Conservative: blockstates/version/world config may have changed.
|
|
545
|
+
clearConversionCache()
|
|
546
|
+
|
|
547
|
+
await initWasm()
|
|
548
|
+
allDataReady = true
|
|
549
|
+
workerIndex = data.workerIndex
|
|
550
|
+
break
|
|
551
|
+
}
|
|
552
|
+
case 'dirty': {
|
|
553
|
+
const loc = new Vec3(data.x, data.y, data.z)
|
|
554
|
+
setSectionDirty(loc, data.value)
|
|
555
|
+
break
|
|
556
|
+
}
|
|
557
|
+
case 'chunk': {
|
|
558
|
+
// Invalidate BEFORE replacing the column reference so a stale entry
|
|
559
|
+
// can never outlive the old chunk object.
|
|
560
|
+
invalidateConversion(data.x, data.z)
|
|
561
|
+
if (!world) break
|
|
562
|
+
world.addColumn(data.x, data.z, data.chunk)
|
|
563
|
+
if (data.customBlockModels) {
|
|
564
|
+
const chunkKey = `${data.x},${data.z}`
|
|
565
|
+
world.customBlockModels.set(chunkKey, data.customBlockModels)
|
|
566
|
+
}
|
|
567
|
+
// Safety-net heightmap push for fully empty columns. With WASM
|
|
568
|
+
// mesher as the sole path, the main thread no longer requests
|
|
569
|
+
// `getHeightmap` on chunk load — heightmaps come from
|
|
570
|
+
// `processColumnTick`. But a fully empty column (no sections, or
|
|
571
|
+
// all sections missing) never enters that path because
|
|
572
|
+
// `setSectionDirty` short-circuits when `chunk.getSection(pos)` is
|
|
573
|
+
// falsy, so `processColumnTick` never sees it. Without this push
|
|
574
|
+
// downstream consumers (e.g. `rain.ts`) would have no heightmap
|
|
575
|
+
// entry for such columns. We send a cheap sentinel-filled
|
|
576
|
+
// `Int16Array(256).fill(-32768)` — no JS heightmap scan — only when
|
|
577
|
+
// we detect zero sections; non-empty columns get their real
|
|
578
|
+
// heightmap from the next `processColumnTick`.
|
|
579
|
+
const sectionH = SECTION_HEIGHT
|
|
580
|
+
const minY = config?.worldMinY ?? 0
|
|
581
|
+
const maxY = config?.worldMaxY ?? 256
|
|
582
|
+
const column = world.getColumn(data.x, data.z)
|
|
583
|
+
let hasAnySection = false
|
|
584
|
+
for (let y = minY; y < maxY; y += sectionH) {
|
|
585
|
+
if (column?.getSection?.(new Vec3(0, y, 0))) {
|
|
586
|
+
hasAnySection = true
|
|
587
|
+
break
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
if (!hasAnySection) {
|
|
591
|
+
const emptyHeightmap = new Int16Array(256).fill(EMPTY_COLUMN_HEIGHTMAP_SENTINEL)
|
|
592
|
+
postMessage(
|
|
593
|
+
{ type: 'heightmap', key: `${data.x >> 4},${data.z >> 4}`, heightmap: emptyHeightmap },
|
|
594
|
+
[emptyHeightmap.buffer]
|
|
595
|
+
)
|
|
596
|
+
}
|
|
597
|
+
break
|
|
598
|
+
}
|
|
599
|
+
case 'unloadChunk': {
|
|
600
|
+
invalidateConversion(data.x, data.z)
|
|
601
|
+
rawMapChunkCache.delete(rawCacheKey(data.x, data.z))
|
|
602
|
+
parsedV17Cache.delete(rawCacheKey(data.x, data.z))
|
|
603
|
+
updateLightV17Cache.delete(rawCacheKey(data.x, data.z))
|
|
604
|
+
parsedV16Cache.delete(rawCacheKey(data.x, data.z))
|
|
605
|
+
updateLightV16Cache.delete(rawCacheKey(data.x, data.z))
|
|
606
|
+
if (!world) break
|
|
607
|
+
world.removeColumn(data.x, data.z)
|
|
608
|
+
world.customBlockModels.delete(`${data.x},${data.z}`)
|
|
609
|
+
if (Object.keys(world.columns).length === 0) softCleanup()
|
|
610
|
+
break
|
|
611
|
+
}
|
|
612
|
+
case 'blockUpdate': {
|
|
613
|
+
const loc = new Vec3(data.pos.x, data.pos.y, data.pos.z).floored()
|
|
614
|
+
if (data.stateId !== undefined && data.stateId !== null) {
|
|
615
|
+
world?.setBlockStateId(loc, data.stateId)
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const chunkX = Math.floor(loc.x / 16) * 16
|
|
619
|
+
const chunkZ = Math.floor(loc.z / 16) * 16
|
|
620
|
+
// In-place mutation preserves chunk identity; explicit invalidation
|
|
621
|
+
// is required so the next tick recomputes from current block state.
|
|
622
|
+
invalidateConversion(chunkX, chunkZ)
|
|
623
|
+
// Stage 3: the cached raw map_chunk no longer matches the live
|
|
624
|
+
// column after a block update — drop it so the next mesh tick walks
|
|
625
|
+
// the (now-updated) prismarine column instead.
|
|
626
|
+
rawMapChunkCache.delete(rawCacheKey(chunkX, chunkZ))
|
|
627
|
+
parsedV17Cache.delete(rawCacheKey(chunkX, chunkZ))
|
|
628
|
+
parsedV16Cache.delete(rawCacheKey(chunkX, chunkZ))
|
|
629
|
+
const chunkKey = `${chunkX},${chunkZ}`
|
|
630
|
+
if (data.customBlockModels) {
|
|
631
|
+
world?.customBlockModels.set(chunkKey, data.customBlockModels)
|
|
632
|
+
}
|
|
633
|
+
break
|
|
634
|
+
}
|
|
635
|
+
case 'setRawMapChunk': {
|
|
636
|
+
// Stage 3 (issue-15-wasm): main thread captured the raw map_chunk
|
|
637
|
+
// bytes mineflayer received and forwarded them here. We cache by
|
|
638
|
+
// (x,z); the next mesh tick will prefer this raw entry over the JS
|
|
639
|
+
// column-walk path. Invalidate the existing conversion cache entry
|
|
640
|
+
// so the next mesh tick picks the new path even if the column
|
|
641
|
+
// identity is unchanged.
|
|
642
|
+
rawMapChunkCache.set(rawCacheKey(data.x, data.z), {
|
|
643
|
+
rawPacket: data.rawPacket as Uint8Array,
|
|
644
|
+
protocol: data.protocol as number,
|
|
645
|
+
numSections: data.numSections as number,
|
|
646
|
+
})
|
|
647
|
+
invalidateConversion(data.x, data.z)
|
|
648
|
+
break
|
|
649
|
+
}
|
|
650
|
+
case 'setParsedMapChunkV17': {
|
|
651
|
+
// 1.17 path: pre-extracted section bytes + bit mask from mineflayer.
|
|
652
|
+
parsedV17Cache.set(rawCacheKey(data.x, data.z), {
|
|
653
|
+
protocol: data.protocol as number,
|
|
654
|
+
numSections: data.numSections as number,
|
|
655
|
+
maxBitsPerBlock: data.maxBitsPerBlock as number,
|
|
656
|
+
chunkData: data.chunkData as Uint8Array,
|
|
657
|
+
bitMapLoHi: data.bitMapLoHi as Uint32Array,
|
|
658
|
+
biomes: data.biomes as Int32Array | undefined,
|
|
659
|
+
})
|
|
660
|
+
invalidateConversion(data.x, data.z)
|
|
661
|
+
break
|
|
662
|
+
}
|
|
663
|
+
case 'setUpdateLightV17': {
|
|
664
|
+
// 1.17 path: parse the raw `update_light` packet via WASM. The
|
|
665
|
+
// (chunkX, chunkZ) come back inside the result — JS doesn't peek at
|
|
666
|
+
// varints. May arrive before or after the matching map_chunk; either
|
|
667
|
+
// way we cache and invalidate the column conversion so the next tick
|
|
668
|
+
// merges real lighting in.
|
|
669
|
+
// If WASM isn't ready yet, the packet is queued in
|
|
670
|
+
// `pendingUpdateLightV17` and replayed by `initWasm`.
|
|
671
|
+
processUpdateLightV17(data.rawPacket as Uint8Array, data.numSections as number)
|
|
672
|
+
break
|
|
673
|
+
}
|
|
674
|
+
case 'setParsedMapChunkV16': {
|
|
675
|
+
// 1.16 path: pre-extracted section bytes + (single-number) bit mask
|
|
676
|
+
// from mineflayer. Stored separately from v17 to keep the two
|
|
677
|
+
// protocol families isolated during version switches.
|
|
678
|
+
parsedV16Cache.set(rawCacheKey(data.x, data.z), {
|
|
679
|
+
protocol: data.protocol as number,
|
|
680
|
+
chunkData: data.chunkData as Uint8Array,
|
|
681
|
+
bitMap: data.bitMap as number,
|
|
682
|
+
biomes: data.biomes as Int32Array,
|
|
683
|
+
})
|
|
684
|
+
invalidateConversion(data.x, data.z)
|
|
685
|
+
break
|
|
686
|
+
}
|
|
687
|
+
case 'setUpdateLightV16': {
|
|
688
|
+
// 1.16 path: shares the wire format / WASM parser with 1.17 but
|
|
689
|
+
// populates a separate `updateLightV16Cache`. Same pre-WASM queueing
|
|
690
|
+
// semantics as v17.
|
|
691
|
+
processUpdateLightV16(data.rawPacket as Uint8Array)
|
|
692
|
+
break
|
|
693
|
+
}
|
|
694
|
+
case 'reset': {
|
|
695
|
+
world = undefined as any
|
|
696
|
+
dirtySections.clear()
|
|
697
|
+
requestTracker.clear()
|
|
698
|
+
clearConversionCache()
|
|
699
|
+
rawMapChunkCache.clear()
|
|
700
|
+
parsedV17Cache.clear()
|
|
701
|
+
updateLightV17Cache.clear()
|
|
702
|
+
parsedV16Cache.clear()
|
|
703
|
+
updateLightV16Cache.clear()
|
|
704
|
+
globalVar.mcData = null
|
|
705
|
+
globalVar.loadedData = null
|
|
706
|
+
allDataReady = false
|
|
707
|
+
break
|
|
708
|
+
}
|
|
709
|
+
case 'mc-web-ping': {
|
|
710
|
+
const replyWorkerIndex = typeof data.workerIndex === 'number' ? data.workerIndex : workerIndex
|
|
711
|
+
global.postMessage({
|
|
712
|
+
type: 'mc-web-pong',
|
|
713
|
+
workerIndex: replyWorkerIndex,
|
|
714
|
+
t: data.t,
|
|
715
|
+
recvAt: typeof performance !== 'undefined' ? performance.now() : undefined,
|
|
716
|
+
})
|
|
717
|
+
break
|
|
718
|
+
}
|
|
719
|
+
case 'mc-web-ping': {
|
|
720
|
+
const replyWorkerIndex = typeof data.workerIndex === 'number' ? data.workerIndex : workerIndex
|
|
721
|
+
global.postMessage({
|
|
722
|
+
type: 'mc-web-pong',
|
|
723
|
+
workerIndex: replyWorkerIndex,
|
|
724
|
+
t: data.t,
|
|
725
|
+
recvAt: typeof performance !== 'undefined' ? performance.now() : undefined,
|
|
726
|
+
})
|
|
727
|
+
break
|
|
728
|
+
}
|
|
729
|
+
case 'mc-web-ping': {
|
|
730
|
+
const replyWorkerIndex = typeof data.workerIndex === 'number' ? data.workerIndex : workerIndex
|
|
731
|
+
global.postMessage({
|
|
732
|
+
type: 'mc-web-pong',
|
|
733
|
+
workerIndex: replyWorkerIndex,
|
|
734
|
+
t: data.t,
|
|
735
|
+
recvAt: typeof performance !== 'undefined' ? performance.now() : undefined,
|
|
736
|
+
})
|
|
737
|
+
break
|
|
738
|
+
}
|
|
739
|
+
case 'getHeightmap': {
|
|
740
|
+
// Fallback path. With WASM column mesher as the sole path, the main
|
|
741
|
+
// thread should be receiving heightmaps as `'heightmap'` push messages
|
|
742
|
+
// posted by `processColumnTick`. This handler stays as a safety net for
|
|
743
|
+
// cases where the WASM heightmap could not be extracted (length mismatch
|
|
744
|
+
// or missing field) — see the `extractColumnHeightmap` warn below.
|
|
745
|
+
console.warn(`[WASM Mesher] explicit getHeightmap request for ${data.x},${data.z} — push from processColumnTick missed?`)
|
|
746
|
+
if (!world) {
|
|
747
|
+
const emptyHeightmap = new Int16Array(256).fill(EMPTY_COLUMN_HEIGHTMAP_SENTINEL)
|
|
748
|
+
postMessage({ type: 'heightmap', key: `${Math.floor(data.x / 16)},${Math.floor(data.z / 16)}`, heightmap: emptyHeightmap })
|
|
749
|
+
break
|
|
750
|
+
}
|
|
751
|
+
const { key, heightmap } = handleGetHeightmap(world, data.x, data.z)
|
|
752
|
+
postMessage({ type: 'heightmap', key, heightmap }, [heightmap.buffer])
|
|
753
|
+
|
|
754
|
+
break
|
|
755
|
+
}
|
|
756
|
+
// Note: getCustomBlockModel not implemented in WASM version
|
|
757
|
+
// as it requires World class functionality
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// eslint-disable-next-line no-restricted-globals -- TODO
|
|
762
|
+
self.onmessage = ({ data }) => {
|
|
763
|
+
if (Array.isArray(data)) {
|
|
764
|
+
// eslint-disable-next-line unicorn/no-array-for-each
|
|
765
|
+
data.forEach(handleMessage)
|
|
766
|
+
return
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
handleMessage(data)
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Section height is always 16 in column mode (the only WASM path).
|
|
773
|
+
const getSectionHeight = () => SECTION_HEIGHT
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
// 3x3 X/Z neighbor set for column meshing. Y-agnostic because full-column
|
|
777
|
+
// meshing converts the entire world Y range in one go.
|
|
778
|
+
function collectChunksForColumn(x: number, z: number) {
|
|
779
|
+
const result = [] as Array<{ x: number, z: number, chunk: any }>
|
|
780
|
+
const target = world.getColumn(x, z)
|
|
781
|
+
if (target) result.push({ x, z, chunk: target })
|
|
782
|
+
const offsets = [-16, 0, 16]
|
|
783
|
+
for (const dx of offsets) {
|
|
784
|
+
for (const dz of offsets) {
|
|
785
|
+
if (dx === 0 && dz === 0) continue
|
|
786
|
+
const nx = x + dx
|
|
787
|
+
const nz = z + dz
|
|
788
|
+
const c = world.getColumn(nx, nz)
|
|
789
|
+
if (c) result.push({ x: nx, z: nz, chunk: c })
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
return result
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function makeEmptyColumnGeometry(sx: number, sy: number, sz: number, sectionHeight: number, hadErrors: boolean): MesherGeometryOutput {
|
|
796
|
+
return {
|
|
797
|
+
sectionYNumber: (sy - (config?.worldMinY || 0)) >> 4,
|
|
798
|
+
chunkKey: worldColumnKey(sx, sz),
|
|
799
|
+
sectionStartY: sy,
|
|
800
|
+
sectionEndY: sy + sectionHeight,
|
|
801
|
+
sectionStartX: sx,
|
|
802
|
+
sectionEndX: sx + 16,
|
|
803
|
+
sectionStartZ: sz,
|
|
804
|
+
sectionEndZ: sz + 16,
|
|
805
|
+
sx: sx + 8,
|
|
806
|
+
sy: sy + 8,
|
|
807
|
+
sz: sz + 8,
|
|
808
|
+
positions: new Float32Array(0),
|
|
809
|
+
normals: new Float32Array(0),
|
|
810
|
+
colors: new Float32Array(0),
|
|
811
|
+
uvs: new Float32Array(0),
|
|
812
|
+
indices: new Uint32Array(0),
|
|
813
|
+
indicesCount: 0,
|
|
814
|
+
using32Array: false,
|
|
815
|
+
tiles: {},
|
|
816
|
+
heads: {},
|
|
817
|
+
signs: {},
|
|
818
|
+
banners: {},
|
|
819
|
+
hadErrors,
|
|
820
|
+
blocksCount: 0,
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// Full-column meshing path — the sole WASM mesh path.
|
|
825
|
+
// It groups dirty section keys by chunk column, runs one WASM call per column
|
|
826
|
+
// over the full Y range, then splits the column output back into per-section
|
|
827
|
+
// geometries. Only requested section keys are emitted back to the main thread.
|
|
828
|
+
function processColumnTick() {
|
|
829
|
+
const worldMinY = config?.worldMinY ?? 0
|
|
830
|
+
const worldMaxY = config?.worldMaxY ?? 256
|
|
831
|
+
const columnHeight = worldMaxY - worldMinY
|
|
832
|
+
const sectionHeight = SECTION_HEIGHT
|
|
833
|
+
|
|
834
|
+
// Group dirty sections by chunk column (`${x},${z}` in world block
|
|
835
|
+
// coords — the same units used by section keys). This guarantees a
|
|
836
|
+
// single WASM call per column per tick even when multiple section keys
|
|
837
|
+
// of the same column are dirty.
|
|
838
|
+
const groups = new Map<string, { x: number, z: number, sections: Array<{ key: string, x: number, y: number, z: number, count: number }> }>()
|
|
839
|
+
for (const [key, count] of dirtySections) {
|
|
840
|
+
const [sx, sy, sz] = key.split(',').map(v => parseInt(v, 10))
|
|
841
|
+
const colKey = `${sx},${sz}`
|
|
842
|
+
let g = groups.get(colKey)
|
|
843
|
+
if (!g) {
|
|
844
|
+
g = { x: sx, z: sz, sections: [] }
|
|
845
|
+
groups.set(colKey, g)
|
|
846
|
+
}
|
|
847
|
+
g.sections.push({ key, x: sx, y: sy, z: sz, count })
|
|
848
|
+
}
|
|
849
|
+
dirtySections.clear()
|
|
850
|
+
|
|
851
|
+
for (const group of groups.values()) {
|
|
852
|
+
const { x, z, sections } = group
|
|
853
|
+
const targetChunk = world.getColumn(x, z)
|
|
854
|
+
|
|
855
|
+
let exportedMap: Map<string, { exported: import('../../three/worldGeometryExport').ExportedSection, blocksCount: number }> | null = null
|
|
856
|
+
let processTime = 0
|
|
857
|
+
let prePhase = 0
|
|
858
|
+
let wasmPhase = 0
|
|
859
|
+
let postPhase = 0
|
|
860
|
+
let preTargetConvert = 0
|
|
861
|
+
let preNeighborConvert = 0
|
|
862
|
+
let preNeighborCount = 0
|
|
863
|
+
let preTypedArrayBuild = 0
|
|
864
|
+
let preOther = 0
|
|
865
|
+
let preCacheHits = 0
|
|
866
|
+
let preCacheMisses = 0
|
|
867
|
+
let hadError = false
|
|
868
|
+
// Outer-scope timestamps so we can finalize `processTime` and
|
|
869
|
+
// `postPhase` AFTER the per-section emit loop runs (the loop builds
|
|
870
|
+
// typed arrays, walks block-entity metadata, and calls postMessage —
|
|
871
|
+
// all of which are part of the worker's real cost and must be
|
|
872
|
+
// attributed to the column).
|
|
873
|
+
let columnStart = 0
|
|
874
|
+
let postStart = 0
|
|
875
|
+
|
|
876
|
+
if (targetChunk && wasm) {
|
|
877
|
+
columnStart = performance.now()
|
|
878
|
+
const start = columnStart
|
|
879
|
+
const t0 = start
|
|
880
|
+
try {
|
|
881
|
+
const chunksToUse = collectChunksForColumn(x, z)
|
|
882
|
+
const chunkCount = chunksToUse.length
|
|
883
|
+
|
|
884
|
+
let wasmResult: any
|
|
885
|
+
let t1 = 0
|
|
886
|
+
let usedFusedPath = false
|
|
887
|
+
|
|
888
|
+
// ------------------------------------------------------------------
|
|
889
|
+
// Fused fast-path: for single-column meshing (no neighbours), parse
|
|
890
|
+
// and mesh in ONE WASM call so no typed arrays leave Rust memory.
|
|
891
|
+
// Falls back to the old two-step path on any failure.
|
|
892
|
+
// ------------------------------------------------------------------
|
|
893
|
+
if (chunkCount === 1) {
|
|
894
|
+
const rawEntry = rawMapChunkCache.get(rawCacheKey(x, z))
|
|
895
|
+
const v17Entry = parsedV17Cache.get(rawCacheKey(x, z))
|
|
896
|
+
const v16Entry = parsedV16Cache.get(rawCacheKey(x, z))
|
|
897
|
+
const meta = getBlockMeta(version)
|
|
898
|
+
|
|
899
|
+
if (rawEntry) {
|
|
900
|
+
wasmResult = meshColumnFromRawV18Plus(rawEntry, x, z, worldMinY, worldMaxY, meta)
|
|
901
|
+
} else if (v17Entry) {
|
|
902
|
+
const v17Light = updateLightV17Cache.get(rawCacheKey(x, z))
|
|
903
|
+
wasmResult = meshColumnFromParsedV16V17(
|
|
904
|
+
v17Entry.chunkData, v17Entry.bitMapLoHi, v17Entry.numSections, v17Entry.maxBitsPerBlock,
|
|
905
|
+
v17Entry.biomes, 1,
|
|
906
|
+
v17Light?.skyLight ?? null, v17Light?.blockLight ?? null,
|
|
907
|
+
x, z, worldMinY, worldMaxY, meta
|
|
908
|
+
)
|
|
909
|
+
} else if (v16Entry) {
|
|
910
|
+
const v16Light = updateLightV16Cache.get(rawCacheKey(x, z))
|
|
911
|
+
const bitMapLoHi = new Uint32Array([v16Entry.bitMap >>> 0, 0])
|
|
912
|
+
wasmResult = meshColumnFromParsedV16V17(
|
|
913
|
+
v16Entry.chunkData, bitMapLoHi, 16, 15,
|
|
914
|
+
v16Entry.biomes, 1,
|
|
915
|
+
v16Light?.skyLight ?? null, v16Light?.blockLight ?? null,
|
|
916
|
+
x, z, worldMinY, worldMaxY, meta
|
|
917
|
+
)
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
if (wasmResult) {
|
|
921
|
+
usedFusedPath = true
|
|
922
|
+
t1 = performance.now()
|
|
923
|
+
wasmPhase = t1 - t0
|
|
924
|
+
preTargetConvert = wasmPhase
|
|
925
|
+
// prePhase stays 0 — no JS conversion loop ran.
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
if (!wasmResult) {
|
|
930
|
+
// --- Old two-step path (multi-column or fused fallback) ---
|
|
931
|
+
const conversions = chunksToUse.map(({ x: cx, z: cz, chunk }) => {
|
|
932
|
+
const cs = performance.now()
|
|
933
|
+
const rawEntry = rawMapChunkCache.get(rawCacheKey(cx, cz))
|
|
934
|
+
const v17Entry = parsedV17Cache.get(rawCacheKey(cx, cz))
|
|
935
|
+
const v17Light = updateLightV17Cache.get(rawCacheKey(cx, cz))
|
|
936
|
+
const v16Entry = parsedV16Cache.get(rawCacheKey(cx, cz))
|
|
937
|
+
const v16Light = updateLightV16Cache.get(rawCacheKey(cx, cz))
|
|
938
|
+
|
|
939
|
+
let conv: ChunkConversionResult | null = null
|
|
940
|
+
let hit = false
|
|
941
|
+
|
|
942
|
+
// WASM fast paths — parse is already fast (2.19× over JS), no
|
|
943
|
+
// cache needed. Bypass getOrConvertColumn so the conversion
|
|
944
|
+
// cache only holds JS-fallback results. When a WASM helper
|
|
945
|
+
// returns null (unsupported protocol, parser error, …) we MUST
|
|
946
|
+
// fall through to the JS column walk — otherwise the column
|
|
947
|
+
// would render as empty geometry.
|
|
948
|
+
if (rawEntry) {
|
|
949
|
+
conv = convertRawMapChunkToWasm(rawEntry, version)
|
|
950
|
+
} else if (v17Entry) {
|
|
951
|
+
conv = convertParsedV17ToWasm(v17Entry, v17Light, version)
|
|
952
|
+
} else if (v16Entry) {
|
|
953
|
+
conv = convertParsedV16ToWasm(v16Entry, v16Light, version)
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
if (!conv) {
|
|
957
|
+
// JS-fallback (column walk) — still cached, since this is the
|
|
958
|
+
// expensive path the conversion cache was built for.
|
|
959
|
+
const cached = getOrConvertColumn(
|
|
960
|
+
cx, cz, chunk, version, worldMinY, worldMaxY,
|
|
961
|
+
() => convertChunkToWasm(chunk, version, cx, cz, worldMinY, worldMaxY),
|
|
962
|
+
chunk
|
|
963
|
+
)
|
|
964
|
+
conv = cached.result
|
|
965
|
+
hit = cached.hit
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
const ce = performance.now()
|
|
969
|
+
if (hit) preCacheHits++
|
|
970
|
+
else preCacheMisses++
|
|
971
|
+
if (cx === x && cz === z) {
|
|
972
|
+
preTargetConvert += ce - cs
|
|
973
|
+
} else {
|
|
974
|
+
preNeighborConvert += ce - cs
|
|
975
|
+
preNeighborCount++
|
|
976
|
+
}
|
|
977
|
+
return conv
|
|
978
|
+
})
|
|
979
|
+
|
|
980
|
+
const {
|
|
981
|
+
invisibleBlocks,
|
|
982
|
+
transparentBlocks,
|
|
983
|
+
noAoBlocks,
|
|
984
|
+
cullIdenticalBlocks,
|
|
985
|
+
occludingBlocks,
|
|
986
|
+
} = conversions[0]
|
|
987
|
+
|
|
988
|
+
if (chunkCount === 1 || !(wasm as any).generate_geometry_multi) {
|
|
989
|
+
const { blockStates, blockLight, skyLight, biomesArray } = conversions[0]
|
|
990
|
+
t1 = performance.now()
|
|
991
|
+
wasmResult = wasm.generate_geometry(
|
|
992
|
+
x, worldMinY, z, columnHeight,
|
|
993
|
+
worldMinY, worldMaxY,
|
|
994
|
+
worldMinY,
|
|
995
|
+
blockStates, blockLight, skyLight, biomesArray,
|
|
996
|
+
invisibleBlocks, transparentBlocks, noAoBlocks, cullIdenticalBlocks, occludingBlocks,
|
|
997
|
+
config?.enableLighting !== false,
|
|
998
|
+
config?.smoothLighting !== false,
|
|
999
|
+
config?.skyLight || 15
|
|
1000
|
+
)
|
|
1001
|
+
} else {
|
|
1002
|
+
const tBuildStart = performance.now()
|
|
1003
|
+
const perChunkLen = conversions[0].blockStates.length
|
|
1004
|
+
const xs = new Int32Array(chunkCount)
|
|
1005
|
+
const zs = new Int32Array(chunkCount)
|
|
1006
|
+
const blockStatesAll = new Uint16Array(perChunkLen * chunkCount)
|
|
1007
|
+
const blockLightAll = new Uint8Array(perChunkLen * chunkCount)
|
|
1008
|
+
const skyLightAll = new Uint8Array(perChunkLen * chunkCount)
|
|
1009
|
+
const biomesAll = new Uint8Array(perChunkLen * chunkCount)
|
|
1010
|
+
|
|
1011
|
+
for (let i = 0; i < chunkCount; i++) {
|
|
1012
|
+
const c = conversions[i]
|
|
1013
|
+
xs[i] = chunksToUse[i].x
|
|
1014
|
+
zs[i] = chunksToUse[i].z
|
|
1015
|
+
blockStatesAll.set(c.blockStates, perChunkLen * i)
|
|
1016
|
+
blockLightAll.set(c.blockLight, perChunkLen * i)
|
|
1017
|
+
skyLightAll.set(c.skyLight, perChunkLen * i)
|
|
1018
|
+
biomesAll.set(c.biomesArray, perChunkLen * i)
|
|
1019
|
+
}
|
|
1020
|
+
preTypedArrayBuild = performance.now() - tBuildStart
|
|
1021
|
+
|
|
1022
|
+
t1 = performance.now()
|
|
1023
|
+
wasmResult = (wasm as any).generate_geometry_multi(
|
|
1024
|
+
x, worldMinY, z, columnHeight,
|
|
1025
|
+
worldMinY, worldMaxY,
|
|
1026
|
+
worldMinY,
|
|
1027
|
+
xs, zs,
|
|
1028
|
+
blockStatesAll, blockLightAll, skyLightAll, biomesAll,
|
|
1029
|
+
invisibleBlocks, transparentBlocks, noAoBlocks, cullIdenticalBlocks, occludingBlocks,
|
|
1030
|
+
config?.enableLighting !== false,
|
|
1031
|
+
config?.smoothLighting !== false,
|
|
1032
|
+
config?.skyLight || 15
|
|
1033
|
+
)
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
const t2 = performance.now()
|
|
1038
|
+
postStart = t2
|
|
1039
|
+
|
|
1040
|
+
// Split full-column output back into per-section ExportedSection
|
|
1041
|
+
// entries — only for the section keys the main thread actually
|
|
1042
|
+
// requested. Sections in the column that were NOT requested are
|
|
1043
|
+
// intentionally skipped (the request tracker would warn if we
|
|
1044
|
+
// emitted sectionFinished for them).
|
|
1045
|
+
const requestedSectionKeys = sections.map(s => ({ x: s.x, y: s.y, z: s.z }))
|
|
1046
|
+
exportedMap = splitColumnWasmOutputToSections(
|
|
1047
|
+
wasmResult,
|
|
1048
|
+
requestedSectionKeys,
|
|
1049
|
+
{ version, world, sectionHeight }
|
|
1050
|
+
)
|
|
1051
|
+
|
|
1052
|
+
// Push heightmap from the WASM column output. With column meshing as
|
|
1053
|
+
// the only WASM path, the main thread does not request heightmaps
|
|
1054
|
+
// explicitly anymore — the worker is the source of truth and pushes
|
|
1055
|
+
// a `'heightmap'` message every column tick. Key shape matches the
|
|
1056
|
+
// legacy `handleGetHeightmap` contract: `${chunkX>>4},${chunkZ>>4}`.
|
|
1057
|
+
const heightmapKey = `${x >> 4},${z >> 4}`
|
|
1058
|
+
const wasmHeightmap = extractColumnHeightmap(wasmResult)
|
|
1059
|
+
if (wasmHeightmap) {
|
|
1060
|
+
postMessage({ type: 'heightmap', key: heightmapKey, heightmap: wasmHeightmap }, [wasmHeightmap.buffer])
|
|
1061
|
+
} else {
|
|
1062
|
+
console.warn(`[WASM Mesher] heightmap extraction returned null for column ${x},${z}, falling back to JS computeHeightmap`)
|
|
1063
|
+
const fallback = handleGetHeightmap(world, x, z)
|
|
1064
|
+
postMessage({ type: 'heightmap', key: fallback.key, heightmap: fallback.heightmap }, [fallback.heightmap.buffer])
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
if (!usedFusedPath) {
|
|
1068
|
+
prePhase = t1 - t0
|
|
1069
|
+
wasmPhase = t2 - t1
|
|
1070
|
+
}
|
|
1071
|
+
preOther = Math.max(0, prePhase - (preTargetConvert + preNeighborConvert + preTypedArrayBuild))
|
|
1072
|
+
// NOTE: `postPhase` and `processTime` are finalized AFTER the
|
|
1073
|
+
// per-section emit loop below — see the `Finalize column phase
|
|
1074
|
+
// numbers` block.
|
|
1075
|
+
} catch (err) {
|
|
1076
|
+
console.error(`[WASM Mesher] Error processing column ${x},${z}:`, err)
|
|
1077
|
+
hadError = true
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// Emit geometry + sectionFinished for each requested section. Column-
|
|
1082
|
+
// level perf metrics are attributed to the first sectionFinished of
|
|
1083
|
+
// the first requested section (others get zeros) so totals don't
|
|
1084
|
+
// double-count.
|
|
1085
|
+
//
|
|
1086
|
+
// Coherent chunk appearance: column mode relies on the existing
|
|
1087
|
+
// `_renderByChunks` / `chunkFinished` contract on the main thread.
|
|
1088
|
+
// ChunkMeshManager batches sections per column and reveals them
|
|
1089
|
+
// atomically once `WorldRendererCommon` sees the last
|
|
1090
|
+
// `sectionFinished` for the column. No dedicated `columnFinished`
|
|
1091
|
+
// worker message is needed.
|
|
1092
|
+
// Pass 1: build geometry + postMessage for each requested section.
|
|
1093
|
+
// We collect finished keys here and emit `sectionFinished` only in
|
|
1094
|
+
// Pass 2 below, after `postPhase` / `processTime` have been
|
|
1095
|
+
// finalized — otherwise the totals attached to the first event
|
|
1096
|
+
// would miss the typed-array allocation, block-entity walk, and
|
|
1097
|
+
// postMessage cost of every section in this column.
|
|
1098
|
+
const finished: Array<{ key: string, count: number }> = []
|
|
1099
|
+
for (const s of sections) {
|
|
1100
|
+
const { key, x: sx, y: sy, z: sz, count } = s
|
|
1101
|
+
|
|
1102
|
+
if (exportedMap && !hadError) {
|
|
1103
|
+
const entry = exportedMap.get(key)
|
|
1104
|
+
const exported = entry?.exported
|
|
1105
|
+
const sectionBlocksCount = entry?.blocksCount ?? 0
|
|
1106
|
+
// Block entity metadata still needs a per-section world walk
|
|
1107
|
+
// (signs/heads/banners), matching the legacy per-section path.
|
|
1108
|
+
const signs: Record<string, SignMeta> = {}
|
|
1109
|
+
const heads: Record<string, HeadMeta> = {}
|
|
1110
|
+
const banners: Record<string, BannerMeta> = {}
|
|
1111
|
+
const beTarget = { signs, heads, banners }
|
|
1112
|
+
const beOpts = { disableBlockEntityTextures: world.config.disableBlockEntityTextures }
|
|
1113
|
+
const cursor = new Vec3(0, 0, 0)
|
|
1114
|
+
for (cursor.y = sy; cursor.y < sy + sectionHeight; cursor.y++) {
|
|
1115
|
+
for (cursor.z = sz; cursor.z < sz + 16; cursor.z++) {
|
|
1116
|
+
for (cursor.x = sx; cursor.x < sx + 16; cursor.x++) {
|
|
1117
|
+
const b = world.getBlock(cursor)
|
|
1118
|
+
if (!b) continue
|
|
1119
|
+
collectBlockEntityMetadata(b, cursor.x, cursor.y, cursor.z, beTarget, beOpts)
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
let geometry: MesherGeometryOutput
|
|
1125
|
+
let transferable: any[] = []
|
|
1126
|
+
if (exported && exported.geometry.indices.length > 0) {
|
|
1127
|
+
const maxIndex = exported.geometry.indices.length > 0
|
|
1128
|
+
? Math.max(...exported.geometry.indices)
|
|
1129
|
+
: 0
|
|
1130
|
+
const using32Array = maxIndex > 65535
|
|
1131
|
+
geometry = {
|
|
1132
|
+
sectionYNumber: (sy - (config?.worldMinY || 0)) >> 4,
|
|
1133
|
+
chunkKey: worldColumnKey(sx, sz),
|
|
1134
|
+
sectionStartY: sy,
|
|
1135
|
+
sectionEndY: sy + sectionHeight,
|
|
1136
|
+
sectionStartX: sx,
|
|
1137
|
+
sectionEndX: sx + 16,
|
|
1138
|
+
sectionStartZ: sz,
|
|
1139
|
+
sectionEndZ: sz + 16,
|
|
1140
|
+
sx: sx + 8,
|
|
1141
|
+
sy: sy + 8,
|
|
1142
|
+
sz: sz + 8,
|
|
1143
|
+
positions: new Float32Array(exported.geometry.positions),
|
|
1144
|
+
normals: new Float32Array(exported.geometry.normals),
|
|
1145
|
+
colors: new Float32Array(exported.geometry.colors),
|
|
1146
|
+
uvs: new Float32Array(exported.geometry.uvs),
|
|
1147
|
+
indices: using32Array
|
|
1148
|
+
? new Uint32Array(exported.geometry.indices)
|
|
1149
|
+
: new Uint16Array(exported.geometry.indices),
|
|
1150
|
+
indicesCount: exported.geometry.indices.length,
|
|
1151
|
+
using32Array,
|
|
1152
|
+
tiles: {},
|
|
1153
|
+
heads,
|
|
1154
|
+
signs,
|
|
1155
|
+
banners,
|
|
1156
|
+
hadErrors: false,
|
|
1157
|
+
// Per-section block bucket size from the column split. The
|
|
1158
|
+
// field is informational (used by `chunkMeshManager` for the
|
|
1159
|
+
// `B:` debug overlay stat) and matches the per-section path's
|
|
1160
|
+
// semantics: number of blocks that contributed faces to this
|
|
1161
|
+
// section's geometry.
|
|
1162
|
+
blocksCount: sectionBlocksCount,
|
|
1163
|
+
}
|
|
1164
|
+
transferable = [
|
|
1165
|
+
geometry.positions?.buffer,
|
|
1166
|
+
geometry.normals?.buffer,
|
|
1167
|
+
geometry.colors?.buffer,
|
|
1168
|
+
geometry.uvs?.buffer,
|
|
1169
|
+
//@ts-ignore
|
|
1170
|
+
geometry.indices?.buffer,
|
|
1171
|
+
].filter(Boolean)
|
|
1172
|
+
} else {
|
|
1173
|
+
geometry = makeEmptyColumnGeometry(sx, sy, sz, sectionHeight, false)
|
|
1174
|
+
// Still attach block entity metadata so the main thread sees
|
|
1175
|
+
// signs/heads/banners even for empty-mesh sections.
|
|
1176
|
+
geometry.signs = signs
|
|
1177
|
+
geometry.heads = heads
|
|
1178
|
+
geometry.banners = banners
|
|
1179
|
+
}
|
|
1180
|
+
postMessage({ type: 'geometry', key, geometry, workerIndex }, transferable)
|
|
1181
|
+
} else if (hadError) {
|
|
1182
|
+
const errorGeometry = makeEmptyColumnGeometry(sx, sy, sz, sectionHeight, true)
|
|
1183
|
+
postMessage({ type: 'geometry', key, geometry: errorGeometry, workerIndex })
|
|
1184
|
+
}
|
|
1185
|
+
// No targetChunk and no error: skip geometry message (mirrors
|
|
1186
|
+
// legacy behavior for sections whose chunk has been unloaded
|
|
1187
|
+
// mid-tick) but still emit sectionFinished below so the main
|
|
1188
|
+
// thread's sectionsWaiting counter unblocks.
|
|
1189
|
+
finished.push({ key, count })
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// Finalize column phase numbers — now they include split + per-
|
|
1193
|
+
// section typed-array build + block-entity walk + geometry
|
|
1194
|
+
// postMessage cost.
|
|
1195
|
+
if (columnStart > 0 && !hadError) {
|
|
1196
|
+
const tEnd = performance.now()
|
|
1197
|
+
if (postStart > 0) postPhase = tEnd - postStart
|
|
1198
|
+
processTime = tEnd - columnStart
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// Pass 2: emit sectionFinished events. Column-level perf metrics
|
|
1202
|
+
// are attributed to the first emitted sectionFinished (others get
|
|
1203
|
+
// zeros) so totals don't double-count.
|
|
1204
|
+
let attributed = false
|
|
1205
|
+
for (const { key, count } of finished) {
|
|
1206
|
+
for (let i = 0; i < count; i++) {
|
|
1207
|
+
emitSectionFinished({
|
|
1208
|
+
type: 'sectionFinished',
|
|
1209
|
+
key,
|
|
1210
|
+
workerIndex,
|
|
1211
|
+
processTime: !attributed ? processTime : 0,
|
|
1212
|
+
pre: !attributed ? prePhase : 0,
|
|
1213
|
+
wasm: !attributed ? wasmPhase : 0,
|
|
1214
|
+
post: !attributed ? postPhase : 0,
|
|
1215
|
+
preTargetConvert: !attributed ? preTargetConvert : 0,
|
|
1216
|
+
preNeighborConvert: !attributed ? preNeighborConvert : 0,
|
|
1217
|
+
preNeighborCount: !attributed ? preNeighborCount : 0,
|
|
1218
|
+
preTypedArrayBuild: !attributed ? preTypedArrayBuild : 0,
|
|
1219
|
+
preOther: !attributed ? preOther : 0,
|
|
1220
|
+
preCacheHits: !attributed ? preCacheHits : 0,
|
|
1221
|
+
preCacheMisses: !attributed ? preCacheMisses : 0,
|
|
1222
|
+
})
|
|
1223
|
+
attributed = true
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
setInterval(async () => {
|
|
1230
|
+
if (!allDataReady) return
|
|
1231
|
+
|
|
1232
|
+
// Ensure WASM is initialized
|
|
1233
|
+
if (!wasmInitialized) {
|
|
1234
|
+
await initWasm()
|
|
1235
|
+
if (!wasmInitialized) return // Still not initialized, skip this cycle
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
if (dirtySections.size === 0) return
|
|
1239
|
+
|
|
1240
|
+
try {
|
|
1241
|
+
processColumnTick()
|
|
1242
|
+
} catch (err) {
|
|
1243
|
+
console.error('[WASM Mesher] processColumnTick failed:', err)
|
|
1244
|
+
// Swallow to avoid breaking the setInterval; individual columns
|
|
1245
|
+
// already have their own try/catch.
|
|
1246
|
+
}
|
|
1247
|
+
}, 50)
|