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
3 changes: 3 additions & 0 deletions packages/engine/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ export {
closeCaptureSession,
captureFrame,
captureFrameToBuffer,
captureFrameToBufferPipelined,
writeCapturedFrame,
discardWarmupCapture,
getCompositionDuration,
getCapturePerfSummary,
Expand All @@ -101,6 +103,7 @@ export {
injectVideoFramesBatch,
syncVideoFrameVisibility,
cdpSessionCache,
probeBeginFrameLiveness,
initTransparentBackground,
captureAlphaPng,
applyDomLayerMask,
Expand Down
3 changes: 2 additions & 1 deletion packages/engine/src/services/browserManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ async function probeHardwareWebGlInfo(

// "beginframe" = atomic compositor control via HeadlessExperimental.beginFrame (Linux only)
// "screenshot" = renderSeek + Page.captureScreenshot (all platforms)
export type CaptureMode = "beginframe" | "screenshot";
// "drawelement" = BeginFrame compositor advance + canvas.drawElementImage capture
export type CaptureMode = "beginframe" | "screenshot" | "drawelement";

export interface AcquiredBrowser {
browser: Browser;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* DrawElement integration coverage — record of browser-level validation.
*
* The unit tests in `drawElementService.test.ts` mock `page.evaluate`, so they
* cannot exercise real frame capture. The behaviours below were validated with
* local Docker harnesses (dev scaffolding under `spikes/`, not committed —
* spikes are untracked in this repo) that drive the real engine functions
* against a real Chrome/headless-shell. This file records what was checked and
* the results, and is the place to add in-suite browser tests if/when the
* package gains a headless-browser test runner.
*
* ── T1 + T2: Docker / SwiftShader (real engine fns vs real SwiftShader) ─────────
* Last validated 2026-06-08 — ALL PASS:
* T1 opaque drawElement frame vs Page.captureScreenshot baseline — PSNR = ∞
* (pixel-identical) on SwiftShader, even with CSS transforms present.
* T2a detectSwiftShader() === true inside headless-shell + --use-angle=swiftshader.
* T2b transparent + SwiftShader → resolveDrawElementCaptureMode === "screenshot"
* (fallback; the transparent drawElement path is broken on SwiftShader).
* T2c opaque + SwiftShader → "drawelement" (T1 confirmed it is pixel-correct).
*
* NOTE (2026-06-12): the engine now routes ALL SwiftShader renders to
* screenshot regardless of T2c's correctness result. drawElement is
* pixel-correct on SwiftShader (T1, PSNR ∞) but yields NO speedup there —
* its only advantage is skipping the GPU→CPU screenshot readback IPC, which
* software rasterization doesn't have. Measured parity (font-variant-numeric
* baseline 7822ms vs fast 7979ms); resolveDrawElementCaptureMode now returns
* "screenshot" for any isSwiftShader. The speedup is GPU-only (macOS 1.6×).
*
* ── E2E: full producer pipeline (--experimental-fast-capture) ───────────────────
* A real producer render (css-spinner composition → mp4) with
* PRODUCER_EXPERIMENTAL_FAST_CAPTURE=true logged "drawElement canvas injected"
* and produced a valid mp4 — proving env → resolveConfig → captureCfg →
* createCaptureSession → drawelement mode, plus jpeg-frame encode through ffmpeg.
*
* ── T3: transparent drawElement on a real GPU host ──────────────────────────────
* PSNR = ∞ vs screenshot on GPU (empty A=0, semi-transparent A≈128, opaque
* A=255). Cannot run in Docker (SwiftShader-only); validated on a GPU host.
*/

import { describe, it } from "vitest";

describe.skip("drawElementService integration (browser/Docker — see validation record above)", () => {
it.skip("T1+T2 validated against real SwiftShader (Docker)", () => {});
it.skip("E2E validated through the producer pipeline (--experimental-fast-capture)", () => {});
it.skip("T3 validated on a real-GPU host", () => {});
});
58 changes: 58 additions & 0 deletions packages/engine/src/services/drawElementService.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { describe, expect, it, vi } from "vitest";
import type { Page } from "puppeteer-core";
import { detectSwiftShader, resolveDrawElementCaptureMode } from "./drawElementService.js";

// ── detectSwiftShader ──────────────────────────────────────────────────────────

describe("detectSwiftShader", () => {
function makePage(evaluateResult: unknown): Page {
return {
evaluate: vi.fn().mockResolvedValue(evaluateResult),
} as unknown as Page;
}

it("returns true when renderer includes 'swiftshader'", async () => {
const page = makePage(true);
expect(await detectSwiftShader(page)).toBe(true);
});

it("returns false for a standard GPU renderer string", async () => {
const page = makePage(false);
expect(await detectSwiftShader(page)).toBe(false);
});

it("returns false when WebGL is unavailable", async () => {
const page = makePage(false);
expect(await detectSwiftShader(page)).toBe(false);
});

it("passes a function to page.evaluate", async () => {
const page = makePage(false);
await detectSwiftShader(page);
expect(page.evaluate).toHaveBeenCalledWith(expect.any(Function));
});
});

// ── resolveDrawElementCaptureMode ──────────────────────────────────────────────

describe("resolveDrawElementCaptureMode", () => {
// signature: (isSwiftShader, transparent)
it("opaque + SwiftShader → screenshot (no GPU egress to skip — parity at best)", () => {
expect(resolveDrawElementCaptureMode(true, false)).toBe("screenshot");
});

it("transparent + SwiftShader → screenshot (also drops sub-layers; crbug 521434899)", () => {
expect(resolveDrawElementCaptureMode(true, true)).toBe("screenshot");
});

it("transparent + GPU → drawelement (GPU handles transparent correctly)", () => {
expect(resolveDrawElementCaptureMode(false, true)).toBe("drawelement");
});

it("opaque + GPU → drawelement", () => {
expect(resolveDrawElementCaptureMode(false, false)).toBe("drawelement");
});

// The <video> gate (proxy for the caption-pattern bug, crbug 521861819) was
// removed once Chrome 151 fixed it — video comps now take the drawElement path.
});
Loading
Loading