Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
cb1642f
perf(engine): overhaul rendering pipeline for speed parity with Remotion
miguel-heygen Apr 25, 2026
39410ee
fix(engine): revert streaming/multi-page/gpu defaults to safe values
miguel-heygen Apr 25, 2026
db1b3ae
feat(native-renderer): scaffold Rust crate with scene graph types
miguel-heygen Apr 25, 2026
8da7cb3
feat(native-renderer): add skia surface creation and pixel readback
miguel-heygen Apr 25, 2026
0f139b4
feat(native-renderer): element painting with transforms, opacity, bor…
miguel-heygen Apr 25, 2026
49762b5
feat(native-renderer): static scene to MP4 render pipeline via FFmpeg
miguel-heygen Apr 25, 2026
f099d76
feat(native-renderer): add CDP scene extraction from Chrome
miguel-heygen Apr 25, 2026
a29b1f5
perf(native-renderer): add skia paint benchmark at 1080p with 20 elem…
miguel-heygen Apr 25, 2026
38cafc8
feat(native-renderer): phase 1 complete — Skia rendering pipeline + b…
miguel-heygen Apr 25, 2026
ccf9bdb
feat(native-renderer): pre-baked timeline extraction from Chrome
miguel-heygen Apr 25, 2026
5d45b8c
feat(native-renderer): phase 2 — effects, images, font caching
miguel-heygen Apr 25, 2026
904cc42
feat(native-renderer): tier 2 CSS effects — box-shadow, blur, gradients
miguel-heygen Apr 25, 2026
4708713
feat(native-renderer): metal GPU surface — 29x faster than CPU raster
miguel-heygen Apr 25, 2026
ff15d34
feat(native-renderer): add hardware encode path
miguel-heygen Apr 25, 2026
36f7904
feat(cli): add render backend selection
miguel-heygen Apr 25, 2026
ab64267
feat(native-renderer): complete supported native proof path
miguel-heygen Apr 25, 2026
a89f9c4
feat(native-renderer): wire native CLI media path
miguel-heygen Apr 25, 2026
efbbc04
feat(native-renderer): harden hybrid auto backend proof
miguel-heygen Apr 25, 2026
02634d0
fix(native-renderer): tighten auto fallback for unsafe animated media
miguel-heygen Apr 25, 2026
5953f14
perf(native-renderer): bgra readback + nv12 videotoolbox encoding
miguel-heygen Apr 26, 2026
81df8fc
feat(native-renderer): cross-platform GPU — vulkan on linux, metal on…
miguel-heygen Apr 26, 2026
f190820
feat(native-renderer): linux docker test image + dockerignore
miguel-heygen Apr 26, 2026
a385abf
feat(native-renderer): linux docker CI — 51 tests pass, cpu raster + …
miguel-heygen Apr 26, 2026
743cfb2
feat(native-renderer): ffmpeg-free pipeline — openh264 + minimp4 in-p…
miguel-heygen Apr 26, 2026
939b026
perf(native-renderer): raw render pipeline + render-only benchmark
miguel-heygen Apr 26, 2026
9bb4b36
ci(native-renderer): github actions workflow for linux cpu tests + bench
miguel-heygen Apr 26, 2026
0f1d340
perf(native-renderer): skip I420 in render loop, pre-allocate buffers
miguel-heygen Apr 26, 2026
cc2bcba
fix(cli): esm compat + lint fixes for native renderer integration
miguel-heygen Apr 26, 2026
f7ad31e
feat(native-renderer): relax support detector for broader composition…
miguel-heygen Apr 26, 2026
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
27 changes: 27 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,34 @@
- run: bun install --frozen-lockfile
- run: bun run --filter @hyperframes/core test:hyperframe-runtime-ci

