@downdraft/engine 0.2.0 → 0.3.1
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 +0 -6
- package/app/src/renderer/feature-log.ts +1 -9
- package/app/src/renderer/mcp-harness.ts +8 -11
- package/app/src/renderer/standard-automation-tools.ts +8 -145
- package/core/src/index.ts +2 -2
- package/core/src/module/host.ts +3 -6
- package/core/src/module/index.ts +1 -1
- package/core/src/module/module.ts +4 -47
- package/core/src/physics/interface.ts +5 -0
- package/core/src/platform/render-surface.ts +13 -0
- package/core/src/platform/rpc.ts +4 -1
- package/core/src/render/canvas-resize-watcher.ts +1 -10
- package/core/src/render/game-renderer.ts +164 -79
- package/core/src/render/index.ts +0 -2
- package/core/src/render/input-manager.ts +7 -7
- package/core/src/render/renderer-module-host.ts +2 -4
- 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/ui-font-scale.ts +5 -38
- package/core/src/util/index.ts +1 -0
- 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 +119 -5
- package/core/src/worker/sim-worker-base.ts +5 -2
- package/core/src/worker/task-pool.ts +4 -0
- package/dist/.dd-manifest.json +893 -908
- package/dist/app/src/renderer/bootstrap.js +0 -4
- package/dist/app/src/renderer/feature-log.js +1 -10
- package/dist/app/src/renderer/mcp-harness.js +8 -11
- package/dist/app/src/renderer/standard-automation-tools.js +6 -138
- package/dist/core/src/index.js +2 -2
- package/dist/core/src/module/host.js +3 -6
- package/dist/core/src/platform/rpc.js +2 -1
- package/dist/core/src/render/canvas-resize-watcher.js +1 -10
- package/dist/core/src/render/game-renderer.js +166 -74
- package/dist/core/src/render/index.js +0 -1
- package/dist/core/src/render/input-manager.js +7 -7
- package/dist/core/src/render/renderer-module-host.js +2 -4
- 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/ui-font-scale.js +5 -38
- package/dist/core/src/util/index.js +1 -0
- 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 +105 -8
- package/dist/core/src/worker/sim-worker-base.js +5 -2
- package/dist/core/src/worker/task-pool.js +5 -0
- package/dist/libraries/blitz-ui/src/native-module.js +8 -1
- package/dist/libraries/devtools/src/backend.js +35 -9
- package/dist/libraries/devtools/src/collectors.js +73 -22
- package/dist/libraries/devtools/src/native-providers.js +0 -2
- package/dist/libraries/entities/src/model-renderer.js +87 -27
- package/dist/libraries/entities/src/shaders/model.wgsl +13 -4
- package/dist/libraries/networking/src/index.js +1 -0
- package/dist/libraries/networking/src/ws-server.js +387 -0
- package/dist/libraries/physics-rapier/src/api.js +15 -0
- package/dist/libraries/physics-rapier/src/backend.js +15 -0
- package/dist/libraries/physics-rapier/src/rapier-backend.js +5 -0
- package/dist/libraries/postfx/src/post-process-stack.js +1 -3
- package/dist/libraries/sand/src/materials.js +2 -32
- package/dist/libraries/sand/src/sand-world.js +0 -3
- package/dist/modules/devtools/src/api.js +8 -23
- package/dist/modules/devtools/src/blitz/fonts.js +75 -0
- package/dist/modules/devtools/src/blitz/host.js +124 -30
- 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 +26 -12
- 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/devtools/src/data-bridge.js +0 -11
- package/dist/modules/devtools/src/index.js +0 -6
- package/dist/modules/devtools/src/init.js +8 -42
- package/dist/modules/devtools/src/scene-inspector.js +1 -4
- package/dist/modules/devtools/src/worker-sync.js +3 -11
- package/dist/modules/html-ui/src/doc-backend.js +52 -3
- package/dist/modules/html-ui/src/host.js +6 -3
- package/dist/test/src/vision-test.js +1 -3
- package/libraries/blitz-ui/native/Cargo.toml +6 -0
- package/libraries/blitz-ui/native-osr/Cargo.toml +3 -1
- package/libraries/blitz-ui/src/native-module.ts +8 -1
- package/libraries/devtools/src/backend.ts +38 -12
- package/libraries/devtools/src/collectors.ts +75 -22
- package/libraries/devtools/src/native-providers.ts +0 -2
- package/libraries/entities/src/model-renderer.ts +88 -27
- package/libraries/entities/src/shaders/model.wgsl +13 -4
- package/libraries/networking/src/index.ts +4 -0
- package/libraries/networking/src/ws-server.ts +397 -0
- package/libraries/physics-rapier/src/api.ts +15 -0
- package/libraries/physics-rapier/src/backend.ts +15 -0
- package/libraries/physics-rapier/src/rapier-backend.ts +6 -0
- package/libraries/postfx/src/post-process-stack.ts +1 -2
- package/libraries/sand/src/materials.ts +2 -32
- package/libraries/sand/src/sand-world.ts +0 -4
- package/modules/devtools/README.md +1 -3
- package/modules/devtools/src/api.ts +10 -34
- package/modules/devtools/src/blitz/fonts.ts +78 -0
- package/modules/devtools/src/blitz/host.ts +107 -31
- 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 +24 -12
- 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/devtools/src/data-bridge.ts +0 -16
- package/modules/devtools/src/index.ts +1 -11
- package/modules/devtools/src/init.ts +8 -51
- package/modules/devtools/src/scene-inspector.ts +2 -6
- package/modules/devtools/src/types.ts +5 -90
- package/modules/devtools/src/worker-sync.ts +3 -13
- package/modules/html-ui/src/doc-backend.ts +67 -4
- package/modules/html-ui/src/host.ts +11 -3
- package/modules/html-ui/src/protocol.ts +1 -1
- package/package.json +4 -5
- package/test/src/vision-test.ts +2 -4
- package/core/src/render/sab-canvas-overlay.ts +0 -122
- package/dist/core/src/render/sab-canvas-overlay.js +0 -83
- package/dist/modules/devtools/src/debug-overlay.js +0 -181
- package/dist/modules/devtools/src/doctor-panel.js +0 -163
- package/dist/modules/devtools/src/label-overlay.js +0 -129
- package/dist/modules/devtools/src/material-stats-panel.js +0 -171
- package/dist/modules/devtools/src/sim-stats-panel.js +0 -250
- package/modules/devtools/src/debug-overlay.ts +0 -238
- package/modules/devtools/src/doctor-panel.ts +0 -182
- package/modules/devtools/src/label-overlay.ts +0 -154
- package/modules/devtools/src/material-stats-panel.ts +0 -188
- package/modules/devtools/src/sim-stats-panel.ts +0 -272
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// Works in both Web Workers and Node.js worker_threads.
|
|
5
5
|
// ============================================================================
|
|
6
6
|
import { createLogger } from "../util/logger.js";
|
|
7
|
+
import { untrackedTimeout } from "../util/session-timer.js";
|
|
7
8
|
const log = createLogger();
|
|
8
9
|
// In Node.js/Bun worker_threads, `parentPort` is non-null only inside a
|
|
9
10
|
// worker connected to a parent — its presence IS the worker check. (The old
|
|
@@ -54,6 +55,54 @@ export function getWorkerHost() {
|
|
|
54
55
|
},
|
|
55
56
|
};
|
|
56
57
|
}
|
|
58
|
+
// --- Event channel stats ---
|
|
59
|
+
// Cheap counters the DOWNDRAFT_EVENT_STATS logger (and QA tooling) read to
|
|
60
|
+
// verify batching is actually engaging: emitted events vs postMessage posts
|
|
61
|
+
// on the worker side, messages vs delivered events on the host side.
|
|
62
|
+
const eventChannelStats = {
|
|
63
|
+
/** Worker side: emit() calls. */
|
|
64
|
+
emitted: 0,
|
|
65
|
+
/** Worker side: postMessage calls carrying events (batch or single). */
|
|
66
|
+
posts: 0,
|
|
67
|
+
/** Host side: messages received carrying events. */
|
|
68
|
+
messages: 0,
|
|
69
|
+
/** Host side: events delivered to listeners. */
|
|
70
|
+
events: 0,
|
|
71
|
+
/** Host side: largest observed __eventBatch. */
|
|
72
|
+
maxBatch: 0,
|
|
73
|
+
};
|
|
74
|
+
export function getEventChannelStats() {
|
|
75
|
+
return eventChannelStats;
|
|
76
|
+
}
|
|
77
|
+
function envFlag(name) {
|
|
78
|
+
try {
|
|
79
|
+
return (globalThis.process?.env?.[name] === "1");
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
let statsLoggerStarted = false;
|
|
86
|
+
function startEventStatsLogger() {
|
|
87
|
+
if (statsLoggerStarted || !envFlag("DOWNDRAFT_EVENT_STATS"))
|
|
88
|
+
return;
|
|
89
|
+
statsLoggerStarted = true;
|
|
90
|
+
let prev = { emitted: 0, posts: 0, messages: 0, events: 0 };
|
|
91
|
+
const t = setInterval(() => {
|
|
92
|
+
const s = eventChannelStats;
|
|
93
|
+
const d = {
|
|
94
|
+
emitted: s.emitted - prev.emitted,
|
|
95
|
+
posts: s.posts - prev.posts,
|
|
96
|
+
messages: s.messages - prev.messages,
|
|
97
|
+
events: s.events - prev.events,
|
|
98
|
+
};
|
|
99
|
+
prev = { emitted: s.emitted, posts: s.posts, messages: s.messages, events: s.events };
|
|
100
|
+
if (d.emitted || d.posts || d.messages || d.events) {
|
|
101
|
+
log.info("rpc-events", `5s Δ emitted=${d.emitted} posts=${d.posts} msgs=${d.messages} events=${d.events} maxBatch=${s.maxBatch}`);
|
|
102
|
+
}
|
|
103
|
+
}, 5000);
|
|
104
|
+
t.unref?.();
|
|
105
|
+
}
|
|
57
106
|
function isRpcRequest(msg) {
|
|
58
107
|
return msg && msg.__rpc === true && typeof msg.method === "string";
|
|
59
108
|
}
|
|
@@ -63,6 +112,9 @@ function isRpcResponse(msg) {
|
|
|
63
112
|
function isEventMessage(msg) {
|
|
64
113
|
return msg && msg.__event === true;
|
|
65
114
|
}
|
|
115
|
+
function isEventBatchMessage(msg) {
|
|
116
|
+
return msg && msg.__eventBatch === true;
|
|
117
|
+
}
|
|
66
118
|
export function expose(api, options) {
|
|
67
119
|
const h = options?.host ?? getWorkerHost();
|
|
68
120
|
h.onHostMessage(async (msg) => {
|
|
@@ -84,14 +136,52 @@ export function expose(api, options) {
|
|
|
84
136
|
}
|
|
85
137
|
export function exposeEvents(host) {
|
|
86
138
|
const h = host ?? getWorkerHost();
|
|
139
|
+
startEventStatsLogger();
|
|
87
140
|
return {
|
|
88
141
|
emit: (kind, data) => {
|
|
142
|
+
eventChannelStats.emitted++;
|
|
143
|
+
eventChannelStats.posts++;
|
|
89
144
|
h.postToHost({ __event: true, kind, data });
|
|
90
145
|
},
|
|
91
146
|
};
|
|
92
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* Event emitter that coalesces emit() calls into a single `__eventBatch`
|
|
150
|
+
* postMessage per macrotask. A sim emitting per-tick or per-entity events
|
|
151
|
+
* otherwise produces one structured clone + one main-thread task per event,
|
|
152
|
+
* and the task burst competes with rAF. Events emitted during a tick batch
|
|
153
|
+
* (the worker never yields to timers mid-tick) all land in one message;
|
|
154
|
+
* events emitted between ticks flush on the next macrotask, so latency for
|
|
155
|
+
* isolated events is unchanged.
|
|
156
|
+
*/
|
|
157
|
+
export function exposeBatchedEvents(host) {
|
|
158
|
+
const h = host ?? getWorkerHost();
|
|
159
|
+
startEventStatsLogger();
|
|
160
|
+
let queue = [];
|
|
161
|
+
let flushScheduled = false;
|
|
162
|
+
const flush = () => {
|
|
163
|
+
flushScheduled = false;
|
|
164
|
+
if (queue.length === 0)
|
|
165
|
+
return;
|
|
166
|
+
eventChannelStats.posts++;
|
|
167
|
+
h.postToHost({ __eventBatch: true, events: queue });
|
|
168
|
+
queue = [];
|
|
169
|
+
};
|
|
170
|
+
return {
|
|
171
|
+
emit: (kind, data) => {
|
|
172
|
+
eventChannelStats.emitted++;
|
|
173
|
+
queue.push({ kind, data });
|
|
174
|
+
if (!flushScheduled) {
|
|
175
|
+
flushScheduled = true;
|
|
176
|
+
setTimeout(flush, 0);
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
flush,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
93
182
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
94
183
|
export function wrap(worker, options) {
|
|
184
|
+
startEventStatsLogger();
|
|
95
185
|
const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
96
186
|
// A worker nobody terminates must not pin the runtime after the host's
|
|
97
187
|
// window closes — in-flight RPCs still hold the loop via their timeout.
|
|
@@ -114,13 +204,20 @@ export function wrap(worker, options) {
|
|
|
114
204
|
p.resolve(msg.result);
|
|
115
205
|
return;
|
|
116
206
|
}
|
|
117
|
-
if (isEventMessage(msg)) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
207
|
+
if (isEventMessage(msg) || isEventBatchMessage(msg)) {
|
|
208
|
+
const events = isEventBatchMessage(msg) ? msg.events : [msg];
|
|
209
|
+
eventChannelStats.messages++;
|
|
210
|
+
eventChannelStats.events += events.length;
|
|
211
|
+
if (events.length > eventChannelStats.maxBatch)
|
|
212
|
+
eventChannelStats.maxBatch = events.length;
|
|
213
|
+
for (const ev of events) {
|
|
214
|
+
for (const cb of eventListeners.values()) {
|
|
215
|
+
try {
|
|
216
|
+
cb(ev.kind, ev.data);
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
log.error("worker-proxy", `Event listener error: ${err}`);
|
|
220
|
+
}
|
|
124
221
|
}
|
|
125
222
|
}
|
|
126
223
|
}
|
|
@@ -172,7 +269,7 @@ export function wrap(worker, options) {
|
|
|
172
269
|
const id = ++reqId;
|
|
173
270
|
const entry = { resolve, reject };
|
|
174
271
|
if (timeoutMs > 0) {
|
|
175
|
-
entry.timer =
|
|
272
|
+
entry.timer = untrackedTimeout(() => {
|
|
176
273
|
if (pending.delete(id)) {
|
|
177
274
|
reject(new Error(`RPC '${method}' timed out after ${timeoutMs}ms`));
|
|
178
275
|
}
|
|
@@ -38,7 +38,7 @@ import "../sab/sab-polyfill.js";
|
|
|
38
38
|
import { usingRealSAB } from "../sab/sab-polyfill.js";
|
|
39
39
|
import { mulberry32 } from "../math/rng.js";
|
|
40
40
|
import { createLogger } from "../util/logger.js";
|
|
41
|
-
import { expose,
|
|
41
|
+
import { expose, exposeBatchedEvents } from "./rpc.js";
|
|
42
42
|
const log = createLogger();
|
|
43
43
|
/**
|
|
44
44
|
* Detect deterministic/test mode inside a worker. `DOWNDRAFT_DETERMINISTIC`
|
|
@@ -158,7 +158,7 @@ export function createSimWorker(opts) {
|
|
|
158
158
|
let tickInFlight = null;
|
|
159
159
|
// Serializes withLoopStopped critical sections.
|
|
160
160
|
let stopChain = Promise.resolve();
|
|
161
|
-
const events =
|
|
161
|
+
const events = exposeBatchedEvents();
|
|
162
162
|
// Deterministic RNG — reseedable via control.setSeed(); the stable `rng`
|
|
163
163
|
// closure is what ctx.rng and the Math.random shim draw from.
|
|
164
164
|
let rngImpl = mulberry32(opts.seed ?? 0x9e3779b9);
|
|
@@ -423,6 +423,9 @@ export function createSimWorker(opts) {
|
|
|
423
423
|
syncWorker = null; // release sync worker reference
|
|
424
424
|
saveStore = null;
|
|
425
425
|
await opts.onShutdown?.();
|
|
426
|
+
// Drain any events queued during onShutdown so they aren't lost to
|
|
427
|
+
// a flush timer the terminating worker never runs.
|
|
428
|
+
events.flush();
|
|
426
429
|
},
|
|
427
430
|
async setSpeed(speed) {
|
|
428
431
|
speedMultiplier = Math.min(opts.maxSpeed ?? Infinity, Math.max(opts.minSpeed ?? 0, speed));
|
|
@@ -78,6 +78,11 @@ export class TaskPool {
|
|
|
78
78
|
for (let i = 0; i < this.workerCount; i++) {
|
|
79
79
|
const worker = this.createWorker();
|
|
80
80
|
worker.onerror = (e) => {
|
|
81
|
+
// Session teardown tags tracked workers __ddExpectTerminate and
|
|
82
|
+
// fires synthetic error events to settle waiters — not faults
|
|
83
|
+
// (see session-tracker.ts terminateTrackedWorker).
|
|
84
|
+
if (worker.__ddExpectTerminate || this.destroyed)
|
|
85
|
+
return;
|
|
81
86
|
if (this.onWorkerError)
|
|
82
87
|
this.onWorkerError(i, e);
|
|
83
88
|
else
|
|
@@ -116,7 +116,14 @@ export function createBlitzUiNativeModule(options) {
|
|
|
116
116
|
const device = ctx.getDevice();
|
|
117
117
|
const view = ensureTexture(device);
|
|
118
118
|
const context = ctx.getSurface().getContext("webgpu");
|
|
119
|
-
|
|
119
|
+
// Join the in-flight frame, never start one: on a frame the
|
|
120
|
+
// game skipped (dirty-tracking), acquiring the surface here
|
|
121
|
+
// would present the UI blit over a blank swapchain image.
|
|
122
|
+
const peek = context
|
|
123
|
+
?.peekCurrentTexture;
|
|
124
|
+
const targetTex = typeof peek === "function"
|
|
125
|
+
? peek.call(context)
|
|
126
|
+
: context?.getCurrentTexture() ?? null;
|
|
120
127
|
const target = targetTex?.createView();
|
|
121
128
|
if (!view || !target || !targetTex)
|
|
122
129
|
return;
|
|
@@ -44,6 +44,12 @@ export class DevtoolsBackend {
|
|
|
44
44
|
providers = new Map();
|
|
45
45
|
commandHandlers = new Map();
|
|
46
46
|
globalCommandHandler = null;
|
|
47
|
+
/** Dynamically-allocated slots for provider names outside the fixed PANEL
|
|
48
|
+
* table — panelSlot() alone collapsed every unknown name onto slot 0
|
|
49
|
+
* ("console"), so a second game provider silently replaced the first. */
|
|
50
|
+
dynSlots = new Map();
|
|
51
|
+
dynSlotNames = new Map();
|
|
52
|
+
nextDynSlot = 100;
|
|
47
53
|
lastProviderPush = new Map();
|
|
48
54
|
providerIntervalMs = 1500;
|
|
49
55
|
/** When non-null, the cadence loop only auto-refreshes this provider
|
|
@@ -178,37 +184,57 @@ export class DevtoolsBackend {
|
|
|
178
184
|
this.emit("console.clear", {});
|
|
179
185
|
}
|
|
180
186
|
// ── Providers / commands ──
|
|
187
|
+
/** Resolve a panel id to its slot. Names outside the fixed PANEL table
|
|
188
|
+
* get a stable dynamically-allocated slot so bespoke game providers can
|
|
189
|
+
* coexist instead of colliding on slot 0. */
|
|
190
|
+
slotFor(panel) {
|
|
191
|
+
if (typeof panel === "number")
|
|
192
|
+
return panel;
|
|
193
|
+
const fixed = PANEL[panel];
|
|
194
|
+
if (fixed !== undefined)
|
|
195
|
+
return fixed;
|
|
196
|
+
let s = this.dynSlots.get(panel);
|
|
197
|
+
if (s === undefined) {
|
|
198
|
+
s = this.nextDynSlot++;
|
|
199
|
+
this.dynSlots.set(panel, s);
|
|
200
|
+
this.dynSlotNames.set(s, panel);
|
|
201
|
+
}
|
|
202
|
+
return s;
|
|
203
|
+
}
|
|
204
|
+
nameFor(slot) {
|
|
205
|
+
return this.dynSlotNames.get(slot) ?? slotName(slot);
|
|
206
|
+
}
|
|
181
207
|
registerProvider(panel, collect) {
|
|
182
|
-
this.providers.set(
|
|
208
|
+
this.providers.set(this.slotFor(panel), collect);
|
|
183
209
|
}
|
|
184
210
|
registerCommandHandler(panel, handler) {
|
|
185
211
|
if (panel === "*") {
|
|
186
212
|
this.globalCommandHandler = handler;
|
|
187
213
|
}
|
|
188
214
|
else {
|
|
189
|
-
this.commandHandlers.set(
|
|
215
|
+
this.commandHandlers.set(this.slotFor(panel), handler);
|
|
190
216
|
}
|
|
191
217
|
}
|
|
192
218
|
/** Registered provider slots — the dynamic tab list for frontends. */
|
|
193
219
|
providerSlots() {
|
|
194
|
-
return [...this.providers.keys()].map((slot) => ({ slot, name:
|
|
220
|
+
return [...this.providers.keys()].map((slot) => ({ slot, name: this.nameFor(slot) }));
|
|
195
221
|
}
|
|
196
222
|
/** Collect + emit one provider's snapshot immediately. */
|
|
197
223
|
refreshPanel(panel) {
|
|
198
224
|
const trace = [performance.now()];
|
|
199
|
-
const slot =
|
|
225
|
+
const slot = this.slotFor(panel);
|
|
200
226
|
trace.push(performance.now());
|
|
201
227
|
const collect = this.providers.get(slot);
|
|
202
228
|
if (!collect)
|
|
203
229
|
return;
|
|
204
230
|
const push = (snap) => {
|
|
205
231
|
if (snap)
|
|
206
|
-
this.emit("snapshot", { panel:
|
|
232
|
+
this.emit("snapshot", { panel: this.nameFor(slot), slot, snap });
|
|
207
233
|
this.lastProviderPush.set(slot, performance.now());
|
|
208
234
|
};
|
|
209
235
|
const fail = (err) => {
|
|
210
236
|
this.emit("snapshot", {
|
|
211
|
-
panel:
|
|
237
|
+
panel: this.nameFor(slot), slot,
|
|
212
238
|
snap: { status: "error", statusMsg: String(err), sections: [] },
|
|
213
239
|
});
|
|
214
240
|
this.lastProviderPush.set(slot, performance.now());
|
|
@@ -231,7 +257,7 @@ export class DevtoolsBackend {
|
|
|
231
257
|
const total = trace[4] - trace[0];
|
|
232
258
|
if (total > 10) {
|
|
233
259
|
const seg = `pre=${(trace[2] - trace[0]).toFixed(1)} collect=${(trace[3] - trace[2]).toFixed(1)} emit=${(trace[4] - trace[3]).toFixed(1)}`;
|
|
234
|
-
this.diagWarn(`slow provider ${
|
|
260
|
+
this.diagWarn(`slow provider ${this.nameFor(slot)}: ${total.toFixed(1)}ms [${seg}]`);
|
|
235
261
|
}
|
|
236
262
|
}
|
|
237
263
|
dispatchCommand(cmd) {
|
|
@@ -374,7 +400,7 @@ export class DevtoolsBackend {
|
|
|
374
400
|
return collectThreads(this.ctx, [...this.threadEvals.keys()]);
|
|
375
401
|
case "snapshot": {
|
|
376
402
|
const panel = params.panel;
|
|
377
|
-
const slot = typeof panel === "number" ? panel :
|
|
403
|
+
const slot = typeof panel === "number" ? panel : this.slotFor(String(panel));
|
|
378
404
|
const collect = this.providers.get(slot);
|
|
379
405
|
if (!collect)
|
|
380
406
|
return { status: "unsupported", statusMsg: `no provider for ${String(panel)}`, sections: [] };
|
|
@@ -388,7 +414,7 @@ export class DevtoolsBackend {
|
|
|
388
414
|
}
|
|
389
415
|
case "command": {
|
|
390
416
|
const panel = params.panel;
|
|
391
|
-
const slot = typeof panel === "number" ? panel :
|
|
417
|
+
const slot = typeof panel === "number" ? panel : this.slotFor(String(panel ?? "console"));
|
|
392
418
|
this.dispatchCommand({ panel: slot, action: String(params.action ?? ""), payload: String(params.payload ?? "") });
|
|
393
419
|
return true;
|
|
394
420
|
}
|
|
@@ -72,31 +72,81 @@ export async function collectThreads(ctx, evalThreadIds) {
|
|
|
72
72
|
});
|
|
73
73
|
return threads;
|
|
74
74
|
}
|
|
75
|
-
|
|
75
|
+
function sceneInspectorApi() {
|
|
76
|
+
const g = globalThis;
|
|
77
|
+
return (g.window?.__sceneInspector ?? g.__sceneInspector ?? null);
|
|
78
|
+
}
|
|
79
|
+
/** Scene-graph tree (label + ctor + child counts). Prefers a live
|
|
80
|
+
* `gameScene.stage` walk; falls back to the SceneInspector's scene store
|
|
81
|
+
* (`__sceneInspector.getSceneTree`) so games without a DOM-like stage still
|
|
82
|
+
* get a populated tree — with stable nodeIds for pick/details. */
|
|
76
83
|
export function collectSceneTree(ctx) {
|
|
77
84
|
const stage = ctx.gameScene?.stage;
|
|
78
|
-
if (
|
|
85
|
+
if (stage) {
|
|
86
|
+
const nodes = [];
|
|
87
|
+
let idCounter = 1;
|
|
88
|
+
const collect = (node, parentId, depth) => {
|
|
89
|
+
const id = idCounter++;
|
|
90
|
+
// Prefer `label` (some scene-graph hosts warn on `name` access); fall
|
|
91
|
+
// back to `name` for hosts that only set that.
|
|
92
|
+
const nodeLabel = typeof node.label === "string" ? node.label
|
|
93
|
+
: typeof node.name === "string" ? node.name : "";
|
|
94
|
+
const ctorName = node.constructor?.name ?? "Node";
|
|
95
|
+
nodes.push({
|
|
96
|
+
id, parentId, depth,
|
|
97
|
+
childCount: node.children?.length ?? 0,
|
|
98
|
+
kind: 0,
|
|
99
|
+
label: nodeLabel || ctorName,
|
|
100
|
+
detail: nodeLabel ? ctorName : "",
|
|
101
|
+
nodeId: typeof node.id === "string" ? node.id : undefined,
|
|
102
|
+
});
|
|
103
|
+
node.children?.forEach((child) => collect(child, id, depth + 1));
|
|
104
|
+
};
|
|
105
|
+
collect(stage, -1, 0);
|
|
106
|
+
return nodes;
|
|
107
|
+
}
|
|
108
|
+
// Scene-store fallback — the same snapshot the inspector's getSceneTree
|
|
109
|
+
// exposes. Nodes are flat with id/parentId links; walk roots → children.
|
|
110
|
+
try {
|
|
111
|
+
const snap = sceneInspectorApi()?.getSceneTree?.();
|
|
112
|
+
const storeNodes = snap?.nodes;
|
|
113
|
+
if (!Array.isArray(storeNodes) || !storeNodes.length)
|
|
114
|
+
return [];
|
|
115
|
+
const byId = new Map(storeNodes.map((n) => [n.id, n]));
|
|
116
|
+
const roots = storeNodes.filter((n) => n.parentId === null || !byId.has(n.parentId));
|
|
117
|
+
const rows = [];
|
|
118
|
+
const visited = new Set();
|
|
119
|
+
let idCounter = 1;
|
|
120
|
+
const walk = (n, parentId, depth) => {
|
|
121
|
+
if (visited.has(n.id))
|
|
122
|
+
return; // corrupted parent/child links — don't loop
|
|
123
|
+
visited.add(n.id);
|
|
124
|
+
const id = idCounter++;
|
|
125
|
+
const p = n.position;
|
|
126
|
+
rows.push({
|
|
127
|
+
id, parentId, depth,
|
|
128
|
+
childCount: n.children?.length ?? 0,
|
|
129
|
+
kind: 0,
|
|
130
|
+
label: String(n.name ?? n.id),
|
|
131
|
+
detail: `${n.type ?? "node"}${Array.isArray(p) ? ` (${p.map((v) => (+v).toFixed(1)).join(", ")})` : ""}`,
|
|
132
|
+
nodeId: String(n.id),
|
|
133
|
+
});
|
|
134
|
+
for (const cid of n.children ?? []) {
|
|
135
|
+
const c = byId.get(cid);
|
|
136
|
+
if (c)
|
|
137
|
+
walk(c, id, depth + 1);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
roots.forEach((n) => walk(n, -1, 0));
|
|
141
|
+
// Orphans (every node has a parent but no root resolves) — still show them.
|
|
142
|
+
for (const n of storeNodes)
|
|
143
|
+
if (!visited.has(n.id))
|
|
144
|
+
walk(n, -1, 0);
|
|
145
|
+
return rows;
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
79
148
|
return [];
|
|
80
|
-
|
|
81
|
-
let idCounter = 1;
|
|
82
|
-
const collect = (node, parentId, depth) => {
|
|
83
|
-
const id = idCounter++;
|
|
84
|
-
// Prefer `label` (some scene-graph hosts warn on `name` access); fall
|
|
85
|
-
// back to `name` for hosts that only set that.
|
|
86
|
-
const nodeLabel = typeof node.label === "string" ? node.label
|
|
87
|
-
: typeof node.name === "string" ? node.name : "";
|
|
88
|
-
const ctorName = node.constructor?.name ?? "Node";
|
|
89
|
-
nodes.push({
|
|
90
|
-
id, parentId, depth,
|
|
91
|
-
childCount: node.children?.length ?? 0,
|
|
92
|
-
kind: 0,
|
|
93
|
-
label: nodeLabel || ctorName,
|
|
94
|
-
detail: nodeLabel ? ctorName : "",
|
|
95
|
-
});
|
|
96
|
-
node.children?.forEach((child) => collect(child, id, depth + 1));
|
|
97
|
-
};
|
|
98
|
-
collect(stage, -1, 0);
|
|
99
|
-
return nodes;
|
|
149
|
+
}
|
|
100
150
|
}
|
|
101
151
|
/** DOM tree: scene-graph stage or ECS entities depending on mode. */
|
|
102
152
|
export function collectDomTree(ctx, mode) {
|
|
@@ -122,6 +172,7 @@ export function collectDomTree(ctx, mode) {
|
|
|
122
172
|
id, parentId: -1, depth: 0, childCount: 0, kind: 1,
|
|
123
173
|
label: `Entity ${ent.idx}`,
|
|
124
174
|
detail: `type=${entityType} pos=(${x.toFixed(1)},${y.toFixed(1)},${z.toFixed(1)})`,
|
|
175
|
+
nodeId: `ent-${ent.idx}`,
|
|
125
176
|
});
|
|
126
177
|
});
|
|
127
178
|
}
|
|
@@ -515,8 +515,6 @@ async function collectWorkers(ctx) {
|
|
|
515
515
|
kind: "kv",
|
|
516
516
|
name: `Worker manifest (v${m.manifestVersion ?? "?"})`,
|
|
517
517
|
rows: [
|
|
518
|
-
kv("Panels", (m.panels ?? []).map((p) => p.id ?? p.name ?? "?").join(", ") || "(none)"),
|
|
519
|
-
kv("Toggles", (m.toggles ?? []).map((t) => t.id ?? "?").join(", ") || "(none)"),
|
|
520
518
|
kv("Data feeds", (m.dataFeeds ?? []).map((f) => `${f.name}@${f.writeRateHz}hz`).join(", ") || "(none)"),
|
|
521
519
|
kv("Commands", (m.commands ?? []).join(", ") || "(none)"),
|
|
522
520
|
],
|