minecraft-renderer 0.1.106 → 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 +105 -76
- package/dist/minecraft-renderer.js.meta.json +1 -1
- package/dist/threeWorker.js +537 -508
- 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 +8 -0
- package/src/graphicsBackend/rendererDefaultOptions.test.ts +51 -0
- package/src/graphicsBackend/rendererDefaultOptions.ts +18 -1
- package/src/graphicsBackend/rendererOptionsSync.ts +4 -3
- package/src/lib/blockEntityLightRegistry.ts +4 -0
- package/src/lib/skyLight.ts +17 -0
- package/src/lib/worldrendererCommon.lightOwner.test.ts +916 -0
- package/src/lib/worldrendererCommon.removeColumn.test.ts +16 -0
- package/src/lib/worldrendererCommon.ts +340 -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 +97 -8
- package/src/three/entity/EntityMesh.ts +1 -0
- package/src/three/entity/legWalkAnimation.test.ts +154 -0
- package/src/three/entity/legWalkAnimation.ts +193 -0
- 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 +191 -0
- package/src/three/modules/index.ts +4 -0
- package/src/three/modules/sunMoon.ts +151 -0
- package/src/three/skyboxRenderer.ts +15 -8
- package/src/three/tests/clouds.test.ts +59 -0
- package/src/three/tests/globalBlockBuffer.test.ts +98 -0
- package/src/three/tests/globalLegacyBuffer.test.ts +108 -0
- package/src/three/tests/waterFog.test.ts +25 -0
- package/src/three/waterFog.ts +28 -0
- package/src/three/worldRendererThree.ts +84 -39
- 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 +3 -0
- package/src/worldView/worldView.geometryReload.test.ts +68 -0
- package/src/worldView/worldView.spiral.test.ts +1 -1
- package/src/worldView/worldView.ts +49 -1
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { describe, expect, test } from 'vitest'
|
|
3
|
+
import {
|
|
4
|
+
INITIAL_TOPOLOGY_REVISION,
|
|
5
|
+
nextTopologyRevision,
|
|
6
|
+
shouldAcceptVersionedMesh,
|
|
7
|
+
shouldDispatchCoveringRemesh,
|
|
8
|
+
snapshotMeshVersions,
|
|
9
|
+
validateMeshResultVersions,
|
|
10
|
+
validateOwnerPackedSection,
|
|
11
|
+
validateOwnerPublicationMessage
|
|
12
|
+
} from './clientLightVersions'
|
|
13
|
+
|
|
14
|
+
describe('shouldAcceptVersionedMesh', () => {
|
|
15
|
+
const required = { requiredVersion: 3, worldGeneration: 1, topologyRevision: 5 }
|
|
16
|
+
|
|
17
|
+
test('legacy bootstrap still accepts unversioned geometry', () => {
|
|
18
|
+
expect(shouldAcceptVersionedMesh({}, required, { ownerManaged: false })).toBe(true)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('owner-managed path rejects unversioned geometry as current', () => {
|
|
22
|
+
expect(shouldAcceptVersionedMesh({}, required, { ownerManaged: true })).toBe(false)
|
|
23
|
+
expect(shouldAcceptVersionedMesh({ meshMode: 'legacyBootstrap' }, required, { ownerManaged: true })).toBe(false)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('fast A→B→C drops B when C is required', () => {
|
|
27
|
+
expect(
|
|
28
|
+
shouldAcceptVersionedMesh({ worldGeneration: 1, lightPublicationVersion: 2, topologyRevision: 4, sessionEpoch: 1 }, required, {
|
|
29
|
+
ownerManaged: true,
|
|
30
|
+
sessionEpoch: 1
|
|
31
|
+
})
|
|
32
|
+
).toBe(false)
|
|
33
|
+
expect(
|
|
34
|
+
shouldAcceptVersionedMesh({ worldGeneration: 1, lightPublicationVersion: 3, topologyRevision: 5, sessionEpoch: 1 }, required, {
|
|
35
|
+
ownerManaged: true,
|
|
36
|
+
sessionEpoch: 1
|
|
37
|
+
})
|
|
38
|
+
).toBe(true)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('late reply from a previous incarnation is dropped', () => {
|
|
42
|
+
expect(
|
|
43
|
+
shouldAcceptVersionedMesh({ worldGeneration: 1, lightPublicationVersion: 3, topologyRevision: 5, sessionEpoch: 1, columnIncarnation: 1 }, required, {
|
|
44
|
+
ownerManaged: true,
|
|
45
|
+
sessionEpoch: 1,
|
|
46
|
+
columnIncarnation: 2
|
|
47
|
+
})
|
|
48
|
+
).toBe(false)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('hadErrors is never a successful empty commit', () => {
|
|
52
|
+
expect(
|
|
53
|
+
shouldAcceptVersionedMesh({ hadErrors: true, worldGeneration: 1, lightPublicationVersion: 3, topologyRevision: 5 }, required, { ownerManaged: true })
|
|
54
|
+
).toBe(false)
|
|
55
|
+
expect(
|
|
56
|
+
shouldAcceptVersionedMesh({ hadErrors: false, worldGeneration: 1, lightPublicationVersion: 3, topologyRevision: 5 }, required, {
|
|
57
|
+
ownerManaged: true
|
|
58
|
+
})
|
|
59
|
+
).toBe(true)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('stale topology with fresh light is rejected', () => {
|
|
63
|
+
expect(
|
|
64
|
+
shouldAcceptVersionedMesh({ worldGeneration: 1, lightPublicationVersion: 3, topologyRevision: 4, sessionEpoch: 1 }, required, {
|
|
65
|
+
ownerManaged: true,
|
|
66
|
+
sessionEpoch: 1
|
|
67
|
+
})
|
|
68
|
+
).toBe(false)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('fresh topology with stale light is accepted so the block can install this frame', () => {
|
|
72
|
+
expect(
|
|
73
|
+
shouldAcceptVersionedMesh({ worldGeneration: 1, lightPublicationVersion: 2, topologyRevision: 5, sessionEpoch: 1 }, required, {
|
|
74
|
+
ownerManaged: true,
|
|
75
|
+
sessionEpoch: 1
|
|
76
|
+
})
|
|
77
|
+
).toBe(true)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('required == null accepts versioned geometry', () => {
|
|
81
|
+
expect(
|
|
82
|
+
shouldAcceptVersionedMesh({ worldGeneration: 1, lightPublicationVersion: 1, topologyRevision: 1, sessionEpoch: 1 }, null, {
|
|
83
|
+
ownerManaged: true,
|
|
84
|
+
sessionEpoch: 1
|
|
85
|
+
})
|
|
86
|
+
).toBe(true)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('legacyBootstrap with a required revision is still rejected', () => {
|
|
90
|
+
expect(shouldAcceptVersionedMesh({ meshMode: 'legacyBootstrap', topologyRevision: 5 }, required, { ownerManaged: true })).toBe(false)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('legacyBootstrap without required is accepted', () => {
|
|
94
|
+
expect(shouldAcceptVersionedMesh({ meshMode: 'legacyBootstrap' }, null, { ownerManaged: true })).toBe(true)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('legacyBootstrap still has to match the column life', () => {
|
|
98
|
+
expect(
|
|
99
|
+
shouldAcceptVersionedMesh({ meshMode: 'legacyBootstrap', sessionEpoch: 1, columnIncarnation: 1 }, null, {
|
|
100
|
+
ownerManaged: true,
|
|
101
|
+
sessionEpoch: 2,
|
|
102
|
+
columnIncarnation: 2
|
|
103
|
+
})
|
|
104
|
+
).toBe(false)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test('legacyBootstrap with a proven neighbor light version covers the requirement', () => {
|
|
108
|
+
expect(
|
|
109
|
+
shouldAcceptVersionedMesh(
|
|
110
|
+
{ meshMode: 'legacyBootstrap', worldGeneration: 1, lightPublicationVersion: 10, sessionEpoch: 1, columnIncarnation: 1 },
|
|
111
|
+
{ requiredVersion: 10, worldGeneration: 1 },
|
|
112
|
+
{ ownerManaged: true, sessionEpoch: 1, columnIncarnation: 1 }
|
|
113
|
+
)
|
|
114
|
+
).toBe(true)
|
|
115
|
+
expect(
|
|
116
|
+
shouldAcceptVersionedMesh(
|
|
117
|
+
{ meshMode: 'legacyBootstrap', worldGeneration: 1, sessionEpoch: 1, columnIncarnation: 1 },
|
|
118
|
+
{ requiredVersion: 10, worldGeneration: 1 },
|
|
119
|
+
{ ownerManaged: true, sessionEpoch: 1, columnIncarnation: 1 }
|
|
120
|
+
)
|
|
121
|
+
).toBe(false)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('owner mesh with an older own-column light version stays rejected', () => {
|
|
125
|
+
expect(
|
|
126
|
+
shouldAcceptVersionedMesh(
|
|
127
|
+
{ meshMode: 'owner', worldGeneration: 1, lightPublicationVersion: 9, sessionEpoch: 1, columnIncarnation: 1 },
|
|
128
|
+
{ requiredVersion: 10, worldGeneration: 1 },
|
|
129
|
+
{ ownerManaged: true, sessionEpoch: 1, columnIncarnation: 1 }
|
|
130
|
+
)
|
|
131
|
+
).toBe(false)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test('without topology on the requirement, stale light still rejects (old gate)', () => {
|
|
135
|
+
expect(
|
|
136
|
+
shouldAcceptVersionedMesh(
|
|
137
|
+
{ worldGeneration: 1, lightPublicationVersion: 2, topologyRevision: 5, sessionEpoch: 1 },
|
|
138
|
+
{ requiredVersion: 3, worldGeneration: 1 },
|
|
139
|
+
{ ownerManaged: true, sessionEpoch: 1 }
|
|
140
|
+
)
|
|
141
|
+
).toBe(false)
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
describe('shouldDispatchCoveringRemesh', () => {
|
|
146
|
+
const required = { requiredVersion: 4, worldGeneration: 1 }
|
|
147
|
+
|
|
148
|
+
test('dispatches when nothing is outstanding', () => {
|
|
149
|
+
expect(shouldDispatchCoveringRemesh({ required, outstandingCount: 0 })).toBe(true)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
test('does not add another remesh when pending already covers the requirement', () => {
|
|
153
|
+
expect(
|
|
154
|
+
shouldDispatchCoveringRemesh({
|
|
155
|
+
required,
|
|
156
|
+
outstandingCount: 1,
|
|
157
|
+
pendingCoveringVersion: 4,
|
|
158
|
+
pendingCoveringGeneration: 1
|
|
159
|
+
})
|
|
160
|
+
).toBe(false)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
test('dispatches when the outstanding remesh is behind the new requirement', () => {
|
|
164
|
+
expect(
|
|
165
|
+
shouldDispatchCoveringRemesh({
|
|
166
|
+
required,
|
|
167
|
+
outstandingCount: 2,
|
|
168
|
+
pendingCoveringVersion: 3,
|
|
169
|
+
pendingCoveringGeneration: 1
|
|
170
|
+
})
|
|
171
|
+
).toBe(true)
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
describe('snapshotMeshVersions', () => {
|
|
176
|
+
test('freezes neighbor revisions with the task snapshot, not a later live map', () => {
|
|
177
|
+
const neighbors = [{ key: '16,64,0', topologyRevision: 2 }]
|
|
178
|
+
const snap = snapshotMeshVersions({
|
|
179
|
+
sessionEpoch: 1,
|
|
180
|
+
columnIncarnation: 3,
|
|
181
|
+
requestId: 9,
|
|
182
|
+
topologyRevision: 4,
|
|
183
|
+
lightPublicationVersion: 7,
|
|
184
|
+
worldGeneration: 1,
|
|
185
|
+
neighborTopologyRevisions: neighbors
|
|
186
|
+
})
|
|
187
|
+
neighbors[0]!.topologyRevision = 99
|
|
188
|
+
expect(snap.neighborTopologyRevisions[0]!.topologyRevision).toBe(2)
|
|
189
|
+
expect(snap.meshMode).toBe('owner')
|
|
190
|
+
})
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
describe('nextTopologyRevision', () => {
|
|
194
|
+
test('increments from zero', () => {
|
|
195
|
+
expect(nextTopologyRevision(undefined)).toBe(1)
|
|
196
|
+
expect(nextTopologyRevision(1)).toBe(2)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
test('the initial revision is not the first edit', () => {
|
|
200
|
+
expect(INITIAL_TOPOLOGY_REVISION).toBe(0)
|
|
201
|
+
expect(nextTopologyRevision(undefined)).not.toBe(INITIAL_TOPOLOGY_REVISION)
|
|
202
|
+
expect(nextTopologyRevision(INITIAL_TOPOLOGY_REVISION)).toBe(1)
|
|
203
|
+
})
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
describe('payload validation', () => {
|
|
207
|
+
test('rejects NaN and non-integer revision ids', () => {
|
|
208
|
+
expect(validateMeshResultVersions({ sessionEpoch: 1.5 })).toEqual({ ok: false, reason: 'sessionEpoch' })
|
|
209
|
+
expect(validateMeshResultVersions({ requestId: -1 })).toEqual({ ok: false, reason: 'requestId' })
|
|
210
|
+
expect(validateMeshResultVersions({ key: '0,64,0', requestId: 3 })).toEqual({ ok: true })
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
test('rejects packed light that is not 2048 bytes per channel', () => {
|
|
214
|
+
expect(
|
|
215
|
+
validateOwnerPackedSection({
|
|
216
|
+
sx: 0,
|
|
217
|
+
sy: 64,
|
|
218
|
+
sz: 0,
|
|
219
|
+
blockLight: new Uint8Array(16)
|
|
220
|
+
})
|
|
221
|
+
).toEqual({ ok: false, reason: 'blockLight' })
|
|
222
|
+
expect(
|
|
223
|
+
validateOwnerPackedSection({
|
|
224
|
+
sx: -16,
|
|
225
|
+
sy: 64,
|
|
226
|
+
sz: 32,
|
|
227
|
+
blockLight: new Uint8Array(2048)
|
|
228
|
+
})
|
|
229
|
+
).toEqual({ ok: true })
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
test('rejects a foreign publication with bad section payload', () => {
|
|
233
|
+
expect(
|
|
234
|
+
validateOwnerPublicationMessage({
|
|
235
|
+
worldGeneration: 1,
|
|
236
|
+
publicationVersion: 2,
|
|
237
|
+
sections: [{ sx: 0, sy: 64, sz: 0, blockLight: new Uint8Array(8) }]
|
|
238
|
+
})
|
|
239
|
+
).toEqual({ ok: false, reason: 'blockLight' })
|
|
240
|
+
})
|
|
241
|
+
})
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* P-02: explicit mesh versions, covering coalesce, and payload validation.
|
|
4
|
+
* Metadata only — never copies raw meshes or light arrays into logs.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const OWNER_STATES_PER_SECTION = 4096
|
|
8
|
+
export const OWNER_PACKED_LIGHT_BYTES = 2048
|
|
9
|
+
|
|
10
|
+
export type MeshGeometryMode = 'owner' | 'legacyBootstrap'
|
|
11
|
+
|
|
12
|
+
export type MeshResultVersions = {
|
|
13
|
+
sessionEpoch: number
|
|
14
|
+
columnIncarnation: number
|
|
15
|
+
requestId: number
|
|
16
|
+
topologyRevision: number
|
|
17
|
+
lightPublicationVersion: number
|
|
18
|
+
worldGeneration: number
|
|
19
|
+
neighborTopologyRevisions: Array<{ key: string; topologyRevision: number }>
|
|
20
|
+
meshMode: MeshGeometryMode
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type MeshSectionLightRequirement = {
|
|
24
|
+
requiredVersion: number
|
|
25
|
+
worldGeneration: number
|
|
26
|
+
topologyRevision?: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function isUnversionedMesh(mesh: {
|
|
30
|
+
worldGeneration?: number
|
|
31
|
+
lightPublicationVersion?: number
|
|
32
|
+
topologyRevision?: number
|
|
33
|
+
sessionEpoch?: number
|
|
34
|
+
}): boolean {
|
|
35
|
+
return mesh.worldGeneration == null && mesh.lightPublicationVersion == null && mesh.topologyRevision == null && mesh.sessionEpoch == null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function shouldAcceptVersionedMesh(
|
|
39
|
+
mesh: {
|
|
40
|
+
worldGeneration?: number
|
|
41
|
+
lightPublicationVersion?: number
|
|
42
|
+
topologyRevision?: number
|
|
43
|
+
sessionEpoch?: number
|
|
44
|
+
columnIncarnation?: number
|
|
45
|
+
hadErrors?: boolean
|
|
46
|
+
meshMode?: MeshGeometryMode
|
|
47
|
+
},
|
|
48
|
+
required: MeshSectionLightRequirement | null | undefined,
|
|
49
|
+
opts: { ownerManaged: boolean; sessionEpoch?: number; columnIncarnation?: number }
|
|
50
|
+
): boolean {
|
|
51
|
+
if (mesh.hadErrors) return false
|
|
52
|
+
if (opts.ownerManaged) {
|
|
53
|
+
if (opts.sessionEpoch != null && mesh.sessionEpoch != null && mesh.sessionEpoch !== opts.sessionEpoch) return false
|
|
54
|
+
if (opts.columnIncarnation != null && mesh.columnIncarnation != null && mesh.columnIncarnation !== opts.columnIncarnation) {
|
|
55
|
+
return false
|
|
56
|
+
}
|
|
57
|
+
if (mesh.meshMode === 'legacyBootstrap') {
|
|
58
|
+
if (required == null) return true
|
|
59
|
+
if (mesh.lightPublicationVersion == null) return false
|
|
60
|
+
} else if (isUnversionedMesh(mesh)) {
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
63
|
+
} else if (isUnversionedMesh(mesh)) {
|
|
64
|
+
return true
|
|
65
|
+
}
|
|
66
|
+
if (required == null) return true
|
|
67
|
+
if (mesh.worldGeneration != null && mesh.worldGeneration !== required.worldGeneration) return false
|
|
68
|
+
if (required.topologyRevision != null && mesh.topologyRevision != null && mesh.topologyRevision < required.topologyRevision) {
|
|
69
|
+
return false
|
|
70
|
+
}
|
|
71
|
+
const topologyFresh = required.topologyRevision != null && mesh.topologyRevision != null && mesh.topologyRevision >= required.topologyRevision
|
|
72
|
+
if (!topologyFresh && mesh.lightPublicationVersion != null && mesh.lightPublicationVersion < required.requiredVersion) {
|
|
73
|
+
return false
|
|
74
|
+
}
|
|
75
|
+
return true
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function isOwnerGeometryLightStale(mesh: { lightPublicationVersion?: number }, required: MeshSectionLightRequirement | null | undefined): boolean {
|
|
79
|
+
return required != null && mesh.lightPublicationVersion != null && mesh.lightPublicationVersion < required.requiredVersion
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function shouldDispatchCoveringRemesh(opts: {
|
|
83
|
+
required: MeshSectionLightRequirement
|
|
84
|
+
outstandingCount: number
|
|
85
|
+
pendingCoveringVersion?: number
|
|
86
|
+
pendingCoveringGeneration?: number
|
|
87
|
+
}): boolean {
|
|
88
|
+
if (opts.outstandingCount <= 0) return true
|
|
89
|
+
if (opts.pendingCoveringGeneration !== opts.required.worldGeneration) return true
|
|
90
|
+
if ((opts.pendingCoveringVersion ?? -1) < opts.required.requiredVersion) return true
|
|
91
|
+
return false
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Stamped on a section that has not been edited. The first edit is always greater. */
|
|
95
|
+
export const INITIAL_TOPOLOGY_REVISION = 0
|
|
96
|
+
|
|
97
|
+
export function nextTopologyRevision(current: number | undefined): number {
|
|
98
|
+
return (current ?? INITIAL_TOPOLOGY_REVISION) + 1
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function snapshotMeshVersions(input: {
|
|
102
|
+
sessionEpoch: number
|
|
103
|
+
columnIncarnation: number
|
|
104
|
+
requestId: number
|
|
105
|
+
topologyRevision: number
|
|
106
|
+
lightPublicationVersion: number
|
|
107
|
+
worldGeneration: number
|
|
108
|
+
neighborTopologyRevisions: Array<{ key: string; topologyRevision: number }>
|
|
109
|
+
meshMode?: MeshGeometryMode
|
|
110
|
+
}): MeshResultVersions {
|
|
111
|
+
return {
|
|
112
|
+
sessionEpoch: input.sessionEpoch,
|
|
113
|
+
columnIncarnation: input.columnIncarnation,
|
|
114
|
+
requestId: input.requestId,
|
|
115
|
+
topologyRevision: input.topologyRevision,
|
|
116
|
+
lightPublicationVersion: input.lightPublicationVersion,
|
|
117
|
+
worldGeneration: input.worldGeneration,
|
|
118
|
+
neighborTopologyRevisions: input.neighborTopologyRevisions.map(entry => ({
|
|
119
|
+
key: entry.key,
|
|
120
|
+
topologyRevision: entry.topologyRevision
|
|
121
|
+
})),
|
|
122
|
+
meshMode: input.meshMode ?? 'owner'
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function isSafeIntegerId(value: unknown): value is number {
|
|
127
|
+
return typeof value === 'number' && Number.isInteger(value) && Number.isSafeInteger(value) && value >= 0
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function isSafeCoord(value: unknown): value is number {
|
|
131
|
+
return typeof value === 'number' && Number.isInteger(value) && Number.isSafeInteger(value)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function validateSectionKey(key: unknown): boolean {
|
|
135
|
+
if (typeof key !== 'string') return false
|
|
136
|
+
const parts = key.split(',')
|
|
137
|
+
if (parts.length !== 3) return false
|
|
138
|
+
return parts.every(part => {
|
|
139
|
+
const n = Number(part)
|
|
140
|
+
return Number.isInteger(n) && Number.isFinite(n)
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function validateOwnerPackedSection(section: {
|
|
145
|
+
sx?: unknown
|
|
146
|
+
sy?: unknown
|
|
147
|
+
sz?: unknown
|
|
148
|
+
blockLight?: unknown
|
|
149
|
+
skyLight?: unknown
|
|
150
|
+
states?: unknown
|
|
151
|
+
}): { ok: true } | { ok: false; reason: string } {
|
|
152
|
+
if (!isSafeCoord(section.sx)) return { ok: false, reason: 'sx' }
|
|
153
|
+
if (!isSafeCoord(section.sy)) return { ok: false, reason: 'sy' }
|
|
154
|
+
if (!isSafeCoord(section.sz)) return { ok: false, reason: 'sz' }
|
|
155
|
+
if (section.blockLight != null) {
|
|
156
|
+
if (!(section.blockLight instanceof Uint8Array) || section.blockLight.length !== OWNER_PACKED_LIGHT_BYTES) {
|
|
157
|
+
return { ok: false, reason: 'blockLight' }
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (section.skyLight != null) {
|
|
161
|
+
if (!(section.skyLight instanceof Uint8Array) || section.skyLight.length !== OWNER_PACKED_LIGHT_BYTES) {
|
|
162
|
+
return { ok: false, reason: 'skyLight' }
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (section.states != null) {
|
|
166
|
+
if (!(section.states instanceof Uint16Array) || section.states.length !== OWNER_STATES_PER_SECTION) {
|
|
167
|
+
return { ok: false, reason: 'states' }
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return { ok: true }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function validateMeshResultVersions(data: {
|
|
174
|
+
sessionEpoch?: unknown
|
|
175
|
+
columnIncarnation?: unknown
|
|
176
|
+
requestId?: unknown
|
|
177
|
+
topologyRevision?: unknown
|
|
178
|
+
lightPublicationVersion?: unknown
|
|
179
|
+
worldGeneration?: unknown
|
|
180
|
+
key?: unknown
|
|
181
|
+
}): { ok: true } | { ok: false; reason: string } {
|
|
182
|
+
if (data.key != null && !validateSectionKey(data.key)) return { ok: false, reason: 'key' }
|
|
183
|
+
for (const field of ['sessionEpoch', 'columnIncarnation', 'requestId', 'topologyRevision', 'lightPublicationVersion', 'worldGeneration'] as const) {
|
|
184
|
+
if (data[field] == null) continue
|
|
185
|
+
if (!isSafeIntegerId(data[field])) return { ok: false, reason: field }
|
|
186
|
+
}
|
|
187
|
+
return { ok: true }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function validateOwnerPublicationMessage(data: {
|
|
191
|
+
worldGeneration?: unknown
|
|
192
|
+
publicationVersion?: unknown
|
|
193
|
+
sections?: unknown
|
|
194
|
+
}): { ok: true } | { ok: false; reason: string } {
|
|
195
|
+
if (!isSafeIntegerId(data.worldGeneration)) return { ok: false, reason: 'worldGeneration' }
|
|
196
|
+
if (!isSafeIntegerId(data.publicationVersion)) return { ok: false, reason: 'publicationVersion' }
|
|
197
|
+
if (!Array.isArray(data.sections)) return { ok: false, reason: 'sections' }
|
|
198
|
+
for (const section of data.sections) {
|
|
199
|
+
const checked = validateOwnerPackedSection(section ?? {})
|
|
200
|
+
if (!checked.ok) return checked
|
|
201
|
+
}
|
|
202
|
+
return { ok: true }
|
|
203
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { faceNeighborKeys, pendingSectionGroups, selectReadySectionFlushes, selectReadySectionUpdates } from './pendingSectionFlush'
|
|
4
|
+
|
|
5
|
+
const SECTION_HEIGHT = 16
|
|
6
|
+
const MAX_BUFFER_MS = 500
|
|
7
|
+
|
|
8
|
+
type Scene = {
|
|
9
|
+
pending: Map<string, number>
|
|
10
|
+
outstanding: Set<string>
|
|
11
|
+
visible: Set<string>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function select(scene: Scene, now: number) {
|
|
15
|
+
return selectReadySectionUpdates({
|
|
16
|
+
pendingKeys: scene.pending.keys(),
|
|
17
|
+
startedAt: key => scene.pending.get(key),
|
|
18
|
+
now,
|
|
19
|
+
maxBufferMs: MAX_BUFFER_MS,
|
|
20
|
+
sectionHeight: SECTION_HEIGHT,
|
|
21
|
+
isOutstanding: key => scene.outstanding.has(key),
|
|
22
|
+
hasSectionObject: key => scene.visible.has(key)
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The per-key policy this module replaced (worldRendererThree before the group
|
|
28
|
+
* flush): each key waited out its OWN deadline and was released on its own.
|
|
29
|
+
* Kept here so the regression it caused stays pinned by a test.
|
|
30
|
+
*/
|
|
31
|
+
function selectPerKeyLegacy(scene: Scene, now: number) {
|
|
32
|
+
const ready: string[] = []
|
|
33
|
+
for (const key of scene.pending.keys()) {
|
|
34
|
+
const startedAt = scene.pending.get(key) ?? now
|
|
35
|
+
if (now - startedAt < MAX_BUFFER_MS) {
|
|
36
|
+
const busy = faceNeighborKeys(key, SECTION_HEIGHT).some(
|
|
37
|
+
neighbor => scene.outstanding.has(neighbor) && !scene.pending.has(neighbor) && scene.visible.has(neighbor)
|
|
38
|
+
)
|
|
39
|
+
if (busy) continue
|
|
40
|
+
}
|
|
41
|
+
ready.push(key)
|
|
42
|
+
}
|
|
43
|
+
return ready
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe('pendingSectionGroups', () => {
|
|
47
|
+
it('splits buffered sections into face-connected components', () => {
|
|
48
|
+
const groups = pendingSectionGroups(['0,64,0', '0,80,0', '160,64,160'], SECTION_HEIGHT)
|
|
49
|
+
expect(groups).toHaveLength(2)
|
|
50
|
+
expect(groups.find(g => g.length === 2)?.sort()).toEqual(['0,64,0', '0,80,0'])
|
|
51
|
+
expect(groups.find(g => g.length === 1)).toEqual(['160,64,160'])
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
describe('selectReadySectionUpdates', () => {
|
|
56
|
+
it('holds a section while a visible face neighbour is still being re-meshed', () => {
|
|
57
|
+
const scene: Scene = {
|
|
58
|
+
pending: new Map([['0,64,0', 1000]]),
|
|
59
|
+
outstanding: new Set(['16,64,0']),
|
|
60
|
+
visible: new Set(['16,64,0'])
|
|
61
|
+
}
|
|
62
|
+
expect(select(scene, 1050)).toEqual([])
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('flushes as soon as every neighbour of the group has arrived, without waiting the deadline', () => {
|
|
66
|
+
const scene: Scene = {
|
|
67
|
+
pending: new Map([
|
|
68
|
+
['0,64,0', 1000],
|
|
69
|
+
['16,64,0', 1040]
|
|
70
|
+
]),
|
|
71
|
+
outstanding: new Set(),
|
|
72
|
+
visible: new Set(['0,64,0', '16,64,0'])
|
|
73
|
+
}
|
|
74
|
+
expect(select(scene, 1050).sort()).toEqual(['0,64,0', '16,64,0'])
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('does not wait on an air neighbour that has no geometry on screen', () => {
|
|
78
|
+
const scene: Scene = {
|
|
79
|
+
pending: new Map([['0,64,0', 1000]]),
|
|
80
|
+
outstanding: new Set(['0,80,0']),
|
|
81
|
+
visible: new Set(['0,64,0'])
|
|
82
|
+
}
|
|
83
|
+
expect(select(scene, 1010)).toEqual(['0,64,0'])
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// The sky-flash regression: the dug section and its face neighbours are
|
|
87
|
+
// buffered at staggered times (they come back on different worker ticks).
|
|
88
|
+
// Releasing the earliest one alone shows the neighbour's still-culled faces
|
|
89
|
+
// as a hole, so the whole group must go out in the same frame.
|
|
90
|
+
it('releases a staggered group in one batch when the deadline expires', () => {
|
|
91
|
+
const scene: Scene = {
|
|
92
|
+
pending: new Map([
|
|
93
|
+
['0,64,0', 1000],
|
|
94
|
+
['16,64,0', 1120],
|
|
95
|
+
['0,80,0', 1180]
|
|
96
|
+
]),
|
|
97
|
+
outstanding: new Set(['32,64,0']),
|
|
98
|
+
visible: new Set(['0,64,0', '16,64,0', '0,80,0', '32,64,0'])
|
|
99
|
+
}
|
|
100
|
+
expect(select(scene, 1500).sort()).toEqual(['0,64,0', '0,80,0', '16,64,0'])
|
|
101
|
+
// The legacy policy installs 0,64,0 while 16,64,0 keeps the geometry it was
|
|
102
|
+
// meshed with before the edit — the faces between them stay culled on both
|
|
103
|
+
// sides for a frame, which is the hole seen as a sky flash.
|
|
104
|
+
expect(selectPerKeyLegacy(scene, 1500)).not.toContain('16,64,0')
|
|
105
|
+
expect(selectPerKeyLegacy(scene, 1500)).toContain('0,64,0')
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('measures the deadline from the oldest member so a group cannot be starved', () => {
|
|
109
|
+
const scene: Scene = {
|
|
110
|
+
pending: new Map([
|
|
111
|
+
['0,64,0', 1000],
|
|
112
|
+
['16,64,0', 1400]
|
|
113
|
+
]),
|
|
114
|
+
outstanding: new Set(['32,64,0']),
|
|
115
|
+
visible: new Set(['0,64,0', '16,64,0', '32,64,0'])
|
|
116
|
+
}
|
|
117
|
+
expect(select(scene, 1499)).toEqual([])
|
|
118
|
+
expect(select(scene, 1500).sort()).toEqual(['0,64,0', '16,64,0'])
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('keeps unrelated groups independent', () => {
|
|
122
|
+
const scene: Scene = {
|
|
123
|
+
pending: new Map([
|
|
124
|
+
['0,64,0', 1000],
|
|
125
|
+
['160,64,160', 1000]
|
|
126
|
+
]),
|
|
127
|
+
outstanding: new Set(['16,64,0']),
|
|
128
|
+
visible: new Set(['0,64,0', '16,64,0', '160,64,160'])
|
|
129
|
+
}
|
|
130
|
+
expect(select(scene, 1010)).toEqual(['160,64,160'])
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('labels complete groups vs deadline flushes', () => {
|
|
134
|
+
const complete: Scene = {
|
|
135
|
+
pending: new Map([['160,64,160', 1000]]),
|
|
136
|
+
outstanding: new Set(),
|
|
137
|
+
visible: new Set(['160,64,160'])
|
|
138
|
+
}
|
|
139
|
+
expect(
|
|
140
|
+
selectReadySectionFlushes({
|
|
141
|
+
pendingKeys: complete.pending.keys(),
|
|
142
|
+
startedAt: key => complete.pending.get(key),
|
|
143
|
+
now: 1010,
|
|
144
|
+
maxBufferMs: MAX_BUFFER_MS,
|
|
145
|
+
sectionHeight: SECTION_HEIGHT,
|
|
146
|
+
isOutstanding: key => complete.outstanding.has(key),
|
|
147
|
+
hasSectionObject: key => complete.visible.has(key)
|
|
148
|
+
})
|
|
149
|
+
).toEqual([{ key: '160,64,160', reason: 'group-complete' }])
|
|
150
|
+
|
|
151
|
+
const expired: Scene = {
|
|
152
|
+
pending: new Map([
|
|
153
|
+
['0,64,0', 1000],
|
|
154
|
+
['16,64,0', 1120]
|
|
155
|
+
]),
|
|
156
|
+
outstanding: new Set(['32,64,0']),
|
|
157
|
+
visible: new Set(['0,64,0', '16,64,0', '32,64,0'])
|
|
158
|
+
}
|
|
159
|
+
expect(
|
|
160
|
+
selectReadySectionFlushes({
|
|
161
|
+
pendingKeys: expired.pending.keys(),
|
|
162
|
+
startedAt: key => expired.pending.get(key),
|
|
163
|
+
now: 1500,
|
|
164
|
+
maxBufferMs: MAX_BUFFER_MS,
|
|
165
|
+
sectionHeight: SECTION_HEIGHT,
|
|
166
|
+
isOutstanding: key => expired.outstanding.has(key),
|
|
167
|
+
hasSectionObject: key => expired.visible.has(key)
|
|
168
|
+
}).map(item => item.reason)
|
|
169
|
+
).toEqual(['deadline', 'deadline'])
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
describe('selectReadySectionFlushes topology vs covering', () => {
|
|
174
|
+
const flushQuery = (scene: Scene, now: number, topologyOutstanding?: Set<string>) =>
|
|
175
|
+
selectReadySectionFlushes({
|
|
176
|
+
pendingKeys: scene.pending.keys(),
|
|
177
|
+
startedAt: key => scene.pending.get(key),
|
|
178
|
+
now,
|
|
179
|
+
maxBufferMs: MAX_BUFFER_MS,
|
|
180
|
+
sectionHeight: SECTION_HEIGHT,
|
|
181
|
+
isOutstanding: key => scene.outstanding.has(key),
|
|
182
|
+
isTopologyOutstanding: topologyOutstanding ? key => topologyOutstanding.has(key) : undefined,
|
|
183
|
+
hasSectionObject: key => scene.visible.has(key)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('holds a group while a visible neighbour is topology-outstanding', () => {
|
|
187
|
+
const scene: Scene = {
|
|
188
|
+
pending: new Map([['0,64,0', 1000]]),
|
|
189
|
+
outstanding: new Set(),
|
|
190
|
+
visible: new Set(['0,64,0', '16,64,0'])
|
|
191
|
+
}
|
|
192
|
+
expect(flushQuery(scene, 1050, new Set(['16,64,0'])).map(item => item.key)).toEqual([])
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('does not hold a group for covering-only outstanding neighbours', () => {
|
|
196
|
+
const scene: Scene = {
|
|
197
|
+
pending: new Map([['0,64,0', 1000]]),
|
|
198
|
+
outstanding: new Set(['16,64,0']),
|
|
199
|
+
visible: new Set(['0,64,0', '16,64,0'])
|
|
200
|
+
}
|
|
201
|
+
expect(flushQuery(scene, 1050, new Set()).map(item => item.key)).toEqual(['0,64,0'])
|
|
202
|
+
expect(flushQuery(scene, 1050, new Set())[0]?.reason).toBe('group-complete')
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('holds a singleton with a topology-outstanding neighbour until the deadline', () => {
|
|
206
|
+
const scene: Scene = {
|
|
207
|
+
pending: new Map([['0,64,0', 1000]]),
|
|
208
|
+
outstanding: new Set(['16,64,0']),
|
|
209
|
+
visible: new Set(['0,64,0', '16,64,0'])
|
|
210
|
+
}
|
|
211
|
+
expect(flushQuery(scene, 1499, new Set(['16,64,0']))).toEqual([])
|
|
212
|
+
expect(flushQuery(scene, 1500, new Set(['16,64,0']))).toEqual([{ key: '0,64,0', reason: 'deadline' }])
|
|
213
|
+
})
|
|
214
|
+
})
|