native-renderer:
name: "Test: native renderer"
needs: changes
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
lfs: true
- uses: oven-sh/setup-bun@v2
- uses: actions/setup-node@v4
with:
node-version: 22
- uses: dtolnay/rust-toolchain@stable
- name: Install FFmpeg
run: sudo apt-get update && sudo apt-get install -y ffmpeg
- run: bun install --frozen-lockfile
- run: cargo test --manifest-path packages/native-renderer/Cargo.toml -- --test-threads=1
- run: bun test packages/cli/src/utils/nativeBackend.test.ts packages/native-renderer/src/scene/extract.test.ts packages/native-renderer/src/scene/support.test.ts packages/native-renderer/src/timeline/bake.test.ts
- name: Native renderer comparison shard
run: |
bun packages/native-renderer/scripts/compare-regression-fixtures.ts \
--fixtures gsap-letters-render-compat \
--max-duration 0.25 \
--artifacts /tmp/native-renderer-comparison-ci

smoke-global-install:

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}
name: "Smoke: global install"
needs: [changes, build]
if: needs.changes.outputs.code == 'true'
Expand Down
65 changes: 65 additions & 0 deletions .github/workflows/native-renderer.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: Native Renderer

on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- "packages/native-renderer/**"
- ".github/workflows/native-renderer.yml"
push:
branches: [main]
paths:
- "packages/native-renderer/**"

concurrency:
group: native-renderer-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
name: Tests (Linux x86_64, CPU raster)
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4

- name: Install Rust
uses: dtolnay/rust-toolchain@stable

- name: Cache Cargo + Skia
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
packages/native-renderer/target
key: native-${{ runner.os }}-${{ hashFiles('packages/native-renderer/Cargo.lock') }}
restore-keys: native-${{ runner.os }}-

- name: Install system deps
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
clang libclang-dev pkg-config \
libfontconfig1-dev libfreetype6-dev \
ninja-build python3 \
ffmpeg fonts-liberation fonts-dejavu-core fontconfig
sudo fc-cache -fv
- name: Build (CPU raster, no GPU)
working-directory: packages/native-renderer
run: cargo build --release --no-default-features --tests

- name: Run tests
working-directory: packages/native-renderer
run: cargo test --release --no-default-features -- --test-threads=1

- name: Benchmark
working-directory: packages/native-renderer
run: |
cargo bench --no-default-features 2>&1 | tee /tmp/bench.txt
echo "## Native Renderer Benchmark" >> $GITHUB_STEP_SUMMARY
echo "Linux x86_64, CPU raster, no GPU" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
grep -E 'time:|^[a-z].*time:' /tmp/bench.txt >> $GITHUB_STEP_SUMMARY || echo "No benchmark output" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}
Comment on lines +20 to +65
1,472 changes: 1,472 additions & 0 deletions docs/superpowers/plans/2026-04-25-native-renderer.md

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
#!/usr/bin/env node

// Polyfill esbuild's __name helper before any import can reference it.
if (typeof (globalThis as Record<string, unknown>).__name !== "function") {
(globalThis as Record<string, unknown>).__name = <T>(fn: T, _n: string): T => fn;
}

// ── Fast-path exits ─────────────────────────────────────────────────────────
// Check --version before importing anything heavy. This makes
// `hyperframes --version` near-instant (~10ms vs ~80ms).
Expand Down
26 changes: 26 additions & 0 deletions packages/cli/src/commands/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { findFFmpeg, getFFmpegInstallHint } from "../browser/ffmpeg.js";
import { VERSION } from "../version.js";
import { getUpdateMeta } from "../utils/updateCheck.js";
import { getSystemMeta, getShmSizeMb, getFreeDiskMb, bytesToMb } from "../telemetry/system.js";
import { findNativeRendererRoot } from "../utils/nativeRender.js";

