@downdraft/engine 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/core/src/index.ts +2 -2
  2. package/core/src/platform/rpc.ts +4 -1
  3. package/core/src/render/game-renderer.ts +127 -64
  4. package/core/src/sab/grid-buffer.ts +55 -2
  5. package/core/src/sab/record-buffer.ts +48 -5
  6. package/core/src/sab/sim-channel.ts +181 -17
  7. package/core/src/sab/slot-buffer.ts +55 -1
  8. package/core/src/sab/types.ts +11 -0
  9. package/core/src/telemetry/gpu-resource-tracker.ts +24 -0
  10. package/core/src/util/index.ts +1 -0
  11. package/core/src/util/session-timer.ts +23 -0
  12. package/core/src/worker/base-worker-host.ts +2 -1
  13. package/core/src/worker/buffer-sync.ts +20 -13
  14. package/core/src/worker/rpc.ts +54 -5
  15. package/core/src/worker/sim-worker-base.ts +5 -2
  16. package/core/src/worker/task-pool.ts +4 -0
  17. package/dist/.dd-manifest.json +861 -851
  18. package/dist/core/src/index.js +2 -2
  19. package/dist/core/src/platform/rpc.js +2 -1
  20. package/dist/core/src/render/game-renderer.js +125 -59
  21. package/dist/core/src/sab/grid-buffer.js +53 -1
  22. package/dist/core/src/sab/record-buffer.js +49 -5
  23. package/dist/core/src/sab/sim-channel.js +173 -17
  24. package/dist/core/src/sab/slot-buffer.js +54 -1
  25. package/dist/core/src/telemetry/gpu-resource-tracker.js +26 -0
  26. package/dist/core/src/util/index.js +1 -0
  27. package/dist/core/src/util/session-timer.js +22 -0
  28. package/dist/core/src/worker/base-worker-host.js +2 -1
  29. package/dist/core/src/worker/buffer-sync.js +16 -9
  30. package/dist/core/src/worker/rpc.js +46 -8
  31. package/dist/core/src/worker/sim-worker-base.js +5 -2
  32. package/dist/core/src/worker/task-pool.js +5 -0
  33. package/dist/libraries/devtools/src/backend.js +35 -9
  34. package/dist/libraries/devtools/src/collectors.js +73 -22
  35. package/dist/modules/devtools/src/blitz/fonts.js +75 -0
  36. package/dist/modules/devtools/src/blitz/host.js +124 -30
  37. package/dist/modules/devtools/src/blitz/panels/console.js +19 -7
  38. package/dist/modules/devtools/src/blitz/panels/elements.js +217 -28
  39. package/dist/modules/devtools/src/blitz/panels/gpu.js +5 -2
  40. package/dist/modules/devtools/src/blitz/panels/performance.js +26 -12
  41. package/dist/modules/devtools/src/blitz/panels/snapshot.js +31 -8
  42. package/dist/modules/devtools/src/blitz/panels/types.js +2 -1
  43. package/dist/modules/devtools/src/blitz/theme.js +26 -4
  44. package/dist/modules/html-ui/src/doc-backend.js +52 -3
  45. package/dist/modules/html-ui/src/host.js +6 -3
  46. package/libraries/blitz-ui/native/Cargo.toml +6 -0
  47. package/libraries/blitz-ui/native-osr/Cargo.toml +3 -1
  48. package/libraries/devtools/src/backend.ts +38 -12
  49. package/libraries/devtools/src/collectors.ts +75 -22
  50. package/modules/devtools/src/blitz/fonts.ts +78 -0
  51. package/modules/devtools/src/blitz/host.ts +107 -31
  52. package/modules/devtools/src/blitz/panels/console.ts +20 -8
  53. package/modules/devtools/src/blitz/panels/elements.ts +212 -31
  54. package/modules/devtools/src/blitz/panels/gpu.ts +4 -2
  55. package/modules/devtools/src/blitz/panels/performance.ts +24 -12
  56. package/modules/devtools/src/blitz/panels/snapshot.ts +30 -8
  57. package/modules/devtools/src/blitz/panels/types.ts +2 -1
  58. package/modules/devtools/src/blitz/theme.ts +26 -4
  59. package/modules/html-ui/src/doc-backend.ts +67 -4
  60. package/modules/html-ui/src/host.ts +11 -3
  61. package/modules/html-ui/src/protocol.ts +1 -1
  62. package/package.json +2 -2
