@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
@@ -235,6 +235,21 @@ export class UniversalPhysicsAPI {
235
235
  setIntegrationDt(realmId, dt) {
236
236
  this.backend.setIntegrationDt(realmId, dt);
237
237
  }
238
+ /**
239
+ * Change gravity across all realms (applies to near/mid/far). Best-effort:
240
+ * backends that can't mutate realm gravity omit `setRealmGravity`, in which
241
+ * case callers should fall back to per-body `gravityScale` in BodyDesc.
242
+ */
243
+ setGravity(gravity) {
244
+ [RealmTier.Near, RealmTier.Mid, RealmTier.Far].forEach((tier) => {
245
+ try {
246
+ const realm = this.realmManager.getRealm(tier);
247
+ this.backend.setRealmGravity?.(realm.id, gravity);
248
+ }
249
+ catch { /* realm tier not initialized */ }
250
+ });
251
+ this.config.gravity = gravity;
252
+ }
238
253
  // --- Realm ---
239
254
  getRealmTier(body) {
240
255
  return this.realmManager.getRealmForBody(body);
@@ -442,6 +442,21 @@ export class RapierPhysicsBackend {
442
442
  this.lib.setIntegrationDt(realmId, dt);
443
443
  }
444
444
  }
445
+ /**
446
+ * Change a live realm's gravity. Updates the stored realm config (the
447
+ * no-lib JS fallback stepper reads it) and forwards to the lib when it
448
+ * supports runtime gravity (the WASM Rapier world.gravity is mutable; the
449
+ * native FFI lib may not implement it — callers should treat this as
450
+ * best-effort).
451
+ */
452
+ setRealmGravity(realmId, gravity) {
453
+ const realm = this.realms.get(realmId);
454
+ if (realm)
455
+ realm.config.gravity = gravity;
456
+ if (this.lib && this.lib.setRealmGravity) {
457
+ this.lib.setRealmGravity(realmId, gravity);
458
+ }
459
+ }
445
460
  setSleepThresholds(realmId, linearThreshold, angularThreshold) {
446
461
  if (this.lib) {
447
462
  this.lib.setSleepThresholds(realmId, linearThreshold, angularThreshold);
@@ -154,6 +154,11 @@ async function doLoadPhysicsLib() {
154
154
  entityByBodyHandle.delete(id);
155
155
  bodyIdByHandle.delete(id);
156
156
  },
157
+ setRealmGravity(id, gravity) {
158
+ const world = realms.get(id);
159
+ if (world)
160
+ world.gravity = { x: gravity[0], y: gravity[1], z: gravity[2] };
161
+ },
157
162
  createBody(realmId, bodyId, desc, entity) {
158
163
  const world = realms.get(realmId);
159
164
  if (!world)
@@ -443,9 +443,7 @@ export class PostProcessStack {
443
443
  createGlyphAtlas() {
444
444
  const chars = " .:-=+*#%@";
445
445
  const gw = 16, gh = 16, cols = chars.length;
446
- const canvas = document.createElement("canvas");
447
- canvas.width = cols * gw;
448
- canvas.height = gh;
446
+ const canvas = new OffscreenCanvas(cols * gw, gh);
449
447
  const ctx = canvas.getContext("2d");
450
448
  ctx.fillStyle = "black";
451
449
  ctx.fillRect(0, 0, canvas.width, canvas.height);
@@ -72,32 +72,18 @@ export var Material;
72
72
  // --- Ingredients ---
73
73
  // Ice (frozen water — produced by cooling)
74
74
  Material[Material["Ice"] = 64] = "Ice";
75
+ // (IDs 67–68, 71–73, 76–77, 79–81, 83–88 were inert alchemy materials —
76
+ // retired; keep the gaps so old saves don't remap onto new materials.)
75
77
  // Liquids
76
78
  Material[Material["Ether"] = 65] = "Ether";
77
79
  Material[Material["Blood"] = 66] = "Blood";
78
- Material[Material["Syrup"] = 67] = "Syrup";
79
- Material[Material["NightshadeExtract"] = 68] = "NightshadeExtract";
80
80
  Material[Material["TrollBlood"] = 69] = "TrollBlood";
81
81
  Material[Material["LiquidShadow"] = 70] = "LiquidShadow";
82
- Material[Material["LoveEssence"] = 71] = "LoveEssence";
83
- Material[Material["HateEssence"] = 72] = "HateEssence";
84
- Material[Material["DreamMist"] = 73] = "DreamMist";
85
82
  Material[Material["VoidEssence"] = 74] = "VoidEssence";
86
83
  // Powders (solid, fall)
87
84
  Material[Material["Sulfur"] = 75] = "Sulfur";
88
- Material[Material["GroundEyeOfNewt"] = 76] = "GroundEyeOfNewt";
89
- Material[Material["GroundBatWing"] = 77] = "GroundBatWing";
90
85
  Material[Material["BoneDust"] = 78] = "BoneDust";
91
- Material[Material["IronFilings"] = 79] = "IronFilings";
92
- Material[Material["MoonstoneDust"] = 80] = "MoonstoneDust";
93
- Material[Material["CrystalDust"] = 81] = "CrystalDust";
94
86
  Material[Material["MushroomSpores"] = 82] = "MushroomSpores";
95
- Material[Material["DragonScale"] = 83] = "DragonScale";
96
- Material[Material["PhoenixFeather"] = 84] = "PhoenixFeather";
97
- Material[Material["UnicornHorn"] = 85] = "UnicornHorn";
98
- Material[Material["MandrakeRoot"] = 86] = "MandrakeRoot";
99
- Material[Material["SpiderSilk"] = 87] = "SpiderSilk";
100
- Material[Material["GraveDust"] = 88] = "GraveDust";
101
87
  Material[Material["StarShard"] = 89] = "StarShard";
102
88
  Material[Material["TimeSand"] = 90] = "TimeSand";
103
89
  // Gases
@@ -232,29 +218,13 @@ export const MATERIALS = {
232
218
  // --- Liquids ---
233
219
  [Material.Ether]: def(65, "Ether", [0.55, 0.45, 0.85, 0.8], { gravity: 1.5, gravityDir: 1, density: 0.7, liquid: true, flammable: true, burnTime: 40, albedo: 0.2, reflectivity: 0.4, brightness: 1.1 }),
234
220
  [Material.Blood]: def(66, "Blood", [0.7, 0.05, 0.05, 0.95], { gravity: 2, gravityDir: 1, density: 1.06, liquid: true, albedo: 0.3, reflectivity: 0.2, brightness: 0.8 }),
235
- [Material.Syrup]: def(67, "Syrup", [0.6, 0.4, 0.15, 0.95], { gravity: 1, gravityDir: 1, density: 1.4, liquid: true, albedo: 0.3, reflectivity: 0.3, brightness: 0.9 }),
236
- [Material.NightshadeExtract]: def(68, "Nightshade Extract", [0.3, 0.05, 0.35, 0.95], { gravity: 2, gravityDir: 1, density: 1.1, liquid: true, albedo: 0.2, reflectivity: 0.1, brightness: 0.7 }),
237
221
  [Material.TrollBlood]: def(69, "Troll Blood", [0.4, 0.2, 0.15, 0.95], { gravity: 2, gravityDir: 1, density: 1.2, liquid: true, albedo: 0.25, reflectivity: 0.15, brightness: 0.8 }),
238
222
  [Material.LiquidShadow]: def(70, "Liquid Shadow", [0.05, 0.05, 0.12, 0.98], { gravity: 3, gravityDir: 1, density: 2.0, liquid: true, albedo: 0.0, reflectivity: 0.0, brightness: 0.3 }),
239
- [Material.LoveEssence]: def(71, "Love Essence", [0.95, 0.4, 0.6, 0.9], { gravity: 1.5, gravityDir: 1, density: 0.9, liquid: true, albedo: 0.3, reflectivity: 0.4, brightness: 1.0 }),
240
- [Material.HateEssence]: def(72, "Hate Essence", [0.2, 0.1, 0.15, 0.9], { gravity: 2, gravityDir: 1, density: 1.3, liquid: true, albedo: 0.2, reflectivity: 0.2, brightness: 0.7 }),
241
- [Material.DreamMist]: def(73, "Dream Mist", [0.6, 0.7, 0.95, 0.6], { gravity: 1, gravityDir: -1, density: 0.3, liquid: true, albedo: 0.2, reflectivity: 0.5, brightness: 1.2 }),
242
223
  [Material.VoidEssence]: def(74, "Void Essence", [0.1, 0.0, 0.2, 0.95], { gravity: 4, gravityDir: 1, density: 3.0, liquid: true, albedo: 0.0, reflectivity: 0.1, brightness: 0.5 }),
243
224
  // --- Alchemy game: powders (solid, fall) ---
244
225
  [Material.Sulfur]: def(75, "Sulfur", [0.9, 0.85, 0.2, 1.0], { gravity: 1, gravityDir: 1, density: 2.0, solid: true, flammable: true, burnTime: 60, albedo: 0.5, reflectivity: 0.1, brightness: 0.9 }),
245
- [Material.GroundEyeOfNewt]: def(76, "Ground Eye of Newt", [0.5, 0.35, 0.2, 1.0], { gravity: 1, gravityDir: 1, density: 1.2, solid: true, albedo: 0.4, brightness: 0.8 }),
246
- [Material.GroundBatWing]: def(77, "Ground Bat Wing", [0.3, 0.25, 0.35, 1.0], { gravity: 0.8, gravityDir: 1, density: 0.6, solid: true, albedo: 0.3, brightness: 0.7 }),
247
226
  [Material.BoneDust]: def(78, "Bone Dust", [0.85, 0.82, 0.75, 1.0], { gravity: 1, gravityDir: 1, density: 1.5, solid: true, albedo: 0.5, brightness: 0.8 }),
248
- [Material.IronFilings]: def(79, "Iron Filings", [0.55, 0.55, 0.6, 1.0], { gravity: 1.5, gravityDir: 1, density: 7.8, solid: true, magnetic: true, albedo: 0.5, reflectivity: 0.3 }),
249
- [Material.MoonstoneDust]: def(80, "Moonstone Dust", [0.8, 0.85, 0.95, 1.0], { gravity: 1, gravityDir: 1, density: 1.0, solid: true, albedo: 0.4, reflectivity: 0.5, brightness: 1.3 }),
250
- [Material.CrystalDust]: def(81, "Crystal Dust", [0.6, 0.9, 0.95, 1.0], { gravity: 1, gravityDir: 1, density: 2.5, solid: true, albedo: 0.3, reflectivity: 0.6, brightness: 1.2 }),
251
227
  [Material.MushroomSpores]: def(82, "Mushroom Spores", [0.6, 0.5, 0.4, 0.9], { gravity: 0.5, gravityDir: 1, density: 0.4, solid: true, albedo: 0.3, brightness: 0.8 }),
252
- [Material.DragonScale]: def(83, "Dragon Scale", [0.5, 0.15, 0.1, 1.0], { gravity: 1.5, gravityDir: 1, density: 5.0, solid: true, albedo: 0.4, reflectivity: 0.3, brightness: 1.0 }),
253
- [Material.PhoenixFeather]: def(84, "Phoenix Feather", [0.95, 0.5, 0.15, 0.9], { gravity: 0.3, gravityDir: 1, density: 0.3, solid: true, flammable: true, burnTime: 200, albedo: 0.4, brightness: 1.4 }),
254
- [Material.UnicornHorn]: def(85, "Unicorn Horn", [0.95, 0.9, 1.0, 1.0], { gravity: 1, gravityDir: 1, density: 1.8, solid: true, albedo: 0.5, reflectivity: 0.5, brightness: 1.5 }),
255
- [Material.MandrakeRoot]: def(86, "Mandrake Root", [0.5, 0.4, 0.25, 1.0], { gravity: 1, gravityDir: 1, density: 1.3, solid: true, albedo: 0.4, brightness: 0.7 }),
256
- [Material.SpiderSilk]: def(87, "Spider Silk", [0.9, 0.9, 0.85, 0.8], { gravity: 0.2, gravityDir: 1, density: 0.2, solid: true, albedo: 0.4, reflectivity: 0.2, brightness: 0.9 }),
257
- [Material.GraveDust]: def(88, "Grave Dust", [0.3, 0.28, 0.25, 1.0], { gravity: 1, gravityDir: 1, density: 1.8, solid: true, albedo: 0.3, brightness: 0.6 }),
258
228
  [Material.StarShard]: def(89, "Star Shard", [0.7, 0.8, 1.0, 1.0], { gravity: 1, gravityDir: 1, density: 2.0, solid: true, albedo: 0.3, reflectivity: 0.7, brightness: 1.6 }),
259
229
  [Material.TimeSand]: def(90, "Time Sand", [0.4, 0.3, 0.6, 0.9], { gravity: 1, gravityDir: 1, density: 1.5, solid: true, albedo: 0.3, reflectivity: 0.4, brightness: 1.2 }),
260
230
  // --- Alchemy game: gases + byproducts ---
@@ -2207,9 +2207,6 @@ export class SandWorld {
2207
2207
  continue;
2208
2208
  }
2209
2209
  }
2210
- // --- PhoenixFeather near fire/lava: glows brighter, resists burning ---
2211
- // (flammable but long burnTime already set; here we just let it shimmer)
2212
- // --- DragonScale: very dense, sinks through liquids (handled by density) ---
2213
2210
  // --- LiquidShadow + light source (fire/plasma) → dissipates to smoke ---
2214
2211
  if (mat === Material.LiquidShadow) {
2215
2212
  let lightNi = -1;
@@ -3,16 +3,15 @@
3
3
  // engine systems, regardless of which thread they run on.
4
4
  //
5
5
  // Realm detection determines transport:
6
- // - Main realm: panels/feeds/commands registered directly on
6
+ // - Main realm: feeds/commands registered directly on
7
7
  // window.__sceneInspector via DevToolsDataBridge.
8
- // - Worker realm: panels/feeds/commands registered in a worker-side
9
- // registry. Data feeds are written to a devtools SAB region (zero-copy,
10
- // synchronous reads). Commands are forwarded via worker RPC. Panel
11
- // declarations are synced to the renderer via a one-time manifest RPC.
8
+ // - Worker realm: feeds/commands registered in a worker-side registry.
9
+ // Data feeds are written to a devtools SAB region (zero-copy,
10
+ // synchronous reads). Commands are forwarded via worker RPC.
12
11
  //
13
12
  // Plugins import `devtools` from @downdraft/engine/modules/devtools and call
14
- // registerPanel/registerDataFeed/registerCommand/registerSABStat — the same
15
- // API works in both realms.
13
+ // registerDataFeed/registerCommand/registerSABStat — the same API works in
14
+ // both realms.
16
15
  // ============================================================================
17
16
  import { resourceToken } from "@downdraft/engine/ecs/resource";
18
17
  import { createLogger } from "@downdraft/engine/util/logger";
@@ -50,7 +49,7 @@ export const DEVTOOLS_REALM = detectRealm();
50
49
  // synchronously. No RPC polling needed.
51
50
  //
52
51
  // Layout:
53
- // [0..3] manifestVersion: Uint32 (bumped when panels/feeds change)
52
+ // [0..3] manifestVersion: Uint32 (bumped when feeds/commands change)
54
53
  // [4..7] feedCount: Uint32
55
54
  // [8..] feed entries: each is [lengthBytes: Uint32][JSON bytes...]
56
55
  // (lengthBytes = 0 means "not yet written" / "unavailable")
@@ -81,8 +80,6 @@ export function allocateDevToolsSAB(maxFeedBlobBytes, statsBytes) {
81
80
  }
82
81
  class DevToolsAPIImpl {
83
82
  realm;
84
- panels = new Map();
85
- toggles = new Map();
86
83
  dataFeeds = new Map();
87
84
  dataFeedNames = []; // ordered list for feedIndex assignment
88
85
  commands = new Map();
@@ -99,14 +96,6 @@ class DevToolsAPIImpl {
99
96
  constructor(realm) {
100
97
  this.realm = realm;
101
98
  }
102
- registerPanel(ext) {
103
- this.panels.set(ext.id, ext);
104
- this.bumpManifest();
105
- }
106
- registerOverlayToggle(toggle) {
107
- this.toggles.set(toggle.id, toggle);
108
- this.bumpManifest();
109
- }
110
99
  registerDataFeed(name, fn, writeRateHz = 10) {
111
100
  if (this.dataFeeds.has(name)) {
112
101
  // Update fn/rate, keep feedIndex and hasWritten
@@ -183,8 +172,6 @@ class DevToolsAPIImpl {
183
172
  }
184
173
  getManifest() {
185
174
  return {
186
- panels: Array.from(this.panels.values()),
187
- toggles: Array.from(this.toggles.values()),
188
175
  dataFeeds: this.dataFeedNames.map((name, i) => ({
189
176
  name,
190
177
  feedIndex: i,
@@ -276,8 +263,6 @@ class DevToolsAPIImpl {
276
263
  return Array.from(this.sabStats.keys());
277
264
  }
278
265
  clear() {
279
- this.panels.clear();
280
- this.toggles.clear();
281
266
  this.dataFeeds.clear();
282
267
  this.dataFeedNames = [];
283
268
  this.commands.clear();
@@ -301,7 +286,7 @@ const _devtools = new DevToolsAPIImpl(DEVTOOLS_REALM);
301
286
  * the appropriate transport for data feeds (direct call vs SAB) and
302
287
  * commands (direct call vs RPC forwarding).
303
288
  *
304
- * Plugins import this and call registerPanel/registerDataFeed/registerCommand.
289
+ * Plugins import this and call registerDataFeed/registerCommand.
305
290
  * The same code works in both realms.
306
291
  */
307
292
  export const devtools = _devtools;
@@ -0,0 +1,75 @@
1
+ // ============================================================================
2
+ // fonts.ts — bind the host's real system fonts into the devtools dock doc.
3
+ //
4
+ // The OSR rasterizer ships a single embedded DejaVu Sans as the fallback for
5
+ // every generic family — legible, but `monospace` resolves to it too, so the
6
+ // console/REPL/tree read as proportional sans. Same fix as cruiser's
7
+ // libraries/app-shell/fonts.ts: resolve the user's actual sans + mono faces
8
+ // via fontconfig (fc-match -f %{file}), register them as `ui://` blobs on the
9
+ // HtmlUiHost before mount, and emit @font-face rules the dock stylesheet
10
+ // binds ahead of the generic fallbacks. Any failure (no fc-match, non-Linux
11
+ // host, unreadable file) leaves the embedded fallback in place.
12
+ // ============================================================================
13
+ import { execFileSync } from "node:child_process";
14
+ import { readFileSync } from "node:fs";
15
+ // Blitz-dom only fetches a @font-face source whose format is detectable —
16
+ // either an explicit format() hint or a file extension on the url — so the
17
+ // urls carry the resolved file's real extension.
18
+ const FACES = [
19
+ { family: "DevTools Sans", pattern: "sans-serif", urlName: "sans", weight: 400 },
20
+ { family: "DevTools Sans", pattern: "sans-serif:weight=bold", urlName: "sans-bold", weight: 700 },
21
+ { family: "DevTools Mono", pattern: "monospace", urlName: "mono", weight: 400 },
22
+ { family: "DevTools Mono", pattern: "monospace:weight=bold", urlName: "mono-bold", weight: 700 },
23
+ ];
24
+ /** `fc-match -f %{file}` for a fontconfig pattern; null when fc-match is
25
+ * absent or the match isn't a font file Blitz can load. */
26
+ function fcFontFile(pattern) {
27
+ try {
28
+ const p = execFileSync("fc-match", ["-f", "%{file}", pattern], { timeout: 5000 }).toString().trim();
29
+ return /\.(ttf|otf|woff2?)$/i.test(p) ? p : null;
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
35
+ function fontExt(path) {
36
+ return path.match(/\.(ttf|otf|woff2?)$/i)?.[0] ?? ".ttf";
37
+ }
38
+ /**
39
+ * Register system sans + mono faces as `ui://` resources and return the
40
+ * @font-face CSS to prepend to the dock stylesheet. Returns "" when nothing
41
+ * registered — the stylesheet's named families then fail to resolve and the
42
+ * generic fallbacks (embedded DejaVu) apply.
43
+ *
44
+ * Call before mount — the doc backend applies resources synchronously and in
45
+ * order, so the first doc resolve sees them.
46
+ */
47
+ export function registerDevtoolsFonts(sink) {
48
+ try {
49
+ // Both weights of a family are required: a 700 rule pointing at the
50
+ // regular file keeps bold text in-family instead of falling back.
51
+ const files = new Map();
52
+ for (const f of FACES)
53
+ files.set(f.urlName, fcFontFile(f.pattern) ?? "");
54
+ if (!files.get("sans"))
55
+ return "";
56
+ if (!files.get("mono"))
57
+ files.set("mono", files.get("sans"));
58
+ if (!files.get("sans-bold"))
59
+ files.set("sans-bold", files.get("sans"));
60
+ if (!files.get("mono-bold"))
61
+ files.set("mono-bold", files.get("mono"));
62
+ let css = "";
63
+ for (const f of FACES) {
64
+ const file = files.get(f.urlName);
65
+ const url = `ui://devtools-fonts/${f.urlName}${fontExt(file)}`;
66
+ sink.registerResource(url, readFileSync(file));
67
+ css += `@font-face { font-family:'${f.family}'; font-weight:${f.weight}; src:url('${url}'); }\n`;
68
+ }
69
+ return css;
70
+ }
71
+ catch {
72
+ // fc-match absent / unreadable font file — the embedded fallback applies.
73
+ return "";
74
+ }
75
+ }
@@ -13,11 +13,13 @@
13
13
  import { createLogger } from "@downdraft/engine";
14
14
  import { CdpBridge, DevtoolsBackend } from "@downdraft/engine/libraries/devtools";
15
15
  import { HtmlUiHost } from "@downdraft/engine/modules/html-ui";
16
+ import { registerDevtoolsFonts } from "./fonts.js";
16
17
  import { ConsolePanel } from "./panels/console.js";
17
18
  import { ElementsPanel } from "./panels/elements.js";
18
19
  import { GpuPanel } from "./panels/gpu.js";
19
20
  import { PerformancePanel } from "./panels/performance.js";
20
21
  import { SnapshotPanel } from "./panels/snapshot.js";
22
+ import { esc } from "./panels/types.js";
21
23
  import { DEVTOOLS_CSS } from "./theme.js";
22
24
  const log = createLogger("info");
23
25
  const MIN_DOCK = 120;
@@ -42,6 +44,9 @@ export class BlitzDevtoolsHost {
42
44
  panelCtx;
43
45
  lastPanelRender = -1e9;
44
46
  lastSlowLog = 0;
47
+ /** @font-face css from registerDevtoolsFonts — "" means "embedded font
48
+ * fallback", null means "not yet attempted". */
49
+ fontCss = null;
45
50
  constructor(opts) {
46
51
  this.opts = opts;
47
52
  this.surfaceW = opts.surface.clientWidth;
@@ -113,19 +118,42 @@ export class BlitzDevtoolsHost {
113
118
  show() {
114
119
  if (!this._ready || this.visible || this.disposed)
115
120
  return;
116
- this.panel = this.opts.ui.mount(this.shellHtml(), {
117
- id: "devtools",
118
- rect: this.dockRect(),
119
- // Above game overlays — menus/loading screens shouldn't bury the dock.
120
- z: 1000,
121
- scale: 1.5,
122
- interactive: true,
123
- // Devtools UI doesn't need vsync-rate raster — cap it so mutation
124
- // bursts (console streams, snapshot refreshes) coalesce into ~66ms
125
- // rasters instead of one raster per message.
126
- maxFps: 15,
127
- onEvent: (ev) => this.handleDocEvent(ev),
128
- });
121
+ // Register real system fonts once per host — ui:// resources persist on
122
+ // the surface across dock remounts, and must land before the doc mounts
123
+ // so the first resolve sees them (embedded DejaVu fallback otherwise).
124
+ if (this.fontCss === null) {
125
+ const ui = this.opts.ui;
126
+ const rr = ui.registerResource?.bind(ui)
127
+ ?? (ui.host ? ui.host.registerResource.bind(ui.host) : undefined);
128
+ this.fontCss = rr ? registerDevtoolsFonts({ registerResource: rr }) : "";
129
+ }
130
+ try {
131
+ this.panel = this.opts.ui.mount(this.shellHtml(), {
132
+ id: "devtools",
133
+ rect: this.dockRect(),
134
+ // Above game overlays — menus/loading screens shouldn't bury the dock.
135
+ z: 1000,
136
+ // Native-res raster: the vendored glifo/FreeType path grid-fits hinted
137
+ // glyph stems to whole pixels — supersample factors >1 just blur that
138
+ // back out on downscale (and cost 2.25x the texture memory).
139
+ scale: 1,
140
+ // Ease wheel detents at pump cadence instead of jumping per event.
141
+ smoothWheel: true,
142
+ interactive: true,
143
+ // Devtools UI doesn't need vsync-rate raster — cap it so mutation
144
+ // bursts (console streams, snapshot refreshes) coalesce into ~66ms
145
+ // rasters instead of one raster per message.
146
+ maxFps: 15,
147
+ onEvent: (ev) => this.handleDocEvent(ev),
148
+ });
149
+ }
150
+ catch (err) {
151
+ // Mount fails when the ui worker is dead — stay hidden so the next F12
152
+ // retries cleanly instead of wedging on a half-mounted doc.
153
+ this.panel = null;
154
+ log.error("blitz-devtools-host", `dock mount failed: ${err}`);
155
+ return;
156
+ }
129
157
  this.syncProviders();
130
158
  this.renderTabs();
131
159
  this.activate(this.activeId);
@@ -150,8 +178,19 @@ export class BlitzDevtoolsHost {
150
178
  const t1 = performance.now();
151
179
  if (!this.visible)
152
180
  return;
153
- this.syncProviders();
154
- this.flush();
181
+ // Guarded like backend.update() above — a throwing panel render can't be
182
+ // allowed to kill the game frame hook that drives this pump.
183
+ try {
184
+ this.syncProviders();
185
+ this.flush();
186
+ }
187
+ catch (err) {
188
+ const tn = performance.now();
189
+ if (tn - this.lastSlowLog > 1000) {
190
+ this.lastSlowLog = tn;
191
+ log.warn("blitz-devtools-host", `panel render error: ${err}`);
192
+ }
193
+ }
155
194
  const t2 = performance.now();
156
195
  // ≤1/sec — these diagnostics used to feed the console panel's logger
157
196
  // sink, and each row forced another dock re-render: a self-feeding
@@ -204,11 +243,13 @@ export class BlitzDevtoolsHost {
204
243
  }
205
244
  // ── Dock layout ──
206
245
  dockRect() {
207
- const h = Math.min(this.dockH, this.surfaceH - 60);
208
- return { x: 0, y: this.surfaceH - h, w: this.surfaceW, h };
246
+ // A tiny surface (or a pathological resize event) must not produce a
247
+ // negative height — keep at least enough room for the tab strip.
248
+ const h = Math.max(48, Math.min(this.dockH, this.surfaceH - 60));
249
+ return { x: 0, y: Math.max(0, this.surfaceH - h), w: this.surfaceW, h };
209
250
  }
210
251
  shellHtml() {
211
- return `<html><head><style>${DEVTOOLS_CSS}</style></head><body>
252
+ return `<html><head><style>${this.fontCss ?? ""}${DEVTOOLS_CSS}</style></head><body>
212
253
  <div id="dt-root">
213
254
  <div id="dt-grip" title="drag to resize"></div>
214
255
  <div id="dt-chrome">
@@ -224,7 +265,10 @@ export class BlitzDevtoolsHost {
224
265
  renderTabs() {
225
266
  if (!this.panel)
226
267
  return;
227
- const tabs = [...this.panels.values()].map((p) => `<span class="tab${p.id === this.activeId ? " active" : ""}" data-action="dt.tab" data-tab="${p.id}">${p.title}</span>`).join("");
268
+ // esc() both interpolations — provider names feed panel ids/titles and
269
+ // arrive as raw strings (a game registering "x\"><img" would otherwise
270
+ // inject markup into the tab strip).
271
+ const tabs = [...this.panels.values()].map((p) => `<span class="tab${p.id === this.activeId ? " active" : ""}" data-action="dt.tab" data-tab="${esc(p.id)}">${esc(p.title)}</span>`).join("");
228
272
  this.panel.setInnerHtml("#dt-tabs", `<span class="brand">downdraft</span>${tabs}<span class="spacer"></span>
229
273
  <span class="hint">docked · F12</span><span id="dt-close" data-action="dt.close">✕</span>`);
230
274
  this.tabsDirty = false;
@@ -235,13 +279,28 @@ export class BlitzDevtoolsHost {
235
279
  this.activeId = id;
236
280
  const p = this.panels.get(id);
237
281
  this.renderTabs();
238
- this.panel.setInnerHtml("#dt-top", p.renderTop?.() ?? "");
239
- this.panel.setInnerHtml("#dt-bottom", p.renderBottom?.() ?? "");
282
+ try {
283
+ this.panel.setInnerHtml("#dt-top", p.renderTop?.() ?? "");
284
+ this.panel.setInnerHtml("#dt-bottom", p.renderBottom?.() ?? "");
285
+ }
286
+ catch (err) {
287
+ log.warn("blitz-devtools-host", `panel ${id} chrome render error: ${err}`);
288
+ }
240
289
  p.dirty = true;
241
290
  p.shellDirty = false;
242
- this.panel.setInnerHtml("#dt-body", p.renderBody());
243
- p.afterRender?.();
244
- p.activate?.();
291
+ this.panel.setInnerHtml("#dt-body", this.safeBody(p));
292
+ try {
293
+ p.afterRender?.();
294
+ }
295
+ catch (err) {
296
+ log.warn("blitz-devtools-host", `panel ${id} afterRender error: ${err}`);
297
+ }
298
+ try {
299
+ p.activate?.();
300
+ }
301
+ catch (err) {
302
+ log.warn("blitz-devtools-host", `panel ${id} activate error: ${err}`);
303
+ }
245
304
  // Only the snapshot tab being viewed gets periodic provider refreshes;
246
305
  // fixed panels suspend auto-refresh entirely (manual Refresh still works).
247
306
  this.backend.providerWatch = p.providerSlot ?? -1;
@@ -307,10 +366,32 @@ export class BlitzDevtoolsHost {
307
366
  }
308
367
  if (p.dirty) {
309
368
  p.dirty = false;
310
- if (p.flushDom?.() !== true) {
311
- this.panel.setInnerHtml("#dt-body", p.renderBody());
369
+ let handled = false;
370
+ try {
371
+ handled = p.flushDom?.() === true;
312
372
  }
313
- p.afterRender?.();
373
+ catch (err) {
374
+ log.warn("blitz-devtools-host", `panel ${p.id} flushDom error: ${err}`);
375
+ }
376
+ if (!handled)
377
+ this.panel.setInnerHtml("#dt-body", this.safeBody(p));
378
+ try {
379
+ p.afterRender?.();
380
+ }
381
+ catch (err) {
382
+ log.warn("blitz-devtools-host", `panel ${p.id} afterRender error: ${err}`);
383
+ }
384
+ }
385
+ }
386
+ /** renderBody that can't take the dock down — the error surfaces in-panel
387
+ * (and the next push retries since dirty just flips back on). */
388
+ safeBody(p) {
389
+ try {
390
+ return p.renderBody();
391
+ }
392
+ catch (err) {
393
+ log.warn("blitz-devtools-host", `panel ${p.id} renderBody error: ${err}`);
394
+ return `<div class="status-banner error">panel render error: ${esc(String(err))}</div>`;
314
395
  }
315
396
  }
316
397
  setStatus(text) {
@@ -345,8 +426,10 @@ export class BlitzDevtoolsHost {
345
426
  return;
346
427
  }
347
428
  }
429
+ // Left-click only — routing actions on contextmenu meant a right-click
430
+ // on a tab/button fired it instead of just focusing.
348
431
  const action = ev.d?.action;
349
- if ((ev.t === "click" || ev.t === "contextmenu") && action) {
432
+ if (ev.t === "click" && action) {
350
433
  if (action === "dt.tab") {
351
434
  const id = ev.d.tab;
352
435
  if (id)
@@ -357,11 +440,21 @@ export class BlitzDevtoolsHost {
357
440
  this.hide();
358
441
  return;
359
442
  }
360
- this.panels.get(this.activeId)?.onAction?.(ev.d, ev);
443
+ try {
444
+ this.panels.get(this.activeId)?.onAction?.(ev.d, ev);
445
+ }
446
+ catch (err) {
447
+ log.warn("blitz-devtools-host", `panel action error: ${err}`);
448
+ }
361
449
  return;
362
450
  }
363
451
  // Everything else (input, keydown/up, scroll, focus…) → active panel.
364
- this.panels.get(this.activeId)?.onEvent?.(ev);
452
+ try {
453
+ this.panels.get(this.activeId)?.onEvent?.(ev);
454
+ }
455
+ catch (err) {
456
+ log.warn("blitz-devtools-host", `panel event error: ${err}`);
457
+ }
365
458
  }
366
459
  }
367
460
  /**
@@ -387,5 +480,6 @@ export function createSelfHostedDevtoolsUi(ctx) {
387
480
  host,
388
481
  mount: (markup, spec) => host.mount({ ...spec, html: markup }),
389
482
  docCaps: () => host.docCaps,
483
+ registerResource: (url, bytes) => host.registerResource(url, bytes),
390
484
  };
391
485
  }
@@ -101,7 +101,7 @@ export class ConsolePanel {
101
101
  onBackendEvent(event, data) {
102
102
  if (event === "console") {
103
103
  const e = data;
104
- this.rows.push({ text: e.text ?? "", severity: e.severity ?? 0, thread: e.thread ?? "main", seq: this.seq++ });
104
+ this.rows.push({ text: String(e?.text ?? ""), severity: e?.severity ?? 0, thread: e?.thread ?? "main", seq: this.seq++ });
105
105
  if (this.rows.length > MAX_ROWS)
106
106
  this.rows.splice(0, this.rows.length - MAX_ROWS);
107
107
  this.dirty = true;
@@ -113,15 +113,24 @@ export class ConsolePanel {
113
113
  }
114
114
  else if (event === "threads") {
115
115
  const list = data ?? [];
116
- this.threads = [{ id: "main", name: "main", kind: 0 }, ...list.filter((t) => t.id !== "main")];
117
- if (!this.threads.some((t) => t.id === this.replThread))
116
+ const next = [{ id: "main", name: "main", kind: 0 }, ...list.filter((t) => t.id !== "main")];
117
+ // Thread chips live in the bottom chrome — flag it so the row
118
+ // actually re-renders when a worker appears or disappears.
119
+ const changed = next.length !== this.threads.length
120
+ || next.some((t, i) => t.id !== this.threads[i].id || t.name !== this.threads[i].name);
121
+ this.threads = next;
122
+ if (!this.threads.some((t) => t.id === this.replThread)) {
118
123
  this.replThread = "main";
124
+ this.shellDirty = true;
125
+ }
126
+ if (changed)
127
+ this.shellDirty = true;
119
128
  }
120
129
  }
121
130
  onAction(data, _ev) {
122
131
  const act = data.action;
123
132
  if (act === "console.clear") {
124
- void this.ctx.call("console.clear");
133
+ void this.ctx.call("console.clear").catch(() => { });
125
134
  }
126
135
  else if (act === "console.sev") {
127
136
  this.minSev = (this.minSev + 1) % SEV_NAMES.length;
@@ -171,8 +180,11 @@ export class ConsolePanel {
171
180
  this.replValue = this.history[--this.histIdx];
172
181
  this.ctx.setValue("[data-dt=repl]", this.replValue);
173
182
  }
174
- else if (ev.k === "ArrowDown" && this.histIdx < this.history.length - 1) {
175
- this.replValue = this.history[++this.histIdx];
183
+ else if (ev.k === "ArrowDown" && this.histIdx >= 0 && this.histIdx < this.history.length) {
184
+ // Stepping past the newest entry restores the empty input —
185
+ // previously the last history item was unreachable-to-leave.
186
+ this.histIdx++;
187
+ this.replValue = this.histIdx < this.history.length ? this.history[this.histIdx] : "";
176
188
  this.ctx.setValue("[data-dt=repl]", this.replValue);
177
189
  }
178
190
  }
@@ -205,7 +217,7 @@ export class ConsolePanel {
205
217
  else {
206
218
  const r = res?.result;
207
219
  this.addRow({
208
- text: typeof r === "string" ? r : JSON.stringify(r, null, 2),
220
+ text: typeof r === "string" ? r : (r === undefined ? "undefined" : JSON.stringify(r, null, 2)),
209
221
  severity: 0, thread: this.replThread,
210
222
  });
211
223
  }