@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
|
@@ -57,7 +57,7 @@ export function writePadToInput(
|
|
|
57
57
|
writer.setGamepadButton(playerIdx, b, (d.buttonsLo & (1 << b)) !== 0);
|
|
58
58
|
}
|
|
59
59
|
if (bindings) {
|
|
60
|
-
for (const bind of bindings) {
|
|
60
|
+
for (const bind of bindings.values()) {
|
|
61
61
|
let active = false;
|
|
62
62
|
if (bind.button !== undefined) {
|
|
63
63
|
active = (d.buttonsLo & (1 << bind.button)) !== 0;
|
|
@@ -171,10 +171,10 @@ export class GamepadHub {
|
|
|
171
171
|
ffSupported: (s.flags & PAD_FLAG.FF_SUPPORTED) !== 0,
|
|
172
172
|
});
|
|
173
173
|
this.devices.set(slot, dev);
|
|
174
|
-
for (const cb of this.listeners.connect) cb(dev);
|
|
174
|
+
for (const cb of this.listeners.connect.values()) cb(dev);
|
|
175
175
|
} else if (!connected && existing) {
|
|
176
176
|
this.devices.delete(slot);
|
|
177
|
-
for (const cb of this.listeners.disconnect) cb(existing);
|
|
177
|
+
for (const cb of this.listeners.disconnect.values()) cb(existing);
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
180
|
this.lastSeq = this.reader.getSequence();
|
|
@@ -53,9 +53,9 @@ export class SabGamepadSource implements GamepadSource {
|
|
|
53
53
|
for (let i = 0; i < GAMEPAD_MAX_SLOTS; i++) {
|
|
54
54
|
if (!(changed & (1 << i))) continue;
|
|
55
55
|
if (mask & (1 << i)) {
|
|
56
|
-
for (const cb of this.connectCbs) cb(i);
|
|
56
|
+
for (const cb of this.connectCbs.values()) cb(i);
|
|
57
57
|
} else {
|
|
58
|
-
for (const cb of this.disconnectCbs) cb(i);
|
|
58
|
+
for (const cb of this.disconnectCbs.values()) cb(i);
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
this.knownMask = mask;
|
|
@@ -58,7 +58,7 @@ async function findBatteryLevel(macHint: string | null): Promise<number | undefi
|
|
|
58
58
|
try {
|
|
59
59
|
const psDir = "/sys/class/power_supply";
|
|
60
60
|
if (!existsSync(psDir)) return undefined;
|
|
61
|
-
for (const entry of await fs.readdir(psDir)) {
|
|
61
|
+
for (const entry of (await fs.readdir(psDir)).values()) {
|
|
62
62
|
const macPath = join(psDir, entry, "mac");
|
|
63
63
|
if (existsSync(macPath)) {
|
|
64
64
|
const mac = (await fs.readFile(macPath, "utf-8")).trim().toLowerCase();
|
|
@@ -98,7 +98,7 @@ export class SysfsGamepadEnricher {
|
|
|
98
98
|
async enrich(slots: GamepadDeviceSlot[]): Promise<EnrichResult[]> {
|
|
99
99
|
if (process.platform !== "linux") return [];
|
|
100
100
|
const out: EnrichResult[] = [];
|
|
101
|
-
for (const s of slots) {
|
|
101
|
+
for (const s of slots.values()) {
|
|
102
102
|
if (s.eventNode < 0) continue;
|
|
103
103
|
const r = await this.enrichSlot(s);
|
|
104
104
|
out.push(r);
|
|
@@ -177,6 +177,7 @@ export class GpuKernel {
|
|
|
177
177
|
const [, oy, oz] = this.outputSize;
|
|
178
178
|
this.workgroupSize = opts.workgroupSize ?? (oy === 1 && oz === 1 ? [64, 1, 1] : [8, 8, 1]);
|
|
179
179
|
const [wx, wy, wz] = this.workgroupSize;
|
|
180
|
+
this.label = opts.label ?? "gpu-kernel";
|
|
180
181
|
const lim = this.device?.limits as {
|
|
181
182
|
maxComputeWorkgroupSizeX?: number;
|
|
182
183
|
maxComputeWorkgroupSizeY?: number;
|
|
@@ -192,7 +193,6 @@ export class GpuKernel {
|
|
|
192
193
|
throw new Error(`kernel "${this.label}": workgroupSize ${wx}×${wy}×${wz} exceeds device limits`);
|
|
193
194
|
}
|
|
194
195
|
}
|
|
195
|
-
this.label = opts.label ?? "gpu-kernel";
|
|
196
196
|
this.memoryBudget = opts.memoryBudget;
|
|
197
197
|
this.extraUsage = opts.outputUsage ?? 0;
|
|
198
198
|
this.externalResult = opts.outputBuffer ?? null;
|
|
@@ -586,7 +586,7 @@ export class GpuKernel {
|
|
|
586
586
|
// Pre-validate against device limits — Adreno-class GPUs allow as few as
|
|
587
587
|
// 16 storage buffers/stage and 256MB/binding, and wgpu otherwise reports a
|
|
588
588
|
// generic "command buffer failed validation" at submit time.
|
|
589
|
-
const limits = this.device.limits as Record<string, number | undefined> | undefined;
|
|
589
|
+
const limits = this.device.limits as unknown as Record<string, number | undefined> | undefined;
|
|
590
590
|
const maxStorage = limits?.maxStorageBuffersPerShaderStage ?? Infinity;
|
|
591
591
|
const maxBinding = limits?.maxStorageBufferBindingSize ?? Infinity;
|
|
592
592
|
const storageCount = unit.transpiled.bindings.length
|
|
@@ -720,7 +720,7 @@ class Emitter {
|
|
|
720
720
|
if (e.ty === "vec") {
|
|
721
721
|
throw new KernelSyntaxError(`arrays can't live in locals — return them directly`, d.init);
|
|
722
722
|
}
|
|
723
|
-
const widen = this.widened.has(name) && e.ty !== "f32" && e.ty !== "bool"
|
|
723
|
+
const widen = this.widened.has(name) && e.ty !== "f32" && e.ty !== "bool";
|
|
724
724
|
this.declare(name, widen ? "f32" : e.ty);
|
|
725
725
|
if (node.kind === "const") this.consts.add(name);
|
|
726
726
|
const kw = node.kind === "const" ? "let" : "var";
|
|
@@ -209,7 +209,7 @@ export class NavController {
|
|
|
209
209
|
|
|
210
210
|
let best: NavNodeInfo | null = null;
|
|
211
211
|
let bestScore = Infinity;
|
|
212
|
-
for (const n of cands) {
|
|
212
|
+
for (const n of cands.values()) {
|
|
213
213
|
if (n === cur || !n.rect) continue;
|
|
214
214
|
const r = n.rect;
|
|
215
215
|
const nx = r.x + r.w / 2, ny = r.y + r.h / 2;
|
|
@@ -50,7 +50,7 @@ export class PadNavDriver {
|
|
|
50
50
|
repeatDelayMs: opts.repeatDelayMs ?? 400,
|
|
51
51
|
repeatIntervalMs: opts.repeatIntervalMs ?? 130,
|
|
52
52
|
};
|
|
53
|
-
for (const d of ["up", "down", "left", "right"] as const) {
|
|
53
|
+
for (const d of (["up", "down", "left", "right"] as const).values()) {
|
|
54
54
|
this.dirs.set(d, { held: false, since: 0, lastFire: 0 });
|
|
55
55
|
}
|
|
56
56
|
}
|
|
@@ -82,7 +82,7 @@ export class PadNavDriver {
|
|
|
82
82
|
this.prevDirAxis = stickDir;
|
|
83
83
|
if (stickDir) pressed.add(stickDir);
|
|
84
84
|
|
|
85
|
-
for (const [dir, st] of this.dirs) {
|
|
85
|
+
for (const [dir, st] of this.dirs.entries()) {
|
|
86
86
|
const isDown = pressed.has(dir);
|
|
87
87
|
if (isDown && !st.held) {
|
|
88
88
|
st.held = true; st.since = now; st.lastFire = now;
|
|
@@ -206,19 +206,19 @@ export class LanDiscovery {
|
|
|
206
206
|
const existing = this.peers.get(pkt.id);
|
|
207
207
|
const peer: LanPeer = { ...pkt, host: rinfo.address, lastSeenMs: this.opts.now() };
|
|
208
208
|
this.peers.set(pkt.id, peer);
|
|
209
|
-
if (!existing) for (const f of this.listeners.join) f(peer);
|
|
209
|
+
if (!existing) for (const f of this.listeners.join.values()) f(peer);
|
|
210
210
|
else if (existing.name !== peer.name || existing.port !== peer.port
|
|
211
211
|
|| JSON.stringify(existing.meta) !== JSON.stringify(peer.meta)) {
|
|
212
|
-
for (const f of this.listeners.update) f(peer);
|
|
212
|
+
for (const f of this.listeners.update.values()) f(peer);
|
|
213
213
|
}
|
|
214
214
|
}
|
|
215
215
|
|
|
216
216
|
private expire() {
|
|
217
217
|
const cutoff = this.opts.now() - this.opts.ttlMs;
|
|
218
|
-
for (const [id, p] of this.peers) {
|
|
218
|
+
for (const [id, p] of this.peers.entries()) {
|
|
219
219
|
if (p.lastSeenMs < cutoff) {
|
|
220
220
|
this.peers.delete(id);
|
|
221
|
-
for (const f of this.listeners.leave) f(p);
|
|
221
|
+
for (const f of this.listeners.leave.values()) f(p);
|
|
222
222
|
}
|
|
223
223
|
}
|
|
224
224
|
}
|
|
@@ -292,7 +292,7 @@ export class MdnsBrowser {
|
|
|
292
292
|
}
|
|
293
293
|
|
|
294
294
|
private requery() {
|
|
295
|
-
for (const t of this.watched) this.sendQuery(t);
|
|
295
|
+
for (const t of this.watched.values()) this.sendQuery(t);
|
|
296
296
|
this.expire();
|
|
297
297
|
}
|
|
298
298
|
|
|
@@ -320,7 +320,7 @@ export class MdnsBrowser {
|
|
|
320
320
|
|
|
321
321
|
private expire() {
|
|
322
322
|
const now = this.opts.now();
|
|
323
|
-
for (const [k, s] of this.table) {
|
|
323
|
+
for (const [k, s] of this.table.entries()) {
|
|
324
324
|
if (s.lastSeenMs + s.ttlMs < now) this.table.delete(k);
|
|
325
325
|
}
|
|
326
326
|
}
|
|
@@ -334,7 +334,7 @@ export class MdnsBrowser {
|
|
|
334
334
|
.filter((r): r is Extract<typeof r, { data: unknown }> => "data" in r);
|
|
335
335
|
const now = this.opts.now();
|
|
336
336
|
// PTR gives instance names; SRV/TXT/A for the same instance fill detail.
|
|
337
|
-
for (const r of recs) {
|
|
337
|
+
for (const r of recs.values()) {
|
|
338
338
|
if (r.type !== "PTR" || !this.watched.has(r.name)) continue;
|
|
339
339
|
const instance = r.data as string;
|
|
340
340
|
const key = `${instance}@${r.name}`;
|
|
@@ -350,7 +350,7 @@ export class MdnsBrowser {
|
|
|
350
350
|
);
|
|
351
351
|
const txtMap: Record<string, string> = {};
|
|
352
352
|
if (txt) {
|
|
353
|
-
for (const item of (txt.data as Buffer[])) {
|
|
353
|
+
for (const item of (txt.data as Buffer[]).values()) {
|
|
354
354
|
const s = item.toString("utf8");
|
|
355
355
|
const eq = s.indexOf("=");
|
|
356
356
|
txtMap[eq < 0 ? s : s.slice(0, eq)] = eq < 0 ? "" : s.slice(eq + 1);
|
|
@@ -205,7 +205,7 @@ export class FileSaveStore implements ISaveStore {
|
|
|
205
205
|
if (opts?.blobs && Object.keys(opts.blobs).length > 0) {
|
|
206
206
|
await fs.mkdir(tmpBlobsDir, { recursive: true });
|
|
207
207
|
for (let _i = 0, _it = Object.entries(opts.blobs) as Array<[string, ArrayBuffer]>, _n = _it.length; _i < _n; _i++) { const [key, buf] = _it[_i];
|
|
208
|
-
const safeKey = key.replace(/[^a-zA-Z0-9_
|
|
208
|
+
const safeKey = key.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
209
209
|
await fs.writeFile(join(tmpBlobsDir, safeKey), new Uint8Array(buf));
|
|
210
210
|
blobBytes += buf.byteLength;
|
|
211
211
|
}
|
|
@@ -524,22 +524,22 @@ export class FileSaveStore implements ISaveStore {
|
|
|
524
524
|
|
|
525
525
|
private slotPath(slot: string): string {
|
|
526
526
|
// Sanitize slot name to prevent path traversal
|
|
527
|
-
const safe = slot.replace(/[^a-zA-Z0-9_
|
|
527
|
+
const safe = slot.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
528
528
|
return join(this.saveDir, safe + SAVE_EXT);
|
|
529
529
|
}
|
|
530
530
|
|
|
531
531
|
private blobsDirPath(slot: string): string {
|
|
532
|
-
const safe = slot.replace(/[^a-zA-Z0-9_
|
|
532
|
+
const safe = slot.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
533
533
|
return join(this.saveDir, safe + BLOBS_DIR_SUFFIX);
|
|
534
534
|
}
|
|
535
535
|
|
|
536
536
|
private thumbPath(slot: string): string {
|
|
537
|
-
const safe = slot.replace(/[^a-zA-Z0-9_
|
|
537
|
+
const safe = slot.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
538
538
|
return join(this.saveDir, safe + THUMB_EXT);
|
|
539
539
|
}
|
|
540
540
|
|
|
541
541
|
private propsPath(slot: string): string {
|
|
542
|
-
const safe = slot.replace(/[^a-zA-Z0-9_
|
|
542
|
+
const safe = slot.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
543
543
|
return join(this.saveDir, safe + PROPS_EXT);
|
|
544
544
|
}
|
|
545
545
|
|
|
@@ -262,7 +262,7 @@ export class OpfsSaveStore implements ISaveStore {
|
|
|
262
262
|
// ── Path helpers ────────────────────────────────────────────────────────
|
|
263
263
|
|
|
264
264
|
private sanitizeSlot(slot: string): string {
|
|
265
|
-
return slot.replace(/[^a-zA-Z0-9_
|
|
265
|
+
return slot.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
266
266
|
}
|
|
267
267
|
|
|
268
268
|
private genDirName(gen: number): string {
|
|
@@ -371,7 +371,7 @@ export class OpfsSaveStore implements ISaveStore {
|
|
|
371
371
|
if (opts?.blobs && Object.keys(opts.blobs).length > 0) {
|
|
372
372
|
const blobsDir = await this.getBlobsDir(genDir, true);
|
|
373
373
|
for (const [key, buf] of Object.entries(opts.blobs)) {
|
|
374
|
-
const safeKey = key.replace(/[^a-zA-Z0-9_
|
|
374
|
+
const safeKey = key.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
375
375
|
await this.writeBinaryFile(blobsDir, safeKey, new Uint8Array(buf));
|
|
376
376
|
blobCount++;
|
|
377
377
|
}
|
|
@@ -138,7 +138,7 @@ export function createControllerUi(): RendererModule {
|
|
|
138
138
|
const dev = selected();
|
|
139
139
|
if (!p || !dev) return;
|
|
140
140
|
const ops = [];
|
|
141
|
-
for (const [bit] of BTN_LABELS) {
|
|
141
|
+
for (const [bit] of BTN_LABELS.values()) {
|
|
142
142
|
ops.push({ op: "attr", sel: `#ctl-btn-${bit}`, name: "data-on",
|
|
143
143
|
value: dev.pressed(bit) ? "true" : "false" } as const);
|
|
144
144
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
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
|
+
|
|
14
|
+
import { execFileSync } from "node:child_process";
|
|
15
|
+
import { readFileSync } from "node:fs";
|
|
16
|
+
|
|
17
|
+
/** Resource sink — HtmlUiHost.registerResource, or anything shaped like it. */
|
|
18
|
+
type ResourceSink = { registerResource(url: string, bytes: Uint8Array | ArrayBuffer): void };
|
|
19
|
+
|
|
20
|
+
interface Face { family: string; pattern: string; urlName: string; weight: number }
|
|
21
|
+
|
|
22
|
+
// Blitz-dom only fetches a @font-face source whose format is detectable —
|
|
23
|
+
// either an explicit format() hint or a file extension on the url — so the
|
|
24
|
+
// urls carry the resolved file's real extension.
|
|
25
|
+
const FACES: Face[] = [
|
|
26
|
+
{ family: "DevTools Sans", pattern: "sans-serif", urlName: "sans", weight: 400 },
|
|
27
|
+
{ family: "DevTools Sans", pattern: "sans-serif:weight=bold", urlName: "sans-bold", weight: 700 },
|
|
28
|
+
{ family: "DevTools Mono", pattern: "monospace", urlName: "mono", weight: 400 },
|
|
29
|
+
{ family: "DevTools Mono", pattern: "monospace:weight=bold", urlName: "mono-bold", weight: 700 },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
/** `fc-match -f %{file}` for a fontconfig pattern; null when fc-match is
|
|
33
|
+
* absent or the match isn't a font file Blitz can load. */
|
|
34
|
+
function fcFontFile(pattern: string): string | null {
|
|
35
|
+
try {
|
|
36
|
+
const p = execFileSync("fc-match", ["-f", "%{file}", pattern], { timeout: 5000 }).toString().trim();
|
|
37
|
+
return /\.(ttf|otf|woff2?)$/i.test(p) ? p : null;
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function fontExt(path: string): string {
|
|
44
|
+
return path.match(/\.(ttf|otf|woff2?)$/i)?.[0] ?? ".ttf";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Register system sans + mono faces as `ui://` resources and return the
|
|
49
|
+
* @font-face CSS to prepend to the dock stylesheet. Returns "" when nothing
|
|
50
|
+
* registered — the stylesheet's named families then fail to resolve and the
|
|
51
|
+
* generic fallbacks (embedded DejaVu) apply.
|
|
52
|
+
*
|
|
53
|
+
* Call before mount — the doc backend applies resources synchronously and in
|
|
54
|
+
* order, so the first doc resolve sees them.
|
|
55
|
+
*/
|
|
56
|
+
export function registerDevtoolsFonts(sink: ResourceSink): string {
|
|
57
|
+
try {
|
|
58
|
+
// Both weights of a family are required: a 700 rule pointing at the
|
|
59
|
+
// regular file keeps bold text in-family instead of falling back.
|
|
60
|
+
const files = new Map<string, string>();
|
|
61
|
+
for (const f of FACES) files.set(f.urlName, fcFontFile(f.pattern) ?? "");
|
|
62
|
+
if (!files.get("sans")) return "";
|
|
63
|
+
if (!files.get("mono")) files.set("mono", files.get("sans")!);
|
|
64
|
+
if (!files.get("sans-bold")) files.set("sans-bold", files.get("sans")!);
|
|
65
|
+
if (!files.get("mono-bold")) files.set("mono-bold", files.get("mono")!);
|
|
66
|
+
let css = "";
|
|
67
|
+
for (const f of FACES) {
|
|
68
|
+
const file = files.get(f.urlName)!;
|
|
69
|
+
const url = `ui://devtools-fonts/${f.urlName}${fontExt(file)}`;
|
|
70
|
+
sink.registerResource(url, readFileSync(file));
|
|
71
|
+
css += `@font-face { font-family:'${f.family}'; font-weight:${f.weight}; src:url('${url}'); }\n`;
|
|
72
|
+
}
|
|
73
|
+
return css;
|
|
74
|
+
} catch {
|
|
75
|
+
// fc-match absent / unreadable font file — the embedded fallback applies.
|
|
76
|
+
return "";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -16,12 +16,13 @@ import { createLogger } from "@downdraft/engine";
|
|
|
16
16
|
import { CdpBridge, DevtoolsBackend } from "@downdraft/engine/libraries/devtools";
|
|
17
17
|
import { HtmlUiHost, type OsrDomEvent, type UiPanelHandle } from "@downdraft/engine/modules/html-ui";
|
|
18
18
|
|
|
19
|
+
import { registerDevtoolsFonts } from "./fonts";
|
|
19
20
|
import { ConsolePanel } from "./panels/console";
|
|
20
21
|
import { ElementsPanel } from "./panels/elements";
|
|
21
22
|
import { GpuPanel } from "./panels/gpu";
|
|
22
23
|
import { PerformancePanel } from "./panels/performance";
|
|
23
24
|
import { SnapshotPanel } from "./panels/snapshot";
|
|
24
|
-
import type
|
|
25
|
+
import { esc, type DtPanel, type DtPanelCtx } from "./panels/types";
|
|
25
26
|
import { DEVTOOLS_CSS } from "./theme";
|
|
26
27
|
|
|
27
28
|
const log = createLogger("info");
|
|
@@ -35,12 +36,17 @@ export interface DevtoolsUiSurface {
|
|
|
35
36
|
z?: number;
|
|
36
37
|
scale?: number;
|
|
37
38
|
maxFps?: number;
|
|
39
|
+
/** Ease wheel deltas at pump cadence (html-ui PanelSpec.smoothWheel). */
|
|
40
|
+
smoothWheel?: boolean;
|
|
38
41
|
interactive?: boolean;
|
|
39
42
|
onEvent?: (ev: OsrDomEvent) => void;
|
|
40
43
|
}): UiPanelHandle;
|
|
41
44
|
/** Doc-backend capabilities — panels use incremental DOM ops only when
|
|
42
45
|
* the underlying cdylib provides them. */
|
|
43
46
|
docCaps?(): { incrementalDom?: boolean };
|
|
47
|
+
/** Register a `ui://` resource (fonts) served to the dock doc — must run
|
|
48
|
+
* before mount. Absent on surfaces that don't own a resource table. */
|
|
49
|
+
registerResource?(url: string, bytes: Uint8Array | ArrayBuffer): void;
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
export interface BlitzDevtoolsOptions {
|
|
@@ -83,6 +89,9 @@ export class BlitzDevtoolsHost {
|
|
|
83
89
|
private panelCtx: DtPanelCtx;
|
|
84
90
|
private lastPanelRender = -1e9;
|
|
85
91
|
private lastSlowLog = 0;
|
|
92
|
+
/** @font-face css from registerDevtoolsFonts — "" means "embedded font
|
|
93
|
+
* fallback", null means "not yet attempted". */
|
|
94
|
+
private fontCss: string | null = null;
|
|
86
95
|
|
|
87
96
|
constructor(opts: BlitzDevtoolsOptions) {
|
|
88
97
|
this.opts = opts;
|
|
@@ -119,7 +128,7 @@ export class BlitzDevtoolsHost {
|
|
|
119
128
|
new ElementsPanel(ctx),
|
|
120
129
|
new PerformancePanel(ctx),
|
|
121
130
|
new GpuPanel(ctx),
|
|
122
|
-
]) this.panels.set(p.id, p);
|
|
131
|
+
].values()) this.panels.set(p.id, p);
|
|
123
132
|
}
|
|
124
133
|
|
|
125
134
|
get visible(): boolean { return this.panel !== null; }
|
|
@@ -154,19 +163,41 @@ export class BlitzDevtoolsHost {
|
|
|
154
163
|
|
|
155
164
|
show(): void {
|
|
156
165
|
if (!this._ready || this.visible || this.disposed) return;
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
166
|
+
// Register real system fonts once per host — ui:// resources persist on
|
|
167
|
+
// the surface across dock remounts, and must land before the doc mounts
|
|
168
|
+
// so the first resolve sees them (embedded DejaVu fallback otherwise).
|
|
169
|
+
if (this.fontCss === null) {
|
|
170
|
+
const ui = this.opts.ui as DevtoolsUiSurface & { host?: HtmlUiHost };
|
|
171
|
+
const rr = ui.registerResource?.bind(ui)
|
|
172
|
+
?? (ui.host ? ui.host.registerResource.bind(ui.host) : undefined);
|
|
173
|
+
this.fontCss = rr ? registerDevtoolsFonts({ registerResource: rr }) : "";
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
this.panel = this.opts.ui.mount(this.shellHtml(), {
|
|
177
|
+
id: "devtools",
|
|
178
|
+
rect: this.dockRect(),
|
|
179
|
+
// Above game overlays — menus/loading screens shouldn't bury the dock.
|
|
180
|
+
z: 1000,
|
|
181
|
+
// Native-res raster: the vendored glifo/FreeType path grid-fits hinted
|
|
182
|
+
// glyph stems to whole pixels — supersample factors >1 just blur that
|
|
183
|
+
// back out on downscale (and cost 2.25x the texture memory).
|
|
184
|
+
scale: 1,
|
|
185
|
+
// Ease wheel detents at pump cadence instead of jumping per event.
|
|
186
|
+
smoothWheel: true,
|
|
187
|
+
interactive: true,
|
|
188
|
+
// Devtools UI doesn't need vsync-rate raster — cap it so mutation
|
|
189
|
+
// bursts (console streams, snapshot refreshes) coalesce into ~66ms
|
|
190
|
+
// rasters instead of one raster per message.
|
|
191
|
+
maxFps: 15,
|
|
192
|
+
onEvent: (ev) => this.handleDocEvent(ev),
|
|
193
|
+
});
|
|
194
|
+
} catch (err) {
|
|
195
|
+
// Mount fails when the ui worker is dead — stay hidden so the next F12
|
|
196
|
+
// retries cleanly instead of wedging on a half-mounted doc.
|
|
197
|
+
this.panel = null;
|
|
198
|
+
log.error("blitz-devtools-host", `dock mount failed: ${err}`);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
170
201
|
this.syncProviders();
|
|
171
202
|
this.renderTabs();
|
|
172
203
|
this.activate(this.activeId);
|
|
@@ -190,8 +221,18 @@ export class BlitzDevtoolsHost {
|
|
|
190
221
|
}
|
|
191
222
|
const t1 = performance.now();
|
|
192
223
|
if (!this.visible) return;
|
|
193
|
-
|
|
194
|
-
this.
|
|
224
|
+
// Guarded like backend.update() above — a throwing panel render can't be
|
|
225
|
+
// allowed to kill the game frame hook that drives this pump.
|
|
226
|
+
try {
|
|
227
|
+
this.syncProviders();
|
|
228
|
+
this.flush();
|
|
229
|
+
} catch (err) {
|
|
230
|
+
const tn = performance.now();
|
|
231
|
+
if (tn - this.lastSlowLog > 1000) {
|
|
232
|
+
this.lastSlowLog = tn;
|
|
233
|
+
log.warn("blitz-devtools-host", `panel render error: ${err}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
195
236
|
const t2 = performance.now();
|
|
196
237
|
// ≤1/sec — these diagnostics used to feed the console panel's logger
|
|
197
238
|
// sink, and each row forced another dock re-render: a self-feeding
|
|
@@ -244,12 +285,14 @@ export class BlitzDevtoolsHost {
|
|
|
244
285
|
// ── Dock layout ──
|
|
245
286
|
|
|
246
287
|
private dockRect() {
|
|
247
|
-
|
|
248
|
-
|
|
288
|
+
// A tiny surface (or a pathological resize event) must not produce a
|
|
289
|
+
// negative height — keep at least enough room for the tab strip.
|
|
290
|
+
const h = Math.max(48, Math.min(this.dockH, this.surfaceH - 60));
|
|
291
|
+
return { x: 0, y: Math.max(0, this.surfaceH - h), w: this.surfaceW, h };
|
|
249
292
|
}
|
|
250
293
|
|
|
251
294
|
private shellHtml(): string {
|
|
252
|
-
return `<html><head><style>${DEVTOOLS_CSS}</style></head><body>
|
|
295
|
+
return `<html><head><style>${this.fontCss ?? ""}${DEVTOOLS_CSS}</style></head><body>
|
|
253
296
|
<div id="dt-root">
|
|
254
297
|
<div id="dt-grip" title="drag to resize"></div>
|
|
255
298
|
<div id="dt-chrome">
|
|
@@ -266,8 +309,11 @@ export class BlitzDevtoolsHost {
|
|
|
266
309
|
|
|
267
310
|
private renderTabs(): void {
|
|
268
311
|
if (!this.panel) return;
|
|
312
|
+
// esc() both interpolations — provider names feed panel ids/titles and
|
|
313
|
+
// arrive as raw strings (a game registering "x\"><img" would otherwise
|
|
314
|
+
// inject markup into the tab strip).
|
|
269
315
|
const tabs = [...this.panels.values()].map((p) =>
|
|
270
|
-
`<span class="tab${p.id === this.activeId ? " active" : ""}" data-action="dt.tab" data-tab="${p.id}">${p.title}</span>`
|
|
316
|
+
`<span class="tab${p.id === this.activeId ? " active" : ""}" data-action="dt.tab" data-tab="${esc(p.id)}">${esc(p.title)}</span>`
|
|
271
317
|
).join("");
|
|
272
318
|
this.panel.setInnerHtml("#dt-tabs",
|
|
273
319
|
`<span class="brand">downdraft</span>${tabs}<span class="spacer"></span>
|
|
@@ -280,13 +326,21 @@ export class BlitzDevtoolsHost {
|
|
|
280
326
|
this.activeId = id;
|
|
281
327
|
const p = this.panels.get(id)!;
|
|
282
328
|
this.renderTabs();
|
|
283
|
-
|
|
284
|
-
|
|
329
|
+
try {
|
|
330
|
+
this.panel.setInnerHtml("#dt-top", p.renderTop?.() ?? "");
|
|
331
|
+
this.panel.setInnerHtml("#dt-bottom", p.renderBottom?.() ?? "");
|
|
332
|
+
} catch (err) {
|
|
333
|
+
log.warn("blitz-devtools-host", `panel ${id} chrome render error: ${err}`);
|
|
334
|
+
}
|
|
285
335
|
p.dirty = true;
|
|
286
336
|
p.shellDirty = false;
|
|
287
|
-
this.panel.setInnerHtml("#dt-body",
|
|
288
|
-
p.afterRender?.();
|
|
289
|
-
|
|
337
|
+
this.panel.setInnerHtml("#dt-body", this.safeBody(p));
|
|
338
|
+
try { p.afterRender?.(); } catch (err) {
|
|
339
|
+
log.warn("blitz-devtools-host", `panel ${id} afterRender error: ${err}`);
|
|
340
|
+
}
|
|
341
|
+
try { p.activate?.(); } catch (err) {
|
|
342
|
+
log.warn("blitz-devtools-host", `panel ${id} activate error: ${err}`);
|
|
343
|
+
}
|
|
290
344
|
// Only the snapshot tab being viewed gets periodic provider refreshes;
|
|
291
345
|
// fixed panels suspend auto-refresh entirely (manual Refresh still works).
|
|
292
346
|
this.backend.providerWatch = p.providerSlot ?? -1;
|
|
@@ -298,7 +352,7 @@ export class BlitzDevtoolsHost {
|
|
|
298
352
|
const slots = this.backend.providerSlots();
|
|
299
353
|
let changed = false;
|
|
300
354
|
const seen = new Set<number>();
|
|
301
|
-
for (const { slot, name } of slots) {
|
|
355
|
+
for (const { slot, name } of slots.values()) {
|
|
302
356
|
seen.add(slot);
|
|
303
357
|
if (!this.snapshotPanels.has(slot)) {
|
|
304
358
|
const p = new SnapshotPanel(this.panelCtx, slot, name);
|
|
@@ -307,7 +361,7 @@ export class BlitzDevtoolsHost {
|
|
|
307
361
|
changed = true;
|
|
308
362
|
}
|
|
309
363
|
}
|
|
310
|
-
for (const [slot, p] of this.snapshotPanels) {
|
|
364
|
+
for (const [slot, p] of this.snapshotPanels.entries()) {
|
|
311
365
|
if (!seen.has(slot)) {
|
|
312
366
|
this.snapshotPanels.delete(slot);
|
|
313
367
|
this.panels.delete(p.id);
|
|
@@ -346,10 +400,25 @@ export class BlitzDevtoolsHost {
|
|
|
346
400
|
}
|
|
347
401
|
if (p.dirty) {
|
|
348
402
|
p.dirty = false;
|
|
349
|
-
|
|
350
|
-
|
|
403
|
+
let handled = false;
|
|
404
|
+
try { handled = p.flushDom?.() === true; } catch (err) {
|
|
405
|
+
log.warn("blitz-devtools-host", `panel ${p.id} flushDom error: ${err}`);
|
|
406
|
+
}
|
|
407
|
+
if (!handled) this.panel.setInnerHtml("#dt-body", this.safeBody(p));
|
|
408
|
+
try { p.afterRender?.(); } catch (err) {
|
|
409
|
+
log.warn("blitz-devtools-host", `panel ${p.id} afterRender error: ${err}`);
|
|
351
410
|
}
|
|
352
|
-
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** renderBody that can't take the dock down — the error surfaces in-panel
|
|
415
|
+
* (and the next push retries since dirty just flips back on). */
|
|
416
|
+
private safeBody(p: DtPanel): string {
|
|
417
|
+
try {
|
|
418
|
+
return p.renderBody();
|
|
419
|
+
} catch (err) {
|
|
420
|
+
log.warn("blitz-devtools-host", `panel ${p.id} renderBody error: ${err}`);
|
|
421
|
+
return `<div class="status-banner error">panel render error: ${esc(String(err))}</div>`;
|
|
353
422
|
}
|
|
354
423
|
}
|
|
355
424
|
|
|
@@ -387,20 +456,26 @@ export class BlitzDevtoolsHost {
|
|
|
387
456
|
}
|
|
388
457
|
}
|
|
389
458
|
|
|
459
|
+
// Left-click only — routing actions on contextmenu meant a right-click
|
|
460
|
+
// on a tab/button fired it instead of just focusing.
|
|
390
461
|
const action = ev.d?.action;
|
|
391
|
-
if (
|
|
462
|
+
if (ev.t === "click" && action) {
|
|
392
463
|
if (action === "dt.tab") {
|
|
393
464
|
const id = ev.d!.tab;
|
|
394
465
|
if (id) this.activate(id);
|
|
395
466
|
return;
|
|
396
467
|
}
|
|
397
468
|
if (action === "dt.close") { this.hide(); return; }
|
|
398
|
-
this.panels.get(this.activeId)?.onAction?.(ev.d!, ev);
|
|
469
|
+
try { this.panels.get(this.activeId)?.onAction?.(ev.d!, ev); } catch (err) {
|
|
470
|
+
log.warn("blitz-devtools-host", `panel action error: ${err}`);
|
|
471
|
+
}
|
|
399
472
|
return;
|
|
400
473
|
}
|
|
401
474
|
|
|
402
475
|
// Everything else (input, keydown/up, scroll, focus…) → active panel.
|
|
403
|
-
this.panels.get(this.activeId)?.onEvent?.(ev);
|
|
476
|
+
try { this.panels.get(this.activeId)?.onEvent?.(ev); } catch (err) {
|
|
477
|
+
log.warn("blitz-devtools-host", `panel event error: ${err}`);
|
|
478
|
+
}
|
|
404
479
|
}
|
|
405
480
|
}
|
|
406
481
|
|
|
@@ -426,5 +501,6 @@ export function createSelfHostedDevtoolsUi(ctx: RendererModuleContext): Devtools
|
|
|
426
501
|
host,
|
|
427
502
|
mount: (markup, spec) => host.mount({ ...spec, html: markup }),
|
|
428
503
|
docCaps: () => host.docCaps,
|
|
504
|
+
registerResource: (url, bytes) => host.registerResource(url, bytes),
|
|
429
505
|
};
|
|
430
506
|
}
|