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,271 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { afterEach, describe, expect, test, vi } from 'vitest'
|
|
3
|
+
import {
|
|
4
|
+
CLIENT_LIGHT_TRACE_BATCH_MS,
|
|
5
|
+
CLIENT_LIGHT_TRACE_BATCH_SIZE,
|
|
6
|
+
CLIENT_LIGHT_TRACE_CAPACITY,
|
|
7
|
+
armClientLightTrace,
|
|
8
|
+
bumpClientLightTraceSessionEpoch,
|
|
9
|
+
clearClientLightTrace,
|
|
10
|
+
comparableNow,
|
|
11
|
+
dumpClientLightTrace,
|
|
12
|
+
enableClientLightTrace,
|
|
13
|
+
flushClientLightTracePosts,
|
|
14
|
+
getClientLightTraceAggregates,
|
|
15
|
+
getClientLightTraceEvents,
|
|
16
|
+
ingestRemoteClientLightTrace,
|
|
17
|
+
ingestRemoteClientLightTraceMessage,
|
|
18
|
+
isClientLightTraceArmed,
|
|
19
|
+
isClientLightTraceEnabled,
|
|
20
|
+
isClientLightTraceMessage,
|
|
21
|
+
maybeEnableClientLightTraceFromLocation,
|
|
22
|
+
nextClientLightEditSeq,
|
|
23
|
+
nextClientLightRequestId,
|
|
24
|
+
postClientLightTrace,
|
|
25
|
+
recordClientLightTrace,
|
|
26
|
+
recordClientLightTraceGpuSample,
|
|
27
|
+
requestClientLightTraceSample
|
|
28
|
+
} from './clientLightTrace'
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
flushClientLightTracePosts()
|
|
32
|
+
enableClientLightTrace(false)
|
|
33
|
+
clearClientLightTrace()
|
|
34
|
+
vi.useRealTimers()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('clientLightTrace is disabled by default and drops records', () => {
|
|
38
|
+
expect(isClientLightTraceEnabled()).toBe(false)
|
|
39
|
+
recordClientLightTrace({ phase: 'blockChange', sectionKey: '0,64,0' })
|
|
40
|
+
expect(getClientLightTraceEvents()).toEqual([])
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('clientLightTrace stores metadata-only events in a bounded ring when enabled', () => {
|
|
44
|
+
enableClientLightTrace(true)
|
|
45
|
+
const first = recordClientLightTrace({
|
|
46
|
+
phase: 'blockChange',
|
|
47
|
+
sectionKey: '0,64,0',
|
|
48
|
+
blockVersion: 3,
|
|
49
|
+
lightVersion: 7
|
|
50
|
+
})
|
|
51
|
+
expect(first).toBeDefined()
|
|
52
|
+
expect(first!.t).toBeGreaterThan(0)
|
|
53
|
+
expect(first!.timeOrigin).toBe(performance.timeOrigin)
|
|
54
|
+
expect(first!.t).toBe(first!.timeOrigin + first!.tLocal)
|
|
55
|
+
const events = getClientLightTraceEvents()
|
|
56
|
+
expect(events).toHaveLength(1)
|
|
57
|
+
expect(events[0]!.phase).toBe('blockChange')
|
|
58
|
+
expect(events[0]!.sectionKey).toBe('0,64,0')
|
|
59
|
+
expect(JSON.stringify(events[0])).not.toMatch(/positions|indices|shaderCubes|chunkJson/)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('clientLightTrace wraps at capacity and keeps the newest events', () => {
|
|
63
|
+
enableClientLightTrace(true)
|
|
64
|
+
for (let i = 0; i < CLIENT_LIGHT_TRACE_CAPACITY + 5; i++) {
|
|
65
|
+
recordClientLightTrace({ phase: 'mesherEnqueue', requestId: i })
|
|
66
|
+
}
|
|
67
|
+
const events = getClientLightTraceEvents()
|
|
68
|
+
expect(events).toHaveLength(CLIENT_LIGHT_TRACE_CAPACITY)
|
|
69
|
+
expect(events[0]!.requestId).toBe(5)
|
|
70
|
+
expect(events[events.length - 1]!.requestId).toBe(CLIENT_LIGHT_TRACE_CAPACITY + 4)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('clientLightTrace comparableNow is timeOrigin plus local now', () => {
|
|
74
|
+
const before = performance.timeOrigin + performance.now()
|
|
75
|
+
const stamped = comparableNow()
|
|
76
|
+
const after = performance.timeOrigin + performance.now()
|
|
77
|
+
expect(stamped).toBeGreaterThanOrEqual(before)
|
|
78
|
+
expect(stamped).toBeLessThanOrEqual(after)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('clientLightTrace increments sessionEpoch, editSeq and requestId', () => {
|
|
82
|
+
enableClientLightTrace(true)
|
|
83
|
+
const epoch0 = bumpClientLightTraceSessionEpoch()
|
|
84
|
+
const epoch1 = bumpClientLightTraceSessionEpoch()
|
|
85
|
+
expect(epoch1).toBe(epoch0 + 1)
|
|
86
|
+
const editA = nextClientLightEditSeq()
|
|
87
|
+
const editB = nextClientLightEditSeq()
|
|
88
|
+
expect(editB).toBe(editA + 1)
|
|
89
|
+
const reqA = nextClientLightRequestId()
|
|
90
|
+
const reqB = nextClientLightRequestId()
|
|
91
|
+
expect(reqB).toBe(reqA + 1)
|
|
92
|
+
recordClientLightTrace({
|
|
93
|
+
phase: 'ownerEnqueue',
|
|
94
|
+
sessionEpoch: epoch1,
|
|
95
|
+
editSeq: editB,
|
|
96
|
+
requestId: reqB
|
|
97
|
+
})
|
|
98
|
+
expect(getClientLightTraceEvents()[0]).toMatchObject({
|
|
99
|
+
sessionEpoch: epoch1,
|
|
100
|
+
editSeq: editB,
|
|
101
|
+
requestId: reqB
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('clientLightTrace remote ingest keeps the worker timestamp', () => {
|
|
106
|
+
enableClientLightTrace(true)
|
|
107
|
+
const remoteT = 1_700_000_000_000.5
|
|
108
|
+
ingestRemoteClientLightTrace({
|
|
109
|
+
phase: 'mesherStart',
|
|
110
|
+
t: remoteT,
|
|
111
|
+
tLocal: 12,
|
|
112
|
+
timeOrigin: remoteT - 12,
|
|
113
|
+
sectionKey: '16,64,16',
|
|
114
|
+
requestId: 9
|
|
115
|
+
})
|
|
116
|
+
expect(getClientLightTraceEvents()[0]).toMatchObject({
|
|
117
|
+
phase: 'mesherStart',
|
|
118
|
+
t: remoteT,
|
|
119
|
+
requestId: 9
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('clientLightTrace remote ingest is a no-op while disabled', () => {
|
|
124
|
+
ingestRemoteClientLightTrace({
|
|
125
|
+
phase: 'mesherEnd',
|
|
126
|
+
t: 1,
|
|
127
|
+
tLocal: 1,
|
|
128
|
+
timeOrigin: 0
|
|
129
|
+
})
|
|
130
|
+
expect(getClientLightTraceEvents()).toEqual([])
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test('clientLightTrace aggregates counts by phase without storing meshes', () => {
|
|
134
|
+
enableClientLightTrace(true)
|
|
135
|
+
recordClientLightTrace({ phase: 'gpuDrawn', drawableFaces: 8000, cullReason: 'frustum' })
|
|
136
|
+
recordClientLightTrace({ phase: 'gpuDrawn', drawableFaces: 8000, cullReason: 'occlusion' })
|
|
137
|
+
recordClientLightTrace({ phase: 'reject', sectionKey: '0,64,0' })
|
|
138
|
+
const agg = getClientLightTraceAggregates()
|
|
139
|
+
expect(agg.byPhase.gpuDrawn).toBe(2)
|
|
140
|
+
expect(agg.byPhase.reject).toBe(1)
|
|
141
|
+
expect(agg.count).toBe(3)
|
|
142
|
+
expect(agg.dropped).toBe(0)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
describe('clientLightTrace enable from location search', () => {
|
|
146
|
+
test('maybeEnableClientLightTraceFromLocation stays off without the query', () => {
|
|
147
|
+
maybeEnableClientLightTraceFromLocation({ search: '?clientLight=1' })
|
|
148
|
+
expect(isClientLightTraceEnabled()).toBe(false)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
test('maybeEnableClientLightTraceFromLocation turns on with clientLightTrace=1', () => {
|
|
152
|
+
maybeEnableClientLightTraceFromLocation({ search: '?clientLight=1&clientLightTrace=1' })
|
|
153
|
+
expect(isClientLightTraceEnabled()).toBe(true)
|
|
154
|
+
expect(isClientLightTraceArmed()).toBe(false)
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
describe('clientLightTrace arm window', () => {
|
|
159
|
+
test('stays disarmed when enabled', () => {
|
|
160
|
+
enableClientLightTrace(true)
|
|
161
|
+
expect(isClientLightTraceEnabled()).toBe(true)
|
|
162
|
+
expect(isClientLightTraceArmed()).toBe(false)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test('exposes arm and dump on the window host when enabled', () => {
|
|
166
|
+
enableClientLightTrace(true)
|
|
167
|
+
const host = globalThis as typeof globalThis & {
|
|
168
|
+
clientLightTraceArm?: (on?: boolean) => void
|
|
169
|
+
clientLightTraceDump?: () => unknown
|
|
170
|
+
}
|
|
171
|
+
expect(typeof host.clientLightTraceArm).toBe('function')
|
|
172
|
+
expect(typeof host.clientLightTraceDump).toBe('function')
|
|
173
|
+
host.clientLightTraceArm?.()
|
|
174
|
+
expect(isClientLightTraceArmed()).toBe(true)
|
|
175
|
+
recordClientLightTrace({ phase: 'blockChange', sectionKey: '0,64,0' })
|
|
176
|
+
const dumped = host.clientLightTraceDump?.() as { events: unknown[]; aggregates: { count: number } }
|
|
177
|
+
expect(dumped.events).toHaveLength(1)
|
|
178
|
+
expect(dumped.aggregates.count).toBe(1)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
test('disabling also disarms', () => {
|
|
182
|
+
enableClientLightTrace(true)
|
|
183
|
+
armClientLightTrace(true)
|
|
184
|
+
enableClientLightTrace(false)
|
|
185
|
+
expect(isClientLightTraceArmed()).toBe(false)
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
describe('clientLightTrace worker post batching', () => {
|
|
190
|
+
test('bulk events without arm send zero inter-thread messages', () => {
|
|
191
|
+
enableClientLightTrace(true)
|
|
192
|
+
const posts: unknown[] = []
|
|
193
|
+
for (let i = 0; i < 50; i++) {
|
|
194
|
+
postClientLightTrace(message => posts.push(message), { phase: 'mesherEnqueue', requestId: i })
|
|
195
|
+
}
|
|
196
|
+
flushClientLightTracePosts()
|
|
197
|
+
expect(posts).toEqual([])
|
|
198
|
+
expect(getClientLightTraceEvents()).toHaveLength(50)
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
test('armed posts batch so messages are far fewer than events and every event is delivered', () => {
|
|
202
|
+
vi.useFakeTimers()
|
|
203
|
+
enableClientLightTrace(true)
|
|
204
|
+
armClientLightTrace(true)
|
|
205
|
+
const posts: Array<{ type?: string; events?: Array<{ requestId?: number }> }> = []
|
|
206
|
+
const eventCount = 50
|
|
207
|
+
for (let i = 0; i < eventCount; i++) {
|
|
208
|
+
postClientLightTrace(message => posts.push(message), { phase: 'mesherEnqueue', requestId: i })
|
|
209
|
+
}
|
|
210
|
+
expect(posts).toEqual([])
|
|
211
|
+
vi.advanceTimersByTime(CLIENT_LIGHT_TRACE_BATCH_MS)
|
|
212
|
+
expect(posts).toHaveLength(1)
|
|
213
|
+
expect(posts[0]!.events).toHaveLength(eventCount)
|
|
214
|
+
expect(posts[0]!.events!.map(event => event.requestId)).toEqual([...Array(eventCount).keys()])
|
|
215
|
+
expect(isClientLightTraceMessage(posts[0])).toBe(true)
|
|
216
|
+
|
|
217
|
+
enableClientLightTrace(false)
|
|
218
|
+
clearClientLightTrace()
|
|
219
|
+
enableClientLightTrace(true)
|
|
220
|
+
for (const post of posts) ingestRemoteClientLightTraceMessage(post)
|
|
221
|
+
expect(getClientLightTraceEvents()).toHaveLength(eventCount)
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
test('armed posts flush immediately at the batch size', () => {
|
|
225
|
+
enableClientLightTrace(true)
|
|
226
|
+
armClientLightTrace(true)
|
|
227
|
+
const posts: Array<{ events?: unknown[] }> = []
|
|
228
|
+
for (let i = 0; i < CLIENT_LIGHT_TRACE_BATCH_SIZE + 3; i++) {
|
|
229
|
+
postClientLightTrace(message => posts.push(message), { phase: 'ownerAdmit', requestId: i })
|
|
230
|
+
}
|
|
231
|
+
expect(posts).toHaveLength(1)
|
|
232
|
+
expect(posts[0]!.events).toHaveLength(CLIENT_LIGHT_TRACE_BATCH_SIZE)
|
|
233
|
+
flushClientLightTracePosts()
|
|
234
|
+
expect(posts).toHaveLength(2)
|
|
235
|
+
expect(posts[1]!.events).toHaveLength(3)
|
|
236
|
+
})
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
describe('clientLightTrace gpu sampling', () => {
|
|
240
|
+
test('1000 unchanged gpuDrawn frames record at most one event until a sample is requested', () => {
|
|
241
|
+
enableClientLightTrace(true)
|
|
242
|
+
for (let i = 0; i < 1000; i++) {
|
|
243
|
+
recordClientLightTraceGpuSample('gpuDrawn', 8000, { phase: 'gpuDrawn', drawableFaces: 8000 })
|
|
244
|
+
}
|
|
245
|
+
expect(getClientLightTraceEvents().filter(event => event.phase === 'gpuDrawn')).toHaveLength(1)
|
|
246
|
+
requestClientLightTraceSample()
|
|
247
|
+
recordClientLightTraceGpuSample('gpuDrawn', 8000, { phase: 'gpuDrawn', drawableFaces: 8000 })
|
|
248
|
+
expect(getClientLightTraceEvents().filter(event => event.phase === 'gpuDrawn')).toHaveLength(2)
|
|
249
|
+
recordClientLightTraceGpuSample('gpuDrawn', 9000, { phase: 'gpuDrawn', drawableFaces: 9000 })
|
|
250
|
+
expect(getClientLightTraceEvents().filter(event => event.phase === 'gpuDrawn')).toHaveLength(3)
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
test('gpuUploaded records only when the sampled metric changes', () => {
|
|
254
|
+
enableClientLightTrace(true)
|
|
255
|
+
recordClientLightTraceGpuSample('gpuUploaded', 128, { phase: 'gpuUploaded', unuploadedRanges: 4, drawableFaces: 128 })
|
|
256
|
+
recordClientLightTraceGpuSample('gpuUploaded', 128, { phase: 'gpuUploaded', unuploadedRanges: 3, drawableFaces: 128 })
|
|
257
|
+
recordClientLightTraceGpuSample('gpuUploaded', 256, { phase: 'gpuUploaded', unuploadedRanges: 2, drawableFaces: 256 })
|
|
258
|
+
const uploaded = getClientLightTraceEvents().filter(event => event.phase === 'gpuUploaded')
|
|
259
|
+
expect(uploaded).toHaveLength(2)
|
|
260
|
+
expect(uploaded[0]!.drawableFaces).toBe(128)
|
|
261
|
+
expect(uploaded[1]!.drawableFaces).toBe(256)
|
|
262
|
+
})
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
test('dumpClientLightTrace requests a gpu sample for the next draw', () => {
|
|
266
|
+
enableClientLightTrace(true)
|
|
267
|
+
recordClientLightTraceGpuSample('gpuDrawn', 10, { phase: 'gpuDrawn', drawableFaces: 10 })
|
|
268
|
+
dumpClientLightTrace()
|
|
269
|
+
recordClientLightTraceGpuSample('gpuDrawn', 10, { phase: 'gpuDrawn', drawableFaces: 10 })
|
|
270
|
+
expect(getClientLightTraceEvents().filter(event => event.phase === 'gpuDrawn')).toHaveLength(2)
|
|
271
|
+
})
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Bounded causal trace for client lighting delivery.
|
|
4
|
+
* Off by default. Records metadata and aggregates only — never meshes, chunks, or raw packets.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const CLIENT_LIGHT_TRACE_CAPACITY = 2048
|
|
8
|
+
export const CLIENT_LIGHT_TRACE_MESSAGE = '__clientLightTrace' as const
|
|
9
|
+
export const CLIENT_LIGHT_TRACE_BATCH_MS = 250
|
|
10
|
+
export const CLIENT_LIGHT_TRACE_BATCH_SIZE = 200
|
|
11
|
+
|
|
12
|
+
export type ClientLightTracePhase =
|
|
13
|
+
| 'inputClick'
|
|
14
|
+
| 'inputHand'
|
|
15
|
+
| 'blockChange'
|
|
16
|
+
| 'ownerEnqueue'
|
|
17
|
+
| 'ownerAdmit'
|
|
18
|
+
| 'ownerComplete'
|
|
19
|
+
| 'mesherEnqueue'
|
|
20
|
+
| 'mesherStart'
|
|
21
|
+
| 'mesherEnd'
|
|
22
|
+
| 'mesherFlush'
|
|
23
|
+
| 'receive'
|
|
24
|
+
| 'reject'
|
|
25
|
+
| 'pending'
|
|
26
|
+
| 'flush'
|
|
27
|
+
| 'gpuStaged'
|
|
28
|
+
| 'gpuUploaded'
|
|
29
|
+
| 'gpuCommitted'
|
|
30
|
+
| 'gpuDrawn'
|
|
31
|
+
|
|
32
|
+
export type ClientLightTraceEvent = {
|
|
33
|
+
t: number
|
|
34
|
+
tLocal: number
|
|
35
|
+
timeOrigin: number
|
|
36
|
+
phase: ClientLightTracePhase
|
|
37
|
+
sessionEpoch?: number
|
|
38
|
+
editSeq?: number
|
|
39
|
+
requestId?: number
|
|
40
|
+
sectionKey?: string
|
|
41
|
+
blockVersion?: number
|
|
42
|
+
lightVersion?: number
|
|
43
|
+
worldGeneration?: number
|
|
44
|
+
eventAgeMs?: number
|
|
45
|
+
currentColumn?: string
|
|
46
|
+
queueDepth?: number
|
|
47
|
+
fallbackRelights?: number
|
|
48
|
+
txnMs?: number
|
|
49
|
+
displayedStart?: number
|
|
50
|
+
displayedCount?: number
|
|
51
|
+
candidateStart?: number
|
|
52
|
+
candidateCount?: number
|
|
53
|
+
pendingReplace?: boolean
|
|
54
|
+
pendingMove?: boolean
|
|
55
|
+
unuploadedRanges?: number
|
|
56
|
+
drawableFaces?: number
|
|
57
|
+
cullReason?: string
|
|
58
|
+
flushReason?: string
|
|
59
|
+
workerIndex?: number
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type ClientLightTracePartial = Omit<ClientLightTraceEvent, 't' | 'tLocal' | 'timeOrigin'> &
|
|
63
|
+
Partial<Pick<ClientLightTraceEvent, 't' | 'tLocal' | 'timeOrigin'>>
|
|
64
|
+
|
|
65
|
+
export type ClientLightTraceMessage = {
|
|
66
|
+
type: typeof CLIENT_LIGHT_TRACE_MESSAGE
|
|
67
|
+
event?: ClientLightTraceEvent
|
|
68
|
+
events?: ClientLightTraceEvent[]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type ClientLightTraceConfig = {
|
|
72
|
+
enabled: boolean
|
|
73
|
+
armed: boolean
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
type ClientLightTraceConfigListener = (config: ClientLightTraceConfig) => void
|
|
77
|
+
|
|
78
|
+
let enabled = false
|
|
79
|
+
let armed = false
|
|
80
|
+
let sessionEpoch = 0
|
|
81
|
+
let editSeq = 0
|
|
82
|
+
let requestId = 0
|
|
83
|
+
let dropped = 0
|
|
84
|
+
let start = 0
|
|
85
|
+
let count = 0
|
|
86
|
+
let sampleRequested = false
|
|
87
|
+
let lastGpuDrawnMetric: number | undefined
|
|
88
|
+
let lastGpuUploadedMetric: number | undefined
|
|
89
|
+
let pendingPost: ((message: ClientLightTraceMessage) => void) | null = null
|
|
90
|
+
let pendingEvents: ClientLightTraceEvent[] = []
|
|
91
|
+
let pendingTimer: ReturnType<typeof setTimeout> | null = null
|
|
92
|
+
let configListener: ClientLightTraceConfigListener | undefined
|
|
93
|
+
const ring: Array<ClientLightTraceEvent | undefined> = new Array(CLIENT_LIGHT_TRACE_CAPACITY)
|
|
94
|
+
|
|
95
|
+
export function isClientLightTraceEnabled(): boolean {
|
|
96
|
+
return enabled
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function isClientLightTraceArmed(): boolean {
|
|
100
|
+
return armed
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function enableClientLightTrace(on: boolean): void {
|
|
104
|
+
enabled = on
|
|
105
|
+
if (!on) {
|
|
106
|
+
armed = false
|
|
107
|
+
dropPendingPosts()
|
|
108
|
+
resetGpuSampleState()
|
|
109
|
+
}
|
|
110
|
+
attachDebugDump(on)
|
|
111
|
+
notifyConfig()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function armClientLightTrace(on = true): void {
|
|
115
|
+
armed = on
|
|
116
|
+
if (!armed) dropPendingPosts()
|
|
117
|
+
notifyConfig()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function setClientLightTraceConfigListener(listener: ClientLightTraceConfigListener | undefined): void {
|
|
121
|
+
configListener = listener
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function maybeEnableClientLightTraceFromLocation(loc: { search?: string } | undefined = typeof location === 'undefined' ? undefined : location): void {
|
|
125
|
+
try {
|
|
126
|
+
if (!loc) return
|
|
127
|
+
if (new URLSearchParams(loc.search ?? '').get('clientLightTrace') === '1') {
|
|
128
|
+
enableClientLightTrace(true)
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
// ignore malformed URL in non-browser runtimes
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function comparableNow(): number {
|
|
136
|
+
return performance.timeOrigin + performance.now()
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function getClientLightTraceSessionEpoch(): number {
|
|
140
|
+
return sessionEpoch
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function bumpClientLightTraceSessionEpoch(): number {
|
|
144
|
+
sessionEpoch++
|
|
145
|
+
return sessionEpoch
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function nextClientLightEditSeq(): number {
|
|
149
|
+
editSeq++
|
|
150
|
+
return editSeq
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function nextClientLightRequestId(): number {
|
|
154
|
+
requestId++
|
|
155
|
+
return requestId
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function currentClientLightTraceIds(): { sessionEpoch: number; editSeq: number } {
|
|
159
|
+
return { sessionEpoch, editSeq }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function clearClientLightTrace(): void {
|
|
163
|
+
start = 0
|
|
164
|
+
count = 0
|
|
165
|
+
dropped = 0
|
|
166
|
+
ring.fill(undefined)
|
|
167
|
+
resetGpuSampleState()
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function recordClientLightTrace(partial: ClientLightTracePartial): ClientLightTraceEvent | undefined {
|
|
171
|
+
if (!enabled) return undefined
|
|
172
|
+
const tLocal = partial.tLocal ?? performance.now()
|
|
173
|
+
const timeOrigin = partial.timeOrigin ?? performance.timeOrigin
|
|
174
|
+
const event: ClientLightTraceEvent = {
|
|
175
|
+
...partial,
|
|
176
|
+
t: partial.t ?? timeOrigin + tLocal,
|
|
177
|
+
tLocal,
|
|
178
|
+
timeOrigin,
|
|
179
|
+
sessionEpoch: partial.sessionEpoch ?? sessionEpoch
|
|
180
|
+
}
|
|
181
|
+
pushEvent(event)
|
|
182
|
+
return event
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function requestClientLightTraceSample(): void {
|
|
186
|
+
sampleRequested = true
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function recordClientLightTraceGpuSample(
|
|
190
|
+
phase: 'gpuDrawn' | 'gpuUploaded',
|
|
191
|
+
metric: number,
|
|
192
|
+
partial: ClientLightTracePartial | (() => ClientLightTracePartial)
|
|
193
|
+
): ClientLightTraceEvent | undefined {
|
|
194
|
+
if (!enabled) return undefined
|
|
195
|
+
const last = phase === 'gpuDrawn' ? lastGpuDrawnMetric : lastGpuUploadedMetric
|
|
196
|
+
if (last === metric && !sampleRequested) return undefined
|
|
197
|
+
if (phase === 'gpuDrawn') lastGpuDrawnMetric = metric
|
|
198
|
+
else lastGpuUploadedMetric = metric
|
|
199
|
+
sampleRequested = false
|
|
200
|
+
return recordClientLightTrace(typeof partial === 'function' ? partial() : partial)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function ingestRemoteClientLightTrace(event: ClientLightTraceEvent | undefined): void {
|
|
204
|
+
if (!enabled || !event || typeof event.t !== 'number' || !event.phase) return
|
|
205
|
+
pushEvent(event)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function ingestRemoteClientLightTraceMessage(message: ClientLightTraceMessage | { type?: unknown; event?: unknown; events?: unknown }): void {
|
|
209
|
+
if (!isClientLightTraceMessage(message)) return
|
|
210
|
+
const events = message.events ?? (message.event ? [message.event] : [])
|
|
211
|
+
for (const event of events) ingestRemoteClientLightTrace(event)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function postClientLightTrace(post: (message: ClientLightTraceMessage) => void, partial: ClientLightTracePartial): void {
|
|
215
|
+
const event = recordClientLightTrace(partial)
|
|
216
|
+
if (!event || !armed) return
|
|
217
|
+
pendingPost = post
|
|
218
|
+
pendingEvents.push(event)
|
|
219
|
+
if (pendingEvents.length >= CLIENT_LIGHT_TRACE_BATCH_SIZE) {
|
|
220
|
+
flushClientLightTracePosts()
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
if (pendingTimer == null) {
|
|
224
|
+
pendingTimer = setTimeout(() => {
|
|
225
|
+
pendingTimer = null
|
|
226
|
+
flushClientLightTracePosts()
|
|
227
|
+
}, CLIENT_LIGHT_TRACE_BATCH_MS)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function flushClientLightTracePosts(): void {
|
|
232
|
+
if (pendingTimer != null) {
|
|
233
|
+
clearTimeout(pendingTimer)
|
|
234
|
+
pendingTimer = null
|
|
235
|
+
}
|
|
236
|
+
if (!pendingPost || pendingEvents.length === 0) {
|
|
237
|
+
pendingEvents = []
|
|
238
|
+
pendingPost = null
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
const events = pendingEvents
|
|
242
|
+
const post = pendingPost
|
|
243
|
+
pendingEvents = []
|
|
244
|
+
pendingPost = null
|
|
245
|
+
post({ type: CLIENT_LIGHT_TRACE_MESSAGE, events })
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function isClientLightTraceMessage(data: unknown): data is ClientLightTraceMessage {
|
|
249
|
+
if (!data || typeof data !== 'object') return false
|
|
250
|
+
const message = data as { type?: unknown; event?: unknown; events?: unknown }
|
|
251
|
+
if (message.type !== CLIENT_LIGHT_TRACE_MESSAGE) return false
|
|
252
|
+
if (Array.isArray(message.events)) return true
|
|
253
|
+
return !!message.event && typeof message.event === 'object'
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function getClientLightTraceEvents(): ClientLightTraceEvent[] {
|
|
257
|
+
const out: ClientLightTraceEvent[] = []
|
|
258
|
+
for (let i = 0; i < count; i++) {
|
|
259
|
+
const event = ring[(start + i) % CLIENT_LIGHT_TRACE_CAPACITY]
|
|
260
|
+
if (event) out.push(event)
|
|
261
|
+
}
|
|
262
|
+
return out
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function getClientLightTraceAggregates(): { count: number; dropped: number; byPhase: Partial<Record<ClientLightTracePhase, number>> } {
|
|
266
|
+
const byPhase: Partial<Record<ClientLightTracePhase, number>> = {}
|
|
267
|
+
for (const event of getClientLightTraceEvents()) {
|
|
268
|
+
byPhase[event.phase] = (byPhase[event.phase] ?? 0) + 1
|
|
269
|
+
}
|
|
270
|
+
return { count: count, dropped, byPhase }
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function dumpClientLightTrace(): {
|
|
274
|
+
events: ClientLightTraceEvent[]
|
|
275
|
+
aggregates: ReturnType<typeof getClientLightTraceAggregates>
|
|
276
|
+
} {
|
|
277
|
+
requestClientLightTraceSample()
|
|
278
|
+
return {
|
|
279
|
+
events: getClientLightTraceEvents(),
|
|
280
|
+
aggregates: getClientLightTraceAggregates()
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function dropPendingPosts(): void {
|
|
285
|
+
if (pendingTimer != null) {
|
|
286
|
+
clearTimeout(pendingTimer)
|
|
287
|
+
pendingTimer = null
|
|
288
|
+
}
|
|
289
|
+
pendingEvents = []
|
|
290
|
+
pendingPost = null
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function resetGpuSampleState(): void {
|
|
294
|
+
sampleRequested = false
|
|
295
|
+
lastGpuDrawnMetric = undefined
|
|
296
|
+
lastGpuUploadedMetric = undefined
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function notifyConfig(): void {
|
|
300
|
+
configListener?.({ enabled, armed })
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function pushEvent(event: ClientLightTraceEvent): void {
|
|
304
|
+
if (count === CLIENT_LIGHT_TRACE_CAPACITY) {
|
|
305
|
+
ring[start] = event
|
|
306
|
+
start = (start + 1) % CLIENT_LIGHT_TRACE_CAPACITY
|
|
307
|
+
dropped++
|
|
308
|
+
return
|
|
309
|
+
}
|
|
310
|
+
ring[(start + count) % CLIENT_LIGHT_TRACE_CAPACITY] = event
|
|
311
|
+
count++
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function attachDebugDump(on: boolean): void {
|
|
315
|
+
if (typeof globalThis === 'undefined') return
|
|
316
|
+
const host = globalThis as typeof globalThis & {
|
|
317
|
+
getClientLightTrace?: typeof getClientLightTraceEvents
|
|
318
|
+
getClientLightTraceAggregates?: typeof getClientLightTraceAggregates
|
|
319
|
+
clientLightTraceArm?: (on?: boolean) => void
|
|
320
|
+
clientLightTraceDump?: typeof dumpClientLightTrace
|
|
321
|
+
}
|
|
322
|
+
if (on) {
|
|
323
|
+
host.getClientLightTrace = getClientLightTraceEvents
|
|
324
|
+
host.getClientLightTraceAggregates = getClientLightTraceAggregates
|
|
325
|
+
host.clientLightTraceArm = (next = true) => {
|
|
326
|
+
armClientLightTrace(next !== false)
|
|
327
|
+
}
|
|
328
|
+
host.clientLightTraceDump = dumpClientLightTrace
|
|
329
|
+
} else {
|
|
330
|
+
delete host.getClientLightTrace
|
|
331
|
+
delete host.getClientLightTraceAggregates
|
|
332
|
+
delete host.clientLightTraceArm
|
|
333
|
+
delete host.clientLightTraceDump
|
|
334
|
+
}
|
|
335
|
+
}
|