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,83 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { describe, expect, test } from 'vitest'
|
|
3
|
+
import { provenMeshLightVersion } from '../worker/mesherLightDependencies'
|
|
4
|
+
|
|
5
|
+
function prove(sx: number, sz: number, used: string[], applied: Array<[string, number]>) {
|
|
6
|
+
return provenMeshLightVersion({
|
|
7
|
+
sx,
|
|
8
|
+
sz,
|
|
9
|
+
usedColumns: new Set(used),
|
|
10
|
+
appliedVersionByColumn: new Map(applied)
|
|
11
|
+
})
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('provenMeshLightVersion', () => {
|
|
15
|
+
test('a bootstrap neighbor wall takes the published column version when that column was meshed', () => {
|
|
16
|
+
expect(prove(16, 0, ['0,0', '16,0'], [['0,0', 10]])).toBe(10)
|
|
17
|
+
expect(prove(0, 16, ['0,0', '0,16'], [['0,0', 8]])).toBe(8)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('negative chunk coordinates use the same column keys', () => {
|
|
21
|
+
expect(prove(-16, -32, ['-32,-32', '-16,-32'], [['-32,-32', 7]])).toBe(7)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('an unused published column is not proof, even if a dirty job requested that version', () => {
|
|
25
|
+
expect(prove(16, 0, ['16,0'], [['0,0', 10]])).toBe(0)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('an older own-column version does not hide a newer neighbor publication that was meshed', () => {
|
|
29
|
+
expect(
|
|
30
|
+
prove(
|
|
31
|
+
16,
|
|
32
|
+
0,
|
|
33
|
+
['0,0', '16,0'],
|
|
34
|
+
[
|
|
35
|
+
['16,0', 4],
|
|
36
|
+
['0,0', 10]
|
|
37
|
+
]
|
|
38
|
+
)
|
|
39
|
+
).toBe(10)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('a later own publication does not cover an earlier neighbor that was left out of the mesh', () => {
|
|
43
|
+
expect(
|
|
44
|
+
prove(
|
|
45
|
+
16,
|
|
46
|
+
0,
|
|
47
|
+
['16,0'],
|
|
48
|
+
[
|
|
49
|
+
['0,0', 10],
|
|
50
|
+
['16,0', 12]
|
|
51
|
+
]
|
|
52
|
+
)
|
|
53
|
+
).toBe(0)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('a used column older than the missing neighbor still counts', () => {
|
|
57
|
+
expect(
|
|
58
|
+
prove(
|
|
59
|
+
16,
|
|
60
|
+
0,
|
|
61
|
+
['16,0'],
|
|
62
|
+
[
|
|
63
|
+
['0,0', 10],
|
|
64
|
+
['16,0', 4]
|
|
65
|
+
]
|
|
66
|
+
)
|
|
67
|
+
).toBe(4)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('a far column publication does not raise the stamp', () => {
|
|
71
|
+
expect(prove(0, 0, ['0,0', '100,100'], [['100,100', 100]])).toBe(0)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('forgetting a column drops it from later proofs', () => {
|
|
75
|
+
const applied = new Map<string, number>([
|
|
76
|
+
['0,0', 10],
|
|
77
|
+
['16,0', 10]
|
|
78
|
+
])
|
|
79
|
+
applied.delete('0,0')
|
|
80
|
+
expect(prove(16, 0, ['16,0'], [...applied.entries()])).toBe(10)
|
|
81
|
+
expect(prove(0, 0, ['0,0'], [...applied.entries()])).toBe(0)
|
|
82
|
+
})
|
|
83
|
+
})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { describe, expect, test } from 'vitest'
|
|
3
|
+
import { BULK_COLUMNS_PER_TICK, selectColumnGroupsForTick } from '../worker/mesherWasmColumnTickSelect'
|
|
4
|
+
|
|
5
|
+
function dirtyOf(keys: string[]): Map<string, number> {
|
|
6
|
+
const dirty = new Map<string, number>()
|
|
7
|
+
for (const key of keys) dirty.set(key, 1)
|
|
8
|
+
return dirty
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe('selectColumnGroupsForTick', () => {
|
|
12
|
+
test('urgent columns are selected before bulk columns', () => {
|
|
13
|
+
const dirty = dirtyOf(['160,64,0', '0,64,0', '32,64,0'])
|
|
14
|
+
const urgent = new Set(['0,64,0'])
|
|
15
|
+
const { selected, remaining } = selectColumnGroupsForTick(dirty, urgent, 4)
|
|
16
|
+
expect(selected.map(group => `${group.x},${group.z}`)).toEqual(['0,0', '160,0', '32,0'])
|
|
17
|
+
expect(remaining.size).toBe(0)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('a tick leaves leftover bulk columns for the next tick', () => {
|
|
21
|
+
const keys = ['0,64,0', '16,64,0', '32,64,0', '48,64,0', '64,64,0', '80,64,0']
|
|
22
|
+
const dirty = dirtyOf(keys)
|
|
23
|
+
const first = selectColumnGroupsForTick(dirty, new Set(), BULK_COLUMNS_PER_TICK)
|
|
24
|
+
expect(first.selected).toHaveLength(4)
|
|
25
|
+
expect([...first.remaining.keys()]).toEqual(['64,64,0', '80,64,0'])
|
|
26
|
+
const second = selectColumnGroupsForTick(first.remaining, new Set(), BULK_COLUMNS_PER_TICK)
|
|
27
|
+
expect(second.selected.map(group => `${group.x},${group.z}`)).toEqual(['64,0', '80,0'])
|
|
28
|
+
expect(second.remaining.size).toBe(0)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('urgent still wins when the same key also has bulk dirties', () => {
|
|
32
|
+
const dirty = dirtyOf(['16,64,0', '0,64,0', '32,64,0', '48,64,0', '64,64,0', '80,64,0', '96,64,0'])
|
|
33
|
+
dirty.set('0,64,0', 2)
|
|
34
|
+
const { selected, remaining } = selectColumnGroupsForTick(dirty, new Set(['0,64,0']), 4)
|
|
35
|
+
expect(selected[0]).toMatchObject({ x: 0, z: 0 })
|
|
36
|
+
expect(selected).toHaveLength(5)
|
|
37
|
+
expect([...remaining.keys()]).toEqual(['80,64,0', '96,64,0'])
|
|
38
|
+
})
|
|
39
|
+
})
|
|
@@ -5,7 +5,8 @@ import {
|
|
|
5
5
|
_peekConversionCache,
|
|
6
6
|
clearConversionCache,
|
|
7
7
|
getOrConvertColumn,
|
|
8
|
-
invalidateConversion
|
|
8
|
+
invalidateConversion,
|
|
9
|
+
patchConversionBlock
|
|
9
10
|
} from '../worker/mesherWasmConversionCache'
|
|
10
11
|
import type { ChunkConversionResult } from '../bridge/convertChunk'
|
|
11
12
|
|
|
@@ -27,6 +28,34 @@ describe('mesherWasmConversionCache', () => {
|
|
|
27
28
|
clearConversionCache()
|
|
28
29
|
})
|
|
29
30
|
|
|
31
|
+
test('patches state and count in place and rejects identity drift', () => {
|
|
32
|
+
const ref = {}
|
|
33
|
+
const result = { ...makeResult(0), blockStates: new Uint16Array(4096) }
|
|
34
|
+
getOrConvertColumn(-16, 0, ref, 'v', -64, -48, () => result, ref)
|
|
35
|
+
expect(patchConversionBlock(-16, 0, ref, -1, -49, 15, 2)).toBe(true)
|
|
36
|
+
expect(result.blockStates[4095]).toBe(2)
|
|
37
|
+
expect(result.blockCount).toBe(1)
|
|
38
|
+
patchConversionBlock(-16, 0, ref, -1, -49, 15, 3)
|
|
39
|
+
expect(result.blockCount).toBe(1)
|
|
40
|
+
expect(patchConversionBlock(-16, 0, {}, -1, -49, 15, 0)).toBe(false)
|
|
41
|
+
patchConversionBlock(-16, 0, ref, -1, -49, 15, 0)
|
|
42
|
+
expect(result.blockCount).toBe(0)
|
|
43
|
+
expect(
|
|
44
|
+
getOrConvertColumn(
|
|
45
|
+
-16,
|
|
46
|
+
0,
|
|
47
|
+
ref,
|
|
48
|
+
'v',
|
|
49
|
+
-64,
|
|
50
|
+
-48,
|
|
51
|
+
() => {
|
|
52
|
+
throw new Error('unexpected conversion')
|
|
53
|
+
},
|
|
54
|
+
ref
|
|
55
|
+
).result
|
|
56
|
+
).toBe(result)
|
|
57
|
+
})
|
|
58
|
+
|
|
30
59
|
test('miss then hit on same chunk ref returns cached result', () => {
|
|
31
60
|
const ref = { id: 'chunk-a' }
|
|
32
61
|
let calls = 0
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { afterEach, describe, expect, test } from 'vitest'
|
|
3
|
+
import { beginTickWasmParseCache, endTickWasmParseCache, getOrConvertTickWasmParse, getTickWasmParseStats } from '../worker/mesherWasmTickParseCache'
|
|
4
|
+
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
endTickWasmParseCache()
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
describe('mesherWasmTickParseCache', () => {
|
|
10
|
+
test('second convert of the same column in one tick is a hit without copying', () => {
|
|
11
|
+
beginTickWasmParseCache()
|
|
12
|
+
let calls = 0
|
|
13
|
+
const convert = () => {
|
|
14
|
+
calls++
|
|
15
|
+
return { tag: calls }
|
|
16
|
+
}
|
|
17
|
+
const a = getOrConvertTickWasmParse('0,0', convert)
|
|
18
|
+
const b = getOrConvertTickWasmParse('0,0', convert)
|
|
19
|
+
expect(a.hit).toBe(false)
|
|
20
|
+
expect(b.hit).toBe(true)
|
|
21
|
+
expect(calls).toBe(1)
|
|
22
|
+
expect(b.result).toBe(a.result)
|
|
23
|
+
expect(getTickWasmParseStats()).toEqual({ hits: 1, misses: 1 })
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('distinct columns miss independently and unique-column count matches the cache', () => {
|
|
27
|
+
beginTickWasmParseCache()
|
|
28
|
+
let calls = 0
|
|
29
|
+
const convert = () => {
|
|
30
|
+
calls++
|
|
31
|
+
return { tag: calls }
|
|
32
|
+
}
|
|
33
|
+
for (let i = 0; i < 9; i++) getOrConvertTickWasmParse(`${i * 16},0`, convert)
|
|
34
|
+
for (let i = 0; i < 9; i++) getOrConvertTickWasmParse(`${i * 16},0`, convert)
|
|
35
|
+
expect(calls).toBe(9)
|
|
36
|
+
expect(getTickWasmParseStats()).toEqual({ hits: 9, misses: 9 })
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('null results are not cached so a later convert can recover', () => {
|
|
40
|
+
beginTickWasmParseCache()
|
|
41
|
+
let calls = 0
|
|
42
|
+
const a = getOrConvertTickWasmParse('16,16', () => {
|
|
43
|
+
calls++
|
|
44
|
+
return null
|
|
45
|
+
})
|
|
46
|
+
const b = getOrConvertTickWasmParse('16,16', () => {
|
|
47
|
+
calls++
|
|
48
|
+
return { tag: 1 }
|
|
49
|
+
})
|
|
50
|
+
expect(a.result).toBeNull()
|
|
51
|
+
expect(a.hit).toBe(false)
|
|
52
|
+
expect(b.hit).toBe(false)
|
|
53
|
+
expect(b.result).toEqual({ tag: 1 })
|
|
54
|
+
expect(calls).toBe(2)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('end of tick drops entries so the next tick misses', () => {
|
|
58
|
+
beginTickWasmParseCache()
|
|
59
|
+
let calls = 0
|
|
60
|
+
getOrConvertTickWasmParse('0,0', () => {
|
|
61
|
+
calls++
|
|
62
|
+
return { tag: 1 }
|
|
63
|
+
})
|
|
64
|
+
endTickWasmParseCache()
|
|
65
|
+
beginTickWasmParseCache()
|
|
66
|
+
getOrConvertTickWasmParse('0,0', () => {
|
|
67
|
+
calls++
|
|
68
|
+
return { tag: 2 }
|
|
69
|
+
})
|
|
70
|
+
expect(calls).toBe(2)
|
|
71
|
+
expect(getTickWasmParseStats()).toEqual({ hits: 0, misses: 1 })
|
|
72
|
+
})
|
|
73
|
+
})
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { describe, expect, test, vi } from 'vitest'
|
|
3
|
+
import { BULK_MESH_INTERVAL_MS, createMeshTickScheduler } from '../worker/mesherWasmTickSchedule'
|
|
4
|
+
|
|
5
|
+
describe('createMeshTickScheduler', () => {
|
|
6
|
+
test('an urgent kick runs on the next turn, before the bulk poll interval', async () => {
|
|
7
|
+
vi.useFakeTimers()
|
|
8
|
+
try {
|
|
9
|
+
let runs = 0
|
|
10
|
+
const scheduler = createMeshTickScheduler(() => {
|
|
11
|
+
runs++
|
|
12
|
+
})
|
|
13
|
+
scheduler.kick()
|
|
14
|
+
expect(runs).toBe(0)
|
|
15
|
+
await vi.advanceTimersByTimeAsync(0)
|
|
16
|
+
expect(runs).toBe(1)
|
|
17
|
+
expect(BULK_MESH_INTERVAL_MS).toBeGreaterThan(0)
|
|
18
|
+
} finally {
|
|
19
|
+
vi.clearAllTimers()
|
|
20
|
+
vi.useRealTimers()
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('a kick during a running tick runs again after that tick finishes', async () => {
|
|
25
|
+
vi.useFakeTimers()
|
|
26
|
+
try {
|
|
27
|
+
let release: () => void = () => {}
|
|
28
|
+
const gate = new Promise<void>(resolve => {
|
|
29
|
+
release = resolve
|
|
30
|
+
})
|
|
31
|
+
let runs = 0
|
|
32
|
+
const scheduler = createMeshTickScheduler(() => {
|
|
33
|
+
runs++
|
|
34
|
+
if (runs === 1) {
|
|
35
|
+
scheduler.kick()
|
|
36
|
+
return gate
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
scheduler.kick()
|
|
40
|
+
await vi.advanceTimersByTimeAsync(0)
|
|
41
|
+
expect(runs).toBe(1)
|
|
42
|
+
release()
|
|
43
|
+
await gate
|
|
44
|
+
await Promise.resolve()
|
|
45
|
+
expect(runs).toBe(2)
|
|
46
|
+
} finally {
|
|
47
|
+
vi.clearAllTimers()
|
|
48
|
+
vi.useRealTimers()
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
})
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { afterEach, describe, expect, test } from 'vitest'
|
|
3
|
+
import {
|
|
4
|
+
TOPOLOGY_POST_CACHE_LIMIT,
|
|
5
|
+
clearTopologyPostCache,
|
|
6
|
+
clearTopologyPostCacheColumn,
|
|
7
|
+
getOrComputeTopologyPost
|
|
8
|
+
} from '../worker/mesherWasmTopologyPostCache'
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
clearTopologyPostCache()
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
describe('mesherWasmTopologyPostCache', () => {
|
|
15
|
+
test('second pass with the same revision does not walk', () => {
|
|
16
|
+
let walks = 0
|
|
17
|
+
const compute = () => {
|
|
18
|
+
walks++
|
|
19
|
+
return { visibilitySet: 1, signs: { a: walks }, heads: {}, banners: {} }
|
|
20
|
+
}
|
|
21
|
+
const first = getOrComputeTopologyPost('0,64,0', 4, compute)
|
|
22
|
+
const second = getOrComputeTopologyPost('0,64,0', 4, compute)
|
|
23
|
+
expect(walks).toBe(1)
|
|
24
|
+
expect(first.hit).toBe(false)
|
|
25
|
+
expect(second.hit).toBe(true)
|
|
26
|
+
expect(second.payload).toBe(first.payload)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('revision change walks again', () => {
|
|
30
|
+
let walks = 0
|
|
31
|
+
const compute = () => {
|
|
32
|
+
walks++
|
|
33
|
+
return { visibilitySet: walks, signs: {}, heads: {}, banners: {} }
|
|
34
|
+
}
|
|
35
|
+
getOrComputeTopologyPost('0,64,0', 4, compute)
|
|
36
|
+
const next = getOrComputeTopologyPost('0,64,0', 5, compute)
|
|
37
|
+
expect(walks).toBe(2)
|
|
38
|
+
expect(next.hit).toBe(false)
|
|
39
|
+
expect(next.payload.visibilitySet).toBe(2)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('missing revision always walks and does not cache', () => {
|
|
43
|
+
let walks = 0
|
|
44
|
+
const compute = () => {
|
|
45
|
+
walks++
|
|
46
|
+
return { visibilitySet: walks, signs: {}, heads: {}, banners: {} }
|
|
47
|
+
}
|
|
48
|
+
getOrComputeTopologyPost('0,64,0', undefined, compute)
|
|
49
|
+
getOrComputeTopologyPost('0,64,0', undefined, compute)
|
|
50
|
+
expect(walks).toBe(2)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('unload of a column clears its cached sections', () => {
|
|
54
|
+
let walks = 0
|
|
55
|
+
const compute = () => {
|
|
56
|
+
walks++
|
|
57
|
+
return { visibilitySet: walks, signs: {}, heads: {}, banners: {} }
|
|
58
|
+
}
|
|
59
|
+
getOrComputeTopologyPost('0,64,0', 3, compute)
|
|
60
|
+
getOrComputeTopologyPost('16,64,0', 3, compute)
|
|
61
|
+
clearTopologyPostCacheColumn(0, 0)
|
|
62
|
+
getOrComputeTopologyPost('0,64,0', 3, compute)
|
|
63
|
+
getOrComputeTopologyPost('16,64,0', 3, compute)
|
|
64
|
+
expect(walks).toBe(3)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('evicts oldest entries past the cap', () => {
|
|
68
|
+
for (let i = 0; i < TOPOLOGY_POST_CACHE_LIMIT + 3; i++) {
|
|
69
|
+
getOrComputeTopologyPost(`${i},64,0`, 1, () => ({ visibilitySet: i, signs: {}, heads: {}, banners: {} }))
|
|
70
|
+
}
|
|
71
|
+
let walks = 0
|
|
72
|
+
getOrComputeTopologyPost('0,64,0', 1, () => {
|
|
73
|
+
walks++
|
|
74
|
+
return { visibilitySet: -1, signs: {}, heads: {}, banners: {} }
|
|
75
|
+
})
|
|
76
|
+
expect(walks).toBe(1)
|
|
77
|
+
})
|
|
78
|
+
})
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { describe, it, expect } from 'vitest'
|
|
3
|
+
import { applyOwnerPublicationToColumnCaches } from './mesherWasmOwnerLight'
|
|
4
|
+
import { displayLightColumn } from './mesherWasmLightMerge'
|
|
5
|
+
|
|
6
|
+
describe('dark publication omission diagnostic (current behavior)', () => {
|
|
7
|
+
const blockOnly = { sx: 0, sy: 0, sz: 0, blockLight: new Uint8Array(2048) }
|
|
8
|
+
const base = { worldMinY: 0, columnWorldX: 0, columnWorldZ: 0, numSections: 2 }
|
|
9
|
+
it('displays sky 15 when initial publication omits sky and no incoming light exists', () => {
|
|
10
|
+
const result = applyOwnerPublicationToColumnCaches({ ...base, incoming: undefined, display: undefined, sections: [blockOnly] })
|
|
11
|
+
expect(
|
|
12
|
+
displayLightColumn(result.display, 'sky')
|
|
13
|
+
.subarray(0, 4096)
|
|
14
|
+
.every(value => value === 15)
|
|
15
|
+
).toBe(true)
|
|
16
|
+
})
|
|
17
|
+
it('retains dark sky when an incoming authoritative cache exists', () => {
|
|
18
|
+
const initial = applyOwnerPublicationToColumnCaches({
|
|
19
|
+
...base,
|
|
20
|
+
incoming: undefined,
|
|
21
|
+
display: undefined,
|
|
22
|
+
sections: [{ ...blockOnly, skyLight: new Uint8Array(2048) }]
|
|
23
|
+
}).display
|
|
24
|
+
const result = applyOwnerPublicationToColumnCaches({ ...base, incoming: initial, display: initial, sections: [blockOnly] })
|
|
25
|
+
expect(
|
|
26
|
+
displayLightColumn(result.display, 'sky')
|
|
27
|
+
.subarray(0, 4096)
|
|
28
|
+
.every(value => value === 0)
|
|
29
|
+
).toBe(true)
|
|
30
|
+
expect(result.display).not.toBe(initial)
|
|
31
|
+
})
|
|
32
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Light-version stamp for one section mesh.
|
|
4
|
+
*
|
|
5
|
+
* Each column keeps only the latest applied owner version. The stamp is the
|
|
6
|
+
* newest of those versions whose sampled columns were actually meshed, and it
|
|
7
|
+
* stops before the first sampled column that was written but left out. The
|
|
8
|
+
* version requested on the dirty job is not an input.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export function sectionLightSampleColumns(sx: number, sz: number): string[] {
|
|
12
|
+
const columns: string[] = []
|
|
13
|
+
for (let dx = -16; dx <= 16; dx += 16) {
|
|
14
|
+
for (let dz = -16; dz <= 16; dz += 16) {
|
|
15
|
+
columns.push(`${sx + dx},${sz + dz}`)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return columns
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function provenMeshLightVersion(opts: {
|
|
22
|
+
sx: number
|
|
23
|
+
sz: number
|
|
24
|
+
usedColumns: ReadonlySet<string>
|
|
25
|
+
appliedVersionByColumn: ReadonlyMap<string, number>
|
|
26
|
+
}): number {
|
|
27
|
+
let gap = Number.POSITIVE_INFINITY
|
|
28
|
+
let covered = 0
|
|
29
|
+
for (const column of sectionLightSampleColumns(opts.sx, opts.sz)) {
|
|
30
|
+
const applied = opts.appliedVersionByColumn.get(column)
|
|
31
|
+
if (applied == null) continue
|
|
32
|
+
if (!opts.usedColumns.has(column)) gap = Math.min(gap, applied)
|
|
33
|
+
else covered = Math.max(covered, applied)
|
|
34
|
+
}
|
|
35
|
+
if (!Number.isFinite(gap)) return covered
|
|
36
|
+
let belowGap = 0
|
|
37
|
+
for (const column of sectionLightSampleColumns(opts.sx, opts.sz)) {
|
|
38
|
+
const applied = opts.appliedVersionByColumn.get(column)
|
|
39
|
+
if (applied == null || !opts.usedColumns.has(column) || applied >= gap) continue
|
|
40
|
+
belowGap = Math.max(belowGap, applied)
|
|
41
|
+
}
|
|
42
|
+
return belowGap
|
|
43
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import MinecraftData from 'minecraft-data'
|
|
3
|
+
import Chunks from 'prismarine-chunk'
|
|
4
|
+
import { Vec3 } from 'vec3'
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
6
|
+
import { packUnpackedLightSection } from '../../mesher-shared/lightNibblePack'
|
|
7
|
+
import { LIGHT_BLOCKS_PER_SECTION, maskBitSet, worldSectionMaskBit, type ParsedUpdateLight } from './mesherWasmLightMerge'
|
|
8
|
+
|
|
9
|
+
// Drives the real worker message handler. The legacy mesh path reads light
|
|
10
|
+
// through `World.getChannelLightNorm`, so that is what these tests observe.
|
|
11
|
+
|
|
12
|
+
const { parseUpdateLightV17 } = vi.hoisted(() => ({ parseUpdateLightV17: vi.fn() }))
|
|
13
|
+
|
|
14
|
+
vi.mock('../runtime-build/wasm_mesher.js', () => ({
|
|
15
|
+
default: vi.fn(async () => undefined),
|
|
16
|
+
parseUpdateLightV17
|
|
17
|
+
}))
|
|
18
|
+
vi.mock('../../mesher-shared/models', async importOriginal => ({
|
|
19
|
+
...(await importOriginal<object>()),
|
|
20
|
+
setBlockStatesData: vi.fn()
|
|
21
|
+
}))
|
|
22
|
+
vi.mock('./mesherWasmTickSchedule', () => ({
|
|
23
|
+
BULK_MESH_INTERVAL_MS: 50,
|
|
24
|
+
createMeshTickScheduler: () => ({ kick: vi.fn() })
|
|
25
|
+
}))
|
|
26
|
+
|
|
27
|
+
const VERSION = '1.17.1'
|
|
28
|
+
const NUM_SECTIONS = 16
|
|
29
|
+
const COLUMN_X = 16
|
|
30
|
+
const COLUMN_Z = 32
|
|
31
|
+
const PUBLISHED_Y = 64
|
|
32
|
+
const UNPUBLISHED_Y = 80
|
|
33
|
+
const PUBLISHED_POS = new Vec3(COLUMN_X + 3, PUBLISHED_Y + 1, COLUMN_Z + 5)
|
|
34
|
+
const UNPUBLISHED_POS = new Vec3(COLUMN_X + 3, UNPUBLISHED_Y + 5, COLUMN_Z + 5)
|
|
35
|
+
const INCOMING = {
|
|
36
|
+
[PUBLISHED_Y]: { sky: 3, block: 1 },
|
|
37
|
+
[UNPUBLISHED_Y]: { sky: 9, block: 4 }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type WorkerSelf = { onmessage: (event: { data: unknown }) => void }
|
|
41
|
+
type WorldLike = {
|
|
42
|
+
getColumn: (
|
|
43
|
+
x: number,
|
|
44
|
+
z: number
|
|
45
|
+
) => {
|
|
46
|
+
setSkyLight: (pos: Vec3, light: number) => void
|
|
47
|
+
setBlockLight: (pos: Vec3, light: number) => void
|
|
48
|
+
skyLightSections: Array<unknown | null>
|
|
49
|
+
blockLightSections: Array<unknown | null>
|
|
50
|
+
}
|
|
51
|
+
getChannelLightNorm: (pos: Vec3) => { block: number; sky: number }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const send = (data: unknown) => (globalThis.self as unknown as WorkerSelf).onmessage({ data })
|
|
55
|
+
const world = () => (globalThis as unknown as { world: WorldLike }).world
|
|
56
|
+
const lightAt = (pos: Vec3) => world().getChannelLightNorm(pos)
|
|
57
|
+
const expectedLight = ({ sky, block }: { sky: number; block: number }) => ({
|
|
58
|
+
sky: Math.min(15, sky + 2) / 15,
|
|
59
|
+
block: Math.min(15, block + 2) / 15
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
function serverColumnJson() {
|
|
63
|
+
const Chunk = Chunks(VERSION) as any
|
|
64
|
+
const chunk = new Chunk(undefined as any)
|
|
65
|
+
const stone = MinecraftData(VERSION).blocksByName.stone!.defaultState
|
|
66
|
+
chunk.setBlockStateId(new Vec3(3, PUBLISHED_POS.y, 5), stone)
|
|
67
|
+
chunk.setBlockStateId(new Vec3(3, UNPUBLISHED_POS.y, 5), stone)
|
|
68
|
+
return chunk.toJson()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function serverLightPacket(): ParsedUpdateLight {
|
|
72
|
+
const columnBytes = NUM_SECTIONS * LIGHT_BLOCKS_PER_SECTION
|
|
73
|
+
const skyLight = new Uint8Array(columnBytes)
|
|
74
|
+
const blockLight = new Uint8Array(columnBytes)
|
|
75
|
+
const skyLightMask = new Uint32Array(2)
|
|
76
|
+
const blockLightMask = new Uint32Array(2)
|
|
77
|
+
for (const [sectionY, light] of Object.entries(INCOMING)) {
|
|
78
|
+
const sectionIndex = Number(sectionY) / 16
|
|
79
|
+
const start = sectionIndex * LIGHT_BLOCKS_PER_SECTION
|
|
80
|
+
skyLight.fill(light.sky, start, start + LIGHT_BLOCKS_PER_SECTION)
|
|
81
|
+
blockLight.fill(light.block, start, start + LIGHT_BLOCKS_PER_SECTION)
|
|
82
|
+
maskBitSet(skyLightMask, worldSectionMaskBit(sectionIndex))
|
|
83
|
+
maskBitSet(blockLightMask, worldSectionMaskBit(sectionIndex))
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
x: COLUMN_X / 16,
|
|
87
|
+
z: COLUMN_Z / 16,
|
|
88
|
+
trustEdges: true,
|
|
89
|
+
numSections: NUM_SECTIONS,
|
|
90
|
+
skyLight,
|
|
91
|
+
blockLight,
|
|
92
|
+
skyLightMask,
|
|
93
|
+
blockLightMask,
|
|
94
|
+
emptySkyLightMask: new Uint32Array(2),
|
|
95
|
+
emptyBlockLightMask: new Uint32Array(2)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function publishOwnerLight(publicationVersion: number, sectionY: number, light: { sky: number; block: number }) {
|
|
100
|
+
const packed = (value: number) => packUnpackedLightSection(new Uint8Array(LIGHT_BLOCKS_PER_SECTION).fill(value))
|
|
101
|
+
send({
|
|
102
|
+
type: 'applyOwnerLightPublication',
|
|
103
|
+
worldGeneration: 1,
|
|
104
|
+
publicationVersion,
|
|
105
|
+
sections: [{ sx: COLUMN_X, sy: sectionY, sz: COLUMN_Z, blockLight: packed(light.block), skyLight: packed(light.sky) }]
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Moves the JS column away from the caches, so a sync of this section is detectable. */
|
|
110
|
+
function markUnpublishedSectionInJsColumn(light: { sky: number; block: number }) {
|
|
111
|
+
const column = world().getColumn(COLUMN_X, COLUMN_Z)
|
|
112
|
+
const local = new Vec3(UNPUBLISHED_POS.x & 15, UNPUBLISHED_POS.y, UNPUBLISHED_POS.z & 15)
|
|
113
|
+
column.setSkyLight(local, light.sky)
|
|
114
|
+
column.setBlockLight(local, light.block)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
describe('mesher worker JS column light with the client light owner', () => {
|
|
118
|
+
beforeEach(async () => {
|
|
119
|
+
vi.resetModules()
|
|
120
|
+
parseUpdateLightV17.mockReset()
|
|
121
|
+
parseUpdateLightV17.mockReturnValue(serverLightPacket())
|
|
122
|
+
vi.stubGlobal('self', {})
|
|
123
|
+
vi.stubGlobal('postMessage', vi.fn())
|
|
124
|
+
const realSetInterval = globalThis.setInterval
|
|
125
|
+
vi.stubGlobal('setInterval', vi.fn())
|
|
126
|
+
await import('./mesherWasm')
|
|
127
|
+
vi.stubGlobal('setInterval', realSetInterval)
|
|
128
|
+
|
|
129
|
+
send({ type: 'mesherData', config: { version: VERSION, worldMinY: 0, worldMaxY: 256, enableLighting: true } })
|
|
130
|
+
send({ type: 'chunk', x: COLUMN_X, z: COLUMN_Z, chunk: serverColumnJson() })
|
|
131
|
+
send({ type: 'setUpdateLightV17', rawPacket: new Uint8Array(1), numSections: NUM_SECTIONS })
|
|
132
|
+
await vi.waitFor(() => expect(parseUpdateLightV17).toHaveBeenCalledOnce())
|
|
133
|
+
|
|
134
|
+
expect(lightAt(PUBLISHED_POS)).toEqual(expectedLight(INCOMING[PUBLISHED_Y]))
|
|
135
|
+
expect(lightAt(UNPUBLISHED_POS)).toEqual(expectedLight(INCOMING[UNPUBLISHED_Y]))
|
|
136
|
+
}, 30_000)
|
|
137
|
+
|
|
138
|
+
afterEach(() => {
|
|
139
|
+
vi.unstubAllGlobals()
|
|
140
|
+
delete (globalThis as any).world
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('keeps authoritative zero light explicit in initially null sections', () => {
|
|
144
|
+
const sectionY = 96
|
|
145
|
+
const pos = new Vec3(COLUMN_X + 3, sectionY + 1, COLUMN_Z + 5)
|
|
146
|
+
expect(lightAt(pos)).toEqual({ block: 0, sky: 1 })
|
|
147
|
+
publishOwnerLight(1, sectionY, { sky: 0, block: 0 })
|
|
148
|
+
expect(lightAt(pos)).toEqual(expectedLight({ sky: 0, block: 0 }))
|
|
149
|
+
send({ type: 'chunk', x: COLUMN_X, z: COLUMN_Z, chunk: serverColumnJson() })
|
|
150
|
+
expect(lightAt(pos)).toEqual(expectedLight({ sky: 0, block: 0 }))
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('allocates only the authoritative channel and leaves omitted sections null', () => {
|
|
154
|
+
const sectionY = 96
|
|
155
|
+
send({
|
|
156
|
+
type: 'applyOwnerLightPublication',
|
|
157
|
+
worldGeneration: 1,
|
|
158
|
+
publicationVersion: 1,
|
|
159
|
+
sections: [{ sx: COLUMN_X, sy: sectionY, sz: COLUMN_Z, blockLight: new Uint8Array(2048) }]
|
|
160
|
+
})
|
|
161
|
+
const column = world().getColumn(COLUMN_X, COLUMN_Z)
|
|
162
|
+
expect(column.blockLightSections[sectionY / 16 + 1]).not.toBeNull()
|
|
163
|
+
expect(column.skyLightSections[sectionY / 16 + 1]).toBeNull()
|
|
164
|
+
expect(column.blockLightSections[sectionY / 16 + 2]).toBeNull()
|
|
165
|
+
expect(column.skyLightSections[sectionY / 16 + 2]).toBeNull()
|
|
166
|
+
expect(lightAt(new Vec3(COLUMN_X + 3, sectionY + 17, COLUMN_Z + 5))).toEqual({ block: 0, sky: 1 })
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('raises JS light when a publication brightens a section', () => {
|
|
170
|
+
const bright = { sky: 12, block: 7 }
|
|
171
|
+
publishOwnerLight(1, PUBLISHED_Y, bright)
|
|
172
|
+
expect(lightAt(PUBLISHED_POS)).toEqual(expectedLight(bright))
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('lowers JS light when a later publication darkens the section', () => {
|
|
176
|
+
const dark = { sky: 0, block: 0 }
|
|
177
|
+
publishOwnerLight(1, PUBLISHED_Y, { sky: 12, block: 7 })
|
|
178
|
+
publishOwnerLight(2, PUBLISHED_Y, dark)
|
|
179
|
+
expect(lightAt(PUBLISHED_POS)).toEqual(expectedLight(dark))
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('writes only the published section Ys into the JS column', () => {
|
|
183
|
+
const marker = { sky: 14, block: 13 }
|
|
184
|
+
const bright = { sky: 12, block: 7 }
|
|
185
|
+
markUnpublishedSectionInJsColumn(marker)
|
|
186
|
+
publishOwnerLight(1, PUBLISHED_Y, bright)
|
|
187
|
+
expect(lightAt(PUBLISHED_POS)).toEqual(expectedLight(bright))
|
|
188
|
+
expect(lightAt(UNPUBLISHED_POS)).toEqual(expectedLight(marker))
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('restores every incoming section in the JS column on revert', () => {
|
|
192
|
+
publishOwnerLight(1, PUBLISHED_Y, { sky: 12, block: 7 })
|
|
193
|
+
markUnpublishedSectionInJsColumn({ sky: 14, block: 13 })
|
|
194
|
+
send({ type: 'revertOwnerLightToIncoming' })
|
|
195
|
+
expect(lightAt(PUBLISHED_POS)).toEqual(expectedLight(INCOMING[PUBLISHED_Y]))
|
|
196
|
+
expect(lightAt(UNPUBLISHED_POS)).toEqual(expectedLight(INCOMING[UNPUBLISHED_Y]))
|
|
197
|
+
})
|
|
198
|
+
})
|