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
|
@@ -0,0 +1,916 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { EventEmitter } from 'events'
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
|
|
4
|
+
import { Vec3 } from 'vec3'
|
|
5
|
+
import { proxy } from 'valtio'
|
|
6
|
+
import * as worldRendererModule from './worldrendererCommon'
|
|
7
|
+
import { WorldRendererCommon } from './worldrendererCommon'
|
|
8
|
+
import { defaultWorldRendererConfig } from '../graphicsBackend/config'
|
|
9
|
+
import { defaultPerformanceInstabilityFactors } from '../performanceMonitor'
|
|
10
|
+
import { getInitialPlayerState } from '../playerState/playerState'
|
|
11
|
+
import type { DisplayWorldOptions, GraphicsInitOptions } from '../graphicsBackend/types'
|
|
12
|
+
import { LIGHT_OWNER_WORKER_SCRIPT } from '../lighting/owner/clientLightOwner'
|
|
13
|
+
import { INITIAL_TOPOLOGY_REVISION } from '../lighting/versioning/clientLightVersions'
|
|
14
|
+
import { clearTopologyPostCache, getOrComputeTopologyPost } from '../wasm-mesher/worker/mesherWasmTopologyPostCache'
|
|
15
|
+
import Chunks from 'prismarine-chunk'
|
|
16
|
+
|
|
17
|
+
vi.mock('./ui/newStats', () => ({
|
|
18
|
+
addNewStat: vi.fn(() => ({ updateText: vi.fn(), setVisibility: vi.fn() })),
|
|
19
|
+
updateStatText: vi.fn(),
|
|
20
|
+
removeAllStats: vi.fn(),
|
|
21
|
+
updatePanesVisibility: vi.fn(),
|
|
22
|
+
MC_RENDERER_DEBUG_OVERLAY_CLASS: 'mc-renderer-debug-overlay'
|
|
23
|
+
}))
|
|
24
|
+
|
|
25
|
+
vi.mock('./utils/skins', () => ({
|
|
26
|
+
setSkinsConfig: vi.fn(),
|
|
27
|
+
steveTexture: {},
|
|
28
|
+
stevePngUrl: ''
|
|
29
|
+
}))
|
|
30
|
+
|
|
31
|
+
function ensurePromiseWithResolvers() {
|
|
32
|
+
if (!Promise.withResolvers) {
|
|
33
|
+
Promise.withResolvers = function <T>() {
|
|
34
|
+
let resolve!: (value: T | PromiseLike<T>) => void
|
|
35
|
+
let reject!: (reason?: unknown) => void
|
|
36
|
+
const promise = new Promise<T>((res, rej) => {
|
|
37
|
+
resolve = res
|
|
38
|
+
reject = rej
|
|
39
|
+
})
|
|
40
|
+
return { promise, resolve, reject }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
class TestWorldRenderer extends WorldRendererCommon {
|
|
46
|
+
outputFormat = 'threeJs' as const
|
|
47
|
+
|
|
48
|
+
changeBackgroundColor() {}
|
|
49
|
+
changeCardinalLight() {}
|
|
50
|
+
handleWorkerMessage() {}
|
|
51
|
+
updateCamera() {}
|
|
52
|
+
render() {}
|
|
53
|
+
updateShowChunksBorder() {}
|
|
54
|
+
updatePlayerEntity() {}
|
|
55
|
+
worldStop() {}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const constructedScripts: string[] = []
|
|
59
|
+
|
|
60
|
+
function createRenderer(enableOwner = false, workerCount = 2, version = '1.17.1') {
|
|
61
|
+
const rendererState = proxy({
|
|
62
|
+
world: {
|
|
63
|
+
chunksLoaded: {} as Record<string, true>,
|
|
64
|
+
heightmaps: {} as Record<string, Int16Array>,
|
|
65
|
+
allChunksLoaded: false,
|
|
66
|
+
mesherWork: false,
|
|
67
|
+
instabilityFactors: defaultPerformanceInstabilityFactors(),
|
|
68
|
+
intersectMedia: null
|
|
69
|
+
},
|
|
70
|
+
renderer: '',
|
|
71
|
+
preventEscapeMenu: false
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const displayOptions: DisplayWorldOptions = {
|
|
75
|
+
version,
|
|
76
|
+
worldView: Object.assign(new EventEmitter(), { reloadLoadedChunks: vi.fn(async () => {}) }) as unknown as DisplayWorldOptions['worldView'],
|
|
77
|
+
inWorldRenderingConfig: proxy({ ...defaultWorldRendererConfig, mesherWorkers: workerCount, enableClientLightOwner: enableOwner }),
|
|
78
|
+
playerStateReactive: getInitialPlayerState(),
|
|
79
|
+
rendererState,
|
|
80
|
+
nonReactiveState: {
|
|
81
|
+
fps: 0,
|
|
82
|
+
worstRenderTime: 0,
|
|
83
|
+
avgRenderTime: 0,
|
|
84
|
+
world: {
|
|
85
|
+
chunksLoadedCount: 0,
|
|
86
|
+
chunksTotalNumber: 0,
|
|
87
|
+
chunksFullInfo: ''
|
|
88
|
+
},
|
|
89
|
+
renderer: {
|
|
90
|
+
timeline: { live: [], frozen: [], lastSecond: [] }
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
resourcesManager: {
|
|
94
|
+
currentResources: {
|
|
95
|
+
mcData: { version: {} },
|
|
96
|
+
blocksAtlasJson: {},
|
|
97
|
+
blockstatesModels: {}
|
|
98
|
+
}
|
|
99
|
+
} as DisplayWorldOptions['resourcesManager']
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const initOptions: GraphicsInitOptions = {
|
|
103
|
+
config: { sceneBackground: '#000' },
|
|
104
|
+
rendererSpecificSettings: {},
|
|
105
|
+
callbacks: {
|
|
106
|
+
displayCriticalError: vi.fn(),
|
|
107
|
+
setRendererSpecificSettings: vi.fn(),
|
|
108
|
+
fireCustomEvent: vi.fn()
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const renderer = new TestWorldRenderer(displayOptions.resourcesManager, displayOptions, initOptions)
|
|
113
|
+
renderer.active = true
|
|
114
|
+
renderer.viewDistance = 8
|
|
115
|
+
renderer.viewerChunkPosition = new Vec3(0, 64, 0)
|
|
116
|
+
renderer.worldSizeParams = { minY: 0, worldHeight: 256 }
|
|
117
|
+
return renderer
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
describe('WorldRendererCommon client light owner spawn', () => {
|
|
121
|
+
test('owner idle releases only settled edits without a covering light publication', () => {
|
|
122
|
+
const renderer = createRenderer(true)
|
|
123
|
+
const internal = renderer.lightCoordinator as any
|
|
124
|
+
internal.interactiveSections.set('0,64,0', { lightVersionAtEdit: 1, editGeneration: 2 })
|
|
125
|
+
internal.interactiveSections.set('16,64,0', { lightVersionAtEdit: 1, editGeneration: 3 })
|
|
126
|
+
internal.interactiveSections.set('32,64,0', { lightVersionAtEdit: 1, editGeneration: 2, coveringVersion: 2 })
|
|
127
|
+
internal.releaseInteractiveEditsWithoutCoveringPublication(2)
|
|
128
|
+
expect(internal.interactiveSections.has('0,64,0')).toBe(false)
|
|
129
|
+
expect(internal.interactiveSections.has('16,64,0')).toBe(true)
|
|
130
|
+
expect(internal.interactiveSections.has('32,64,0')).toBe(true)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
beforeEach(() => {
|
|
134
|
+
ensurePromiseWithResolvers()
|
|
135
|
+
constructedScripts.length = 0
|
|
136
|
+
vi.stubGlobal('location', { href: 'http://localhost/' })
|
|
137
|
+
vi.stubGlobal(
|
|
138
|
+
'Worker',
|
|
139
|
+
class MockWorker {
|
|
140
|
+
script: string
|
|
141
|
+
postMessage = vi.fn()
|
|
142
|
+
terminate = vi.fn()
|
|
143
|
+
addEventListener = vi.fn()
|
|
144
|
+
onmessage: ((event: MessageEvent) => void) | null = null
|
|
145
|
+
constructor(script: string) {
|
|
146
|
+
this.script = script
|
|
147
|
+
constructedScripts.push(script)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
)
|
|
151
|
+
vi.spyOn(worldRendererModule, 'meshersSendMcData').mockImplementation(() => {})
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
afterEach(() => {
|
|
155
|
+
vi.restoreAllMocks()
|
|
156
|
+
vi.unstubAllGlobals()
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
test('flag-off initWorkers does not spawn the light-owner worker', () => {
|
|
160
|
+
const renderer = createRenderer(false, 2)
|
|
161
|
+
renderer.initWorkers(2)
|
|
162
|
+
expect(renderer.workers).toHaveLength(2)
|
|
163
|
+
expect(constructedScripts.every(script => script !== LIGHT_OWNER_WORKER_SCRIPT)).toBe(true)
|
|
164
|
+
expect(renderer.hasClientLightOwner()).toBe(false)
|
|
165
|
+
expect(defaultWorldRendererConfig.enableClientLightOwner).toBe(false)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
test('flag-on on a non-1.17.1 session does not load the 1.17.1 tables', () => {
|
|
169
|
+
const renderer = createRenderer(true, 1, '1.16.5')
|
|
170
|
+
renderer.initWorkers(1)
|
|
171
|
+
expect(renderer.workers).toHaveLength(1)
|
|
172
|
+
expect(constructedScripts.every(script => script !== LIGHT_OWNER_WORKER_SCRIPT)).toBe(true)
|
|
173
|
+
expect(renderer.getClientLightOwnerWorker()).toBeNull()
|
|
174
|
+
expect(renderer.hasClientLightOwner()).toBe(false)
|
|
175
|
+
expect(renderer.getClientLightOwnerFailureReason()).toMatch(/1\.17\.1/)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
test('flag-on initWorkers spawns a dedicated owner, not a mesh worker', () => {
|
|
179
|
+
const renderer = createRenderer(true, 2)
|
|
180
|
+
renderer.initWorkers(2)
|
|
181
|
+
expect(renderer.workers).toHaveLength(2)
|
|
182
|
+
expect(constructedScripts.filter(script => script === LIGHT_OWNER_WORKER_SCRIPT)).toHaveLength(1)
|
|
183
|
+
expect(constructedScripts.filter(script => script !== LIGHT_OWNER_WORKER_SCRIPT)).toHaveLength(2)
|
|
184
|
+
expect(renderer.getClientLightOwnerWorker()).toBeTruthy()
|
|
185
|
+
expect(renderer.hasClientLightOwner()).toBe(false)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
test('flag-on owner is not working until the worker reports ready', () => {
|
|
189
|
+
const renderer = createRenderer(true, 1)
|
|
190
|
+
renderer.initWorkers(1)
|
|
191
|
+
expect(renderer.hasClientLightOwner()).toBe(false)
|
|
192
|
+
const owner = renderer.getClientLightOwnerWorker() as { onmessage: ((event: MessageEvent) => void) | null }
|
|
193
|
+
owner.onmessage?.({ data: { type: 'ready' } } as MessageEvent)
|
|
194
|
+
expect(renderer.hasClientLightOwner()).toBe(true)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
test('flag-on owner goes failed on worker error and is not working', () => {
|
|
198
|
+
const renderer = createRenderer(true, 1)
|
|
199
|
+
renderer.initWorkers(1)
|
|
200
|
+
const owner = renderer.getClientLightOwnerWorker() as { onmessage: ((event: MessageEvent) => void) | null }
|
|
201
|
+
owner.onmessage?.({ data: { type: 'error', error: 'NetworkError loading lightOwnerWorker.js' } } as MessageEvent)
|
|
202
|
+
expect(renderer.hasClientLightOwner()).toBe(false)
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
test('flag-off setBlock / addColumn / removeColumn do not post to an owner worker', () => {
|
|
206
|
+
const renderer = createRenderer(false, 2)
|
|
207
|
+
renderer.initWorkers(2)
|
|
208
|
+
const ownerPosts = constructedScripts.map((_, i) => i).filter(i => constructedScripts[i] === LIGHT_OWNER_WORKER_SCRIPT)
|
|
209
|
+
expect(ownerPosts).toHaveLength(0)
|
|
210
|
+
|
|
211
|
+
const meshPosts = renderer.workers.map((w: { postMessage: ReturnType<typeof vi.fn> }) => w.postMessage)
|
|
212
|
+
renderer.loadedChunks['0,0'] = true
|
|
213
|
+
renderer.setBlockStateIdInner(new Vec3(1, 64, 1), 1)
|
|
214
|
+
renderer.removeColumn(0, 0)
|
|
215
|
+
|
|
216
|
+
expect(meshPosts.some(fn => fn.mock.calls.length > 0)).toBe(true)
|
|
217
|
+
expect(renderer.hasClientLightOwner()).toBe(false)
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
test('startup light stays bulk; a later publication of an edited section stays urgent', () => {
|
|
221
|
+
const renderer = createRenderer(true, 1)
|
|
222
|
+
renderer.initWorkers(1)
|
|
223
|
+
renderer.forceCallFromMesherReplayer = true
|
|
224
|
+
renderer.neighborChunkUpdates = false
|
|
225
|
+
renderer.loadedChunks['0,0'] = true
|
|
226
|
+
renderer.loadedChunks['64,64'] = true
|
|
227
|
+
const owner = renderer.getClientLightOwnerWorker() as { onmessage: ((event: MessageEvent) => void) | null }
|
|
228
|
+
owner.onmessage?.({ data: { type: 'ready' } } as MessageEvent)
|
|
229
|
+
const mesh = renderer.workers[0] as { postMessage: ReturnType<typeof vi.fn> }
|
|
230
|
+
const publish = (version: number, sx: number, sy: number, sz: number) => {
|
|
231
|
+
owner.onmessage?.({
|
|
232
|
+
data: {
|
|
233
|
+
type: 'publication',
|
|
234
|
+
publication: {
|
|
235
|
+
worldGeneration: 1,
|
|
236
|
+
publicationVersion: version,
|
|
237
|
+
sections: [{ sx, sy, sz, blockLight: new Uint8Array(2048) }]
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
} as MessageEvent)
|
|
241
|
+
}
|
|
242
|
+
const dirties = () => mesh.postMessage.mock.calls.map(call => call[0]).filter((message: { type?: string }) => message?.type === 'dirty')
|
|
243
|
+
|
|
244
|
+
mesh.postMessage.mockClear()
|
|
245
|
+
publish(1, 0, 4, 0)
|
|
246
|
+
const startup = dirties()
|
|
247
|
+
expect(startup.length).toBeGreaterThan(0)
|
|
248
|
+
expect(startup.every((message: { urgent?: boolean }) => message.urgent !== true)).toBe(true)
|
|
249
|
+
|
|
250
|
+
mesh.postMessage.mockClear()
|
|
251
|
+
renderer.setBlockStateIdInner(new Vec3(1, 64, 1), 1)
|
|
252
|
+
const editDirties = dirties()
|
|
253
|
+
expect(editDirties.length).toBeGreaterThan(0)
|
|
254
|
+
expect(editDirties.every((message: { urgent?: boolean }) => message.urgent === true)).toBe(true)
|
|
255
|
+
|
|
256
|
+
mesh.postMessage.mockClear()
|
|
257
|
+
publish(2, 0, 4, 0)
|
|
258
|
+
const covering = dirties()
|
|
259
|
+
expect(covering.length).toBeGreaterThan(0)
|
|
260
|
+
expect(covering.every((message: { urgent?: boolean }) => message.urgent === true)).toBe(true)
|
|
261
|
+
|
|
262
|
+
mesh.postMessage.mockClear()
|
|
263
|
+
publish(3, 4, 4, 4)
|
|
264
|
+
const unrelated = dirties()
|
|
265
|
+
expect(unrelated.length).toBeGreaterThan(0)
|
|
266
|
+
expect(unrelated.every((message: { urgent?: boolean }) => message.urgent !== true)).toBe(true)
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
test('geometry at the pre-edit light version keeps the edit urgent until its own publication is committed', () => {
|
|
270
|
+
const renderer = createRenderer(true, 1)
|
|
271
|
+
renderer.initWorkers(1)
|
|
272
|
+
renderer.forceCallFromMesherReplayer = true
|
|
273
|
+
renderer.neighborChunkUpdates = false
|
|
274
|
+
renderer.loadedChunks['0,0'] = true
|
|
275
|
+
const owner = renderer.getClientLightOwnerWorker() as { onmessage: ((event: MessageEvent) => void) | null }
|
|
276
|
+
owner.onmessage?.({ data: { type: 'ready' } } as MessageEvent)
|
|
277
|
+
const mesh = renderer.workers[0] as { postMessage: ReturnType<typeof vi.fn> }
|
|
278
|
+
const publish = (version: number) => {
|
|
279
|
+
owner.onmessage?.({
|
|
280
|
+
data: {
|
|
281
|
+
type: 'publication',
|
|
282
|
+
publication: {
|
|
283
|
+
worldGeneration: 1,
|
|
284
|
+
publicationVersion: version,
|
|
285
|
+
sections: [{ sx: 0, sy: 4, sz: 0, blockLight: new Uint8Array(2048) }]
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
} as MessageEvent)
|
|
289
|
+
}
|
|
290
|
+
const dirties = () => mesh.postMessage.mock.calls.map(call => call[0]).filter((message: { type?: string }) => message?.type === 'dirty')
|
|
291
|
+
publish(5)
|
|
292
|
+
renderer.setBlockStateIdInner(new Vec3(1, 64, 1), 1)
|
|
293
|
+
const topology = (renderer as any).lightCoordinator.topologyRevisionBySection.get('0,64,0') as number
|
|
294
|
+
const geometry = {
|
|
295
|
+
type: 'geometry',
|
|
296
|
+
key: '0,64,0',
|
|
297
|
+
worldGeneration: 1,
|
|
298
|
+
lightPublicationVersion: 5,
|
|
299
|
+
sessionEpoch: 1,
|
|
300
|
+
columnIncarnation: 1,
|
|
301
|
+
requestId: 1,
|
|
302
|
+
topologyRevision: topology,
|
|
303
|
+
meshMode: 'owner' as const,
|
|
304
|
+
workerIndex: 0,
|
|
305
|
+
geometry: {}
|
|
306
|
+
}
|
|
307
|
+
const early = (renderer as any).lightCoordinator.evaluateOwnerGeometry(geometry)
|
|
308
|
+
expect(early.accepted).toBe(true)
|
|
309
|
+
expect(early.keepCovering).toBe(false)
|
|
310
|
+
renderer.handleMessage(geometry)
|
|
311
|
+
owner.onmessage?.({ data: { type: 'stepped', remaining: false, stepGeneration: 0 } } as MessageEvent)
|
|
312
|
+
mesh.postMessage.mockClear()
|
|
313
|
+
publish(6)
|
|
314
|
+
const covering = dirties()
|
|
315
|
+
expect(covering.length).toBeGreaterThan(0)
|
|
316
|
+
expect(covering.every((message: { urgent?: boolean }) => message.urgent === true)).toBe(true)
|
|
317
|
+
|
|
318
|
+
renderer.handleMessage({ ...geometry, lightPublicationVersion: 6 })
|
|
319
|
+
mesh.postMessage.mockClear()
|
|
320
|
+
publish(7)
|
|
321
|
+
const afterCommit = dirties()
|
|
322
|
+
expect(afterCommit.length).toBeGreaterThan(0)
|
|
323
|
+
expect(afterCommit.every((message: { urgent?: boolean }) => message.urgent !== true)).toBe(true)
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
test('a second urgent edit in the throttle window is dispatched immediately', () => {
|
|
327
|
+
vi.useFakeTimers()
|
|
328
|
+
try {
|
|
329
|
+
const renderer = createRenderer(true, 1)
|
|
330
|
+
renderer.initWorkers(1)
|
|
331
|
+
renderer.forceCallFromMesherReplayer = true
|
|
332
|
+
renderer.neighborChunkUpdates = false
|
|
333
|
+
renderer.loadedChunks['0,0'] = true
|
|
334
|
+
const owner = renderer.getClientLightOwnerWorker() as { onmessage: ((event: MessageEvent) => void) | null }
|
|
335
|
+
owner.onmessage?.({ data: { type: 'ready' } } as MessageEvent)
|
|
336
|
+
const mesh = renderer.workers[0] as { postMessage: ReturnType<typeof vi.fn> }
|
|
337
|
+
renderer.setBlockStateIdInner(new Vec3(1, 64, 1), 1)
|
|
338
|
+
mesh.postMessage.mockClear()
|
|
339
|
+
renderer.setBlockStateIdInner(new Vec3(2, 64, 1), 2)
|
|
340
|
+
const second = mesh.postMessage.mock.calls.map(call => call[0]).filter((message: { type?: string }) => message?.type === 'dirty')
|
|
341
|
+
expect(second.length).toBeGreaterThan(0)
|
|
342
|
+
expect(second.every((message: { urgent?: boolean }) => message.urgent === true)).toBe(true)
|
|
343
|
+
} finally {
|
|
344
|
+
vi.clearAllTimers()
|
|
345
|
+
vi.useRealTimers()
|
|
346
|
+
}
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
test('flag-on setBlock / addColumn / removeColumn / update_light forward to the owner', () => {
|
|
350
|
+
const renderer = createRenderer(true, 2)
|
|
351
|
+
renderer.initWorkers(2)
|
|
352
|
+
const owner = renderer.getClientLightOwnerWorker() as unknown as { postMessage: ReturnType<typeof vi.fn> }
|
|
353
|
+
expect(owner).toBeTruthy()
|
|
354
|
+
owner.postMessage.mockClear()
|
|
355
|
+
|
|
356
|
+
renderer.loadedChunks['0,0'] = true
|
|
357
|
+
renderer.setBlockStateIdInner(new Vec3(8, 64, 8), 2)
|
|
358
|
+
const Chunk = Chunks('1.17.1') as any
|
|
359
|
+
renderer.addColumn(0, 0, new Chunk().toJson(), false)
|
|
360
|
+
renderer.removeColumn(0, 0)
|
|
361
|
+
renderer.feedChunkPacket({
|
|
362
|
+
kind: 'setUpdateLightV17',
|
|
363
|
+
protocol: 756,
|
|
364
|
+
numSections: 16,
|
|
365
|
+
rawPacket: new Uint8Array([1, 2, 3])
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
const payloads = owner.postMessage.mock.calls.map(call => call[0])
|
|
369
|
+
expect(payloads.some(p => p?.type === 'pushEvent' && p.event?.type === 'blockChange')).toBe(true)
|
|
370
|
+
expect(payloads.some(p => p?.type === 'pushEvent' && p.event?.type === 'ingestBlockSection')).toBe(true)
|
|
371
|
+
expect(payloads.some(p => p?.type === 'pushEvent' && p.event?.type === 'unloadColumn')).toBe(true)
|
|
372
|
+
expect(payloads.some(p => p?.type === 'setUpdateLightV17')).toBe(true)
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
test('resetWorld terminates the owner worker when it was spawned', () => {
|
|
376
|
+
const renderer = createRenderer(true, 1)
|
|
377
|
+
renderer.initWorkers(1)
|
|
378
|
+
const owner = renderer.getClientLightOwnerWorker() as unknown as { terminate: ReturnType<typeof vi.fn> }
|
|
379
|
+
expect(owner).toBeTruthy()
|
|
380
|
+
renderer.resetWorld()
|
|
381
|
+
expect(owner.terminate).toHaveBeenCalled()
|
|
382
|
+
expect(renderer.hasClientLightOwner()).toBe(false)
|
|
383
|
+
expect(renderer.workers).toHaveLength(0)
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
test('owner publication remesh does not wait the 100ms trailing window', () => {
|
|
387
|
+
vi.useFakeTimers()
|
|
388
|
+
try {
|
|
389
|
+
const renderer = createRenderer(true, 1)
|
|
390
|
+
renderer.initWorkers(1)
|
|
391
|
+
renderer.forceCallFromMesherReplayer = true
|
|
392
|
+
const mesh = renderer.workers[0] as { postMessage: ReturnType<typeof vi.fn> }
|
|
393
|
+
renderer.setSectionDirty(new Vec3(0, 64, 0), true, true)
|
|
394
|
+
mesh.postMessage.mockClear()
|
|
395
|
+
;(renderer as any).lightCoordinator.onPublication({
|
|
396
|
+
applied: true,
|
|
397
|
+
lastVersion: 7,
|
|
398
|
+
acceptedGeneration: 1,
|
|
399
|
+
dirtyMeshSections: [{ sx: 0, sy: 64, sz: 0 }],
|
|
400
|
+
workerMessage: null
|
|
401
|
+
})
|
|
402
|
+
const immediate = mesh.postMessage.mock.calls.map(call => call[0]).filter((message: { type?: string }) => message?.type === 'dirty')
|
|
403
|
+
expect(immediate.some((message: { lightPublicationVersion?: number }) => message.lightPublicationVersion === 7)).toBe(true)
|
|
404
|
+
mesh.postMessage.mockClear()
|
|
405
|
+
vi.advanceTimersByTime(WorldRendererCommon['GEOMETRY_THROTTLE_DELAY'])
|
|
406
|
+
const trailing = mesh.postMessage.mock.calls.map(call => call[0]).filter((message: { type?: string }) => message?.type === 'dirty')
|
|
407
|
+
expect(trailing).toHaveLength(0)
|
|
408
|
+
} finally {
|
|
409
|
+
vi.useRealTimers()
|
|
410
|
+
}
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
test('owner publication is posted only to mesh workers that own dirty sections', () => {
|
|
414
|
+
const renderer = createRenderer(true, 8)
|
|
415
|
+
renderer.initWorkers(8)
|
|
416
|
+
for (const worker of renderer.workers) {
|
|
417
|
+
;(worker as { postMessage: ReturnType<typeof vi.fn> }).postMessage.mockClear()
|
|
418
|
+
}
|
|
419
|
+
const workerMessage = {
|
|
420
|
+
type: 'applyOwnerLightPublication',
|
|
421
|
+
worldGeneration: 1,
|
|
422
|
+
publicationVersion: 4,
|
|
423
|
+
sections: [{ sx: 0, sy: 64, sz: 0, blockLight: new Uint8Array(2048) }]
|
|
424
|
+
}
|
|
425
|
+
;(renderer as any).lightCoordinator.onPublication({
|
|
426
|
+
applied: true,
|
|
427
|
+
lastVersion: 4,
|
|
428
|
+
acceptedGeneration: 1,
|
|
429
|
+
dirtyMeshSections: [{ sx: 0, sy: 64, sz: 0 }],
|
|
430
|
+
workerMessage
|
|
431
|
+
})
|
|
432
|
+
const posted = renderer.workers.map(worker =>
|
|
433
|
+
(worker as { postMessage: ReturnType<typeof vi.fn> }).postMessage.mock.calls.some(call => call[0]?.type === 'applyOwnerLightPublication')
|
|
434
|
+
)
|
|
435
|
+
expect(posted.filter(Boolean).length).toBeGreaterThan(0)
|
|
436
|
+
expect(posted.filter(Boolean).length).toBeLessThan(renderer.workers.length)
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
test('trailing dirty after an owner publication keeps the target revision', () => {
|
|
440
|
+
vi.useFakeTimers()
|
|
441
|
+
try {
|
|
442
|
+
const renderer = createRenderer(true, 1)
|
|
443
|
+
renderer.initWorkers(1)
|
|
444
|
+
renderer.forceCallFromMesherReplayer = true
|
|
445
|
+
const mesh = renderer.workers[0] as { postMessage: ReturnType<typeof vi.fn> }
|
|
446
|
+
mesh.postMessage.mockClear()
|
|
447
|
+
renderer.setSectionDirty(new Vec3(0, 64, 0), true, true)
|
|
448
|
+
;(renderer as any).lightCoordinator.onPublication({
|
|
449
|
+
applied: true,
|
|
450
|
+
lastVersion: 7,
|
|
451
|
+
acceptedGeneration: 1,
|
|
452
|
+
dirtyMeshSections: [{ sx: 0, sy: 64, sz: 0 }],
|
|
453
|
+
workerMessage: null
|
|
454
|
+
})
|
|
455
|
+
vi.advanceTimersByTime(WorldRendererCommon['GEOMETRY_THROTTLE_DELAY'])
|
|
456
|
+
const dirties = mesh.postMessage.mock.calls.map(call => call[0]).filter((message: { type?: string }) => message?.type === 'dirty')
|
|
457
|
+
const trailing = dirties.at(-1)
|
|
458
|
+
expect(trailing?.lightPublicationVersion).toBe(7)
|
|
459
|
+
expect(trailing?.worldGeneration).toBe(1)
|
|
460
|
+
} finally {
|
|
461
|
+
vi.useRealTimers()
|
|
462
|
+
}
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
test('rejected stale owner geometry does not add a remesh when covering is already outstanding', () => {
|
|
466
|
+
const renderer = createRenderer(true, 1)
|
|
467
|
+
renderer.initWorkers(1)
|
|
468
|
+
renderer.forceCallFromMesherReplayer = true
|
|
469
|
+
renderer.loadedChunks['0,0'] = true
|
|
470
|
+
const owner = renderer.getClientLightOwnerWorker() as { onmessage: ((event: MessageEvent) => void) | null }
|
|
471
|
+
owner.onmessage?.({ data: { type: 'ready' } } as MessageEvent)
|
|
472
|
+
owner.onmessage?.({
|
|
473
|
+
data: {
|
|
474
|
+
type: 'publication',
|
|
475
|
+
publication: {
|
|
476
|
+
worldGeneration: 1,
|
|
477
|
+
publicationVersion: 2,
|
|
478
|
+
sections: [{ sx: 0, sy: 4, sz: 0, blockLight: new Uint8Array(2048) }]
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
} as MessageEvent)
|
|
482
|
+
|
|
483
|
+
const mesh = renderer.workers[0] as { postMessage: ReturnType<typeof vi.fn> }
|
|
484
|
+
mesh.postMessage.mockClear()
|
|
485
|
+
renderer.sectionsWaiting.set('0,64,0', 1)
|
|
486
|
+
renderer.handleMessage({
|
|
487
|
+
type: 'geometry',
|
|
488
|
+
key: '0,64,0',
|
|
489
|
+
worldGeneration: 1,
|
|
490
|
+
lightPublicationVersion: 1,
|
|
491
|
+
sessionEpoch: 1,
|
|
492
|
+
columnIncarnation: 1,
|
|
493
|
+
requestId: 1,
|
|
494
|
+
topologyRevision: 1,
|
|
495
|
+
workerIndex: 0,
|
|
496
|
+
geometry: {}
|
|
497
|
+
})
|
|
498
|
+
const dirties = mesh.postMessage.mock.calls.map(call => call[0]).filter((message: { type?: string }) => message?.type === 'dirty')
|
|
499
|
+
expect(dirties).toHaveLength(0)
|
|
500
|
+
|
|
501
|
+
renderer.handleMessage({ type: 'sectionFinished', key: '0,64,0', workerIndex: 0, processTime: 0 })
|
|
502
|
+
const replacement = mesh.postMessage.mock.calls.map(call => call[0]).filter((message: { type?: string }) => message?.type === 'dirty')
|
|
503
|
+
expect(replacement).toHaveLength(1)
|
|
504
|
+
expect(replacement[0]?.lightPublicationVersion).toBe(2)
|
|
505
|
+
expect(renderer.sectionsWaiting.get('0,64,0')).toBe(1)
|
|
506
|
+
expect((renderer as any).lightCoordinator.rejectedFinishedBySection.get('0,64,0') ?? 0).toBe(0)
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
test('fast A→B→C drops B and keeps a single covering remesh for C', () => {
|
|
510
|
+
const renderer = createRenderer(true, 1)
|
|
511
|
+
renderer.initWorkers(1)
|
|
512
|
+
renderer.forceCallFromMesherReplayer = true
|
|
513
|
+
renderer.loadedChunks['0,0'] = true
|
|
514
|
+
const owner = renderer.getClientLightOwnerWorker() as { onmessage: ((event: MessageEvent) => void) | null }
|
|
515
|
+
owner.onmessage?.({ data: { type: 'ready' } } as MessageEvent)
|
|
516
|
+
const publish = (version: number) => {
|
|
517
|
+
owner.onmessage?.({
|
|
518
|
+
data: {
|
|
519
|
+
type: 'publication',
|
|
520
|
+
publication: {
|
|
521
|
+
worldGeneration: 1,
|
|
522
|
+
publicationVersion: version,
|
|
523
|
+
sections: [{ sx: 0, sy: 4, sz: 0, blockLight: new Uint8Array(2048) }]
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
} as MessageEvent)
|
|
527
|
+
}
|
|
528
|
+
publish(1)
|
|
529
|
+
publish(2)
|
|
530
|
+
publish(3)
|
|
531
|
+
const mesh = renderer.workers[0] as { postMessage: ReturnType<typeof vi.fn> }
|
|
532
|
+
mesh.postMessage.mockClear()
|
|
533
|
+
renderer.sectionsWaiting.set('0,64,0', 1)
|
|
534
|
+
renderer.handleMessage({
|
|
535
|
+
type: 'geometry',
|
|
536
|
+
key: '0,64,0',
|
|
537
|
+
worldGeneration: 1,
|
|
538
|
+
lightPublicationVersion: 2,
|
|
539
|
+
sessionEpoch: 1,
|
|
540
|
+
columnIncarnation: 1,
|
|
541
|
+
requestId: 2,
|
|
542
|
+
topologyRevision: 1,
|
|
543
|
+
workerIndex: 0,
|
|
544
|
+
geometry: {}
|
|
545
|
+
})
|
|
546
|
+
expect(mesh.postMessage.mock.calls.filter(call => call[0]?.type === 'dirty')).toHaveLength(0)
|
|
547
|
+
const accepted = (renderer as any).lightCoordinator.evaluateOwnerGeometry({
|
|
548
|
+
type: 'geometry',
|
|
549
|
+
key: '0,64,0',
|
|
550
|
+
worldGeneration: 1,
|
|
551
|
+
lightPublicationVersion: 3,
|
|
552
|
+
sessionEpoch: 1,
|
|
553
|
+
columnIncarnation: 1,
|
|
554
|
+
requestId: 3,
|
|
555
|
+
topologyRevision: 1,
|
|
556
|
+
geometry: {}
|
|
557
|
+
})
|
|
558
|
+
expect(accepted.accepted).toBe(true)
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
test('owner failure posts a revert to mesh workers and rejects later owner publications', () => {
|
|
562
|
+
const renderer = createRenderer(true, 1)
|
|
563
|
+
renderer.initWorkers(1)
|
|
564
|
+
renderer.forceCallFromMesherReplayer = true
|
|
565
|
+
const owner = renderer.getClientLightOwnerWorker() as { onmessage: ((event: MessageEvent) => void) | null }
|
|
566
|
+
owner.onmessage?.({ data: { type: 'ready' } } as MessageEvent)
|
|
567
|
+
const mesh = renderer.workers[0] as { postMessage: ReturnType<typeof vi.fn> }
|
|
568
|
+
mesh.postMessage.mockClear()
|
|
569
|
+
owner.onmessage?.({ data: { type: 'error', error: 'owner wasm crashed' } } as MessageEvent)
|
|
570
|
+
expect(renderer.hasClientLightOwner()).toBe(false)
|
|
571
|
+
expect((renderer as any).getClientLightOwnerFailureReason()).toBe('owner wasm crashed')
|
|
572
|
+
expect(mesh.postMessage.mock.calls.some(call => call[0]?.type === 'revertOwnerLightToIncoming')).toBe(true)
|
|
573
|
+
const late = (renderer as any).lightCoordinator.evaluateOwnerGeometry({
|
|
574
|
+
type: 'geometry',
|
|
575
|
+
key: '0,64,0',
|
|
576
|
+
worldGeneration: 1,
|
|
577
|
+
lightPublicationVersion: 9,
|
|
578
|
+
sessionEpoch: 1,
|
|
579
|
+
meshMode: 'owner',
|
|
580
|
+
geometry: {}
|
|
581
|
+
})
|
|
582
|
+
expect(late.accepted).toBe(false)
|
|
583
|
+
})
|
|
584
|
+
|
|
585
|
+
test('reload of the same column increments incarnation so a late reply is dropped', () => {
|
|
586
|
+
const renderer = createRenderer(true, 1)
|
|
587
|
+
renderer.initWorkers(1)
|
|
588
|
+
renderer.forceCallFromMesherReplayer = true
|
|
589
|
+
renderer.loadedChunks['0,0'] = true
|
|
590
|
+
const owner = renderer.getClientLightOwnerWorker() as { onmessage: ((event: MessageEvent) => void) | null }
|
|
591
|
+
owner.onmessage?.({ data: { type: 'ready' } } as MessageEvent)
|
|
592
|
+
const first = (renderer as any).columnIncarnationFor('0,0')
|
|
593
|
+
renderer.removeColumn(0, 0)
|
|
594
|
+
renderer.loadedChunks['0,0'] = true
|
|
595
|
+
const second = (renderer as any).columnIncarnationFor('0,0')
|
|
596
|
+
expect(second).toBeGreaterThan(first ?? 0)
|
|
597
|
+
const late = (renderer as any).lightCoordinator.evaluateOwnerGeometry({
|
|
598
|
+
type: 'geometry',
|
|
599
|
+
key: '0,64,0',
|
|
600
|
+
worldGeneration: 1,
|
|
601
|
+
lightPublicationVersion: 4,
|
|
602
|
+
sessionEpoch: 1,
|
|
603
|
+
columnIncarnation: first ?? 1,
|
|
604
|
+
requestId: 4,
|
|
605
|
+
topologyRevision: 1,
|
|
606
|
+
meshMode: 'owner',
|
|
607
|
+
geometry: {}
|
|
608
|
+
})
|
|
609
|
+
expect(late.accepted).toBe(false)
|
|
610
|
+
})
|
|
611
|
+
|
|
612
|
+
test('flag-on ready owner does not fan-out raw update_light to mesh workers', () => {
|
|
613
|
+
const renderer = createRenderer(true, 2)
|
|
614
|
+
renderer.initWorkers(2)
|
|
615
|
+
const owner = renderer.getClientLightOwnerWorker() as unknown as {
|
|
616
|
+
onmessage: ((event: MessageEvent) => void) | null
|
|
617
|
+
postMessage: ReturnType<typeof vi.fn>
|
|
618
|
+
}
|
|
619
|
+
owner.onmessage?.({ data: { type: 'ready' } } as MessageEvent)
|
|
620
|
+
owner.postMessage.mockClear()
|
|
621
|
+
for (const worker of renderer.workers) {
|
|
622
|
+
;(worker as { postMessage: ReturnType<typeof vi.fn> }).postMessage.mockClear()
|
|
623
|
+
}
|
|
624
|
+
renderer.feedChunkPacket({
|
|
625
|
+
kind: 'setUpdateLightV17',
|
|
626
|
+
protocol: 756,
|
|
627
|
+
numSections: 16,
|
|
628
|
+
rawPacket: new Uint8Array([1, 2, 3])
|
|
629
|
+
})
|
|
630
|
+
expect(owner.postMessage.mock.calls.some(call => call[0]?.type === 'setUpdateLightV17')).toBe(true)
|
|
631
|
+
expect(
|
|
632
|
+
renderer.workers.every(worker =>
|
|
633
|
+
(worker as { postMessage: ReturnType<typeof vi.fn> }).postMessage.mock.calls.every(call => call[0]?.type !== 'setUpdateLightV17')
|
|
634
|
+
)
|
|
635
|
+
).toBe(true)
|
|
636
|
+
})
|
|
637
|
+
|
|
638
|
+
test('stale topology with fresh light is rejected and covering stays outstanding', () => {
|
|
639
|
+
const renderer = createRenderer(true, 1)
|
|
640
|
+
renderer.initWorkers(1)
|
|
641
|
+
renderer.forceCallFromMesherReplayer = true
|
|
642
|
+
renderer.neighborChunkUpdates = false
|
|
643
|
+
renderer.loadedChunks['0,0'] = true
|
|
644
|
+
const owner = renderer.getClientLightOwnerWorker() as { onmessage: ((event: MessageEvent) => void) | null }
|
|
645
|
+
owner.onmessage?.({ data: { type: 'ready' } } as MessageEvent)
|
|
646
|
+
renderer.setBlockStateIdInner(new Vec3(1, 64, 1), 1)
|
|
647
|
+
renderer.setBlockStateIdInner(new Vec3(1, 64, 1), 2)
|
|
648
|
+
const topology = (renderer as any).lightCoordinator.topologyRevisionBySection.get('0,64,0') as number
|
|
649
|
+
expect(topology).toBeGreaterThan(1)
|
|
650
|
+
owner.onmessage?.({
|
|
651
|
+
data: {
|
|
652
|
+
type: 'publication',
|
|
653
|
+
publication: {
|
|
654
|
+
worldGeneration: 1,
|
|
655
|
+
publicationVersion: 4,
|
|
656
|
+
sections: [{ sx: 0, sy: 4, sz: 0, blockLight: new Uint8Array(2048) }]
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
} as MessageEvent)
|
|
660
|
+
expect((renderer as any).lightCoordinator.session.requiredLightForSection('0,64,0')?.topologyRevision).toBe(topology)
|
|
661
|
+
const mesh = renderer.workers[0] as { postMessage: ReturnType<typeof vi.fn> }
|
|
662
|
+
mesh.postMessage.mockClear()
|
|
663
|
+
renderer.sectionsWaiting.set('0,64,0', 1)
|
|
664
|
+
renderer.handleMessage({
|
|
665
|
+
type: 'geometry',
|
|
666
|
+
key: '0,64,0',
|
|
667
|
+
worldGeneration: 1,
|
|
668
|
+
lightPublicationVersion: 4,
|
|
669
|
+
sessionEpoch: 1,
|
|
670
|
+
columnIncarnation: 1,
|
|
671
|
+
requestId: 1,
|
|
672
|
+
topologyRevision: topology - 1,
|
|
673
|
+
workerIndex: 0,
|
|
674
|
+
geometry: {}
|
|
675
|
+
})
|
|
676
|
+
expect(
|
|
677
|
+
(renderer as any).lightCoordinator.evaluateOwnerGeometry({
|
|
678
|
+
type: 'geometry',
|
|
679
|
+
key: '0,64,0',
|
|
680
|
+
worldGeneration: 1,
|
|
681
|
+
lightPublicationVersion: 4,
|
|
682
|
+
sessionEpoch: 1,
|
|
683
|
+
columnIncarnation: 1,
|
|
684
|
+
topologyRevision: topology - 1,
|
|
685
|
+
geometry: {}
|
|
686
|
+
}).accepted
|
|
687
|
+
).toBe(false)
|
|
688
|
+
expect(mesh.postMessage.mock.calls.filter(call => call[0]?.type === 'dirty')).toHaveLength(0)
|
|
689
|
+
expect((renderer as any).lightCoordinator.pendingTopologyBySection.get('0,64,0')).toBe(topology)
|
|
690
|
+
renderer.handleMessage({ type: 'sectionFinished', key: '0,64,0', workerIndex: 0, processTime: 0 })
|
|
691
|
+
const replacement = mesh.postMessage.mock.calls.map(call => call[0]).filter((message: { type?: string }) => message?.type === 'dirty')
|
|
692
|
+
expect(replacement).toHaveLength(1)
|
|
693
|
+
expect(replacement[0]?.topologyRevision).toBe(topology)
|
|
694
|
+
expect(replacement[0]?.lightPublicationVersion).toBe(4)
|
|
695
|
+
expect(renderer.sectionsWaiting.get('0,64,0')).toBe(1)
|
|
696
|
+
expect((renderer as any).lightCoordinator.pendingTopologyBySection.get('0,64,0')).toBe(topology)
|
|
697
|
+
})
|
|
698
|
+
|
|
699
|
+
test('fresh topology with stale light installs and keeps covering without a second dispatch', () => {
|
|
700
|
+
const renderer = createRenderer(true, 1)
|
|
701
|
+
renderer.initWorkers(1)
|
|
702
|
+
renderer.forceCallFromMesherReplayer = true
|
|
703
|
+
renderer.neighborChunkUpdates = false
|
|
704
|
+
renderer.loadedChunks['0,0'] = true
|
|
705
|
+
const owner = renderer.getClientLightOwnerWorker() as { onmessage: ((event: MessageEvent) => void) | null }
|
|
706
|
+
owner.onmessage?.({ data: { type: 'ready' } } as MessageEvent)
|
|
707
|
+
renderer.setBlockStateIdInner(new Vec3(1, 64, 1), 1)
|
|
708
|
+
const topology = (renderer as any).lightCoordinator.topologyRevisionBySection.get('0,64,0') as number
|
|
709
|
+
owner.onmessage?.({
|
|
710
|
+
data: {
|
|
711
|
+
type: 'publication',
|
|
712
|
+
publication: {
|
|
713
|
+
worldGeneration: 1,
|
|
714
|
+
publicationVersion: 3,
|
|
715
|
+
sections: [{ sx: 0, sy: 4, sz: 0, blockLight: new Uint8Array(2048) }]
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
} as MessageEvent)
|
|
719
|
+
const mesh = renderer.workers[0] as { postMessage: ReturnType<typeof vi.fn> }
|
|
720
|
+
mesh.postMessage.mockClear()
|
|
721
|
+
renderer.sectionsWaiting.set('0,64,0', 2)
|
|
722
|
+
renderer.handleMessage({
|
|
723
|
+
type: 'geometry',
|
|
724
|
+
key: '0,64,0',
|
|
725
|
+
worldGeneration: 1,
|
|
726
|
+
lightPublicationVersion: 1,
|
|
727
|
+
sessionEpoch: 1,
|
|
728
|
+
columnIncarnation: 1,
|
|
729
|
+
requestId: 1,
|
|
730
|
+
topologyRevision: topology,
|
|
731
|
+
workerIndex: 0,
|
|
732
|
+
geometry: {}
|
|
733
|
+
})
|
|
734
|
+
const decision = (renderer as any).lightCoordinator.evaluateOwnerGeometry({
|
|
735
|
+
type: 'geometry',
|
|
736
|
+
key: '0,64,0',
|
|
737
|
+
worldGeneration: 1,
|
|
738
|
+
lightPublicationVersion: 1,
|
|
739
|
+
sessionEpoch: 1,
|
|
740
|
+
columnIncarnation: 1,
|
|
741
|
+
topologyRevision: topology,
|
|
742
|
+
geometry: {}
|
|
743
|
+
})
|
|
744
|
+
expect(decision.accepted).toBe(true)
|
|
745
|
+
expect(decision.keepCovering).toBe(true)
|
|
746
|
+
expect(mesh.postMessage.mock.calls.filter(call => call[0]?.type === 'dirty')).toHaveLength(0)
|
|
747
|
+
expect((renderer as any).lightCoordinator.pendingCoveringBySection.has('0,64,0')).toBe(true)
|
|
748
|
+
expect((renderer as any).lightCoordinator.pendingTopologyBySection.has('0,64,0')).toBe(false)
|
|
749
|
+
renderer.handleMessage({ type: 'sectionFinished', key: '0,64,0', workerIndex: 0, processTime: 0 })
|
|
750
|
+
expect(renderer.sectionsWaiting.get('0,64,0')).toBe(1)
|
|
751
|
+
expect((renderer as any).lightCoordinator.pendingCoveringBySection.has('0,64,0')).toBe(true)
|
|
752
|
+
})
|
|
753
|
+
|
|
754
|
+
test('fast A→B→C publications keep the max topology revision', () => {
|
|
755
|
+
const renderer = createRenderer(true, 1)
|
|
756
|
+
renderer.initWorkers(1)
|
|
757
|
+
renderer.forceCallFromMesherReplayer = true
|
|
758
|
+
renderer.neighborChunkUpdates = false
|
|
759
|
+
renderer.loadedChunks['0,0'] = true
|
|
760
|
+
const owner = renderer.getClientLightOwnerWorker() as { onmessage: ((event: MessageEvent) => void) | null }
|
|
761
|
+
owner.onmessage?.({ data: { type: 'ready' } } as MessageEvent)
|
|
762
|
+
const publish = (version: number) => {
|
|
763
|
+
owner.onmessage?.({
|
|
764
|
+
data: {
|
|
765
|
+
type: 'publication',
|
|
766
|
+
publication: {
|
|
767
|
+
worldGeneration: 1,
|
|
768
|
+
publicationVersion: version,
|
|
769
|
+
sections: [{ sx: 0, sy: 4, sz: 0, blockLight: new Uint8Array(2048) }]
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
} as MessageEvent)
|
|
773
|
+
}
|
|
774
|
+
renderer.setBlockStateIdInner(new Vec3(1, 64, 1), 1)
|
|
775
|
+
publish(1)
|
|
776
|
+
renderer.setBlockStateIdInner(new Vec3(1, 64, 1), 2)
|
|
777
|
+
publish(2)
|
|
778
|
+
renderer.setBlockStateIdInner(new Vec3(1, 64, 1), 3)
|
|
779
|
+
publish(3)
|
|
780
|
+
const topology = (renderer as any).lightCoordinator.topologyRevisionBySection.get('0,64,0')
|
|
781
|
+
expect((renderer as any).lightCoordinator.session.requiredLightForSection('0,64,0')).toMatchObject({
|
|
782
|
+
requiredVersion: 3,
|
|
783
|
+
worldGeneration: 1,
|
|
784
|
+
topologyRevision: topology
|
|
785
|
+
})
|
|
786
|
+
})
|
|
787
|
+
|
|
788
|
+
test('publishing light only for column A accepts the neighbor wall without a publication of B', () => {
|
|
789
|
+
const renderer = createRenderer(true, 2)
|
|
790
|
+
renderer.initWorkers(2)
|
|
791
|
+
renderer.forceCallFromMesherReplayer = true
|
|
792
|
+
renderer.loadedChunks['0,0'] = true
|
|
793
|
+
renderer.loadedChunks['16,0'] = true
|
|
794
|
+
renderer.loadedChunks['-16,0'] = true
|
|
795
|
+
const owner = renderer.getClientLightOwnerWorker() as { onmessage: ((event: MessageEvent) => void) | null }
|
|
796
|
+
owner.onmessage?.({ data: { type: 'ready' } } as MessageEvent)
|
|
797
|
+
owner.onmessage?.({
|
|
798
|
+
data: {
|
|
799
|
+
type: 'publication',
|
|
800
|
+
publication: {
|
|
801
|
+
worldGeneration: 1,
|
|
802
|
+
publicationVersion: 8,
|
|
803
|
+
sections: [{ sx: 0, sy: 4, sz: 0, blockLight: new Uint8Array(2048) }]
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
} as MessageEvent)
|
|
807
|
+
|
|
808
|
+
const posted = renderer.workers.map(worker =>
|
|
809
|
+
(worker as { postMessage: ReturnType<typeof vi.fn> }).postMessage.mock.calls.some(call => call[0]?.type === 'applyOwnerLightPublication')
|
|
810
|
+
)
|
|
811
|
+
expect(posted).toEqual([true, true])
|
|
812
|
+
expect((renderer as any).lightCoordinator.session.requiredLightForSection('16,64,0')?.requiredVersion).toBe(8)
|
|
813
|
+
expect((renderer as any).lightCoordinator.session.requiredLightForSection('-16,64,0')?.requiredVersion).toBe(8)
|
|
814
|
+
|
|
815
|
+
const evaluate = (key: string, lightPublicationVersion: number, meshMode: 'owner' | 'legacyBootstrap') => {
|
|
816
|
+
const [x, , z] = key.split(',').map(Number)
|
|
817
|
+
return (renderer as any).lightCoordinator.evaluateOwnerGeometry({
|
|
818
|
+
type: 'geometry',
|
|
819
|
+
key,
|
|
820
|
+
worldGeneration: 1,
|
|
821
|
+
lightPublicationVersion,
|
|
822
|
+
sessionEpoch: 1,
|
|
823
|
+
columnIncarnation: renderer.columnIncarnationFor(`${x},${z}`),
|
|
824
|
+
requestId: 1,
|
|
825
|
+
topologyRevision: INITIAL_TOPOLOGY_REVISION,
|
|
826
|
+
meshMode,
|
|
827
|
+
geometry: {}
|
|
828
|
+
})
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const bootstrapWall = evaluate('-16,64,0', 8, 'legacyBootstrap')
|
|
832
|
+
expect(bootstrapWall.accepted).toBe(true)
|
|
833
|
+
expect(bootstrapWall.keepCovering).toBe(false)
|
|
834
|
+
expect(evaluate('16,64,0', 3, 'owner').accepted).toBe(false)
|
|
835
|
+
expect(evaluate('16,64,0', 8, 'owner').accepted).toBe(true)
|
|
836
|
+
})
|
|
837
|
+
|
|
838
|
+
test('bootstrap geometry from the previous column life is rejected', () => {
|
|
839
|
+
const renderer = createRenderer(true, 1)
|
|
840
|
+
renderer.initWorkers(1)
|
|
841
|
+
renderer.forceCallFromMesherReplayer = true
|
|
842
|
+
renderer.loadedChunks['0,0'] = true
|
|
843
|
+
const owner = renderer.getClientLightOwnerWorker() as { onmessage: ((event: MessageEvent) => void) | null }
|
|
844
|
+
owner.onmessage?.({ data: { type: 'ready' } } as MessageEvent)
|
|
845
|
+
const first = renderer.columnIncarnationFor('0,0')
|
|
846
|
+
renderer.removeColumn(0, 0)
|
|
847
|
+
renderer.loadedChunks['0,0'] = true
|
|
848
|
+
const late = (renderer as any).lightCoordinator.evaluateOwnerGeometry({
|
|
849
|
+
type: 'geometry',
|
|
850
|
+
key: '0,64,0',
|
|
851
|
+
meshMode: 'legacyBootstrap',
|
|
852
|
+
sessionEpoch: 1,
|
|
853
|
+
columnIncarnation: first,
|
|
854
|
+
geometry: {}
|
|
855
|
+
})
|
|
856
|
+
expect(late.accepted).toBe(false)
|
|
857
|
+
})
|
|
858
|
+
|
|
859
|
+
test('the first edit misses the initial topology cache and covering remesh carries the new revision', () => {
|
|
860
|
+
vi.useFakeTimers()
|
|
861
|
+
try {
|
|
862
|
+
const renderer = createRenderer(true, 1)
|
|
863
|
+
renderer.initWorkers(1)
|
|
864
|
+
renderer.forceCallFromMesherReplayer = true
|
|
865
|
+
renderer.neighborChunkUpdates = false
|
|
866
|
+
renderer.loadedChunks['0,0'] = true
|
|
867
|
+
const owner = renderer.getClientLightOwnerWorker() as { onmessage: ((event: MessageEvent) => void) | null }
|
|
868
|
+
owner.onmessage?.({ data: { type: 'ready' } } as MessageEvent)
|
|
869
|
+
const mesh = renderer.workers[0] as { postMessage: ReturnType<typeof vi.fn> }
|
|
870
|
+
mesh.postMessage.mockClear()
|
|
871
|
+
renderer.setSectionDirty(new Vec3(1, 64, 1), true, true)
|
|
872
|
+
const initial = mesh.postMessage.mock.calls
|
|
873
|
+
.map(call => call[0])
|
|
874
|
+
.filter((message: { type?: string }) => message?.type === 'dirty')
|
|
875
|
+
.at(-1)
|
|
876
|
+
expect(initial?.topologyRevision).toBe(INITIAL_TOPOLOGY_REVISION)
|
|
877
|
+
|
|
878
|
+
let walks = 0
|
|
879
|
+
const compute = () => {
|
|
880
|
+
walks++
|
|
881
|
+
return { visibilitySet: walks, signs: {}, heads: {}, banners: {} }
|
|
882
|
+
}
|
|
883
|
+
getOrComputeTopologyPost('0,64,0', initial?.topologyRevision, compute)
|
|
884
|
+
renderer.setBlockStateIdInner(new Vec3(1, 64, 1), 1)
|
|
885
|
+
owner.onmessage?.({
|
|
886
|
+
data: {
|
|
887
|
+
type: 'publication',
|
|
888
|
+
publication: {
|
|
889
|
+
worldGeneration: 1,
|
|
890
|
+
publicationVersion: 2,
|
|
891
|
+
sections: [{ sx: 0, sy: 4, sz: 0, blockLight: new Uint8Array(2048) }]
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
} as MessageEvent)
|
|
895
|
+
const covering = mesh.postMessage.mock.calls
|
|
896
|
+
.map(call => call[0])
|
|
897
|
+
.filter((message: { type?: string; lightPublicationVersion?: number; x?: number; y?: number; z?: number }) => {
|
|
898
|
+
if (message?.type !== 'dirty' || message.lightPublicationVersion !== 2) return false
|
|
899
|
+
const sx = Math.floor((message.x ?? 0) / 16) * 16
|
|
900
|
+
const sy = Math.floor((message.y ?? 0) / 16) * 16
|
|
901
|
+
const sz = Math.floor((message.z ?? 0) / 16) * 16
|
|
902
|
+
return `${sx},${sy},${sz}` === '0,64,0'
|
|
903
|
+
})
|
|
904
|
+
expect(covering).toHaveLength(1)
|
|
905
|
+
expect(covering[0]?.topologyRevision).not.toBe(initial?.topologyRevision)
|
|
906
|
+
const editedRevision = covering[0]?.topologyRevision
|
|
907
|
+
const recomputed = getOrComputeTopologyPost('0,64,0', editedRevision, compute)
|
|
908
|
+
expect(recomputed.hit).toBe(false)
|
|
909
|
+
expect(walks).toBe(2)
|
|
910
|
+
} finally {
|
|
911
|
+
clearTopologyPostCache()
|
|
912
|
+
vi.clearAllTimers()
|
|
913
|
+
vi.useRealTimers()
|
|
914
|
+
}
|
|
915
|
+
})
|
|
916
|
+
})
|