@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
|
@@ -25,6 +25,12 @@ export class PerformancePanel {
|
|
|
25
25
|
</div>`;
|
|
26
26
|
}
|
|
27
27
|
renderBody() {
|
|
28
|
+
// `chart + table + prof || note` could never fall through — the table
|
|
29
|
+
// markup is non-empty even with zero slots, so the empty-state hint was
|
|
30
|
+
// dead. Gate the whole thing on having anything to show.
|
|
31
|
+
if (!this.slots.length && !this.profile && !this.recording) {
|
|
32
|
+
return `<div class="empty-note">no metrics — is ProfilingSAB attached?</div>`;
|
|
33
|
+
}
|
|
28
34
|
const heapSeries = [...this.history.entries()].map(([slot, rec]) => ({
|
|
29
35
|
name: `heap — ${this.slotName(slot)}`,
|
|
30
36
|
values: rec.heap,
|
|
@@ -33,10 +39,13 @@ export class PerformancePanel {
|
|
|
33
39
|
const chart = heapSeries.length
|
|
34
40
|
? `<div class="snap-section"><h3>Heap per thread (MB)</h3>${heapSeries.map((s) => chartHtml(s.values, { max: Math.max(64, ...heapSeries.flatMap((x) => x.values)), color: s.color, label: s.name })).join("")}</div>`
|
|
35
41
|
: "";
|
|
36
|
-
let tbl =
|
|
42
|
+
let tbl = "";
|
|
43
|
+
if (this.slots.length) {
|
|
44
|
+
tbl = `<table class="dt"><tr>
|
|
37
45
|
<th>slot</th><th>name</th><th>cpu%</th><th>heap</th>
|
|
38
46
|
<th>gc max µs</th><th>task p95 µs</th></tr>`;
|
|
39
|
-
|
|
47
|
+
}
|
|
48
|
+
for (const s of this.slots.values()) {
|
|
40
49
|
const h = s.history?.[0] ?? {};
|
|
41
50
|
tbl += `<tr><td>${s.slotIndex}</td><td>${esc(s.name)}</td>
|
|
42
51
|
<td>${(h.cpuPercent ?? 0).toFixed(1)}</td>
|
|
@@ -44,25 +53,30 @@ export class PerformancePanel {
|
|
|
44
53
|
<td>${(h.gcPauseMaxUs ?? 0).toFixed(0)}</td>
|
|
45
54
|
<td>${(h.taskLatencyP95Us ?? 0).toFixed(0)}</td></tr>`;
|
|
46
55
|
}
|
|
47
|
-
|
|
48
|
-
|
|
56
|
+
if (this.slots.length)
|
|
57
|
+
tbl += `</table>`;
|
|
58
|
+
const table = tbl ? `<div class="snap-section"><h3>Threads</h3>${tbl}</div>` : "";
|
|
49
59
|
let prof = "";
|
|
50
60
|
if (this.recording) {
|
|
51
61
|
prof = `<div class="status-banner">● recording — press Stop to capture</div>`;
|
|
52
62
|
}
|
|
63
|
+
else if (this.profile && !Array.isArray(this.profile.nodes)) {
|
|
64
|
+
prof = `<div class="status-banner error">malformed profile payload</div>`;
|
|
65
|
+
}
|
|
53
66
|
else if (this.profile) {
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
67
|
+
const nodes = this.profile.nodes;
|
|
68
|
+
const top = [...nodes].sort((a, b) => (b.hitCount ?? 0) - (a.hitCount ?? 0)).slice(0, 40);
|
|
69
|
+
const total = Math.max(1, top.reduce((s, n) => s + (n.hitCount ?? 0), 0));
|
|
70
|
+
const durMs = ((this.profile.endUs ?? 0) - (this.profile.startUs ?? 0)) / 1000;
|
|
71
|
+
prof = `<div class="snap-section"><h3>CPU profile — ${nodes.length} nodes, ${durMs.toFixed(0)} ms</h3>
|
|
72
|
+
<div class="lines">${top.map((n) => esc(`${(100 * (n.hitCount ?? 0) / total).toFixed(1).padStart(5)}% ${n.callFrame} ${n.url}:${n.line}`)).join("\n")}</div></div>`;
|
|
59
73
|
}
|
|
60
|
-
return chart + table + prof
|
|
74
|
+
return chart + table + prof;
|
|
61
75
|
}
|
|
62
76
|
onBackendEvent(event, data) {
|
|
63
77
|
if (event === "metrics") {
|
|
64
|
-
this.slots = data
|
|
65
|
-
for (const s of this.slots) {
|
|
78
|
+
this.slots = Array.isArray(data) ? data : [];
|
|
79
|
+
for (const s of this.slots.values()) {
|
|
66
80
|
const h = s.history?.[0];
|
|
67
81
|
if (!h)
|
|
68
82
|
continue;
|
|
@@ -71,8 +85,8 @@ export class PerformancePanel {
|
|
|
71
85
|
rec = { cpu: [], heap: [] };
|
|
72
86
|
this.history.set(s.slotIndex, rec);
|
|
73
87
|
}
|
|
74
|
-
rec.cpu.push(h.cpuPercent);
|
|
75
|
-
rec.heap.push(h.heapUsed / 1048576);
|
|
88
|
+
rec.cpu.push(h.cpuPercent ?? 0);
|
|
89
|
+
rec.heap.push((h.heapUsed ?? 0) / 1048576);
|
|
76
90
|
if (rec.cpu.length > HIST) {
|
|
77
91
|
rec.cpu.shift();
|
|
78
92
|
rec.heap.shift();
|
|
@@ -18,6 +18,7 @@ export class SnapshotPanel {
|
|
|
18
18
|
slot;
|
|
19
19
|
snap = null;
|
|
20
20
|
updatedAt = "";
|
|
21
|
+
lastError = "";
|
|
21
22
|
constructor(ctx, slot, name) {
|
|
22
23
|
this.ctx = ctx;
|
|
23
24
|
this.slot = slot;
|
|
@@ -32,19 +33,22 @@ export class SnapshotPanel {
|
|
|
32
33
|
</div>`;
|
|
33
34
|
}
|
|
34
35
|
renderBody() {
|
|
36
|
+
const err = this.lastError
|
|
37
|
+
? `<div class="status-banner error">${esc(this.lastError)}</div>` : "";
|
|
35
38
|
const snap = this.snap;
|
|
36
39
|
if (!snap)
|
|
37
|
-
return `<div class="empty-note">no data</div>`;
|
|
40
|
+
return err + `<div class="empty-note">no data</div>`;
|
|
38
41
|
const banner = snap.status && snap.status !== "ok"
|
|
39
42
|
? `<div class="status-banner${snap.status === "error" ? " error" : ""}">${esc(snap.statusMsg ?? snap.status)}</div>`
|
|
40
43
|
: "";
|
|
41
|
-
return banner + (snap.sections ?? []).map((s) => this.renderSection(s)).join("");
|
|
44
|
+
return err + banner + (snap.sections ?? []).map((s) => this.renderSection(s)).join("");
|
|
42
45
|
}
|
|
43
46
|
onBackendEvent(event, data) {
|
|
44
47
|
if (event === "snapshot") {
|
|
45
48
|
const d = data;
|
|
46
49
|
if (d?.slot === this.slot) {
|
|
47
50
|
this.snap = d.snap;
|
|
51
|
+
this.lastError = "";
|
|
48
52
|
this.updatedAt = `updated ${new Date().toLocaleTimeString()}`;
|
|
49
53
|
this.dirty = true;
|
|
50
54
|
this.shellDirty = true;
|
|
@@ -61,18 +65,32 @@ export class SnapshotPanel {
|
|
|
61
65
|
return;
|
|
62
66
|
void this.ctx.call("command", {
|
|
63
67
|
panel: this.slot, action: cmd, payload: data.payload ?? "",
|
|
64
|
-
}).then(() => this.refresh());
|
|
68
|
+
}).then(() => this.refresh()).catch(() => { });
|
|
65
69
|
}
|
|
66
70
|
}
|
|
67
71
|
activate() {
|
|
68
72
|
void this.refresh();
|
|
69
73
|
}
|
|
74
|
+
refreshSeq = 0;
|
|
70
75
|
async refresh() {
|
|
76
|
+
// Overlapping refreshes (activate + provider push + manual Refresh) are
|
|
77
|
+
// last-call-wins — a slow stale response must not clobber a newer one.
|
|
78
|
+
const req = ++this.refreshSeq;
|
|
71
79
|
try {
|
|
72
|
-
|
|
80
|
+
const snap = (await this.ctx.call("snapshot", { panel: this.slot }));
|
|
81
|
+
if (req !== this.refreshSeq)
|
|
82
|
+
return;
|
|
83
|
+
this.snap = snap;
|
|
84
|
+
this.lastError = "";
|
|
73
85
|
this.updatedAt = `updated ${new Date().toLocaleTimeString()}`;
|
|
74
86
|
}
|
|
75
|
-
catch {
|
|
87
|
+
catch (err) {
|
|
88
|
+
if (req !== this.refreshSeq)
|
|
89
|
+
return;
|
|
90
|
+
// Keep the last good snapshot; surface the failure instead of
|
|
91
|
+
// pretending the data is current.
|
|
92
|
+
this.lastError = `refresh failed: ${String(err)}`;
|
|
93
|
+
}
|
|
76
94
|
this.dirty = true;
|
|
77
95
|
this.shellDirty = true;
|
|
78
96
|
}
|
|
@@ -118,12 +136,17 @@ export class SnapshotPanel {
|
|
|
118
136
|
return `<label><span class="dbtn${c.checked ? " on" : ""}" ${cmdAttr} data-payload="${c.checked ? "0" : "1"}">${c.checked ? "☑" : "☐"}</span> ${esc(c.label)}</label>`;
|
|
119
137
|
}
|
|
120
138
|
if (c.type === "slider") {
|
|
121
|
-
|
|
139
|
+
// Providers can emit a slider without min/max/value — coerce so the
|
|
140
|
+
// stepper never ships "NaN" payloads back through snap.cmd.
|
|
141
|
+
const min = Number.isFinite(c.min) ? c.min : 0;
|
|
142
|
+
const max = Number.isFinite(c.max) ? c.max : min + 1;
|
|
143
|
+
const value = Number.isFinite(c.value) ? c.value : min;
|
|
144
|
+
const span = max - min || 1;
|
|
122
145
|
const step = span / 20;
|
|
123
|
-
const dec = (d) => Math.min(
|
|
146
|
+
const dec = (d) => Math.min(max, Math.max(min, value + d * step)).toFixed(2);
|
|
124
147
|
return `<label>${esc(c.label)}
|
|
125
148
|
<span class="dbtn" ${cmdAttr} data-payload="${dec(-1)}">−</span>
|
|
126
|
-
<span class="val">${
|
|
149
|
+
<span class="val">${value.toFixed(2)}</span>
|
|
127
150
|
<span class="dbtn" ${cmdAttr} data-payload="${dec(1)}">+</span></label>`;
|
|
128
151
|
}
|
|
129
152
|
return esc(c.label ?? "");
|
|
@@ -39,7 +39,8 @@ export function chartHtml(values, opts = {}) {
|
|
|
39
39
|
const step = Math.max(1, Math.floor(vals.length / cols));
|
|
40
40
|
const cells = [];
|
|
41
41
|
for (let i = 0; i < vals.length; i += step) {
|
|
42
|
-
const
|
|
42
|
+
const v = Number.isFinite(vals[i]) ? vals[i] : 0;
|
|
43
|
+
const h = Math.max(0, Math.min(100, (v / max) * 100));
|
|
43
44
|
cells.push(`<div class="col" style="height:${h.toFixed(1)}%;background:${color}"></div>`);
|
|
44
45
|
}
|
|
45
46
|
if (!cells.length)
|
|
@@ -14,7 +14,7 @@ export const DEVTOOLS_CSS = `
|
|
|
14
14
|
--warn: #e0b341;
|
|
15
15
|
--err: #e5534b;
|
|
16
16
|
--ok: #57ab5a;
|
|
17
|
-
--mono: "SF Mono", "Cascadia Mono", Consolas, monospace;
|
|
17
|
+
--mono: "DevTools Mono", "SF Mono", "Cascadia Mono", Consolas, monospace;
|
|
18
18
|
}
|
|
19
19
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
20
20
|
/* height:100% must be on html AND body — Blitz resolves body % against
|
|
@@ -22,7 +22,7 @@ export const DEVTOOLS_CSS = `
|
|
|
22
22
|
html, body {
|
|
23
23
|
width: 100%; height: 100%; overflow: hidden;
|
|
24
24
|
background: var(--bg); color: var(--fg);
|
|
25
|
-
font: 12px/1.45 system-ui, sans-serif;
|
|
25
|
+
font: 12px/1.45 "DevTools Sans", system-ui, sans-serif;
|
|
26
26
|
}
|
|
27
27
|
#dt-root {
|
|
28
28
|
/* Absolute inset — Blitz doesn't resolve height:100% against the doc
|
|
@@ -50,7 +50,11 @@ html, body {
|
|
|
50
50
|
display: flex; align-items: center; gap: 2px;
|
|
51
51
|
background: var(--bg2); border-bottom: 1px solid var(--line);
|
|
52
52
|
padding: 0 8px; height: 30px; flex-shrink: 0;
|
|
53
|
+
/* Provider tabs can overflow the strip — wheel-scroll it instead of
|
|
54
|
+
* clipping tabs unreachable. */
|
|
55
|
+
overflow-x: auto; overflow-y: hidden;
|
|
53
56
|
}
|
|
57
|
+
#dt-tabs .tab, #dt-tabs .brand, #dt-tabs .hint { flex-shrink: 0; }
|
|
54
58
|
#dt-tabs .brand { color: var(--accent); font-weight: 600; margin-right: 12px; }
|
|
55
59
|
#dt-tabs .spacer { flex: 1; }
|
|
56
60
|
#dt-tabs .hint { font-size: 11px; color: var(--fg-dim); }
|
|
@@ -124,14 +128,32 @@ html, body {
|
|
|
124
128
|
.console-repl input:focus { border-color: var(--accent); }
|
|
125
129
|
.console-repl .prompt { color: var(--accent); font-family: var(--mono); }
|
|
126
130
|
|
|
127
|
-
/* ── Tree (scene/
|
|
131
|
+
/* ── Tree (scene/ecs) ── */
|
|
128
132
|
.tree { font-family: var(--mono); font-size: 11.5px; }
|
|
129
|
-
.trow { padding: 1px 4px; white-space: nowrap; }
|
|
133
|
+
.trow { padding: 1px 4px; white-space: nowrap; cursor: default; }
|
|
130
134
|
.trow:hover { background: var(--bg3); }
|
|
131
135
|
.trow.sel { background: #2d4a72; }
|
|
136
|
+
.trow .caret {
|
|
137
|
+
display: inline-block; width: 12px; color: var(--fg-dim);
|
|
138
|
+
cursor: pointer; user-select: none;
|
|
139
|
+
}
|
|
140
|
+
.trow .caret:hover { color: var(--fg); }
|
|
132
141
|
.trow .detail { color: var(--fg-dim); margin-left: 8px; }
|
|
133
142
|
.trow .kids { color: var(--fg-dim); }
|
|
134
143
|
|
|
144
|
+
/* ── Elements details strip (bottom chrome) ── */
|
|
145
|
+
.el-details { border-top: 1px solid var(--line); background: var(--bg); }
|
|
146
|
+
.el-details-head {
|
|
147
|
+
display: flex; align-items: center; gap: 8px;
|
|
148
|
+
padding: 3px 8px; border-bottom: 1px solid var(--line);
|
|
149
|
+
}
|
|
150
|
+
.el-details-head .dt-title { color: var(--accent); font-weight: 600; }
|
|
151
|
+
.el-details-head .spacer { flex: 1; }
|
|
152
|
+
.el-details-body {
|
|
153
|
+
max-height: 140px; overflow-y: auto; padding: 6px 8px;
|
|
154
|
+
font-family: var(--mono); font-size: 11.5px; white-space: pre-wrap;
|
|
155
|
+
}
|
|
156
|
+
|
|
135
157
|
/* ── KV / tables / snapshot panels ── */
|
|
136
158
|
.snap-section { margin-bottom: 14px; }
|
|
137
159
|
.snap-section > h3 {
|
|
@@ -147,9 +147,32 @@ export function createDocCore(emit, gpuHooks) {
|
|
|
147
147
|
s.statsLast = now;
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
|
+
// Smooth-wheel ease: fraction of the remainder applied per elapsed ms.
|
|
151
|
+
// τ≈45ms → ~30% per 16ms tick, ~95% drained in ~135ms — snappy but
|
|
152
|
+
// animated. A 1/MAX_TICKS floor bounds any backlog at ~320ms of drain —
|
|
153
|
+
// a fast wheel can't make scroll trail input indefinitely. Dispatching
|
|
154
|
+
// real wheel events keeps Blitz's hit-testing, scroll chaining, scrollbar
|
|
155
|
+
// reveal, and scroll DOM events intact.
|
|
156
|
+
const WHEEL_TAU_MS = 45;
|
|
157
|
+
const WHEEL_MAX_TICKS = 20;
|
|
158
|
+
function stepWheel(s, now) {
|
|
159
|
+
const dt = Math.min(50, Math.max(1, now - s.wheelT));
|
|
160
|
+
s.wheelT = now;
|
|
161
|
+
const f = Math.max(1 - Math.exp(-dt / WHEEL_TAU_MS), 1 / WHEEL_MAX_TICKS);
|
|
162
|
+
// Snap the tail: a step below ~¾px/axis applies the whole residue instead
|
|
163
|
+
// of asymptoting through invisible sub-pixel repaints.
|
|
164
|
+
const dx = Math.abs(s.wheelRx) * f < 0.75 ? s.wheelRx : s.wheelRx * f;
|
|
165
|
+
const dy = Math.abs(s.wheelRy) * f < 0.75 ? s.wheelRy : s.wheelRy * f;
|
|
166
|
+
s.wheelRx -= dx;
|
|
167
|
+
s.wheelRy -= dy;
|
|
168
|
+
const k = s.scale;
|
|
169
|
+
s.doc.wheel(s.wheelX * k, s.wheelY * k, dx * k, dy * k, s.wheelMods);
|
|
170
|
+
}
|
|
150
171
|
function pump() {
|
|
151
172
|
const now = performance.now();
|
|
152
173
|
for (const [id, s] of docs.entries()) {
|
|
174
|
+
if (s.smoothWheel && (s.wheelRx !== 0 || s.wheelRy !== 0))
|
|
175
|
+
stepWheel(s, now);
|
|
153
176
|
const events = s.doc.pollEvents();
|
|
154
177
|
if (events.length)
|
|
155
178
|
emit({ type: "events", id, events });
|
|
@@ -233,6 +256,8 @@ export function createDocCore(emit, gpuHooks) {
|
|
|
233
256
|
doc, scale: m.scale, cssW: m.cssW, cssH: m.cssH,
|
|
234
257
|
maxFps: m.maxFps ?? 0, lastFrame: -1e9,
|
|
235
258
|
sab: null, sabU32: null, sabDisabled: false, gpuTex: null,
|
|
259
|
+
smoothWheel: m.smoothWheel === true,
|
|
260
|
+
wheelRx: 0, wheelRy: 0, wheelX: 0, wheelY: 0, wheelT: 0,
|
|
236
261
|
stats: freshStats(), statsLast: 0,
|
|
237
262
|
};
|
|
238
263
|
docs.set(m.id, s);
|
|
@@ -245,11 +270,18 @@ export function createDocCore(emit, gpuHooks) {
|
|
|
245
270
|
s.maxFps = m.maxFps;
|
|
246
271
|
break;
|
|
247
272
|
}
|
|
248
|
-
case "setHtml":
|
|
273
|
+
case "setHtml": {
|
|
249
274
|
if (/float\s*:/.test(m.html))
|
|
250
275
|
emit({ type: "error", id: m.id, message: "CSS float hangs Blitz layout — use flex instead" });
|
|
251
|
-
docs.get(m.id)
|
|
276
|
+
const s = docs.get(m.id);
|
|
277
|
+
if (!s)
|
|
278
|
+
break;
|
|
279
|
+
// Drop any in-flight smooth-wheel drain — its remainder belongs to
|
|
280
|
+
// the document being replaced.
|
|
281
|
+
s.wheelRx = s.wheelRy = 0;
|
|
282
|
+
s.doc.setHtml(m.html);
|
|
252
283
|
break;
|
|
284
|
+
}
|
|
253
285
|
case "resize": {
|
|
254
286
|
const s = docs.get(m.id);
|
|
255
287
|
if (s) {
|
|
@@ -374,13 +406,30 @@ export function createDocCore(emit, gpuHooks) {
|
|
|
374
406
|
s.doc.pointerMove(msg.x * k, msg.y * k, msg.mods);
|
|
375
407
|
break;
|
|
376
408
|
case "down":
|
|
409
|
+
// A press interrupts any in-flight wheel scroll (mirrors
|
|
410
|
+
// Blitz's interrupt_animation semantics for user input).
|
|
411
|
+
s.wheelRx = 0;
|
|
412
|
+
s.wheelRy = 0;
|
|
377
413
|
s.doc.pointerDown(msg.x * k, msg.y * k, msg.button, msg.mods);
|
|
378
414
|
break;
|
|
379
415
|
case "up":
|
|
380
416
|
s.doc.pointerUp(msg.x * k, msg.y * k, msg.button, msg.mods);
|
|
381
417
|
break;
|
|
382
418
|
case "wheel":
|
|
383
|
-
s.
|
|
419
|
+
if (s.smoothWheel) {
|
|
420
|
+
// A fresh gesture back-dates wheelT so the first pump tick
|
|
421
|
+
// applies a full ~16ms step immediately — no dead frame.
|
|
422
|
+
if (s.wheelRx === 0 && s.wheelRy === 0)
|
|
423
|
+
s.wheelT = performance.now() - 16;
|
|
424
|
+
s.wheelRx += msg.deltaX;
|
|
425
|
+
s.wheelRy += msg.deltaY;
|
|
426
|
+
s.wheelX = msg.x;
|
|
427
|
+
s.wheelY = msg.y;
|
|
428
|
+
s.wheelMods = msg.mods;
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
s.doc.wheel(msg.x * k, msg.y * k, msg.deltaX * k, msg.deltaY * k, msg.mods);
|
|
432
|
+
}
|
|
384
433
|
break;
|
|
385
434
|
case "key":
|
|
386
435
|
s.doc.key(msg.down, msg.key, msg.code, msg.text, msg.mods);
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// ============================================================================
|
|
8
8
|
import { getHostCapabilities, getNativeHost } from "@downdraft/engine/platform/runtime";
|
|
9
9
|
import { createLogger } from "@downdraft/engine/util/logger";
|
|
10
|
+
import { untrackedTimeout } from "@downdraft/engine/util/session-timer";
|
|
10
11
|
import { PanelBlitPass } from "./composite.js";
|
|
11
12
|
import { createLocalBackend, createWorkerBackend } from "./doc-backend.js";
|
|
12
13
|
const log = createLogger("info");
|
|
@@ -136,7 +137,7 @@ export class HtmlUiHost {
|
|
|
136
137
|
this.order.push(id);
|
|
137
138
|
// Doc viewport scale = raster px per CSS px = scale * zoom; layout size
|
|
138
139
|
// is rect/zoom CSS px so the raster still covers rect*scale texture px.
|
|
139
|
-
this.send({ type: "create", id, cssW: spec.rect.w / zoom, cssH: spec.docH ?? spec.rect.h / zoom, scale: scale * zoom, html: spec.html, maxFps: spec.maxFps });
|
|
140
|
+
this.send({ type: "create", id, cssW: spec.rect.w / zoom, cssH: spec.docH ?? spec.rect.h / zoom, scale: scale * zoom, html: spec.html, maxFps: spec.maxFps, smoothWheel: spec.smoothWheel });
|
|
140
141
|
// GPU-direct: create the panel texture eagerly so the worker's texBind
|
|
141
142
|
// lands before its first upload — the host knows raster dims at mount.
|
|
142
143
|
if (this.gpuReady)
|
|
@@ -301,6 +302,8 @@ export class HtmlUiHost {
|
|
|
301
302
|
/** Doc-backend capabilities (populated by the `ready` handshake —
|
|
302
303
|
* `incrementalDom` is false until then, and forever on old cdylibs). */
|
|
303
304
|
get docCaps() { return this.caps; }
|
|
305
|
+
/** Which doc backend is live — "worker" (raster off-thread) or "local". */
|
|
306
|
+
get backendKind() { return this.backend.kind; }
|
|
304
307
|
/** Compositor surface for GameRenderer's end-of-frame UI pass. */
|
|
305
308
|
get compositor() {
|
|
306
309
|
return {
|
|
@@ -349,7 +352,7 @@ export class HtmlUiHost {
|
|
|
349
352
|
this.terminating = true;
|
|
350
353
|
this.send({ type: "gpuDetach" });
|
|
351
354
|
const backend = this.backend;
|
|
352
|
-
|
|
355
|
+
untrackedTimeout(() => {
|
|
353
356
|
if (!this.gpuDetachAcked)
|
|
354
357
|
backend.dispose();
|
|
355
358
|
this.destroyRetired();
|
|
@@ -775,7 +778,7 @@ export class HtmlUiHost {
|
|
|
775
778
|
|| (p.postedBindPtr !== null && BigInt(ptr) === p.postedBindPtr));
|
|
776
779
|
if (borrowed) {
|
|
777
780
|
const key = BigInt(ptr);
|
|
778
|
-
const timer =
|
|
781
|
+
const timer = untrackedTimeout(() => {
|
|
779
782
|
if (this.retiredTex.delete(key))
|
|
780
783
|
t.destroy();
|
|
781
784
|
}, 1000);
|
|
@@ -31,3 +31,9 @@ web-time = "1"
|
|
|
31
31
|
# Workaround copied from blitz examples: disable unicode URL support
|
|
32
32
|
# (see https://github.com/hsivonen/idna_adapter)
|
|
33
33
|
idna_adapter = "=1.0.0"
|
|
34
|
+
|
|
35
|
+
# Enable the vendored glifo's FreeType mask path (see [patch.crates-io] in the
|
|
36
|
+
# workspace root). Feature unification turns it on inside vello_cpu's copy.
|
|
37
|
+
# Target-gated: freetype-sys compiles bundled C, which wasm32 can't build.
|
|
38
|
+
[target.'cfg(not(target_family = "wasm"))'.dependencies]
|
|
39
|
+
glifo = { version = "0.2", features = ["freetype"] }
|
|
@@ -13,7 +13,9 @@ crate-type = ["cdylib"]
|
|
|
13
13
|
blitz-html = { git = "https://github.com/DioxusLabs/blitz", rev = "efef6519fe6520caf66dc82df557580eecbf652b" }
|
|
14
14
|
blitz-dom = { git = "https://github.com/DioxusLabs/blitz", rev = "efef6519fe6520caf66dc82df557580eecbf652b", default-features = false, features = ["woff"] }
|
|
15
15
|
blitz-traits = { git = "https://github.com/DioxusLabs/blitz", rev = "efef6519fe6520caf66dc82df557580eecbf652b" }
|
|
16
|
-
|
|
16
|
+
# scrollbars: painted overlay thumbs on scroll containers + thumb drag/hover
|
|
17
|
+
# hit-testing (pulls blitz-dom/scrollbars transitively).
|
|
18
|
+
blitz-paint = { git = "https://github.com/DioxusLabs/blitz", rev = "efef6519fe6520caf66dc82df557580eecbf652b", features = ["custom-widget", "scrollbars"] }
|
|
17
19
|
# Enable the vendored glifo's FreeType mask path (see [patch.crates-io] in the
|
|
18
20
|
# workspace root). Feature unification turns it on inside vello_cpu's copy.
|
|
19
21
|
glifo = { version = "0.2", features = ["freetype"] }
|
|
@@ -77,6 +77,12 @@ export class DevtoolsBackend {
|
|
|
77
77
|
private providers = new Map<number, PanelProvider>();
|
|
78
78
|
private commandHandlers = new Map<number, PanelCommandHandler>();
|
|
79
79
|
private globalCommandHandler: PanelCommandHandler | null = null;
|
|
80
|
+
/** Dynamically-allocated slots for provider names outside the fixed PANEL
|
|
81
|
+
* table — panelSlot() alone collapsed every unknown name onto slot 0
|
|
82
|
+
* ("console"), so a second game provider silently replaced the first. */
|
|
83
|
+
private dynSlots = new Map<string, number>();
|
|
84
|
+
private dynSlotNames = new Map<number, string>();
|
|
85
|
+
private nextDynSlot = 100;
|
|
80
86
|
private lastProviderPush = new Map<number, number>();
|
|
81
87
|
providerIntervalMs = 1500;
|
|
82
88
|
/** When non-null, the cadence loop only auto-refreshes this provider
|
|
@@ -237,37 +243,57 @@ export class DevtoolsBackend {
|
|
|
237
243
|
|
|
238
244
|
// ── Providers / commands ──
|
|
239
245
|
|
|
240
|
-
|
|
241
|
-
|
|
246
|
+
/** Resolve a panel id to its slot. Names outside the fixed PANEL table
|
|
247
|
+
* get a stable dynamically-allocated slot so bespoke game providers can
|
|
248
|
+
* coexist instead of colliding on slot 0. */
|
|
249
|
+
private slotFor(panel: PanelName | number | string): number {
|
|
250
|
+
if (typeof panel === "number") return panel;
|
|
251
|
+
const fixed = PANEL[panel as PanelName];
|
|
252
|
+
if (fixed !== undefined) return fixed;
|
|
253
|
+
let s = this.dynSlots.get(panel);
|
|
254
|
+
if (s === undefined) {
|
|
255
|
+
s = this.nextDynSlot++;
|
|
256
|
+
this.dynSlots.set(panel, s);
|
|
257
|
+
this.dynSlotNames.set(s, panel);
|
|
258
|
+
}
|
|
259
|
+
return s;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private nameFor(slot: number): string {
|
|
263
|
+
return this.dynSlotNames.get(slot) ?? slotName(slot);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
registerProvider(panel: PanelName | number | string, collect: PanelProvider): void {
|
|
267
|
+
this.providers.set(this.slotFor(panel), collect);
|
|
242
268
|
}
|
|
243
269
|
|
|
244
|
-
registerCommandHandler(panel: PanelName | number |
|
|
270
|
+
registerCommandHandler(panel: PanelName | number | string, handler: PanelCommandHandler): void {
|
|
245
271
|
if (panel === "*") {
|
|
246
272
|
this.globalCommandHandler = handler;
|
|
247
273
|
} else {
|
|
248
|
-
this.commandHandlers.set(
|
|
274
|
+
this.commandHandlers.set(this.slotFor(panel), handler);
|
|
249
275
|
}
|
|
250
276
|
}
|
|
251
277
|
|
|
252
278
|
/** Registered provider slots — the dynamic tab list for frontends. */
|
|
253
279
|
providerSlots(): { slot: number; name: string }[] {
|
|
254
|
-
return [...this.providers.keys()].map((slot) => ({ slot, name:
|
|
280
|
+
return [...this.providers.keys()].map((slot) => ({ slot, name: this.nameFor(slot) }));
|
|
255
281
|
}
|
|
256
282
|
|
|
257
283
|
/** Collect + emit one provider's snapshot immediately. */
|
|
258
|
-
refreshPanel(panel: PanelName | number): void {
|
|
284
|
+
refreshPanel(panel: PanelName | number | string): void {
|
|
259
285
|
const trace: number[] = [performance.now()];
|
|
260
|
-
const slot =
|
|
286
|
+
const slot = this.slotFor(panel);
|
|
261
287
|
trace.push(performance.now());
|
|
262
288
|
const collect = this.providers.get(slot);
|
|
263
289
|
if (!collect) return;
|
|
264
290
|
const push = (snap: PanelSnapshot | null | undefined) => {
|
|
265
|
-
if (snap) this.emit("snapshot", { panel:
|
|
291
|
+
if (snap) this.emit("snapshot", { panel: this.nameFor(slot), slot, snap });
|
|
266
292
|
this.lastProviderPush.set(slot, performance.now());
|
|
267
293
|
};
|
|
268
294
|
const fail = (err: unknown) => {
|
|
269
295
|
this.emit("snapshot", {
|
|
270
|
-
panel:
|
|
296
|
+
panel: this.nameFor(slot), slot,
|
|
271
297
|
snap: { status: "error", statusMsg: String(err), sections: [] },
|
|
272
298
|
});
|
|
273
299
|
this.lastProviderPush.set(slot, performance.now());
|
|
@@ -288,7 +314,7 @@ export class DevtoolsBackend {
|
|
|
288
314
|
const total = trace[4]! - trace[0]!;
|
|
289
315
|
if (total > 10) {
|
|
290
316
|
const seg = `pre=${(trace[2]! - trace[0]!).toFixed(1)} collect=${(trace[3]! - trace[2]!).toFixed(1)} emit=${(trace[4]! - trace[3]!).toFixed(1)}`;
|
|
291
|
-
this.diagWarn(`slow provider ${
|
|
317
|
+
this.diagWarn(`slow provider ${this.nameFor(slot)}: ${total.toFixed(1)}ms [${seg}]`);
|
|
292
318
|
}
|
|
293
319
|
}
|
|
294
320
|
|
|
@@ -431,7 +457,7 @@ export class DevtoolsBackend {
|
|
|
431
457
|
return collectThreads(this.ctx, [...this.threadEvals.keys()]);
|
|
432
458
|
case "snapshot": {
|
|
433
459
|
const panel = params.panel;
|
|
434
|
-
const slot = typeof panel === "number" ? panel :
|
|
460
|
+
const slot = typeof panel === "number" ? panel : this.slotFor(String(panel));
|
|
435
461
|
const collect = this.providers.get(slot);
|
|
436
462
|
if (!collect) return { status: "unsupported", statusMsg: `no provider for ${String(panel)}`, sections: [] };
|
|
437
463
|
try {
|
|
@@ -443,7 +469,7 @@ export class DevtoolsBackend {
|
|
|
443
469
|
}
|
|
444
470
|
case "command": {
|
|
445
471
|
const panel = params.panel;
|
|
446
|
-
const slot = typeof panel === "number" ? panel :
|
|
472
|
+
const slot = typeof panel === "number" ? panel : this.slotFor(String(panel ?? "console"));
|
|
447
473
|
this.dispatchCommand({ panel: slot, action: String(params.action ?? ""), payload: String(params.payload ?? "") });
|
|
448
474
|
return true;
|
|
449
475
|
}
|
|
@@ -18,6 +18,11 @@ export interface ThreadInfo { id: string; name: string; kind: number }
|
|
|
18
18
|
export interface TreeRow {
|
|
19
19
|
id: number; parentId: number; depth: number; childCount: number;
|
|
20
20
|
kind: number; label: string; detail: string;
|
|
21
|
+
/** Stable node identity for pick/inspect — scene-store id, stage node id,
|
|
22
|
+
* or ECS entity index ("ent-N") when the collector can provide one. The
|
|
23
|
+
* numeric `id` is ephemeral (re-assigned per collect); `nodeId` survives
|
|
24
|
+
* refreshes and is what `inspector.call` methods take. */
|
|
25
|
+
nodeId?: string;
|
|
21
26
|
}
|
|
22
27
|
export interface GpuInfoJson {
|
|
23
28
|
entries: { key: string; value: string; isHeader: boolean }[];
|
|
@@ -100,30 +105,77 @@ export async function collectThreads(
|
|
|
100
105
|
return threads;
|
|
101
106
|
}
|
|
102
107
|
|
|
103
|
-
|
|
108
|
+
function sceneInspectorApi(): Record<string, any> | null {
|
|
109
|
+
const g = globalThis as Record<string, any>;
|
|
110
|
+
return (g.window?.__sceneInspector ?? g.__sceneInspector ?? null) as Record<string, any> | null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Scene-graph tree (label + ctor + child counts). Prefers a live
|
|
114
|
+
* `gameScene.stage` walk; falls back to the SceneInspector's scene store
|
|
115
|
+
* (`__sceneInspector.getSceneTree`) so games without a DOM-like stage still
|
|
116
|
+
* get a populated tree — with stable nodeIds for pick/details. */
|
|
104
117
|
export function collectSceneTree(ctx: CollectorContext): TreeRow[] {
|
|
105
118
|
const stage = ctx.gameScene?.stage;
|
|
106
|
-
if (
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
119
|
+
if (stage) {
|
|
120
|
+
const nodes: TreeRow[] = [];
|
|
121
|
+
let idCounter = 1;
|
|
122
|
+
const collect = (node: any, parentId: number, depth: number): void => {
|
|
123
|
+
const id = idCounter++;
|
|
124
|
+
// Prefer `label` (some scene-graph hosts warn on `name` access); fall
|
|
125
|
+
// back to `name` for hosts that only set that.
|
|
126
|
+
const nodeLabel = typeof node.label === "string" ? node.label
|
|
127
|
+
: typeof node.name === "string" ? node.name : "";
|
|
128
|
+
const ctorName = node.constructor?.name ?? "Node";
|
|
129
|
+
nodes.push({
|
|
130
|
+
id, parentId, depth,
|
|
131
|
+
childCount: node.children?.length ?? 0,
|
|
132
|
+
kind: 0,
|
|
133
|
+
label: nodeLabel || ctorName,
|
|
134
|
+
detail: nodeLabel ? ctorName : "",
|
|
135
|
+
nodeId: typeof node.id === "string" ? node.id : undefined,
|
|
136
|
+
});
|
|
137
|
+
node.children?.forEach((child: any) => collect(child, id, depth + 1));
|
|
138
|
+
};
|
|
139
|
+
collect(stage, -1, 0);
|
|
140
|
+
return nodes;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Scene-store fallback — the same snapshot the inspector's getSceneTree
|
|
144
|
+
// exposes. Nodes are flat with id/parentId links; walk roots → children.
|
|
145
|
+
try {
|
|
146
|
+
const snap = sceneInspectorApi()?.getSceneTree?.();
|
|
147
|
+
const storeNodes = snap?.nodes;
|
|
148
|
+
if (!Array.isArray(storeNodes) || !storeNodes.length) return [];
|
|
149
|
+
const byId = new Map<string, any>(storeNodes.map((n: any) => [n.id, n]));
|
|
150
|
+
const roots = storeNodes.filter((n: any) => n.parentId === null || !byId.has(n.parentId));
|
|
151
|
+
const rows: TreeRow[] = [];
|
|
152
|
+
const visited = new Set<string>();
|
|
153
|
+
let idCounter = 1;
|
|
154
|
+
const walk = (n: any, parentId: number, depth: number): void => {
|
|
155
|
+
if (visited.has(n.id)) return; // corrupted parent/child links — don't loop
|
|
156
|
+
visited.add(n.id);
|
|
157
|
+
const id = idCounter++;
|
|
158
|
+
const p = n.position;
|
|
159
|
+
rows.push({
|
|
160
|
+
id, parentId, depth,
|
|
161
|
+
childCount: n.children?.length ?? 0,
|
|
162
|
+
kind: 0,
|
|
163
|
+
label: String(n.name ?? n.id),
|
|
164
|
+
detail: `${n.type ?? "node"}${Array.isArray(p) ? ` (${p.map((v: number) => (+v).toFixed(1)).join(", ")})` : ""}`,
|
|
165
|
+
nodeId: String(n.id),
|
|
166
|
+
});
|
|
167
|
+
for (const cid of n.children ?? []) {
|
|
168
|
+
const c = byId.get(cid);
|
|
169
|
+
if (c) walk(c, id, depth + 1);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
roots.forEach((n: any) => walk(n, -1, 0));
|
|
173
|
+
// Orphans (every node has a parent but no root resolves) — still show them.
|
|
174
|
+
for (const n of storeNodes) if (!visited.has(n.id)) walk(n, -1, 0);
|
|
175
|
+
return rows;
|
|
176
|
+
} catch {
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
127
179
|
}
|
|
128
180
|
|
|
129
181
|
/** DOM tree: scene-graph stage or ECS entities depending on mode. */
|
|
@@ -148,6 +200,7 @@ export function collectDomTree(ctx: CollectorContext, mode: "scene" | "ecs"): Tr
|
|
|
148
200
|
id, parentId: -1, depth: 0, childCount: 0, kind: 1,
|
|
149
201
|
label: `Entity ${ent.idx}`,
|
|
150
202
|
detail: `type=${entityType} pos=(${x.toFixed(1)},${y.toFixed(1)},${z.toFixed(1)})`,
|
|
203
|
+
nodeId: `ent-${ent.idx}`,
|
|
151
204
|
});
|
|
152
205
|
});
|
|
153
206
|
} catch { /* sim reader hiccup */ }
|