@downdraft/engine 0.2.0 → 0.3.1

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.
Files changed (138) hide show
  1. package/app/src/renderer/bootstrap.ts +0 -6
  2. package/app/src/renderer/feature-log.ts +1 -9
  3. package/app/src/renderer/mcp-harness.ts +8 -11
  4. package/app/src/renderer/standard-automation-tools.ts +8 -145
  5. package/core/src/index.ts +2 -2
  6. package/core/src/module/host.ts +3 -6
  7. package/core/src/module/index.ts +1 -1
  8. package/core/src/module/module.ts +4 -47
  9. package/core/src/physics/interface.ts +5 -0
  10. package/core/src/platform/render-surface.ts +13 -0
  11. package/core/src/platform/rpc.ts +4 -1
  12. package/core/src/render/canvas-resize-watcher.ts +1 -10
  13. package/core/src/render/game-renderer.ts +164 -79
  14. package/core/src/render/index.ts +0 -2
  15. package/core/src/render/input-manager.ts +7 -7
  16. package/core/src/render/renderer-module-host.ts +2 -4
  17. package/core/src/sab/grid-buffer.ts +55 -2
  18. package/core/src/sab/record-buffer.ts +48 -5
  19. package/core/src/sab/sim-channel.ts +181 -17
  20. package/core/src/sab/slot-buffer.ts +55 -1
  21. package/core/src/sab/types.ts +11 -0
  22. package/core/src/telemetry/gpu-resource-tracker.ts +24 -0
  23. package/core/src/ui-font-scale.ts +5 -38
  24. package/core/src/util/index.ts +1 -0
  25. package/core/src/util/session-timer.ts +23 -0
  26. package/core/src/worker/base-worker-host.ts +2 -1
  27. package/core/src/worker/buffer-sync.ts +20 -13
  28. package/core/src/worker/rpc.ts +119 -5
  29. package/core/src/worker/sim-worker-base.ts +5 -2
  30. package/core/src/worker/task-pool.ts +4 -0
  31. package/dist/.dd-manifest.json +893 -908
  32. package/dist/app/src/renderer/bootstrap.js +0 -4
  33. package/dist/app/src/renderer/feature-log.js +1 -10
  34. package/dist/app/src/renderer/mcp-harness.js +8 -11
  35. package/dist/app/src/renderer/standard-automation-tools.js +6 -138
  36. package/dist/core/src/index.js +2 -2
  37. package/dist/core/src/module/host.js +3 -6
  38. package/dist/core/src/platform/rpc.js +2 -1
  39. package/dist/core/src/render/canvas-resize-watcher.js +1 -10
  40. package/dist/core/src/render/game-renderer.js +166 -74
  41. package/dist/core/src/render/index.js +0 -1
  42. package/dist/core/src/render/input-manager.js +7 -7
  43. package/dist/core/src/render/renderer-module-host.js +2 -4
  44. package/dist/core/src/sab/grid-buffer.js +53 -1
  45. package/dist/core/src/sab/record-buffer.js +49 -5
  46. package/dist/core/src/sab/sim-channel.js +173 -17
  47. package/dist/core/src/sab/slot-buffer.js +54 -1
  48. package/dist/core/src/telemetry/gpu-resource-tracker.js +26 -0
  49. package/dist/core/src/ui-font-scale.js +5 -38
  50. package/dist/core/src/util/index.js +1 -0
  51. package/dist/core/src/util/session-timer.js +22 -0
  52. package/dist/core/src/worker/base-worker-host.js +2 -1
  53. package/dist/core/src/worker/buffer-sync.js +16 -9
  54. package/dist/core/src/worker/rpc.js +105 -8
  55. package/dist/core/src/worker/sim-worker-base.js +5 -2
  56. package/dist/core/src/worker/task-pool.js +5 -0
  57. package/dist/libraries/blitz-ui/src/native-module.js +8 -1
  58. package/dist/libraries/devtools/src/backend.js +35 -9
  59. package/dist/libraries/devtools/src/collectors.js +73 -22
  60. package/dist/libraries/devtools/src/native-providers.js +0 -2
  61. package/dist/libraries/entities/src/model-renderer.js +87 -27
  62. package/dist/libraries/entities/src/shaders/model.wgsl +13 -4
  63. package/dist/libraries/networking/src/index.js +1 -0
  64. package/dist/libraries/networking/src/ws-server.js +387 -0
  65. package/dist/libraries/physics-rapier/src/api.js +15 -0
  66. package/dist/libraries/physics-rapier/src/backend.js +15 -0
  67. package/dist/libraries/physics-rapier/src/rapier-backend.js +5 -0
  68. package/dist/libraries/postfx/src/post-process-stack.js +1 -3
  69. package/dist/libraries/sand/src/materials.js +2 -32
  70. package/dist/libraries/sand/src/sand-world.js +0 -3
  71. package/dist/modules/devtools/src/api.js +8 -23
  72. package/dist/modules/devtools/src/blitz/fonts.js +75 -0
  73. package/dist/modules/devtools/src/blitz/host.js +124 -30
  74. package/dist/modules/devtools/src/blitz/panels/console.js +19 -7
  75. package/dist/modules/devtools/src/blitz/panels/elements.js +217 -28
  76. package/dist/modules/devtools/src/blitz/panels/gpu.js +5 -2
  77. package/dist/modules/devtools/src/blitz/panels/performance.js +26 -12
  78. package/dist/modules/devtools/src/blitz/panels/snapshot.js +31 -8
  79. package/dist/modules/devtools/src/blitz/panels/types.js +2 -1
  80. package/dist/modules/devtools/src/blitz/theme.js +26 -4
  81. package/dist/modules/devtools/src/data-bridge.js +0 -11
  82. package/dist/modules/devtools/src/index.js +0 -6
  83. package/dist/modules/devtools/src/init.js +8 -42
  84. package/dist/modules/devtools/src/scene-inspector.js +1 -4
  85. package/dist/modules/devtools/src/worker-sync.js +3 -11
  86. package/dist/modules/html-ui/src/doc-backend.js +52 -3
  87. package/dist/modules/html-ui/src/host.js +6 -3
  88. package/dist/test/src/vision-test.js +1 -3
  89. package/libraries/blitz-ui/native/Cargo.toml +6 -0
  90. package/libraries/blitz-ui/native-osr/Cargo.toml +3 -1
  91. package/libraries/blitz-ui/src/native-module.ts +8 -1
  92. package/libraries/devtools/src/backend.ts +38 -12
  93. package/libraries/devtools/src/collectors.ts +75 -22
  94. package/libraries/devtools/src/native-providers.ts +0 -2
  95. package/libraries/entities/src/model-renderer.ts +88 -27
  96. package/libraries/entities/src/shaders/model.wgsl +13 -4
  97. package/libraries/networking/src/index.ts +4 -0
  98. package/libraries/networking/src/ws-server.ts +397 -0
  99. package/libraries/physics-rapier/src/api.ts +15 -0
  100. package/libraries/physics-rapier/src/backend.ts +15 -0
  101. package/libraries/physics-rapier/src/rapier-backend.ts +6 -0
  102. package/libraries/postfx/src/post-process-stack.ts +1 -2
  103. package/libraries/sand/src/materials.ts +2 -32
  104. package/libraries/sand/src/sand-world.ts +0 -4
  105. package/modules/devtools/README.md +1 -3
  106. package/modules/devtools/src/api.ts +10 -34
  107. package/modules/devtools/src/blitz/fonts.ts +78 -0
  108. package/modules/devtools/src/blitz/host.ts +107 -31
  109. package/modules/devtools/src/blitz/panels/console.ts +20 -8
  110. package/modules/devtools/src/blitz/panels/elements.ts +212 -31
  111. package/modules/devtools/src/blitz/panels/gpu.ts +4 -2
  112. package/modules/devtools/src/blitz/panels/performance.ts +24 -12
  113. package/modules/devtools/src/blitz/panels/snapshot.ts +30 -8
  114. package/modules/devtools/src/blitz/panels/types.ts +2 -1
  115. package/modules/devtools/src/blitz/theme.ts +26 -4
  116. package/modules/devtools/src/data-bridge.ts +0 -16
  117. package/modules/devtools/src/index.ts +1 -11
  118. package/modules/devtools/src/init.ts +8 -51
  119. package/modules/devtools/src/scene-inspector.ts +2 -6
  120. package/modules/devtools/src/types.ts +5 -90
  121. package/modules/devtools/src/worker-sync.ts +3 -13
  122. package/modules/html-ui/src/doc-backend.ts +67 -4
  123. package/modules/html-ui/src/host.ts +11 -3
  124. package/modules/html-ui/src/protocol.ts +1 -1
  125. package/package.json +4 -5
  126. package/test/src/vision-test.ts +2 -4
  127. package/core/src/render/sab-canvas-overlay.ts +0 -122
  128. package/dist/core/src/render/sab-canvas-overlay.js +0 -83
  129. package/dist/modules/devtools/src/debug-overlay.js +0 -181
  130. package/dist/modules/devtools/src/doctor-panel.js +0 -163
  131. package/dist/modules/devtools/src/label-overlay.js +0 -129
  132. package/dist/modules/devtools/src/material-stats-panel.js +0 -171
  133. package/dist/modules/devtools/src/sim-stats-panel.js +0 -250
  134. package/modules/devtools/src/debug-overlay.ts +0 -238
  135. package/modules/devtools/src/doctor-panel.ts +0 -182
  136. package/modules/devtools/src/label-overlay.ts +0 -154
  137. package/modules/devtools/src/material-stats-panel.ts +0 -188
  138. 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 + crossOriginIsolated (browser isolation only —
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
- return new Promise((resolve, reject) => {
125
- const reader = new FileReader();
126
- reader.onloadend = () => {
127
- const result = reader.result as string;
128
- const base64 = result.split(",")[1];
129
- resolve(base64 ?? "");
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 { getHostCapabilities, KEY, type RenderSurface } from "@downdraft/engine";
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
- const hasDom = getHostCapabilities().hasDom;
426
- // DOM hosts hit-test the topmost element (mimics a real user click
427
- // through UI overlays). On native there is no DOM tree — the surface
428
- // itself is the input target.
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
- if (isNativeDom) (window as any).dispatchEvent?.(ev);
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 Partial<HTMLElement>;
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 ?? (targetEl as { id?: string }).id ?? "surface",
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/index.ts CHANGED
@@ -56,7 +56,7 @@ export type { MultiInputChannelInstance, MultiInputChannelOptions } from "./sab/
56
56
  export { INP, INP_FLAG, INP_HDR, INPUT_MAGIC, INPUT_VERSION, InputBufferReader, InputBufferWriter, InputChannel, KEY, MAX_INPUT_PLAYERS } from "./sab/game-input";
57
57
 
58
58
  // Sim SAB Channel
59
- export { allocateInputBuffer, allocateSimBuffer, ENT, MAX_ENTITIES, MAX_PLAYERS, PLR, PLR_FLAG, SIM_ENTITY_SLOT_SIZE, SIM_HDR, SIM_MAGIC, SIM_PLAYER_SLOT_SIZE, SIM_VERSION, SimBufferReader, SimBufferWriter, SimChannel } from "./sab/sim-channel";
59
+ export { allocateInputBuffer, allocateSimBuffer, ENT, MAX_ENTITIES, MAX_PLAYERS, PLR, PLR_FLAG, PREV, SIM_ENTITY_SLOT_SIZE, SIM_HDR, SIM_MAGIC, SIM_PLAYER_SLOT_SIZE, SIM_VERSION, SimBufferReader, SimBufferWriter, SimChannel } from "./sab/sim-channel";
60
60
  export type { PhysicsTimingData } from "./sab/sim-channel";
61
61
 
62
62
  // SAB Framework
@@ -90,7 +90,7 @@ export { EntitySimWorkerHost } from "./worker/entity-sim-worker-host";
90
90
  export type { EntitySimApi, EntitySimHostOptions } from "./worker/entity-sim-worker-host";
91
91
  export { exposeProfilingApi, InstrumentedWorkerHost } from "./worker/instrumented-worker-host";
92
92
  export type { InstrumentedWorkerHostOptions } from "./worker/instrumented-worker-host";
93
- export { expose, exposeEvents, getWorkerHost, wrap } from "./worker/rpc";
93
+ export { expose, exposeBatchedEvents, exposeEvents, getWorkerHost, wrap } from "./worker/rpc";
94
94
  export type { ExposeOptions, HostMessageHandler, WorkerApi, WorkerEventEmitter, WorkerHost, WorkerProxy } from "./worker/rpc";
95
95
  export { createSimWorker } from "./worker/sim-worker-base";
96
96
  export type { CreateSimWorkerOptions, SimAfterTicksContext, SimTickContext, SimWorkerControl, SimWorkerStats } from "./worker/sim-worker-base";
@@ -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 debug panels via
51
- * `ctx.devtools.registerPanel(...)` during their `register()` lifecycle.
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.registerPanel() don't crash if devtools isn't wired).
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: () => {},
@@ -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 { DevToolsOverlayToggleRegistration, DevToolsPanelRegistration, Module, ModuleContext, ModuleDevToolsAPI, SABChannel } from "./module";
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.registerPanel(...)` etc. to self-register
54
- * debug screens. The same interface works in both main and worker realms.
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 debug panels,
99
- * data feeds, and commands via `ctx.devtools.register*(...)`.
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
  }
@@ -1,3 +1,6 @@
1
+
2
+ import { untrackedTimeout } from "../util/session-timer";
3
+
1
4
  export type RPCMessageType = "request" | "response" | "event";
2
5
 
3
6
  export interface RPCMessage {
@@ -34,7 +37,7 @@ export class RPC {
34
37
  }
35
38
  const id = this.messageIdCounter = (this.messageIdCounter + 1) % 0xFFFFFFFF;
36
39
  return new Promise((resolve, reject) => {
37
- const timer = setTimeout(() => {
40
+ const timer = untrackedTimeout(() => {
38
41
  if (this.pending.has(id)) {
39
42
  this.pending.delete(id);
40
43
  reject(new Error(`[RPC] Call to "${channel}" timed out after ${timeoutMs}ms`));
@@ -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
- // DOM hosts: element box changes fire ResizeObserver. Native surfaces
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
  }