Skip to content
Merged
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
29 changes: 26 additions & 3 deletions docs/guides/rendering.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ Render your Hyperframes [compositions](/concepts/compositions) to MP4, MOV, or W

**Pros:**
- Fast startup, no container overhead
- Uses your system GPU for hardware-accelerated encoding (with `--gpu`)
- Can use your system GPU for Chrome/WebGL capture by default
- Can use your system GPU for hardware-accelerated encoding (with `--gpu`)
- Best for iterative development

**Cons:**
Expand All @@ -90,7 +91,8 @@ Render your Hyperframes [compositions](/concepts/compositions) to MP4, MOV, or W

**Cons:**
- Slower startup due to container initialization
- No GPU acceleration inside the container
- Browser capture stays on the deterministic software-GL path
- GPU encoding requires Docker host GPU passthrough and is not cross-platform on Docker Desktop

<Note>
Docker mode uses `chrome-headless-shell` with [BeginFrame](/concepts/determinism#how-it-works) control for frame-perfect, deterministic capture.
Expand Down Expand Up @@ -121,7 +123,8 @@ Render your Hyperframes [compositions](/concepts/compositions) to MP4, MOV, or W
| `--video-bitrate` | e.g. `10M`, `5000k` | — | Target bitrate encoding. Cannot combine with `--crf` |
| `--workers` | 1-8 or `auto` | auto | Parallel render workers (see [Workers](#workers) below) |
| `--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) |
| `--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 |
| `--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 @@ -149,6 +152,26 @@ npx hyperframes render --video-bitrate 10M --output controlled.mp4

**Tip**: The default `standard` preset (CRF 18) is visually lossless at 1080p — most people cannot distinguish it from the source. Use `--quality draft` for faster iteration, or `--quality high` / `--crf 10` when file size is no concern.

## GPU Acceleration

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.

```bash Terminal
# Add hardware FFmpeg encoding to the default local browser-GPU render
npx hyperframes render --gpu --output encoded-fast.mp4

# Opt out of hardware Chrome/WebGL capture
npx hyperframes render --no-browser-gpu --output software-browser.mp4

# Use browser GPU plus hardware FFmpeg encoding
npx hyperframes render --gpu --output gpu.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.

## Workers

Each render worker launches a **separate Chrome browser process** to capture frames in parallel. More workers can speed up rendering, but each one consumes ~256 MB of RAM and significant CPU.
Expand Down
7 changes: 4 additions & 3 deletions docs/guides/troubleshooting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,10 @@ If your issue is about a specific coding mistake (animations not working, video

1. Use `--quality draft` during development for faster encoding
2. Run `npx hyperframes benchmark` to find the optimal worker count for your system
3. Use `--gpu` for hardware-accelerated encoding (local mode only)
4. Reduce `--fps` to 24 if 30fps is not needed
5. Check that your composition does not have unnecessary elements or overly complex animations
3. Local Chrome/WebGL GPU capture is enabled automatically; compare with `--no-browser-gpu` if troubleshooting
4. Use `--gpu` for hardware-accelerated encoding (local mode only)
5. Reduce `--fps` to 24 if 30fps is not needed
6. Check that your composition does not have unnecessary elements or overly complex animations

See [Rendering: Options](/guides/rendering#options) for all available flags.
</Accordion>
Expand Down
9 changes: 8 additions & 1 deletion docs/packages/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,12 @@ This is suppressed in CI environments, non-TTY shells, and when `HYPERFRAMES_NO_

# With options
npx hyperframes render --output output.mp4 --fps 60 --quality high

# Opt out of local browser GPU capture
npx hyperframes render --no-browser-gpu --output cpu-browser.mp4

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

| Flag | Values | Default | Description |
Expand All @@ -543,7 +549,8 @@ This is suppressed in CI environments, non-TTY shells, and when `HYPERFRAMES_NO_
| `--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 |
| `--workers` | 1-8 | 4 | Parallel render workers |
| `--gpu` | — | off | GPU encoding (NVENC, VideoToolbox, VAAPI) |
| `--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 |
| `--docker` | — | off | Use Docker for [deterministic rendering](/concepts/determinism) |
| `--quiet` | — | off | Suppress verbose output |

Expand Down
2 changes: 1 addition & 1 deletion docs/packages/engine.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ await applyFaststart(inputPath, outputPath);

// Detect GPU encoding support
const gpu = await detectGpuEncoder();
// gpu: "nvenc" | "videotoolbox" | "vaapi" | null
// gpu: "nvenc" | "videotoolbox" | "vaapi" | "qsv" | null
```

#### WebM with VP9 Alpha
Expand Down
19 changes: 16 additions & 3 deletions docs/packages/producer.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -213,18 +213,31 @@ npx hyperframes render --docker --output output.mp4

The producer supports hardware-accelerated encoding for faster renders:

| Platform | Encoder | Flag |
|----------|---------|------|
| Platform | Encoder | Selection |
|----------|---------|-----------|
| NVIDIA | NVENC | Auto-detected |
| macOS | VideoToolbox | Auto-detected |
| Linux | VAAPI | Auto-detected |
| Intel | QSV | Auto-detected |

GPU encoding is automatically used when available. To check your system's capabilities:
When GPU encoding is enabled, Hyperframes detects the available FFmpeg hardware encoder automatically. To check your system's capabilities:

```bash
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';

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

## Additional Exports

The producer also re-exports key engine functionality for convenience:
Expand Down
2 changes: 1 addition & 1 deletion docs/quickstart.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,6 @@ The agent handles scaffolding, animation, and rendering. See the [prompting guid
Start from built-in examples like Warm Grain and Swiss Grid
</Card>
<Card title="Rendering" icon="film" href="/guides/rendering">
Explore render options: quality presets, Docker mode, and GPU encoding
Explore render options: quality presets, Docker mode, and GPU acceleration
</Card>
</CardGroup>
102 changes: 102 additions & 0 deletions packages/cli/src/commands/render.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

const producerState = vi.hoisted(() => ({
createdJobs: [] as Array<Record<string, unknown>>,
resolveConfigCalls: [] as Array<Record<string, unknown>>,
}));

vi.mock("../utils/producer.js", () => ({
loadProducer: vi.fn(async () => ({
resolveConfig: vi.fn((overrides: Record<string, unknown>) => {
producerState.resolveConfigCalls.push(overrides);
return { ...overrides, resolved: true };
}),
createRenderJob: vi.fn((config: Record<string, unknown>) => {
producerState.createdJobs.push(config);
return { config, progress: 100 };
}),
executeRenderJob: vi.fn(async () => undefined),
})),
}));

vi.mock("../telemetry/events.js", () => ({
trackRenderComplete: vi.fn(),
trackRenderError: vi.fn(),
}));

describe("renderLocal browser GPU config", () => {
const savedEnv = new Map<string, string | undefined>();

function setEnv(key: string, value: string) {
savedEnv.set(key, process.env[key]);
process.env[key] = value;
}

beforeEach(() => {
producerState.createdJobs = [];
producerState.resolveConfigCalls = [];
savedEnv.clear();
});

afterEach(() => {
for (const [key, value] of savedEnv) {
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
vi.clearAllMocks();
});

it("passes an explicit software override for --no-browser-gpu even when env requests hardware", async () => {
setEnv("PRODUCER_BROWSER_GPU_MODE", "hardware");

const { renderLocal } = await import("./render.js");
await renderLocal("/tmp/project", "/tmp/out.mp4", {
fps: 30,
quality: "standard",
format: "mp4",
gpu: false,
browserGpu: false,
hdrMode: "auto",
quiet: true,
});

expect(producerState.resolveConfigCalls).toContainEqual({ browserGpuMode: "software" });
expect(producerState.createdJobs[0]?.producerConfig).toMatchObject({
browserGpuMode: "software",
resolved: true,
});
});

it("passes an explicit hardware override for default local browser GPU", async () => {
const { renderLocal } = await import("./render.js");
await renderLocal("/tmp/project", "/tmp/out.mp4", {
fps: 30,
quality: "standard",
format: "mp4",
gpu: false,
browserGpu: true,
hdrMode: "auto",
quiet: true,
});

expect(producerState.resolveConfigCalls).toContainEqual({ browserGpuMode: "hardware" });
expect(producerState.createdJobs[0]?.producerConfig).toMatchObject({
browserGpuMode: "hardware",
resolved: true,
});
});

it("resolves browser GPU from CLI flags, Docker mode, and env fallback", async () => {
const { resolveBrowserGpuForCli } = await import("./render.js");

expect(resolveBrowserGpuForCli(false, undefined, undefined)).toBe(true);
expect(resolveBrowserGpuForCli(false, undefined, "hardware")).toBe(true);
expect(resolveBrowserGpuForCli(false, undefined, "software")).toBe(false);
expect(resolveBrowserGpuForCli(false, true, "software")).toBe(true);
expect(resolveBrowserGpuForCli(false, false, "hardware")).toBe(false);
expect(resolveBrowserGpuForCli(true, undefined, "hardware")).toBe(false);
});
});
44 changes: 43 additions & 1 deletion packages/cli/src/commands/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const examples: Example[] = [
["High quality at 60fps", "hyperframes render --fps 60 --quality high --output hd.mp4"],
["Deterministic render via Docker", "hyperframes render --docker --output deterministic.mp4"],
["Parallel rendering with 6 workers", "hyperframes render --workers 6 --output fast.mp4"],
["Opt out of browser GPU render", "hyperframes render --no-browser-gpu --output cpu.mp4"],
["HDR output (auto-detected)", "hyperframes render --output hdr-output.mp4"],
];
import { cpus, freemem, tmpdir } from "node:os";
Expand Down Expand Up @@ -99,6 +100,11 @@ export default defineCommand({
description: "Target video bitrate such as 10M. Mutually exclusive with --crf.",
},
gpu: { type: "boolean", description: "Use GPU encoding", default: false },
"browser-gpu": {
type: "boolean",
description:
"Use host GPU acceleration for Chrome/WebGL capture. Enabled by default for local renders; use --no-browser-gpu to opt out.",
},
quiet: {
type: "boolean",
description: "Suppress verbose output",
Expand Down Expand Up @@ -186,6 +192,8 @@ export default defineCommand({

const useDocker = args.docker ?? false;
const useGpu = args.gpu ?? false;
const browserGpuArg = args["browser-gpu"];
const useBrowserGpu = resolveBrowserGpuForCli(useDocker, browserGpuArg);
const quiet = args.quiet ?? false;
const strictAll = args["strict-all"] ?? false;
const strictErrors = (args.strict ?? false) || strictAll;
Expand All @@ -197,6 +205,15 @@ export default defineCommand({
process.exit(1);
}

if (useDocker && browserGpuArg === true) {
errorBox(
"Browser GPU is local-only",
"--browser-gpu uses the host Chrome GPU backend. Docker mode keeps browser rendering deterministic and does not expose a cross-platform Chrome GPU backend.",
"Run without --docker, or use --gpu for Docker GPU encoding where your Docker host supports GPU passthrough.",
);
process.exit(1);
}

let crf: number | undefined;
if (crfRaw != null) {
const parsed = Number(crfRaw);
Expand Down Expand Up @@ -227,6 +244,13 @@ export default defineCommand({
c.dim(" \u2192 " + outputPath),
);
console.log(c.dim(" " + fps + "fps \u00B7 " + quality + " \u00B7 " + workerLabel));
if (useGpu || useBrowserGpu) {
const gpuModes = [
useGpu ? "encoder GPU" : null,
useBrowserGpu ? "browser GPU (auto)" : null,
].filter(Boolean);
console.log(c.dim(" GPU: " + gpuModes.join(" + ")));
}
console.log("");
}

Expand Down Expand Up @@ -312,6 +336,7 @@ export default defineCommand({
format,
workers,
gpu: useGpu,
browserGpu: useBrowserGpu,
hdrMode: args.sdr ? "force-sdr" : args.hdr ? "force-hdr" : "auto",
crf,
videoBitrate,
Expand All @@ -324,6 +349,7 @@ export default defineCommand({
format,
workers,
gpu: useGpu,
browserGpu: useBrowserGpu,
hdrMode: args.sdr ? "force-sdr" : args.hdr ? "force-hdr" : "auto",
crf,
videoBitrate,
Expand All @@ -340,13 +366,25 @@ interface RenderOptions {
format: "mp4" | "webm" | "mov";
workers?: number;
gpu: boolean;
browserGpu: boolean;
hdrMode: "auto" | "force-hdr" | "force-sdr";
crf?: number;
videoBitrate?: string;
quiet: boolean;
browserPath?: string;
}

export function resolveBrowserGpuForCli(
useDocker: boolean,
browserGpuArg: boolean | undefined,
envMode = process.env.PRODUCER_BROWSER_GPU_MODE,
): boolean {
if (useDocker) return false;
if (browserGpuArg !== undefined) return browserGpuArg;
if (envMode === "software") return false;
return true;
}

const DOCKER_IMAGE_PREFIX = "hyperframes-renderer";

function dockerImageTag(version: string): string {
Expand Down Expand Up @@ -464,6 +502,7 @@ async function renderDocker(
format: options.format,
workers: options.workers,
gpu: options.gpu,
browserGpu: options.browserGpu,
hdrMode: options.hdrMode,
crf: options.crf,
videoBitrate: options.videoBitrate,
Expand Down Expand Up @@ -508,7 +547,7 @@ async function renderDocker(
printRenderComplete(outputPath, elapsed, options.quiet);
}

async function renderLocal(
export async function renderLocal(
projectDir: string,
outputPath: string,
options: RenderOptions,
Expand All @@ -530,6 +569,9 @@ async function renderLocal(
format: options.format,
workers: options.workers,
useGpu: options.gpu,
producerConfig: producer.resolveConfig({
browserGpuMode: options.browserGpu ? "hardware" : "software",
}),
hdrMode: options.hdrMode,
crf: options.crf,
videoBitrate: options.videoBitrate,
Expand Down
5 changes: 4 additions & 1 deletion packages/cli/src/docs/rendering.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,14 @@ Requires: Docker installed and running.
- `-w, --workers` — Parallel workers 1-8 (default: auto)
- `--crf` — Override encoder CRF (mutually exclusive with `--video-bitrate`)
- `--video-bitrate` — Target video bitrate such as `10M` (mutually exclusive with `--crf`)
- `--gpu` — Use GPU encoding (NVENC, VideoToolbox, VAAPI)
- `--gpu` — Use GPU encoding (NVENC, VideoToolbox, VAAPI, QSV)
- `--browser-gpu` / `--no-browser-gpu` — Use or opt out of host GPU acceleration for local Chrome/WebGL capture (enabled by default for local renders, disabled in Docker)
- `-o, --output` — Custom output path

## Tips

- Use `draft` quality for fast previews during development
- Local renders use browser GPU capture automatically; use `--no-browser-gpu` to compare against the software-browser path
- Use `--gpu` when a local render also benefits from hardware FFmpeg encoding
- Use `npx hyperframes benchmark` to find optimal settings
- 4 workers is usually the sweet spot for most compositions
Loading
Loading