minecraft-renderer 0.1.83 → 0.1.85
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.map +2 -2
- package/dist/mesherWasm.js +214 -214
- package/dist/minecraft-renderer.js +14 -14
- package/dist/minecraft-renderer.js.meta.json +1 -1
- package/dist/threeWorker.js +1 -1
- package/package.json +1 -1
- package/src/lib/worldrendererCommon.ts +7 -0
- package/src/mesher-shared/shared.ts +5 -0
- package/src/wasm-mesher/bridge/render-from-wasm.ts +60 -27
- package/src/wasm-mesher/tests/blendEmissionOrder.test.ts +123 -0
- package/src/wasm-mesher/tests/crossColumnCulling.test.ts +205 -0
- package/src/wasm-mesher/tests/mesherWasmChunkBuffer.test.ts +24 -0
- package/src/wasm-mesher/tests/mesherWasmNeighborhood.test.ts +91 -0
- package/src/wasm-mesher/worker/mesherWasm.ts +194 -67
- package/src/wasm-mesher/worker/mesherWasmChunkBuffer.ts +28 -0
- package/src/wasm-mesher/worker/mesherWasmNeighborhood.ts +125 -0
|
@@ -10,6 +10,16 @@ import { collectBlockEntityMetadata, type SignMeta, type HeadMeta, type BannerMe
|
|
|
10
10
|
import { SectionRequestTracker } from './mesherWasmRequestTracker'
|
|
11
11
|
import { sectionYsForLightColumnDirty } from './mesherWasmLightDirty'
|
|
12
12
|
import { CONVERSION_CACHE_LIMIT, clearConversionCache, getOrConvertColumn, invalidateConversion, setConversionCacheLimit } from './mesherWasmConversionCache'
|
|
13
|
+
import { PendingChunkBuffer } from './mesherWasmChunkBuffer'
|
|
14
|
+
import {
|
|
15
|
+
PendingNeighborHealTracker,
|
|
16
|
+
SIDE_NEIGHBOR_OFFSETS,
|
|
17
|
+
collectChunksForColumnUnion,
|
|
18
|
+
columnDataAvailable,
|
|
19
|
+
countParsedCache3x3,
|
|
20
|
+
countWorldColumns3x3,
|
|
21
|
+
type PacketCaches
|
|
22
|
+
} from './mesherWasmNeighborhood'
|
|
13
23
|
|
|
14
24
|
let wasm: typeof import('../runtime-build/wasm_mesher.js') | null = null
|
|
15
25
|
let wasmInitialized = false
|
|
@@ -44,6 +54,9 @@ function processUpdateLightV17(rawPacket: Uint8Array, numSections: number): void
|
|
|
44
54
|
})
|
|
45
55
|
invalidateConversion(x, z)
|
|
46
56
|
dirtyColumnSectionsForLightUpdate(x, z)
|
|
57
|
+
const hadColumn = !!world?.getColumn(x, z)
|
|
58
|
+
syncV17LightToColumn(x, z)
|
|
59
|
+
if (!hadColumn) pendingLightDirtyColumns.add(rawCacheKey(x, z))
|
|
47
60
|
} catch (err) {
|
|
48
61
|
console.warn('[WASM Mesher] parseUpdateLightV17 failed:', err)
|
|
49
62
|
}
|
|
@@ -248,6 +261,44 @@ interface UpdateLightV17Entry {
|
|
|
248
261
|
blockLight: Uint8Array
|
|
249
262
|
}
|
|
250
263
|
const updateLightV17Cache = new Map<string, UpdateLightV17Entry>()
|
|
264
|
+
/** Columns that received `update_light` before the worker column existed. */
|
|
265
|
+
const pendingLightDirtyColumns = new Set<string>()
|
|
266
|
+
const _syncLightPos = new Vec3(0, 0, 0)
|
|
267
|
+
|
|
268
|
+
function resolveUpdateLightV17Entry(x: number, z: number, chunk: any | undefined, worldMinY: number, worldMaxY: number): UpdateLightV17Entry | undefined {
|
|
269
|
+
const cached = updateLightV17Cache.get(rawCacheKey(x, z))
|
|
270
|
+
if (cached?.blockLight?.length) return cached
|
|
271
|
+
if (!chunk) return cached
|
|
272
|
+
const { result } = getOrConvertColumn(x, z, chunk, version, worldMinY, worldMaxY, () => convertChunkToWasm(chunk, version, x, z, worldMinY, worldMaxY), chunk)
|
|
273
|
+
return { blockLight: result.blockLight, skyLight: result.skyLight }
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Write cached 1.17 `update_light` arrays into the worker prismarine column. */
|
|
277
|
+
function syncV17LightToColumn(x: number, z: number): boolean {
|
|
278
|
+
if (!world) return false
|
|
279
|
+
const col = world.getColumn(x, z)
|
|
280
|
+
const entry = updateLightV17Cache.get(rawCacheKey(x, z))
|
|
281
|
+
if (!col || !entry) return false
|
|
282
|
+
|
|
283
|
+
const CHUNK_SIZE = 16
|
|
284
|
+
const minY = config?.worldMinY ?? 0
|
|
285
|
+
const maxY = config?.worldMaxY ?? 256
|
|
286
|
+
const { blockLight, skyLight } = entry
|
|
287
|
+
|
|
288
|
+
for (let y = minY; y < maxY; y++) {
|
|
289
|
+
for (let lz = 0; lz < CHUNK_SIZE; lz++) {
|
|
290
|
+
for (let lx = 0; lx < CHUNK_SIZE; lx++) {
|
|
291
|
+
const idx = lx + lz * CHUNK_SIZE + (y - minY) * CHUNK_SIZE * CHUNK_SIZE
|
|
292
|
+
if (idx >= blockLight.length) continue
|
|
293
|
+
_syncLightPos.set(lx, y, lz)
|
|
294
|
+
col.setBlockLight(_syncLightPos, blockLight[idx]!)
|
|
295
|
+
col.setSkyLight(_syncLightPos, skyLight[idx]!)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return true
|
|
301
|
+
}
|
|
251
302
|
|
|
252
303
|
// 1.16 path: pre-extracted section bytes + bit mask. Bit mask in 1.16
|
|
253
304
|
// is a varint that fits in i32 (only 16 sections), so we accept it as a
|
|
@@ -263,6 +314,77 @@ interface ParsedV16Entry {
|
|
|
263
314
|
}
|
|
264
315
|
const parsedV16Cache = new Map<string, ParsedV16Entry>()
|
|
265
316
|
|
|
317
|
+
const pendingChunks = new PendingChunkBuffer()
|
|
318
|
+
const pendingNeighborHeal = new PendingNeighborHealTracker()
|
|
319
|
+
let blindMeshWarnCount = 0
|
|
320
|
+
const BLIND_MESH_WARN_LIMIT = 5
|
|
321
|
+
|
|
322
|
+
const packetCaches = (): PacketCaches => ({
|
|
323
|
+
raw: rawMapChunkCache,
|
|
324
|
+
v17: parsedV17Cache,
|
|
325
|
+
v16: parsedV16Cache
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
function notifyColumnsAwaitingNeighbor(neighborX: number, neighborZ: number) {
|
|
329
|
+
for (const col of pendingNeighborHeal.takeColumnsAwaitingNeighbor(neighborX, neighborZ)) {
|
|
330
|
+
postMessage({ type: 'neighborDataArrived', x: col.x, z: col.z, workerIndex })
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function onColumnDataArrived(x: number, z: number) {
|
|
335
|
+
notifyColumnsAwaitingNeighbor(x, z)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function warnGenuinelyBlindMesh(columnX: number, columnZ: number, missingSides: number) {
|
|
339
|
+
if (missingSides <= 0) return
|
|
340
|
+
blindMeshWarnCount++
|
|
341
|
+
if (blindMeshWarnCount <= BLIND_MESH_WARN_LIMIT) {
|
|
342
|
+
console.warn(`[mesher] column ${columnX},${columnZ} meshed with ${missingSides}/4 side neighbors missing from both pipelines — genuinely blind mesh`)
|
|
343
|
+
} else if (blindMeshWarnCount === BLIND_MESH_WARN_LIMIT + 1) {
|
|
344
|
+
console.warn(`[mesher] genuinely blind mesh warnings suppressed after ${BLIND_MESH_WARN_LIMIT} occurrences`)
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function processChunkMessage(data: { x: number; z: number; chunk: any; customBlockModels?: any }) {
|
|
349
|
+
world.addColumn(data.x, data.z, data.chunk)
|
|
350
|
+
// --- Light sync (folded in from main): apply any light that arrived before
|
|
351
|
+
// this column's data. Doing it here (not inline in the handler) means
|
|
352
|
+
// chunks replayed from pendingChunks via drainPendingChunks() also get
|
|
353
|
+
// their buffered light applied.
|
|
354
|
+
syncV17LightToColumn(data.x, data.z)
|
|
355
|
+
const lightKey = rawCacheKey(data.x, data.z)
|
|
356
|
+
if (pendingLightDirtyColumns.delete(lightKey) || updateLightV17Cache.has(lightKey)) {
|
|
357
|
+
dirtyColumnSectionsForLightUpdate(data.x, data.z)
|
|
358
|
+
}
|
|
359
|
+
if (data.customBlockModels) {
|
|
360
|
+
const chunkKey = `${data.x},${data.z}`
|
|
361
|
+
world.customBlockModels.set(chunkKey, data.customBlockModels)
|
|
362
|
+
}
|
|
363
|
+
const sectionH = SECTION_HEIGHT
|
|
364
|
+
const minY = config?.worldMinY ?? 0
|
|
365
|
+
const maxY = config?.worldMaxY ?? 256
|
|
366
|
+
const column = world.getColumn(data.x, data.z)
|
|
367
|
+
let hasAnySection = false
|
|
368
|
+
for (let y = minY; y < maxY; y += sectionH) {
|
|
369
|
+
if (column?.getSection?.(new Vec3(0, y, 0))) {
|
|
370
|
+
hasAnySection = true
|
|
371
|
+
break
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (!hasAnySection) {
|
|
375
|
+
const emptyHeightmap = new Int16Array(256).fill(EMPTY_COLUMN_HEIGHTMAP_SENTINEL)
|
|
376
|
+
postMessage({ type: 'heightmap', key: `${data.x >> 4},${data.z >> 4}`, heightmap: emptyHeightmap }, [emptyHeightmap.buffer])
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function drainPendingChunks() {
|
|
381
|
+
if (!world) return
|
|
382
|
+
pendingChunks.drain(msg => {
|
|
383
|
+
processChunkMessage(msg)
|
|
384
|
+
onColumnDataArrived(msg.x, msg.z)
|
|
385
|
+
})
|
|
386
|
+
}
|
|
387
|
+
|
|
266
388
|
// 1.16 sky/block light cache — same shape as the v17 entry, populated by
|
|
267
389
|
// `processUpdateLightV16` (which calls the shared `parseUpdateLightV17`
|
|
268
390
|
// WASM export). Separate map for the same isolation reasons as
|
|
@@ -625,7 +747,7 @@ const meshMultiColumnsFromParsedV16V17 = (
|
|
|
625
747
|
bitMapLoHi[i * 2] = entry.bitMapLoHi[0]
|
|
626
748
|
bitMapLoHi[i * 2 + 1] = entry.bitMapLoHi[1]
|
|
627
749
|
if (i === 0) maxBitsPerBlock = entry.maxBitsPerBlock
|
|
628
|
-
const light =
|
|
750
|
+
const light = resolveUpdateLightV17Entry(chunksToUse[i].x, chunksToUse[i].z, chunksToUse[i].chunk, worldMinY, worldMaxY)
|
|
629
751
|
skyLightList.push(light?.skyLight ?? new Uint8Array(0))
|
|
630
752
|
blockLightList.push(light?.blockLight ?? new Uint8Array(0))
|
|
631
753
|
} else {
|
|
@@ -707,6 +829,7 @@ const handleMessage = async (data: any) => {
|
|
|
707
829
|
await initWasm()
|
|
708
830
|
allDataReady = true
|
|
709
831
|
workerIndex = data.workerIndex
|
|
832
|
+
drainPendingChunks()
|
|
710
833
|
break
|
|
711
834
|
}
|
|
712
835
|
case 'dirty': {
|
|
@@ -718,39 +841,17 @@ const handleMessage = async (data: any) => {
|
|
|
718
841
|
// Invalidate BEFORE replacing the column reference so a stale entry
|
|
719
842
|
// can never outlive the old chunk object.
|
|
720
843
|
invalidateConversion(data.x, data.z)
|
|
721
|
-
if (!world)
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
// `getHeightmap` on chunk load — heightmaps come from
|
|
730
|
-
// `processColumnTick`. But a fully empty column (no sections, or
|
|
731
|
-
// all sections missing) never enters that path because
|
|
732
|
-
// `setSectionDirty` short-circuits when `chunk.getSection(pos)` is
|
|
733
|
-
// falsy, so `processColumnTick` never sees it. Without this push
|
|
734
|
-
// downstream consumers (e.g. `rain.ts`) would have no heightmap
|
|
735
|
-
// entry for such columns. We send a cheap sentinel-filled
|
|
736
|
-
// `Int16Array(256).fill(-32768)` — no JS heightmap scan — only when
|
|
737
|
-
// we detect zero sections; non-empty columns get their real
|
|
738
|
-
// heightmap from the next `processColumnTick`.
|
|
739
|
-
const sectionH = SECTION_HEIGHT
|
|
740
|
-
const minY = config?.worldMinY ?? 0
|
|
741
|
-
const maxY = config?.worldMaxY ?? 256
|
|
742
|
-
const column = world.getColumn(data.x, data.z)
|
|
743
|
-
let hasAnySection = false
|
|
744
|
-
for (let y = minY; y < maxY; y += sectionH) {
|
|
745
|
-
if (column?.getSection?.(new Vec3(0, y, 0))) {
|
|
746
|
-
hasAnySection = true
|
|
747
|
-
break
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
if (!hasAnySection) {
|
|
751
|
-
const emptyHeightmap = new Int16Array(256).fill(EMPTY_COLUMN_HEIGHTMAP_SENTINEL)
|
|
752
|
-
postMessage({ type: 'heightmap', key: `${data.x >> 4},${data.z >> 4}`, heightmap: emptyHeightmap }, [emptyHeightmap.buffer])
|
|
844
|
+
if (!world) {
|
|
845
|
+
pendingChunks.enqueue({
|
|
846
|
+
x: data.x,
|
|
847
|
+
z: data.z,
|
|
848
|
+
chunk: data.chunk,
|
|
849
|
+
customBlockModels: data.customBlockModels
|
|
850
|
+
})
|
|
851
|
+
break
|
|
753
852
|
}
|
|
853
|
+
processChunkMessage(data)
|
|
854
|
+
onColumnDataArrived(data.x, data.z)
|
|
754
855
|
break
|
|
755
856
|
}
|
|
756
857
|
case 'unloadChunk': {
|
|
@@ -760,9 +861,11 @@ const handleMessage = async (data: any) => {
|
|
|
760
861
|
updateLightV17Cache.delete(rawCacheKey(data.x, data.z))
|
|
761
862
|
parsedV16Cache.delete(rawCacheKey(data.x, data.z))
|
|
762
863
|
updateLightV16Cache.delete(rawCacheKey(data.x, data.z))
|
|
864
|
+
pendingLightDirtyColumns.delete(rawCacheKey(data.x, data.z))
|
|
763
865
|
if (!world) break
|
|
764
866
|
world.removeColumn(data.x, data.z)
|
|
765
867
|
world.customBlockModels.delete(`${data.x},${data.z}`)
|
|
868
|
+
pendingNeighborHeal.clearColumn(data.x, data.z)
|
|
766
869
|
requestTracker.clearColumn(data.x, data.z)
|
|
767
870
|
for (const key of [...dirtySections.keys()]) {
|
|
768
871
|
const [sx, , sz] = key.split(',').map(Number)
|
|
@@ -809,6 +912,7 @@ const handleMessage = async (data: any) => {
|
|
|
809
912
|
numSections: data.numSections as number
|
|
810
913
|
})
|
|
811
914
|
invalidateConversion(data.x, data.z)
|
|
915
|
+
onColumnDataArrived(data.x, data.z)
|
|
812
916
|
break
|
|
813
917
|
}
|
|
814
918
|
case 'setParsedMapChunkV17': {
|
|
@@ -822,6 +926,7 @@ const handleMessage = async (data: any) => {
|
|
|
822
926
|
biomes: data.biomes as Int32Array | undefined
|
|
823
927
|
})
|
|
824
928
|
invalidateConversion(data.x, data.z)
|
|
929
|
+
onColumnDataArrived(data.x, data.z)
|
|
825
930
|
break
|
|
826
931
|
}
|
|
827
932
|
case 'setUpdateLightV17': {
|
|
@@ -846,6 +951,7 @@ const handleMessage = async (data: any) => {
|
|
|
846
951
|
biomes: data.biomes as Int32Array
|
|
847
952
|
})
|
|
848
953
|
invalidateConversion(data.x, data.z)
|
|
954
|
+
onColumnDataArrived(data.x, data.z)
|
|
849
955
|
break
|
|
850
956
|
}
|
|
851
957
|
case 'setUpdateLightV16': {
|
|
@@ -863,8 +969,12 @@ const handleMessage = async (data: any) => {
|
|
|
863
969
|
rawMapChunkCache.clear()
|
|
864
970
|
parsedV17Cache.clear()
|
|
865
971
|
updateLightV17Cache.clear()
|
|
972
|
+
pendingLightDirtyColumns.clear()
|
|
866
973
|
parsedV16Cache.clear()
|
|
867
974
|
updateLightV16Cache.clear()
|
|
975
|
+
pendingChunks.clear()
|
|
976
|
+
pendingNeighborHeal.clear()
|
|
977
|
+
blindMeshWarnCount = 0
|
|
868
978
|
globalVar.mcData = null
|
|
869
979
|
globalVar.loadedData = null
|
|
870
980
|
allDataReady = false
|
|
@@ -936,23 +1046,8 @@ self.onmessage = ({ data }) => {
|
|
|
936
1046
|
// Section height is always 16 in column mode (the only WASM path).
|
|
937
1047
|
const getSectionHeight = () => SECTION_HEIGHT
|
|
938
1048
|
|
|
939
|
-
// 3x3 X/Z neighbor set for column meshing. Y-agnostic because full-column
|
|
940
|
-
// meshing converts the entire world Y range in one go.
|
|
941
1049
|
function collectChunksForColumn(x: number, z: number) {
|
|
942
|
-
|
|
943
|
-
const target = world.getColumn(x, z)
|
|
944
|
-
if (target) result.push({ x, z, chunk: target })
|
|
945
|
-
const offsets = [-16, 0, 16]
|
|
946
|
-
for (const dx of offsets) {
|
|
947
|
-
for (const dz of offsets) {
|
|
948
|
-
if (dx === 0 && dz === 0) continue
|
|
949
|
-
const nx = x + dx
|
|
950
|
-
const nz = z + dz
|
|
951
|
-
const c = world.getColumn(nx, nz)
|
|
952
|
-
if (c) result.push({ x: nx, z: nz, chunk: c })
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
return result
|
|
1050
|
+
return collectChunksForColumnUnion(x, z, (nx, nz) => world.getColumn(nx, nz), packetCaches())
|
|
956
1051
|
}
|
|
957
1052
|
|
|
958
1053
|
function makeEmptyColumnGeometry(sx: number, sy: number, sz: number, sectionHeight: number, hadErrors: boolean): MesherGeometryOutput {
|
|
@@ -1031,6 +1126,9 @@ function processColumnTick() {
|
|
|
1031
1126
|
let preCacheMisses = 0
|
|
1032
1127
|
let hadError = false
|
|
1033
1128
|
let columnMeshPath = 'none'
|
|
1129
|
+
let chunkCount = 0
|
|
1130
|
+
let worldColumns3x3 = 0
|
|
1131
|
+
let parsedCache3x3 = 0
|
|
1034
1132
|
// Outer-scope timestamps so we can finalize `processTime` and
|
|
1035
1133
|
// `postPhase` AFTER the per-section emit loop runs (the loop builds
|
|
1036
1134
|
// typed arrays, walks block-entity metadata, and calls postMessage —
|
|
@@ -1045,7 +1143,21 @@ function processColumnTick() {
|
|
|
1045
1143
|
const t0 = start
|
|
1046
1144
|
try {
|
|
1047
1145
|
const chunksToUse = collectChunksForColumn(x, z)
|
|
1048
|
-
|
|
1146
|
+
chunkCount = chunksToUse.length
|
|
1147
|
+
const caches = packetCaches()
|
|
1148
|
+
worldColumns3x3 = countWorldColumns3x3(x, z, (nx, nz) => world.getColumn(nx, nz))
|
|
1149
|
+
parsedCache3x3 = countParsedCache3x3(x, z, caches)
|
|
1150
|
+
|
|
1151
|
+
let missingSideCount = 0
|
|
1152
|
+
for (const [dx, dz] of SIDE_NEIGHBOR_OFFSETS) {
|
|
1153
|
+
const nx = x + dx
|
|
1154
|
+
const nz = z + dz
|
|
1155
|
+
if (!columnDataAvailable(nx, nz, (cx, cz) => world.getColumn(cx, cz), caches)) {
|
|
1156
|
+
pendingNeighborHeal.recordMissingSide(x, z, nx, nz)
|
|
1157
|
+
missingSideCount++
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
warnGenuinelyBlindMesh(x, z, missingSideCount)
|
|
1049
1161
|
|
|
1050
1162
|
let wasmResult: any
|
|
1051
1163
|
let t1 = 0
|
|
@@ -1068,7 +1180,7 @@ function processColumnTick() {
|
|
|
1068
1180
|
wasmResult = meshColumnFromRawV18Plus(rawEntry, x, z, worldMinY, worldMaxY, meta)
|
|
1069
1181
|
if (wasmResult) columnMeshPath = 'v18_fused'
|
|
1070
1182
|
} else if (v17Entry) {
|
|
1071
|
-
const v17Light =
|
|
1183
|
+
const v17Light = resolveUpdateLightV17Entry(x, z, targetChunk, worldMinY, worldMaxY)
|
|
1072
1184
|
wasmResult = meshColumnFromParsedV16V17(
|
|
1073
1185
|
v17Entry.chunkData,
|
|
1074
1186
|
v17Entry.bitMapLoHi,
|
|
@@ -1135,11 +1247,13 @@ function processColumnTick() {
|
|
|
1135
1247
|
|
|
1136
1248
|
if (!wasmResult) {
|
|
1137
1249
|
// --- Old two-step path (multi-column or fused fallback) ---
|
|
1138
|
-
const conversions
|
|
1250
|
+
const conversions: ChunkConversionResult[] = []
|
|
1251
|
+
const meshChunks: typeof chunksToUse = []
|
|
1252
|
+
for (const { x: cx, z: cz, chunk } of chunksToUse) {
|
|
1139
1253
|
const cs = performance.now()
|
|
1140
1254
|
const rawEntry = rawMapChunkCache.get(rawCacheKey(cx, cz))
|
|
1141
1255
|
const v17Entry = parsedV17Cache.get(rawCacheKey(cx, cz))
|
|
1142
|
-
const v17Light =
|
|
1256
|
+
const v17Light = resolveUpdateLightV17Entry(cx, cz, chunk, worldMinY, worldMaxY)
|
|
1143
1257
|
const v16Entry = parsedV16Cache.get(rawCacheKey(cx, cz))
|
|
1144
1258
|
const v16Light = updateLightV16Cache.get(rawCacheKey(cx, cz))
|
|
1145
1259
|
|
|
@@ -1163,6 +1277,9 @@ function processColumnTick() {
|
|
|
1163
1277
|
if (!conv) {
|
|
1164
1278
|
// JS-fallback (column walk) — still cached, since this is the
|
|
1165
1279
|
// expensive path the conversion cache was built for.
|
|
1280
|
+
// Cache-only neighbors (pipeline B without world.columns) are
|
|
1281
|
+
// skipped — fused multi should have handled them already.
|
|
1282
|
+
if (!chunk) continue
|
|
1166
1283
|
const cached = getOrConvertColumn(
|
|
1167
1284
|
cx,
|
|
1168
1285
|
cz,
|
|
@@ -1186,12 +1303,18 @@ function processColumnTick() {
|
|
|
1186
1303
|
preNeighborConvert += ce - cs
|
|
1187
1304
|
preNeighborCount++
|
|
1188
1305
|
}
|
|
1189
|
-
|
|
1190
|
-
|
|
1306
|
+
conversions.push(conv)
|
|
1307
|
+
meshChunks.push({ x: cx, z: cz, chunk })
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
const twoStepChunkCount = conversions.length
|
|
1311
|
+
if (twoStepChunkCount === 0) {
|
|
1312
|
+
throw new Error(`[WASM Mesher] no convertible columns for ${x},${z}`)
|
|
1313
|
+
}
|
|
1191
1314
|
|
|
1192
1315
|
const { invisibleBlocks, transparentBlocks, noAoBlocks, cullIdenticalBlocks, occludingBlocks } = conversions[0]
|
|
1193
1316
|
|
|
1194
|
-
if (
|
|
1317
|
+
if (twoStepChunkCount === 1 || !(wasm as any).generate_geometry_multi) {
|
|
1195
1318
|
const { blockStates, blockLight, skyLight, biomesArray } = conversions[0]
|
|
1196
1319
|
t1 = performance.now()
|
|
1197
1320
|
wasmResult = wasm.generate_geometry(
|
|
@@ -1219,17 +1342,17 @@ function processColumnTick() {
|
|
|
1219
1342
|
} else {
|
|
1220
1343
|
const tBuildStart = performance.now()
|
|
1221
1344
|
const perChunkLen = conversions[0].blockStates.length
|
|
1222
|
-
const xs = new Int32Array(
|
|
1223
|
-
const zs = new Int32Array(
|
|
1224
|
-
const blockStatesAll = new Uint16Array(perChunkLen *
|
|
1225
|
-
const blockLightAll = new Uint8Array(perChunkLen *
|
|
1226
|
-
const skyLightAll = new Uint8Array(perChunkLen *
|
|
1227
|
-
const biomesAll = new Uint8Array(perChunkLen *
|
|
1228
|
-
|
|
1229
|
-
for (let i = 0; i <
|
|
1345
|
+
const xs = new Int32Array(twoStepChunkCount)
|
|
1346
|
+
const zs = new Int32Array(twoStepChunkCount)
|
|
1347
|
+
const blockStatesAll = new Uint16Array(perChunkLen * twoStepChunkCount)
|
|
1348
|
+
const blockLightAll = new Uint8Array(perChunkLen * twoStepChunkCount)
|
|
1349
|
+
const skyLightAll = new Uint8Array(perChunkLen * twoStepChunkCount)
|
|
1350
|
+
const biomesAll = new Uint8Array(perChunkLen * twoStepChunkCount)
|
|
1351
|
+
|
|
1352
|
+
for (let i = 0; i < twoStepChunkCount; i++) {
|
|
1230
1353
|
const c = conversions[i]
|
|
1231
|
-
xs[i] =
|
|
1232
|
-
zs[i] =
|
|
1354
|
+
xs[i] = meshChunks[i].x
|
|
1355
|
+
zs[i] = meshChunks[i].z
|
|
1233
1356
|
blockStatesAll.set(c.blockStates, perChunkLen * i)
|
|
1234
1357
|
blockLightAll.set(c.blockLight, perChunkLen * i)
|
|
1235
1358
|
skyLightAll.set(c.skyLight, perChunkLen * i)
|
|
@@ -1497,7 +1620,11 @@ function processColumnTick() {
|
|
|
1497
1620
|
preTypedArrayBuild: !attributed ? preTypedArrayBuild : 0,
|
|
1498
1621
|
preOther: !attributed ? preOther : 0,
|
|
1499
1622
|
preCacheHits: !attributed ? preCacheHits : 0,
|
|
1500
|
-
preCacheMisses: !attributed ? preCacheMisses : 0
|
|
1623
|
+
preCacheMisses: !attributed ? preCacheMisses : 0,
|
|
1624
|
+
chunkCount: !attributed ? chunkCount : 0,
|
|
1625
|
+
worldColumns3x3: !attributed ? worldColumns3x3 : 0,
|
|
1626
|
+
parsedCache3x3: !attributed ? parsedCache3x3 : 0,
|
|
1627
|
+
columnMeshPath: !attributed ? columnMeshPath : 'none'
|
|
1501
1628
|
})
|
|
1502
1629
|
attributed = true
|
|
1503
1630
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
export type PendingChunkMessage = {
|
|
3
|
+
x: number
|
|
4
|
+
z: number
|
|
5
|
+
chunk: any
|
|
6
|
+
customBlockModels?: any
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class PendingChunkBuffer {
|
|
10
|
+
private pending: PendingChunkMessage[] = []
|
|
11
|
+
|
|
12
|
+
enqueue(msg: PendingChunkMessage) {
|
|
13
|
+
this.pending.push(msg)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
drain(apply: (msg: PendingChunkMessage) => void) {
|
|
17
|
+
const batch = this.pending.splice(0)
|
|
18
|
+
for (const msg of batch) apply(msg)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get size() {
|
|
22
|
+
return this.pending.length
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
clear() {
|
|
26
|
+
this.pending.length = 0
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
export type ColumnChunkEntry = { x: number; z: number; chunk: any | null }
|
|
3
|
+
|
|
4
|
+
export interface PacketCacheSet {
|
|
5
|
+
has: (key: string) => boolean
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface PacketCaches {
|
|
9
|
+
raw: PacketCacheSet
|
|
10
|
+
v17: PacketCacheSet
|
|
11
|
+
v16: PacketCacheSet
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function columnCacheKey(x: number, z: number) {
|
|
15
|
+
return `${x},${z}`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function columnHasPacketCache(x: number, z: number, caches: PacketCaches): boolean {
|
|
19
|
+
const k = columnCacheKey(x, z)
|
|
20
|
+
return caches.raw.has(k) || caches.v17.has(k) || caches.v16.has(k)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function columnDataAvailable(x: number, z: number, getColumn: (x: number, z: number) => any | null | undefined, caches: PacketCaches): boolean {
|
|
24
|
+
return !!getColumn(x, z) || columnHasPacketCache(x, z, caches)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function countWorldColumns3x3(x: number, z: number, getColumn: (x: number, z: number) => any | null | undefined): number {
|
|
28
|
+
let count = 0
|
|
29
|
+
for (const dx of [-16, 0, 16]) {
|
|
30
|
+
for (const dz of [-16, 0, 16]) {
|
|
31
|
+
if (getColumn(x + dx, z + dz)) count++
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return count
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function countParsedCache3x3(x: number, z: number, caches: PacketCaches): number {
|
|
38
|
+
let count = 0
|
|
39
|
+
for (const dx of [-16, 0, 16]) {
|
|
40
|
+
for (const dz of [-16, 0, 16]) {
|
|
41
|
+
if (columnHasPacketCache(x + dx, z + dz, caches)) count++
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return count
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function collectChunksForColumnUnion(
|
|
48
|
+
x: number,
|
|
49
|
+
z: number,
|
|
50
|
+
getColumn: (x: number, z: number) => any | null | undefined,
|
|
51
|
+
caches: PacketCaches
|
|
52
|
+
): ColumnChunkEntry[] {
|
|
53
|
+
const result: ColumnChunkEntry[] = []
|
|
54
|
+
const seen = new Set<string>()
|
|
55
|
+
|
|
56
|
+
const add = (nx: number, nz: number) => {
|
|
57
|
+
const key = columnCacheKey(nx, nz)
|
|
58
|
+
if (seen.has(key)) return
|
|
59
|
+
seen.add(key)
|
|
60
|
+
const chunk = getColumn(nx, nz) ?? null
|
|
61
|
+
if (chunk || columnHasPacketCache(nx, nz, caches)) {
|
|
62
|
+
result.push({ x: nx, z: nz, chunk })
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
add(x, z)
|
|
67
|
+
for (const dx of [-16, 0, 16]) {
|
|
68
|
+
for (const dz of [-16, 0, 16]) {
|
|
69
|
+
if (dx === 0 && dz === 0) continue
|
|
70
|
+
add(x + dx, z + dz)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return result
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const SIDE_NEIGHBOR_OFFSETS = [
|
|
77
|
+
[-16, 0],
|
|
78
|
+
[16, 0],
|
|
79
|
+
[0, -16],
|
|
80
|
+
[0, 16]
|
|
81
|
+
] as const
|
|
82
|
+
|
|
83
|
+
export class PendingNeighborHealTracker {
|
|
84
|
+
// Neighbor column key -> columns that meshed without this neighbor.
|
|
85
|
+
private awaiting = new Map<string, Set<string>>()
|
|
86
|
+
|
|
87
|
+
private colKey(x: number, z: number) {
|
|
88
|
+
return columnCacheKey(x, z)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
recordMissingSide(columnX: number, columnZ: number, missingNeighborX: number, missingNeighborZ: number) {
|
|
92
|
+
const neighborKey = this.colKey(missingNeighborX, missingNeighborZ)
|
|
93
|
+
const columnKey = this.colKey(columnX, columnZ)
|
|
94
|
+
let set = this.awaiting.get(neighborKey)
|
|
95
|
+
if (!set) {
|
|
96
|
+
set = new Set()
|
|
97
|
+
this.awaiting.set(neighborKey, set)
|
|
98
|
+
}
|
|
99
|
+
set.add(columnKey)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
takeColumnsAwaitingNeighbor(neighborX: number, neighborZ: number): Array<{ x: number; z: number }> {
|
|
103
|
+
const key = this.colKey(neighborX, neighborZ)
|
|
104
|
+
const waiting = this.awaiting.get(key)
|
|
105
|
+
if (!waiting || waiting.size === 0) return []
|
|
106
|
+
this.awaiting.delete(key)
|
|
107
|
+
return [...waiting].map(k => {
|
|
108
|
+
const [sx, sz] = k.split(',').map(Number)
|
|
109
|
+
return { x: sx, z: sz }
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
clearColumn(x: number, z: number) {
|
|
114
|
+
const columnKey = this.colKey(x, z)
|
|
115
|
+
for (const [neighborKey, set] of this.awaiting) {
|
|
116
|
+
set.delete(columnKey)
|
|
117
|
+
if (set.size === 0) this.awaiting.delete(neighborKey)
|
|
118
|
+
}
|
|
119
|
+
this.awaiting.delete(columnKey)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
clear() {
|
|
123
|
+
this.awaiting.clear()
|
|
124
|
+
}
|
|
125
|
+
}
|