@@ -76,6 +76,19 @@ interface DocState {
76
76
  gpuTex: { ptr: number | bigint; tex: unknown; w: number; h: number } | null;
77
77
  stats: { frames: number; resolveMs: number; paintMs: number; diffMs: number; bytes: number };
78
78
  statsLast: number;
79
+ /** smoothWheel (PanelSpec): wheel deltas accumulate here and drain as
80
+ * eased per-tick steps in pump() instead of each event repainting an
81
+ * instant jump. Deltas/positions stay in doc CSS px (scale applied at
82
+ * dispatch); DOM sign convention (+y = scroll down). */
83
+ smoothWheel: boolean;
84
+ wheelRx: number;
85
+ wheelRy: number;
86
+ wheelX: number;
87
+ wheelY: number;
88
+ wheelMods?: string[];
89
+ /** performance.now() of the last step — the ease is dt-based so the curve
90
+ * holds regardless of pump cadence. */
91
+ wheelT: number;
79
92
  }
80
93
 
81
94
  // Bound-buffer header (u32 indices) — mirrors BoundBuf layout in lib.rs.
@@ -211,9 +224,32 @@ export function createDocCore(emit: (m: WorkerToUi) => void, gpuHooks?: DocGpuHo
211
224
  }
212
225
  }
213
226
 
