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,574 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { Vec3 } from 'vec3'
|
|
3
|
+
import type { WorldRendererConfig } from '../../graphicsBackend/config'
|
|
4
|
+
import type { PlayerStateReactive } from '../../playerState/playerState'
|
|
5
|
+
import type { MesherMainEvent } from '../../mesher-shared/shared'
|
|
6
|
+
import type { RendererLightCache } from '../cache/rendererLightCache'
|
|
7
|
+
import { isClientLightProbeSection, setClientLightProbePositions } from '../debug/clientLightProbe'
|
|
8
|
+
import {
|
|
9
|
+
currentClientLightTraceIds,
|
|
10
|
+
isClientLightTraceArmed,
|
|
11
|
+
isClientLightTraceEnabled,
|
|
12
|
+
nextClientLightEditSeq,
|
|
13
|
+
nextClientLightRequestId,
|
|
14
|
+
recordClientLightTrace
|
|
15
|
+
} from '../debug/clientLightTrace'
|
|
16
|
+
import {
|
|
17
|
+
ClientLightOwnerSession,
|
|
18
|
+
clientLightOwnerVersionBlockReason,
|
|
19
|
+
coveringReplacementForReject,
|
|
20
|
+
meshWorkerIndexesForDirtySections,
|
|
21
|
+
raiseRequiredLightRevisions,
|
|
22
|
+
shouldAcceptMeshGeometry,
|
|
23
|
+
shouldSpawnClientLightOwner,
|
|
24
|
+
skyLightEnabledFromRendererState,
|
|
25
|
+
type OwnerPublicationApplyResult
|
|
26
|
+
} from '../owner/clientLightOwner'
|
|
27
|
+
import {
|
|
28
|
+
INITIAL_TOPOLOGY_REVISION,
|
|
29
|
+
isOwnerGeometryLightStale,
|
|
30
|
+
nextTopologyRevision,
|
|
31
|
+
shouldDispatchCoveringRemesh,
|
|
32
|
+
validateMeshResultVersions,
|
|
33
|
+
type MeshSectionLightRequirement
|
|
34
|
+
} from '../versioning/clientLightVersions'
|
|
35
|
+
|
|
36
|
+
const CHUNK_SIZE = 16
|
|
37
|
+
|
|
38
|
+
export type DirtyLightMeta = { lightPublicationVersion?: number; worldGeneration?: number }
|
|
39
|
+
|
|
40
|
+
/** What the coordinator needs from the world renderer. Getters are read lazily, the renderer replaces `workers` on reset. */
|
|
41
|
+
export type ClientLightHost = {
|
|
42
|
+
readonly version: string
|
|
43
|
+
readonly workers: Worker[]
|
|
44
|
+
readonly rendererLightCache: RendererLightCache
|
|
45
|
+
readonly worldRendererConfig: WorldRendererConfig
|
|
46
|
+
readonly worldSizeParams: { minY: number; worldHeight: number }
|
|
47
|
+
readonly playerStateReactive: PlayerStateReactive
|
|
48
|
+
readonly sectionsWaiting: Map<string, number>
|
|
49
|
+
readonly loadedChunks: Record<string, boolean>
|
|
50
|
+
/** Last stored light stamp per section, owned by the renderer's dirty throttle. */
|
|
51
|
+
readonly sectionDirtyLightMeta: Map<string, { lightPublicationVersion: number; worldGeneration?: number }>
|
|
52
|
+
getSectionHeight(): number
|
|
53
|
+
getWorkerNumber(pos: Vec3, updateAction?: boolean): number
|
|
54
|
+
setSectionDirty(pos: Vec3, value?: boolean, useChangeWorker?: boolean): void
|
|
55
|
+
dispatchDirtyImmediate(pos: Vec3, value: boolean, useChangeWorker: boolean): void
|
|
56
|
+
createLightOwnerWorker(onMessage: (data: any) => void): Worker
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
type OwnerGeometryEvent = {
|
|
60
|
+
type?: string
|
|
61
|
+
key?: string
|
|
62
|
+
worldGeneration?: number
|
|
63
|
+
lightPublicationVersion?: number
|
|
64
|
+
topologyRevision?: number
|
|
65
|
+
sessionEpoch?: number
|
|
66
|
+
columnIncarnation?: number
|
|
67
|
+
hadErrors?: boolean
|
|
68
|
+
meshMode?: 'owner' | 'legacyBootstrap'
|
|
69
|
+
geometry?: { hadErrors?: boolean }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const columnMatches = (key: string, x: number, z: number) => key.startsWith(`${x},`) && key.endsWith(`,${z}`)
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Owns the client light owner session and every piece of version bookkeeping that keeps owner-lit meshes
|
|
76
|
+
* consistent: column incarnations, per-section topology revisions, outstanding covering remeshes and
|
|
77
|
+
* player edits waiting for their light. The world renderer only forwards events and stays unaware of the details.
|
|
78
|
+
*/
|
|
79
|
+
export class ClientLightCoordinator {
|
|
80
|
+
session: ClientLightOwnerSession | null = null
|
|
81
|
+
failureReason: string | null = null
|
|
82
|
+
fence = { sessionEpoch: 1, rejectStaleOwnerMeshes: false }
|
|
83
|
+
readonly columnIncarnationByColumn = new Map<string, number>()
|
|
84
|
+
readonly topologyRevisionBySection = new Map<string, number>()
|
|
85
|
+
readonly pendingTopologyBySection = new Map<string, number>()
|
|
86
|
+
readonly pendingCoveringBySection = new Map<string, MeshSectionLightRequirement>()
|
|
87
|
+
readonly rejectedFinishedBySection = new Map<string, number>()
|
|
88
|
+
/** Stamp applied to the next dirty dispatch; set only while a publication is being turned into remeshes. */
|
|
89
|
+
nextDirtyLightMeta: DirtyLightMeta = {}
|
|
90
|
+
/** Dispatch the next dirty message ahead of the bulk queue. */
|
|
91
|
+
nextDirtyUrgent = false
|
|
92
|
+
/**
|
|
93
|
+
* Player edits whose covering light publication is still outstanding.
|
|
94
|
+
* `lightVersionAtEdit` is the owner version already on screen. Geometry at that
|
|
95
|
+
* version must not clear the edit: the publication for this edit is a newer one.
|
|
96
|
+
*/
|
|
97
|
+
readonly interactiveSections = new Map<string, { lightVersionAtEdit: number; editGeneration: number; coveringVersion?: number }>()
|
|
98
|
+
|
|
99
|
+
constructor(private readonly host: ClientLightHost) {}
|
|
100
|
+
|
|
101
|
+
// --- session lifecycle -------------------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
hasOwner(): boolean {
|
|
104
|
+
return this.session?.isReady === true
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
isSessionLive(): boolean {
|
|
108
|
+
return this.session != null
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
getWorker(): Worker | null {
|
|
112
|
+
return this.session?.worker ?? null
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
getFailureReason(): string | null {
|
|
116
|
+
return this.failureReason ?? this.session?.lastFailureReason ?? null
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
setProbe(points: Array<[number, number, number]>) {
|
|
120
|
+
setClientLightProbePositions(points)
|
|
121
|
+
for (const worker of this.host.workers) worker.postMessage({ type: 'clientLightProbe', points })
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
syncTraceConfig() {
|
|
125
|
+
const message = {
|
|
126
|
+
type: 'clientLightTraceConfig' as const,
|
|
127
|
+
enabled: isClientLightTraceEnabled(),
|
|
128
|
+
armed: isClientLightTraceArmed()
|
|
129
|
+
}
|
|
130
|
+
for (const worker of this.host.workers) {
|
|
131
|
+
worker.postMessage(message)
|
|
132
|
+
}
|
|
133
|
+
this.session?.worker.postMessage(message)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
maybeSpawn() {
|
|
137
|
+
if (this.session) return
|
|
138
|
+
const { worldRendererConfig, version } = this.host
|
|
139
|
+
if (!shouldSpawnClientLightOwner(worldRendererConfig, version)) {
|
|
140
|
+
if (worldRendererConfig.enableClientLightOwner === true) {
|
|
141
|
+
this.failureReason = clientLightOwnerVersionBlockReason(version)
|
|
142
|
+
}
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
this.fence = { sessionEpoch: this.fence.sessionEpoch, rejectStaleOwnerMeshes: false }
|
|
146
|
+
this.failureReason = null
|
|
147
|
+
this.session = new ClientLightOwnerSession(this.host.rendererLightCache, {
|
|
148
|
+
createWorker: onMessage => this.host.createLightOwnerWorker(onMessage),
|
|
149
|
+
worldMinY: this.host.worldSizeParams.minY,
|
|
150
|
+
worldHeight: this.host.worldSizeParams.worldHeight,
|
|
151
|
+
skyLightEnabled: skyLightEnabledFromRendererState(this.host.playerStateReactive),
|
|
152
|
+
onApplied: result => this.onPublication(result),
|
|
153
|
+
onFailed: reason => this.onFailed(reason),
|
|
154
|
+
onOwnerIdle: settledEditGeneration => this.releaseInteractiveEditsWithoutCoveringPublication(settledEditGeneration)
|
|
155
|
+
})
|
|
156
|
+
this.postAcceptFence()
|
|
157
|
+
this.syncTraceConfig()
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
terminate() {
|
|
161
|
+
if (this.session) {
|
|
162
|
+
this.revertToIncoming('owner-disabled')
|
|
163
|
+
}
|
|
164
|
+
this.session?.terminate()
|
|
165
|
+
this.session = null
|
|
166
|
+
this.nextDirtyLightMeta = {}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private onFailed(reason: string) {
|
|
170
|
+
this.failureReason = reason
|
|
171
|
+
this.revertToIncoming(reason)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private revertToIncoming(reason: string) {
|
|
175
|
+
this.fence = {
|
|
176
|
+
sessionEpoch: this.fence.sessionEpoch + 1,
|
|
177
|
+
rejectStaleOwnerMeshes: true
|
|
178
|
+
}
|
|
179
|
+
this.failureReason = reason
|
|
180
|
+
this.interactiveSections.clear()
|
|
181
|
+
this.session?.requiredLightBySection.clear()
|
|
182
|
+
const message = {
|
|
183
|
+
type: 'revertOwnerLightToIncoming' as const,
|
|
184
|
+
sessionEpoch: this.fence.sessionEpoch,
|
|
185
|
+
worldGeneration: this.session?.gate.acceptedGeneration ?? 1
|
|
186
|
+
}
|
|
187
|
+
for (const worker of this.host.workers) {
|
|
188
|
+
worker.postMessage(message)
|
|
189
|
+
worker.postMessage({
|
|
190
|
+
type: 'setOwnerAcceptFence',
|
|
191
|
+
sessionEpoch: this.fence.sessionEpoch,
|
|
192
|
+
rejectOwnerPublications: true
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private postAcceptFence() {
|
|
198
|
+
for (const worker of this.host.workers) {
|
|
199
|
+
worker.postMessage({
|
|
200
|
+
type: 'setOwnerAcceptFence',
|
|
201
|
+
sessionEpoch: this.fence.sessionEpoch,
|
|
202
|
+
rejectOwnerPublications: this.fence.rejectStaleOwnerMeshes
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Mesh workers were replaced: their topology revisions belong to the previous mesher lifetime. */
|
|
208
|
+
forgetRequiredTopology() {
|
|
209
|
+
for (const required of this.session?.requiredLightBySection.values() ?? []) {
|
|
210
|
+
delete required.topologyRevision
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Drops everything tied to outstanding mesher requests (the workers that owed us answers are gone). */
|
|
215
|
+
clearPendingMesherState() {
|
|
216
|
+
this.pendingCoveringBySection.clear()
|
|
217
|
+
this.pendingTopologyBySection.clear()
|
|
218
|
+
this.rejectedFinishedBySection.clear()
|
|
219
|
+
this.interactiveSections.clear()
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// --- publications ------------------------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
onPublication(result: OwnerPublicationApplyResult) {
|
|
225
|
+
const { workers } = this.host
|
|
226
|
+
if (result.workerMessage) {
|
|
227
|
+
const workerMessage = {
|
|
228
|
+
...result.workerMessage,
|
|
229
|
+
sessionEpoch: this.fence.sessionEpoch
|
|
230
|
+
}
|
|
231
|
+
// A column's own worker needs its light even when nothing there is dirty; a missing section meshes as sky 15.
|
|
232
|
+
const targets = meshWorkerIndexesForDirtySections([...result.dirtyMeshSections, ...workerMessage.sections], workers.length, (sx, sy, sz) =>
|
|
233
|
+
this.host.getWorkerNumber(new Vec3(sx, sy, sz), true)
|
|
234
|
+
)
|
|
235
|
+
const indexes = targets.length ? targets : workers.map((_, index) => index)
|
|
236
|
+
for (const index of indexes) {
|
|
237
|
+
workers[index]?.postMessage(workerMessage)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
this.nextDirtyLightMeta = {
|
|
241
|
+
lightPublicationVersion: result.lastVersion,
|
|
242
|
+
worldGeneration: result.acceptedGeneration
|
|
243
|
+
}
|
|
244
|
+
this.noteCoveringPublication(
|
|
245
|
+
result.lastVersion,
|
|
246
|
+
result.dirtyMeshSections.map(section => `${section.sx},${section.sy},${section.sz}`)
|
|
247
|
+
)
|
|
248
|
+
try {
|
|
249
|
+
if (this.session) {
|
|
250
|
+
raiseRequiredLightRevisions(
|
|
251
|
+
this.session.requiredLightBySection,
|
|
252
|
+
result.dirtyMeshSections,
|
|
253
|
+
result.lastVersion,
|
|
254
|
+
result.acceptedGeneration,
|
|
255
|
+
this.topologyRevisionBySection
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
for (const section of result.dirtyMeshSections) {
|
|
259
|
+
const key = `${section.sx},${section.sy},${section.sz}`
|
|
260
|
+
const required = this.session?.requiredLightForSection(key)
|
|
261
|
+
this.pendingCoveringBySection.set(
|
|
262
|
+
key,
|
|
263
|
+
required ?? {
|
|
264
|
+
requiredVersion: result.lastVersion,
|
|
265
|
+
worldGeneration: result.acceptedGeneration
|
|
266
|
+
}
|
|
267
|
+
)
|
|
268
|
+
const urgentCovering = this.sectionTouchesInteractiveWork(key)
|
|
269
|
+
this.nextDirtyUrgent = urgentCovering
|
|
270
|
+
try {
|
|
271
|
+
this.host.setSectionDirty(new Vec3(section.sx, section.sy, section.sz), true, true)
|
|
272
|
+
} finally {
|
|
273
|
+
this.nextDirtyUrgent = false
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
} finally {
|
|
277
|
+
this.nextDirtyLightMeta = {}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// --- mesh results ------------------------------------------------------------------------------------------
|
|
282
|
+
|
|
283
|
+
columnIncarnationFor(columnKey: string): number {
|
|
284
|
+
return this.columnIncarnationByColumn.get(columnKey) ?? 1
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
evaluateOwnerGeometry(data: OwnerGeometryEvent): { accepted: boolean; keepCovering: boolean } {
|
|
288
|
+
if (data.type !== 'geometry') return { accepted: true, keepCovering: false }
|
|
289
|
+
const ownerManaged = this.session?.isReady === true || this.fence.rejectStaleOwnerMeshes
|
|
290
|
+
if (!ownerManaged && this.session == null) return { accepted: true, keepCovering: false }
|
|
291
|
+
if (data.key) {
|
|
292
|
+
const versions = validateMeshResultVersions({ ...data, key: data.key })
|
|
293
|
+
if (!versions.ok && this.session?.isReady) return { accepted: false, keepCovering: false }
|
|
294
|
+
}
|
|
295
|
+
const required = data.key ? this.session?.requiredLightForSection(data.key) : undefined
|
|
296
|
+
const [x, , z] = (data.key ?? '').split(',').map(Number)
|
|
297
|
+
const accepted = shouldAcceptMeshGeometry(
|
|
298
|
+
{
|
|
299
|
+
worldGeneration: data.worldGeneration,
|
|
300
|
+
lightPublicationVersion: data.lightPublicationVersion,
|
|
301
|
+
topologyRevision: data.topologyRevision,
|
|
302
|
+
sessionEpoch: data.sessionEpoch,
|
|
303
|
+
columnIncarnation: data.columnIncarnation,
|
|
304
|
+
hadErrors: data.hadErrors ?? data.geometry?.hadErrors,
|
|
305
|
+
meshMode: data.meshMode
|
|
306
|
+
},
|
|
307
|
+
this.session?.gate ?? { acceptedGeneration: 1, lastVersion: 0 },
|
|
308
|
+
required,
|
|
309
|
+
{
|
|
310
|
+
ownerManaged,
|
|
311
|
+
sessionEpoch: this.fence.sessionEpoch,
|
|
312
|
+
columnIncarnation: Number.isFinite(x) && Number.isFinite(z) ? this.columnIncarnationFor(`${x},${z}`) : undefined
|
|
313
|
+
}
|
|
314
|
+
)
|
|
315
|
+
return {
|
|
316
|
+
accepted,
|
|
317
|
+
keepCovering: accepted && isOwnerGeometryLightStale(data, required)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
acknowledgeAcceptedTopology(data: { key?: string; topologyRevision?: number }) {
|
|
322
|
+
if (!data.key || data.topologyRevision == null) return
|
|
323
|
+
const pending = this.pendingTopologyBySection.get(data.key)
|
|
324
|
+
if (pending != null && data.topologyRevision >= pending) {
|
|
325
|
+
this.pendingTopologyBySection.delete(data.key)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
maybeDispatchCoveringRemesh(key: string, required?: MeshSectionLightRequirement | null) {
|
|
330
|
+
required ??= this.session?.requiredLightForSection(key)
|
|
331
|
+
if (required == null) return
|
|
332
|
+
const outstandingCount = this.host.sectionsWaiting.get(key) ?? 0
|
|
333
|
+
const pending = this.pendingCoveringBySection.get(key)
|
|
334
|
+
if (
|
|
335
|
+
!shouldDispatchCoveringRemesh({
|
|
336
|
+
required,
|
|
337
|
+
outstandingCount,
|
|
338
|
+
pendingCoveringVersion: pending?.requiredVersion,
|
|
339
|
+
pendingCoveringGeneration: pending?.worldGeneration
|
|
340
|
+
})
|
|
341
|
+
) {
|
|
342
|
+
return
|
|
343
|
+
}
|
|
344
|
+
const replacement = coveringReplacementForReject({ accepted: false, required })
|
|
345
|
+
const [x, y, z] = key.split(',').map(Number)
|
|
346
|
+
if (replacement && Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(z) && this.host.loadedChunks[`${x},${z}`]) {
|
|
347
|
+
this.pendingCoveringBySection.set(key, {
|
|
348
|
+
requiredVersion: replacement.lightPublicationVersion,
|
|
349
|
+
worldGeneration: replacement.worldGeneration,
|
|
350
|
+
topologyRevision: required.topologyRevision
|
|
351
|
+
})
|
|
352
|
+
this.host.sectionDirtyLightMeta.set(key, replacement)
|
|
353
|
+
this.nextDirtyLightMeta = replacement
|
|
354
|
+
this.nextDirtyUrgent = this.sectionTouchesInteractiveWork(key)
|
|
355
|
+
try {
|
|
356
|
+
this.host.dispatchDirtyImmediate(new Vec3(x, y, z), true, true)
|
|
357
|
+
} finally {
|
|
358
|
+
this.nextDirtyLightMeta = {}
|
|
359
|
+
this.nextDirtyUrgent = false
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
noteRejectedOwnerGeometry(key: string, required: MeshSectionLightRequirement | null | undefined, opts?: { expectLaterFinished?: boolean }) {
|
|
365
|
+
if (opts?.expectLaterFinished !== false) {
|
|
366
|
+
this.rejectedFinishedBySection.set(key, (this.rejectedFinishedBySection.get(key) ?? 0) + 1)
|
|
367
|
+
return
|
|
368
|
+
}
|
|
369
|
+
this.maybeDispatchCoveringRemesh(key, required)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** Returns true when the geometry must be dropped. */
|
|
373
|
+
rejectStaleOwnerGeometry(data: MesherMainEvent & OwnerGeometryEvent): boolean {
|
|
374
|
+
if (data.type !== 'geometry') return false
|
|
375
|
+
const decision = this.evaluateOwnerGeometry(data)
|
|
376
|
+
if (isClientLightProbeSection(data.key)) {
|
|
377
|
+
console.log('[lightProbe:mesh]', {
|
|
378
|
+
time: Date.now(),
|
|
379
|
+
key: data.key,
|
|
380
|
+
lightPublicationVersion: data.lightPublicationVersion,
|
|
381
|
+
required: this.session?.requiredLightForSection(data.key),
|
|
382
|
+
accepted: decision.accepted,
|
|
383
|
+
keepCovering: decision.keepCovering
|
|
384
|
+
})
|
|
385
|
+
}
|
|
386
|
+
if (!decision.accepted) {
|
|
387
|
+
this.noteRejectedOwnerGeometry(data.key, this.session?.requiredLightForSection(data.key), {
|
|
388
|
+
expectLaterFinished: true
|
|
389
|
+
})
|
|
390
|
+
return true
|
|
391
|
+
}
|
|
392
|
+
this.acknowledgeAcceptedTopology(data)
|
|
393
|
+
if (decision.keepCovering) this.maybeDispatchCoveringRemesh(data.key)
|
|
394
|
+
else this.releaseInteractiveSectionIfLightCommitted(data)
|
|
395
|
+
return false
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** `sectionFinished` arrived: true when it answers a geometry we rejected (so the section is not finished yet). */
|
|
399
|
+
consumeRejectedFinish(key: string): boolean {
|
|
400
|
+
const rejectedFinishes = this.rejectedFinishedBySection.get(key) ?? 0
|
|
401
|
+
if (rejectedFinishes <= 0) return false
|
|
402
|
+
const nextRejected = rejectedFinishes - 1
|
|
403
|
+
if (nextRejected === 0) this.rejectedFinishedBySection.delete(key)
|
|
404
|
+
else this.rejectedFinishedBySection.set(key, nextRejected)
|
|
405
|
+
return true
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
forgetCovering(key: string) {
|
|
409
|
+
this.pendingCoveringBySection.delete(key)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// --- dirty dispatch ----------------------------------------------------------------------------------------
|
|
413
|
+
|
|
414
|
+
/** Version stamps for a dirty message of section `key`; the caller spreads them into the message. */
|
|
415
|
+
describeDirty(pos: Vec3, key: string) {
|
|
416
|
+
const lightMeta = this.nextDirtyLightMeta.lightPublicationVersion != null ? this.nextDirtyLightMeta : this.host.sectionDirtyLightMeta.get(key)
|
|
417
|
+
const ownerManaged = this.session != null || this.fence.rejectStaleOwnerMeshes
|
|
418
|
+
const requestId = ownerManaged || isClientLightTraceEnabled() ? nextClientLightRequestId() : undefined
|
|
419
|
+
const traceIds = isClientLightTraceEnabled() ? { ...currentClientLightTraceIds(), requestId } : undefined
|
|
420
|
+
const ownerVersions = ownerManaged
|
|
421
|
+
? {
|
|
422
|
+
sessionEpoch: this.fence.sessionEpoch,
|
|
423
|
+
columnIncarnation: this.columnIncarnationFor(`${Math.floor(pos.x / CHUNK_SIZE) * CHUNK_SIZE},${Math.floor(pos.z / CHUNK_SIZE) * CHUNK_SIZE}`),
|
|
424
|
+
requestId,
|
|
425
|
+
topologyRevision: this.topologyRevisionBySection.get(key) ?? INITIAL_TOPOLOGY_REVISION,
|
|
426
|
+
neighborTopologyRevisions: this.snapshotNeighborTopology(key)
|
|
427
|
+
}
|
|
428
|
+
: {}
|
|
429
|
+
return { lightMeta, ownerVersions, traceIds }
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** Runs `fn` with the urgent flag temporarily set from a deferred dispatch. */
|
|
433
|
+
withDirtyUrgency(urgent: boolean, fn: () => void) {
|
|
434
|
+
const previousUrgent = this.nextDirtyUrgent
|
|
435
|
+
this.nextDirtyUrgent = urgent
|
|
436
|
+
try {
|
|
437
|
+
fn()
|
|
438
|
+
} finally {
|
|
439
|
+
this.nextDirtyUrgent = previousUrgent
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// --- block edits and columns -------------------------------------------------------------------------------
|
|
444
|
+
|
|
445
|
+
/** A block changed: forward it to the owner, bump topology (also of touched neighbours) and track the player edit. */
|
|
446
|
+
noteBlockChange(pos: Vec3, stateId: number | undefined, neighborUpdates: boolean) {
|
|
447
|
+
const sectionHeight = this.host.getSectionHeight()
|
|
448
|
+
if (stateId !== undefined) {
|
|
449
|
+
this.session?.onBlockChange(pos.x, pos.y, pos.z, stateId)
|
|
450
|
+
}
|
|
451
|
+
this.bumpTopologyRevision(pos)
|
|
452
|
+
if (neighborUpdates) {
|
|
453
|
+
if ((pos.x & 15) === 0) this.bumpTopologyRevision(pos.offset(-CHUNK_SIZE, 0, 0))
|
|
454
|
+
if ((pos.x & 15) === 15) this.bumpTopologyRevision(pos.offset(CHUNK_SIZE, 0, 0))
|
|
455
|
+
if ((pos.y & (sectionHeight - 1)) === 0) this.bumpTopologyRevision(pos.offset(0, -sectionHeight, 0))
|
|
456
|
+
if ((pos.y & (sectionHeight - 1)) === sectionHeight - 1) this.bumpTopologyRevision(pos.offset(0, sectionHeight, 0))
|
|
457
|
+
if ((pos.z & 15) === 0) this.bumpTopologyRevision(pos.offset(0, 0, -CHUNK_SIZE))
|
|
458
|
+
if ((pos.z & 15) === 15) this.bumpTopologyRevision(pos.offset(0, 0, CHUNK_SIZE))
|
|
459
|
+
}
|
|
460
|
+
if (isClientLightTraceEnabled()) {
|
|
461
|
+
recordClientLightTrace({
|
|
462
|
+
phase: 'blockChange',
|
|
463
|
+
editSeq: nextClientLightEditSeq(),
|
|
464
|
+
sectionKey: this.sectionKeyFor(pos.x, pos.y, pos.z),
|
|
465
|
+
blockVersion: stateId
|
|
466
|
+
})
|
|
467
|
+
}
|
|
468
|
+
this.noteInteractiveSection(pos)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
onColumnRemoved(x: number, z: number, opts?: { preserveClientLight?: boolean }) {
|
|
472
|
+
if (opts?.preserveClientLight && this.session?.isReady) {
|
|
473
|
+
this.session.onGeometryReload(x, z)
|
|
474
|
+
} else {
|
|
475
|
+
this.session?.onUnload(x, z)
|
|
476
|
+
}
|
|
477
|
+
const columnKey = `${x},${z}`
|
|
478
|
+
this.columnIncarnationByColumn.set(columnKey, this.columnIncarnationFor(columnKey) + 1)
|
|
479
|
+
for (const map of [this.topologyRevisionBySection, this.pendingCoveringBySection, this.pendingTopologyBySection, this.rejectedFinishedBySection]) {
|
|
480
|
+
for (const key of [...map.keys()]) {
|
|
481
|
+
if (columnMatches(key, x, z)) map.delete(key)
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
forgetInteractiveSection(key: string) {
|
|
487
|
+
this.interactiveSections.delete(key)
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// --- internals ---------------------------------------------------------------------------------------------
|
|
491
|
+
|
|
492
|
+
private sectionKeyFor(x: number, y: number, z: number): string {
|
|
493
|
+
const sectionHeight = this.host.getSectionHeight()
|
|
494
|
+
return `${Math.floor(x / CHUNK_SIZE) * CHUNK_SIZE},${Math.floor(y / sectionHeight) * sectionHeight},${Math.floor(z / CHUNK_SIZE) * CHUNK_SIZE}`
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
private noteInteractiveSection(pos: Vec3) {
|
|
498
|
+
const key = this.sectionKeyFor(pos.x, pos.y, pos.z)
|
|
499
|
+
const requiredVersion = this.session?.requiredLightForSection(key)?.requiredVersion ?? 0
|
|
500
|
+
const gateVersion = this.session?.gate.lastVersion ?? 0
|
|
501
|
+
const prev = this.interactiveSections.get(key)
|
|
502
|
+
this.interactiveSections.set(key, {
|
|
503
|
+
lightVersionAtEdit: Math.max(prev?.lightVersionAtEdit ?? 0, requiredVersion, gateVersion),
|
|
504
|
+
editGeneration: this.session?.notePlayerEdit() ?? 0,
|
|
505
|
+
coveringVersion: undefined
|
|
506
|
+
})
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
sectionTouchesInteractiveWork(key: string): boolean {
|
|
510
|
+
if (this.interactiveSections.has(key)) return true
|
|
511
|
+
const [x, y, z] = key.split(',').map(Number)
|
|
512
|
+
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) return false
|
|
513
|
+
const sectionHeight = this.host.getSectionHeight()
|
|
514
|
+
for (let dx = -16; dx <= 16; dx += 16) {
|
|
515
|
+
for (let dy = -sectionHeight; dy <= sectionHeight; dy += sectionHeight) {
|
|
516
|
+
for (let dz = -16; dz <= 16; dz += 16) {
|
|
517
|
+
if (this.interactiveSections.has(`${x + dx},${y + dy},${z + dz}`)) return true
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return false
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
private noteCoveringPublication(version: number, dirtyKeys: Iterable<string>) {
|
|
525
|
+
const dirty = new Set(dirtyKeys)
|
|
526
|
+
for (const [key, edit] of this.interactiveSections) {
|
|
527
|
+
if (!dirty.has(key) || version <= edit.lightVersionAtEdit) continue
|
|
528
|
+
this.interactiveSections.set(key, {
|
|
529
|
+
lightVersionAtEdit: edit.lightVersionAtEdit,
|
|
530
|
+
editGeneration: edit.editGeneration,
|
|
531
|
+
coveringVersion: Math.max(edit.coveringVersion ?? 0, version)
|
|
532
|
+
})
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
private releaseInteractiveEditsWithoutCoveringPublication(settledEditGeneration: number) {
|
|
537
|
+
for (const [key, edit] of this.interactiveSections) {
|
|
538
|
+
if (edit.coveringVersion == null && edit.editGeneration <= settledEditGeneration) this.interactiveSections.delete(key)
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
private releaseInteractiveSectionIfLightCommitted(data: { key?: string; lightPublicationVersion?: number }) {
|
|
543
|
+
if (!data.key || data.lightPublicationVersion == null) return
|
|
544
|
+
const edit = this.interactiveSections.get(data.key)
|
|
545
|
+
if (edit?.coveringVersion == null || data.lightPublicationVersion < edit.coveringVersion) return
|
|
546
|
+
this.interactiveSections.delete(data.key)
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
private bumpTopologyRevision(pos: Vec3) {
|
|
550
|
+
const key = this.sectionKeyFor(pos.x, pos.y, pos.z)
|
|
551
|
+
const next = nextTopologyRevision(this.topologyRevisionBySection.get(key))
|
|
552
|
+
this.topologyRevisionBySection.set(key, next)
|
|
553
|
+
if (this.session != null) {
|
|
554
|
+
this.pendingTopologyBySection.set(key, next)
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
private snapshotNeighborTopology(key: string): Array<{ key: string; topologyRevision: number }> {
|
|
559
|
+
const [x, y, z] = key.split(',').map(Number)
|
|
560
|
+
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) return []
|
|
561
|
+
const sectionHeight = this.host.getSectionHeight()
|
|
562
|
+
const out: Array<{ key: string; topologyRevision: number }> = []
|
|
563
|
+
for (let dx = -16; dx <= 16; dx += 16) {
|
|
564
|
+
for (let dy = -sectionHeight; dy <= sectionHeight; dy += sectionHeight) {
|
|
565
|
+
for (let dz = -16; dz <= 16; dz += 16) {
|
|
566
|
+
const neighborKey = `${x + dx},${y + dy},${z + dz}`
|
|
567
|
+
const topologyRevision = this.topologyRevisionBySection.get(neighborKey)
|
|
568
|
+
if (topologyRevision != null) out.push({ key: neighborKey, topologyRevision })
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return out
|
|
573
|
+
}
|
|
574
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { describe, it, expect } from 'vitest'
|
|
3
|
+
import { setClientLightProbePositions, isClientLightProbeBlock, isClientLightProbeSection } from './clientLightProbe'
|
|
4
|
+
|
|
5
|
+
describe('client light probe selection', () => {
|
|
6
|
+
it('maps negative and boundary coordinates to section origins', () => {
|
|
7
|
+
setClientLightProbePositions([
|
|
8
|
+
[-1, 63, 16],
|
|
9
|
+
[16, 64, 0]
|
|
10
|
+
])
|
|
11
|
+
expect(isClientLightProbeBlock(-1, 63, 16)).toBe(true)
|
|
12
|
+
expect(isClientLightProbeSection('-16,48,16')).toBe(true)
|
|
13
|
+
expect(isClientLightProbeSection('16,64,0')).toBe(true)
|
|
14
|
+
expect(isClientLightProbeBlock(0, 63, 16)).toBe(false)
|
|
15
|
+
setClientLightProbePositions([])
|
|
16
|
+
expect(isClientLightProbeSection('-16,48,16')).toBe(false)
|
|
17
|
+
})
|
|
18
|
+
it('rejects invalid coordinates without changing the selection', () => {
|
|
19
|
+
setClientLightProbePositions([[1, 2, 3]])
|
|
20
|
+
expect(() => setClientLightProbePositions([[1.5, 2, 3]])).toThrow()
|
|
21
|
+
expect(isClientLightProbeBlock(1, 2, 3)).toBe(true)
|
|
22
|
+
setClientLightProbePositions([])
|
|
23
|
+
})
|
|
24
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
// Temporary opt-in diagnostics. Each worker receives its own copy of the selection.
|
|
3
|
+
let positions = new Set<string>()
|
|
4
|
+
let sections = new Set<string>()
|
|
5
|
+
|
|
6
|
+
export function setClientLightProbePositions(points: Array<[number, number, number]>) {
|
|
7
|
+
if (!Array.isArray(points) || points.some(point => !Array.isArray(point) || point.length !== 3 || point.some(value => !Number.isInteger(value)))) {
|
|
8
|
+
throw new Error('Light probe positions must be integer [x, y, z] triples')
|
|
9
|
+
}
|
|
10
|
+
positions = new Set(points.map(point => point.join(',')))
|
|
11
|
+
sections = new Set(points.map(point => point.map(value => Math.floor(value / 16) * 16).join(',')))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isClientLightProbeBlock(x: number, y: number, z: number) {
|
|
15
|
+
return positions.has(`${x},${y},${z}`)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isClientLightProbeSection(key: string) {
|
|
19
|
+
return sections.has(key)
|
|
20
|
+
}
|