@cyberart-io/engine 0.0.7 → 0.0.8

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/README.md CHANGED
@@ -90,6 +90,7 @@ Full API for the event router, deterministic replay, and CI harness (so agents c
90
90
  - [Events and router](docs/events.md) — mailbox, envelope, typed contracts, `createEventRouter`, hops, idempotency, rejections
91
91
  - [Deterministic mode](docs/deterministic-mode.md) — `step` / `schedule`, clocks, `ScriptedAction`, replay diffs
92
92
  - [Headless harness](docs/headless-harness.md) — `createHeadlessHarness` from `@cyberart-io/engine/headless`, software Canvas2D, `compareImageData` / `assertPixelsEqual`, inspect / screenshot
93
+ - [Frame benchmarking](docs/frame-benchmark.md) — `benchmarkCartFrames` (headless) and optional `onFrameTiming` (live); zero cost when unset
93
94
  - [Presentation adapter](docs/presentation-adapter.md) — host-owned render model, intents, loading / error / unsupported
94
95
  - [Asset resolver](docs/asset-resolver.md) — host-pluggable images/audio/fonts/spritesheets, cache, preload, fallbacks
95
96
  - [Presentation cue](docs/presentation-cue.md) — deterministic `createPresentationTimeline`, duplicate policy, reduced-motion, lifecycle events
@@ -272,6 +273,8 @@ CI and agents should drive the **same** `createRuntime({ deterministic })` path.
272
273
 
273
274
  Full options, `click` clock rule, Node-only `captureFrame`, remount, visual asserts, and the reproduce command: [headless harness](docs/headless-harness.md).
274
275
 
276
+ Hash / trait cost A/B without a browser: `benchmarkCartFrames` / `formatFrameBenchmarkResult` from the same headless entry. Live overlays: `runtime.onFrameTiming`. Details and the zero-overhead-when-off contract: [frame benchmarking](docs/frame-benchmark.md).
277
+
275
278
  ```ts
276
279
  import { createHeadlessHarness } from '@cyberart-io/engine/headless';
277
280
 
@@ -333,6 +333,73 @@ type CartStateBundle = {
333
333
  state: unknown;
334
334
  };
335
335
 
336
+ /**
337
+ * Copyright (c) 2026 Aaron Boyarsky
338
+ * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
339
+ * See packages/engine/LICENSE
340
+ *
341
+ * Host-controlled time, input, and asset completion for deterministic replays.
342
+ * Production kaleidoscope / Art Blocks playback does not enable this mode.
343
+ * Logical asset URLs are resolved by the host preloader (`assetResolver.ts`);
344
+ * this module only times `ASSET_READY_EVENT` / `ASSET_FAILED_EVENT` delivery.
345
+ */
346
+
347
+ type PointerKind = 'down' | 'move' | 'up';
348
+ type ScriptedAction = {
349
+ atFrame: number;
350
+ } & ({
351
+ type: 'pointer';
352
+ pointer: {
353
+ kind: PointerKind;
354
+ x: number;
355
+ y: number;
356
+ };
357
+ } | {
358
+ type: 'key';
359
+ key: string;
360
+ } | {
361
+ type: 'event';
362
+ event: HostEvent;
363
+ } | {
364
+ type: 'asset';
365
+ id: string;
366
+ status: 'ready' | 'failed';
367
+ /** Optional structured failure or resolved resource. Envelope is unchanged. */
368
+ detail?: unknown;
369
+ });
370
+ type DeterministicRuntimeOptions = {
371
+ /** Virtual clock origin in ms. Default 0. */
372
+ origin?: number;
373
+ /** Actions applied at the start of `atFrame`, before `update`. */
374
+ actions?: ScriptedAction[];
375
+ };
376
+ type ClockSnapshot = {
377
+ now: number;
378
+ framesElapsed: number;
379
+ frameRate: number;
380
+ };
381
+ type ReplayMetadata = {
382
+ seed: string;
383
+ clock: ClockSnapshot;
384
+ rng: RandomState;
385
+ actions: ScriptedAction[];
386
+ applied: AppliedAction[];
387
+ events: HostEvent[];
388
+ state: unknown;
389
+ };
390
+ type AppliedAction = {
391
+ frame: number;
392
+ action: ScriptedAction;
393
+ };
394
+
395
+ type FrameTimingSample = {
396
+ frame: number;
397
+ updateMs: number;
398
+ renderMs: number;
399
+ drawMs: number;
400
+ totalMs: number;
401
+ };
402
+
336
403
  /**
337
404
  * Copyright (c) 2026 Aaron Boyarsky
338
405
  * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
@@ -403,65 +470,6 @@ type ExportSnapshotOptions = {
403
470
  runtimeVersion?: string;
404
471
  };
405
472
 
406
- /**
407
- * Copyright (c) 2026 Aaron Boyarsky
408
- * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
409
- * See packages/engine/LICENSE
410
- *
411
- * Host-controlled time, input, and asset completion for deterministic replays.
412
- * Production kaleidoscope / Art Blocks playback does not enable this mode.
413
- * Logical asset URLs are resolved by the host preloader (`assetResolver.ts`);
414
- * this module only times `ASSET_READY_EVENT` / `ASSET_FAILED_EVENT` delivery.
415
- */
416
-
417
- type PointerKind = 'down' | 'move' | 'up';
418
- type ScriptedAction = {
419
- atFrame: number;
420
- } & ({
421
- type: 'pointer';
422
- pointer: {
423
- kind: PointerKind;
424
- x: number;
425
- y: number;
426
- };
427
- } | {
428
- type: 'key';
429
- key: string;
430
- } | {
431
- type: 'event';
432
- event: HostEvent;
433
- } | {
434
- type: 'asset';
435
- id: string;
436
- status: 'ready' | 'failed';
437
- /** Optional structured failure or resolved resource. Envelope is unchanged. */
438
- detail?: unknown;
439
- });
440
- type DeterministicRuntimeOptions = {
441
- /** Virtual clock origin in ms. Default 0. */
442
- origin?: number;
443
- /** Actions applied at the start of `atFrame`, before `update`. */
444
- actions?: ScriptedAction[];
445
- };
446
- type ClockSnapshot = {
447
- now: number;
448
- framesElapsed: number;
449
- frameRate: number;
450
- };
451
- type ReplayMetadata = {
452
- seed: string;
453
- clock: ClockSnapshot;
454
- rng: RandomState;
455
- actions: ScriptedAction[];
456
- applied: AppliedAction[];
457
- events: HostEvent[];
458
- state: unknown;
459
- };
460
- type AppliedAction = {
461
- frame: number;
462
- action: ScriptedAction;
463
- };
464
-
465
473
  declare const ASSET_KINDS: readonly ["image", "audio", "font", "spritesheet"];
466
474
  type AssetKind = (typeof ASSET_KINDS)[number];
467
475
  declare const ASSET_FAILURE_CODES: readonly ["timeout", "cors", "not-found", "invalid", "aborted", "resolver"];
@@ -630,6 +638,7 @@ type FrameErrorInfo = {
630
638
  consecutive: number;
631
639
  stopped: boolean;
632
640
  };
641
+
633
642
  type CreateRuntimeOptions = {
634
643
  /** Required mount point. The runtime creates or adopts a canvas inside this element. */
635
644
  container: HTMLElement;
@@ -679,6 +688,11 @@ type CreateRuntimeOptions = {
679
688
  * `update`, and host-channel events still run. Default `'render'`.
680
689
  */
681
690
  kind?: CartKind;
691
+ /**
692
+ * Optional per-frame update/render/draw timings. Can also be assigned later
693
+ * via `runtime.onFrameTiming`. Unset = no `performance.now` in the draw loop.
694
+ */
695
+ onFrameTiming?: (sample: FrameTimingSample) => void;
682
696
  };
683
697
  type MountOptions<T = unknown> = {
684
698
  /** Boot overrides passed as `customState` into `getDefaultState`. Not a live-state replay. */
@@ -760,6 +774,11 @@ type CyberArtRuntime = {
760
774
  /** `'render'` (default) or `'calculation'` (no canvas / paint). */
761
775
  readonly kind: CartKind;
762
776
  onError?: (error: unknown, info: FrameErrorInfo) => void;
777
+ /**
778
+ * Optional per-frame update/render/draw timings. Unset = zero overhead in the
779
+ * draw loop (single null check). Same pattern as `onError`.
780
+ */
781
+ onFrameTiming?: (sample: FrameTimingSample) => void;
763
782
  };
764
783
 
765
784
  /**
@@ -1689,6 +1708,53 @@ type VisualLayerCapture = {
1689
1708
  declare function captureVisualLayers(controller: VisualLayerController): VisualLayerCapture;
1690
1709
  declare function createVisualLayerController(options: CreateVisualLayerControllerOptions): VisualLayerController;
1691
1710
 
1711
+ /**
1712
+ * Copyright (c) 2026 Aaron Boyarsky
1713
+ * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
1714
+ * See packages/engine/LICENSE
1715
+ *
1716
+ * Cart-agnostic update/render frame benchmark for CI and agent workflows.
1717
+ * Prefer this over ad-hoc vitest probes when evaluating hash / trait cost.
1718
+ */
1719
+
1720
+ type FrameBenchmarkOptions = {
1721
+ /** Measured frames after warmup. Default 30. */
1722
+ frames?: number;
1723
+ /** Discarded frames before measurement. Default 5. */
1724
+ warmupFrames?: number;
1725
+ /** Simulated frame advance in ms. Default 1000/60. */
1726
+ frameStepMs?: number;
1727
+ width?: number;
1728
+ height?: number;
1729
+ tokenId?: string;
1730
+ /**
1731
+ * Mutate state after `getDefaultState` (e.g. skip Tone init in jsdom by
1732
+ * setting `audioContextStarted = true`).
1733
+ */
1734
+ prepareState?: (state: unknown, featureState: unknown) => void;
1735
+ /** Optional stub drawing context; defaults to a no-op `putImageData`. */
1736
+ drawingContext?: CanvasRenderingContext2D;
1737
+ };
1738
+ type FrameBenchmarkResult = {
1739
+ frames: number;
1740
+ updateAvgMs: number;
1741
+ renderAvgMs: number;
1742
+ totalAvgMs: number;
1743
+ updateMaxMs: number;
1744
+ renderMaxMs: number;
1745
+ estFps: number;
1746
+ updatePct: number;
1747
+ renderPct: number;
1748
+ };
1749
+ /**
1750
+ * Run a cart's update/render loop headlessly and report average / max phase
1751
+ * timings. Does not start audio or mount a live AnimationManager — suitable
1752
+ * for jsdom vitest and agent hash probes.
1753
+ */
1754
+ declare function benchmarkCartFrames<T, TFeatureState = undefined>(cart: AnimationCart<T, TFeatureState>, hash: string, rawParams: number[], options?: FrameBenchmarkOptions): FrameBenchmarkResult;
1755
+ /** Round timing fields for stable console / snapshot logging. */
1756
+ declare function formatFrameBenchmarkResult(result: FrameBenchmarkResult, digits?: number): Record<string, number>;
1757
+
1692
1758
  /**
1693
1759
  * Copyright (c) 2026 Aaron Boyarsky
1694
1760
  * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
@@ -1709,4 +1775,4 @@ type WriteComposedFrameResult = ComposedFrame & {
1709
1775
  */
1710
1776
  declare function writeComposedFrame(compositor: Compositor, path?: string): Promise<WriteComposedFrameResult>;
1711
1777
 
1712
- export { type BoundReplaySession, type CausationTreeNode, type CreateHeadlessHarnessOptions, type CreateHeadlessMultiCartHarnessOptions, type CreateReplayInspectorOptions, DEFAULT_HEADLESS_HEIGHT, DEFAULT_HEADLESS_WIDTH, type GlyphAtlas, HEADLESS_PNG_DATA_URL, type HeadlessAudioAdapter, type HeadlessCanvas2DSettings, type HeadlessFrameError, type HeadlessHarness, type HeadlessImageFixture, type HeadlessInspect, type HeadlessMultiCartHarness, HeadlessUnsupportedOperationError, type InspectorRecord, type InstallHeadlessCanvasOptions, type ReplayCompareResult, type ReplayInspector, type ReplayInspectorExport, type ReplayInspectorReport, type ReplayParticipantSummary, type ReplayTapeAction, type ReplayTraceFilter, UPDATE_GOLDEN_ENV, type VisualArtifactPaths, type VisualArtifacts, type VisualCompareOptions, type VisualCompareResult, type VisualLayerCapture, type VisualLayerController, type VisualLayerInspect, type WriteComposedFrameResult, assertPixelsEqual, assertPngDataUrlsEqual, attachHeadlessCanvas2D, captureVisualLayers, compareImageData, compareReplayTraces, createDefaultGlyphAtlas, createHeadlessAudioAdapter, createHeadlessHarness, createHeadlessMultiCartHarness, createImageFixture, createReplayInspector, createVisualLayerController, decodePng, encodePng, encodePngDataUrl, getHeadlessSurface, imageDataFromPngDataUrl, installHeadlessCanvas, makeImageData, replayExportedTrace, setDefaultGlyphAtlas, shouldUpdateGolden, writeComposedFrame, writeVisualArtifacts };
1778
+ export { type BoundReplaySession, type CausationTreeNode, type CreateHeadlessHarnessOptions, type CreateHeadlessMultiCartHarnessOptions, type CreateReplayInspectorOptions, DEFAULT_HEADLESS_HEIGHT, DEFAULT_HEADLESS_WIDTH, type FrameBenchmarkOptions, type FrameBenchmarkResult, type GlyphAtlas, HEADLESS_PNG_DATA_URL, type HeadlessAudioAdapter, type HeadlessCanvas2DSettings, type HeadlessFrameError, type HeadlessHarness, type HeadlessImageFixture, type HeadlessInspect, type HeadlessMultiCartHarness, HeadlessUnsupportedOperationError, type InspectorRecord, type InstallHeadlessCanvasOptions, type ReplayCompareResult, type ReplayInspector, type ReplayInspectorExport, type ReplayInspectorReport, type ReplayParticipantSummary, type ReplayTapeAction, type ReplayTraceFilter, UPDATE_GOLDEN_ENV, type VisualArtifactPaths, type VisualArtifacts, type VisualCompareOptions, type VisualCompareResult, type VisualLayerCapture, type VisualLayerController, type VisualLayerInspect, type WriteComposedFrameResult, assertPixelsEqual, assertPngDataUrlsEqual, attachHeadlessCanvas2D, benchmarkCartFrames, captureVisualLayers, compareImageData, compareReplayTraces, createDefaultGlyphAtlas, createHeadlessAudioAdapter, createHeadlessHarness, createHeadlessMultiCartHarness, createImageFixture, createReplayInspector, createVisualLayerController, decodePng, encodePng, encodePngDataUrl, formatFrameBenchmarkResult, getHeadlessSurface, imageDataFromPngDataUrl, installHeadlessCanvas, makeImageData, replayExportedTrace, setDefaultGlyphAtlas, shouldUpdateGolden, writeComposedFrame, writeVisualArtifacts };