minecraft-renderer 0.1.107 → 0.1.108
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 +80 -80
- package/dist/minecraft-renderer.js.meta.json +1 -1
- package/dist/threeWorker.js +513 -513
- 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/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/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
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { createEmptyLightCache, isLightSectionPresent, worldSectionMaskBit, type ParsedUpdateLight } from './mesherWasmLightMerge'
|
|
4
|
+
import {
|
|
5
|
+
applyOwnerPublicationToColumnCaches,
|
|
6
|
+
applyPackedOwnerSectionsToLightCache,
|
|
7
|
+
applyRawLightPacketToCaches,
|
|
8
|
+
detachDisplayOnOwnerTakeover,
|
|
9
|
+
ownerDeltaSectionWorldYs,
|
|
10
|
+
revertDisplayToIncoming
|
|
11
|
+
} from './mesherWasmOwnerLight'
|
|
12
|
+
import { packUnpackedLightSection } from '../../mesher-shared/lightNibblePack'
|
|
13
|
+
|
|
14
|
+
describe('applyPackedOwnerSectionsToLightCache', () => {
|
|
15
|
+
it('unpacks owner packed channels into the worker column cache without touching other sections', () => {
|
|
16
|
+
const cache = createEmptyLightCache(16)
|
|
17
|
+
const unpacked = new Uint8Array(4096).fill(0)
|
|
18
|
+
unpacked[0] = 9
|
|
19
|
+
const packed = packUnpackedLightSection(unpacked)
|
|
20
|
+
const next = applyPackedOwnerSectionsToLightCache(cache, [{ sx: 0, sy: 64, sz: 0, blockLight: packed, skyLight: packed }], 0, 0, 0, 16)
|
|
21
|
+
expect(next.blockLight[4 * 4096]).toBe(9)
|
|
22
|
+
expect(next.skyLight[4 * 4096]).toBe(9)
|
|
23
|
+
expect(isLightSectionPresent(next.blockPresent, worldSectionMaskBit(4))).toBe(true)
|
|
24
|
+
expect(next.blockLight[0]).toBe(0)
|
|
25
|
+
expect(isLightSectionPresent(next.blockPresent, worldSectionMaskBit(0))).toBe(false)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('writes a section delta in place without cloning the full column arrays', () => {
|
|
29
|
+
const cache = createEmptyLightCache(16)
|
|
30
|
+
cache.blockLight[0] = 4
|
|
31
|
+
cache.skyLight[0] = 7
|
|
32
|
+
const blockRef = cache.blockLight
|
|
33
|
+
const skyRef = cache.skyLight
|
|
34
|
+
const unpacked = new Uint8Array(4096).fill(0)
|
|
35
|
+
unpacked[0] = 9
|
|
36
|
+
const packed = packUnpackedLightSection(unpacked)
|
|
37
|
+
const next = applyPackedOwnerSectionsToLightCache(cache, [{ sx: 0, sy: 64, sz: 0, blockLight: packed, skyLight: packed }], 0, 0, 0, 16)
|
|
38
|
+
expect(next.blockLight).toBe(blockRef)
|
|
39
|
+
expect(next.skyLight).toBe(skyRef)
|
|
40
|
+
expect(next.blockLight[0]).toBe(4)
|
|
41
|
+
expect(next.skyLight[0]).toBe(7)
|
|
42
|
+
expect(next.blockLight[4 * 4096]).toBe(9)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('lists only the published section Y origins for a column', () => {
|
|
46
|
+
expect(
|
|
47
|
+
ownerDeltaSectionWorldYs(
|
|
48
|
+
[
|
|
49
|
+
{ sx: 0, sy: 64, sz: 0 },
|
|
50
|
+
{ sx: 16, sy: 64, sz: 0 },
|
|
51
|
+
{ sx: 0, sy: 80, sz: 0 }
|
|
52
|
+
],
|
|
53
|
+
0,
|
|
54
|
+
0
|
|
55
|
+
)
|
|
56
|
+
).toEqual([64, 80])
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe('applyRawLightPacketToCaches', () => {
|
|
61
|
+
function parsedBlock(sectionIndex: number, value: number): ParsedUpdateLight {
|
|
62
|
+
const blockLight = new Uint8Array(16 * 4096)
|
|
63
|
+
blockLight.fill(value, sectionIndex * 4096, (sectionIndex + 1) * 4096)
|
|
64
|
+
const blockLightMask = new Uint32Array(2)
|
|
65
|
+
blockLightMask[0] = 1 << (sectionIndex + 1)
|
|
66
|
+
return {
|
|
67
|
+
x: 0,
|
|
68
|
+
z: 0,
|
|
69
|
+
trustEdges: true,
|
|
70
|
+
numSections: 16,
|
|
71
|
+
skyLight: new Uint8Array(16 * 4096),
|
|
72
|
+
blockLight,
|
|
73
|
+
skyLightMask: new Uint32Array(2),
|
|
74
|
+
emptySkyLightMask: new Uint32Array(2),
|
|
75
|
+
blockLightMask,
|
|
76
|
+
emptyBlockLightMask: new Uint32Array(2)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
it('writes incoming server light into the display cache when the owner does not own the column', () => {
|
|
81
|
+
const result = applyRawLightPacketToCaches({
|
|
82
|
+
ownerOwnsColumn: false,
|
|
83
|
+
incoming: undefined,
|
|
84
|
+
display: undefined,
|
|
85
|
+
parsed: parsedBlock(4, 9)
|
|
86
|
+
})
|
|
87
|
+
expect(result.dirtyDisplay).toBe(true)
|
|
88
|
+
expect(result.display?.blockLight[4 * 4096]).toBe(9)
|
|
89
|
+
expect(result.incoming.blockLight[4 * 4096]).toBe(9)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('keeps incoming server light separate and does not overwrite owner display cache', () => {
|
|
93
|
+
const display = createEmptyLightCache(16)
|
|
94
|
+
display.blockLight[4 * 4096] = 3
|
|
95
|
+
const result = applyRawLightPacketToCaches({
|
|
96
|
+
ownerOwnsColumn: true,
|
|
97
|
+
incoming: undefined,
|
|
98
|
+
display,
|
|
99
|
+
parsed: parsedBlock(4, 9)
|
|
100
|
+
})
|
|
101
|
+
expect(result.dirtyDisplay).toBe(false)
|
|
102
|
+
expect(result.display?.blockLight[4 * 4096]).toBe(3)
|
|
103
|
+
expect(result.incoming.blockLight[4 * 4096]).toBe(9)
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
describe('owner takeover incoming/display split', () => {
|
|
108
|
+
it('clones display once so an in-place owner write does not change incoming server light', () => {
|
|
109
|
+
const incoming = createEmptyLightCache(16)
|
|
110
|
+
incoming.blockLight[4 * 4096] = 9
|
|
111
|
+
const aliased = detachDisplayOnOwnerTakeover({ incoming, display: incoming })
|
|
112
|
+
expect(aliased.display).not.toBe(aliased.incoming)
|
|
113
|
+
|
|
114
|
+
const unpacked = new Uint8Array(4096).fill(0)
|
|
115
|
+
unpacked[0] = 3
|
|
116
|
+
const packed = packUnpackedLightSection(unpacked)
|
|
117
|
+
const applied = applyOwnerPublicationToColumnCaches({
|
|
118
|
+
incoming: aliased.incoming,
|
|
119
|
+
display: aliased.display,
|
|
120
|
+
sections: [{ sx: 0, sy: 64, sz: 0, blockLight: packed }],
|
|
121
|
+
worldMinY: 0,
|
|
122
|
+
columnWorldX: 0,
|
|
123
|
+
columnWorldZ: 0,
|
|
124
|
+
numSections: 16
|
|
125
|
+
})
|
|
126
|
+
expect(applied.incoming?.blockLight[4 * 4096]).toBe(9)
|
|
127
|
+
expect(applied.display.blockLight[4 * 4096]).toBe(3)
|
|
128
|
+
expect(applied.display.blockLight).not.toBe(applied.incoming?.blockLight)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('does not clone the full column on a second owner delta', () => {
|
|
132
|
+
const incoming = createEmptyLightCache(16)
|
|
133
|
+
incoming.blockLight[4 * 4096] = 9
|
|
134
|
+
const first = detachDisplayOnOwnerTakeover({ incoming, display: incoming })
|
|
135
|
+
const displayRef = first.display!.blockLight
|
|
136
|
+
const unpacked = new Uint8Array(4096).fill(0)
|
|
137
|
+
unpacked[0] = 3
|
|
138
|
+
const packed = packUnpackedLightSection(unpacked)
|
|
139
|
+
const second = applyOwnerPublicationToColumnCaches({
|
|
140
|
+
incoming: first.incoming,
|
|
141
|
+
display: first.display,
|
|
142
|
+
sections: [{ sx: 0, sy: 64, sz: 0, blockLight: packed }],
|
|
143
|
+
worldMinY: 0,
|
|
144
|
+
columnWorldX: 0,
|
|
145
|
+
columnWorldZ: 0,
|
|
146
|
+
numSections: 16
|
|
147
|
+
})
|
|
148
|
+
expect(second.display.blockLight).toBe(displayRef)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('reverts display to last server incoming without zeroing omitted sections', () => {
|
|
152
|
+
const incoming = createEmptyLightCache(16)
|
|
153
|
+
incoming.blockLight[4 * 4096] = 9
|
|
154
|
+
const display = createEmptyLightCache(16)
|
|
155
|
+
display.blockLight[4 * 4096] = 3
|
|
156
|
+
const reverted = revertDisplayToIncoming({ incoming, display })
|
|
157
|
+
expect(reverted.display?.blockLight[4 * 4096]).toBe(9)
|
|
158
|
+
expect(reverted.incoming?.blockLight[4 * 4096]).toBe(9)
|
|
159
|
+
expect(isLightSectionPresent(reverted.incoming!.blockPresent, worldSectionMaskBit(0))).toBe(false)
|
|
160
|
+
})
|
|
161
|
+
})
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import {
|
|
3
|
+
cloneLightCache,
|
|
4
|
+
createEmptyLightCache,
|
|
5
|
+
maskBitSet,
|
|
6
|
+
mergeUpdateLight,
|
|
7
|
+
worldSectionMaskBit,
|
|
8
|
+
type ParsedUpdateLight,
|
|
9
|
+
type UpdateLightColumnCache
|
|
10
|
+
} from './mesherWasmLightMerge'
|
|
11
|
+
import { unpackPackedLightSection } from '../../mesher-shared/lightNibblePack'
|
|
12
|
+
|
|
13
|
+
export type OwnerPackedSection = {
|
|
14
|
+
sx: number
|
|
15
|
+
sy: number
|
|
16
|
+
sz: number
|
|
17
|
+
blockLight: Uint8Array
|
|
18
|
+
skyLight?: Uint8Array
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** World-origin Y of published sections that belong to this column. */
|
|
22
|
+
export function ownerDeltaSectionWorldYs(sections: Array<{ sx: number; sy: number; sz: number }>, columnWorldX: number, columnWorldZ: number): number[] {
|
|
23
|
+
const colSx = Math.floor(columnWorldX / 16) * 16
|
|
24
|
+
const colSz = Math.floor(columnWorldZ / 16) * 16
|
|
25
|
+
const ys: number[] = []
|
|
26
|
+
const seen = new Set<number>()
|
|
27
|
+
for (const section of sections) {
|
|
28
|
+
if (section.sx !== colSx || section.sz !== colSz) continue
|
|
29
|
+
if (seen.has(section.sy)) continue
|
|
30
|
+
seen.add(section.sy)
|
|
31
|
+
ys.push(section.sy)
|
|
32
|
+
}
|
|
33
|
+
return ys
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Write owner packed 2048-byte channels into the worker's unpacked column cache. */
|
|
37
|
+
export function applyPackedOwnerSectionsToLightCache(
|
|
38
|
+
cache: UpdateLightColumnCache | undefined,
|
|
39
|
+
sections: OwnerPackedSection[],
|
|
40
|
+
worldMinY: number,
|
|
41
|
+
columnWorldX: number,
|
|
42
|
+
columnWorldZ: number,
|
|
43
|
+
numSections: number
|
|
44
|
+
): UpdateLightColumnCache {
|
|
45
|
+
const next = cache ?? createEmptyLightCache(numSections)
|
|
46
|
+
const colSx = Math.floor(columnWorldX / 16) * 16
|
|
47
|
+
const colSz = Math.floor(columnWorldZ / 16) * 16
|
|
48
|
+
for (const section of sections) {
|
|
49
|
+
if (section.sx !== colSx || section.sz !== colSz) continue
|
|
50
|
+
const sectionIndex = Math.floor((section.sy - worldMinY) / 16)
|
|
51
|
+
if (sectionIndex < 0 || sectionIndex >= next.numSections) continue
|
|
52
|
+
const start = sectionIndex * 4096
|
|
53
|
+
const block = unpackPackedLightSection(section.blockLight)
|
|
54
|
+
next.blockLight.set(block, start)
|
|
55
|
+
maskBitSet(next.blockPresent, worldSectionMaskBit(sectionIndex))
|
|
56
|
+
if (section.skyLight) {
|
|
57
|
+
const sky = unpackPackedLightSection(section.skyLight)
|
|
58
|
+
next.skyLight.set(sky, start)
|
|
59
|
+
maskBitSet(next.skyPresent, worldSectionMaskBit(sectionIndex))
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return next
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function applyRawLightPacketToCaches(opts: {
|
|
66
|
+
ownerOwnsColumn: boolean
|
|
67
|
+
incoming: UpdateLightColumnCache | undefined
|
|
68
|
+
display: UpdateLightColumnCache | undefined
|
|
69
|
+
parsed: ParsedUpdateLight
|
|
70
|
+
}): { incoming: UpdateLightColumnCache; display: UpdateLightColumnCache | undefined; dirtyDisplay: boolean } {
|
|
71
|
+
const incoming = mergeUpdateLight(opts.incoming, opts.parsed)
|
|
72
|
+
if (opts.ownerOwnsColumn) {
|
|
73
|
+
return { incoming, display: opts.display, dirtyDisplay: false }
|
|
74
|
+
}
|
|
75
|
+
return { incoming, display: incoming, dirtyDisplay: true }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** First owner takeover: clone incoming onto display once. Later deltas write display in place. */
|
|
79
|
+
export function detachDisplayOnOwnerTakeover(opts: { incoming: UpdateLightColumnCache | undefined; display: UpdateLightColumnCache | undefined }): {
|
|
80
|
+
incoming: UpdateLightColumnCache | undefined
|
|
81
|
+
display: UpdateLightColumnCache | undefined
|
|
82
|
+
} {
|
|
83
|
+
if (!opts.incoming) return { incoming: undefined, display: opts.display }
|
|
84
|
+
if (opts.display == null || opts.display === opts.incoming) {
|
|
85
|
+
return { incoming: opts.incoming, display: cloneLightCache(opts.incoming) }
|
|
86
|
+
}
|
|
87
|
+
return { incoming: opts.incoming, display: opts.display }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function applyOwnerPublicationToColumnCaches(opts: {
|
|
91
|
+
incoming: UpdateLightColumnCache | undefined
|
|
92
|
+
display: UpdateLightColumnCache | undefined
|
|
93
|
+
sections: OwnerPackedSection[]
|
|
94
|
+
worldMinY: number
|
|
95
|
+
columnWorldX: number
|
|
96
|
+
columnWorldZ: number
|
|
97
|
+
numSections: number
|
|
98
|
+
}): { incoming: UpdateLightColumnCache | undefined; display: UpdateLightColumnCache } {
|
|
99
|
+
const detached = detachDisplayOnOwnerTakeover({ incoming: opts.incoming, display: opts.display })
|
|
100
|
+
return {
|
|
101
|
+
incoming: detached.incoming,
|
|
102
|
+
display: applyPackedOwnerSectionsToLightCache(detached.display, opts.sections, opts.worldMinY, opts.columnWorldX, opts.columnWorldZ, opts.numSections)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function revertDisplayToIncoming(opts: { incoming: UpdateLightColumnCache | undefined; display: UpdateLightColumnCache | undefined }): {
|
|
107
|
+
incoming: UpdateLightColumnCache | undefined
|
|
108
|
+
display: UpdateLightColumnCache | undefined
|
|
109
|
+
} {
|
|
110
|
+
if (!opts.incoming) return { incoming: undefined, display: opts.display }
|
|
111
|
+
return { incoming: opts.incoming, display: cloneLightCache(opts.incoming) }
|
|
112
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Tick-scoped cache for WASM column parses in the two-step mesh path.
|
|
4
|
+
*
|
|
5
|
+
* `getOrConvertColumn` only covers the JS walk. The three WASM converters
|
|
6
|
+
* (`convertRawMapChunkToWasm` / `convertParsedV17ToWasm` / `convertParsedV16ToWasm`)
|
|
7
|
+
* used to re-parse the same neighbour for every dirty target column in a tick.
|
|
8
|
+
* This map lives for one `processColumnTick` and is dropped in `finally`.
|
|
9
|
+
*
|
|
10
|
+
* Same contract as `getOrConvertColumn`: cached values are `set()` sources /
|
|
11
|
+
* WASM inputs only. Consumers must not mutate them, so we do not copy.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
let cache: Map<string, unknown> | null = null
|
|
15
|
+
let hits = 0
|
|
16
|
+
let misses = 0
|
|
17
|
+
|
|
18
|
+
export function beginTickWasmParseCache(): void {
|
|
19
|
+
cache = new Map()
|
|
20
|
+
hits = 0
|
|
21
|
+
misses = 0
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function endTickWasmParseCache(): void {
|
|
25
|
+
cache = null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getTickWasmParseStats(): { hits: number; misses: number } {
|
|
29
|
+
return { hits, misses }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getOrConvertTickWasmParse<T>(key: string, convert: () => T | null): { result: T | null; hit: boolean } {
|
|
33
|
+
if (!cache) {
|
|
34
|
+
misses++
|
|
35
|
+
return { result: convert(), hit: false }
|
|
36
|
+
}
|
|
37
|
+
if (cache.has(key)) {
|
|
38
|
+
hits++
|
|
39
|
+
return { result: cache.get(key) as T, hit: true }
|
|
40
|
+
}
|
|
41
|
+
const result = convert()
|
|
42
|
+
misses++
|
|
43
|
+
if (result != null) cache.set(key, result)
|
|
44
|
+
return { result, hit: false }
|
|
45
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Mesh ticks. Bulk work is polled on an interval. An urgent dirty kicks the
|
|
4
|
+
* same runner on the next turn, and a kick during an in-progress tick reruns
|
|
5
|
+
* after that tick yields instead of waiting for the next poll.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const BULK_MESH_INTERVAL_MS = 50
|
|
9
|
+
|
|
10
|
+
export function createMeshTickScheduler(run: () => Promise<void> | void) {
|
|
11
|
+
let running = false
|
|
12
|
+
let rerun = false
|
|
13
|
+
let timer: ReturnType<typeof setTimeout> | null = null
|
|
14
|
+
|
|
15
|
+
const pump = async () => {
|
|
16
|
+
timer = null
|
|
17
|
+
if (running) {
|
|
18
|
+
rerun = true
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
running = true
|
|
22
|
+
try {
|
|
23
|
+
do {
|
|
24
|
+
rerun = false
|
|
25
|
+
await run()
|
|
26
|
+
} while (rerun)
|
|
27
|
+
} finally {
|
|
28
|
+
running = false
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
kick() {
|
|
34
|
+
if (running) {
|
|
35
|
+
rerun = true
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
if (timer != null) return
|
|
39
|
+
timer = setTimeout(() => {
|
|
40
|
+
void pump()
|
|
41
|
+
}, 0)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Topology-keyed cache for the per-section visGraph + block-entity post-pass.
|
|
4
|
+
*
|
|
5
|
+
* Wave-2 covering remeshes carry the same `topologyRevision` as wave-1, so the
|
|
6
|
+
* 4096-cell walk can be reused. A new revision always recomputes. Legacy /
|
|
7
|
+
* unversioned jobs (no revision) never cache.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const TOPOLOGY_POST_CACHE_LIMIT = 1024
|
|
11
|
+
|
|
12
|
+
export type TopologyPostPayload = {
|
|
13
|
+
visibilitySet: unknown
|
|
14
|
+
signs: Record<string, unknown>
|
|
15
|
+
heads: Record<string, unknown>
|
|
16
|
+
banners: Record<string, unknown>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type CacheEntry = {
|
|
20
|
+
topologyRevision: number
|
|
21
|
+
payload: TopologyPostPayload
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const cache = new Map<string, CacheEntry>()
|
|
25
|
+
|
|
26
|
+
export function getOrComputeTopologyPost<T extends TopologyPostPayload>(
|
|
27
|
+
key: string,
|
|
28
|
+
topologyRevision: number | undefined,
|
|
29
|
+
compute: () => T
|
|
30
|
+
): { payload: T; hit: boolean } {
|
|
31
|
+
if (topologyRevision == null) {
|
|
32
|
+
return { payload: compute(), hit: false }
|
|
33
|
+
}
|
|
34
|
+
const existing = cache.get(key)
|
|
35
|
+
if (existing && existing.topologyRevision === topologyRevision) {
|
|
36
|
+
cache.delete(key)
|
|
37
|
+
cache.set(key, existing)
|
|
38
|
+
return { payload: existing.payload as T, hit: true }
|
|
39
|
+
}
|
|
40
|
+
const payload = compute()
|
|
41
|
+
cache.set(key, { topologyRevision, payload })
|
|
42
|
+
evictIfNeeded()
|
|
43
|
+
return { payload, hit: false }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function clearTopologyPostCacheColumn(x: number, z: number): void {
|
|
47
|
+
for (const key of [...cache.keys()]) {
|
|
48
|
+
const [sx, , sz] = key.split(',').map(Number)
|
|
49
|
+
if (sx === x && sz === z) cache.delete(key)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function clearTopologyPostCache(): void {
|
|
54
|
+
cache.clear()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function evictIfNeeded(): void {
|
|
58
|
+
while (cache.size > TOPOLOGY_POST_CACHE_LIMIT) {
|
|
59
|
+
const oldest = cache.keys().next().value
|
|
60
|
+
if (oldest == null) break
|
|
61
|
+
cache.delete(oldest)
|
|
62
|
+
}
|
|
63
|
+
}
|
package/src/worldView/types.ts
CHANGED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { WorldView } from './worldView'
|
|
4
|
+
|
|
5
|
+
const column = () => ({ toJson: () => '{}', minY: 0, worldHeight: 256 })
|
|
6
|
+
function setup(getColumnAt = vi.fn(async () => column())) {
|
|
7
|
+
const view = new WorldView({ getColumnAt, setBlockStateId: vi.fn() }, 4)
|
|
8
|
+
view.loadedChunks = { '0,0': true, '16,0': true }
|
|
9
|
+
return { view, getColumnAt }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe('geometry-only column reload', () => {
|
|
13
|
+
it('retains membership and requests light preservation', async () => {
|
|
14
|
+
const { view } = setup()
|
|
15
|
+
const unload = vi.fn()
|
|
16
|
+
const load = vi.fn()
|
|
17
|
+
view.on('unloadChunk', unload)
|
|
18
|
+
view.on('loadChunk', load)
|
|
19
|
+
await view.reloadColumnGeometry()
|
|
20
|
+
expect(view.loadedChunks).toEqual({ '0,0': true, '16,0': true })
|
|
21
|
+
expect(unload.mock.calls.map(([data]) => data)).toEqual([
|
|
22
|
+
{ x: 0, z: 0, preserveClientLight: true },
|
|
23
|
+
{ x: 16, z: 0, preserveClientLight: true }
|
|
24
|
+
])
|
|
25
|
+
expect(load).toHaveBeenCalledTimes(2)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('does not resurrect a column unloaded during an asynchronous fetch', async () => {
|
|
29
|
+
let resolve!: (value: ReturnType<typeof column>) => void
|
|
30
|
+
const pending = new Promise<ReturnType<typeof column>>(r => {
|
|
31
|
+
resolve = r
|
|
32
|
+
})
|
|
33
|
+
const { view } = setup(vi.fn(() => pending))
|
|
34
|
+
view.loadedChunks = { '0,0': true }
|
|
35
|
+
const load = vi.fn()
|
|
36
|
+
view.on('loadChunk', load)
|
|
37
|
+
const reload = view.reloadColumnGeometry()
|
|
38
|
+
expect(view.reloadColumnGeometry()).toBe(reload)
|
|
39
|
+
view.unloadChunk({ x: 0, z: 0 })
|
|
40
|
+
resolve(column())
|
|
41
|
+
await reload
|
|
42
|
+
expect(load).not.toHaveBeenCalled()
|
|
43
|
+
expect(view.loadedChunks['0,0']).toBeUndefined()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('cleans up failed columns and continues loading the rest', async () => {
|
|
47
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
48
|
+
try {
|
|
49
|
+
const get = vi.fn().mockRejectedValueOnce(new Error('fetch failed')).mockResolvedValue(column())
|
|
50
|
+
const { view } = setup(get)
|
|
51
|
+
const load = vi.fn()
|
|
52
|
+
view.on('loadChunk', load)
|
|
53
|
+
await view.reloadColumnGeometry()
|
|
54
|
+
expect(view.loadedChunks).toEqual({ '16,0': true })
|
|
55
|
+
expect(load).toHaveBeenCalledTimes(1)
|
|
56
|
+
expect(warn).toHaveBeenCalledOnce()
|
|
57
|
+
} finally {
|
|
58
|
+
warn.mockRestore()
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('truly unloads columns outside the current range or missing from the world', async () => {
|
|
63
|
+
const { view } = setup(vi.fn(async () => null) as any)
|
|
64
|
+
view.loadedChunks['160,0'] = true
|
|
65
|
+
await view.reloadColumnGeometry()
|
|
66
|
+
expect(view.loadedChunks).toEqual({})
|
|
67
|
+
})
|
|
68
|
+
})
|
|
@@ -11,7 +11,7 @@ describe('WorldView._loadChunks spiral guard', () => {
|
|
|
11
11
|
}
|
|
12
12
|
const view = new WorldView(world, 8, new Vec3(0, 64, 0))
|
|
13
13
|
view.addWaitTime = 0
|
|
14
|
-
view.loadChunk = vi.fn(async () =>
|
|
14
|
+
view.loadChunk = vi.fn(async () => true)
|
|
15
15
|
|
|
16
16
|
const positions = [new Vec3(0, 0, 0)]
|
|
17
17
|
const spiralA = view._loadChunks(positions, new Vec3(0, 64, 0))
|
|
@@ -115,6 +115,8 @@ export class WorldView extends (EventEmitter as new () => TypedEmitter<WorldView
|
|
|
115
115
|
gotPanicLastTime = false
|
|
116
116
|
panicChunksReload = () => {}
|
|
117
117
|
loadedChunks: Record<ChunkPosKey, boolean> = {}
|
|
118
|
+
private columnUnloadEpoch = new Map<string, number>()
|
|
119
|
+
private geometryReload?: Promise<void>
|
|
118
120
|
inLoading = false
|
|
119
121
|
chunkReceiveTimes: number[] = []
|
|
120
122
|
lastChunkReceiveTime = 0
|
|
@@ -276,7 +278,9 @@ export class WorldView extends (EventEmitter as new () => TypedEmitter<WorldView
|
|
|
276
278
|
/**
|
|
277
279
|
* Load a chunk at the given position.
|
|
278
280
|
*/
|
|
279
|
-
async loadChunk(pos: { x: number; z: number; y?: number }, isLightUpdate = false, reason = 'spiral'): Promise<
|
|
281
|
+
async loadChunk(pos: { x: number; z: number; y?: number }, isLightUpdate = false, reason = 'spiral'): Promise<boolean> {
|
|
282
|
+
const key = `${pos.x},${pos.z}`
|
|
283
|
+
const epoch = this.columnUnloadEpoch.get(key) ?? 0
|
|
280
284
|
const [botX, botZ] = chunkPos(this.lastPos)
|
|
281
285
|
const dx = Math.abs(botX - Math.floor(pos.x / 16))
|
|
282
286
|
const dz = Math.abs(botZ - Math.floor(pos.z / 16))
|
|
@@ -284,6 +288,14 @@ export class WorldView extends (EventEmitter as new () => TypedEmitter<WorldView
|
|
|
284
288
|
if (dx <= this.viewDistance && dz <= this.viewDistance) {
|
|
285
289
|
const column = await this.world.getColumnAt(pos.y !== undefined ? (pos as Vec3) : new Vec3(pos.x, 0, pos.z))
|
|
286
290
|
|
|
291
|
+
// An asynchronous fetch must not resurrect a truly unloaded column.
|
|
292
|
+
const [currentX, currentZ] = chunkPos(this.lastPos)
|
|
293
|
+
if (
|
|
294
|
+
(this.columnUnloadEpoch.get(key) ?? 0) !== epoch ||
|
|
295
|
+
Math.abs(currentX - Math.floor(pos.x / 16)) > this.viewDistance ||
|
|
296
|
+
Math.abs(currentZ - Math.floor(pos.z / 16)) > this.viewDistance
|
|
297
|
+
)
|
|
298
|
+
return false
|
|
287
299
|
if (column) {
|
|
288
300
|
const chunk = column.toJson()
|
|
289
301
|
const worldConfig: WorldSizeParams = {
|
|
@@ -307,10 +319,41 @@ export class WorldView extends (EventEmitter as new () => TypedEmitter<WorldView
|
|
|
307
319
|
reason,
|
|
308
320
|
time: Date.now()
|
|
309
321
|
})
|
|
322
|
+
return true
|
|
310
323
|
} else if (this.isPlayground) {
|
|
311
324
|
this.emit('markAsLoaded', { x: pos.x, z: pos.z })
|
|
312
325
|
}
|
|
313
326
|
}
|
|
327
|
+
return false
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
reloadColumnGeometry(): Promise<void> {
|
|
331
|
+
this.geometryReload ??= this.runColumnGeometryReload().finally(() => {
|
|
332
|
+
this.geometryReload = undefined
|
|
333
|
+
})
|
|
334
|
+
return this.geometryReload
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
private async runColumnGeometryReload(): Promise<void> {
|
|
338
|
+
const coords = Object.keys(this.loadedChunks)
|
|
339
|
+
.filter(key => this.loadedChunks[key])
|
|
340
|
+
.map(key => key.split(',').map(Number) as [number, number])
|
|
341
|
+
coords.sort(([ax, az], [bx, bz]) => (ax - this.lastPos.x) ** 2 + (az - this.lastPos.z) ** 2 - (bx - this.lastPos.x) ** 2 - (bz - this.lastPos.z) ** 2)
|
|
342
|
+
for (const [x, z] of coords) this.emit('unloadChunk', { x, z, preserveClientLight: true })
|
|
343
|
+
for (const [x, z] of coords) {
|
|
344
|
+
const key = `${x},${z}`
|
|
345
|
+
if (!this.loadedChunks[key]) continue
|
|
346
|
+
const epoch = this.columnUnloadEpoch.get(key) ?? 0
|
|
347
|
+
let reloaded = false
|
|
348
|
+
try {
|
|
349
|
+
reloaded = await this.loadChunk({ x, z }, false, 'geometry-reload')
|
|
350
|
+
} catch (error) {
|
|
351
|
+
console.warn(`Failed to reload column geometry ${key}`, error)
|
|
352
|
+
}
|
|
353
|
+
if (!reloaded && this.loadedChunks[key] && (this.columnUnloadEpoch.get(key) ?? 0) === epoch) {
|
|
354
|
+
this.unloadChunk({ x, z })
|
|
355
|
+
}
|
|
356
|
+
}
|
|
314
357
|
}
|
|
315
358
|
|
|
316
359
|
/**
|
|
@@ -338,6 +381,8 @@ export class WorldView extends (EventEmitter as new () => TypedEmitter<WorldView
|
|
|
338
381
|
* Unload a specific chunk.
|
|
339
382
|
*/
|
|
340
383
|
unloadChunk(pos: { x: number; z: number }): void {
|
|
384
|
+
const key = `${pos.x},${pos.z}`
|
|
385
|
+
this.columnUnloadEpoch.set(key, (this.columnUnloadEpoch.get(key) ?? 0) + 1)
|
|
341
386
|
this.emit('unloadChunk', { x: pos.x, z: pos.z })
|
|
342
387
|
delete this.loadedChunks[`${pos.x},${pos.z}`]
|
|
343
388
|
delete this.debugChunksInfo[`${pos.x},${pos.z}`]
|