@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
|
@@ -111,8 +111,6 @@ export interface BootstrapDevToolsOptions {
|
|
|
111
111
|
* Called after the renderer is initialized.
|
|
112
112
|
*/
|
|
113
113
|
createSimStatsProvider?: (renderer: any) => any;
|
|
114
|
-
/** DevTools panels (game-specific). Can be a factory or static array. */
|
|
115
|
-
panels?: any[] | ((renderer: any) => any[]);
|
|
116
114
|
/** Whether to enable scene inspector. Default: false. */
|
|
117
115
|
sceneInspector?: boolean;
|
|
118
116
|
/** Custom scene-inspector bridge class (extends BaseSceneInspector).
|
|
@@ -236,15 +234,11 @@ export async function bootstrapGame(opts: BootstrapGameOptions): Promise<void> {
|
|
|
236
234
|
if (!devtoolsMod) return;
|
|
237
235
|
const { initDevTools } = devtoolsMod;
|
|
238
236
|
const simStatsProvider = opts.devtools.createSimStatsProvider?.(renderer);
|
|
239
|
-
const panels = typeof opts.devtools.panels === "function"
|
|
240
|
-
? opts.devtools.panels(renderer)
|
|
241
|
-
: opts.devtools.panels ?? [];
|
|
242
237
|
const workerHosts = typeof opts.devtools.workerHosts === "function"
|
|
243
238
|
? opts.devtools.workerHosts(renderer)
|
|
244
239
|
: opts.devtools.workerHosts;
|
|
245
240
|
const devtoolsBridge = await importOrDead(initDevTools(renderer, {
|
|
246
241
|
simStatsProvider,
|
|
247
|
-
panels,
|
|
248
242
|
sceneInspector: opts.devtools.sceneInspector,
|
|
249
243
|
bridgeClass: opts.devtools.bridgeClass,
|
|
250
244
|
workerHosts,
|
|
@@ -86,20 +86,12 @@ export function collectRendererFeatureLog(opts: RendererFeatureLogOptions): Feat
|
|
|
86
86
|
if (nav?.deviceMemory) data.mem = `${nav.deviceMemory}G`;
|
|
87
87
|
} catch { /* navigator unavailable */ }
|
|
88
88
|
|
|
89
|
-
// --- SharedArrayBuffer
|
|
90
|
-
// there is no origin model on the native host) ---
|
|
89
|
+
// --- SharedArrayBuffer ---
|
|
91
90
|
try {
|
|
92
91
|
data.sab = typeof SharedArrayBuffer !== "undefined" ? 1 : 0;
|
|
93
92
|
} catch {
|
|
94
93
|
data.sab = 0;
|
|
95
94
|
}
|
|
96
|
-
if (getHostCapabilities().hasDom) {
|
|
97
|
-
try {
|
|
98
|
-
data.coi = (self as any).crossOriginIsolated ? 1 : 0;
|
|
99
|
-
} catch {
|
|
100
|
-
data.coi = 0;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
95
|
|
|
104
96
|
// --- Worker load status (best-effort; na = not checked) ---
|
|
105
97
|
data.wk = "na";
|
|
@@ -120,17 +120,14 @@ export function createMcpHarness(opts: McpHarnessOptions): void {
|
|
|
120
120
|
// Games import them instead of duplicating the implementation.
|
|
121
121
|
|
|
122
122
|
/** Convert a Blob to a base64 string (without the data: prefix). */
|
|
123
|
-
export function blobToBase64(blob: Blob): Promise<string> {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
reader.onerror = reject;
|
|
132
|
-
reader.readAsDataURL(blob);
|
|
133
|
-
});
|
|
123
|
+
export async function blobToBase64(blob: Blob): Promise<string> {
|
|
124
|
+
const bytes = new Uint8Array(await blob.arrayBuffer());
|
|
125
|
+
let bin = "";
|
|
126
|
+
const CHUNK = 0x8000;
|
|
127
|
+
for (let i = 0; i < bytes.length; i += CHUNK) {
|
|
128
|
+
bin += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
|
|
129
|
+
}
|
|
130
|
+
return btoa(bin);
|
|
134
131
|
}
|
|
135
132
|
|
|
136
133
|
/** MCP result helper: wrap data as a JSON content array. */
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
// });
|
|
27
27
|
// ============================================================================
|
|
28
28
|
|
|
29
|
-
import {
|
|
29
|
+
import { KEY, type RenderSurface } from "@downdraft/engine";
|
|
30
30
|
import type { InspectableRegistry } from "@downdraft/engine/libraries/inspectable";
|
|
31
31
|
import { createLogger } from "@downdraft/engine/util/logger";
|
|
32
32
|
import { downdraft } from "./index";
|
|
@@ -108,14 +108,6 @@ export interface StandardAutomationContext {
|
|
|
108
108
|
/** The game render surface (layer 0). Used by capture_screenshot + dispatch_click. */
|
|
109
109
|
surface: RenderSurface | (() => RenderSurface | null);
|
|
110
110
|
|
|
111
|
-
/**
|
|
112
|
-
* Extra canvases to composite between the game canvas and the DOM overlay
|
|
113
|
-
* in full-page screenshots (e.g. a 2D tile canvas). DOM hosts only —
|
|
114
|
-
* native capture returns the composited swapchain via captureFrame().
|
|
115
|
-
* Default: none.
|
|
116
|
-
*/
|
|
117
|
-
extraLayers?: () => HTMLCanvasElement[];
|
|
118
|
-
|
|
119
111
|
/**
|
|
120
112
|
* Render one frame when the render loop is paused (deterministic mode), so
|
|
121
113
|
* screenshots reflect current state. Called by capture_screenshot when the
|
|
@@ -422,20 +414,13 @@ export function createStandardAutomationTools(ctx: StandardAutomationContext): M
|
|
|
422
414
|
const rect = surface.getBoundingClientRect();
|
|
423
415
|
const clientX = rect.left + x;
|
|
424
416
|
const clientY = rect.top + y;
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
//
|
|
428
|
-
//
|
|
429
|
-
const target = hasDom
|
|
430
|
-
? (document.elementFromPoint(clientX, clientY) ?? (surface as unknown as HTMLElement))
|
|
431
|
-
: (surface as unknown as EventTarget);
|
|
432
|
-
// MiniEventTarget (native event polyfill) has no capture/bubble —
|
|
433
|
-
// mirror real propagation by also dispatching on window. In a real
|
|
434
|
-
// DOM this would double-fire, so only do it under the polyfill.
|
|
435
|
-
const isNativeDom = !hasDom;
|
|
417
|
+
// There is no DOM tree — the surface itself is the input target.
|
|
418
|
+
const target = surface as unknown as EventTarget;
|
|
419
|
+
// MiniEventTarget (native event target) has no capture/bubble —
|
|
420
|
+
// mirror DOM propagation by also dispatching on window.
|
|
436
421
|
const fire = (ev: Event) => {
|
|
437
422
|
target.dispatchEvent(ev);
|
|
438
|
-
|
|
423
|
+
(window as any).dispatchEvent?.(ev);
|
|
439
424
|
};
|
|
440
425
|
if (type === "wheel") {
|
|
441
426
|
const deltaX = (params.deltaX as number) ?? 0;
|
|
@@ -474,10 +459,10 @@ export function createStandardAutomationTools(ctx: StandardAutomationContext): M
|
|
|
474
459
|
if (pointerType) fireOne(pointerType, true, buttons);
|
|
475
460
|
fireOne(type, false, buttons);
|
|
476
461
|
}
|
|
477
|
-
const targetEl = target as
|
|
462
|
+
const targetEl = target as { tagName?: string; id?: string; className?: unknown };
|
|
478
463
|
return jsonResult({
|
|
479
464
|
dispatched: true, x, y, type,
|
|
480
|
-
targetTag: targetEl.tagName ??
|
|
465
|
+
targetTag: targetEl.tagName ?? targetEl.id ?? "surface",
|
|
481
466
|
targetClass: targetEl.className?.toString().slice(0, 80),
|
|
482
467
|
});
|
|
483
468
|
},
|
|
@@ -617,128 +602,6 @@ export function createStandardAutomationTools(ctx: StandardAutomationContext): M
|
|
|
617
602
|
});
|
|
618
603
|
}
|
|
619
604
|
|
|
620
|
-
// ── get_element_bounds ──
|
|
621
|
-
if (want("get_element_bounds")) {
|
|
622
|
-
tools.push({
|
|
623
|
-
def: {
|
|
624
|
-
name: "get_element_bounds",
|
|
625
|
-
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.",
|
|
626
|
-
inputSchema: {
|
|
627
|
-
type: "object",
|
|
628
|
-
properties: { selector: { type: "string", description: "CSS selector" } },
|
|
629
|
-
required: ["selector"],
|
|
630
|
-
},
|
|
631
|
-
},
|
|
632
|
-
handler: (params: Record<string, unknown>) => {
|
|
633
|
-
const selector = params.selector as string;
|
|
634
|
-
try {
|
|
635
|
-
const el = document.querySelector(selector);
|
|
636
|
-
if (!el) return jsonResult({ found: false, selector });
|
|
637
|
-
const rect = el.getBoundingClientRect();
|
|
638
|
-
return jsonResult({
|
|
639
|
-
found: true, selector,
|
|
640
|
-
x: rect.x, y: rect.y, width: rect.width, height: rect.height,
|
|
641
|
-
top: rect.top, left: rect.left, bottom: rect.bottom, right: rect.right,
|
|
642
|
-
});
|
|
643
|
-
} catch (e) {
|
|
644
|
-
return errorResult(`Failed to query selector "${selector}": ${(e as Error).message}`);
|
|
645
|
-
}
|
|
646
|
-
},
|
|
647
|
-
});
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
// ── inspect_dom ──
|
|
651
|
-
if (want("inspect_dom")) {
|
|
652
|
-
tools.push({
|
|
653
|
-
def: {
|
|
654
|
-
name: "inspect_dom",
|
|
655
|
-
description: "Inspect the main thread's DOM for debugging CSS/layout issues. 'stylesheets' lists all <style>/<link> tags; 'element' gets details for a selector.",
|
|
656
|
-
inputSchema: {
|
|
657
|
-
type: "object",
|
|
658
|
-
properties: {
|
|
659
|
-
action: { type: "string", enum: ["stylesheets", "element"] },
|
|
660
|
-
selector: { type: "string", description: "CSS selector (for action='element')" },
|
|
661
|
-
},
|
|
662
|
-
required: ["action"],
|
|
663
|
-
},
|
|
664
|
-
},
|
|
665
|
-
handler: (params: Record<string, unknown>) => {
|
|
666
|
-
if (!getHostCapabilities().hasDom) {
|
|
667
|
-
return errorResult("inspect_dom requires a real DOM — the native host's document is a canvas-compat facade, not a DOM tree");
|
|
668
|
-
}
|
|
669
|
-
const action = params.action as string;
|
|
670
|
-
if (action === "stylesheets") {
|
|
671
|
-
const styles = Array.from(document.querySelectorAll("style, link[rel='stylesheet']")).map((el) => {
|
|
672
|
-
const tag = el.tagName.toLowerCase();
|
|
673
|
-
if (tag === "style") {
|
|
674
|
-
const text = el.textContent ?? "";
|
|
675
|
-
return { tag, length: text.length, preview: text.slice(0, 200) };
|
|
676
|
-
}
|
|
677
|
-
return { tag, href: (el as HTMLLinkElement).href };
|
|
678
|
-
});
|
|
679
|
-
return jsonResult({ stylesheets: styles, count: styles.length });
|
|
680
|
-
}
|
|
681
|
-
if (action === "element" && params.selector) {
|
|
682
|
-
const el = document.querySelector(params.selector as string);
|
|
683
|
-
if (!el) return jsonResult({ found: false, selector: params.selector });
|
|
684
|
-
const cs = getComputedStyle(el);
|
|
685
|
-
return jsonResult({
|
|
686
|
-
found: true,
|
|
687
|
-
selector: params.selector,
|
|
688
|
-
tagName: el.tagName,
|
|
689
|
-
className: el.className,
|
|
690
|
-
id: el.id,
|
|
691
|
-
style: {
|
|
692
|
-
width: cs.width, height: cs.height,
|
|
693
|
-
maxWidth: cs.maxWidth, maxHeight: cs.maxHeight,
|
|
694
|
-
display: cs.display, position: cs.position,
|
|
695
|
-
top: cs.top, left: cs.left, overflow: cs.overflow,
|
|
696
|
-
},
|
|
697
|
-
bounds: el.getBoundingClientRect().toJSON(),
|
|
698
|
-
childCount: el.children.length,
|
|
699
|
-
});
|
|
700
|
-
}
|
|
701
|
-
return errorResult("Invalid action or missing selector");
|
|
702
|
-
},
|
|
703
|
-
});
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
// ── get_element_style ──
|
|
707
|
-
if (want("get_element_style")) {
|
|
708
|
-
tools.push({
|
|
709
|
-
def: {
|
|
710
|
-
name: "get_element_style",
|
|
711
|
-
description: "Get computed style of a DOM element matching a CSS selector. Pass properties[] to read specific CSS properties.",
|
|
712
|
-
inputSchema: {
|
|
713
|
-
type: "object",
|
|
714
|
-
properties: {
|
|
715
|
-
selector: { type: "string", description: "CSS selector" },
|
|
716
|
-
properties: { type: "array", items: { type: "string" }, description: "CSS properties to read" },
|
|
717
|
-
},
|
|
718
|
-
required: ["selector"],
|
|
719
|
-
},
|
|
720
|
-
},
|
|
721
|
-
handler: (params: Record<string, unknown>) => {
|
|
722
|
-
if (!getHostCapabilities().hasDom) {
|
|
723
|
-
return errorResult("get_element_style requires a real DOM — no computed styles on the native host");
|
|
724
|
-
}
|
|
725
|
-
const selector = params.selector as string;
|
|
726
|
-
const props = (params.properties as string[]) ?? [];
|
|
727
|
-
try {
|
|
728
|
-
const el = document.querySelector(selector);
|
|
729
|
-
if (!el) return jsonResult({ found: false, selector });
|
|
730
|
-
const cs = getComputedStyle(el);
|
|
731
|
-
const result: Record<string, unknown> = { found: true, selector };
|
|
732
|
-
props.forEach((p) => { result[p] = cs.getPropertyValue(p);; });
|
|
733
|
-
result._className = el.className;
|
|
734
|
-
return jsonResult(result);
|
|
735
|
-
} catch (e) {
|
|
736
|
-
return errorResult(`Failed to query style for "${selector}": ${(e as Error).message}`);
|
|
737
|
-
}
|
|
738
|
-
},
|
|
739
|
-
});
|
|
740
|
-
}
|
|
741
|
-
|
|
742
605
|
// ── inspectable_list / inspectable_get / inspectable_set ──
|
|
743
606
|
const inspectables = ctx.inspectables;
|
|
744
607
|
if (want("inspectable_list") && inspectables) {
|
package/core/src/module/host.ts
CHANGED
|
@@ -47,8 +47,8 @@ export class ModuleHost implements ModuleContext {
|
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
49
|
* Inject the DevTools API. Called by the game bootstrap after
|
|
50
|
-
* `initDevTools()` so plugins can self-register
|
|
51
|
-
* `ctx.devtools.
|
|
50
|
+
* `initDevTools()` so plugins can self-register data feeds via
|
|
51
|
+
* `ctx.devtools.registerDataFeed(...)` during their `register()` lifecycle.
|
|
52
52
|
*/
|
|
53
53
|
setDevToolsAPI(api: ModuleDevToolsAPI): void {
|
|
54
54
|
this._devtools = api;
|
|
@@ -56,7 +56,7 @@ export class ModuleHost implements ModuleContext {
|
|
|
56
56
|
|
|
57
57
|
get devtools(): ModuleDevToolsAPI {
|
|
58
58
|
// Return the injected API, or a no-op stub if not set (so plugins that
|
|
59
|
-
// call ctx.devtools.
|
|
59
|
+
// call ctx.devtools.register*() don't crash if devtools isn't wired).
|
|
60
60
|
return this._devtools ?? NoopDevToolsAPI;
|
|
61
61
|
}
|
|
62
62
|
|
|
@@ -488,10 +488,7 @@ export class ModuleHost implements ModuleContext {
|
|
|
488
488
|
}
|
|
489
489
|
|
|
490
490
|
// No-op DevTools API stub — used when setDevToolsAPI() hasn't been called.
|
|
491
|
-
// Plugins that call ctx.devtools.registerPanel() will silently no-op.
|
|
492
491
|
const NoopDevToolsAPI: ModuleDevToolsAPI = {
|
|
493
|
-
registerPanel: () => {},
|
|
494
|
-
registerOverlayToggle: () => {},
|
|
495
492
|
registerDataFeed: () => {},
|
|
496
493
|
registerCommand: () => {},
|
|
497
494
|
registerSABStat: () => {},
|
package/core/src/module/index.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { buildCrossThreadReport, crossThreadToken } from "./cross-thread";
|
|
|
3
3
|
export type { CrossThreadReport, CrossThreadToken, ModuleThreadInfo, ThreadTag } from "./cross-thread";
|
|
4
4
|
export { DiagnosticError, isStrict, setStrict } from "./diagnostics";
|
|
5
5
|
export { ModuleHost } from "./host";
|
|
6
|
-
export type {
|
|
6
|
+
export type { Module, ModuleContext, ModuleDevToolsAPI, SABChannel } from "./module";
|
|
7
7
|
export { ModuleRegistry } from "./registry";
|
|
8
8
|
export type {
|
|
9
9
|
CameraControllerLike,
|
|
@@ -7,59 +7,16 @@ export interface SABChannel {
|
|
|
7
7
|
buffer: SharedArrayBuffer;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
/**
|
|
11
|
-
* Panel registration payload for `ctx.devtools.registerPanel()`.
|
|
12
|
-
* Structurally compatible with `IDevToolsPanelExtension` from
|
|
13
|
-
* `@downdraft/engine/modules/devtools` — core declares the minimal shape
|
|
14
|
-
* so it doesn't depend on the devtools module (layering).
|
|
15
|
-
*/
|
|
16
|
-
export interface DevToolsPanelRegistration {
|
|
17
|
-
/** Unique id for this extension (e.g. "debug-info", "game-panel"). */
|
|
18
|
-
id: string;
|
|
19
|
-
/** Tab label shown in the tab bar. */
|
|
20
|
-
tabLabel: string;
|
|
21
|
-
/** Tab tooltip. */
|
|
22
|
-
tabTooltip?: string;
|
|
23
|
-
/** Order/priority for tab placement. Core tabs: 0-100, game tabs: 100+. Default: 100. */
|
|
24
|
-
order?: number;
|
|
25
|
-
/** __sceneInspector methods that must exist for this tab to be shown. */
|
|
26
|
-
requiredMethods?: string[];
|
|
27
|
-
/** HTML content for the panel body. */
|
|
28
|
-
html: string;
|
|
29
|
-
/** CSS to inject into the panel document. */
|
|
30
|
-
css?: string;
|
|
31
|
-
/** JS to eval in the panel context. */
|
|
32
|
-
script?: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Overlay toggle registration payload for `ctx.devtools.registerOverlayToggle()`.
|
|
37
|
-
* Structurally compatible with `IDevToolsOverlayToggle` from
|
|
38
|
-
* `@downdraft/engine/modules/devtools`.
|
|
39
|
-
*/
|
|
40
|
-
export interface DevToolsOverlayToggleRegistration {
|
|
41
|
-
/** Unique id (e.g. "chunk-grid", "vel-arrows"). */
|
|
42
|
-
id: string;
|
|
43
|
-
/** Label shown next to the checkbox. */
|
|
44
|
-
label: string;
|
|
45
|
-
/** __sceneInspector methods that must exist for this toggle to be shown. */
|
|
46
|
-
requiredMethods?: string[];
|
|
47
|
-
/** JS to eval in the panel context. */
|
|
48
|
-
script?: string;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
10
|
/**
|
|
52
11
|
* DevTools registration surface exposed on ModuleContext.
|
|
53
|
-
* Plugins call `ctx.devtools.
|
|
54
|
-
* debug
|
|
12
|
+
* Plugins call `ctx.devtools.registerDataFeed(...)` etc. to self-register
|
|
13
|
+
* debug data. The same interface works in both main and worker realms.
|
|
55
14
|
*
|
|
56
15
|
* The actual implementation is provided by @downdraft/engine/modules/devtools via
|
|
57
16
|
* the `devtools` singleton. Core defines the interface; the host injects
|
|
58
17
|
* the concrete object at construction time (see ModuleHost.setDevToolsAPI).
|
|
59
18
|
*/
|
|
60
19
|
export interface ModuleDevToolsAPI {
|
|
61
|
-
registerPanel(ext: DevToolsPanelRegistration): void;
|
|
62
|
-
registerOverlayToggle(toggle: DevToolsOverlayToggleRegistration): void;
|
|
63
20
|
registerDataFeed(name: string, fn: () => unknown, writeRateHz?: number): void;
|
|
64
21
|
registerCommand(name: string, fn: (...args: unknown[]) => unknown): void;
|
|
65
22
|
registerSABStat(name: string, offset: number, type: "u32" | "f32" | "i32"): void;
|
|
@@ -95,8 +52,8 @@ export interface ModuleContext {
|
|
|
95
52
|
registerMigration(fromVersion: number, fn: (data: unknown) => unknown): void;
|
|
96
53
|
onDispose(fn: () => void): void;
|
|
97
54
|
/**
|
|
98
|
-
* DevTools registration surface. Plugins self-register
|
|
99
|
-
*
|
|
55
|
+
* DevTools registration surface. Plugins self-register data feeds and
|
|
56
|
+
* commands via `ctx.devtools.register*(...)`.
|
|
100
57
|
* Works in both sim (worker) and renderer (main) plugin contexts.
|
|
101
58
|
*/
|
|
102
59
|
readonly devtools: ModuleDevToolsAPI;
|
|
@@ -399,6 +399,11 @@ export interface PhysicsBackend {
|
|
|
399
399
|
/** Set integration timestep on a realm. */
|
|
400
400
|
setIntegrationDt(realmId: number, dt: number): void;
|
|
401
401
|
|
|
402
|
+
/** Change a live realm's gravity vector. Optional — backends that can't
|
|
403
|
+
* mutate realm gravity simply omit it (callers fall back to per-body
|
|
404
|
+
* gravityScale). */
|
|
405
|
+
setRealmGravity?(realmId: number, gravity: [number, number, number]): void;
|
|
406
|
+
|
|
402
407
|
/** Per-realm sleep thresholds for graceful degradation (load shedding first line). */
|
|
403
408
|
setSleepThresholds(realmId: number, linearThreshold: number, angularThreshold: number): void;
|
|
404
409
|
/** Per-realm solver iteration count (Rapier `IntegrationParameters.numSolverIterations`). */
|
|
@@ -29,6 +29,15 @@ export interface RenderSurfaceContext {
|
|
|
29
29
|
* the swapchain can't produce a frame (resize in flight, occluded
|
|
30
30
|
* window) — callers must treat null as "skip this frame". */
|
|
31
31
|
getCurrentTexture(): GPUTexture | null;
|
|
32
|
+
/** The texture already acquired for the in-flight frame, if any —
|
|
33
|
+
* WITHOUT acquiring one. Null when nothing has acquired this frame.
|
|
34
|
+
* End-of-frame overlay passes (UI composites) should join a frame via
|
|
35
|
+
* peek rather than acquire: on hosts with frame retention (native),
|
|
36
|
+
* acquiring here fabricates a frame the game deliberately skipped and
|
|
37
|
+
* presents the overlay over a blank swapchain image. Optional — DOM
|
|
38
|
+
* GPUCanvasContext has no equivalent; callers fall back to
|
|
39
|
+
* getCurrentTexture() where acquire is idempotent within a frame. */
|
|
40
|
+
peekCurrentTexture?(): GPUTexture | null;
|
|
32
41
|
}
|
|
33
42
|
|
|
34
43
|
/** Listener signature — the native event target dispatches plain event
|
|
@@ -74,4 +83,8 @@ export interface RenderSurface {
|
|
|
74
83
|
* concept doesn't exist; presence is the feature check. */
|
|
75
84
|
requestPointerLock?(): Promise<void> | void;
|
|
76
85
|
exitPointerLock?(): void;
|
|
86
|
+
/** Current pointer-lock state. Optional — read instead of the DOM's
|
|
87
|
+
* `document.pointerLockElement` (there is no DOM on native). The surface
|
|
88
|
+
* also dispatches a `"pointerlockchange"` event on transitions. */
|
|
89
|
+
readonly pointerLocked?: boolean;
|
|
77
90
|
}
|
|
@@ -5,7 +5,6 @@ export interface CanvasResizeHandler {
|
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
export class CanvasResizeWatcher {
|
|
8
|
-
private observer: ResizeObserver | null = null;
|
|
9
8
|
private dpr = 1;
|
|
10
9
|
private onSurfaceResize = () => this.resize();
|
|
11
10
|
|
|
@@ -14,13 +13,7 @@ export class CanvasResizeWatcher {
|
|
|
14
13
|
private handler: CanvasResizeHandler,
|
|
15
14
|
) {
|
|
16
15
|
this.dpr = (typeof window !== "undefined" ? window.devicePixelRatio : 0) || 1;
|
|
17
|
-
//
|
|
18
|
-
// aren't Elements — they push a "resize" event instead. Both paths feed
|
|
19
|
-
// the same resize() so either host drives layout updates.
|
|
20
|
-
if (typeof ResizeObserver !== "undefined") {
|
|
21
|
-
this.observer = new ResizeObserver(() => this.resize());
|
|
22
|
-
this.observer.observe(canvas as unknown as Element);
|
|
23
|
-
}
|
|
16
|
+
// Native surfaces push a "resize" event when the backing store changes.
|
|
24
17
|
canvas.addEventListener("resize", this.onSurfaceResize);
|
|
25
18
|
this.resize();
|
|
26
19
|
}
|
|
@@ -39,8 +32,6 @@ export class CanvasResizeWatcher {
|
|
|
39
32
|
}
|
|
40
33
|
|
|
41
34
|
destroy(): void {
|
|
42
|
-
this.observer?.disconnect();
|
|
43
|
-
this.observer = null;
|
|
44
35
|
this.canvas.removeEventListener("resize", this.onSurfaceResize);
|
|
45
36
|
}
|
|
46
37
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { UIInputRouter } from "../input/ui-router";
|
|
10
10
|
import type { RendererModule } from "../module/renderer-module";
|
|
11
11
|
import type { RenderSurface, RenderSurfaceContext } from "../platform/render-surface";
|
|
12
|
-
import {
|
|
12
|
+
import { getNativeHost } from "../platform/runtime";
|
|
13
13
|
import { TelemetryCollector } from "../telemetry/collector";
|
|
14
14
|
import type { GPUAdapterInfo as GPUAdapterInfoData } from "../telemetry/gpu-profiler";
|
|
15
15
|
import { GPUProfiler, type FrameGraphData, type GPUInfo } from "../telemetry/gpu-profiler";
|
|
@@ -630,17 +630,15 @@ export class GameRenderer implements CanvasResizeHandler {
|
|
|
630
630
|
*/
|
|
631
631
|
setResolutionScale(scale: number): void {
|
|
632
632
|
this.resolutionScale = Math.max(0.25, Math.min(1, scale));
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
log.warn("game-renderer", "resolutionScale is not supported on the native surface (swapchain must match the window size) — ignoring");
|
|
643
|
-
}
|
|
633
|
+
// On the native host the swapchain extent must equal the window's
|
|
634
|
+
// real size — wgpu compares the configured size against the surface's
|
|
635
|
+
// currentExtent at acquire time and returns Outdated forever when
|
|
636
|
+
// they differ. Shrinking canvas.width/height would also corrupt
|
|
637
|
+
// clientWidth (UI layout + input coords). Resolution scaling needs
|
|
638
|
+
// an offscreen-render + upscale path to work here; for now it is a
|
|
639
|
+
// no-op rather than a brick.
|
|
640
|
+
if (this.resolutionScale < 1) {
|
|
641
|
+
log.warn("game-renderer", "resolutionScale is not supported on the native surface (swapchain must match the window size) — ignoring");
|
|
644
642
|
}
|
|
645
643
|
if (this.lastCssW > 0) this.onResize(this.lastCssW, this.lastCssH, this.lastRawDpr);
|
|
646
644
|
}
|
|
@@ -657,7 +655,7 @@ export class GameRenderer implements CanvasResizeHandler {
|
|
|
657
655
|
// performance knob when fill rate is a concern. On native the
|
|
658
656
|
// swapchain is window-sized (see setResolutionScale) so the scale is
|
|
659
657
|
// never applied to the backing store.
|
|
660
|
-
const cappedDpr = dpr
|
|
658
|
+
const cappedDpr = dpr;
|
|
661
659
|
this.dpr = cappedDpr;
|
|
662
660
|
const w = Math.round(cssWidth * cappedDpr);
|
|
663
661
|
const h = Math.round(cssHeight * cappedDpr);
|
|
@@ -1031,11 +1029,12 @@ export class GameRenderer implements CanvasResizeHandler {
|
|
|
1031
1029
|
// final image (skip on GPU error to avoid cascade). This must run AFTER
|
|
1032
1030
|
// afterFrame — 2D games (viewportCount=0) do their custom rendering in
|
|
1033
1031
|
// the afterFrame callback, so drawing UI earlier would put it underneath
|
|
1034
|
-
// their clear pass.
|
|
1032
|
+
// their clear pass. The texture comes from peek: a frame the game
|
|
1033
|
+
// skipped must not be started just to draw UI on a blank swapchain.
|
|
1035
1034
|
if (!gpuError && this.device && this.context) {
|
|
1036
1035
|
const compositors = this.uiCompositorOrder.filter((c) => c.hasContent());
|
|
1037
1036
|
const uiCanvasView = compositors.length > 0
|
|
1038
|
-
? this.
|
|
1037
|
+
? this.getFrameSurfaceTexture()?.createView()
|
|
1039
1038
|
: undefined;
|
|
1040
1039
|
if (uiCanvasView) {
|
|
1041
1040
|
const uiEncoder = this.device.createCommandEncoder();
|
|
@@ -1047,7 +1046,10 @@ export class GameRenderer implements CanvasResizeHandler {
|
|
|
1047
1046
|
storeOp: "store" as GPUStoreOp,
|
|
1048
1047
|
}],
|
|
1049
1048
|
});
|
|
1050
|
-
|
|
1049
|
+
// Panel rects are CSS px — pass the CSS surface size so
|
|
1050
|
+
// rect/surface maps correctly on HiDPI displays (canvas.width is
|
|
1051
|
+
// physical px; identical where scaleFactor=1).
|
|
1052
|
+
compositors.forEach((c) => { c.render(uiPass, this.canvas.clientWidth, this.canvas.clientHeight); });
|
|
1051
1053
|
uiPass.end();
|
|
1052
1054
|
// "preUi" slot — worker passes ordered ahead of the UI-composite
|
|
1053
1055
|
// buffer inside the end-of-frame submit.
|
|
@@ -1108,6 +1110,25 @@ export class GameRenderer implements CanvasResizeHandler {
|
|
|
1108
1110
|
}
|
|
1109
1111
|
}
|
|
1110
1112
|
|
|
1113
|
+
/**
|
|
1114
|
+
* The surface texture already acquired for the in-flight frame, or null
|
|
1115
|
+
* when nothing has acquired one yet. End-of-frame overlays (the UI
|
|
1116
|
+
* composite) use this so they join a frame rather than start one: on
|
|
1117
|
+
* hosts with frame retention (native), acquiring here would fabricate a
|
|
1118
|
+
* frame the game deliberately skipped (dirty-tracking) and present the
|
|
1119
|
+
* overlay over a blank swapchain image — a full-screen flash. Falls
|
|
1120
|
+
* back to acquiring on contexts without peek (DOM semantics:
|
|
1121
|
+
* getCurrentTexture is idempotent within a frame and present is
|
|
1122
|
+
* implicit, so there is nothing to defeat).
|
|
1123
|
+
*/
|
|
1124
|
+
private getFrameSurfaceTexture(): GPUTexture | null {
|
|
1125
|
+
const peek = (this.context as { peekCurrentTexture?: () => GPUTexture | null } | null)?.peekCurrentTexture;
|
|
1126
|
+
if (typeof peek === "function") {
|
|
1127
|
+
try { return peek.call(this.context) ?? null; } catch { return null; }
|
|
1128
|
+
}
|
|
1129
|
+
return this.getSurfaceTexture();
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1111
1132
|
private renderViewport(viewportIdx: number, dt: number, offscreen: OffscreenMode | null): GPUCommandBuffer | null {
|
|
1112
1133
|
if (!this.device || !this.context) return null;
|
|
1113
1134
|
|
|
@@ -1381,7 +1402,7 @@ export class GameRenderer implements CanvasResizeHandler {
|
|
|
1381
1402
|
protected renderScreenUiCompositors(): void {
|
|
1382
1403
|
const compositors = this.uiCompositorOrder.filter((c) => c.hasContent());
|
|
1383
1404
|
if (compositors.length === 0 || !this.device || !this.context) return;
|
|
1384
|
-
const canvasView = this.
|
|
1405
|
+
const canvasView = this.getFrameSurfaceTexture()?.createView();
|
|
1385
1406
|
if (!canvasView) return;
|
|
1386
1407
|
const uiEncoder = this.device.createCommandEncoder();
|
|
1387
1408
|
const uiPass = uiEncoder.beginRenderPass({
|
|
@@ -1392,7 +1413,8 @@ export class GameRenderer implements CanvasResizeHandler {
|
|
|
1392
1413
|
storeOp: "store" as GPUStoreOp,
|
|
1393
1414
|
}],
|
|
1394
1415
|
});
|
|
1395
|
-
|
|
1416
|
+
// Panel rects are CSS px — see the main UI composite block above.
|
|
1417
|
+
compositors.forEach((c) => { c.render(uiPass, this.canvas.clientWidth, this.canvas.clientHeight); });
|
|
1396
1418
|
uiPass.end();
|
|
1397
1419
|
this.device.queue.submit([uiEncoder.finish()]);
|
|
1398
1420
|
}
|
package/core/src/render/index.ts
CHANGED
|
@@ -111,8 +111,6 @@ export type { FrameGraphBuilder } from "./render-pass";
|
|
|
111
111
|
export { RendererInputBusImpl } from "./renderer-input-bus";
|
|
112
112
|
export { RendererModuleHost } from "./renderer-module-host";
|
|
113
113
|
export type { RendererModuleHostCallbacks } from "./renderer-module-host";
|
|
114
|
-
export { SabCanvasOverlay } from "./sab-canvas-overlay";
|
|
115
|
-
export type { SabCanvasOverlayOptions } from "./sab-canvas-overlay";
|
|
116
114
|
export { clearShaderValidationDedup, createValidatedShaderModule, createValidatedShaderModuleAsync, installShaderValidationGuard } from "./shader-validator";
|
|
117
115
|
export { CSM_SHADER_CHUNK, POINT_SHADOW_SHADER_CHUNK, SPOT_SHADOW_SHADER_CHUNK } from "./shaders/shadow-chunks";
|
|
118
116
|
export { SkyboxRenderer } from "./skybox";
|
|
@@ -79,9 +79,9 @@ export class InputManager {
|
|
|
79
79
|
}
|
|
80
80
|
}) as EventListener);
|
|
81
81
|
|
|
82
|
-
add(
|
|
82
|
+
add(this.canvas, "pointerlockchange", (() => {
|
|
83
83
|
const wasLocked = this.pointerLocked;
|
|
84
|
-
this.pointerLocked =
|
|
84
|
+
this.pointerLocked = this.canvas.pointerLocked === true;
|
|
85
85
|
if (this.pointerLocked) {
|
|
86
86
|
this.pointerLockRetryCount = 0;
|
|
87
87
|
if (this.pointerLockRetryTimer) {
|
|
@@ -97,7 +97,7 @@ export class InputManager {
|
|
|
97
97
|
}
|
|
98
98
|
}) as EventListener);
|
|
99
99
|
|
|
100
|
-
add(
|
|
100
|
+
add(this.canvas, "pointerlockerror", (() => {
|
|
101
101
|
// No action needed — tryLockPointer's fallback timer will retry.
|
|
102
102
|
}) as EventListener);
|
|
103
103
|
}
|
|
@@ -180,10 +180,10 @@ export class InputManager {
|
|
|
180
180
|
}
|
|
181
181
|
}) as EventListener, { passive: false });
|
|
182
182
|
|
|
183
|
-
//
|
|
184
|
-
// sandjongg's tile canvas) would otherwise trigger the canvas's
|
|
185
|
-
// mouseleave and break UI hover/pressed state.
|
|
186
|
-
add(
|
|
183
|
+
// Window-level mouseleave: a second interactive canvas stacked above
|
|
184
|
+
// (e.g. sandjongg's tile canvas) would otherwise trigger the canvas's
|
|
185
|
+
// own mouseleave and break UI hover/pressed state.
|
|
186
|
+
add(window, "mouseleave", (() => {
|
|
187
187
|
this.uiInputRouter?.handlePointerLeave();
|
|
188
188
|
}) as EventListener);
|
|
189
189
|
|
|
@@ -116,8 +116,8 @@ export class RendererModuleHost {
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
/**
|
|
119
|
-
* Inject the DevTools API so renderer plugins can self-register
|
|
120
|
-
*
|
|
119
|
+
* Inject the DevTools API so renderer plugins can self-register data
|
|
120
|
+
* feeds via `ctx.devtools.registerDataFeed(...)` during their `register()`.
|
|
121
121
|
*/
|
|
122
122
|
setDevToolsAPI(api: ModuleDevToolsAPI): void {
|
|
123
123
|
this._devtools = api;
|
|
@@ -635,8 +635,6 @@ export class RendererModuleHost {
|
|
|
635
635
|
|
|
636
636
|
// No-op DevTools API stub — used when setDevToolsAPI() hasn't been called.
|
|
637
637
|
const NoopRendererDevToolsAPI: ModuleDevToolsAPI = {
|
|
638
|
-
registerPanel: () => {},
|
|
639
|
-
registerOverlayToggle: () => {},
|
|
640
638
|
registerDataFeed: () => {},
|
|
641
639
|
registerCommand: () => {},
|
|
642
640
|
registerSABStat: () => {},
|