@downdraft/engine 0.2.0 → 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/core/src/index.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/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 +5 -2
- package/core/src/worker/task-pool.ts +4 -0
- package/dist/.dd-manifest.json +861 -851
- package/dist/core/src/index.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/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 +5 -2
- 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/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/html-ui/src/doc-backend.js +52 -3
- package/dist/modules/html-ui/src/host.js +6 -3
- 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/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/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 +2 -2
|
@@ -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
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// fonts.ts — bind the host's real system fonts into the devtools dock doc.
|
|
3
|
+
//
|
|
4
|
+
// The OSR rasterizer ships a single embedded DejaVu Sans as the fallback for
|
|
5
|
+
// every generic family — legible, but `monospace` resolves to it too, so the
|
|
6
|
+
// console/REPL/tree read as proportional sans. Same fix as cruiser's
|
|
7
|
+
// libraries/app-shell/fonts.ts: resolve the user's actual sans + mono faces
|
|
8
|
+
// via fontconfig (fc-match -f %{file}), register them as `ui://` blobs on the
|
|
9
|
+
// HtmlUiHost before mount, and emit @font-face rules the dock stylesheet
|
|
10
|
+
// binds ahead of the generic fallbacks. Any failure (no fc-match, non-Linux
|
|
11
|
+
// host, unreadable file) leaves the embedded fallback in place.
|
|
12
|
+
// ============================================================================
|
|
13
|
+
import { execFileSync } from "node:child_process";
|
|
14
|
+
import { readFileSync } from "node:fs";
|
|
15
|
+
// Blitz-dom only fetches a @font-face source whose format is detectable —
|
|
16
|
+
// either an explicit format() hint or a file extension on the url — so the
|
|
17
|
+
// urls carry the resolved file's real extension.
|
|
18
|
+
const FACES = [
|
|
19
|
+
{ family: "DevTools Sans", pattern: "sans-serif", urlName: "sans", weight: 400 },
|
|
20
|
+
{ family: "DevTools Sans", pattern: "sans-serif:weight=bold", urlName: "sans-bold", weight: 700 },
|
|
21
|
+
{ family: "DevTools Mono", pattern: "monospace", urlName: "mono", weight: 400 },
|
|
22
|
+
{ family: "DevTools Mono", pattern: "monospace:weight=bold", urlName: "mono-bold", weight: 700 },
|
|
23
|
+
];
|
|
24
|
+
/** `fc-match -f %{file}` for a fontconfig pattern; null when fc-match is
|
|
25
|
+
* absent or the match isn't a font file Blitz can load. */
|
|
26
|
+
function fcFontFile(pattern) {
|
|
27
|
+
try {
|
|
28
|
+
const p = execFileSync("fc-match", ["-f", "%{file}", pattern], { timeout: 5000 }).toString().trim();
|
|
29
|
+
return /\.(ttf|otf|woff2?)$/i.test(p) ? p : null;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function fontExt(path) {
|
|
36
|
+
return path.match(/\.(ttf|otf|woff2?)$/i)?.[0] ?? ".ttf";
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Register system sans + mono faces as `ui://` resources and return the
|
|
40
|
+
* @font-face CSS to prepend to the dock stylesheet. Returns "" when nothing
|
|
41
|
+
* registered — the stylesheet's named families then fail to resolve and the
|
|
42
|
+
* generic fallbacks (embedded DejaVu) apply.
|
|
43
|
+
*
|
|
44
|
+
* Call before mount — the doc backend applies resources synchronously and in
|
|
45
|
+
* order, so the first doc resolve sees them.
|
|
46
|
+
*/
|
|
47
|
+
export function registerDevtoolsFonts(sink) {
|
|
48
|
+
try {
|
|
49
|
+
// Both weights of a family are required: a 700 rule pointing at the
|
|
50
|
+
// regular file keeps bold text in-family instead of falling back.
|
|
51
|
+
const files = new Map();
|
|
52
|
+
for (const f of FACES)
|
|
53
|
+
files.set(f.urlName, fcFontFile(f.pattern) ?? "");
|
|
54
|
+
if (!files.get("sans"))
|
|
55
|
+
return "";
|
|
56
|
+
if (!files.get("mono"))
|
|
57
|
+
files.set("mono", files.get("sans"));
|
|
58
|
+
if (!files.get("sans-bold"))
|
|
59
|
+
files.set("sans-bold", files.get("sans"));
|
|
60
|
+
if (!files.get("mono-bold"))
|
|
61
|
+
files.set("mono-bold", files.get("mono"));
|
|
62
|
+
let css = "";
|
|
63
|
+
for (const f of FACES) {
|
|
64
|
+
const file = files.get(f.urlName);
|
|
65
|
+
const url = `ui://devtools-fonts/${f.urlName}${fontExt(file)}`;
|
|
66
|
+
sink.registerResource(url, readFileSync(file));
|
|
67
|
+
css += `@font-face { font-family:'${f.family}'; font-weight:${f.weight}; src:url('${url}'); }\n`;
|
|
68
|
+
}
|
|
69
|
+
return css;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// fc-match absent / unreadable font file — the embedded fallback applies.
|
|
73
|
+
return "";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -13,11 +13,13 @@
|
|
|
13
13
|
import { createLogger } from "@downdraft/engine";
|
|
14
14
|
import { CdpBridge, DevtoolsBackend } from "@downdraft/engine/libraries/devtools";
|
|
15
15
|
import { HtmlUiHost } from "@downdraft/engine/modules/html-ui";
|
|
16
|
+
import { registerDevtoolsFonts } from "./fonts.js";
|
|
16
17
|
import { ConsolePanel } from "./panels/console.js";
|
|
17
18
|
import { ElementsPanel } from "./panels/elements.js";
|
|
18
19
|
import { GpuPanel } from "./panels/gpu.js";
|
|
19
20
|
import { PerformancePanel } from "./panels/performance.js";
|
|
20
21
|
import { SnapshotPanel } from "./panels/snapshot.js";
|
|
22
|
+
import { esc } from "./panels/types.js";
|
|
21
23
|
import { DEVTOOLS_CSS } from "./theme.js";
|
|
22
24
|
const log = createLogger("info");
|
|
23
25
|
const MIN_DOCK = 120;
|
|
@@ -42,6 +44,9 @@ export class BlitzDevtoolsHost {
|
|
|
42
44
|
panelCtx;
|
|
43
45
|
lastPanelRender = -1e9;
|
|
44
46
|
lastSlowLog = 0;
|
|
47
|
+
/** @font-face css from registerDevtoolsFonts — "" means "embedded font
|
|
48
|
+
* fallback", null means "not yet attempted". */
|
|
49
|
+
fontCss = null;
|
|
45
50
|
constructor(opts) {
|
|
46
51
|
this.opts = opts;
|
|
47
52
|
this.surfaceW = opts.surface.clientWidth;
|
|
@@ -113,19 +118,42 @@ export class BlitzDevtoolsHost {
|
|
|
113
118
|
show() {
|
|
114
119
|
if (!this._ready || this.visible || this.disposed)
|
|
115
120
|
return;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
121
|
+
// Register real system fonts once per host — ui:// resources persist on
|
|
122
|
+
// the surface across dock remounts, and must land before the doc mounts
|
|
123
|
+
// so the first resolve sees them (embedded DejaVu fallback otherwise).
|
|
124
|
+
if (this.fontCss === null) {
|
|
125
|
+
const ui = this.opts.ui;
|
|
126
|
+
const rr = ui.registerResource?.bind(ui)
|
|
127
|
+
?? (ui.host ? ui.host.registerResource.bind(ui.host) : undefined);
|
|
128
|
+
this.fontCss = rr ? registerDevtoolsFonts({ registerResource: rr }) : "";
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
this.panel = this.opts.ui.mount(this.shellHtml(), {
|
|
132
|
+
id: "devtools",
|
|
133
|
+
rect: this.dockRect(),
|
|
134
|
+
// Above game overlays — menus/loading screens shouldn't bury the dock.
|
|
135
|
+
z: 1000,
|
|
136
|
+
// Native-res raster: the vendored glifo/FreeType path grid-fits hinted
|
|
137
|
+
// glyph stems to whole pixels — supersample factors >1 just blur that
|
|
138
|
+
// back out on downscale (and cost 2.25x the texture memory).
|
|
139
|
+
scale: 1,
|
|
140
|
+
// Ease wheel detents at pump cadence instead of jumping per event.
|
|
141
|
+
smoothWheel: true,
|
|
142
|
+
interactive: true,
|
|
143
|
+
// Devtools UI doesn't need vsync-rate raster — cap it so mutation
|
|
144
|
+
// bursts (console streams, snapshot refreshes) coalesce into ~66ms
|
|
145
|
+
// rasters instead of one raster per message.
|
|
146
|
+
maxFps: 15,
|
|
147
|
+
onEvent: (ev) => this.handleDocEvent(ev),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
// Mount fails when the ui worker is dead — stay hidden so the next F12
|
|
152
|
+
// retries cleanly instead of wedging on a half-mounted doc.
|
|
153
|
+
this.panel = null;
|
|
154
|
+
log.error("blitz-devtools-host", `dock mount failed: ${err}`);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
129
157
|
this.syncProviders();
|
|
130
158
|
this.renderTabs();
|
|
131
159
|
this.activate(this.activeId);
|
|
@@ -150,8 +178,19 @@ export class BlitzDevtoolsHost {
|
|
|
150
178
|
const t1 = performance.now();
|
|
151
179
|
if (!this.visible)
|
|
152
180
|
return;
|
|
153
|
-
|
|
154
|
-
this.
|
|
181
|
+
// Guarded like backend.update() above — a throwing panel render can't be
|
|
182
|
+
// allowed to kill the game frame hook that drives this pump.
|
|
183
|
+
try {
|
|
184
|
+
this.syncProviders();
|
|
185
|
+
this.flush();
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
const tn = performance.now();
|
|
189
|
+
if (tn - this.lastSlowLog > 1000) {
|
|
190
|
+
this.lastSlowLog = tn;
|
|
191
|
+
log.warn("blitz-devtools-host", `panel render error: ${err}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
155
194
|
const t2 = performance.now();
|
|
156
195
|
// ≤1/sec — these diagnostics used to feed the console panel's logger
|
|
157
196
|
// sink, and each row forced another dock re-render: a self-feeding
|
|
@@ -204,11 +243,13 @@ export class BlitzDevtoolsHost {
|
|
|
204
243
|
}
|
|
205
244
|
// ── Dock layout ──
|
|
206
245
|
dockRect() {
|
|
207
|
-
|
|
208
|
-
|
|
246
|
+
// A tiny surface (or a pathological resize event) must not produce a
|
|
247
|
+
// negative height — keep at least enough room for the tab strip.
|
|
248
|
+
const h = Math.max(48, Math.min(this.dockH, this.surfaceH - 60));
|
|
249
|
+
return { x: 0, y: Math.max(0, this.surfaceH - h), w: this.surfaceW, h };
|
|
209
250
|
}
|
|
210
251
|
shellHtml() {
|
|
211
|
-
return `<html><head><style>${DEVTOOLS_CSS}</style></head><body>
|
|
252
|
+
return `<html><head><style>${this.fontCss ?? ""}${DEVTOOLS_CSS}</style></head><body>
|
|
212
253
|
<div id="dt-root">
|
|
213
254
|
<div id="dt-grip" title="drag to resize"></div>
|
|
214
255
|
<div id="dt-chrome">
|
|
@@ -224,7 +265,10 @@ export class BlitzDevtoolsHost {
|
|
|
224
265
|
renderTabs() {
|
|
225
266
|
if (!this.panel)
|
|
226
267
|
return;
|
|
227
|
-
|
|
268
|
+
// esc() both interpolations — provider names feed panel ids/titles and
|
|
269
|
+
// arrive as raw strings (a game registering "x\"><img" would otherwise
|
|
270
|
+
// inject markup into the tab strip).
|
|
271
|
+
const tabs = [...this.panels.values()].map((p) => `<span class="tab${p.id === this.activeId ? " active" : ""}" data-action="dt.tab" data-tab="${esc(p.id)}">${esc(p.title)}</span>`).join("");
|
|
228
272
|
this.panel.setInnerHtml("#dt-tabs", `<span class="brand">downdraft</span>${tabs}<span class="spacer"></span>
|
|
229
273
|
<span class="hint">docked · F12</span><span id="dt-close" data-action="dt.close">✕</span>`);
|
|
230
274
|
this.tabsDirty = false;
|
|
@@ -235,13 +279,28 @@ export class BlitzDevtoolsHost {
|
|
|
235
279
|
this.activeId = id;
|
|
236
280
|
const p = this.panels.get(id);
|
|
237
281
|
this.renderTabs();
|
|
238
|
-
|
|
239
|
-
|
|
282
|
+
try {
|
|
283
|
+
this.panel.setInnerHtml("#dt-top", p.renderTop?.() ?? "");
|
|
284
|
+
this.panel.setInnerHtml("#dt-bottom", p.renderBottom?.() ?? "");
|
|
285
|
+
}
|
|
286
|
+
catch (err) {
|
|
287
|
+
log.warn("blitz-devtools-host", `panel ${id} chrome render error: ${err}`);
|
|
288
|
+
}
|
|
240
289
|
p.dirty = true;
|
|
241
290
|
p.shellDirty = false;
|
|
242
|
-
this.panel.setInnerHtml("#dt-body",
|
|
243
|
-
|
|
244
|
-
|
|
291
|
+
this.panel.setInnerHtml("#dt-body", this.safeBody(p));
|
|
292
|
+
try {
|
|
293
|
+
p.afterRender?.();
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
log.warn("blitz-devtools-host", `panel ${id} afterRender error: ${err}`);
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
p.activate?.();
|
|
300
|
+
}
|
|
301
|
+
catch (err) {
|
|
302
|
+
log.warn("blitz-devtools-host", `panel ${id} activate error: ${err}`);
|
|
303
|
+
}
|
|
245
304
|
// Only the snapshot tab being viewed gets periodic provider refreshes;
|
|
246
305
|
// fixed panels suspend auto-refresh entirely (manual Refresh still works).
|
|
247
306
|
this.backend.providerWatch = p.providerSlot ?? -1;
|
|
@@ -307,10 +366,32 @@ export class BlitzDevtoolsHost {
|
|
|
307
366
|
}
|
|
308
367
|
if (p.dirty) {
|
|
309
368
|
p.dirty = false;
|
|
310
|
-
|
|
311
|
-
|
|
369
|
+
let handled = false;
|
|
370
|
+
try {
|
|
371
|
+
handled = p.flushDom?.() === true;
|
|
312
372
|
}
|
|
313
|
-
|
|
373
|
+
catch (err) {
|
|
374
|
+
log.warn("blitz-devtools-host", `panel ${p.id} flushDom error: ${err}`);
|
|
375
|
+
}
|
|
376
|
+
if (!handled)
|
|
377
|
+
this.panel.setInnerHtml("#dt-body", this.safeBody(p));
|
|
378
|
+
try {
|
|
379
|
+
p.afterRender?.();
|
|
380
|
+
}
|
|
381
|
+
catch (err) {
|
|
382
|
+
log.warn("blitz-devtools-host", `panel ${p.id} afterRender error: ${err}`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
/** renderBody that can't take the dock down — the error surfaces in-panel
|
|
387
|
+
* (and the next push retries since dirty just flips back on). */
|
|
388
|
+
safeBody(p) {
|
|
389
|
+
try {
|
|
390
|
+
return p.renderBody();
|
|
391
|
+
}
|
|
392
|
+
catch (err) {
|
|
393
|
+
log.warn("blitz-devtools-host", `panel ${p.id} renderBody error: ${err}`);
|
|
394
|
+
return `<div class="status-banner error">panel render error: ${esc(String(err))}</div>`;
|
|
314
395
|
}
|
|
315
396
|
}
|
|
316
397
|
setStatus(text) {
|
|
@@ -345,8 +426,10 @@ export class BlitzDevtoolsHost {
|
|
|
345
426
|
return;
|
|
346
427
|
}
|
|
347
428
|
}
|
|
429
|
+
// Left-click only — routing actions on contextmenu meant a right-click
|
|
430
|
+
// on a tab/button fired it instead of just focusing.
|
|
348
431
|
const action = ev.d?.action;
|
|
349
|
-
if (
|
|
432
|
+
if (ev.t === "click" && action) {
|
|
350
433
|
if (action === "dt.tab") {
|
|
351
434
|
const id = ev.d.tab;
|
|
352
435
|
if (id)
|
|
@@ -357,11 +440,21 @@ export class BlitzDevtoolsHost {
|
|
|
357
440
|
this.hide();
|
|
358
441
|
return;
|
|
359
442
|
}
|
|
360
|
-
|
|
443
|
+
try {
|
|
444
|
+
this.panels.get(this.activeId)?.onAction?.(ev.d, ev);
|
|
445
|
+
}
|
|
446
|
+
catch (err) {
|
|
447
|
+
log.warn("blitz-devtools-host", `panel action error: ${err}`);
|
|
448
|
+
}
|
|
361
449
|
return;
|
|
362
450
|
}
|
|
363
451
|
// Everything else (input, keydown/up, scroll, focus…) → active panel.
|
|
364
|
-
|
|
452
|
+
try {
|
|
453
|
+
this.panels.get(this.activeId)?.onEvent?.(ev);
|
|
454
|
+
}
|
|
455
|
+
catch (err) {
|
|
456
|
+
log.warn("blitz-devtools-host", `panel event error: ${err}`);
|
|
457
|
+
}
|
|
365
458
|
}
|
|
366
459
|
}
|
|
367
460
|
/**
|
|
@@ -387,5 +480,6 @@ export function createSelfHostedDevtoolsUi(ctx) {
|
|
|
387
480
|
host,
|
|
388
481
|
mount: (markup, spec) => host.mount({ ...spec, html: markup }),
|
|
389
482
|
docCaps: () => host.docCaps,
|
|
483
|
+
registerResource: (url, bytes) => host.registerResource(url, bytes),
|
|
390
484
|
};
|
|
391
485
|
}
|
|
@@ -101,7 +101,7 @@ export class ConsolePanel {
|
|
|
101
101
|
onBackendEvent(event, data) {
|
|
102
102
|
if (event === "console") {
|
|
103
103
|
const e = data;
|
|
104
|
-
this.rows.push({ text: e
|
|
104
|
+
this.rows.push({ text: String(e?.text ?? ""), severity: e?.severity ?? 0, thread: e?.thread ?? "main", seq: this.seq++ });
|
|
105
105
|
if (this.rows.length > MAX_ROWS)
|
|
106
106
|
this.rows.splice(0, this.rows.length - MAX_ROWS);
|
|
107
107
|
this.dirty = true;
|
|
@@ -113,15 +113,24 @@ export class ConsolePanel {
|
|
|
113
113
|
}
|
|
114
114
|
else if (event === "threads") {
|
|
115
115
|
const list = data ?? [];
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
const next = [{ id: "main", name: "main", kind: 0 }, ...list.filter((t) => t.id !== "main")];
|
|
117
|
+
// Thread chips live in the bottom chrome — flag it so the row
|
|
118
|
+
// actually re-renders when a worker appears or disappears.
|
|
119
|
+
const changed = next.length !== this.threads.length
|
|
120
|
+
|| next.some((t, i) => t.id !== this.threads[i].id || t.name !== this.threads[i].name);
|
|
121
|
+
this.threads = next;
|
|
122
|
+
if (!this.threads.some((t) => t.id === this.replThread)) {
|
|
118
123
|
this.replThread = "main";
|
|
124
|
+
this.shellDirty = true;
|
|
125
|
+
}
|
|
126
|
+
if (changed)
|
|
127
|
+
this.shellDirty = true;
|
|
119
128
|
}
|
|
120
129
|
}
|
|
121
130
|
onAction(data, _ev) {
|
|
122
131
|
const act = data.action;
|
|
123
132
|
if (act === "console.clear") {
|
|
124
|
-
void this.ctx.call("console.clear");
|
|
133
|
+
void this.ctx.call("console.clear").catch(() => { });
|
|
125
134
|
}
|
|
126
135
|
else if (act === "console.sev") {
|
|
127
136
|
this.minSev = (this.minSev + 1) % SEV_NAMES.length;
|
|
@@ -171,8 +180,11 @@ export class ConsolePanel {
|
|
|
171
180
|
this.replValue = this.history[--this.histIdx];
|
|
172
181
|
this.ctx.setValue("[data-dt=repl]", this.replValue);
|
|
173
182
|
}
|
|
174
|
-
else if (ev.k === "ArrowDown" && this.histIdx < this.history.length
|
|
175
|
-
|
|
183
|
+
else if (ev.k === "ArrowDown" && this.histIdx >= 0 && this.histIdx < this.history.length) {
|
|
184
|
+
// Stepping past the newest entry restores the empty input —
|
|
185
|
+
// previously the last history item was unreachable-to-leave.
|
|
186
|
+
this.histIdx++;
|
|
187
|
+
this.replValue = this.histIdx < this.history.length ? this.history[this.histIdx] : "";
|
|
176
188
|
this.ctx.setValue("[data-dt=repl]", this.replValue);
|
|
177
189
|
}
|
|
178
190
|
}
|
|
@@ -205,7 +217,7 @@ export class ConsolePanel {
|
|
|
205
217
|
else {
|
|
206
218
|
const r = res?.result;
|
|
207
219
|
this.addRow({
|
|
208
|
-
text: typeof r === "string" ? r : JSON.stringify(r, null, 2),
|
|
220
|
+
text: typeof r === "string" ? r : (r === undefined ? "undefined" : JSON.stringify(r, null, 2)),
|
|
209
221
|
severity: 0, thread: this.replThread,
|
|
210
222
|
});
|
|
211
223
|
}
|