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.
Files changed (98) hide show
  1. package/dist/lightOwnerWorker.js +9 -0
  2. package/dist/lightOwnerWorker.js.map +7 -0
  3. package/dist/mesher.js +1 -1
  4. package/dist/mesher.js.map +2 -2
  5. package/dist/mesherWasm.js +222 -222
  6. package/dist/minecraft-renderer.js +105 -76
  7. package/dist/minecraft-renderer.js.meta.json +1 -1
  8. package/dist/threeWorker.js +537 -508
  9. package/package.json +1 -1
  10. package/src/bundler/bundlePrepare.test.ts +25 -0
  11. package/src/bundler/bundlePrepare.ts +1 -1
  12. package/src/graphicsBackend/appViewer.ts +15 -0
  13. package/src/graphicsBackend/config.ts +8 -0
  14. package/src/graphicsBackend/rendererDefaultOptions.test.ts +51 -0
  15. package/src/graphicsBackend/rendererDefaultOptions.ts +18 -1
  16. package/src/graphicsBackend/rendererOptionsSync.ts +4 -3
  17. package/src/lib/blockEntityLightRegistry.ts +4 -0
  18. package/src/lib/skyLight.ts +17 -0
  19. package/src/lib/worldrendererCommon.lightOwner.test.ts +916 -0
  20. package/src/lib/worldrendererCommon.removeColumn.test.ts +16 -0
  21. package/src/lib/worldrendererCommon.ts +340 -67
  22. package/src/lighting/cache/rendererLightCache.test.ts +158 -0
  23. package/src/lighting/cache/rendererLightCache.ts +248 -0
  24. package/src/lighting/coordinator/clientLightCoordinator.test.ts +102 -0
  25. package/src/lighting/coordinator/clientLightCoordinator.ts +574 -0
  26. package/src/lighting/debug/clientLightProbe.test.ts +24 -0
  27. package/src/lighting/debug/clientLightProbe.ts +20 -0
  28. package/src/lighting/debug/clientLightTrace.test.ts +271 -0
  29. package/src/lighting/debug/clientLightTrace.ts +335 -0
  30. package/src/lighting/entity/entityLightController.test.ts +216 -0
  31. package/src/lighting/entity/entityLightController.ts +322 -0
  32. package/src/lighting/owner/clientLightOwner.test.ts +467 -0
  33. package/src/lighting/owner/clientLightOwner.ts +523 -0
  34. package/src/lighting/owner/lightOwnerHost.test.ts +78 -0
  35. package/src/lighting/owner/lightOwnerHost.ts +202 -0
  36. package/src/lighting/owner/lightOwnerWorker.ts +154 -0
  37. package/src/lighting/owner/updateLight1171OwnerPath.test.ts +68 -0
  38. package/src/lighting/owner/updateLightToOwnerEvents.ts +49 -0
  39. package/src/lighting/tables/lightTables1171.generated.json +10 -0
  40. package/src/lighting/tables/lightTables1171.test.ts +168 -0
  41. package/src/lighting/tables/lightTables1171.ts +59 -0
  42. package/src/lighting/versioning/clientLightVersions.test.ts +241 -0
  43. package/src/lighting/versioning/clientLightVersions.ts +203 -0
  44. package/src/lighting/versioning/pendingSectionFlush.test.ts +214 -0
  45. package/src/lighting/versioning/pendingSectionFlush.ts +123 -0
  46. package/src/mesher-shared/lightNibblePack.ts +26 -0
  47. package/src/mesher-shared/mcDataWorkerShim.cjs +9 -7
  48. package/src/mesher-shared/modelsGeometryCommon.test.ts +6 -1
  49. package/src/mesher-shared/shared.ts +48 -1
  50. package/src/three/chunkMeshManager.ts +26 -0
  51. package/src/three/entities.ts +97 -8
  52. package/src/three/entity/EntityMesh.ts +1 -0
  53. package/src/three/entity/legWalkAnimation.test.ts +154 -0
  54. package/src/three/entity/legWalkAnimation.ts +193 -0
  55. package/src/three/globalBlockBuffer.ts +94 -2
  56. package/src/three/globalLegacyBuffer.ts +94 -2
  57. package/src/three/graphicsBackendBase.ts +2 -9
  58. package/src/three/modules/clouds.ts +191 -0
  59. package/src/three/modules/index.ts +4 -0
  60. package/src/three/modules/sunMoon.ts +151 -0
  61. package/src/three/skyboxRenderer.ts +15 -8
  62. package/src/three/tests/clouds.test.ts +59 -0
  63. package/src/three/tests/globalBlockBuffer.test.ts +98 -0
  64. package/src/three/tests/globalLegacyBuffer.test.ts +108 -0
  65. package/src/three/tests/waterFog.test.ts +25 -0
  66. package/src/three/waterFog.ts +28 -0
  67. package/src/three/worldRendererThree.ts +84 -39
  68. package/src/wasm-mesher/bridge/render-from-wasm.ts +46 -1
  69. package/src/wasm-mesher/runtime-build/wasm_mesher.d.ts +31 -5
  70. package/src/wasm-mesher/runtime-build/wasm_mesher.js +180 -5
  71. package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm +0 -0
  72. package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm.d.ts +11 -0
  73. package/src/wasm-mesher/tests/mesherLightDependencies.test.ts +83 -0
  74. package/src/wasm-mesher/tests/mesherWasmColumnTickSelect.test.ts +39 -0
  75. package/src/wasm-mesher/tests/mesherWasmConversionCache.test.ts +30 -1
  76. package/src/wasm-mesher/tests/mesherWasmTickParseCache.test.ts +73 -0
  77. package/src/wasm-mesher/tests/mesherWasmTickSchedule.test.ts +51 -0
  78. package/src/wasm-mesher/tests/mesherWasmTopologyPostCache.test.ts +78 -0
  79. package/src/wasm-mesher/worker/clientLightDarkPublication.probe.test.ts +32 -0
  80. package/src/wasm-mesher/worker/mesherLightDependencies.ts +43 -0
  81. package/src/wasm-mesher/worker/mesherWasm.ownerLightSync.test.ts +198 -0
  82. package/src/wasm-mesher/worker/mesherWasm.ts +506 -96
  83. package/src/wasm-mesher/worker/mesherWasmChunkBuffer.ts +1 -0
  84. package/src/wasm-mesher/worker/mesherWasmColumnTickSelect.ts +47 -0
  85. package/src/wasm-mesher/worker/mesherWasmConversionCache.ts +16 -5
  86. package/src/wasm-mesher/worker/mesherWasmLightDirty.test.ts +17 -1
  87. package/src/wasm-mesher/worker/mesherWasmLightDirty.ts +10 -0
  88. package/src/wasm-mesher/worker/mesherWasmLightMerge.test.ts +208 -0
  89. package/src/wasm-mesher/worker/mesherWasmLightMerge.ts +228 -0
  90. package/src/wasm-mesher/worker/mesherWasmOwnerLight.test.ts +161 -0
  91. package/src/wasm-mesher/worker/mesherWasmOwnerLight.ts +112 -0
  92. package/src/wasm-mesher/worker/mesherWasmTickParseCache.ts +45 -0
  93. package/src/wasm-mesher/worker/mesherWasmTickSchedule.ts +44 -0
  94. package/src/wasm-mesher/worker/mesherWasmTopologyPostCache.ts +63 -0
  95. package/src/worldView/types.ts +3 -0
  96. package/src/worldView/worldView.geometryReload.test.ts +68 -0
  97. package/src/worldView/worldView.spiral.test.ts +1 -1
  98. 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
+ }