227
+ // Smooth-wheel ease: fraction of the remainder applied per elapsed ms.
228
+ // τ≈45ms → ~30% per 16ms tick, ~95% drained in ~135ms — snappy but
229
+ // animated. A 1/MAX_TICKS floor bounds any backlog at ~320ms of drain —
230
+ // a fast wheel can't make scroll trail input indefinitely. Dispatching
231
+ // real wheel events keeps Blitz's hit-testing, scroll chaining, scrollbar
232
+ // reveal, and scroll DOM events intact.
233
+ const WHEEL_TAU_MS = 45;
234
+ const WHEEL_MAX_TICKS = 20;
235
+ function stepWheel(s: DocState, now: number): void {
236
+ const dt = Math.min(50, Math.max(1, now - s.wheelT));
237
+ s.wheelT = now;
238
+ const f = Math.max(1 - Math.exp(-dt / WHEEL_TAU_MS), 1 / WHEEL_MAX_TICKS);
239
+ // Snap the tail: a step below ~¾px/axis applies the whole residue instead
240
+ // of asymptoting through invisible sub-pixel repaints.
241
+ const dx = Math.abs(s.wheelRx) * f < 0.75 ? s.wheelRx : s.wheelRx * f;
242
+ const dy = Math.abs(s.wheelRy) * f < 0.75 ? s.wheelRy : s.wheelRy * f;
243
+ s.wheelRx -= dx;
244
+ s.wheelRy -= dy;
245
+ const k = s.scale;
246
+ s.doc.wheel(s.wheelX * k, s.wheelY * k, dx * k, dy * k, s.wheelMods);
247
+ }
248
+
214
249
  function pump() {
215
250
  const now = performance.now();
216
251
  for (const [id, s] of docs.entries()) {
252
+ if (s.smoothWheel && (s.wheelRx !== 0 || s.wheelRy !== 0)) stepWheel(s, now);
217
253
  const events = s.doc.pollEvents();
218
254
  if (events.length) emit({ type: "events", id, events });
219
255
  if (!s.doc.pending()) continue;
@@ -284,6 +320,8 @@ export function createDocCore(emit: (m: WorkerToUi) => void, gpuHooks?: DocGpuHo
284
320
  doc, scale: m.scale, cssW: m.cssW, cssH: m.cssH,
285
321
  maxFps: m.maxFps ?? 0, lastFrame: -1e9,
286
322
  sab: null, sabU32: null, sabDisabled: false, gpuTex: null,
323
+ smoothWheel: m.smoothWheel === true,
324
+ wheelRx: 0, wheelRy: 0, wheelX: 0, wheelY: 0, wheelT: 0,
287
325
  stats: freshStats(), statsLast: 0,
288
326
  };
289
327
  docs.set(m.id, s);
@@ -295,9 +333,16 @@ export function createDocCore(emit: (m: WorkerToUi) => void, gpuHooks?: DocGpuHo
295
333
  if (s) s.maxFps = m.maxFps;
296
334
  break;
297
335
  }
298
- case "setHtml":
336
+ case "setHtml": {
299
337
  if (/float\s*:/.test(m.html)) emit({ type: "error", id: m.id, message: "CSS float hangs Blitz layout — use flex instead" });
300
- docs.get(m.id)?.doc.setHtml(m.html); break;
338
+ const s = docs.get(m.id);
339
+ if (!s) break;
340
+ // Drop any in-flight smooth-wheel drain — its remainder belongs to
341
+ // the document being replaced.
342
+ s.wheelRx = s.wheelRy = 0;
343
+ s.doc.setHtml(m.html);
344
+ break;
345
+ }
301
346
  case "resize": {
302
347
  const s = docs.get(m.id);
303
348
  if (s) {
@@ -403,9 +448,27 @@ export function createDocCore(emit: (m: WorkerToUi) => void, gpuHooks?: DocGpuHo
403
448
  const k = s.scale;
404
449
  switch (msg.kind) {
405
450
  case "move": s.doc.pointerMove(msg.x * k, msg.y * k, msg.mods); break;
406
- case "down": s.doc.pointerDown(msg.x * k, msg.y * k, msg.button, msg.mods); break;
451
+ case "down":
452
+ // A press interrupts any in-flight wheel scroll (mirrors
453
+ // Blitz's interrupt_animation semantics for user input).
454
+ s.wheelRx = 0; s.wheelRy = 0;
455
+ s.doc.pointerDown(msg.x * k, msg.y * k, msg.button, msg.mods);
456
+ break;
407
457
  case "up": s.doc.pointerUp(msg.x * k, msg.y * k, msg.button, msg.mods); break;
408
- case "wheel": s.doc.wheel(msg.x * k, msg.y * k, msg.deltaX * k, msg.deltaY * k, msg.mods); break;
458
+ case "wheel":
459
+ if (s.smoothWheel) {
460
+ // A fresh gesture back-dates wheelT so the first pump tick
461
+ // applies a full ~16ms step immediately — no dead frame.
462
+ if (s.wheelRx === 0 && s.wheelRy === 0) s.wheelT = performance.now() - 16;
463
+ s.wheelRx += msg.deltaX;
464
+ s.wheelRy += msg.deltaY;
465
+ s.wheelX = msg.x;
466
+ s.wheelY = msg.y;
467
+ s.wheelMods = msg.mods;
468
+ } else {
469
+ s.doc.wheel(msg.x * k, msg.y * k, msg.deltaX * k, msg.deltaY * k, msg.mods);
470
+ }
471
+ break;
409
472
  case "key": s.doc.key(msg.down, msg.key, msg.code, msg.text, msg.mods); break;
410
473
  }
411
474
  break;
@@ -10,6 +10,7 @@ import type { OsrDomEvent } from "@downdraft/engine/libraries/blitz-ui/native-os
10
10
  import type { InputEventControl, RendererInputBus } from "@downdraft/engine/module/renderer-module";
11
11
  import { getHostCapabilities, getNativeHost } from "@downdraft/engine/platform/runtime";
12
12
  import { createLogger } from "@downdraft/engine/util/logger";
13
+ import { untrackedTimeout } from "@downdraft/engine/util/session-timer";
13
14
  import { PanelBlitPass } from "./composite";
14
15
  import { createLocalBackend, createWorkerBackend, type DocBackend } from "./doc-backend";
15
16
  import type { DocInputMsg, DocMutation, NavNodeInfo, UiPointerMsg, WorkerToUi } from "./protocol";
@@ -40,6 +41,10 @@ export interface PanelSpec {
40
41
  zoom?: number;
41
42
  /** Raster-rate cap for animating docs (0 = uncapped). Static docs repaint on demand regardless. */
42
43
  maxFps?: number;
44
+ /** Ease wheel deltas out at frame cadence instead of applying each detent
45
+ * as an instant jump — scrolling animates smoothly and repaints stay at
46
+ * one per pump tick regardless of input rate. Default false. */
47
+ smoothWheel?: boolean;
43
48
  /** Initial markup (or `vdom` rendered beforehand by the caller). */
44
49
  html: string;
45
50
  /** When false the panel never receives pointer input and `panelAt` skips
@@ -306,7 +311,7 @@ export class HtmlUiHost {
306
311
  this.order.push(id);
307
312
  // Doc viewport scale = raster px per CSS px = scale * zoom; layout size
308
313
  // is rect/zoom CSS px so the raster still covers rect*scale texture px.
309
- this.send({ type: "create", id, cssW: spec.rect.w / zoom, cssH: spec.docH ?? spec.rect.h / zoom, scale: scale * zoom, html: spec.html, maxFps: spec.maxFps });
314
+ this.send({ type: "create", id, cssW: spec.rect.w / zoom, cssH: spec.docH ?? spec.rect.h / zoom, scale: scale * zoom, html: spec.html, maxFps: spec.maxFps, smoothWheel: spec.smoothWheel });
310
315
  // GPU-direct: create the panel texture eagerly so the worker's texBind
311
316
  // lands before its first upload — the host knows raster dims at mount.
312
317
  if (this.gpuReady) this.bindPanelTex(panel);
@@ -470,6 +475,9 @@ export class HtmlUiHost {
470
475
  * `incrementalDom` is false until then, and forever on old cdylibs). */
471
476
  get docCaps(): { incrementalDom: boolean } { return this.caps; }
472
477
 
478
+ /** Which doc backend is live — "worker" (raster off-thread) or "local". */
479
+ get backendKind(): "worker" | "local" { return this.backend.kind; }
480
+
473
481
  /** Compositor surface for GameRenderer's end-of-frame UI pass. */
474
482
  get compositor(): { hasContent(): boolean; render(pass: GPURenderPassEncoder, w: number, h: number): void } {
475
483
  return {
@@ -511,7 +519,7 @@ export class HtmlUiHost {
511
519
  this.terminating = true;
512
520
  this.send({ type: "gpuDetach" });
513
521
  const backend = this.backend;
514
- setTimeout(() => {
522
+ untrackedTimeout(() => {
515
523
  if (!this.gpuDetachAcked) backend.dispose();
516
524
  this.destroyRetired();
517
525
  }, 150);
@@ -912,7 +920,7 @@ export class HtmlUiHost {
912
920
  || (p.postedBindPtr !== null && BigInt(ptr) === p.postedBindPtr));
913
921
  if (borrowed) {
914
922
  const key = BigInt(ptr);
915
- const timer = setTimeout(() => {
923
+ const timer = untrackedTimeout(() => {
916
924
  if (this.retiredTex.delete(key)) t.destroy();
917
925
  }, 1000);
918
926
  this.retiredTex.set(key, { tex: t, timer });
@@ -52,7 +52,7 @@ export interface NavNodeInfo {
52
52
 
53
53
  /** Host → backend */
54
54
  export type UiToWorker =
55
- | { type: "create"; id: string; cssW: number; cssH: number; scale: number; html: string; maxFps?: number }
55
+ | { type: "create"; id: string; cssW: number; cssH: number; scale: number; html: string; maxFps?: number; smoothWheel?: boolean }
56
56
  | { type: "fps"; id: string; maxFps: number }
57
57
  | { type: "setHtml"; id: string; html: string }
58
58
  | { type: "resize"; id: string; cssW: number; cssH: number; scale: number }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@downdraft/engine",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/knackstedt/downdraft.git",
@@ -865,7 +865,7 @@
865
865
  "zustand": "^5.0.2"
866
866
  },
867
867
  "peerDependencies": {
868
- "@downdraft/platform-native": "^0.2.0"
868
+ "@downdraft/platform-native": "^0.3.0"
869
869
  },
870
870
  "peerDependenciesMeta": {
871
871
  "@downdraft/platform-native": {