feat(core): add TypeGPU/WebGPU runtime adapter (#755)

* feat(core): add TypeGPU/WebGPU runtime adapter

Adds a deterministic seek adapter for compositions that render with
TypeGPU or raw WebGPU. Follows the same push+poll pattern as the
Three.js adapter:

- Sets `window.__hfTypegpuTime` on every seek so render loops can
  poll it instead of `performance.now()`.
- Dispatches a `"hf-seek"` CustomEvent on `window` so compositions
  can imperatively re-render a single frame at the new seek position.

Compositions listen for the event and update their time uniform:

```js
window.addEventListener("hf-seek", (e) => render(e.detail.time));
```

Works with TypeGPU (docs.swmansion.com/TypeGPU) and raw WebGPU alike.
No assumptions are made about pipeline construction — multiple canvases
or renderers are supported by sharing the same event.

- 9 unit tests, all pass
- wired in init.ts adapter array
- `__hfTypegpuTime` declared in window.d.ts

* fix(core): deduplicate hf-seek dispatch across GPU adapters

Both three and typegpu adapters previously dispatched the same
"hf-seek" CustomEvent independently, causing any composition that
registered a listener to receive two events per seek tick — doubling
per-scrub GPU work even though the renders are idempotent.

Fix: extract a shared `dispatchSeekEvent` helper (seek-dispatch.ts)
that deduplicates by exact float equality within the same synchronous
call stack. Both adapters now call this helper instead of dispatching
directly.

Also adds:
- `resetSeekDispatchState()` export for test isolation
- `beforeEach` reset in three.test.ts and typegpu.test.ts
- New typegpu test: "duplicate seek to same time fires event only once"
- Docstring additions to typegpu.ts: render-mode determinism contract
  (await device.queue.onSubmittedWorkDone()) and navigator.gpu feature
  detection guidance for composition authors

* feat(core): video-texture render compat + TypeGPU skill

Adds the missing pieces for video-backed WebGPU effects in render mode:

- `video-texture-compat.ts`: monkey-patches `GPUQueue.copyExternalImageToTexture`
  to detect the engine's injected `<img class="__render_frame__">` siblings and
  transparently substitute them for `<video>` sources. Headless Chrome can't
  supply decoded video frames to WebGPU, but the engine's pre-extracted frame
  images work. Falls through to the original path in preview mode.

- `patchVideoTextureCompat()` wired in init.ts after adapter array creation.

- `skills/typegpu/SKILL.md`: full authoring guide for TypeGPU/WebGPU compositions
  covering contract, timeline registration, video-backed effects, frosted blur
  via downsample pass, WGSL patterns, and deterministic rendering.

* test(producer): add typegpu-adapter regression test

Self-contained WebGPU composition with:
- Procedural gradient background (no video dependency)
- Animated ring driven by hf-seek time uniform
- Pulsing center glow
- Two GSAP-driven captions testing adapter sync

Verifies the TypeGPU adapter's hf-seek → WebGPU render pipeline
produces deterministic frames. workers: 1 for consistency.

Note: output.mp4 baseline needs to be generated in CI — the local
Docker image can't launch Chrome (ARM/x86 mismatch on Mac).
This commit is contained in:
Miguel Ángel
2026-05-13 03:31:56 +02:00
committed by GitHub
parent 82fd2967c4
commit ac671bdf5c
12 changed files with 750 additions and 7 deletions
+9
View File
@@ -35,6 +35,15 @@ declare global {
__HF_FPS?: number;
__HF_MAX_DURATION_SEC?: number;
__hfThreeTime?: number;
/**
* Current seek position in seconds, set by the TypeGPU/WebGPU adapter.
* Poll this from your WebGPU render loop instead of `performance.now()`
* to get the deterministic seek position.
*
* Also listen for the `"hf-seek"` CustomEvent on `window` for an
* imperative push signal: `window.addEventListener("hf-seek", e => render(e.detail.time))`.
*/
__hfTypegpuTime?: number;
__HF_PICKER_API?: HyperframePickerApi;
gsap?: {
timeline: (params?: { paused?: boolean }) => RuntimeTimelineLike;