Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ packages/producer/src/services/fontData.generated.ts
qa-artifacts/
my-video/
examples/
!registry/examples/
!registry/examples/**
packages/studio/data/
.desloppify/
.worktrees/
Expand Down
1 change: 1 addition & 0 deletions docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"guides/prompting",
"guides/hyperframes-vs-remotion",
"guides/gsap-animation",
"guides/webgpu",
"guides/rendering",
"guides/deploy",
"guides/remove-background",
Expand Down
23 changes: 23 additions & 0 deletions docs/examples.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,17 @@ npx hyperframes init my-video --example <name>
<video className="w-full aspect-video object-cover block" src="https://static.heygen.ai/hyperframes-oss/docs/images/templates/nyt-graph.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/templates/nyt-graph.png" muted loop playsInline preload="metadata" />
<div className="absolute bottom-0 left-0 right-0 pt-6 pb-2 px-3" style={{background: "linear-gradient(transparent, rgba(0,0,0,0.75))"}}><strong className="text-sm font-semibold text-white block">NYT Graph</strong><span className="text-xs text-zinc-300">Data stories</span></div>
</div>
<div className="tpl-card relative rounded-xl overflow-hidden bg-zinc-950 transition-transform hover:-translate-y-0.5 hover:shadow-xl">
<div className="w-full aspect-video block relative overflow-hidden" style={{background: "#080b12"}}>
<div className="absolute inset-0" style={{background: "linear-gradient(135deg, #111827, #020617)"}} />
<div className="absolute inset-0 opacity-50" style={{backgroundImage: "linear-gradient(rgba(255,255,255,0.08) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.08) 1px, transparent 1px)", backgroundSize: "48px 48px"}} />
<div className="absolute inset-x-8 top-10 h-28 rounded" style={{background: "rgba(255,255,255,0.08)", border: "1px solid rgba(255,255,255,0.16)"}} />
<div className="absolute left-10 bottom-12 h-3 w-48 rounded-full" style={{background: "#2dd4bf"}} />
<div className="absolute left-10 bottom-20 h-7 w-80 rounded-sm" style={{background: "rgba(255,255,255,0.88)"}} />
<div className="absolute right-12 bottom-14 h-24 w-24 rotate-12 rounded-sm" style={{background: "linear-gradient(135deg, #2dd4bf, #f472b6 54%, #fde68a)"}} />
</div>
<div className="absolute bottom-0 left-0 right-0 pt-6 pb-2 px-3" style={{background: "linear-gradient(transparent, rgba(0,0,0,0.75))"}}><strong className="text-sm font-semibold text-white block">WebGPU Showcase</strong><span className="text-xs text-zinc-300">Mixed DOM and GPU canvas</span></div>
</div>
</div>

## Portrait Templates
Expand Down Expand Up @@ -70,6 +81,7 @@ npx hyperframes init my-video --example <name>
| `decision-tree` | Diagrammatic | Landscape | Explainers, tutorials |
| `product-promo` | Multi-scene | Landscape | Product showcases, demos |
| `nyt-graph` | Editorial data | Landscape | Data stories, reports |
| `webgpu-showcase` | GPU canvas + DOM | Landscape | WebGPU, TypeGPU, shader-backed scenes |
| `vignelli` | Bold, typographic | Portrait | Headlines, announcements |
| `blank` | Minimal scaffolding | — | Full control, agent-generated |

Expand Down Expand Up @@ -199,6 +211,17 @@ npx hyperframes init my-video --example <name>
└── nyt-chart.html
```
</Tab>
<Tab title="webgpu-showcase">
### webgpu-showcase

Mixed DOM and WebGPU canvas composition that uses the same seek-driven render path as ordinary HTML scenes.

```
my-video/
├── meta.json
└── index.html
```
</Tab>
<Tab title="blank">
### blank

Expand Down
6 changes: 6 additions & 0 deletions docs/guides/rendering.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ Render your Hyperframes [compositions](/concepts/compositions) to MP4, MOV, or W
| `--max-concurrent-renders` | 1-10 | 2 | Max simultaneous renders via the producer server (see [Concurrent Renders](#concurrent-renders) below) |
| `--gpu` | — | off | GPU encoding (NVENC, VideoToolbox, VAAPI, QSV) |
| `--browser-gpu` / `--no-browser-gpu` | — | on locally, off in Docker | Use or opt out of host GPU acceleration for local Chrome/WebGL capture |
| `--webgpu` | auto, required, off | auto locally, off in Docker | WebGPU policy for raw WebGPU and TypeGPU scenes. See [WebGPU and TypeGPU](/guides/webgpu) |
| `--webgpu-unsafe` | — | off | Pass Chrome's unsafe WebGPU opt-in flag for local adapters that require it |
| `--hdr` | — | off | Force HDR output even if no HDR sources are detected (MP4 only). See [HDR Rendering](/guides/hdr) |
| `--sdr` | — | off | Force SDR output even if HDR sources are detected |
| `--docker` | — | off | Use Docker for [deterministic rendering](/concepts/determinism) |
Expand Down Expand Up @@ -158,6 +160,7 @@ Hyperframes has two separate GPU acceleration surfaces:

- `--gpu` uses a hardware video encoder in FFmpeg when one is available. Supported backends include VideoToolbox on macOS, NVENC on NVIDIA systems, VAAPI on Linux, and Intel QSV on supported Windows/Linux hosts.
- Browser GPU uses the host GPU for local Chrome/WebGL capture. It is enabled automatically for local renders and disabled in Docker. Use `--no-browser-gpu` to opt out.
- WebGPU capture uses Chrome's WebGPU adapter for raw WebGPU and TypeGPU canvases. It is `auto` for local renders and `off` in Docker. Use `--webgpu required` for WebGPU-only scenes.

```bash Terminal
# Add hardware FFmpeg encoding to the default local browser-GPU render
Expand All @@ -168,6 +171,9 @@ npx hyperframes render --no-browser-gpu --output software-browser.mp4

# Use browser GPU plus hardware FFmpeg encoding
npx hyperframes render --gpu --output gpu.mp4

# Require WebGPU before capturing a WebGPU/TypeGPU composition
npx hyperframes render --webgpu required --output webgpu.mp4
```

Browser GPU capture is local-mode only. It maps to platform-native Chrome GPU backends: Metal on macOS, D3D11 on Windows, and EGL on Linux. Use `--no-browser-gpu` or Docker mode when exact cross-machine reproducibility matters more than local render speed.
Expand Down
16 changes: 16 additions & 0 deletions docs/guides/troubleshooting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,22 @@ If your issue is about a specific coding mistake (animations not working, video
- **Image pull fails:** Check your internet connection; the first render downloads the Hyperframes Docker image
</Accordion>

<Accordion title="WebGPU render says WebGPU is unavailable">
WebGPU and TypeGPU renders use local Chrome's WebGPU adapter. Use required mode when the scene cannot fall back:

```bash Terminal
npx hyperframes render --webgpu required --output output.mp4
```

If Chrome exposes the adapter only behind an opt-in flag, add:

```bash Terminal
npx hyperframes render --webgpu required --webgpu-unsafe --output output.mp4
```

Docker mode keeps WebGPU off because browser WebGPU passthrough is not portable across Docker hosts. Render WebGPU compositions locally, or use `--webgpu off` only for scenes with a non-WebGPU fallback.
</Accordion>

<Accordion title="Render is slow">
Try these optimizations:

Expand Down
93 changes: 93 additions & 0 deletions docs/guides/webgpu.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
---
title: WebGPU and TypeGPU
description: Build seekable WebGPU and TypeGPU compositions that render through the HyperFrames CLI.
---

# WebGPU and TypeGPU

HyperFrames supports raw WebGPU and TypeGPU through the `typegpu` runtime adapter. WebGPU scenes use the same seek-driven model as GSAP, Three.js, canvas, and HTML scenes: render the exact frame requested by HyperFrames, then let the CLI capture Chromium's composited output.

## Runtime Contract

Use HyperFrames time, not wall-clock time:

```js
function render(timeSeconds) {
// update uniforms, encode commands, submit work
}

window.addEventListener("hf-seek", (event) => {
render(event.detail.time);
});

render(window.__hfTypegpuTime ?? 0);
```

HyperFrames auto-instruments main-thread devices created with
`navigator.gpu.requestAdapter().requestDevice()` and wraps `device.queue.submit(...)` when the
runtime is loaded before your WebGPU code. Explicit registration is still recommended when you
control the code because it makes the render contract obvious:

```js
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) throw new Error("WebGPU unavailable");

const device = await adapter.requestDevice();
window.__hfWebGpu?.registerDevice(device);

function render(timeSeconds) {
device.queue.writeBuffer(timeBuffer, 0, new Float32Array([timeSeconds]));

const encoder = device.createCommandEncoder();
// render pass...
device.queue.submit([encoder.finish()]);

window.__hfWebGpu?.registerFrame(device.queue.onSubmittedWorkDone());
}
```

`registerDevice(device)` is enough for most scenes. Use `registerFrame(promise)` when your renderer has a more specific per-frame completion promise.

## Async Setup

If the first captured frame depends on async pipeline setup, register readiness:

```js
window.__hfWebGpu?.setReady(
(async () => {
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) throw new Error("WebGPU unavailable");
const device = await adapter.requestDevice();
window.__hfWebGpu?.registerDevice(device);
return device;
})(),
);
```

## Rendering

Local renders enable WebGPU automatically when Chromium supports it:

```bash
hyperframes render --output out.mp4
```

Use required mode for WebGPU-only compositions:

```bash
hyperframes render --webgpu required --output out.mp4
```

If your local Chrome needs explicit WebGPU opt-in:

```bash
hyperframes render --webgpu required --webgpu-unsafe --output out.mp4
```

Docker renders keep browser WebGPU off because there is no portable cross-platform WebGPU passthrough for deterministic container capture.

## Mixing Scene Types

WebGPU canvases can be layered with DOM, HTML canvas, SVG, video frame injection, GSAP timelines, and other adapters. The producer detects WebGPU/TypeGPU usage and routes those compositions through screenshot capture so Chromium composites all layers together.

Keep the DOM and GPU layers in the same composition tree and seek everything from the same `hf-seek` event. A main-thread `requestAnimationFrame` loop that submits WebGPU work is captured, but it must render from `window.__hfTypegpuTime` or another seek-driven value instead of wall-clock time if the export needs to be deterministic. Worker or `OffscreenCanvas` WebGPU paths need an explicit main-thread `window.__hfWebGpuWaitForFrame(time)` bridge because the runtime cannot observe queue submissions inside the worker.
6 changes: 6 additions & 0 deletions docs/packages/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ This is suppressed in CI environments, non-TTY shells, and when `HYPERFRAMES_NO_
| `play-mode` | Playful elastic animations |
| `swiss-grid` | Structured grid layout |
| `vignelli` | Bold typography with red accents |
| `webgpu-showcase` | Mixed DOM and WebGPU canvas scene |

In non-interactive mode, `--example` is required — the CLI errors with a usage example if missing. In interactive mode (default on TTY), you choose the example interactively. Pass `--non-interactive` to require `--example` via flag. When `--video` or `--audio` is provided, the CLI automatically transcribes the audio with Whisper and patches captions into the composition (use `--skip-transcribe` to disable).

Expand Down Expand Up @@ -602,6 +603,9 @@ This is suppressed in CI environments, non-TTY shells, and when `HYPERFRAMES_NO_

# Add hardware FFmpeg encoding
npx hyperframes render --gpu --output gpu.mp4

# Require WebGPU for raw WebGPU or TypeGPU scenes
npx hyperframes render --webgpu required --output webgpu.mp4
```

| Flag | Values | Default | Description |
Expand All @@ -618,6 +622,8 @@ This is suppressed in CI environments, non-TTY shells, and when `HYPERFRAMES_NO_
| `--workers` | 1-8 | 4 | Parallel render workers |
| `--gpu` | — | off | GPU encoding (NVENC, VideoToolbox, VAAPI, QSV) |
| `--browser-gpu` / `--no-browser-gpu` | — | on locally, off in Docker | Use or opt out of host GPU acceleration for local Chrome/WebGL capture |
| `--webgpu` | auto, required, off | auto locally, off in Docker | WebGPU policy for raw WebGPU and TypeGPU scenes. See [WebGPU and TypeGPU](/guides/webgpu) |
| `--webgpu-unsafe` | — | off | Pass Chrome's unsafe WebGPU opt-in flag for local adapters that require it |
| `--docker` | — | off | Use Docker for [deterministic rendering](/concepts/determinism) |
| `--quiet` | — | off | Suppress verbose output |
| `--variables` | JSON object | — | Variable overrides merged over `data-composition-variables` defaults. Read via `window.__hyperframes.getVariables()` |
Expand Down
8 changes: 5 additions & 3 deletions docs/packages/producer.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -229,15 +229,17 @@ npx hyperframes doctor
The CLI enables local Chrome/WebGL GPU capture automatically and supports `--no-browser-gpu` as an opt-out. When using the producer API directly, pass an engine config override:

```typescript
import { resolveConfig } from '@hyperframes/producer';
import { createRenderJob, resolveConfig } from "@hyperframes/producer";

const job = createRenderJob({
fps: 30,
quality: 'standard',
producerConfig: resolveConfig({ browserGpuMode: 'hardware' }),
quality: "standard",
producerConfig: resolveConfig({ browserGpuMode: "hardware" }),
});
```

WebGPU and TypeGPU compositions are auto-detected during compile. Direct producer renders lift the default browser capture config to `browserGpuMode: "auto"` and `browserWebGpuMode: "auto"` when static hints prove the scene uses WebGPU, unless you provided an explicit `producerConfig` or env override. Pin `browserWebGpuMode: "required"` when a render must fail fast if Chrome cannot expose WebGPU.

## Additional Exports

The producer also re-exports key engine functionality for convenience:
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"lint:fix": "oxlint --fix .",
"format": "oxfmt .",
"test": "bun run --filter '*' test",
"test:webgpu": "bun run --filter @hyperframes/producer test:webgpu",
"player:perf": "bun run --filter @hyperframes/player perf",
"format:check": "oxfmt --check .",
"knip": "knip",
Expand Down
16 changes: 7 additions & 9 deletions packages/cli/src/capture/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,20 +72,18 @@ export async function captureWebsite(
const { ensureBrowser } = await import("../browser/manager.js");
const browser = await ensureBrowser();
const puppeteer = await import("puppeteer-core");
const { buildChromeArgs } = await import("@hyperframes/engine");
const browserGpuMode =
process.env.PRODUCER_BROWSER_GPU_MODE === "software" ? "software" : "hardware";
const chromeBrowser = await puppeteer.default.launch({
headless: true,
executablePath: browser.executablePath,
args: [
"--no-sandbox",
"--disable-dev-shm-usage",
"--enable-webgl",
"--ignore-gpu-blocklist",
"--use-gl=angle",
"--use-angle=swiftshader",
...buildChromeArgs(
{ width: viewportWidth, height: viewportHeight, captureMode: "screenshot" },
{ browserGpuMode, browserWebGpuMode: "auto" },
),
"--disable-blink-features=AutomationControlled",
"--disable-background-timer-throttling",
"--disable-renderer-backgrounding",
`--window-size=${viewportWidth},${viewportHeight}`,
],
});

Expand Down
17 changes: 9 additions & 8 deletions packages/cli/src/commands/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { c } from "../ui/colors.js";
import { resolveProject } from "../utils/project.js";
import { serveStaticProjectHtml } from "../utils/staticProjectServer.js";
import { withMeta } from "../utils/updateCheck.js";
import { waitForOptionalWebGpuFrame } from "../utils/webgpuFrame.js";
import {
buildLayoutSampleTimes,
collapseStaticLayoutIssues,
Expand Down Expand Up @@ -87,6 +88,7 @@ async function seekTo(page: import("puppeteer-core").Page, time: number): Promis
}
}
}, time);
await waitForOptionalWebGpuFrame(page, time);
await page.evaluate(
() =>
new Promise<void>((resolveFrame) =>
Expand Down Expand Up @@ -147,17 +149,16 @@ async function runLayoutAudit(

try {
const browser = await ensureBrowser();
const { buildChromeArgs } = await import("@hyperframes/engine");
const browserGpuMode =
process.env.PRODUCER_BROWSER_GPU_MODE === "software" ? "software" : "hardware";
chromeBrowser = await puppeteer.default.launch({
headless: true,
executablePath: browser.executablePath,
args: [
"--no-sandbox",
"--disable-gpu",
"--disable-dev-shm-usage",
"--enable-webgl",
"--use-gl=angle",
"--use-angle=swiftshader",
],
args: buildChromeArgs(
{ width: 1920, height: 1080, captureMode: "screenshot" },
{ browserGpuMode, browserWebGpuMode: "auto" },
),
});

const page = await chromeBrowser.newPage();
Expand Down
32 changes: 28 additions & 4 deletions packages/cli/src/commands/render.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@ describe("renderLocal browser GPU config", () => {
// `beforeAll` keeps every test fast and isolated.
let renderLocal: typeof import("./render.js").renderLocal;
let resolveBrowserGpuForCli: typeof import("./render.js").resolveBrowserGpuForCli;
let resolveBrowserWebGpuForCli: typeof import("./render.js").resolveBrowserWebGpuForCli;

beforeAll(async () => {
({ renderLocal, resolveBrowserGpuForCli } = await import("./render.js"));
({ renderLocal, resolveBrowserGpuForCli, resolveBrowserWebGpuForCli } =
await import("./render.js"));
});

function setEnv(key: string, value: string) {
Expand Down Expand Up @@ -77,7 +79,11 @@ describe("renderLocal browser GPU config", () => {
quiet: true,
});

expect(producerState.resolveConfigCalls).toContainEqual({ browserGpuMode: "software" });
expect(producerState.resolveConfigCalls).toContainEqual({
browserGpuMode: "software",
browserWebGpuMode: "off",
browserWebGpuUnsafe: false,
});
expect(producerState.createdJobs[0]?.producerConfig).toMatchObject({
browserGpuMode: "software",
resolved: true,
Expand All @@ -95,7 +101,11 @@ describe("renderLocal browser GPU config", () => {
quiet: true,
});

expect(producerState.resolveConfigCalls).toContainEqual({ browserGpuMode: "auto" });
expect(producerState.resolveConfigCalls).toContainEqual({
browserGpuMode: "auto",
browserWebGpuMode: "off",
browserWebGpuUnsafe: false,
});
expect(producerState.createdJobs[0]?.producerConfig).toMatchObject({
browserGpuMode: "auto",
resolved: true,
Expand All @@ -113,7 +123,11 @@ describe("renderLocal browser GPU config", () => {
quiet: true,
});

expect(producerState.resolveConfigCalls).toContainEqual({ browserGpuMode: "hardware" });
expect(producerState.resolveConfigCalls).toContainEqual({
browserGpuMode: "hardware",
browserWebGpuMode: "off",
browserWebGpuUnsafe: false,
});
expect(producerState.createdJobs[0]?.producerConfig).toMatchObject({
browserGpuMode: "hardware",
resolved: true,
Expand All @@ -135,6 +149,16 @@ describe("renderLocal browser GPU config", () => {
expect(resolveBrowserGpuForCli(true, undefined, "auto")).toBe("software");
});

it("resolves browser WebGPU mode from CLI flags, Docker mode, and env fallback", () => {
expect(resolveBrowserWebGpuForCli(false, undefined, undefined)).toBe("auto");
expect(resolveBrowserWebGpuForCli(false, undefined, "required")).toBe("required");
expect(resolveBrowserWebGpuForCli(false, "off", "required")).toBe("off");
expect(resolveBrowserWebGpuForCli(false, "auto", "off")).toBe("auto");
expect(resolveBrowserWebGpuForCli(false, "required", "off")).toBe("required");
expect(resolveBrowserWebGpuForCli(true, undefined, "required")).toBe("off");
expect(resolveBrowserWebGpuForCli(true, "required", "off")).toBe("off");
});

it("forwards parsed --variables payload to createRenderJob", async () => {
await renderLocal("/tmp/project", "/tmp/out.mp4", {
fps: { num: 30, den: 1 },
Expand Down
Loading