@downdraft/engine 0.1.2-alpha.5 → 0.3.0
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/app/src/renderer/bootstrap.ts +1 -1
- package/app/src/renderer/feature-log.ts +1 -1
- package/core/src/index.ts +2 -2
- package/core/src/input/local-player-manager.ts +2 -2
- package/core/src/platform/rpc.ts +4 -1
- package/core/src/render/game-renderer.ts +127 -64
- package/core/src/sab/grid-buffer.ts +55 -2
- package/core/src/sab/record-buffer.ts +48 -5
- package/core/src/sab/sim-channel.ts +181 -17
- package/core/src/sab/slot-buffer.ts +55 -1
- package/core/src/sab/types.ts +11 -0
- package/core/src/telemetry/gpu-resource-tracker.ts +24 -0
- package/core/src/util/index.ts +1 -0
- package/core/src/util/logger.ts +2 -2
- package/core/src/util/session-timer.ts +23 -0
- package/core/src/worker/base-worker-host.ts +2 -1
- package/core/src/worker/buffer-sync.ts +20 -13
- package/core/src/worker/rpc.ts +54 -5
- package/core/src/worker/sim-worker-base.ts +6 -3
- package/core/src/worker/task-pool.ts +4 -0
- package/dist/.dd-manifest.json +879 -869
- package/dist/app/src/renderer/bootstrap.js +1 -1
- package/dist/app/src/renderer/feature-log.js +1 -1
- package/dist/core/src/index.js +2 -2
- package/dist/core/src/input/local-player-manager.js +2 -2
- package/dist/core/src/platform/rpc.js +2 -1
- package/dist/core/src/render/game-renderer.js +125 -59
- package/dist/core/src/sab/grid-buffer.js +53 -1
- package/dist/core/src/sab/record-buffer.js +49 -5
- package/dist/core/src/sab/sim-channel.js +173 -17
- package/dist/core/src/sab/slot-buffer.js +54 -1
- package/dist/core/src/telemetry/gpu-resource-tracker.js +26 -0
- package/dist/core/src/util/index.js +1 -0
- package/dist/core/src/util/logger.js +2 -2
- package/dist/core/src/util/session-timer.js +22 -0
- package/dist/core/src/worker/base-worker-host.js +2 -1
- package/dist/core/src/worker/buffer-sync.js +16 -9
- package/dist/core/src/worker/rpc.js +46 -8
- package/dist/core/src/worker/sim-worker-base.js +6 -3
- package/dist/core/src/worker/task-pool.js +5 -0
- package/dist/libraries/devtools/src/backend.js +35 -9
- package/dist/libraries/devtools/src/collectors.js +73 -22
- package/dist/libraries/gamepad/src/feed-input.js +1 -1
- package/dist/libraries/gamepad/src/hub.js +2 -2
- package/dist/libraries/gamepad/src/source-sab.js +2 -2
- package/dist/libraries/gamepad/src/sysfs-enrich.js +2 -2
- package/dist/libraries/gpu-kernels/src/kernel.js +1 -1
- package/dist/libraries/gpu-kernels/src/transpile.js +1 -1
- package/dist/libraries/html-ui-kit/src/nav/nav-controller.js +1 -1
- package/dist/libraries/html-ui-kit/src/nav/nav-input.js +2 -2
- package/dist/libraries/networking/src/discovery.js +8 -8
- package/dist/libraries/persistence/src/file-save-store.js +5 -5
- package/dist/libraries/persistence/src/opfs-save-store.js +2 -2
- package/dist/modules/controller-ui/src/index.js +1 -1
- package/dist/modules/devtools/src/blitz/fonts.js +75 -0
- package/dist/modules/devtools/src/blitz/host.js +127 -33
- package/dist/modules/devtools/src/blitz/panels/console.js +19 -7
- package/dist/modules/devtools/src/blitz/panels/elements.js +217 -28
- package/dist/modules/devtools/src/blitz/panels/gpu.js +5 -2
- package/dist/modules/devtools/src/blitz/panels/performance.js +28 -14
- package/dist/modules/devtools/src/blitz/panels/snapshot.js +31 -8
- package/dist/modules/devtools/src/blitz/panels/types.js +2 -1
- package/dist/modules/devtools/src/blitz/theme.js +26 -4
- package/dist/modules/html-ui/src/doc-backend.js +52 -3
- package/dist/modules/html-ui/src/host.js +6 -3
- package/dist/modules/html-ui/src/jsx-runtime.js +1 -1
- package/libraries/blitz-ui/native/Cargo.toml +6 -0
- package/libraries/blitz-ui/native-osr/Cargo.toml +3 -1
- package/libraries/devtools/src/backend.ts +38 -12
- package/libraries/devtools/src/collectors.ts +75 -22
- package/libraries/gamepad/src/feed-input.ts +1 -1
- package/libraries/gamepad/src/hub.ts +2 -2
- package/libraries/gamepad/src/source-sab.ts +2 -2
- package/libraries/gamepad/src/sysfs-enrich.ts +2 -2
- package/libraries/gpu-kernels/src/kernel.ts +2 -2
- package/libraries/gpu-kernels/src/transpile.ts +1 -1
- package/libraries/html-ui-kit/src/nav/nav-controller.ts +1 -1
- package/libraries/html-ui-kit/src/nav/nav-input.ts +2 -2
- package/libraries/networking/src/discovery.ts +8 -8
- package/libraries/persistence/src/file-save-store.ts +5 -5
- package/libraries/persistence/src/opfs-save-store.ts +2 -2
- package/libraries/physics-rapier/src/realm-worker-script.ts +1 -1
- package/modules/controller-ui/src/index.ts +1 -1
- package/modules/devtools/src/blitz/fonts.ts +78 -0
- package/modules/devtools/src/blitz/host.ts +110 -34
- package/modules/devtools/src/blitz/panels/console.ts +20 -8
- package/modules/devtools/src/blitz/panels/elements.ts +212 -31
- package/modules/devtools/src/blitz/panels/gpu.ts +4 -2
- package/modules/devtools/src/blitz/panels/performance.ts +26 -14
- package/modules/devtools/src/blitz/panels/snapshot.ts +30 -8
- package/modules/devtools/src/blitz/panels/types.ts +2 -1
- package/modules/devtools/src/blitz/theme.ts +26 -4
- package/modules/html-ui/src/doc-backend.ts +67 -4
- package/modules/html-ui/src/host.ts +11 -3
- package/modules/html-ui/src/jsx-runtime.ts +1 -1
- package/modules/html-ui/src/protocol.ts +1 -1
- package/package.json +2 -2
|
@@ -34,7 +34,7 @@ export const SIM_PLAYER_SLOT_SIZE = 256;
|
|
|
34
34
|
export const SimChannel = defineChannel({
|
|
35
35
|
name: "game-sim",
|
|
36
36
|
magic: 0x53494d42,
|
|
37
|
-
version:
|
|
37
|
+
version: 2,
|
|
38
38
|
mode: "slots",
|
|
39
39
|
header: {
|
|
40
40
|
size: 256,
|
|
@@ -76,6 +76,12 @@ export const SimChannel = defineChannel({
|
|
|
76
76
|
physTimingCcdSolver: { type: "f32" },
|
|
77
77
|
physTimingIslandConstruction: { type: "f32" },
|
|
78
78
|
physTimingUserChanges: { type: "f32" },
|
|
79
|
+
// Presentation-interpolation timing, stamped by incrementTick().
|
|
80
|
+
// publishTimeMs is wall-clock epoch ms (timeOrigin + now) so it
|
|
81
|
+
// compares correctly across the worker/renderer thread boundary —
|
|
82
|
+
// performance.now() epochs differ per thread.
|
|
83
|
+
publishTimeMs: { type: "f64" },
|
|
84
|
+
publishDeltaMs: { type: "f32" },
|
|
79
85
|
},
|
|
80
86
|
},
|
|
81
87
|
sections: [
|
|
@@ -131,11 +137,33 @@ export const SimChannel = defineChannel({
|
|
|
131
137
|
// modifying the core SAB schema.
|
|
132
138
|
},
|
|
133
139
|
},
|
|
140
|
+
// Previous-publish transforms for renderer-side interpolation. The
|
|
141
|
+
// writer snapshots pos+rot here before overwriting a slot, and fills
|
|
142
|
+
// the rest from cur at commit — so prev always equals state(t-1)
|
|
143
|
+
// and cur equals state(t). Same field layout as ENT/PLR transforms.
|
|
144
|
+
{
|
|
145
|
+
name: "entitiesPrev",
|
|
146
|
+
maxSlots: MAX_ENTITIES,
|
|
147
|
+
slotSize: 32,
|
|
148
|
+
fields: {
|
|
149
|
+
pos: { type: "f32", count: 4 },
|
|
150
|
+
rot: { type: "f32", count: 4 },
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: "playersPrev",
|
|
155
|
+
maxSlots: MAX_PLAYERS,
|
|
156
|
+
slotSize: 32,
|
|
157
|
+
fields: {
|
|
158
|
+
pos: { type: "f32", count: 4 },
|
|
159
|
+
rot: { type: "f32", count: 4 },
|
|
160
|
+
},
|
|
161
|
+
},
|
|
134
162
|
],
|
|
135
163
|
});
|
|
136
164
|
|
|
137
165
|
export const SIM_MAGIC = 0x53494d42;
|
|
138
|
-
export const SIM_VERSION =
|
|
166
|
+
export const SIM_VERSION = 2;
|
|
139
167
|
|
|
140
168
|
export const SIM_HDR = {
|
|
141
169
|
MAGIC: 0,
|
|
@@ -178,6 +206,8 @@ export const SIM_HDR = {
|
|
|
178
206
|
PHYS_TIMING_CCD_SOLVER: 37,
|
|
179
207
|
PHYS_TIMING_ISLAND_CONSTRUCTION: 38,
|
|
180
208
|
PHYS_TIMING_USER_CHANGES: 39,
|
|
209
|
+
PUBLISH_TIME_MS: 40, // f64 — occupies u32 words 40–41
|
|
210
|
+
PUBLISH_DELTA_MS: 42,
|
|
181
211
|
} as const;
|
|
182
212
|
|
|
183
213
|
export const ENT = {
|
|
@@ -227,6 +257,14 @@ export const PLR_FLAG = {
|
|
|
227
257
|
GROUNDED: 1 << 9,
|
|
228
258
|
} as const;
|
|
229
259
|
|
|
260
|
+
// Field offsets inside the entitiesPrev/playersPrev sections. pos holds
|
|
261
|
+
// [x, y, z, w] where w is entity scale / player heading; rot is the
|
|
262
|
+
// quaternion. Matches ENT.POS_*/ENT.ROT_* and PLR.POS_*/PLR.ROT_* layout.
|
|
263
|
+
export const PREV = {
|
|
264
|
+
POS_X: 0, POS_Y: 1, POS_Z: 2, POS_W: 3,
|
|
265
|
+
ROT_X: 4, ROT_Y: 5, ROT_Z: 6, ROT_W: 7,
|
|
266
|
+
} as const;
|
|
267
|
+
|
|
230
268
|
export function allocateSimBuffer(): SharedArrayBuffer {
|
|
231
269
|
return SimChannel.allocate();
|
|
232
270
|
}
|
|
@@ -241,11 +279,15 @@ export class SimBufferReader {
|
|
|
241
279
|
private reader: ReturnType<typeof SimChannel.reader>;
|
|
242
280
|
private entitySlots: ReturnType<typeof SimChannel.reader>["sections"]["entities"];
|
|
243
281
|
private playerSlots: ReturnType<typeof SimChannel.reader>["sections"]["players"];
|
|
282
|
+
private entityPrevSlots: ReturnType<typeof SimChannel.reader>["sections"]["entitiesPrev"];
|
|
283
|
+
private playerPrevSlots: ReturnType<typeof SimChannel.reader>["sections"]["playersPrev"];
|
|
244
284
|
|
|
245
285
|
constructor(sab: SharedArrayBuffer) {
|
|
246
286
|
this.reader = SimChannel.reader(sab);
|
|
247
287
|
this.entitySlots = this.reader.sections.entities;
|
|
248
288
|
this.playerSlots = this.reader.sections.players;
|
|
289
|
+
this.entityPrevSlots = this.reader.sections.entitiesPrev;
|
|
290
|
+
this.playerPrevSlots = this.reader.sections.playersPrev;
|
|
249
291
|
}
|
|
250
292
|
|
|
251
293
|
isValid(): boolean {
|
|
@@ -258,6 +300,17 @@ export class SimBufferReader {
|
|
|
258
300
|
|
|
259
301
|
getTick(): number { return this.reader.header.u32[SimChannel.offsets.header.tick]; }
|
|
260
302
|
getSequence(): number { return this.reader.getSequence(); }
|
|
303
|
+
/** True while the sim worker is mid-publish (sequence word is odd). */
|
|
304
|
+
isWriteInProgress(): boolean { return this.reader.isWriteInProgress(); }
|
|
305
|
+
/**
|
|
306
|
+
* Run `fn` against a consistent snapshot of the sim state — retries
|
|
307
|
+
* while the sim worker is mid-publish or publishes during the read
|
|
308
|
+
* (seqlock). Wrap per-frame read loops (entity iteration, player slot
|
|
309
|
+
* reads) in this to avoid torn reads across a sim commit.
|
|
310
|
+
*/
|
|
311
|
+
readConsistent<T>(fn: () => T, maxRetries?: number): T {
|
|
312
|
+
return this.reader.readConsistent(fn, maxRetries);
|
|
313
|
+
}
|
|
261
314
|
getEntityCount(): number { return this.reader.header.u32[SimChannel.offsets.header.entityCount]; }
|
|
262
315
|
getPlayerCount(): number { return this.reader.header.u32[SimChannel.offsets.header.playerCount]; }
|
|
263
316
|
getTimeOfDay(): number { return this.reader.header.f32[SimChannel.offsets.header.timeOfDay]; }
|
|
@@ -328,6 +381,46 @@ export class SimBufferReader {
|
|
|
328
381
|
yield { idx: i, f32: sv.f32, u32: sv.u32 };
|
|
329
382
|
}
|
|
330
383
|
}
|
|
384
|
+
|
|
385
|
+
// ── Presentation interpolation ──
|
|
386
|
+
// prev = state(t-1) transform, cur = state(t); the renderer lerps by
|
|
387
|
+
// getInterpolationAlpha(). Wall-clock epoch ms (timeOrigin + now) —
|
|
388
|
+
// comparable across the worker/renderer thread boundary.
|
|
389
|
+
|
|
390
|
+
getPublishTimeMs(): number {
|
|
391
|
+
const o = SimChannel.offsets.header;
|
|
392
|
+
return this.reader.header.f64[o.publishTimeMs >> 1];
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
getPublishDeltaMs(): number {
|
|
396
|
+
const o = SimChannel.offsets.header;
|
|
397
|
+
return this.reader.header.f32[o.publishDeltaMs];
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Interpolation alpha in [0,1] for the current frame. 0 = exactly at
|
|
402
|
+
* the last publish, 1 = a full publish interval past it (use cur).
|
|
403
|
+
* Clamped — no extrapolation.
|
|
404
|
+
*/
|
|
405
|
+
getInterpolationAlpha(nowMs?: number): number {
|
|
406
|
+
const delta = this.getPublishDeltaMs();
|
|
407
|
+
if (delta <= 0) return 1;
|
|
408
|
+
const now = nowMs ?? (performance.timeOrigin ?? 0) + performance.now();
|
|
409
|
+
const a = (now - this.getPublishTimeMs()) / delta;
|
|
410
|
+
return a <= 0 ? 0 : a >= 1 ? 1 : a;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** Previous-publish transform for an entity slot (prev pos+rot, PREV.*). */
|
|
414
|
+
getEntityPrevSlot(idx: number): { f32: Float32Array; u32: Uint32Array } {
|
|
415
|
+
const sv = this.entityPrevSlots.slot(idx);
|
|
416
|
+
return { f32: sv.f32, u32: sv.u32 };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** Previous-publish transform for a player slot (prev pos+rot, PREV.*). */
|
|
420
|
+
getPlayerPrevSlot(idx: number): { f32: Float32Array; u32: Uint32Array } {
|
|
421
|
+
const sv = this.playerPrevSlots.slot(idx);
|
|
422
|
+
return { f32: sv.f32, u32: sv.u32 };
|
|
423
|
+
}
|
|
331
424
|
}
|
|
332
425
|
|
|
333
426
|
// --- Sim Buffer Writer (sim worker side) ---
|
|
@@ -336,52 +429,67 @@ export class SimBufferWriter {
|
|
|
336
429
|
private writer: ReturnType<typeof SimChannel.writer>;
|
|
337
430
|
private entitySlots: ReturnType<typeof SimChannel.writer>["sections"]["entities"];
|
|
338
431
|
private playerSlots: ReturnType<typeof SimChannel.writer>["sections"]["players"];
|
|
432
|
+
private entityPrevSlots: ReturnType<typeof SimChannel.writer>["sections"]["entitiesPrev"];
|
|
433
|
+
private playerPrevSlots: ReturnType<typeof SimChannel.writer>["sections"]["playersPrev"];
|
|
339
434
|
// Dirty flags: 1 = entity/player slot has changed since last writeToBuffer.
|
|
340
435
|
// writeToBuffer skips slots where the flag is 0. Cleared after each writeToBuffer.
|
|
341
436
|
private dirtyEntities: Uint8Array;
|
|
342
437
|
private dirtyPlayers: Uint8Array;
|
|
438
|
+
// Snapshot flags: 1 = the slot's prev-transform was already captured this
|
|
439
|
+
// publish (on first write access). Cleared at each incrementTick commit.
|
|
440
|
+
private snapEntities: Uint8Array;
|
|
441
|
+
private snapPlayers: Uint8Array;
|
|
343
442
|
|
|
344
443
|
constructor(sab: SharedArrayBuffer) {
|
|
345
444
|
this.writer = SimChannel.writer(sab);
|
|
346
445
|
this.entitySlots = this.writer.sections.entities;
|
|
347
446
|
this.playerSlots = this.writer.sections.players;
|
|
447
|
+
this.entityPrevSlots = this.writer.sections.entitiesPrev;
|
|
448
|
+
this.playerPrevSlots = this.writer.sections.playersPrev;
|
|
348
449
|
this.dirtyEntities = new Uint8Array(MAX_ENTITIES);
|
|
349
450
|
this.dirtyPlayers = new Uint8Array(MAX_PLAYERS);
|
|
451
|
+
this.snapEntities = new Uint8Array(MAX_ENTITIES);
|
|
452
|
+
this.snapPlayers = new Uint8Array(MAX_PLAYERS);
|
|
350
453
|
}
|
|
351
454
|
|
|
352
455
|
init() {
|
|
353
456
|
const w = this.writer;
|
|
354
457
|
const h = SimChannel.offsets.header;
|
|
458
|
+
w.beginWrite();
|
|
355
459
|
w.header.u32[h.maxEntities] = MAX_ENTITIES;
|
|
356
460
|
w.header.u32[h.maxPlayers] = MAX_PLAYERS;
|
|
357
461
|
w.header.u32[h.entityCount] = 0;
|
|
358
462
|
w.header.u32[h.playerCount] = 0;
|
|
359
463
|
w.header.u32[h.activePlayers] = 0;
|
|
464
|
+
w.endWrite();
|
|
360
465
|
}
|
|
361
466
|
|
|
362
|
-
setEntityCount(n: number) { this.writer.header.u32[SimChannel.offsets.header.entityCount] = n; }
|
|
363
|
-
setPlayerCount(n: number) { this.writer.header.u32[SimChannel.offsets.header.playerCount] = n; }
|
|
364
|
-
setTimeOfDay(t: number) { this.writer.header.f32[SimChannel.offsets.header.timeOfDay] = t; }
|
|
467
|
+
setEntityCount(n: number) { this.writer.beginWrite(); this.writer.header.u32[SimChannel.offsets.header.entityCount] = n; }
|
|
468
|
+
setPlayerCount(n: number) { this.writer.beginWrite(); this.writer.header.u32[SimChannel.offsets.header.playerCount] = n; }
|
|
469
|
+
setTimeOfDay(t: number) { this.writer.beginWrite(); this.writer.header.f32[SimChannel.offsets.header.timeOfDay] = t; }
|
|
365
470
|
setWeather(type: number, intensity: number) {
|
|
471
|
+
this.writer.beginWrite();
|
|
366
472
|
this.writer.header.u32[SimChannel.offsets.header.weatherType] = type;
|
|
367
473
|
this.writer.header.f32[SimChannel.offsets.header.weatherIntensity] = intensity;
|
|
368
474
|
}
|
|
369
475
|
setWind(speed: number, dirX: number, dirZ: number) {
|
|
476
|
+
this.writer.beginWrite();
|
|
370
477
|
this.writer.header.f32[SimChannel.offsets.header.windSpeed] = speed;
|
|
371
478
|
this.writer.header.f32[SimChannel.offsets.header.windDirX] = dirX;
|
|
372
479
|
this.writer.header.f32[SimChannel.offsets.header.windDirZ] = dirZ;
|
|
373
480
|
}
|
|
374
|
-
setVisibility(v: number) { this.writer.header.f32[SimChannel.offsets.header.visibility] = v; }
|
|
375
|
-
setAmbientTemp(t: number) { this.writer.header.f32[SimChannel.offsets.header.ambientTemp] = t; }
|
|
376
|
-
setActivePlayers(mask: number) { this.writer.header.u32[SimChannel.offsets.header.activePlayers] = mask; }
|
|
377
|
-
setGamemode(mode: number) { this.writer.header.u32[SimChannel.offsets.header.gamemode] = mode; }
|
|
378
|
-
setPhysicsInitialized(v: number) { this.writer.header.u32[SimChannel.offsets.header.physicsInitialized] = v; }
|
|
379
|
-
setPhysicsFailed(v: number) { this.writer.header.u32[SimChannel.offsets.header.physicsFailed] = v; }
|
|
380
|
-
setPhysicsBodyCount(v: number) { this.writer.header.u32[SimChannel.offsets.header.physicsBodyCount] = v; }
|
|
381
|
-
setPhysicsTickCount(v: number) { this.writer.header.u32[SimChannel.offsets.header.physicsTickCount] = v; }
|
|
382
|
-
setChunkCount(v: number) { this.writer.header.u32[SimChannel.offsets.header.chunkCount] = v; }
|
|
383
|
-
setPhysicsProfilerEnabled(v: number) { this.writer.header.u32[SimChannel.offsets.header.physicsProfilerEnabled] = v; }
|
|
481
|
+
setVisibility(v: number) { this.writer.beginWrite(); this.writer.header.f32[SimChannel.offsets.header.visibility] = v; }
|
|
482
|
+
setAmbientTemp(t: number) { this.writer.beginWrite(); this.writer.header.f32[SimChannel.offsets.header.ambientTemp] = t; }
|
|
483
|
+
setActivePlayers(mask: number) { this.writer.beginWrite(); this.writer.header.u32[SimChannel.offsets.header.activePlayers] = mask; }
|
|
484
|
+
setGamemode(mode: number) { this.writer.beginWrite(); this.writer.header.u32[SimChannel.offsets.header.gamemode] = mode; }
|
|
485
|
+
setPhysicsInitialized(v: number) { this.writer.beginWrite(); this.writer.header.u32[SimChannel.offsets.header.physicsInitialized] = v; }
|
|
486
|
+
setPhysicsFailed(v: number) { this.writer.beginWrite(); this.writer.header.u32[SimChannel.offsets.header.physicsFailed] = v; }
|
|
487
|
+
setPhysicsBodyCount(v: number) { this.writer.beginWrite(); this.writer.header.u32[SimChannel.offsets.header.physicsBodyCount] = v; }
|
|
488
|
+
setPhysicsTickCount(v: number) { this.writer.beginWrite(); this.writer.header.u32[SimChannel.offsets.header.physicsTickCount] = v; }
|
|
489
|
+
setChunkCount(v: number) { this.writer.beginWrite(); this.writer.header.u32[SimChannel.offsets.header.chunkCount] = v; }
|
|
490
|
+
setPhysicsProfilerEnabled(v: number) { this.writer.beginWrite(); this.writer.header.u32[SimChannel.offsets.header.physicsProfilerEnabled] = v; }
|
|
384
491
|
setPhysicsTiming(data: PhysicsTimingData) {
|
|
492
|
+
this.writer.beginWrite();
|
|
385
493
|
const h = this.writer.header.f32;
|
|
386
494
|
const o = SimChannel.offsets.header;
|
|
387
495
|
h[o.physTimingStep] = data.step;
|
|
@@ -402,10 +510,58 @@ export class SimBufferWriter {
|
|
|
402
510
|
h[o.physTimingUserChanges] = data.userChanges;
|
|
403
511
|
}
|
|
404
512
|
|
|
513
|
+
/**
|
|
514
|
+
* Copy the slot's current transform (pos+rot, first 8 f32s) into its
|
|
515
|
+
* prev slot — once per publish, on first write access, so prev always
|
|
516
|
+
* holds the last committed transform for interpolation.
|
|
517
|
+
*/
|
|
518
|
+
private snapshotEntityPrev(idx: number): void {
|
|
519
|
+
if (this.snapEntities[idx]) return;
|
|
520
|
+
this.snapEntities[idx] = 1;
|
|
521
|
+
this.entityPrevSlots.slot(idx).f32.set(this.entitySlots.slot(idx).f32.subarray(0, 8));
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
private snapshotPlayerPrev(idx: number): void {
|
|
525
|
+
if (this.snapPlayers[idx]) return;
|
|
526
|
+
this.snapPlayers[idx] = 1;
|
|
527
|
+
this.playerPrevSlots.slot(idx).f32.set(this.playerSlots.slot(idx).f32.subarray(0, 8));
|
|
528
|
+
}
|
|
529
|
+
|
|
405
530
|
incrementTick() {
|
|
406
531
|
const h = SimChannel.offsets.header;
|
|
407
|
-
|
|
408
|
-
this
|
|
532
|
+
const w = this.writer;
|
|
533
|
+
// Prev-sync: slots not written this publish keep their old cur, so
|
|
534
|
+
// prev must equal cur (identity lerp). Slots that were written were
|
|
535
|
+
// already snapshotted on first access. Runs inside the open write
|
|
536
|
+
// window, before the commit — readers never see a mixed prev/cur.
|
|
537
|
+
const entCount = Math.min(w.header.u32[h.entityCount], MAX_ENTITIES);
|
|
538
|
+
for (let i = 0; i < entCount; i++) {
|
|
539
|
+
if (!this.snapEntities[i]) {
|
|
540
|
+
this.entityPrevSlots.slot(i).f32.set(this.entitySlots.slot(i).f32.subarray(0, 8));
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
this.snapEntities.fill(0);
|
|
544
|
+
const plrCount = Math.min(w.header.u32[h.playerCount], MAX_PLAYERS);
|
|
545
|
+
for (let i = 0; i < plrCount; i++) {
|
|
546
|
+
if (!this.snapPlayers[i]) {
|
|
547
|
+
this.playerPrevSlots.slot(i).f32.set(this.playerSlots.slot(i).f32.subarray(0, 8));
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
this.snapPlayers.fill(0);
|
|
551
|
+
|
|
552
|
+
// Wall-clock epoch ms — performance.timeOrigin is defined per thread
|
|
553
|
+
// and makes the stamp comparable on the renderer side.
|
|
554
|
+
const now = (performance.timeOrigin ?? 0) + performance.now();
|
|
555
|
+
const tIdx = h.publishTimeMs >> 1; // u32 index 40 → f64 index 20
|
|
556
|
+
const last = w.header.f64[tIdx];
|
|
557
|
+
w.header.f64[tIdx] = now;
|
|
558
|
+
w.header.f32[h.publishDeltaMs] = last > 0 ? now - last : 0;
|
|
559
|
+
|
|
560
|
+
Atomics.add(w.header.u32, h.tick, 1);
|
|
561
|
+
// Closes the beginWrite() window opened by the first mutation of this
|
|
562
|
+
// publish (odd→even) — or steps +2 when nothing wrote this tick, so
|
|
563
|
+
// the sequence still advances once per publish for change detection.
|
|
564
|
+
w.bumpSequence();
|
|
409
565
|
}
|
|
410
566
|
|
|
411
567
|
markEntityDirty(slot: number): void {
|
|
@@ -436,18 +592,26 @@ export class SimBufferWriter {
|
|
|
436
592
|
}
|
|
437
593
|
|
|
438
594
|
getEntityF32(idx: number): Float32Array {
|
|
595
|
+
this.writer.beginWrite();
|
|
596
|
+
this.snapshotEntityPrev(idx);
|
|
439
597
|
return this.entitySlots.slot(idx).f32;
|
|
440
598
|
}
|
|
441
599
|
|
|
442
600
|
getEntityU32(idx: number): Uint32Array {
|
|
601
|
+
this.writer.beginWrite();
|
|
602
|
+
this.snapshotEntityPrev(idx);
|
|
443
603
|
return this.entitySlots.slot(idx).u32;
|
|
444
604
|
}
|
|
445
605
|
|
|
446
606
|
getPlayerF32(idx: number): Float32Array {
|
|
607
|
+
this.writer.beginWrite();
|
|
608
|
+
this.snapshotPlayerPrev(idx);
|
|
447
609
|
return this.playerSlots.slot(idx).f32;
|
|
448
610
|
}
|
|
449
611
|
|
|
450
612
|
getPlayerU32(idx: number): Uint32Array {
|
|
613
|
+
this.writer.beginWrite();
|
|
614
|
+
this.snapshotPlayerPrev(idx);
|
|
451
615
|
return this.playerSlots.slot(idx).u32;
|
|
452
616
|
}
|
|
453
617
|
}
|
|
@@ -117,6 +117,32 @@ export class SlotReader {
|
|
|
117
117
|
return this.getSequence() !== lastSeen;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
/**
|
|
121
|
+
* True while the writer holds an open write window (sequence is odd).
|
|
122
|
+
* Readers can use this to cheaply defer sync work past a publish.
|
|
123
|
+
*/
|
|
124
|
+
isWriteInProgress(): boolean {
|
|
125
|
+
return (Atomics.load(this.seqArr, 0) & 1) !== 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Run `fn` against a consistent snapshot of the channel. The sequence
|
|
130
|
+
* word is a seqlock: odd while the writer is publishing, even when the
|
|
131
|
+
* data is stable. The read is retried while the word is odd or changes
|
|
132
|
+
* mid-read; after maxRetries it returns whatever the last attempt read
|
|
133
|
+
* rather than starving the reader (writers publish every tick).
|
|
134
|
+
*/
|
|
135
|
+
readConsistent<T>(fn: () => T, maxRetries = 64): T {
|
|
136
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
137
|
+
const s1 = Atomics.load(this.seqArr, 0);
|
|
138
|
+
if ((s1 & 1) === 0) {
|
|
139
|
+
const out = fn();
|
|
140
|
+
if (Atomics.load(this.seqArr, 0) === s1) return out;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return fn();
|
|
144
|
+
}
|
|
145
|
+
|
|
120
146
|
snapshot(): Record<string, unknown> {
|
|
121
147
|
const data: Record<string, unknown> = {};
|
|
122
148
|
data["sequence"] = this.getSequence();
|
|
@@ -154,6 +180,8 @@ export class SlotWriter {
|
|
|
154
180
|
sections: Record<string, SlotAccessor>;
|
|
155
181
|
private seqArr: Int32Array;
|
|
156
182
|
private layout: ChannelLayout;
|
|
183
|
+
/** True while a beginWrite() window is open (sequence is odd). */
|
|
184
|
+
private writeOpen = false;
|
|
157
185
|
|
|
158
186
|
constructor(sab: SharedArrayBuffer, layout: ChannelLayout) {
|
|
159
187
|
this.layout = layout;
|
|
@@ -180,10 +208,36 @@ export class SlotWriter {
|
|
|
180
208
|
this.header.u32[this.layout.header.versionIndex] = this.layout.version;
|
|
181
209
|
}
|
|
182
210
|
|
|
183
|
-
|
|
211
|
+
/**
|
|
212
|
+
* Open a write window — marks the sequence word odd so readers know a
|
|
213
|
+
* publish is in progress. Idempotent: nested/extra calls are free, so
|
|
214
|
+
* mutating accessors can call it unconditionally on every write path.
|
|
215
|
+
*/
|
|
216
|
+
beginWrite(): void {
|
|
217
|
+
if (this.writeOpen) return;
|
|
218
|
+
this.writeOpen = true;
|
|
184
219
|
Atomics.add(this.seqArr, 0, 1);
|
|
185
220
|
}
|
|
186
221
|
|
|
222
|
+
/** Close the write window opened by beginWrite() — sequence back to even. */
|
|
223
|
+
endWrite(): void {
|
|
224
|
+
if (!this.writeOpen) return;
|
|
225
|
+
this.writeOpen = false;
|
|
226
|
+
Atomics.add(this.seqArr, 0, 1);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Publish a commit. Legacy callers use this without a beginWrite()
|
|
231
|
+
* window: it then steps +2 (even→even) so the sequence always lands on
|
|
232
|
+
* a stable value while still advancing once per publish. Inside a
|
|
233
|
+
* beginWrite() window it closes the window (+1, odd→even).
|
|
234
|
+
*/
|
|
235
|
+
bumpSequence(): void {
|
|
236
|
+
const step = this.writeOpen ? 1 : 2;
|
|
237
|
+
this.writeOpen = false;
|
|
238
|
+
Atomics.add(this.seqArr, 0, step);
|
|
239
|
+
}
|
|
240
|
+
|
|
187
241
|
getSequence(): number {
|
|
188
242
|
return Atomics.load(this.seqArr, 0);
|
|
189
243
|
}
|
package/core/src/sab/types.ts
CHANGED
|
@@ -126,6 +126,13 @@ export interface ChannelReader {
|
|
|
126
126
|
validationError(): string | null;
|
|
127
127
|
getSequence(): number;
|
|
128
128
|
hasChanged(lastSeen: number): boolean;
|
|
129
|
+
/** True while the writer holds an open write window (sequence odd). */
|
|
130
|
+
isWriteInProgress(): boolean;
|
|
131
|
+
/**
|
|
132
|
+
* Run `fn` against a consistent snapshot — retries while the writer is
|
|
133
|
+
* mid-publish or publishes during the read (seqlock). Bounded retries.
|
|
134
|
+
*/
|
|
135
|
+
readConsistent<T>(fn: () => T, maxRetries?: number): T;
|
|
129
136
|
header: HeaderViews;
|
|
130
137
|
sections?: Record<string, SlotAccessor>;
|
|
131
138
|
layers?: Record<string, Float32Array | Int32Array | Uint32Array | Float64Array>;
|
|
@@ -135,6 +142,10 @@ export interface ChannelReader {
|
|
|
135
142
|
|
|
136
143
|
export interface ChannelWriter {
|
|
137
144
|
init(): void;
|
|
145
|
+
/** Open a write window (sequence → odd). Idempotent. */
|
|
146
|
+
beginWrite(): void;
|
|
147
|
+
/** Close the current write window (sequence → even). */
|
|
148
|
+
endWrite(): void;
|
|
138
149
|
bumpSequence(): void;
|
|
139
150
|
getSequence(): number;
|
|
140
151
|
header: HeaderViews;
|
|
@@ -168,6 +168,26 @@ export class GPUResourceTracker {
|
|
|
168
168
|
* inform. Enable via constructor for leak-hunting sessions.
|
|
169
169
|
*/
|
|
170
170
|
private captureCallsites: boolean;
|
|
171
|
+
/**
|
|
172
|
+
* Resources dropped without destroy() are released through the native
|
|
173
|
+
* FinalizationRegistry (platform-native/gpu/registry.ts) — the wrapped
|
|
174
|
+
* destroy() never runs, so without this second registry their tracked
|
|
175
|
+
* entries (and reported VRAM) would grow forever. The held value is the
|
|
176
|
+
* tracked id; the resource object itself is the unregister token.
|
|
177
|
+
*/
|
|
178
|
+
private gcRegistry = new FinalizationRegistry<number>((id) => {
|
|
179
|
+
const r = this.resources.get(id);
|
|
180
|
+
if (!r) return;
|
|
181
|
+
this.resources.delete(id);
|
|
182
|
+
if (r.type === "texture") {
|
|
183
|
+
this.textureBytes -= r.size;
|
|
184
|
+
this.textureCount--;
|
|
185
|
+
} else {
|
|
186
|
+
this.bufferBytes -= r.size;
|
|
187
|
+
this.bufferCount--;
|
|
188
|
+
}
|
|
189
|
+
this.dirty = true;
|
|
190
|
+
});
|
|
171
191
|
|
|
172
192
|
constructor(opts: { captureCallsites?: boolean } = {}) {
|
|
173
193
|
this.captureCallsites = opts.captureCallsites ?? false;
|
|
@@ -201,6 +221,7 @@ export class GPUResourceTracker {
|
|
|
201
221
|
tracker.textureBytes += bytes;
|
|
202
222
|
tracker.textureCount++;
|
|
203
223
|
tracker.dirty = true;
|
|
224
|
+
tracker.gcRegistry.register(tex, id, tex);
|
|
204
225
|
|
|
205
226
|
// Wrap destroy to remove from tracking
|
|
206
227
|
const origDestroy = tex.destroy.bind(tex);
|
|
@@ -212,6 +233,7 @@ export class GPUResourceTracker {
|
|
|
212
233
|
tracker.textureCount--;
|
|
213
234
|
tracker.dirty = true;
|
|
214
235
|
}
|
|
236
|
+
tracker.gcRegistry.unregister(tex);
|
|
215
237
|
origDestroy();
|
|
216
238
|
};
|
|
217
239
|
|
|
@@ -234,6 +256,7 @@ export class GPUResourceTracker {
|
|
|
234
256
|
tracker.bufferBytes += bytes;
|
|
235
257
|
tracker.bufferCount++;
|
|
236
258
|
tracker.dirty = true;
|
|
259
|
+
tracker.gcRegistry.register(buf, id, buf);
|
|
237
260
|
|
|
238
261
|
// Wrap destroy to remove from tracking
|
|
239
262
|
const origDestroy = buf.destroy.bind(buf);
|
|
@@ -245,6 +268,7 @@ export class GPUResourceTracker {
|
|
|
245
268
|
tracker.bufferCount--;
|
|
246
269
|
tracker.dirty = true;
|
|
247
270
|
}
|
|
271
|
+
tracker.gcRegistry.unregister(buf);
|
|
248
272
|
origDestroy();
|
|
249
273
|
};
|
|
250
274
|
|
package/core/src/util/index.ts
CHANGED
|
@@ -14,4 +14,5 @@ export {
|
|
|
14
14
|
export type { FeatureLogData } from "./feature-log";
|
|
15
15
|
export { addLogSink, ConsoleLogger, createLogger, getLogTheme, getRecentLogs, setThreadTag } from "./logger";
|
|
16
16
|
export type { Logger, LogSink, LogSinkEntry } from "./logger";
|
|
17
|
+
export { untrackedTimeout } from "./session-timer";
|
|
17
18
|
|
package/core/src/util/logger.ts
CHANGED
|
@@ -272,12 +272,12 @@ function highlightJson(json: string): string {
|
|
|
272
272
|
out += jsonColors().string + str + reset;
|
|
273
273
|
}
|
|
274
274
|
i = end + 1;
|
|
275
|
-
} else if (/[
|
|
275
|
+
} else if (/[{\}[\]]/.test(ch)) {
|
|
276
276
|
out += jsonColors().brace + ch + reset;
|
|
277
277
|
i++;
|
|
278
278
|
} else if (/\d/.test(ch) || (ch === '-' && /\d/.test(json[i + 1]))) {
|
|
279
279
|
let end = i + 1;
|
|
280
|
-
while (end < json.length && /[\d.eE
|
|
280
|
+
while (end < json.length && /[\d.eE+-]/.test(json[end])) end++;
|
|
281
281
|
out += jsonColors().number + json.slice(i, end) + reset;
|
|
282
282
|
i = end;
|
|
283
283
|
} else if (json.slice(i, i + 4) === 'true') {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// session-timer.ts — scheduling that survives session teardown
|
|
3
|
+
//
|
|
4
|
+
// Under the native dev shell the SessionTracker wraps global setTimeout:
|
|
5
|
+
// callbacks armed while the session is dead (teardown in flight) self-disarm
|
|
6
|
+
// via WeakRef and never run — correct for session-owned work, fatal for
|
|
7
|
+
// bound/grace-period timers whose whole job is to fire DURING teardown
|
|
8
|
+
// (shutdown-RPC bounds, retire fallbacks, response timeouts). Those must
|
|
9
|
+
// route through the tracker's untrackedTimeout. Outside the dev shell
|
|
10
|
+
// __ddSession is absent and this is a plain setTimeout.
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* setTimeout that is exempt from dev-session tracking. The returned id pairs
|
|
15
|
+
* with plain clearTimeout (the wrapped clear delegates to the platform clear).
|
|
16
|
+
*/
|
|
17
|
+
export function untrackedTimeout(fn: () => void, ms: number): ReturnType<typeof setTimeout> {
|
|
18
|
+
const tracker = (globalThis as any).__ddSession;
|
|
19
|
+
if (typeof tracker?.untrackedTimeout === "function") {
|
|
20
|
+
return tracker.untrackedTimeout(fn, ms);
|
|
21
|
+
}
|
|
22
|
+
return setTimeout(fn, ms);
|
|
23
|
+
}
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
import { usingRealSAB } from "../sab/sab-polyfill";
|
|
26
26
|
import { createLogger } from "../util/logger";
|
|
27
|
+
import { untrackedTimeout } from "../util/session-timer";
|
|
27
28
|
import type { BufferSyncConfig, BufferSyncHost } from "./buffer-sync";
|
|
28
29
|
import { wrap, type WorkerApi, type WorkerProxy } from "./rpc";
|
|
29
30
|
|
|
@@ -171,7 +172,7 @@ export abstract class BaseWorkerHost<TApi extends WorkerApi> {
|
|
|
171
172
|
await Promise.race([
|
|
172
173
|
this.proxy.proxy.shutdown(),
|
|
173
174
|
new Promise((_, rej) => {
|
|
174
|
-
timer =
|
|
175
|
+
timer = untrackedTimeout(() => rej(new Error("shutdown RPC timed out")), 2_000);
|
|
175
176
|
}),
|
|
176
177
|
]);
|
|
177
178
|
} finally {
|
|
@@ -113,10 +113,10 @@ export interface BufferSyncMessage {
|
|
|
113
113
|
regions: Record<string, RegionCopy[]>;
|
|
114
114
|
/**
|
|
115
115
|
* Multi-message transfer metadata. Large transfers are split into one
|
|
116
|
-
* region-copy per message
|
|
117
|
-
* between
|
|
118
|
-
* ATOMICALLY once all arrive — otherwise the buffer
|
|
119
|
-
*
|
|
116
|
+
* region-copy per message (each its own macrotask, so the main thread
|
|
117
|
+
* can render between deliveries); the receiver buffers the parts and
|
|
118
|
+
* applies them ATOMICALLY once all arrive — otherwise the buffer would
|
|
119
|
+
* visibly tear while partially-updated regions are rendered.
|
|
120
120
|
*/
|
|
121
121
|
batch?: { id: number; index: number; total: number };
|
|
122
122
|
}
|
|
@@ -457,10 +457,11 @@ export class BufferSyncWorker {
|
|
|
457
457
|
len = Math.min(r.length, count * r.lengthMultiplier);
|
|
458
458
|
}
|
|
459
459
|
// Chunked transfer: if the region is large, split into chunks
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
// and postMessage structured
|
|
460
|
+
// sent as separate postMessages so the main thread interleaves
|
|
461
|
+
// receive+apply tasks with rAF/render instead of blocking on one
|
|
462
|
+
// giant structured clone. This prevents 100-900ms main-thread
|
|
463
|
+
// blocks where SAB is unavailable (and postMessage structured
|
|
464
|
+
// clone is ~10MB/s).
|
|
464
465
|
const CHUNK_SIZE = 64 * 1024; // 64KB per chunk — small enough to avoid >50ms blocks
|
|
465
466
|
if (len > CHUNK_SIZE) {
|
|
466
467
|
const numChunks = Math.ceil(len / CHUNK_SIZE);
|
|
@@ -497,10 +498,16 @@ export class BufferSyncWorker {
|
|
|
497
498
|
// main thread to render between them.
|
|
498
499
|
const allCopies = Object.values(regions).flat();
|
|
499
500
|
if (allCopies.length > 1 && totalBytes > 256 * 1024) {
|
|
500
|
-
// Large transfer: send chunks
|
|
501
|
-
//
|
|
502
|
-
// metadata — the receiver buffers parts and
|
|
503
|
-
// once all arrive, so the buffer never
|
|
501
|
+
// Large transfer: send chunks as separate postMessages so the main
|
|
502
|
+
// thread interleaves receive+apply tasks with rAF/render. Each
|
|
503
|
+
// message carries batch metadata — the receiver buffers parts and
|
|
504
|
+
// applies them atomically once all arrive, so the buffer never
|
|
505
|
+
// renders partially-updated. A setTimeout(0) between posts yields
|
|
506
|
+
// the worker's macrotask queue once per chunk — letting urgent
|
|
507
|
+
// worker-side messages (RPC responses, other sync traffic) insert
|
|
508
|
+
// between chunks rather than queueing behind the whole batch — but
|
|
509
|
+
// adds no fixed delay: measured delivery was ~16ms/chunk with a
|
|
510
|
+
// 16ms gap (~1s for a 4MB region) vs sub-ms/chunk now.
|
|
504
511
|
// Precompute chunk→buffer ownership once (was O(chunks×regions) before).
|
|
505
512
|
const chunkOwner: string[] = [];
|
|
506
513
|
for (const [name, regionList] of Object.entries(regions)) {
|
|
@@ -520,7 +527,7 @@ export class BufferSyncWorker {
|
|
|
520
527
|
[copy.data],
|
|
521
528
|
);
|
|
522
529
|
if (chunkIdx < total) {
|
|
523
|
-
setTimeout(sendNextChunk,
|
|
530
|
+
setTimeout(sendNextChunk, 0); // yield the macrotask queue between chunk posts
|
|
524
531
|
}
|
|
525
532
|
};
|
|
526
533
|
sendNextChunk();
|