@downdraft/engine 0.3.0 → 0.4.0-alpha.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 +0 -6
- package/app/src/renderer/feature-log.ts +1 -9
- package/app/src/renderer/mcp-harness.ts +8 -11
- package/app/src/renderer/standard-automation-tools.ts +8 -145
- package/core/src/module/host.ts +3 -6
- package/core/src/module/index.ts +1 -1
- package/core/src/module/module.ts +4 -47
- package/core/src/physics/interface.ts +5 -0
- package/core/src/platform/render-surface.ts +13 -0
- package/core/src/render/canvas-resize-watcher.ts +1 -10
- package/core/src/render/game-renderer.ts +40 -18
- package/core/src/render/index.ts +0 -2
- package/core/src/render/input-manager.ts +7 -7
- package/core/src/render/renderer-module-host.ts +2 -4
- package/core/src/ui-font-scale.ts +5 -38
- package/core/src/worker/rpc.ts +65 -0
- package/dist/.dd-manifest.json +859 -884
- package/dist/app/src/renderer/bootstrap.js +0 -4
- package/dist/app/src/renderer/feature-log.js +1 -10
- package/dist/app/src/renderer/mcp-harness.js +8 -11
- package/dist/app/src/renderer/standard-automation-tools.js +6 -138
- package/dist/core/src/module/host.js +3 -6
- package/dist/core/src/render/canvas-resize-watcher.js +1 -10
- package/dist/core/src/render/game-renderer.js +44 -18
- package/dist/core/src/render/index.js +0 -1
- package/dist/core/src/render/input-manager.js +7 -7
- package/dist/core/src/render/renderer-module-host.js +2 -4
- package/dist/core/src/ui-font-scale.js +5 -38
- package/dist/core/src/worker/rpc.js +59 -0
- package/dist/libraries/blitz-ui/src/native-module.js +8 -1
- package/dist/libraries/devtools/src/native-providers.js +0 -2
- package/dist/libraries/entities/src/model-renderer.js +87 -27
- package/dist/libraries/entities/src/shaders/model.wgsl +13 -4
- package/dist/libraries/networking/src/index.js +1 -0
- package/dist/libraries/networking/src/ws-server.js +387 -0
- package/dist/libraries/physics-rapier/src/api.js +15 -0
- package/dist/libraries/physics-rapier/src/backend.js +15 -0
- package/dist/libraries/physics-rapier/src/rapier-backend.js +5 -0
- package/dist/libraries/postfx/src/post-process-stack.js +1 -3
- package/dist/libraries/sand/src/materials.js +2 -32
- package/dist/libraries/sand/src/sand-world.js +0 -3
- package/dist/modules/devtools/src/api.js +8 -23
- package/dist/modules/devtools/src/data-bridge.js +0 -11
- package/dist/modules/devtools/src/index.js +0 -6
- package/dist/modules/devtools/src/init.js +8 -42
- package/dist/modules/devtools/src/scene-inspector.js +1 -4
- package/dist/modules/devtools/src/worker-sync.js +3 -11
- package/dist/test/src/vision-test.js +1 -3
- package/libraries/blitz-ui/src/native-module.ts +8 -1
- package/libraries/devtools/src/native-providers.ts +0 -2
- package/libraries/entities/src/model-renderer.ts +88 -27
- package/libraries/entities/src/shaders/model.wgsl +13 -4
- package/libraries/networking/src/index.ts +4 -0
- package/libraries/networking/src/ws-server.ts +397 -0
- package/libraries/physics-rapier/src/api.ts +15 -0
- package/libraries/physics-rapier/src/backend.ts +15 -0
- package/libraries/physics-rapier/src/rapier-backend.ts +6 -0
- package/libraries/postfx/src/post-process-stack.ts +1 -2
- package/libraries/sand/src/materials.ts +2 -32
- package/libraries/sand/src/sand-world.ts +0 -4
- package/modules/devtools/README.md +1 -3
- package/modules/devtools/src/api.ts +10 -34
- package/modules/devtools/src/data-bridge.ts +0 -16
- package/modules/devtools/src/index.ts +1 -11
- package/modules/devtools/src/init.ts +8 -51
- package/modules/devtools/src/scene-inspector.ts +2 -6
- package/modules/devtools/src/types.ts +5 -90
- package/modules/devtools/src/worker-sync.ts +3 -13
- package/package.json +4 -5
- package/test/src/vision-test.ts +2 -4
- package/core/src/render/sab-canvas-overlay.ts +0 -122
- package/dist/core/src/render/sab-canvas-overlay.js +0 -83
- package/dist/modules/devtools/src/debug-overlay.js +0 -181
- package/dist/modules/devtools/src/doctor-panel.js +0 -163
- package/dist/modules/devtools/src/label-overlay.js +0 -129
- package/dist/modules/devtools/src/material-stats-panel.js +0 -171
- package/dist/modules/devtools/src/sim-stats-panel.js +0 -250
- package/modules/devtools/src/debug-overlay.ts +0 -238
- package/modules/devtools/src/doctor-panel.ts +0 -182
- package/modules/devtools/src/label-overlay.ts +0 -154
- package/modules/devtools/src/material-stats-panel.ts +0 -188
- package/modules/devtools/src/sim-stats-panel.ts +0 -272
|
@@ -132,15 +132,11 @@ export async function bootstrapGame(opts) {
|
|
|
132
132
|
return;
|
|
133
133
|
const { initDevTools } = devtoolsMod;
|
|
134
134
|
const simStatsProvider = opts.devtools.createSimStatsProvider?.(renderer);
|
|
135
|
-
const panels = typeof opts.devtools.panels === "function"
|
|
136
|
-
? opts.devtools.panels(renderer)
|
|
137
|
-
: opts.devtools.panels ?? [];
|
|
138
135
|
const workerHosts = typeof opts.devtools.workerHosts === "function"
|
|
139
136
|
? opts.devtools.workerHosts(renderer)
|
|
140
137
|
: opts.devtools.workerHosts;
|
|
141
138
|
const devtoolsBridge = await importOrDead(initDevTools(renderer, {
|
|
142
139
|
simStatsProvider,
|
|
143
|
-
panels,
|
|
144
140
|
sceneInspector: opts.devtools.sceneInspector,
|
|
145
141
|
bridgeClass: opts.devtools.bridgeClass,
|
|
146
142
|
workerHosts,
|
|
@@ -82,22 +82,13 @@ export function collectRendererFeatureLog(opts) {
|
|
|
82
82
|
data.mem = `${nav.deviceMemory}G`;
|
|
83
83
|
}
|
|
84
84
|
catch { /* navigator unavailable */ }
|
|
85
|
-
// --- SharedArrayBuffer
|
|
86
|
-
// there is no origin model on the native host) ---
|
|
85
|
+
// --- SharedArrayBuffer ---
|
|
87
86
|
try {
|
|
88
87
|
data.sab = typeof SharedArrayBuffer !== "undefined" ? 1 : 0;
|
|
89
88
|
}
|
|
90
89
|
catch {
|
|
91
90
|
data.sab = 0;
|
|
92
91
|
}
|
|
93
|
-
if (getHostCapabilities().hasDom) {
|
|
94
|
-
try {
|
|
95
|
-
data.coi = self.crossOriginIsolated ? 1 : 0;
|
|
96
|
-
}
|
|
97
|
-
catch {
|
|
98
|
-
data.coi = 0;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
92
|
// --- Worker load status (best-effort; na = not checked) ---
|
|
102
93
|
data.wk = "na";
|
|
103
94
|
// --- Active plugins ---
|
|
@@ -90,17 +90,14 @@ export function createMcpHarness(opts) {
|
|
|
90
90
|
// These utilities are used by multiple games' MCP automation harnesses.
|
|
91
91
|
// Games import them instead of duplicating the implementation.
|
|
92
92
|
/** Convert a Blob to a base64 string (without the data: prefix). */
|
|
93
|
-
export function blobToBase64(blob) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
reader.onerror = reject;
|
|
102
|
-
reader.readAsDataURL(blob);
|
|
103
|
-
});
|
|
93
|
+
export async function blobToBase64(blob) {
|
|
94
|
+
const bytes = new Uint8Array(await blob.arrayBuffer());
|
|
95
|
+
let bin = "";
|
|
96
|
+
const CHUNK = 0x8000;
|
|
97
|
+
for (let i = 0; i < bytes.length; i += CHUNK) {
|
|
98
|
+
bin += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
|
|
99
|
+
}
|
|
100
|
+
return btoa(bin);
|
|
104
101
|
}
|
|
105
102
|
/** MCP result helper: wrap data as a JSON content array. */
|
|
106
103
|
export function jsonResult(data) {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
// }),
|
|
26
26
|
// });
|
|
27
27
|
// ============================================================================
|
|
28
|
-
import {
|
|
28
|
+
import { KEY } from "@downdraft/engine";
|
|
29
29
|
import { createLogger } from "@downdraft/engine/util/logger";
|
|
30
30
|
import { downdraft } from "./index.js";
|
|
31
31
|
import { blobToBase64, createMcpHarness, errorResult, jsonResult } from "./mcp-harness.js";
|
|
@@ -290,21 +290,13 @@ export function createStandardAutomationTools(ctx) {
|
|
|
290
290
|
const rect = surface.getBoundingClientRect();
|
|
291
291
|
const clientX = rect.left + x;
|
|
292
292
|
const clientY = rect.top + y;
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
const target = hasDom
|
|
298
|
-
? (document.elementFromPoint(clientX, clientY) ?? surface)
|
|
299
|
-
: surface;
|
|
300
|
-
// MiniEventTarget (native event polyfill) has no capture/bubble —
|
|
301
|
-
// mirror real propagation by also dispatching on window. In a real
|
|
302
|
-
// DOM this would double-fire, so only do it under the polyfill.
|
|
303
|
-
const isNativeDom = !hasDom;
|
|
293
|
+
// There is no DOM tree — the surface itself is the input target.
|
|
294
|
+
const target = surface;
|
|
295
|
+
// MiniEventTarget (native event target) has no capture/bubble —
|
|
296
|
+
// mirror DOM propagation by also dispatching on window.
|
|
304
297
|
const fire = (ev) => {
|
|
305
298
|
target.dispatchEvent(ev);
|
|
306
|
-
|
|
307
|
-
window.dispatchEvent?.(ev);
|
|
299
|
+
window.dispatchEvent?.(ev);
|
|
308
300
|
};
|
|
309
301
|
if (type === "wheel") {
|
|
310
302
|
const deltaX = params.deltaX ?? 0;
|
|
@@ -487,130 +479,6 @@ export function createStandardAutomationTools(ctx) {
|
|
|
487
479
|
handler: () => jsonResult(ctx.getUiState()),
|
|
488
480
|
});
|
|
489
481
|
}
|
|
490
|
-
// ── get_element_bounds ──
|
|
491
|
-
if (want("get_element_bounds")) {
|
|
492
|
-
tools.push({
|
|
493
|
-
def: {
|
|
494
|
-
name: "get_element_bounds",
|
|
495
|
-
description: "Get the bounding box of a DOM element matching a CSS selector. Returns {x, y, width, height, top, left, bottom, right} or found:false.",
|
|
496
|
-
inputSchema: {
|
|
497
|
-
type: "object",
|
|
498
|
-
properties: { selector: { type: "string", description: "CSS selector" } },
|
|
499
|
-
required: ["selector"],
|
|
500
|
-
},
|
|
501
|
-
},
|
|
502
|
-
handler: (params) => {
|
|
503
|
-
const selector = params.selector;
|
|
504
|
-
try {
|
|
505
|
-
const el = document.querySelector(selector);
|
|
506
|
-
if (!el)
|
|
507
|
-
return jsonResult({ found: false, selector });
|
|
508
|
-
const rect = el.getBoundingClientRect();
|
|
509
|
-
return jsonResult({
|
|
510
|
-
found: true, selector,
|
|
511
|
-
x: rect.x, y: rect.y, width: rect.width, height: rect.height,
|
|
512
|
-
top: rect.top, left: rect.left, bottom: rect.bottom, right: rect.right,
|
|
513
|
-
});
|
|
514
|
-
}
|
|
515
|
-
catch (e) {
|
|
516
|
-
return errorResult(`Failed to query selector "${selector}": ${e.message}`);
|
|
517
|
-
}
|
|
518
|
-
},
|
|
519
|
-
});
|
|
520
|
-
}
|
|
521
|
-
// ── inspect_dom ──
|
|
522
|
-
if (want("inspect_dom")) {
|
|
523
|
-
tools.push({
|
|
524
|
-
def: {
|
|
525
|
-
name: "inspect_dom",
|
|
526
|
-
description: "Inspect the main thread's DOM for debugging CSS/layout issues. 'stylesheets' lists all <style>/<link> tags; 'element' gets details for a selector.",
|
|
527
|
-
inputSchema: {
|
|
528
|
-
type: "object",
|
|
529
|
-
properties: {
|
|
530
|
-
action: { type: "string", enum: ["stylesheets", "element"] },
|
|
531
|
-
selector: { type: "string", description: "CSS selector (for action='element')" },
|
|
532
|
-
},
|
|
533
|
-
required: ["action"],
|
|
534
|
-
},
|
|
535
|
-
},
|
|
536
|
-
handler: (params) => {
|
|
537
|
-
if (!getHostCapabilities().hasDom) {
|
|
538
|
-
return errorResult("inspect_dom requires a real DOM — the native host's document is a canvas-compat facade, not a DOM tree");
|
|
539
|
-
}
|
|
540
|
-
const action = params.action;
|
|
541
|
-
if (action === "stylesheets") {
|
|
542
|
-
const styles = Array.from(document.querySelectorAll("style, link[rel='stylesheet']")).map((el) => {
|
|
543
|
-
const tag = el.tagName.toLowerCase();
|
|
544
|
-
if (tag === "style") {
|
|
545
|
-
const text = el.textContent ?? "";
|
|
546
|
-
return { tag, length: text.length, preview: text.slice(0, 200) };
|
|
547
|
-
}
|
|
548
|
-
return { tag, href: el.href };
|
|
549
|
-
});
|
|
550
|
-
return jsonResult({ stylesheets: styles, count: styles.length });
|
|
551
|
-
}
|
|
552
|
-
if (action === "element" && params.selector) {
|
|
553
|
-
const el = document.querySelector(params.selector);
|
|
554
|
-
if (!el)
|
|
555
|
-
return jsonResult({ found: false, selector: params.selector });
|
|
556
|
-
const cs = getComputedStyle(el);
|
|
557
|
-
return jsonResult({
|
|
558
|
-
found: true,
|
|
559
|
-
selector: params.selector,
|
|
560
|
-
tagName: el.tagName,
|
|
561
|
-
className: el.className,
|
|
562
|
-
id: el.id,
|
|
563
|
-
style: {
|
|
564
|
-
width: cs.width, height: cs.height,
|
|
565
|
-
maxWidth: cs.maxWidth, maxHeight: cs.maxHeight,
|
|
566
|
-
display: cs.display, position: cs.position,
|
|
567
|
-
top: cs.top, left: cs.left, overflow: cs.overflow,
|
|
568
|
-
},
|
|
569
|
-
bounds: el.getBoundingClientRect().toJSON(),
|
|
570
|
-
childCount: el.children.length,
|
|
571
|
-
});
|
|
572
|
-
}
|
|
573
|
-
return errorResult("Invalid action or missing selector");
|
|
574
|
-
},
|
|
575
|
-
});
|
|
576
|
-
}
|
|
577
|
-
// ── get_element_style ──
|
|
578
|
-
if (want("get_element_style")) {
|
|
579
|
-
tools.push({
|
|
580
|
-
def: {
|
|
581
|
-
name: "get_element_style",
|
|
582
|
-
description: "Get computed style of a DOM element matching a CSS selector. Pass properties[] to read specific CSS properties.",
|
|
583
|
-
inputSchema: {
|
|
584
|
-
type: "object",
|
|
585
|
-
properties: {
|
|
586
|
-
selector: { type: "string", description: "CSS selector" },
|
|
587
|
-
properties: { type: "array", items: { type: "string" }, description: "CSS properties to read" },
|
|
588
|
-
},
|
|
589
|
-
required: ["selector"],
|
|
590
|
-
},
|
|
591
|
-
},
|
|
592
|
-
handler: (params) => {
|
|
593
|
-
if (!getHostCapabilities().hasDom) {
|
|
594
|
-
return errorResult("get_element_style requires a real DOM — no computed styles on the native host");
|
|
595
|
-
}
|
|
596
|
-
const selector = params.selector;
|
|
597
|
-
const props = params.properties ?? [];
|
|
598
|
-
try {
|
|
599
|
-
const el = document.querySelector(selector);
|
|
600
|
-
if (!el)
|
|
601
|
-
return jsonResult({ found: false, selector });
|
|
602
|
-
const cs = getComputedStyle(el);
|
|
603
|
-
const result = { found: true, selector };
|
|
604
|
-
props.forEach((p) => { result[p] = cs.getPropertyValue(p); ; });
|
|
605
|
-
result._className = el.className;
|
|
606
|
-
return jsonResult(result);
|
|
607
|
-
}
|
|
608
|
-
catch (e) {
|
|
609
|
-
return errorResult(`Failed to query style for "${selector}": ${e.message}`);
|
|
610
|
-
}
|
|
611
|
-
},
|
|
612
|
-
});
|
|
613
|
-
}
|
|
614
482
|
// ── inspectable_list / inspectable_get / inspectable_set ──
|
|
615
483
|
const inspectables = ctx.inspectables;
|
|
616
484
|
if (want("inspectable_list") && inspectables) {
|
|
@@ -23,15 +23,15 @@ export class ModuleHost {
|
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
25
25
|
* Inject the DevTools API. Called by the game bootstrap after
|
|
26
|
-
* `initDevTools()` so plugins can self-register
|
|
27
|
-
* `ctx.devtools.
|
|
26
|
+
* `initDevTools()` so plugins can self-register data feeds via
|
|
27
|
+
* `ctx.devtools.registerDataFeed(...)` during their `register()` lifecycle.
|
|
28
28
|
*/
|
|
29
29
|
setDevToolsAPI(api) {
|
|
30
30
|
this._devtools = api;
|
|
31
31
|
}
|
|
32
32
|
get devtools() {
|
|
33
33
|
// Return the injected API, or a no-op stub if not set (so plugins that
|
|
34
|
-
// call ctx.devtools.
|
|
34
|
+
// call ctx.devtools.register*() don't crash if devtools isn't wired).
|
|
35
35
|
return this._devtools ?? NoopDevToolsAPI;
|
|
36
36
|
}
|
|
37
37
|
getRegistry() {
|
|
@@ -439,10 +439,7 @@ export class ModuleHost {
|
|
|
439
439
|
}
|
|
440
440
|
}
|
|
441
441
|
// No-op DevTools API stub — used when setDevToolsAPI() hasn't been called.
|
|
442
|
-
// Plugins that call ctx.devtools.registerPanel() will silently no-op.
|
|
443
442
|
const NoopDevToolsAPI = {
|
|
444
|
-
registerPanel: () => { },
|
|
445
|
-
registerOverlayToggle: () => { },
|
|
446
443
|
registerDataFeed: () => { },
|
|
447
444
|
registerCommand: () => { },
|
|
448
445
|
registerSABStat: () => { },
|
|
@@ -1,20 +1,13 @@
|
|
|
1
1
|
export class CanvasResizeWatcher {
|
|
2
2
|
canvas;
|
|
3
3
|
handler;
|
|
4
|
-
observer = null;
|
|
5
4
|
dpr = 1;
|
|
6
5
|
onSurfaceResize = () => this.resize();
|
|
7
6
|
constructor(canvas, handler) {
|
|
8
7
|
this.canvas = canvas;
|
|
9
8
|
this.handler = handler;
|
|
10
9
|
this.dpr = (typeof window !== "undefined" ? window.devicePixelRatio : 0) || 1;
|
|
11
|
-
//
|
|
12
|
-
// aren't Elements — they push a "resize" event instead. Both paths feed
|
|
13
|
-
// the same resize() so either host drives layout updates.
|
|
14
|
-
if (typeof ResizeObserver !== "undefined") {
|
|
15
|
-
this.observer = new ResizeObserver(() => this.resize());
|
|
16
|
-
this.observer.observe(canvas);
|
|
17
|
-
}
|
|
10
|
+
// Native surfaces push a "resize" event when the backing store changes.
|
|
18
11
|
canvas.addEventListener("resize", this.onSurfaceResize);
|
|
19
12
|
this.resize();
|
|
20
13
|
}
|
|
@@ -29,8 +22,6 @@ export class CanvasResizeWatcher {
|
|
|
29
22
|
return this.dpr;
|
|
30
23
|
}
|
|
31
24
|
destroy() {
|
|
32
|
-
this.observer?.disconnect();
|
|
33
|
-
this.observer = null;
|
|
34
25
|
this.canvas.removeEventListener("resize", this.onSurfaceResize);
|
|
35
26
|
}
|
|
36
27
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// logic through callback hooks.
|
|
7
7
|
// ============================================================================
|
|
8
8
|
import { UIInputRouter } from "../input/ui-router.js";
|
|
9
|
-
import {
|
|
9
|
+
import { getNativeHost } from "../platform/runtime.js";
|
|
10
10
|
import { TelemetryCollector } from "../telemetry/collector.js";
|
|
11
11
|
import { GPUProfiler } from "../telemetry/gpu-profiler.js";
|
|
12
12
|
import { GPUResourceTracker } from "../telemetry/gpu-resource-tracker.js";
|
|
@@ -405,17 +405,15 @@ export class GameRenderer {
|
|
|
405
405
|
*/
|
|
406
406
|
setResolutionScale(scale) {
|
|
407
407
|
this.resolutionScale = Math.max(0.25, Math.min(1, scale));
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
log.warn("game-renderer", "resolutionScale is not supported on the native surface (swapchain must match the window size) — ignoring");
|
|
418
|
-
}
|
|
408
|
+
// On the native host the swapchain extent must equal the window's
|
|
409
|
+
// real size — wgpu compares the configured size against the surface's
|
|
410
|
+
// currentExtent at acquire time and returns Outdated forever when
|
|
411
|
+
// they differ. Shrinking canvas.width/height would also corrupt
|
|
412
|
+
// clientWidth (UI layout + input coords). Resolution scaling needs
|
|
413
|
+
// an offscreen-render + upscale path to work here; for now it is a
|
|
414
|
+
// no-op rather than a brick.
|
|
415
|
+
if (this.resolutionScale < 1) {
|
|
416
|
+
log.warn("game-renderer", "resolutionScale is not supported on the native surface (swapchain must match the window size) — ignoring");
|
|
419
417
|
}
|
|
420
418
|
if (this.lastCssW > 0)
|
|
421
419
|
this.onResize(this.lastCssW, this.lastCssH, this.lastRawDpr);
|
|
@@ -431,7 +429,7 @@ export class GameRenderer {
|
|
|
431
429
|
// performance knob when fill rate is a concern. On native the
|
|
432
430
|
// swapchain is window-sized (see setResolutionScale) so the scale is
|
|
433
431
|
// never applied to the backing store.
|
|
434
|
-
const cappedDpr = dpr
|
|
432
|
+
const cappedDpr = dpr;
|
|
435
433
|
this.dpr = cappedDpr;
|
|
436
434
|
const w = Math.round(cssWidth * cappedDpr);
|
|
437
435
|
const h = Math.round(cssHeight * cappedDpr);
|
|
@@ -777,11 +775,12 @@ export class GameRenderer {
|
|
|
777
775
|
// final image (skip on GPU error to avoid cascade). This must run AFTER
|
|
778
776
|
// afterFrame — 2D games (viewportCount=0) do their custom rendering in
|
|
779
777
|
// the afterFrame callback, so drawing UI earlier would put it underneath
|
|
780
|
-
// their clear pass.
|
|
778
|
+
// their clear pass. The texture comes from peek: a frame the game
|
|
779
|
+
// skipped must not be started just to draw UI on a blank swapchain.
|
|
781
780
|
if (!gpuError && this.device && this.context) {
|
|
782
781
|
const compositors = this.uiCompositorOrder.filter((c) => c.hasContent());
|
|
783
782
|
const uiCanvasView = compositors.length > 0
|
|
784
|
-
? this.
|
|
783
|
+
? this.getFrameSurfaceTexture()?.createView()
|
|
785
784
|
: undefined;
|
|
786
785
|
if (uiCanvasView) {
|
|
787
786
|
const uiEncoder = this.device.createCommandEncoder();
|
|
@@ -793,7 +792,10 @@ export class GameRenderer {
|
|
|
793
792
|
storeOp: "store",
|
|
794
793
|
}],
|
|
795
794
|
});
|
|
796
|
-
|
|
795
|
+
// Panel rects are CSS px — pass the CSS surface size so
|
|
796
|
+
// rect/surface maps correctly on HiDPI displays (canvas.width is
|
|
797
|
+
// physical px; identical where scaleFactor=1).
|
|
798
|
+
compositors.forEach((c) => { c.render(uiPass, this.canvas.clientWidth, this.canvas.clientHeight); });
|
|
797
799
|
uiPass.end();
|
|
798
800
|
// "preUi" slot — worker passes ordered ahead of the UI-composite
|
|
799
801
|
// buffer inside the end-of-frame submit.
|
|
@@ -853,6 +855,29 @@ export class GameRenderer {
|
|
|
853
855
|
return null;
|
|
854
856
|
}
|
|
855
857
|
}
|
|
858
|
+
/**
|
|
859
|
+
* The surface texture already acquired for the in-flight frame, or null
|
|
860
|
+
* when nothing has acquired one yet. End-of-frame overlays (the UI
|
|
861
|
+
* composite) use this so they join a frame rather than start one: on
|
|
862
|
+
* hosts with frame retention (native), acquiring here would fabricate a
|
|
863
|
+
* frame the game deliberately skipped (dirty-tracking) and present the
|
|
864
|
+
* overlay over a blank swapchain image — a full-screen flash. Falls
|
|
865
|
+
* back to acquiring on contexts without peek (DOM semantics:
|
|
866
|
+
* getCurrentTexture is idempotent within a frame and present is
|
|
867
|
+
* implicit, so there is nothing to defeat).
|
|
868
|
+
*/
|
|
869
|
+
getFrameSurfaceTexture() {
|
|
870
|
+
const peek = this.context?.peekCurrentTexture;
|
|
871
|
+
if (typeof peek === "function") {
|
|
872
|
+
try {
|
|
873
|
+
return peek.call(this.context) ?? null;
|
|
874
|
+
}
|
|
875
|
+
catch {
|
|
876
|
+
return null;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
return this.getSurfaceTexture();
|
|
880
|
+
}
|
|
856
881
|
renderViewport(viewportIdx, dt, offscreen) {
|
|
857
882
|
if (!this.device || !this.context)
|
|
858
883
|
return null;
|
|
@@ -1101,7 +1126,7 @@ export class GameRenderer {
|
|
|
1101
1126
|
const compositors = this.uiCompositorOrder.filter((c) => c.hasContent());
|
|
1102
1127
|
if (compositors.length === 0 || !this.device || !this.context)
|
|
1103
1128
|
return;
|
|
1104
|
-
const canvasView = this.
|
|
1129
|
+
const canvasView = this.getFrameSurfaceTexture()?.createView();
|
|
1105
1130
|
if (!canvasView)
|
|
1106
1131
|
return;
|
|
1107
1132
|
const uiEncoder = this.device.createCommandEncoder();
|
|
@@ -1113,7 +1138,8 @@ export class GameRenderer {
|
|
|
1113
1138
|
storeOp: "store",
|
|
1114
1139
|
}],
|
|
1115
1140
|
});
|
|
1116
|
-
|
|
1141
|
+
// Panel rects are CSS px — see the main UI composite block above.
|
|
1142
|
+
compositors.forEach((c) => { c.render(uiPass, this.canvas.clientWidth, this.canvas.clientHeight); });
|
|
1117
1143
|
uiPass.end();
|
|
1118
1144
|
this.device.queue.submit([uiEncoder.finish()]);
|
|
1119
1145
|
}
|
|
@@ -57,7 +57,6 @@ export { PipelineCache } from "./pipeline.js";
|
|
|
57
57
|
export { RenderPass } from "./render-pass.js";
|
|
58
58
|
export { RendererInputBusImpl } from "./renderer-input-bus.js";
|
|
59
59
|
export { RendererModuleHost } from "./renderer-module-host.js";
|
|
60
|
-
export { SabCanvasOverlay } from "./sab-canvas-overlay.js";
|
|
61
60
|
export { clearShaderValidationDedup, createValidatedShaderModule, createValidatedShaderModuleAsync, installShaderValidationGuard } from "./shader-validator.js";
|
|
62
61
|
export { CSM_SHADER_CHUNK, POINT_SHADOW_SHADER_CHUNK, SPOT_SHADOW_SHADER_CHUNK } from "./shaders/shadow-chunks.js";
|
|
63
62
|
export { SkyboxRenderer } from "./skybox.js";
|
|
@@ -58,9 +58,9 @@ export class InputManager {
|
|
|
58
58
|
this.tryLockPointer();
|
|
59
59
|
}
|
|
60
60
|
}));
|
|
61
|
-
add(
|
|
61
|
+
add(this.canvas, "pointerlockchange", (() => {
|
|
62
62
|
const wasLocked = this.pointerLocked;
|
|
63
|
-
this.pointerLocked =
|
|
63
|
+
this.pointerLocked = this.canvas.pointerLocked === true;
|
|
64
64
|
if (this.pointerLocked) {
|
|
65
65
|
this.pointerLockRetryCount = 0;
|
|
66
66
|
if (this.pointerLockRetryTimer) {
|
|
@@ -76,7 +76,7 @@ export class InputManager {
|
|
|
76
76
|
this.mouseDelta.dy = 0;
|
|
77
77
|
}
|
|
78
78
|
}));
|
|
79
|
-
add(
|
|
79
|
+
add(this.canvas, "pointerlockerror", (() => {
|
|
80
80
|
// No action needed — tryLockPointer's fallback timer will retry.
|
|
81
81
|
}));
|
|
82
82
|
}
|
|
@@ -150,10 +150,10 @@ export class InputManager {
|
|
|
150
150
|
e.preventDefault();
|
|
151
151
|
}
|
|
152
152
|
}), { passive: false });
|
|
153
|
-
//
|
|
154
|
-
// sandjongg's tile canvas) would otherwise trigger the canvas's
|
|
155
|
-
// mouseleave and break UI hover/pressed state.
|
|
156
|
-
add(
|
|
153
|
+
// Window-level mouseleave: a second interactive canvas stacked above
|
|
154
|
+
// (e.g. sandjongg's tile canvas) would otherwise trigger the canvas's
|
|
155
|
+
// own mouseleave and break UI hover/pressed state.
|
|
156
|
+
add(window, "mouseleave", (() => {
|
|
157
157
|
this.uiInputRouter?.handlePointerLeave();
|
|
158
158
|
}));
|
|
159
159
|
add(this.canvas, "contextmenu", ((e) => {
|
|
@@ -50,8 +50,8 @@ export class RendererModuleHost {
|
|
|
50
50
|
this.inputBus = new RendererInputBusImpl(canvas);
|
|
51
51
|
}
|
|
52
52
|
/**
|
|
53
|
-
* Inject the DevTools API so renderer plugins can self-register
|
|
54
|
-
*
|
|
53
|
+
* Inject the DevTools API so renderer plugins can self-register data
|
|
54
|
+
* feeds via `ctx.devtools.registerDataFeed(...)` during their `register()`.
|
|
55
55
|
*/
|
|
56
56
|
setDevToolsAPI(api) {
|
|
57
57
|
this._devtools = api;
|
|
@@ -531,8 +531,6 @@ export class RendererModuleHost {
|
|
|
531
531
|
}
|
|
532
532
|
// No-op DevTools API stub — used when setDevToolsAPI() hasn't been called.
|
|
533
533
|
const NoopRendererDevToolsAPI = {
|
|
534
|
-
registerPanel: () => { },
|
|
535
|
-
registerOverlayToggle: () => { },
|
|
536
534
|
registerDataFeed: () => { },
|
|
537
535
|
registerCommand: () => { },
|
|
538
536
|
registerSABStat: () => { },
|
|
@@ -13,46 +13,13 @@
|
|
|
13
13
|
// ============================================================================
|
|
14
14
|
let cachedSystemScale = null;
|
|
15
15
|
/**
|
|
16
|
-
* Detect the system
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* Example: if the user set their browser default font to 20px, this returns
|
|
20
|
-
* 1.25. If the browser default is 16px (normal), this returns 1.0.
|
|
21
|
-
*
|
|
22
|
-
* Must be called on the main thread (requires `document`).
|
|
16
|
+
* Detect the system default font size and return a scale factor relative to
|
|
17
|
+
* 16px. On the native host there is no CSS engine and no system font-size
|
|
18
|
+
* preference to measure — always 1.
|
|
23
19
|
*/
|
|
24
20
|
export function detectSystemFontScale() {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
// On the native host the document is a canvas-compat facade — there is no
|
|
28
|
-
// CSS engine and no getComputedStyle, so there is no "system font size"
|
|
29
|
-
// to measure. Always 1.
|
|
30
|
-
if (typeof document === "undefined" || !document.body || typeof getComputedStyle !== "function") {
|
|
31
|
-
cachedSystemScale = 1;
|
|
32
|
-
return 1;
|
|
33
|
-
}
|
|
34
|
-
const el = document.createElement("div");
|
|
35
|
-
el.style.fontSize = "1rem";
|
|
36
|
-
el.style.position = "absolute";
|
|
37
|
-
el.style.visibility = "hidden";
|
|
38
|
-
el.style.lineHeight = "0";
|
|
39
|
-
el.style.margin = "0";
|
|
40
|
-
el.style.padding = "0";
|
|
41
|
-
document.body.appendChild(el);
|
|
42
|
-
let scale = 1;
|
|
43
|
-
try {
|
|
44
|
-
const size = parseFloat(getComputedStyle(el).fontSize);
|
|
45
|
-
// 16px is the browser default. Only scale up (never down).
|
|
46
|
-
scale = size > 0 ? Math.max(1, size / 16) : 1;
|
|
47
|
-
}
|
|
48
|
-
catch {
|
|
49
|
-
scale = 1;
|
|
50
|
-
}
|
|
51
|
-
finally {
|
|
52
|
-
document.body.removeChild(el);
|
|
53
|
-
}
|
|
54
|
-
cachedSystemScale = scale;
|
|
55
|
-
return scale;
|
|
21
|
+
cachedSystemScale = 1;
|
|
22
|
+
return 1;
|
|
56
23
|
}
|
|
57
24
|
/**
|
|
58
25
|
* Cached accessor for the system font scale. After the first call to
|
|
@@ -55,6 +55,54 @@ export function getWorkerHost() {
|
|
|
55
55
|
},
|
|
56
56
|
};
|
|
57
57
|
}
|
|
58
|
+
// --- Event channel stats ---
|
|
59
|
+
// Cheap counters the DOWNDRAFT_EVENT_STATS logger (and QA tooling) read to
|
|
60
|
+
// verify batching is actually engaging: emitted events vs postMessage posts
|
|
61
|
+
// on the worker side, messages vs delivered events on the host side.
|
|
62
|
+
const eventChannelStats = {
|
|
63
|
+
/** Worker side: emit() calls. */
|
|
64
|
+
emitted: 0,
|
|
65
|
+
/** Worker side: postMessage calls carrying events (batch or single). */
|
|
66
|
+
posts: 0,
|
|
67
|
+
/** Host side: messages received carrying events. */
|
|
68
|
+
messages: 0,
|
|
69
|
+
/** Host side: events delivered to listeners. */
|
|
70
|
+
events: 0,
|
|
71
|
+
/** Host side: largest observed __eventBatch. */
|
|
72
|
+
maxBatch: 0,
|
|
73
|
+
};
|
|
74
|
+
export function getEventChannelStats() {
|
|
75
|
+
return eventChannelStats;
|
|
76
|
+
}
|
|
77
|
+
function envFlag(name) {
|
|
78
|
+
try {
|
|
79
|
+
return (globalThis.process?.env?.[name] === "1");
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
let statsLoggerStarted = false;
|
|
86
|
+
function startEventStatsLogger() {
|
|
87
|
+
if (statsLoggerStarted || !envFlag("DOWNDRAFT_EVENT_STATS"))
|
|
88
|
+
return;
|
|
89
|
+
statsLoggerStarted = true;
|
|
90
|
+
let prev = { emitted: 0, posts: 0, messages: 0, events: 0 };
|
|
91
|
+
const t = setInterval(() => {
|
|
92
|
+
const s = eventChannelStats;
|
|
93
|
+
const d = {
|
|
94
|
+
emitted: s.emitted - prev.emitted,
|
|
95
|
+
posts: s.posts - prev.posts,
|
|
96
|
+
messages: s.messages - prev.messages,
|
|
97
|
+
events: s.events - prev.events,
|
|
98
|
+
};
|
|
99
|
+
prev = { emitted: s.emitted, posts: s.posts, messages: s.messages, events: s.events };
|
|
100
|
+
if (d.emitted || d.posts || d.messages || d.events) {
|
|
101
|
+
log.info("rpc-events", `5s Δ emitted=${d.emitted} posts=${d.posts} msgs=${d.messages} events=${d.events} maxBatch=${s.maxBatch}`);
|
|
102
|
+
}
|
|
103
|
+
}, 5000);
|
|
104
|
+
t.unref?.();
|
|
105
|
+
}
|
|
58
106
|
function isRpcRequest(msg) {
|
|
59
107
|
return msg && msg.__rpc === true && typeof msg.method === "string";
|
|
60
108
|
}
|
|
@@ -88,8 +136,11 @@ export function expose(api, options) {
|
|
|
88
136
|
}
|
|
89
137
|
export function exposeEvents(host) {
|
|
90
138
|
const h = host ?? getWorkerHost();
|
|
139
|
+
startEventStatsLogger();
|
|
91
140
|
return {
|
|
92
141
|
emit: (kind, data) => {
|
|
142
|
+
eventChannelStats.emitted++;
|
|
143
|
+
eventChannelStats.posts++;
|
|
93
144
|
h.postToHost({ __event: true, kind, data });
|
|
94
145
|
},
|
|
95
146
|
};
|
|
@@ -105,17 +156,20 @@ export function exposeEvents(host) {
|
|
|
105
156
|
*/
|
|
106
157
|
export function exposeBatchedEvents(host) {
|
|
107
158
|
const h = host ?? getWorkerHost();
|
|
159
|
+
startEventStatsLogger();
|
|
108
160
|
let queue = [];
|
|
109
161
|
let flushScheduled = false;
|
|
110
162
|
const flush = () => {
|
|
111
163
|
flushScheduled = false;
|
|
112
164
|
if (queue.length === 0)
|
|
113
165
|
return;
|
|
166
|
+
eventChannelStats.posts++;
|
|
114
167
|
h.postToHost({ __eventBatch: true, events: queue });
|
|
115
168
|
queue = [];
|
|
116
169
|
};
|
|
117
170
|
return {
|
|
118
171
|
emit: (kind, data) => {
|
|
172
|
+
eventChannelStats.emitted++;
|
|
119
173
|
queue.push({ kind, data });
|
|
120
174
|
if (!flushScheduled) {
|
|
121
175
|
flushScheduled = true;
|
|
@@ -127,6 +181,7 @@ export function exposeBatchedEvents(host) {
|
|
|
127
181
|
}
|
|
128
182
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
129
183
|
export function wrap(worker, options) {
|
|
184
|
+
startEventStatsLogger();
|
|
130
185
|
const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
131
186
|
// A worker nobody terminates must not pin the runtime after the host's
|
|
132
187
|
// window closes — in-flight RPCs still hold the loop via their timeout.
|
|
@@ -151,6 +206,10 @@ export function wrap(worker, options) {
|
|
|
151
206
|
}
|
|
152
207
|
if (isEventMessage(msg) || isEventBatchMessage(msg)) {
|
|
153
208
|
const events = isEventBatchMessage(msg) ? msg.events : [msg];
|
|
209
|
+
eventChannelStats.messages++;
|
|
210
|
+
eventChannelStats.events += events.length;
|
|
211
|
+
if (events.length > eventChannelStats.maxBatch)
|
|
212
|
+
eventChannelStats.maxBatch = events.length;
|
|
154
213
|
for (const ev of events) {
|
|
155
214
|
for (const cb of eventListeners.values()) {
|
|
156
215
|
try {
|
|
@@ -116,7 +116,14 @@ export function createBlitzUiNativeModule(options) {
|
|
|
116
116
|
const device = ctx.getDevice();
|
|
117
117
|
const view = ensureTexture(device);
|
|
118
118
|
const context = ctx.getSurface().getContext("webgpu");
|
|
119
|
-
|
|
119
|
+
// Join the in-flight frame, never start one: on a frame the
|
|
120
|
+
// game skipped (dirty-tracking), acquiring the surface here
|
|
121
|
+
// would present the UI blit over a blank swapchain image.
|
|
122
|
+
const peek = context
|
|
123
|
+
?.peekCurrentTexture;
|
|
124
|
+
const targetTex = typeof peek === "function"
|
|
125
|
+
? peek.call(context)
|
|
126
|
+
: context?.getCurrentTexture() ?? null;
|
|
120
127
|
const target = targetTex?.createView();
|
|
121
128
|
if (!view || !target || !targetTex)
|
|
122
129
|
return;
|