minecraft-renderer 0.1.107 → 0.1.109
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/lightOwnerWorker.js +9 -0
- package/dist/lightOwnerWorker.js.map +7 -0
- package/dist/mesher.js +1 -1
- package/dist/mesher.js.map +2 -2
- package/dist/mesherWasm.js +222 -222
- package/dist/minecraft-renderer.js +84 -84
- package/dist/minecraft-renderer.js.meta.json +1 -1
- package/dist/threeWorker.js +517 -517
- package/package.json +1 -1
- package/src/bundler/bundlePrepare.test.ts +25 -0
- package/src/bundler/bundlePrepare.ts +1 -1
- package/src/graphicsBackend/appViewer.ts +15 -0
- package/src/graphicsBackend/config.ts +5 -0
- package/src/graphicsBackend/rendererDefaultOptions.test.ts +51 -0
- package/src/graphicsBackend/rendererDefaultOptions.ts +13 -1
- package/src/graphicsBackend/rendererOptionsSync.ts +3 -3
- package/src/lib/blockEntityLightRegistry.ts +4 -0
- package/src/lib/worldrendererCommon.lightOwner.test.ts +916 -0
- package/src/lib/worldrendererCommon.removeColumn.test.ts +16 -0
- package/src/lib/worldrendererCommon.ts +329 -67
- package/src/lighting/cache/rendererLightCache.test.ts +158 -0
- package/src/lighting/cache/rendererLightCache.ts +248 -0
- package/src/lighting/coordinator/clientLightCoordinator.test.ts +102 -0
- package/src/lighting/coordinator/clientLightCoordinator.ts +574 -0
- package/src/lighting/debug/clientLightProbe.test.ts +24 -0
- package/src/lighting/debug/clientLightProbe.ts +20 -0
- package/src/lighting/debug/clientLightTrace.test.ts +271 -0
- package/src/lighting/debug/clientLightTrace.ts +335 -0
- package/src/lighting/entity/entityLightController.test.ts +216 -0
- package/src/lighting/entity/entityLightController.ts +322 -0
- package/src/lighting/owner/clientLightOwner.test.ts +467 -0
- package/src/lighting/owner/clientLightOwner.ts +523 -0
- package/src/lighting/owner/lightOwnerHost.test.ts +78 -0
- package/src/lighting/owner/lightOwnerHost.ts +202 -0
- package/src/lighting/owner/lightOwnerWorker.ts +154 -0
- package/src/lighting/owner/updateLight1171OwnerPath.test.ts +68 -0
- package/src/lighting/owner/updateLightToOwnerEvents.ts +49 -0
- package/src/lighting/tables/lightTables1171.generated.json +10 -0
- package/src/lighting/tables/lightTables1171.test.ts +168 -0
- package/src/lighting/tables/lightTables1171.ts +59 -0
- package/src/lighting/versioning/clientLightVersions.test.ts +241 -0
- package/src/lighting/versioning/clientLightVersions.ts +203 -0
- package/src/lighting/versioning/pendingSectionFlush.test.ts +214 -0
- package/src/lighting/versioning/pendingSectionFlush.ts +123 -0
- package/src/mesher-shared/lightNibblePack.ts +26 -0
- package/src/mesher-shared/mcDataWorkerShim.cjs +9 -7
- package/src/mesher-shared/modelsGeometryCommon.test.ts +6 -1
- package/src/mesher-shared/shared.ts +48 -1
- package/src/three/chunkMeshManager.ts +26 -0
- package/src/three/cloudCover.ts +22 -0
- package/src/three/cloudGeometry.ts +71 -0
- package/src/three/entities.ts +77 -11
- package/src/three/entity/EntityMesh.ts +1 -0
- package/src/three/entity/legWalkAnimation.test.ts +11 -2
- package/src/three/entity/legWalkAnimation.ts +1 -9
- package/src/three/globalBlockBuffer.ts +94 -2
- package/src/three/globalLegacyBuffer.ts +94 -2
- package/src/three/graphicsBackendBase.ts +2 -9
- package/src/three/modules/clouds.ts +98 -75
- package/src/three/tests/cloudGeometry.test.ts +87 -0
- package/src/three/tests/clouds.test.ts +65 -14
- package/src/three/tests/globalBlockBuffer.test.ts +98 -0
- package/src/three/tests/globalLegacyBuffer.test.ts +108 -0
- package/src/three/worldRendererThree.ts +75 -34
- package/src/wasm-mesher/bridge/render-from-wasm.ts +46 -1
- package/src/wasm-mesher/runtime-build/wasm_mesher.d.ts +31 -5
- package/src/wasm-mesher/runtime-build/wasm_mesher.js +180 -5
- package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm +0 -0
- package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm.d.ts +11 -0
- package/src/wasm-mesher/tests/mesherLightDependencies.test.ts +83 -0
- package/src/wasm-mesher/tests/mesherWasmColumnTickSelect.test.ts +39 -0
- package/src/wasm-mesher/tests/mesherWasmConversionCache.test.ts +30 -1
- package/src/wasm-mesher/tests/mesherWasmTickParseCache.test.ts +73 -0
- package/src/wasm-mesher/tests/mesherWasmTickSchedule.test.ts +51 -0
- package/src/wasm-mesher/tests/mesherWasmTopologyPostCache.test.ts +78 -0
- package/src/wasm-mesher/worker/clientLightDarkPublication.probe.test.ts +32 -0
- package/src/wasm-mesher/worker/mesherLightDependencies.ts +43 -0
- package/src/wasm-mesher/worker/mesherWasm.ownerLightSync.test.ts +198 -0
- package/src/wasm-mesher/worker/mesherWasm.ts +506 -96
- package/src/wasm-mesher/worker/mesherWasmChunkBuffer.ts +1 -0
- package/src/wasm-mesher/worker/mesherWasmColumnTickSelect.ts +47 -0
- package/src/wasm-mesher/worker/mesherWasmConversionCache.ts +16 -5
- package/src/wasm-mesher/worker/mesherWasmLightDirty.test.ts +17 -1
- package/src/wasm-mesher/worker/mesherWasmLightDirty.ts +10 -0
- package/src/wasm-mesher/worker/mesherWasmLightMerge.test.ts +208 -0
- package/src/wasm-mesher/worker/mesherWasmLightMerge.ts +228 -0
- package/src/wasm-mesher/worker/mesherWasmOwnerLight.test.ts +161 -0
- package/src/wasm-mesher/worker/mesherWasmOwnerLight.ts +112 -0
- package/src/wasm-mesher/worker/mesherWasmTickParseCache.ts +45 -0
- package/src/wasm-mesher/worker/mesherWasmTickSchedule.ts +44 -0
- package/src/wasm-mesher/worker/mesherWasmTopologyPostCache.ts +63 -0
- package/src/worldView/types.ts +1 -0
- package/src/worldView/worldView.geometryReload.test.ts +68 -0
- package/src/worldView/worldView.spiral.test.ts +1 -1
- package/src/worldView/worldView.ts +46 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
//@ts-nocheck
|
|
2
2
|
import { Vec3 } from 'vec3'
|
|
3
|
+
import { setClientLightProbePositions } from '../../lighting/debug/clientLightProbe'
|
|
3
4
|
import { convertChunkToWasm, getBlockMeta, getGlobalPaletteBits, type ChunkConversionResult } from '../bridge/convertChunk'
|
|
4
5
|
import { extractColumnHeightmap, splitColumnWasmOutputToSections } from '../bridge/render-from-wasm'
|
|
5
6
|
import { createGeometryIndexArray } from '../bridge/geometryIndices'
|
|
@@ -12,8 +13,30 @@ import { worldColumnKey, World } from '../../mesher-shared/world'
|
|
|
12
13
|
import { handleGetHeightmap, EMPTY_COLUMN_HEIGHTMAP_SENTINEL } from '../../mesher-shared/computeHeightmap'
|
|
13
14
|
import { collectBlockEntityMetadata, type SignMeta, type HeadMeta, type BannerMeta } from '../../mesher-shared/blockEntityMetadata'
|
|
14
15
|
import { SectionRequestTracker } from './mesherWasmRequestTracker'
|
|
15
|
-
import { sectionYsForLightColumnDirty } from './mesherWasmLightDirty'
|
|
16
|
-
import {
|
|
16
|
+
import { dropRawMapChunkOnLightOnlyReload, sectionYsForLightColumnDirty } from './mesherWasmLightDirty'
|
|
17
|
+
import { applyOwnerPublicationToColumnCaches, applyRawLightPacketToCaches, ownerDeltaSectionWorldYs, revertDisplayToIncoming } from './mesherWasmOwnerLight'
|
|
18
|
+
import { INITIAL_TOPOLOGY_REVISION, snapshotMeshVersions, validateOwnerPublicationMessage } from '../../lighting/versioning/clientLightVersions'
|
|
19
|
+
import { provenMeshLightVersion } from './mesherLightDependencies'
|
|
20
|
+
import { BULK_MESH_INTERVAL_MS, createMeshTickScheduler } from './mesherWasmTickSchedule'
|
|
21
|
+
import {
|
|
22
|
+
displayLightColumn,
|
|
23
|
+
overlayDisplayLight,
|
|
24
|
+
isLightSectionPresent,
|
|
25
|
+
parsedUpdateLightFromWasm,
|
|
26
|
+
worldSectionMaskBit,
|
|
27
|
+
type UpdateLightColumnCache
|
|
28
|
+
} from './mesherWasmLightMerge'
|
|
29
|
+
import {
|
|
30
|
+
CONVERSION_CACHE_LIMIT,
|
|
31
|
+
clearConversionCache,
|
|
32
|
+
getOrConvertColumn,
|
|
33
|
+
invalidateConversion,
|
|
34
|
+
patchConversionBlock,
|
|
35
|
+
setConversionCacheLimit
|
|
36
|
+
} from './mesherWasmConversionCache'
|
|
37
|
+
import { beginTickWasmParseCache, endTickWasmParseCache, getOrConvertTickWasmParse } from './mesherWasmTickParseCache'
|
|
38
|
+
import { clearTopologyPostCache, clearTopologyPostCacheColumn, getOrComputeTopologyPost } from './mesherWasmTopologyPostCache'
|
|
39
|
+
import { selectColumnGroupsForTick } from './mesherWasmColumnTickSelect'
|
|
17
40
|
import { PendingChunkBuffer } from './mesherWasmChunkBuffer'
|
|
18
41
|
import {
|
|
19
42
|
PendingNeighborHealTracker,
|
|
@@ -24,6 +47,14 @@ import {
|
|
|
24
47
|
countWorldColumns3x3,
|
|
25
48
|
type PacketCaches
|
|
26
49
|
} from './mesherWasmNeighborhood'
|
|
50
|
+
import {
|
|
51
|
+
CLIENT_LIGHT_TRACE_MESSAGE,
|
|
52
|
+
armClientLightTrace,
|
|
53
|
+
enableClientLightTrace,
|
|
54
|
+
isClientLightTraceArmed,
|
|
55
|
+
isClientLightTraceEnabled,
|
|
56
|
+
postClientLightTrace
|
|
57
|
+
} from '../../lighting/debug/clientLightTrace'
|
|
27
58
|
|
|
28
59
|
let wasm: typeof import('../runtime-build/wasm_mesher.js') | null = null
|
|
29
60
|
let wasmInitialized = false
|
|
@@ -51,11 +82,16 @@ function processUpdateLightV17(rawPacket: Uint8Array, numSections: number): void
|
|
|
51
82
|
const parsed: any = (wasm as any).parseUpdateLightV17(rawPacket, numSections)
|
|
52
83
|
const x = (parsed.x as number) * 16
|
|
53
84
|
const z = (parsed.z as number) * 16
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
85
|
+
const key = rawCacheKey(x, z)
|
|
86
|
+
const result = applyRawLightPacketToCaches({
|
|
87
|
+
ownerOwnsColumn: ownerOwnedLightColumns.has(key),
|
|
88
|
+
incoming: incomingUpdateLightV17Cache.get(key),
|
|
89
|
+
display: updateLightV17Cache.get(key),
|
|
90
|
+
parsed: parsedUpdateLightFromWasm(parsed, numSections)
|
|
58
91
|
})
|
|
92
|
+
incomingUpdateLightV17Cache.set(key, result.incoming)
|
|
93
|
+
if (!result.dirtyDisplay || !result.display) return
|
|
94
|
+
updateLightV17Cache.set(key, result.display)
|
|
59
95
|
invalidateConversion(x, z)
|
|
60
96
|
dirtyColumnSectionsForLightUpdate(x, z)
|
|
61
97
|
const hadColumn = !!world?.getColumn(x, z)
|
|
@@ -80,10 +116,16 @@ function processUpdateLightV16(rawPacket: Uint8Array): void {
|
|
|
80
116
|
const parsed: any = (wasm as any).parseUpdateLightV17(rawPacket, 16)
|
|
81
117
|
const x = (parsed.x as number) * 16
|
|
82
118
|
const z = (parsed.z as number) * 16
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
119
|
+
const key = rawCacheKey(x, z)
|
|
120
|
+
const result = applyRawLightPacketToCaches({
|
|
121
|
+
ownerOwnsColumn: ownerOwnedLightColumns.has(key),
|
|
122
|
+
incoming: incomingUpdateLightV16Cache.get(key),
|
|
123
|
+
display: updateLightV16Cache.get(key),
|
|
124
|
+
parsed: parsedUpdateLightFromWasm(parsed, 16)
|
|
86
125
|
})
|
|
126
|
+
incomingUpdateLightV16Cache.set(key, result.incoming)
|
|
127
|
+
if (!result.dirtyDisplay || !result.display) return
|
|
128
|
+
updateLightV16Cache.set(key, result.display)
|
|
87
129
|
invalidateConversion(x, z)
|
|
88
130
|
dirtyColumnSectionsForLightUpdate(x, z)
|
|
89
131
|
} catch (err) {
|
|
@@ -135,6 +177,23 @@ let config = defaultMesherConfig
|
|
|
135
177
|
let version = '1.16.5'
|
|
136
178
|
let world: World // chunkKey -> chunk data
|
|
137
179
|
let dirtySections = new Map<string, number>()
|
|
180
|
+
const urgentSections = new Set<string>()
|
|
181
|
+
const dirtyLightMeta = new Map<string, { lightPublicationVersion?: number; worldGeneration?: number }>()
|
|
182
|
+
const dirtyTraceMeta = new Map<string, { clientLightRequestId?: number; clientLightEditSeq?: number; clientLightSessionEpoch?: number }>()
|
|
183
|
+
const dirtyVersionMeta = new Map<
|
|
184
|
+
string,
|
|
185
|
+
{
|
|
186
|
+
sessionEpoch?: number
|
|
187
|
+
columnIncarnation?: number
|
|
188
|
+
requestId?: number
|
|
189
|
+
topologyRevision?: number
|
|
190
|
+
neighborTopologyRevisions?: Array<{ key: string; topologyRevision: number }>
|
|
191
|
+
}
|
|
192
|
+
>()
|
|
193
|
+
const appliedOwnerLightVersion = new Map<string, number>()
|
|
194
|
+
const columnIncarnation = new Map<string, number>()
|
|
195
|
+
let ownerSessionEpoch = 0
|
|
196
|
+
let rejectOwnerPublications = false
|
|
138
197
|
// Kept in sync with `dirtySections` so column mode can filter outgoing
|
|
139
198
|
// geometry/sectionFinished events to only the section keys requested by the
|
|
140
199
|
// main thread, even though a full-column WASM call may generate more data.
|
|
@@ -162,8 +221,19 @@ const postMessage = (data: any, transferList: any[] = []) => {
|
|
|
162
221
|
})
|
|
163
222
|
}
|
|
164
223
|
|
|
224
|
+
const postTrace = (message: { type: typeof CLIENT_LIGHT_TRACE_MESSAGE; events?: unknown; event?: unknown }) => {
|
|
225
|
+
global.postMessage(message)
|
|
226
|
+
}
|
|
227
|
+
|
|
165
228
|
function drainQueue(from: number, to: number) {
|
|
166
229
|
const messages = queuedMessages.slice(from, to)
|
|
230
|
+
const remaining = queuedMessages.length - to
|
|
231
|
+
if (isClientLightTraceEnabled() && isClientLightTraceArmed()) {
|
|
232
|
+
postClientLightTrace(postTrace, {
|
|
233
|
+
phase: 'mesherFlush',
|
|
234
|
+
queueDepth: remaining
|
|
235
|
+
})
|
|
236
|
+
}
|
|
167
237
|
global.postMessage(
|
|
168
238
|
messages.map(m => m.data),
|
|
169
239
|
messages.flatMap(m => m.transferList) as unknown as string
|
|
@@ -260,25 +330,71 @@ const parsedV17Cache = new Map<string, ParsedV17Entry>()
|
|
|
260
330
|
// WASM (`parseUpdateLightV17`) and cache per-block arrays keyed by the
|
|
261
331
|
// chunk origin — the next mesh tick of that column merges them in instead
|
|
262
332
|
// of the sky=15/block=0 fallback. May arrive before or after `map_chunk`.
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
333
|
+
const updateLightV17Cache = new Map<string, UpdateLightColumnCache>()
|
|
334
|
+
const incomingUpdateLightV17Cache = new Map<string, UpdateLightColumnCache>()
|
|
335
|
+
const ownerOwnedLightColumns = new Set<string>()
|
|
336
|
+
|
|
337
|
+
function takeGeometryVersionMeta(key: string, sx: number, sz: number, usedColumns: ReadonlySet<string>) {
|
|
338
|
+
const lightMeta = dirtyLightMeta.get(key)
|
|
339
|
+
dirtyLightMeta.delete(key)
|
|
340
|
+
const traceMeta = dirtyTraceMeta.get(key)
|
|
341
|
+
dirtyTraceMeta.delete(key)
|
|
342
|
+
const job = dirtyVersionMeta.get(key)
|
|
343
|
+
dirtyVersionMeta.delete(key)
|
|
344
|
+
const columnKey = rawCacheKey(sx, sz)
|
|
345
|
+
const owned = ownerOwnedLightColumns.has(columnKey)
|
|
346
|
+
const lightPublicationVersion = provenMeshLightVersion({
|
|
347
|
+
sx,
|
|
348
|
+
sz,
|
|
349
|
+
usedColumns,
|
|
350
|
+
appliedVersionByColumn: appliedOwnerLightVersion
|
|
351
|
+
})
|
|
352
|
+
if (!job && !owned && !lightMeta && lightPublicationVersion === 0) return { ...traceMeta }
|
|
353
|
+
const appliedLightMeta = { ...(lightMeta ?? {}) }
|
|
354
|
+
delete appliedLightMeta.lightPublicationVersion
|
|
355
|
+
return {
|
|
356
|
+
...appliedLightMeta,
|
|
357
|
+
...traceMeta,
|
|
358
|
+
...snapshotMeshVersions({
|
|
359
|
+
sessionEpoch: job?.sessionEpoch ?? ownerSessionEpoch,
|
|
360
|
+
columnIncarnation: columnIncarnation.get(columnKey) ?? job?.columnIncarnation ?? 1,
|
|
361
|
+
requestId: job?.requestId ?? 0,
|
|
362
|
+
topologyRevision: job?.topologyRevision ?? INITIAL_TOPOLOGY_REVISION,
|
|
363
|
+
lightPublicationVersion,
|
|
364
|
+
worldGeneration: lightMeta?.worldGeneration ?? 0,
|
|
365
|
+
neighborTopologyRevisions: job?.neighborTopologyRevisions ?? [],
|
|
366
|
+
meshMode: owned ? 'owner' : 'legacyBootstrap'
|
|
367
|
+
})
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function displayCachedLight(entry: UpdateLightColumnCache | undefined): { skyLight: Uint8Array; blockLight: Uint8Array } | undefined {
|
|
372
|
+
if (!entry) return undefined
|
|
373
|
+
return {
|
|
374
|
+
skyLight: displayLightColumn(entry, 'sky', config?.skyLight ?? 15),
|
|
375
|
+
blockLight: displayLightColumn(entry, 'block', 0)
|
|
376
|
+
}
|
|
266
377
|
}
|
|
267
|
-
const updateLightV17Cache = new Map<string, UpdateLightV17Entry>()
|
|
268
378
|
/** Columns that received `update_light` before the worker column existed. */
|
|
269
379
|
const pendingLightDirtyColumns = new Set<string>()
|
|
270
380
|
const _syncLightPos = new Vec3(0, 0, 0)
|
|
271
381
|
|
|
272
|
-
function resolveUpdateLightV17Entry(
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
382
|
+
function resolveUpdateLightV17Entry(
|
|
383
|
+
x: number,
|
|
384
|
+
z: number,
|
|
385
|
+
chunk: any | undefined,
|
|
386
|
+
worldMinY: number,
|
|
387
|
+
worldMaxY: number
|
|
388
|
+
): { skyLight: Uint8Array; blockLight: Uint8Array } | undefined {
|
|
389
|
+
const displayed = displayCachedLight(updateLightV17Cache.get(rawCacheKey(x, z)))
|
|
390
|
+
if (displayed) return displayed
|
|
391
|
+
if (!chunk) return undefined
|
|
276
392
|
const { result } = getOrConvertColumn(x, z, chunk, version, worldMinY, worldMaxY, () => convertChunkToWasm(chunk, version, x, z, worldMinY, worldMaxY), chunk)
|
|
277
393
|
return { blockLight: result.blockLight, skyLight: result.skyLight }
|
|
278
394
|
}
|
|
279
395
|
|
|
280
396
|
/** Write cached 1.17 `update_light` arrays into the worker prismarine column. */
|
|
281
|
-
function syncV17LightToColumn(x: number, z: number): boolean {
|
|
397
|
+
function syncV17LightToColumn(x: number, z: number, onlySectionWorldYs?: number[]): boolean {
|
|
282
398
|
if (!world) return false
|
|
283
399
|
const col = world.getColumn(x, z)
|
|
284
400
|
const entry = updateLightV17Cache.get(rawCacheKey(x, z))
|
|
@@ -287,16 +403,37 @@ function syncV17LightToColumn(x: number, z: number): boolean {
|
|
|
287
403
|
const CHUNK_SIZE = 16
|
|
288
404
|
const minY = config?.worldMinY ?? 0
|
|
289
405
|
const maxY = config?.worldMaxY ?? 256
|
|
290
|
-
const { blockLight, skyLight } = entry
|
|
406
|
+
const { blockLight, skyLight, blockPresent, skyPresent } = entry
|
|
407
|
+
const onlyYs = onlySectionWorldYs && onlySectionWorldYs.length ? new Set(onlySectionWorldYs) : null
|
|
291
408
|
|
|
292
409
|
for (let y = minY; y < maxY; y++) {
|
|
410
|
+
const sectionOriginY = Math.floor((y - minY) / CHUNK_SIZE) * CHUNK_SIZE + minY
|
|
411
|
+
if (onlyYs && !onlyYs.has(sectionOriginY)) continue
|
|
412
|
+
const sectionIndex = Math.floor((y - minY) / CHUNK_SIZE)
|
|
413
|
+
const skyAuthoritative = isLightSectionPresent(skyPresent, worldSectionMaskBit(sectionIndex))
|
|
414
|
+
const blockAuthoritative = isLightSectionPresent(blockPresent, worldSectionMaskBit(sectionIndex))
|
|
415
|
+
if (!skyAuthoritative && !blockAuthoritative) continue
|
|
416
|
+
// Prismarine skips zero writes into null sections. Allocate through its
|
|
417
|
+
// setters so masks remain correct; the normal loop restores the true value.
|
|
418
|
+
const lightSectionIndex = sectionIndex + 1
|
|
419
|
+
const lightSections = col as typeof col & {
|
|
420
|
+
blockLightSections?: Array<unknown | null>
|
|
421
|
+
skyLightSections?: Array<unknown | null>
|
|
422
|
+
}
|
|
423
|
+
_syncLightPos.set(0, y, 0)
|
|
424
|
+
if (blockAuthoritative && lightSections.blockLightSections?.[lightSectionIndex] === null) {
|
|
425
|
+
col.setBlockLight(_syncLightPos, 1)
|
|
426
|
+
}
|
|
427
|
+
if (skyAuthoritative && lightSections.skyLightSections?.[lightSectionIndex] === null) {
|
|
428
|
+
col.setSkyLight(_syncLightPos, 1)
|
|
429
|
+
}
|
|
293
430
|
for (let lz = 0; lz < CHUNK_SIZE; lz++) {
|
|
294
431
|
for (let lx = 0; lx < CHUNK_SIZE; lx++) {
|
|
295
432
|
const idx = lx + lz * CHUNK_SIZE + (y - minY) * CHUNK_SIZE * CHUNK_SIZE
|
|
296
|
-
if (idx >= blockLight.length) continue
|
|
433
|
+
if (idx >= blockLight.length && idx >= skyLight.length) continue
|
|
297
434
|
_syncLightPos.set(lx, y, lz)
|
|
298
|
-
col.setBlockLight(_syncLightPos, blockLight[idx]!)
|
|
299
|
-
col.setSkyLight(_syncLightPos, skyLight[idx]!)
|
|
435
|
+
if (blockAuthoritative && idx < blockLight.length) col.setBlockLight(_syncLightPos, blockLight[idx]!)
|
|
436
|
+
if (skyAuthoritative && idx < skyLight.length) col.setSkyLight(_syncLightPos, skyLight[idx]!)
|
|
300
437
|
}
|
|
301
438
|
}
|
|
302
439
|
}
|
|
@@ -384,6 +521,7 @@ function processChunkMessage(data: { x: number; z: number; chunk: any; customBlo
|
|
|
384
521
|
function drainPendingChunks() {
|
|
385
522
|
if (!world) return
|
|
386
523
|
pendingChunks.drain(msg => {
|
|
524
|
+
dropRawMapChunkOnLightOnlyReload(msg.isLightUpdate, rawMapChunkCache, rawCacheKey(msg.x, msg.z))
|
|
387
525
|
processChunkMessage(msg)
|
|
388
526
|
onColumnDataArrived(msg.x, msg.z)
|
|
389
527
|
})
|
|
@@ -393,7 +531,8 @@ function drainPendingChunks() {
|
|
|
393
531
|
// `processUpdateLightV16` (which calls the shared `parseUpdateLightV17`
|
|
394
532
|
// WASM export). Separate map for the same isolation reasons as
|
|
395
533
|
// `parsedV16Cache` above.
|
|
396
|
-
const updateLightV16Cache = new Map<string,
|
|
534
|
+
const updateLightV16Cache = new Map<string, UpdateLightColumnCache>()
|
|
535
|
+
const incomingUpdateLightV16Cache = new Map<string, UpdateLightColumnCache>()
|
|
397
536
|
|
|
398
537
|
// Mirrors `convertChunkToWasm`'s output (same layout: x + z*16 + y*256,
|
|
399
538
|
// y outer) so it can be dropped straight into `generate_geometry`.
|
|
@@ -434,7 +573,7 @@ const convertRawMapChunkToWasm = (raw: RawMapChunkEntry, version: string): Chunk
|
|
|
434
573
|
// (expanded from the 4×4×4 cell layout). Light comes from the paired
|
|
435
574
|
// `update_light` cache when available; otherwise we fall back to full
|
|
436
575
|
// daylight (sky=15) and no block light so geometry stays visible.
|
|
437
|
-
const convertParsedV17ToWasm = (entry: ParsedV17Entry, lightEntry:
|
|
576
|
+
const convertParsedV17ToWasm = (entry: ParsedV17Entry, lightEntry: UpdateLightColumnCache | undefined, version: string): ChunkConversionResult | null => {
|
|
438
577
|
if (!wasm || !(wasm as any).parseChunkSectionsV16V17) return null
|
|
439
578
|
// Empty `Int32Array` signals "no biomes captured" — WASM falls back to
|
|
440
579
|
// `default_biome` for every block. Plains (id 1) matches the JS path.
|
|
@@ -451,9 +590,9 @@ const convertParsedV17ToWasm = (entry: ParsedV17Entry, lightEntry: UpdateLightV1
|
|
|
451
590
|
const totalBlocks = blockStates.length
|
|
452
591
|
let blockLight: Uint8Array
|
|
453
592
|
let skyLight: Uint8Array
|
|
454
|
-
if (lightEntry
|
|
455
|
-
skyLight = lightEntry
|
|
456
|
-
blockLight = lightEntry
|
|
593
|
+
if (lightEntry) {
|
|
594
|
+
skyLight = displayLightColumn(lightEntry, 'sky', config?.skyLight ?? 15)
|
|
595
|
+
blockLight = displayLightColumn(lightEntry, 'block', 0)
|
|
457
596
|
} else {
|
|
458
597
|
blockLight = new Uint8Array(totalBlocks)
|
|
459
598
|
skyLight = new Uint8Array(totalBlocks)
|
|
@@ -485,7 +624,7 @@ const convertParsedV17ToWasm = (entry: ParsedV17Entry, lightEntry: UpdateLightV1
|
|
|
485
624
|
// defaults — anything else means a non-vanilla server we don't support
|
|
486
625
|
// on the fast path, in which case we return null and fall back to the
|
|
487
626
|
// JS column-walk via `convertChunkToWasm`.
|
|
488
|
-
const convertParsedV16ToWasm = (entry: ParsedV16Entry, lightEntry:
|
|
627
|
+
const convertParsedV16ToWasm = (entry: ParsedV16Entry, lightEntry: UpdateLightColumnCache | undefined, version: string): ChunkConversionResult | null => {
|
|
489
628
|
if (!wasm || !(wasm as any).parseChunkSectionsV16V17) return null
|
|
490
629
|
const NUM_SECTIONS = 16
|
|
491
630
|
const MAX_BITS_PER_BLOCK = 15
|
|
@@ -506,9 +645,9 @@ const convertParsedV16ToWasm = (entry: ParsedV16Entry, lightEntry: UpdateLightV1
|
|
|
506
645
|
const totalBlocks = blockStates.length
|
|
507
646
|
let blockLight: Uint8Array
|
|
508
647
|
let skyLight: Uint8Array
|
|
509
|
-
if (lightEntry
|
|
510
|
-
skyLight = lightEntry
|
|
511
|
-
blockLight = lightEntry
|
|
648
|
+
if (lightEntry) {
|
|
649
|
+
skyLight = displayLightColumn(lightEntry, 'sky', config?.skyLight ?? 15)
|
|
650
|
+
blockLight = displayLightColumn(lightEntry, 'block', 0)
|
|
512
651
|
} else {
|
|
513
652
|
blockLight = new Uint8Array(totalBlocks)
|
|
514
653
|
skyLight = new Uint8Array(totalBlocks)
|
|
@@ -758,7 +897,7 @@ const meshMultiColumnsFromParsedV16V17 = (
|
|
|
758
897
|
const bm = entry.bitMap >>> 0
|
|
759
898
|
bitMapLoHi[i * 2] = bm
|
|
760
899
|
bitMapLoHi[i * 2 + 1] = 0
|
|
761
|
-
const light = updateLightV16Cache.get(key)
|
|
900
|
+
const light = displayCachedLight(updateLightV16Cache.get(key))
|
|
762
901
|
skyLightList.push(light?.skyLight ?? new Uint8Array(0))
|
|
763
902
|
blockLightList.push(light?.blockLight ?? new Uint8Array(0))
|
|
764
903
|
}
|
|
@@ -820,6 +959,10 @@ const handleMessage = async (data: any) => {
|
|
|
820
959
|
}
|
|
821
960
|
|
|
822
961
|
switch (data.type) {
|
|
962
|
+
case 'clientLightProbe': {
|
|
963
|
+
setClientLightProbePositions(data.points)
|
|
964
|
+
break
|
|
965
|
+
}
|
|
823
966
|
case 'mesherData': {
|
|
824
967
|
setMesherData(data.blockstatesModels, data.blocksAtlas, data.config.outputFormat === 'webgpu')
|
|
825
968
|
;(globalThis as any).__wasmBlockModelCache = new Map()
|
|
@@ -835,18 +978,78 @@ const handleMessage = async (data: any) => {
|
|
|
835
978
|
case 'dirty': {
|
|
836
979
|
const loc = new Vec3(data.x, data.y, data.z)
|
|
837
980
|
setSectionDirty(loc, data.value)
|
|
981
|
+
{
|
|
982
|
+
const sectionHeight = getSectionHeight()
|
|
983
|
+
const key = sectionKey(Math.floor(loc.x / 16) * 16, Math.floor(loc.y / sectionHeight) * sectionHeight, Math.floor(loc.z / 16) * 16)
|
|
984
|
+
if (data.urgent) {
|
|
985
|
+
urgentSections.add(key)
|
|
986
|
+
meshTickScheduler.kick()
|
|
987
|
+
} else if (!data.value) urgentSections.delete(key)
|
|
988
|
+
}
|
|
989
|
+
if (typeof data.lightPublicationVersion === 'number') {
|
|
990
|
+
const sectionHeight = getSectionHeight()
|
|
991
|
+
const key = sectionKey(Math.floor(loc.x / 16) * 16, Math.floor(loc.y / sectionHeight) * sectionHeight, Math.floor(loc.z / 16) * 16)
|
|
992
|
+
dirtyLightMeta.set(key, {
|
|
993
|
+
lightPublicationVersion: data.lightPublicationVersion,
|
|
994
|
+
worldGeneration: data.worldGeneration
|
|
995
|
+
})
|
|
996
|
+
}
|
|
997
|
+
{
|
|
998
|
+
const sectionHeight = getSectionHeight()
|
|
999
|
+
const key = sectionKey(Math.floor(loc.x / 16) * 16, Math.floor(loc.y / sectionHeight) * sectionHeight, Math.floor(loc.z / 16) * 16)
|
|
1000
|
+
if (
|
|
1001
|
+
typeof data.sessionEpoch === 'number' ||
|
|
1002
|
+
typeof data.columnIncarnation === 'number' ||
|
|
1003
|
+
typeof data.requestId === 'number' ||
|
|
1004
|
+
typeof data.topologyRevision === 'number'
|
|
1005
|
+
) {
|
|
1006
|
+
dirtyVersionMeta.set(key, {
|
|
1007
|
+
sessionEpoch: data.sessionEpoch,
|
|
1008
|
+
columnIncarnation: data.columnIncarnation,
|
|
1009
|
+
requestId: data.requestId,
|
|
1010
|
+
topologyRevision: data.topologyRevision,
|
|
1011
|
+
neighborTopologyRevisions: data.neighborTopologyRevisions
|
|
1012
|
+
})
|
|
1013
|
+
}
|
|
1014
|
+
if (typeof data.clientLightRequestId === 'number' || typeof data.clientLightEditSeq === 'number') {
|
|
1015
|
+
dirtyTraceMeta.set(key, {
|
|
1016
|
+
clientLightRequestId: data.clientLightRequestId,
|
|
1017
|
+
clientLightEditSeq: data.clientLightEditSeq,
|
|
1018
|
+
clientLightSessionEpoch: data.clientLightSessionEpoch
|
|
1019
|
+
})
|
|
1020
|
+
if (isClientLightTraceEnabled() && isClientLightTraceArmed()) {
|
|
1021
|
+
postClientLightTrace(postTrace, {
|
|
1022
|
+
phase: 'mesherEnqueue',
|
|
1023
|
+
sectionKey: key,
|
|
1024
|
+
requestId: data.clientLightRequestId,
|
|
1025
|
+
editSeq: data.clientLightEditSeq,
|
|
1026
|
+
sessionEpoch: data.clientLightSessionEpoch,
|
|
1027
|
+
lightVersion: data.lightPublicationVersion,
|
|
1028
|
+
worldGeneration: data.worldGeneration,
|
|
1029
|
+
workerIndex
|
|
1030
|
+
})
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
break
|
|
1035
|
+
}
|
|
1036
|
+
case 'clientLightTraceConfig': {
|
|
1037
|
+
enableClientLightTrace(Boolean(data.enabled))
|
|
1038
|
+
armClientLightTrace(Boolean(data.armed))
|
|
838
1039
|
break
|
|
839
1040
|
}
|
|
840
1041
|
case 'chunk': {
|
|
841
1042
|
// Invalidate BEFORE replacing the column reference so a stale entry
|
|
842
1043
|
// can never outlive the old chunk object.
|
|
843
1044
|
invalidateConversion(data.x, data.z)
|
|
1045
|
+
dropRawMapChunkOnLightOnlyReload(data.isLightUpdate, rawMapChunkCache, rawCacheKey(data.x, data.z))
|
|
844
1046
|
if (!world) {
|
|
845
1047
|
pendingChunks.enqueue({
|
|
846
1048
|
x: data.x,
|
|
847
1049
|
z: data.z,
|
|
848
1050
|
chunk: data.chunk,
|
|
849
|
-
customBlockModels: data.customBlockModels
|
|
1051
|
+
customBlockModels: data.customBlockModels,
|
|
1052
|
+
isLightUpdate: data.isLightUpdate
|
|
850
1053
|
})
|
|
851
1054
|
break
|
|
852
1055
|
}
|
|
@@ -856,21 +1059,32 @@ const handleMessage = async (data: any) => {
|
|
|
856
1059
|
}
|
|
857
1060
|
case 'unloadChunk': {
|
|
858
1061
|
invalidateConversion(data.x, data.z)
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
1062
|
+
const unloadKey = rawCacheKey(data.x, data.z)
|
|
1063
|
+
rawMapChunkCache.delete(unloadKey)
|
|
1064
|
+
parsedV17Cache.delete(unloadKey)
|
|
1065
|
+
updateLightV17Cache.delete(unloadKey)
|
|
1066
|
+
incomingUpdateLightV17Cache.delete(unloadKey)
|
|
1067
|
+
parsedV16Cache.delete(unloadKey)
|
|
1068
|
+
updateLightV16Cache.delete(unloadKey)
|
|
1069
|
+
incomingUpdateLightV16Cache.delete(unloadKey)
|
|
1070
|
+
ownerOwnedLightColumns.delete(unloadKey)
|
|
1071
|
+
appliedOwnerLightVersion.delete(unloadKey)
|
|
1072
|
+
columnIncarnation.set(unloadKey, (columnIncarnation.get(unloadKey) ?? 1) + 1)
|
|
1073
|
+
pendingLightDirtyColumns.delete(unloadKey)
|
|
865
1074
|
if (!world) break
|
|
866
1075
|
world.removeColumn(data.x, data.z)
|
|
867
1076
|
world.customBlockModels.delete(`${data.x},${data.z}`)
|
|
868
1077
|
pendingNeighborHeal.clearColumn(data.x, data.z)
|
|
869
1078
|
requestTracker.clearColumn(data.x, data.z)
|
|
1079
|
+
clearTopologyPostCacheColumn(data.x, data.z)
|
|
870
1080
|
for (const key of [...dirtySections.keys()]) {
|
|
871
1081
|
const [sx, , sz] = key.split(',').map(Number)
|
|
872
1082
|
if (sx === data.x && sz === data.z) {
|
|
873
1083
|
dirtySections.delete(key)
|
|
1084
|
+
dirtyLightMeta.delete(key)
|
|
1085
|
+
dirtyTraceMeta.delete(key)
|
|
1086
|
+
dirtyVersionMeta.delete(key)
|
|
1087
|
+
urgentSections.delete(key)
|
|
874
1088
|
}
|
|
875
1089
|
}
|
|
876
1090
|
if (Object.keys(world.columns).length === 0) softCleanup()
|
|
@@ -884,9 +1098,10 @@ const handleMessage = async (data: any) => {
|
|
|
884
1098
|
|
|
885
1099
|
const chunkX = Math.floor(loc.x / 16) * 16
|
|
886
1100
|
const chunkZ = Math.floor(loc.z / 16) * 16
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
1101
|
+
const liveColumn = world?.getColumn(chunkX, chunkZ)
|
|
1102
|
+
if (data.stateId == null || !patchConversionBlock(chunkX, chunkZ, liveColumn, loc.x, loc.y, loc.z, data.stateId)) {
|
|
1103
|
+
invalidateConversion(chunkX, chunkZ)
|
|
1104
|
+
}
|
|
890
1105
|
// Stage 3: the cached raw map_chunk no longer matches the live
|
|
891
1106
|
// column after a block update — drop it so the next mesh tick walks
|
|
892
1107
|
// the (now-updated) prismarine column instead.
|
|
@@ -961,17 +1176,110 @@ const handleMessage = async (data: any) => {
|
|
|
961
1176
|
processUpdateLightV16(data.rawPacket as Uint8Array)
|
|
962
1177
|
break
|
|
963
1178
|
}
|
|
1179
|
+
case 'setOwnerAcceptFence': {
|
|
1180
|
+
if (typeof data.sessionEpoch === 'number') ownerSessionEpoch = data.sessionEpoch
|
|
1181
|
+
rejectOwnerPublications = data.rejectOwnerPublications === true
|
|
1182
|
+
break
|
|
1183
|
+
}
|
|
1184
|
+
case 'revertOwnerLightToIncoming': {
|
|
1185
|
+
if (typeof data.sessionEpoch === 'number') ownerSessionEpoch = data.sessionEpoch
|
|
1186
|
+
rejectOwnerPublications = true
|
|
1187
|
+
for (const key of [...ownerOwnedLightColumns]) {
|
|
1188
|
+
const [cx, cz] = key.split(',').map(Number)
|
|
1189
|
+
const v16In = incomingUpdateLightV16Cache.get(key)
|
|
1190
|
+
const v17In = incomingUpdateLightV17Cache.get(key)
|
|
1191
|
+
const v16 = updateLightV16Cache.get(key)
|
|
1192
|
+
const v17 = updateLightV17Cache.get(key)
|
|
1193
|
+
if (v16 && !v17) {
|
|
1194
|
+
const reverted = revertDisplayToIncoming({ incoming: v16In, display: v16 })
|
|
1195
|
+
if (reverted.display) updateLightV16Cache.set(key, reverted.display)
|
|
1196
|
+
} else {
|
|
1197
|
+
const reverted = revertDisplayToIncoming({ incoming: v17In, display: v17 })
|
|
1198
|
+
if (reverted.display) {
|
|
1199
|
+
updateLightV17Cache.set(key, reverted.display)
|
|
1200
|
+
// The legacy mesh path reads the JS column, so it must leave owner light too.
|
|
1201
|
+
syncV17LightToColumn(cx, cz)
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
ownerOwnedLightColumns.delete(key)
|
|
1205
|
+
appliedOwnerLightVersion.delete(key)
|
|
1206
|
+
}
|
|
1207
|
+
break
|
|
1208
|
+
}
|
|
1209
|
+
case 'applyOwnerLightPublication': {
|
|
1210
|
+
if (rejectOwnerPublications) break
|
|
1211
|
+
if (typeof data.sessionEpoch === 'number' && ownerSessionEpoch !== 0 && data.sessionEpoch !== ownerSessionEpoch) break
|
|
1212
|
+
const checked = validateOwnerPublicationMessage(data)
|
|
1213
|
+
if (!checked.ok) break
|
|
1214
|
+
const sections = (data.sections ?? []) as Array<{ sx: number; sy: number; sz: number; blockLight: Uint8Array; skyLight?: Uint8Array }>
|
|
1215
|
+
const worldMinY = config?.worldMinY ?? 0
|
|
1216
|
+
const worldMaxY = config?.worldMaxY ?? 256
|
|
1217
|
+
const numSections = Math.max(1, Math.floor((worldMaxY - worldMinY) / 16))
|
|
1218
|
+
const columns = new Set<string>()
|
|
1219
|
+
for (const section of sections) columns.add(`${section.sx},${section.sz}`)
|
|
1220
|
+
const publicationVersion = typeof data.publicationVersion === 'number' ? data.publicationVersion : 0
|
|
1221
|
+
for (const col of columns) {
|
|
1222
|
+
const [cx, cz] = col.split(',').map(Number)
|
|
1223
|
+
const key = rawCacheKey(cx, cz)
|
|
1224
|
+
const v16 = updateLightV16Cache.get(key)
|
|
1225
|
+
const v17 = updateLightV17Cache.get(key)
|
|
1226
|
+
if (v16 && !v17) {
|
|
1227
|
+
const applied = applyOwnerPublicationToColumnCaches({
|
|
1228
|
+
incoming: incomingUpdateLightV16Cache.get(key),
|
|
1229
|
+
display: v16,
|
|
1230
|
+
sections,
|
|
1231
|
+
worldMinY,
|
|
1232
|
+
columnWorldX: cx,
|
|
1233
|
+
columnWorldZ: cz,
|
|
1234
|
+
numSections: v16.numSections
|
|
1235
|
+
})
|
|
1236
|
+
if (applied.incoming) incomingUpdateLightV16Cache.set(key, applied.incoming)
|
|
1237
|
+
updateLightV16Cache.set(key, applied.display)
|
|
1238
|
+
} else {
|
|
1239
|
+
const applied = applyOwnerPublicationToColumnCaches({
|
|
1240
|
+
incoming: incomingUpdateLightV17Cache.get(key),
|
|
1241
|
+
display: v17,
|
|
1242
|
+
sections,
|
|
1243
|
+
worldMinY,
|
|
1244
|
+
columnWorldX: cx,
|
|
1245
|
+
columnWorldZ: cz,
|
|
1246
|
+
numSections: v17?.numSections ?? numSections
|
|
1247
|
+
})
|
|
1248
|
+
if (applied.incoming) incomingUpdateLightV17Cache.set(key, applied.incoming)
|
|
1249
|
+
updateLightV17Cache.set(key, applied.display)
|
|
1250
|
+
// Light packets stop reaching this worker once the owner is ready, and the
|
|
1251
|
+
// legacy mesh path reads the JS column rather than the display cache.
|
|
1252
|
+
syncV17LightToColumn(cx, cz, ownerDeltaSectionWorldYs(sections, cx, cz))
|
|
1253
|
+
}
|
|
1254
|
+
ownerOwnedLightColumns.add(key)
|
|
1255
|
+
appliedOwnerLightVersion.set(key, publicationVersion)
|
|
1256
|
+
if (typeof data.sessionEpoch === 'number') ownerSessionEpoch = data.sessionEpoch
|
|
1257
|
+
}
|
|
1258
|
+
break
|
|
1259
|
+
}
|
|
964
1260
|
case 'reset': {
|
|
965
1261
|
world = undefined as any
|
|
966
1262
|
dirtySections.clear()
|
|
1263
|
+
dirtyLightMeta.clear()
|
|
1264
|
+
dirtyTraceMeta.clear()
|
|
1265
|
+
dirtyVersionMeta.clear()
|
|
1266
|
+
urgentSections.clear()
|
|
1267
|
+
appliedOwnerLightVersion.clear()
|
|
1268
|
+
columnIncarnation.clear()
|
|
1269
|
+
ownerSessionEpoch = 0
|
|
1270
|
+
rejectOwnerPublications = false
|
|
967
1271
|
requestTracker.clear()
|
|
968
1272
|
clearConversionCache()
|
|
1273
|
+
clearTopologyPostCache()
|
|
969
1274
|
rawMapChunkCache.clear()
|
|
970
1275
|
parsedV17Cache.clear()
|
|
971
1276
|
updateLightV17Cache.clear()
|
|
1277
|
+
incomingUpdateLightV17Cache.clear()
|
|
1278
|
+
ownerOwnedLightColumns.clear()
|
|
972
1279
|
pendingLightDirtyColumns.clear()
|
|
973
1280
|
parsedV16Cache.clear()
|
|
974
1281
|
updateLightV16Cache.clear()
|
|
1282
|
+
incomingUpdateLightV16Cache.clear()
|
|
975
1283
|
pendingChunks.clear()
|
|
976
1284
|
pendingNeighborHeal.clear()
|
|
977
1285
|
blindMeshWarnCount = 0
|
|
@@ -1086,7 +1394,38 @@ function makeEmptyColumnGeometry(sx: number, sy: number, sz: number, sectionHeig
|
|
|
1086
1394
|
// It groups dirty section keys by chunk column, runs one WASM call per column
|
|
1087
1395
|
// over the full Y range, then splits the column output back into per-section
|
|
1088
1396
|
// geometries. Only requested section keys are emitted back to the main thread.
|
|
1089
|
-
function processColumnTick() {
|
|
1397
|
+
async function processColumnTick() {
|
|
1398
|
+
beginTickWasmParseCache()
|
|
1399
|
+
try {
|
|
1400
|
+
await processColumnTickInner()
|
|
1401
|
+
} finally {
|
|
1402
|
+
endTickWasmParseCache()
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
function dirtyHasUrgent() {
|
|
1407
|
+
for (const key of dirtySections.keys()) {
|
|
1408
|
+
if (urgentSections.has(key)) return true
|
|
1409
|
+
}
|
|
1410
|
+
return false
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
function requeueColumnGroups(pending: Array<{ sections: Array<{ key: string; count: number }> }>, urgentKeys: Set<string>) {
|
|
1414
|
+
for (const group of pending) {
|
|
1415
|
+
for (const section of group.sections) {
|
|
1416
|
+
dirtySections.set(section.key, (dirtySections.get(section.key) ?? 0) + section.count)
|
|
1417
|
+
if (urgentKeys.has(section.key)) urgentSections.add(section.key)
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
function yieldToIncomingMessages() {
|
|
1423
|
+
return new Promise<void>(resolve => {
|
|
1424
|
+
setTimeout(resolve, 0)
|
|
1425
|
+
})
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
async function processColumnTickInner() {
|
|
1090
1429
|
const worldMinY = config?.worldMinY ?? 0
|
|
1091
1430
|
const worldMaxY = config?.worldMaxY ?? 256
|
|
1092
1431
|
const columnHeight = worldMaxY - worldMinY
|
|
@@ -1096,20 +1435,25 @@ function processColumnTick() {
|
|
|
1096
1435
|
// coords — the same units used by section keys). This guarantees a
|
|
1097
1436
|
// single WASM call per column per tick even when multiple section keys
|
|
1098
1437
|
// of the same column are dirty.
|
|
1099
|
-
const
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
groups.set(colKey, g)
|
|
1438
|
+
const { selected: groups, remaining } = selectColumnGroupsForTick(dirtySections, urgentSections)
|
|
1439
|
+
dirtySections.clear()
|
|
1440
|
+
for (const [key, count] of remaining) dirtySections.set(key, count)
|
|
1441
|
+
const selectedUrgent = new Set<string>()
|
|
1442
|
+
for (const group of groups) {
|
|
1443
|
+
for (const section of group.sections) {
|
|
1444
|
+
if (urgentSections.delete(section.key)) selectedUrgent.add(section.key)
|
|
1107
1445
|
}
|
|
1108
|
-
g.sections.push({ key, x: sx, y: sy, z: sz, count })
|
|
1109
1446
|
}
|
|
1110
|
-
|
|
1447
|
+
if (isClientLightTraceEnabled() && isClientLightTraceArmed()) {
|
|
1448
|
+
postClientLightTrace(postTrace, {
|
|
1449
|
+
phase: 'mesherStart',
|
|
1450
|
+
queueDepth: groups.length,
|
|
1451
|
+
workerIndex
|
|
1452
|
+
})
|
|
1453
|
+
}
|
|
1111
1454
|
|
|
1112
|
-
for (
|
|
1455
|
+
for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) {
|
|
1456
|
+
const group = groups[groupIndex]!
|
|
1113
1457
|
const { x, z, sections } = group
|
|
1114
1458
|
const targetChunk = world.getColumn(x, z)
|
|
1115
1459
|
|
|
@@ -1125,6 +1469,8 @@ function processColumnTick() {
|
|
|
1125
1469
|
let preOther = 0
|
|
1126
1470
|
let preCacheHits = 0
|
|
1127
1471
|
let preCacheMisses = 0
|
|
1472
|
+
let preWasmParseHits = 0
|
|
1473
|
+
let preWasmParseMisses = 0
|
|
1128
1474
|
let hadError = false
|
|
1129
1475
|
let columnMeshPath = 'none'
|
|
1130
1476
|
let chunkCount = 0
|
|
@@ -1137,6 +1483,7 @@ function processColumnTick() {
|
|
|
1137
1483
|
// attributed to the column).
|
|
1138
1484
|
let columnStart = 0
|
|
1139
1485
|
let postStart = 0
|
|
1486
|
+
let usedLightColumns = new Set<string>()
|
|
1140
1487
|
|
|
1141
1488
|
if (targetChunk && wasm) {
|
|
1142
1489
|
columnStart = performance.now()
|
|
@@ -1144,6 +1491,7 @@ function processColumnTick() {
|
|
|
1144
1491
|
const t0 = start
|
|
1145
1492
|
try {
|
|
1146
1493
|
const chunksToUse = collectChunksForColumn(x, z)
|
|
1494
|
+
usedLightColumns = new Set(chunksToUse.map(entry => rawCacheKey(entry.x, entry.z)))
|
|
1147
1495
|
chunkCount = chunksToUse.length
|
|
1148
1496
|
const caches = packetCaches()
|
|
1149
1497
|
worldColumns3x3 = countWorldColumns3x3(x, z, (nx, nz) => world.getColumn(nx, nz))
|
|
@@ -1199,7 +1547,7 @@ function processColumnTick() {
|
|
|
1199
1547
|
)
|
|
1200
1548
|
if (wasmResult) columnMeshPath = 'v17_fused'
|
|
1201
1549
|
} else if (v16Entry) {
|
|
1202
|
-
const v16Light = updateLightV16Cache.get(rawCacheKey(x, z))
|
|
1550
|
+
const v16Light = displayCachedLight(updateLightV16Cache.get(rawCacheKey(x, z)))
|
|
1203
1551
|
const bitMapLoHi = new Uint32Array([v16Entry.bitMap >>> 0, 0])
|
|
1204
1552
|
wasmResult = meshColumnFromParsedV16V17(
|
|
1205
1553
|
v16Entry.chunkData,
|
|
@@ -1254,25 +1602,53 @@ function processColumnTick() {
|
|
|
1254
1602
|
const cs = performance.now()
|
|
1255
1603
|
const rawEntry = rawMapChunkCache.get(rawCacheKey(cx, cz))
|
|
1256
1604
|
const v17Entry = parsedV17Cache.get(rawCacheKey(cx, cz))
|
|
1257
|
-
const v17Light =
|
|
1605
|
+
const v17Light = updateLightV17Cache.get(rawCacheKey(cx, cz))
|
|
1258
1606
|
const v16Entry = parsedV16Cache.get(rawCacheKey(cx, cz))
|
|
1259
1607
|
const v16Light = updateLightV16Cache.get(rawCacheKey(cx, cz))
|
|
1260
1608
|
|
|
1261
1609
|
let conv: ChunkConversionResult | null = null
|
|
1262
1610
|
let hit = false
|
|
1263
1611
|
|
|
1264
|
-
// WASM fast paths — parse is already fast (2.19× over JS)
|
|
1265
|
-
//
|
|
1266
|
-
//
|
|
1267
|
-
//
|
|
1268
|
-
//
|
|
1269
|
-
// would render as
|
|
1270
|
-
|
|
1271
|
-
|
|
1612
|
+
// WASM fast paths — parse is already fast (2.19× over JS). Cache
|
|
1613
|
+
// for the rest of this tick so 9 dirty columns share neighbour
|
|
1614
|
+
// parses. Bypass getOrConvertColumn so the conversion cache only
|
|
1615
|
+
// holds JS-fallback results. When a WASM helper returns null
|
|
1616
|
+
// (unsupported protocol, parser error, …) we MUST fall through
|
|
1617
|
+
// to the JS column walk — otherwise the column would render as
|
|
1618
|
+
// empty geometry.
|
|
1619
|
+
const tickKey = rawCacheKey(cx, cz)
|
|
1620
|
+
if (chunk) {
|
|
1621
|
+
const cached = getOrConvertColumn(
|
|
1622
|
+
cx,
|
|
1623
|
+
cz,
|
|
1624
|
+
chunk,
|
|
1625
|
+
version,
|
|
1626
|
+
worldMinY,
|
|
1627
|
+
worldMaxY,
|
|
1628
|
+
() =>
|
|
1629
|
+
(rawEntry && convertRawMapChunkToWasm(rawEntry, version)) ||
|
|
1630
|
+
(v17Entry && convertParsedV17ToWasm(v17Entry, undefined, version)) ||
|
|
1631
|
+
(v16Entry && convertParsedV16ToWasm(v16Entry, undefined, version)) ||
|
|
1632
|
+
convertChunkToWasm(chunk, version, cx, cz, worldMinY, worldMaxY),
|
|
1633
|
+
chunk
|
|
1634
|
+
)
|
|
1635
|
+
conv = cached.result
|
|
1636
|
+
hit = cached.hit
|
|
1637
|
+
} else if (rawEntry) {
|
|
1638
|
+
const cached = getOrConvertTickWasmParse(tickKey, () => convertRawMapChunkToWasm(rawEntry, version))
|
|
1639
|
+
conv = cached.result
|
|
1640
|
+
if (cached.hit) preWasmParseHits++
|
|
1641
|
+
else preWasmParseMisses++
|
|
1272
1642
|
} else if (v17Entry) {
|
|
1273
|
-
|
|
1643
|
+
const cached = getOrConvertTickWasmParse(tickKey, () => convertParsedV17ToWasm(v17Entry, v17Light, version))
|
|
1644
|
+
conv = cached.result
|
|
1645
|
+
if (cached.hit) preWasmParseHits++
|
|
1646
|
+
else preWasmParseMisses++
|
|
1274
1647
|
} else if (v16Entry) {
|
|
1275
|
-
|
|
1648
|
+
const cached = getOrConvertTickWasmParse(tickKey, () => convertParsedV16ToWasm(v16Entry, v16Light, version))
|
|
1649
|
+
conv = cached.result
|
|
1650
|
+
if (cached.hit) preWasmParseHits++
|
|
1651
|
+
else preWasmParseMisses++
|
|
1276
1652
|
}
|
|
1277
1653
|
|
|
1278
1654
|
if (!conv) {
|
|
@@ -1304,9 +1680,15 @@ function processColumnTick() {
|
|
|
1304
1680
|
preNeighborConvert += ce - cs
|
|
1305
1681
|
preNeighborCount++
|
|
1306
1682
|
}
|
|
1683
|
+
const display = v17Light ?? v16Light
|
|
1684
|
+
if (display) {
|
|
1685
|
+
overlayDisplayLight(display, 'block', conv.blockLight, 0)
|
|
1686
|
+
overlayDisplayLight(display, 'sky', conv.skyLight, config?.skyLight ?? 15)
|
|
1687
|
+
}
|
|
1307
1688
|
conversions.push(conv)
|
|
1308
1689
|
meshChunks.push({ x: cx, z: cz, chunk })
|
|
1309
1690
|
}
|
|
1691
|
+
usedLightColumns = new Set(meshChunks.map(entry => rawCacheKey(entry.x, entry.z)))
|
|
1310
1692
|
|
|
1311
1693
|
const twoStepChunkCount = conversions.length
|
|
1312
1694
|
if (twoStepChunkCount === 0) {
|
|
@@ -1461,27 +1843,39 @@ function processColumnTick() {
|
|
|
1461
1843
|
const sectionBlocksCount = entry?.blocksCount ?? 0
|
|
1462
1844
|
// Block entity metadata still needs a per-section world walk
|
|
1463
1845
|
// (signs/heads/banners), matching the legacy per-section path.
|
|
1464
|
-
const
|
|
1465
|
-
const
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1846
|
+
const topologyRevision = dirtyVersionMeta.get(key)?.topologyRevision
|
|
1847
|
+
const post = getOrComputeTopologyPost(key, topologyRevision, () => {
|
|
1848
|
+
const signs: Record<string, SignMeta> = {}
|
|
1849
|
+
const heads: Record<string, HeadMeta> = {}
|
|
1850
|
+
const banners: Record<string, BannerMeta> = {}
|
|
1851
|
+
const beTarget = { signs, heads, banners }
|
|
1852
|
+
const beOpts = { disableBlockEntityTextures: world.config.disableBlockEntityTextures }
|
|
1853
|
+
const { occludingLookup } = getBlockMeta(version)
|
|
1854
|
+
const visGraph = new VisGraph()
|
|
1855
|
+
const cursor = new Vec3(0, 0, 0)
|
|
1856
|
+
for (cursor.y = sy; cursor.y < sy + sectionHeight; cursor.y++) {
|
|
1857
|
+
for (cursor.z = sz; cursor.z < sz + 16; cursor.z++) {
|
|
1858
|
+
for (cursor.x = sx; cursor.x < sx + 16; cursor.x++) {
|
|
1859
|
+
const b = world.getBlock(cursor)
|
|
1860
|
+
if (!b) continue
|
|
1861
|
+
if (occludingLookup[b.stateId]) {
|
|
1862
|
+
visGraph.setOpaque(cursor.x - sx, cursor.y - sy, cursor.z - sz)
|
|
1863
|
+
}
|
|
1864
|
+
collectBlockEntityMetadata(b, cursor.x, cursor.y, cursor.z, beTarget, beOpts, world)
|
|
1479
1865
|
}
|
|
1480
|
-
collectBlockEntityMetadata(b, cursor.x, cursor.y, cursor.z, beTarget, beOpts, world)
|
|
1481
1866
|
}
|
|
1482
1867
|
}
|
|
1483
|
-
|
|
1484
|
-
|
|
1868
|
+
return {
|
|
1869
|
+
visibilitySet: packVisibilitySet(visGraph.resolve()),
|
|
1870
|
+
signs,
|
|
1871
|
+
heads,
|
|
1872
|
+
banners
|
|
1873
|
+
}
|
|
1874
|
+
})
|
|
1875
|
+
const signs = post.payload.signs as Record<string, SignMeta>
|
|
1876
|
+
const heads = post.payload.heads as Record<string, HeadMeta>
|
|
1877
|
+
const banners = post.payload.banners as Record<string, BannerMeta>
|
|
1878
|
+
const sectionVisibilitySet = post.payload.visibilitySet as ReturnType<typeof packVisibilitySet>
|
|
1485
1879
|
|
|
1486
1880
|
let geometry: MesherGeometryOutput
|
|
1487
1881
|
let transferable: any[] = []
|
|
@@ -1587,10 +1981,12 @@ function processColumnTick() {
|
|
|
1587
1981
|
geometry.heads = heads
|
|
1588
1982
|
geometry.banners = banners
|
|
1589
1983
|
}
|
|
1590
|
-
|
|
1984
|
+
const versionMeta = takeGeometryVersionMeta(key, sx, sz, usedLightColumns)
|
|
1985
|
+
postMessage({ type: 'geometry', key, geometry, workerIndex, ...versionMeta }, transferable)
|
|
1591
1986
|
} else if (hadError) {
|
|
1592
1987
|
const errorGeometry = makeEmptyColumnGeometry(sx, sy, sz, sectionHeight, true)
|
|
1593
|
-
|
|
1988
|
+
const versionMeta = takeGeometryVersionMeta(key, sx, sz, usedLightColumns)
|
|
1989
|
+
postMessage({ type: 'geometry', key, geometry: errorGeometry, workerIndex, ...versionMeta })
|
|
1594
1990
|
}
|
|
1595
1991
|
// No targetChunk and no error: skip geometry message (mirrors
|
|
1596
1992
|
// legacy behavior for sections whose chunk has been unloaded
|
|
@@ -1629,6 +2025,8 @@ function processColumnTick() {
|
|
|
1629
2025
|
preOther: !attributed ? preOther : 0,
|
|
1630
2026
|
preCacheHits: !attributed ? preCacheHits : 0,
|
|
1631
2027
|
preCacheMisses: !attributed ? preCacheMisses : 0,
|
|
2028
|
+
preWasmParseHits: !attributed ? preWasmParseHits : 0,
|
|
2029
|
+
preWasmParseMisses: !attributed ? preWasmParseMisses : 0,
|
|
1632
2030
|
chunkCount: !attributed ? chunkCount : 0,
|
|
1633
2031
|
worldColumns3x3: !attributed ? worldColumns3x3 : 0,
|
|
1634
2032
|
parsedCache3x3: !attributed ? parsedCache3x3 : 0,
|
|
@@ -1637,25 +2035,37 @@ function processColumnTick() {
|
|
|
1637
2035
|
attributed = true
|
|
1638
2036
|
}
|
|
1639
2037
|
}
|
|
2038
|
+
if (groupIndex < groups.length - 1) {
|
|
2039
|
+
await yieldToIncomingMessages()
|
|
2040
|
+
if (dirtyHasUrgent()) {
|
|
2041
|
+
requeueColumnGroups(groups.slice(groupIndex + 1), selectedUrgent)
|
|
2042
|
+
break
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
if (isClientLightTraceEnabled() && isClientLightTraceArmed()) {
|
|
2047
|
+
postClientLightTrace(postTrace, {
|
|
2048
|
+
phase: 'mesherEnd',
|
|
2049
|
+
queueDepth: 0,
|
|
2050
|
+
workerIndex
|
|
2051
|
+
})
|
|
1640
2052
|
}
|
|
1641
2053
|
}
|
|
1642
2054
|
|
|
1643
|
-
|
|
2055
|
+
const meshTickScheduler = createMeshTickScheduler(async () => {
|
|
1644
2056
|
if (!allDataReady) return
|
|
1645
|
-
|
|
1646
|
-
// Ensure WASM is initialized
|
|
1647
2057
|
if (!wasmInitialized) {
|
|
1648
2058
|
await initWasm()
|
|
1649
|
-
if (!wasmInitialized) return
|
|
2059
|
+
if (!wasmInitialized) return
|
|
1650
2060
|
}
|
|
1651
|
-
|
|
1652
2061
|
if (dirtySections.size === 0) return
|
|
1653
|
-
|
|
1654
2062
|
try {
|
|
1655
|
-
processColumnTick()
|
|
2063
|
+
await processColumnTick()
|
|
1656
2064
|
} catch (err) {
|
|
1657
2065
|
console.error('[WASM Mesher] processColumnTick failed:', err)
|
|
1658
|
-
// Swallow to avoid breaking the setInterval; individual columns
|
|
1659
|
-
// already have their own try/catch.
|
|
1660
2066
|
}
|
|
1661
|
-
}
|
|
2067
|
+
})
|
|
2068
|
+
|
|
2069
|
+
setInterval(() => {
|
|
2070
|
+
meshTickScheduler.kick()
|
|
2071
|
+
}, BULK_MESH_INTERVAL_MS)
|