interface Check {
name: string;
Expand Down Expand Up @@ -181,6 +182,30 @@ function checkEnvironment(): CheckResult {
return { ok: true, detail: parts.join(" \u00B7 ") };
}

function checkNativeRenderer(): CheckResult {
const root = findNativeRendererRoot();
if (!root) {
return {
ok: true,
detail: "Not bundled in this installation; --backend auto will use Chrome fallback",
};
}

try {
const cargo = execSync("cargo --version", { encoding: "utf-8", timeout: 5000 }).trim();
return {
ok: true,
detail: `${root} \u00B7 ${cargo} \u00B7 auto gates native by support detection; zero-copy is not proven yet`,
};
} catch {
return {
ok: false,
detail: `${root} \u00B7 cargo not found`,
hint: "Install Rust from https://rustup.rs/ to use --backend native from source; --backend auto falls back to Chrome when native is unavailable or unsupported.",
};
}
}

export default defineCommand({
meta: { name: "doctor", description: "Check system dependencies and environment" },
args: {},
Expand All @@ -207,6 +232,7 @@ export default defineCommand({
{ name: "FFmpeg", run: checkFFmpeg },
{ name: "FFprobe", run: checkFFprobe },
{ name: "Chrome", run: checkChrome },
{ name: "Native renderer", run: checkNativeRenderer },
{ name: "Docker", run: checkDocker },
{ name: "Docker running", run: checkDockerRunning },
);
Expand Down
177 changes: 156 additions & 21 deletions packages/cli/src/commands/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ import { bytesToMb } from "../telemetry/system.js";
import { VERSION } from "../version.js";
import { isDevMode } from "../utils/env.js";
import { buildDockerRunArgs } from "../utils/dockerRunArgs.js";
import {
parseRenderBackend,
resolveRenderBackend,
type RenderBackend,
} from "../utils/nativeBackend.js";
import {
formatUnsupportedNativeFeatures,
isNativeRendererAvailable,
renderNativeProject,
type NativeRenderResult,
} from "../utils/nativeRender.js";
import type { RenderJob } from "@hyperframes/producer";

const VALID_FPS = new Set([24, 30, 60]);
Expand Down Expand Up @@ -73,6 +84,11 @@ export default defineCommand({
description: "Output format: mp4, webm, mov (MOV/WebM render with transparency)",
default: "mp4",
},
backend: {
type: "string",
description: "Render backend: chrome, native, or auto",
default: "chrome",
},
workers: {
type: "string",
alias: "w",
Expand Down Expand Up @@ -147,6 +163,14 @@ export default defineCommand({
}
const format = formatRaw as "mp4" | "webm" | "mov";

// ── Validate backend ────────────────────────────────────────────────
const backendRaw = args.backend ?? "chrome";
const backend = parseRenderBackend(backendRaw);
if (!backend) {
errorBox("Invalid backend", `Got "${backendRaw}". Must be chrome, native, or auto.`);
process.exit(1);
}

// ── Validate workers ──────────────────────────────────────────────────
let workers: number | undefined;
if (args.workers != null && args.workers !== "auto") {
Expand Down Expand Up @@ -215,6 +239,22 @@ export default defineCommand({
process.exit(1);
}

const backendDecision = resolveRenderBackend({
requested: backend,
docker: useDocker,
format,
hdr: args.hdr ?? false,
nativeRuntimeAvailable: isNativeRendererAvailable(),
});
if (backendDecision.kind === "unavailable") {
errorBox(
"Native renderer unavailable",
backendDecision.reasons.map((reason) => `- ${reason}`).join("\n"),
"Use --backend chrome, or --backend auto to fall back automatically.",
);
process.exit(1);
}

// ── Print render plan ─────────────────────────────────────────────────
const workerCount = workers ?? defaultWorkerCount();
if (!quiet) {
Expand All @@ -229,7 +269,22 @@ export default defineCommand({
c.accent(project.name) +
c.dim(" \u2192 " + outputPath),
);
console.log(c.dim(" " + fps + "fps \u00B7 " + quality + " \u00B7 " + workerLabel));
const backendLabel = formatBackendLabel(backendDecision.requested, backendDecision.kind);
console.log(
c.dim(
" " +
fps +
"fps \u00B7 " +
quality +
" \u00B7 " +
workerLabel +
" \u00B7 backend " +
backendLabel,
),
);
if (backendDecision.requested === "auto" && backendDecision.reasons.length > 0) {
console.log(c.dim(" Native fallback: " + backendDecision.reasons.join("; ")));
}
console.log("");
}

Expand Down Expand Up @@ -302,31 +357,80 @@ export default defineCommand({
}

// ── Render ────────────────────────────────────────────────────────────
const renderOptions: RenderOptions = {
fps,
quality,
format,
workers: workerCount,
gpu: useGpu,
hdr: args.hdr ?? false,
crf,
videoBitrate,
quiet,
browserPath,
};

if (useDocker) {
await renderDocker(project.dir, outputPath, {
await renderDocker(project.dir, outputPath, renderOptions);
} else if (backendDecision.kind === "native") {
const nativeStart = Date.now();
const nativeResult = await renderNativeProject(project.dir, outputPath, {
fps,
quality,
format,
workers: workerCount,
gpu: useGpu,
hdr: args.hdr ?? false,
crf,
videoBitrate,
browserPath,
quiet,
});
}).catch((error: unknown) =>
handleRenderError(
error,
renderOptions,
nativeStart,
false,
"Use --backend chrome to render through the Chrome pipeline",
),
);

if (nativeResult.kind === "rendered") {
trackNativeRenderMetrics(nativeResult, renderOptions);
printRenderComplete(outputPath, nativeResult.elapsedMs, quiet);
return;
}

if (nativeResult.kind === "unsupported") {
const reasons = formatUnsupportedNativeFeatures(nativeResult.support.reasons);
if (backendDecision.requested === "auto") {
if (!quiet) {
console.log(c.dim(" Native fallback:\n" + reasons));
console.log("");
}
await renderLocal(project.dir, outputPath, renderOptions);
return;
}

errorBox(
"Native renderer fallback required",
reasons,
"Use --backend auto or --backend chrome for this composition.",
);
process.exit(1);
}

if (backendDecision.requested === "auto") {
if (!quiet) {
console.log(c.dim(" Native fallback: " + nativeResult.reasons.join("; ")));
console.log("");
}
await renderLocal(project.dir, outputPath, renderOptions);
return;
}

errorBox(
"Native renderer unavailable",
nativeResult.reasons.map((reason) => `- ${reason}`).join("\n"),
"Use --backend chrome, or install the native renderer toolchain.",
);
process.exit(1);
} else {
await renderLocal(project.dir, outputPath, {
fps,
quality,
format,
workers: workerCount,
gpu: useGpu,
hdr: args.hdr ?? false,
crf,
videoBitrate,
quiet,
browserPath,
});
await renderLocal(project.dir, outputPath, renderOptions);
}
},
});
Expand All @@ -344,6 +448,10 @@ interface RenderOptions {
browserPath?: string;
}

function formatBackendLabel(requested: RenderBackend, selected: "chrome" | "native"): string {
return requested === "auto" ? `auto \u2192 ${selected}` : selected;
}

const DOCKER_IMAGE_PREFIX = "hyperframes-renderer";

function dockerImageTag(version: string): string {
Expand Down Expand Up @@ -578,6 +686,33 @@ function handleRenderError(
process.exit(1);
}

function trackNativeRenderMetrics(
result: Extract<NativeRenderResult, { kind: "rendered" }>,
options: RenderOptions,
): void {
const compositionDurationMs = Math.round(result.duration * 1000);
const speedRatio =
compositionDurationMs > 0 && result.elapsedMs > 0
? Math.round((compositionDurationMs / result.elapsedMs) * 100) / 100
: undefined;

trackRenderComplete({
durationMs: result.elapsedMs,
fps: options.fps,
quality: options.quality,
workers: options.workers,
docker: false,
gpu: true,
compositionDurationMs,
compositionWidth: result.width,
compositionHeight: result.height,
totalFrames: result.totalFrames,
speedRatio,
captureAvgMs: result.renderer.avgPaintMs,
...getMemorySnapshot(),
});
}

/**
* Extract rich metrics from the completed render job and send to telemetry.
* speed_ratio = composition_duration / render_time — higher is better, >1 means faster than realtime.
Expand Down
Loading
Loading