@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
|
@@ -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));
|
|
@@ -550,7 +553,7 @@ export function createSimWorker(opts) {
|
|
|
550
553
|
}
|
|
551
554
|
: {}),
|
|
552
555
|
};
|
|
553
|
-
const api = { ...standardApi, ...
|
|
556
|
+
const api = { ...standardApi, ...opts.extraApi };
|
|
554
557
|
// Optionally wrap the API before exposing (e.g. with exposeDevToolsApi
|
|
555
558
|
// to add devtools + profiling RPC methods).
|
|
556
559
|
expose(opts.wrapExpose ? opts.wrapExpose(api) : api);
|
|
@@ -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
|
|
@@ -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
|
}
|
|
@@ -29,7 +29,7 @@ export function writePadToInput(writer, reader, playerIdx, deviceSlot, bindings,
|
|
|
29
29
|
writer.setGamepadButton(playerIdx, b, (d.buttonsLo & (1 << b)) !== 0);
|
|
30
30
|
}
|
|
31
31
|
if (bindings) {
|
|
32
|
-
for (const bind of bindings) {
|
|
32
|
+
for (const bind of bindings.values()) {
|
|
33
33
|
let active = false;
|
|
34
34
|
if (bind.button !== undefined) {
|
|
35
35
|
active = (d.buttonsLo & (1 << bind.button)) !== 0;
|
|
@@ -131,12 +131,12 @@ export class GamepadHub {
|
|
|
131
131
|
ffSupported: (s.flags & PAD_FLAG.FF_SUPPORTED) !== 0,
|
|
132
132
|
});
|
|
133
133
|
this.devices.set(slot, dev);
|
|
134
|
-
for (const cb of this.listeners.connect)
|
|
134
|
+
for (const cb of this.listeners.connect.values())
|
|
135
135
|
cb(dev);
|
|
136
136
|
}
|
|
137
137
|
else if (!connected && existing) {
|
|
138
138
|
this.devices.delete(slot);
|
|
139
|
-
for (const cb of this.listeners.disconnect)
|
|
139
|
+
for (const cb of this.listeners.disconnect.values())
|
|
140
140
|
cb(existing);
|
|
141
141
|
}
|
|
142
142
|
}
|
|
@@ -44,11 +44,11 @@ export class SabGamepadSource {
|
|
|
44
44
|
if (!(changed & (1 << i)))
|
|
45
45
|
continue;
|
|
46
46
|
if (mask & (1 << i)) {
|
|
47
|
-
for (const cb of this.connectCbs)
|
|
47
|
+
for (const cb of this.connectCbs.values())
|
|
48
48
|
cb(i);
|
|
49
49
|
}
|
|
50
50
|
else {
|
|
51
|
-
for (const cb of this.disconnectCbs)
|
|
51
|
+
for (const cb of this.disconnectCbs.values())
|
|
52
52
|
cb(i);
|
|
53
53
|
}
|
|
54
54
|
}
|
|
@@ -54,7 +54,7 @@ async function findBatteryLevel(macHint) {
|
|
|
54
54
|
const psDir = "/sys/class/power_supply";
|
|
55
55
|
if (!existsSync(psDir))
|
|
56
56
|
return undefined;
|
|
57
|
-
for (const entry of await fs.readdir(psDir)) {
|
|
57
|
+
for (const entry of (await fs.readdir(psDir)).values()) {
|
|
58
58
|
const macPath = join(psDir, entry, "mac");
|
|
59
59
|
if (existsSync(macPath)) {
|
|
60
60
|
const mac = (await fs.readFile(macPath, "utf-8")).trim().toLowerCase();
|
|
@@ -84,7 +84,7 @@ export class SysfsGamepadEnricher {
|
|
|
84
84
|
if (process.platform !== "linux")
|
|
85
85
|
return [];
|
|
86
86
|
const out = [];
|
|
87
|
-
for (const s of slots) {
|
|
87
|
+
for (const s of slots.values()) {
|
|
88
88
|
if (s.eventNode < 0)
|
|
89
89
|
continue;
|
|
90
90
|
const r = await this.enrichSlot(s);
|
|
@@ -100,6 +100,7 @@ export class GpuKernel {
|
|
|
100
100
|
const [, oy, oz] = this.outputSize;
|
|
101
101
|
this.workgroupSize = opts.workgroupSize ?? (oy === 1 && oz === 1 ? [64, 1, 1] : [8, 8, 1]);
|
|
102
102
|
const [wx, wy, wz] = this.workgroupSize;
|
|
103
|
+
this.label = opts.label ?? "gpu-kernel";
|
|
103
104
|
const lim = this.device?.limits;
|
|
104
105
|
if (lim) {
|
|
105
106
|
const perDim = wx > (lim.maxComputeWorkgroupSizeX ?? wx)
|
|
@@ -110,7 +111,6 @@ export class GpuKernel {
|
|
|
110
111
|
throw new Error(`kernel "${this.label}": workgroupSize ${wx}×${wy}×${wz} exceeds device limits`);
|
|
111
112
|
}
|
|
112
113
|
}
|
|
113
|
-
this.label = opts.label ?? "gpu-kernel";
|
|
114
114
|
this.memoryBudget = opts.memoryBudget;
|
|
115
115
|
this.extraUsage = opts.outputUsage ?? 0;
|
|
116
116
|
this.externalResult = opts.outputBuffer ?? null;
|
|
@@ -659,7 +659,7 @@ class Emitter {
|
|
|
659
659
|
if (e.ty === "vec") {
|
|
660
660
|
throw new KernelSyntaxError(`arrays can't live in locals — return them directly`, d.init);
|
|
661
661
|
}
|
|
662
|
-
const widen = this.widened.has(name) && e.ty !== "f32" && e.ty !== "bool"
|
|
662
|
+
const widen = this.widened.has(name) && e.ty !== "f32" && e.ty !== "bool";
|
|
663
663
|
this.declare(name, widen ? "f32" : e.ty);
|
|
664
664
|
if (node.kind === "const")
|
|
665
665
|
this.consts.add(name);
|
|
@@ -181,7 +181,7 @@ export class NavController {
|
|
|
181
181
|
const sign = dir === "down" || dir === "right" ? 1 : -1;
|
|
182
182
|
let best = null;
|
|
183
183
|
let bestScore = Infinity;
|
|
184
|
-
for (const n of cands) {
|
|
184
|
+
for (const n of cands.values()) {
|
|
185
185
|
if (n === cur || !n.rect)
|
|
186
186
|
continue;
|
|
187
187
|
const r = n.rect;
|
|
@@ -29,7 +29,7 @@ export class PadNavDriver {
|
|
|
29
29
|
repeatDelayMs: opts.repeatDelayMs ?? 400,
|
|
30
30
|
repeatIntervalMs: opts.repeatIntervalMs ?? 130,
|
|
31
31
|
};
|
|
32
|
-
for (const d of ["up", "down", "left", "right"]) {
|
|
32
|
+
for (const d of ["up", "down", "left", "right"].values()) {
|
|
33
33
|
this.dirs.set(d, { held: false, since: 0, lastFire: 0 });
|
|
34
34
|
}
|
|
35
35
|
}
|
|
@@ -67,7 +67,7 @@ export class PadNavDriver {
|
|
|
67
67
|
this.prevDirAxis = stickDir;
|
|
68
68
|
if (stickDir)
|
|
69
69
|
pressed.add(stickDir);
|
|
70
|
-
for (const [dir, st] of this.dirs) {
|
|
70
|
+
for (const [dir, st] of this.dirs.entries()) {
|
|
71
71
|
const isDown = pressed.has(dir);
|
|
72
72
|
if (isDown && !st.held) {
|
|
73
73
|
st.held = true;
|
|
@@ -165,20 +165,20 @@ export class LanDiscovery {
|
|
|
165
165
|
const peer = { ...pkt, host: rinfo.address, lastSeenMs: this.opts.now() };
|
|
166
166
|
this.peers.set(pkt.id, peer);
|
|
167
167
|
if (!existing)
|
|
168
|
-
for (const f of this.listeners.join)
|
|
168
|
+
for (const f of this.listeners.join.values())
|
|
169
169
|
f(peer);
|
|
170
170
|
else if (existing.name !== peer.name || existing.port !== peer.port
|
|
171
171
|
|| JSON.stringify(existing.meta) !== JSON.stringify(peer.meta)) {
|
|
172
|
-
for (const f of this.listeners.update)
|
|
172
|
+
for (const f of this.listeners.update.values())
|
|
173
173
|
f(peer);
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
176
|
expire() {
|
|
177
177
|
const cutoff = this.opts.now() - this.opts.ttlMs;
|
|
178
|
-
for (const [id, p] of this.peers) {
|
|
178
|
+
for (const [id, p] of this.peers.entries()) {
|
|
179
179
|
if (p.lastSeenMs < cutoff) {
|
|
180
180
|
this.peers.delete(id);
|
|
181
|
-
for (const f of this.listeners.leave)
|
|
181
|
+
for (const f of this.listeners.leave.values())
|
|
182
182
|
f(p);
|
|
183
183
|
}
|
|
184
184
|
}
|
|
@@ -227,7 +227,7 @@ export class MdnsBrowser {
|
|
|
227
227
|
this.sendQuery(type);
|
|
228
228
|
}
|
|
229
229
|
requery() {
|
|
230
|
-
for (const t of this.watched)
|
|
230
|
+
for (const t of this.watched.values())
|
|
231
231
|
this.sendQuery(t);
|
|
232
232
|
this.expire();
|
|
233
233
|
}
|
|
@@ -257,7 +257,7 @@ export class MdnsBrowser {
|
|
|
257
257
|
}
|
|
258
258
|
expire() {
|
|
259
259
|
const now = this.opts.now();
|
|
260
|
-
for (const [k, s] of this.table) {
|
|
260
|
+
for (const [k, s] of this.table.entries()) {
|
|
261
261
|
if (s.lastSeenMs + s.ttlMs < now)
|
|
262
262
|
this.table.delete(k);
|
|
263
263
|
}
|
|
@@ -276,7 +276,7 @@ export class MdnsBrowser {
|
|
|
276
276
|
.filter((r) => "data" in r);
|
|
277
277
|
const now = this.opts.now();
|
|
278
278
|
// PTR gives instance names; SRV/TXT/A for the same instance fill detail.
|
|
279
|
-
for (const r of recs) {
|
|
279
|
+
for (const r of recs.values()) {
|
|
280
280
|
if (r.type !== "PTR" || !this.watched.has(r.name))
|
|
281
281
|
continue;
|
|
282
282
|
const instance = r.data;
|
|
@@ -291,7 +291,7 @@ export class MdnsBrowser {
|
|
|
291
291
|
.map((x) => x.ttl ?? 120)));
|
|
292
292
|
const txtMap = {};
|
|
293
293
|
if (txt) {
|
|
294
|
-
for (const item of txt.data) {
|
|
294
|
+
for (const item of txt.data.values()) {
|
|
295
295
|
const s = item.toString("utf8");
|
|
296
296
|
const eq = s.indexOf("=");
|
|
297
297
|
txtMap[eq < 0 ? s : s.slice(0, eq)] = eq < 0 ? "" : s.slice(eq + 1);
|
|
@@ -152,7 +152,7 @@ export class FileSaveStore {
|
|
|
152
152
|
await fs.mkdir(tmpBlobsDir, { recursive: true });
|
|
153
153
|
for (let _i = 0, _it = Object.entries(opts.blobs), _n = _it.length; _i < _n; _i++) {
|
|
154
154
|
const [key, buf] = _it[_i];
|
|
155
|
-
const safeKey = key.replace(/[^a-zA-Z0-9_
|
|
155
|
+
const safeKey = key.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
156
156
|
await fs.writeFile(join(tmpBlobsDir, safeKey), new Uint8Array(buf));
|
|
157
157
|
blobBytes += buf.byteLength;
|
|
158
158
|
}
|
|
@@ -480,19 +480,19 @@ export class FileSaveStore {
|
|
|
480
480
|
}
|
|
481
481
|
slotPath(slot) {
|
|
482
482
|
// Sanitize slot name to prevent path traversal
|
|
483
|
-
const safe = slot.replace(/[^a-zA-Z0-9_
|
|
483
|
+
const safe = slot.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
484
484
|
return join(this.saveDir, safe + SAVE_EXT);
|
|
485
485
|
}
|
|
486
486
|
blobsDirPath(slot) {
|
|
487
|
-
const safe = slot.replace(/[^a-zA-Z0-9_
|
|
487
|
+
const safe = slot.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
488
488
|
return join(this.saveDir, safe + BLOBS_DIR_SUFFIX);
|
|
489
489
|
}
|
|
490
490
|
thumbPath(slot) {
|
|
491
|
-
const safe = slot.replace(/[^a-zA-Z0-9_
|
|
491
|
+
const safe = slot.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
492
492
|
return join(this.saveDir, safe + THUMB_EXT);
|
|
493
493
|
}
|
|
494
494
|
propsPath(slot) {
|
|
495
|
-
const safe = slot.replace(/[^a-zA-Z0-9_
|
|
495
|
+
const safe = slot.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
496
496
|
return join(this.saveDir, safe + PROPS_EXT);
|
|
497
497
|
}
|
|
498
498
|
hashEqual(a, b) {
|
|
@@ -147,7 +147,7 @@ export class OpfsSaveStore {
|
|
|
147
147
|
}
|
|
148
148
|
// ── Path helpers ────────────────────────────────────────────────────────
|
|
149
149
|
sanitizeSlot(slot) {
|
|
150
|
-
return slot.replace(/[^a-zA-Z0-9_
|
|
150
|
+
return slot.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
151
151
|
}
|
|
152
152
|
genDirName(gen) {
|
|
153
153
|
return String(gen).padStart(4, "0");
|
|
@@ -239,7 +239,7 @@ export class OpfsSaveStore {
|
|
|
239
239
|
if (opts?.blobs && Object.keys(opts.blobs).length > 0) {
|
|
240
240
|
const blobsDir = await this.getBlobsDir(genDir, true);
|
|
241
241
|
for (const [key, buf] of Object.entries(opts.blobs)) {
|
|
242
|
-
const safeKey = key.replace(/[^a-zA-Z0-9_
|
|
242
|
+
const safeKey = key.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
243
243
|
await this.writeBinaryFile(blobsDir, safeKey, new Uint8Array(buf));
|
|
244
244
|
blobCount++;
|
|
245
245
|
}
|
|
@@ -125,7 +125,7 @@ export function createControllerUi() {
|
|
|
125
125
|
if (!p || !dev)
|
|
126
126
|
return;
|
|
127
127
|
const ops = [];
|
|
128
|
-
for (const [bit] of BTN_LABELS) {
|
|
128
|
+
for (const [bit] of BTN_LABELS.values()) {
|
|
129
129
|
ops.push({ op: "attr", sel: `#ctl-btn-${bit}`, name: "data-on",
|
|
130
130
|
value: dev.pressed(bit) ? "true" : "false" });
|
|
131
131
|
}
|
|
@@ -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
|
+
}
|