minecraft-renderer 0.1.38 → 0.1.39
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 +20 -20
- package/dist/mesher.js.map +4 -4
- package/dist/mesherWasm.js +72 -75
- package/dist/minecraft-renderer.js +54 -54
- package/dist/minecraft-renderer.js.meta.json +1 -1
- package/dist/threeWorker.js +336 -336
- package/package.json +3 -1
- package/src/graphicsBackend/config.ts +5 -0
- package/src/lib/worldrendererCommon.ts +119 -53
- package/src/mesher/blockEntityMetadata.ts +70 -0
- package/src/mesher/computeHeightmap.ts +66 -0
- package/src/mesher/mesher.ts +13 -20
- package/src/mesher/mesherWasm.ts +432 -140
- package/src/mesher/mesherWasmConversionCache.ts +155 -0
- package/src/mesher/mesherWasmRequestTracker.ts +56 -0
- package/src/mesher/models.ts +2 -46
- package/src/mesher/shared.ts +22 -3
- package/src/mesher/test/heightmapParity.test.ts +231 -0
- package/src/mesher/test/mesherWasmConversionCache.test.ts +128 -0
- package/src/mesher/test/run/chunk.ts +2 -2
- package/src/mesher/test/splitColumnWasmOutput.test.ts +163 -0
- package/src/three/chunkMeshManager.ts +177 -1
- package/src/three/modules/cameraBobbing.ts +8 -1
- package/src/three/worldRendererThree.ts +7 -0
- package/src/wasm-lib/render-from-wasm.ts +158 -13
- package/wasm/wasm_mesher.d.ts +4 -4
- package/wasm/wasm_mesher.js +23 -66
- package/wasm/wasm_mesher_bg.wasm +0 -0
- package/wasm/wasm_mesher_bg.wasm.d.ts +9 -0
package/src/mesher/mesherWasm.ts
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
//@ts-nocheck
|
|
2
2
|
import { Vec3 } from 'vec3'
|
|
3
3
|
import { convertChunkToWasm } from '../wasm-lib/convertChunk'
|
|
4
|
-
import {
|
|
4
|
+
import { extractColumnHeightmap, splitColumnWasmOutputToSections } from '../wasm-lib/render-from-wasm'
|
|
5
5
|
import { setBlockStatesData as setMesherData } from './models'
|
|
6
|
-
import { defaultMesherConfig, type MesherGeometryOutput,
|
|
6
|
+
import { defaultMesherConfig, type MesherGeometryOutput, SECTION_HEIGHT } from './shared'
|
|
7
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'
|
|
8
18
|
|
|
9
19
|
let wasm: typeof import('../../wasm/wasm_mesher.js') | null = null
|
|
10
20
|
let wasmInitialized = false
|
|
@@ -41,6 +51,10 @@ let config = defaultMesherConfig
|
|
|
41
51
|
let version = '1.16.5'
|
|
42
52
|
let world: World // chunkKey -> chunk data
|
|
43
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()
|
|
44
58
|
let allDataReady = false
|
|
45
59
|
|
|
46
60
|
function sectionKey(x: number, y: number, z: number) {
|
|
@@ -70,6 +84,21 @@ function drainQueue(from: number, to: number) {
|
|
|
70
84
|
queuedMessages = queuedMessages.slice(to)
|
|
71
85
|
}
|
|
72
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
|
+
|
|
73
102
|
let hadDirty = false
|
|
74
103
|
function setSectionDirty(pos: Vec3, value = true) {
|
|
75
104
|
if (hadDirty) return
|
|
@@ -82,7 +111,10 @@ function setSectionDirty(pos: Vec3, value = true) {
|
|
|
82
111
|
const key = sectionKey(x, y, z)
|
|
83
112
|
if (!value) {
|
|
84
113
|
dirtySections.delete(key)
|
|
85
|
-
|
|
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 })
|
|
86
118
|
return
|
|
87
119
|
}
|
|
88
120
|
|
|
@@ -90,8 +122,11 @@ function setSectionDirty(pos: Vec3, value = true) {
|
|
|
90
122
|
const chunk = world?.getColumn(x, z)
|
|
91
123
|
if (chunk?.getSection(pos)) {
|
|
92
124
|
dirtySections.set(key, (dirtySections.get(key) || 0) + 1)
|
|
125
|
+
requestTracker.addRequest(key)
|
|
93
126
|
} else {
|
|
94
|
-
|
|
127
|
+
// Missing chunks still owe the main thread a sectionFinished response.
|
|
128
|
+
requestTracker.addRequest(key)
|
|
129
|
+
emitSectionFinished({ type: 'sectionFinished', key, workerIndex })
|
|
95
130
|
}
|
|
96
131
|
}
|
|
97
132
|
|
|
@@ -115,12 +150,15 @@ const handleMessage = async (data: any) => {
|
|
|
115
150
|
world.config = { ...world.config, ...data.config }
|
|
116
151
|
globalThis.world = world
|
|
117
152
|
globalThis.Vec3 = Vec3
|
|
153
|
+
setConversionCacheLimit(config.disableConversionCache ? 0 : CONVERSION_CACHE_LIMIT)
|
|
118
154
|
}
|
|
119
155
|
|
|
120
156
|
switch (data.type) {
|
|
121
157
|
case 'mesherData': {
|
|
122
158
|
setMesherData(data.blockstatesModels, data.blocksAtlas, data.config.outputFormat === 'webgpu')
|
|
123
159
|
;(globalThis as any).__wasmBlockModelCache = new Map()
|
|
160
|
+
// Conservative: blockstates/version/world config may have changed.
|
|
161
|
+
clearConversionCache()
|
|
124
162
|
|
|
125
163
|
await initWasm()
|
|
126
164
|
allDataReady = true
|
|
@@ -133,14 +171,50 @@ const handleMessage = async (data: any) => {
|
|
|
133
171
|
break
|
|
134
172
|
}
|
|
135
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
|
|
136
178
|
world.addColumn(data.x, data.z, data.chunk)
|
|
137
179
|
if (data.customBlockModels) {
|
|
138
180
|
const chunkKey = `${data.x},${data.z}`
|
|
139
181
|
world.customBlockModels.set(chunkKey, data.customBlockModels)
|
|
140
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
|
+
}
|
|
141
213
|
break
|
|
142
214
|
}
|
|
143
215
|
case 'unloadChunk': {
|
|
216
|
+
invalidateConversion(data.x, data.z)
|
|
217
|
+
if (!world) break
|
|
144
218
|
world.removeColumn(data.x, data.z)
|
|
145
219
|
world.customBlockModels.delete(`${data.x},${data.z}`)
|
|
146
220
|
if (Object.keys(world.columns).length === 0) softCleanup()
|
|
@@ -152,7 +226,12 @@ const handleMessage = async (data: any) => {
|
|
|
152
226
|
world?.setBlockStateId(loc, data.stateId)
|
|
153
227
|
}
|
|
154
228
|
|
|
155
|
-
const
|
|
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}`
|
|
156
235
|
if (data.customBlockModels) {
|
|
157
236
|
world?.customBlockModels.set(chunkKey, data.customBlockModels)
|
|
158
237
|
}
|
|
@@ -161,13 +240,32 @@ const handleMessage = async (data: any) => {
|
|
|
161
240
|
case 'reset': {
|
|
162
241
|
world = undefined as any
|
|
163
242
|
dirtySections.clear()
|
|
243
|
+
requestTracker.clear()
|
|
244
|
+
clearConversionCache()
|
|
164
245
|
globalVar.mcData = null
|
|
165
246
|
globalVar.loadedData = null
|
|
166
247
|
allDataReady = false
|
|
167
248
|
break
|
|
168
249
|
}
|
|
169
|
-
|
|
170
|
-
|
|
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
|
|
171
269
|
}
|
|
172
270
|
}
|
|
173
271
|
|
|
@@ -182,18 +280,16 @@ self.onmessage = ({ data }) => {
|
|
|
182
280
|
handleMessage(data)
|
|
183
281
|
}
|
|
184
282
|
|
|
185
|
-
//
|
|
186
|
-
const getSectionHeight = () =>
|
|
187
|
-
if (IS_FULL_WORLD_SECTION && config) {
|
|
188
|
-
return (config.worldMaxY || 256) - (config.worldMinY || 0)
|
|
189
|
-
}
|
|
190
|
-
return SECTION_HEIGHT
|
|
191
|
-
}
|
|
283
|
+
// Section height is always 16 in column mode (the only WASM path).
|
|
284
|
+
const getSectionHeight = () => SECTION_HEIGHT
|
|
192
285
|
|
|
193
286
|
|
|
194
|
-
|
|
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) {
|
|
195
290
|
const result = [] as Array<{ x: number, z: number, chunk: any }>
|
|
196
|
-
|
|
291
|
+
const target = world.getColumn(x, z)
|
|
292
|
+
if (target) result.push({ x, z, chunk: target })
|
|
197
293
|
const offsets = [-16, 0, 16]
|
|
198
294
|
for (const dx of offsets) {
|
|
199
295
|
for (const dz of offsets) {
|
|
@@ -204,53 +300,129 @@ function collectChunksForSection(x: number, y: number, z: number) {
|
|
|
204
300
|
if (c) result.push({ x: nx, z: nz, chunk: c })
|
|
205
301
|
}
|
|
206
302
|
}
|
|
207
|
-
return result
|
|
303
|
+
return result
|
|
208
304
|
}
|
|
209
305
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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,
|
|
217
332
|
}
|
|
333
|
+
}
|
|
218
334
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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()
|
|
222
361
|
|
|
223
|
-
for (const
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
const chunk = world.getColumn(x, z)
|
|
362
|
+
for (const group of groups.values()) {
|
|
363
|
+
const { x, z, sections } = group
|
|
364
|
+
const targetChunk = world.getColumn(x, z)
|
|
227
365
|
|
|
366
|
+
let exportedMap: Map<string, { exported: import('../three/worldGeometryExport').ExportedSection, blocksCount: number }> | null = null
|
|
228
367
|
let processTime = 0
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
|
232
391
|
try {
|
|
233
|
-
|
|
234
|
-
// If IS_FULL_WORLD_SECTION is false, only convert the specific section
|
|
235
|
-
const worldMinY = config?.worldMinY || 0
|
|
236
|
-
const worldMaxY = config?.worldMaxY || 256
|
|
237
|
-
const sectionY = IS_FULL_WORLD_SECTION ? undefined : y
|
|
238
|
-
const convertSectionHeight = IS_FULL_WORLD_SECTION ? undefined : sectionHeight
|
|
239
|
-
|
|
240
|
-
// Run WASM mesher for this section
|
|
241
|
-
const chunksToUse = collectChunksForSection(x, y, z)
|
|
392
|
+
const chunksToUse = collectChunksForColumn(x, z)
|
|
242
393
|
const chunkCount = chunksToUse.length
|
|
243
394
|
|
|
244
|
-
const conversions = chunksToUse.map(({ x: cx, z: cz, chunk }) =>
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
+
})
|
|
254
426
|
|
|
255
427
|
const {
|
|
256
428
|
invisibleBlocks,
|
|
@@ -260,12 +432,18 @@ setInterval(async () => {
|
|
|
260
432
|
occludingBlocks,
|
|
261
433
|
} = conversions[0]
|
|
262
434
|
|
|
263
|
-
let wasmResult
|
|
435
|
+
let wasmResult: any
|
|
436
|
+
let t1: number
|
|
264
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.
|
|
265
441
|
const { blockStates, blockLight, skyLight, biomesArray } = conversions[0]
|
|
442
|
+
t1 = performance.now()
|
|
266
443
|
wasmResult = wasm.generate_geometry(
|
|
267
|
-
x,
|
|
444
|
+
x, worldMinY, z, columnHeight,
|
|
268
445
|
worldMinY, worldMaxY,
|
|
446
|
+
worldMinY,
|
|
269
447
|
blockStates, blockLight, skyLight, biomesArray,
|
|
270
448
|
invisibleBlocks, transparentBlocks, noAoBlocks, cullIdenticalBlocks, occludingBlocks,
|
|
271
449
|
config?.enableLighting !== false,
|
|
@@ -273,6 +451,7 @@ setInterval(async () => {
|
|
|
273
451
|
config?.skyLight || 15
|
|
274
452
|
)
|
|
275
453
|
} else {
|
|
454
|
+
const tBuildStart = performance.now()
|
|
276
455
|
const perChunkLen = conversions[0].blockStates.length
|
|
277
456
|
const xs = new Int32Array(chunkCount)
|
|
278
457
|
const zs = new Int32Array(chunkCount)
|
|
@@ -290,10 +469,13 @@ setInterval(async () => {
|
|
|
290
469
|
skyLightAll.set(c.skyLight, perChunkLen * i)
|
|
291
470
|
biomesAll.set(c.biomesArray, perChunkLen * i)
|
|
292
471
|
}
|
|
472
|
+
preTypedArrayBuild = performance.now() - tBuildStart
|
|
293
473
|
|
|
474
|
+
t1 = performance.now()
|
|
294
475
|
wasmResult = (wasm as any).generate_geometry_multi(
|
|
295
|
-
x,
|
|
476
|
+
x, worldMinY, z, columnHeight,
|
|
296
477
|
worldMinY, worldMaxY,
|
|
478
|
+
worldMinY,
|
|
297
479
|
xs, zs,
|
|
298
480
|
blockStatesAll, blockLightAll, skyLightAll, biomesAll,
|
|
299
481
|
invisibleBlocks, transparentBlocks, noAoBlocks, cullIdenticalBlocks, occludingBlocks,
|
|
@@ -303,102 +485,212 @@ setInterval(async () => {
|
|
|
303
485
|
)
|
|
304
486
|
}
|
|
305
487
|
|
|
488
|
+
const t2 = performance.now()
|
|
489
|
+
postStart = t2
|
|
306
490
|
|
|
307
|
-
//
|
|
308
|
-
|
|
309
|
-
|
|
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(
|
|
310
498
|
wasmResult,
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
{ x: x + 8, y: y + 8, z: z + 8 },
|
|
314
|
-
world
|
|
499
|
+
requestedSectionKeys,
|
|
500
|
+
{ version, world, sectionHeight }
|
|
315
501
|
)
|
|
316
502
|
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
sectionStartZ: z,
|
|
331
|
-
sectionEndZ: z + 16,
|
|
332
|
-
sx: x + 8,
|
|
333
|
-
sy: y + 8,
|
|
334
|
-
sz: z + 8,
|
|
335
|
-
positions: new Float32Array(exportedSection.geometry.positions),
|
|
336
|
-
normals: new Float32Array(exportedSection.geometry.normals),
|
|
337
|
-
colors: new Float32Array(exportedSection.geometry.colors),
|
|
338
|
-
uvs: new Float32Array(exportedSection.geometry.uvs),
|
|
339
|
-
indices: using32Array
|
|
340
|
-
? new Uint32Array(exportedSection.geometry.indices)
|
|
341
|
-
: new Uint16Array(exportedSection.geometry.indices),
|
|
342
|
-
indicesCount: exportedSection.geometry.indices.length,
|
|
343
|
-
using32Array,
|
|
344
|
-
tiles: {},
|
|
345
|
-
heads: {},
|
|
346
|
-
signs: {},
|
|
347
|
-
banners: {},
|
|
348
|
-
hadErrors: false,
|
|
349
|
-
blocksCount: wasmResult.block_count,
|
|
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])
|
|
350
516
|
}
|
|
351
517
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
geometry.indices?.buffer,
|
|
359
|
-
].filter(Boolean)
|
|
360
|
-
|
|
361
|
-
postMessage({ type: 'geometry', key, geometry, workerIndex }, transferable)
|
|
362
|
-
processTime = performance.now() - start
|
|
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.
|
|
363
524
|
} catch (err) {
|
|
364
|
-
console.error(`[WASM Mesher] Error processing
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
+
}
|
|
391
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)
|
|
392
632
|
postMessage({ type: 'geometry', key, geometry: errorGeometry, workerIndex })
|
|
393
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 })
|
|
394
639
|
}
|
|
395
640
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
|
401
648
|
}
|
|
402
|
-
|
|
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.
|
|
403
695
|
}
|
|
404
696
|
}, 50)
|