Route Windows Lightning exports through NVIDIA native planner

This commit is contained in:
wiiiii123
2026-05-12 20:33:07 +07:00
parent 7bc06c9925
commit f2d9ab73b0
5 changed files with 624 additions and 46 deletions
+73
View File
@@ -421,6 +421,10 @@ describe("getExperimentalNvidiaCudaExportSkipReason", () => {
process.env[exportEnvName] = "1";
process.env[forceEnvName] = "1";
delete process.env[allowAudioEnvName];
fsMocks.access.mockResolvedValue(undefined);
electronAppMock.getGPUInfo.mockResolvedValue({
gpuDevice: [{ vendorId: 0x10de, deviceString: "NVIDIA GeForce GTX 1650" }],
});
try {
const reason = await getExperimentalNvidiaCudaExportSkipReason(
@@ -446,6 +450,31 @@ describe("getExperimentalNvidiaCudaExportSkipReason", () => {
} else {
process.env[allowAudioEnvName] = originalAllowAudioEnv;
}
electronAppMock.getGPUInfo.mockReset();
electronAppMock.getGPUInfo.mockResolvedValue({ gpuDevice: [] });
resetFsAccessMock();
}
});
it("reports explicit lab CUDA as unavailable when the wrapper cannot be resolved", async () => {
const exportEnvName = "RECORDLY_EXPERIMENTAL_NVIDIA_CUDA_EXPORT";
const originalExportEnv = process.env[exportEnvName];
process.env[exportEnvName] = "1";
try {
const reason = await getExperimentalNvidiaCudaExportSkipReason(
createNvidiaCudaSkipOptions(),
);
expect(reason).toBe(
process.platform === "win32" ? "cuda-wrapper-unavailable" : "not-windows",
);
} finally {
if (originalExportEnv === undefined) {
delete process.env[exportEnvName];
} else {
process.env[exportEnvName] = originalExportEnv;
}
}
});
@@ -675,6 +704,50 @@ describe("buildExperimentalNvidiaCudaStaticLayoutArgs", () => {
});
describe("buildExperimentalWindowsGpuStaticLayoutArgs", () => {
it("prefers the high-performance adapter by default for D3D11 fallback diagnostics", () => {
const envName = "RECORDLY_WINDOWS_GPU_EXPORT_ADAPTER_INDEX";
const originalValue = process.env[envName];
delete process.env[envName];
try {
const args = buildExperimentalWindowsGpuStaticLayoutArgs(
createNvidiaCudaSkipOptions(),
"output.mp4",
);
expect(args).toContain("--prefer-high-performance-adapter");
expect(args).not.toContain("--adapter-index");
} finally {
if (originalValue === undefined) {
delete process.env[envName];
} else {
process.env[envName] = originalValue;
}
}
});
it("passes an explicit D3D11 adapter index when configured", () => {
const envName = "RECORDLY_WINDOWS_GPU_EXPORT_ADAPTER_INDEX";
const originalValue = process.env[envName];
try {
process.env[envName] = "2";
const args = buildExperimentalWindowsGpuStaticLayoutArgs(
createNvidiaCudaSkipOptions(),
"output.mp4",
);
expect(args).toEqual(expect.arrayContaining(["--adapter-index", "2"]));
expect(args).not.toContain("--prefer-high-performance-adapter");
} finally {
if (originalValue === undefined) {
delete process.env[envName];
} else {
process.env[envName] = originalValue;
}
}
});
it("passes background blur to the D3D11 compositor", () => {
const args = buildExperimentalWindowsGpuStaticLayoutArgs(
createNvidiaCudaSkipOptions({
+359 -41
View File
@@ -50,6 +50,10 @@ const NVIDIA_CUDA_AUTO_STALL_TIMEOUT_ENV = "RECORDLY_NVIDIA_CUDA_AUTO_STALL_TIME
const DEFAULT_NVIDIA_CUDA_AUTO_STALL_TIMEOUT_MS = 120_000;
const NATIVE_GPU_STALL_TIMEOUT_ENV = "RECORDLY_NATIVE_GPU_STALL_TIMEOUT_MS";
const DEFAULT_NATIVE_GPU_STALL_TIMEOUT_MS = 120_000;
const WINDOWS_GPU_ADAPTER_INDEX_ENV = "RECORDLY_WINDOWS_GPU_EXPORT_ADAPTER_INDEX";
const WINDOWS_GPU_PREFER_HIGH_PERFORMANCE_ADAPTER_ENV =
"RECORDLY_WINDOWS_GPU_EXPORT_PREFER_HIGH_PERFORMANCE_ADAPTER";
const WINDOWS_GPU_NVENC_SDK_ENV = "RECORDLY_WINDOWS_GPU_EXPORT_NVENC_SDK";
const NATIVE_STATIC_LAYOUT_SOURCE_PROXY_REFERENCE_PIXEL_RATE = 1920 * 1080 * 30;
const NATIVE_STATIC_LAYOUT_SOURCE_PROXY_1080P30_BITRATE = 24_000_000;
const NATIVE_STATIC_LAYOUT_SOURCE_PROXY_MAX_BITRATE = 80_000_000;
@@ -65,6 +69,63 @@ type ElectronGpuInfoLike = {
gpuDevice?: ElectronGpuDeviceLike[];
};
type NativeStaticLayoutSourceInput = {
inputPath: string;
elapsedMs: number;
sourceCodec: string;
proxyCodec?: string;
proxyCreated: boolean;
};
type NativeStaticLayoutRouteId =
| "nvidia-cuda-compositor"
| "windows-d3d11-compositor"
| "ffmpeg-static-layout";
type NativeStaticLayoutRouteDecision = {
route: NativeStaticLayoutRouteId;
status: "selected" | "fallback" | "rejected";
reasons: string[];
};
type NvidiaCudaExportCapabilityProbe = {
platform: NodeJS.Platform;
appPackaged: boolean;
explicitEnabled: boolean;
explicitDisabled: boolean;
packagedAutoCandidateEnabled: boolean;
packagedAutoCandidateActive: boolean;
windowsGpuCompositorEnabled: boolean;
wrapperPath: string | null;
hasNvidiaGpu: boolean | null;
audioMode: NativeVideoExportAudioMode;
audioSkipReason: string | null;
stallTimeoutMs: number | null;
skipReason: string | null;
};
type WindowsD3D11ExportCapabilityProbe = {
platform: NodeJS.Platform;
windowsGpuCompositorEnabled: boolean;
helperPath: string | null;
adapterIndexOverride: number | null;
preferHighPerformanceAdapter: boolean;
nvencSdkRequested: boolean;
skipReason: string | null;
};
type NativeStaticLayoutRoutePlan = {
selectedRoute: NativeStaticLayoutRouteId;
decisions: NativeStaticLayoutRouteDecision[];
cuda: NvidiaCudaExportCapabilityProbe;
d3d11: WindowsD3D11ExportCapabilityProbe;
source: {
inputCodec: string;
proxyCodec?: string;
proxyCreated: boolean;
};
};
export type NativeVideoExportSession = {
ffmpegProcess: ChildProcessByStdio<Writable, null, Readable>;
outputPath: string;
@@ -1866,6 +1927,24 @@ async function hasNvidiaGpuForCudaExportCandidate() {
}
}
function getWindowsGpuAdapterIndexOverride() {
const rawValue = process.env[WINDOWS_GPU_ADAPTER_INDEX_ENV]?.trim();
if (!rawValue) {
return null;
}
const parsed = Number(rawValue);
return Number.isInteger(parsed) && parsed >= 0 ? parsed : null;
}
function shouldPreferHighPerformanceWindowsGpuAdapter() {
return process.env[WINDOWS_GPU_PREFER_HIGH_PERFORMANCE_ADAPTER_ENV] !== "0";
}
function isWindowsGpuNvencSdkRequested() {
return process.env[WINDOWS_GPU_NVENC_SDK_ENV] === "1";
}
function getNativeBinPlatformArch() {
return process.arch === "arm64" ? "win32-arm64" : "win32-x64";
}
@@ -1998,31 +2077,178 @@ export function getNativeGpuCompositorStallTimeoutMs() {
export async function getExperimentalNvidiaCudaExportSkipReason(
options: NativeStaticLayoutExportOptions,
) {
if (process.platform !== "win32") {
return "not-windows";
}
return (await probeExperimentalNvidiaCudaExportCapability(options)).skipReason;
}
export async function probeExperimentalNvidiaCudaExportCapability(
options: NativeStaticLayoutExportOptions,
): Promise<NvidiaCudaExportCapabilityProbe> {
const explicitCuda = isExplicitNvidiaCudaExportEnabled();
const packagedAutoCandidate = isPackagedNvidiaCudaExportAutoCandidateEnabled();
if (!explicitCuda && !packagedAutoCandidate) {
return "env-disabled";
}
if (!options.experimentalWindowsGpuCompositor) {
return "windows-gpu-compositor-disabled";
}
if (packagedAutoCandidate && !explicitCuda) {
if (!(await resolveExperimentalNvidiaCudaExportScriptPath())) {
return "cuda-wrapper-unavailable";
}
if (!(await hasNvidiaGpuForCudaExportCandidate())) {
return "nvidia-gpu-unavailable";
}
}
return getNvidiaCudaAudioExportSkipReason(options.audioOptions?.audioMode, {
const packagedAutoCandidateEnabled = isPackagedNvidiaCudaExportAutoCandidateEnabled();
const packagedAutoCandidateActive = isPackagedNvidiaCudaExportAutoCandidateActive();
const shouldProbeHelper = explicitCuda || packagedAutoCandidateEnabled;
const wrapperPath =
process.platform === "win32" && shouldProbeHelper
? await resolveExperimentalNvidiaCudaExportScriptPath()
: null;
const hasNvidiaGpu =
process.platform === "win32" && shouldProbeHelper && wrapperPath
? await hasNvidiaGpuForCudaExportCandidate()
: null;
const audioMode = options.audioOptions?.audioMode ?? "none";
const audioSkipReason = getNvidiaCudaAudioExportSkipReason(audioMode, {
allowValidatedFallbackCandidate:
packagedAutoCandidate || isNvidiaCudaForceVideoOnlyEnabled(),
packagedAutoCandidateEnabled || isNvidiaCudaForceVideoOnlyEnabled(),
});
let skipReason: string | null = null;
if (process.platform !== "win32") {
skipReason = "not-windows";
} else if (!explicitCuda && !packagedAutoCandidateEnabled) {
skipReason = "env-disabled";
} else if (!options.experimentalWindowsGpuCompositor) {
skipReason = "windows-gpu-compositor-disabled";
} else if (!wrapperPath) {
skipReason = "cuda-wrapper-unavailable";
} else if (hasNvidiaGpu === false) {
skipReason = "nvidia-gpu-unavailable";
} else {
skipReason = audioSkipReason;
}
return {
platform: process.platform,
appPackaged: app.isPackaged,
explicitEnabled: explicitCuda,
explicitDisabled: isExplicitNvidiaCudaExportDisabled(),
packagedAutoCandidateEnabled,
packagedAutoCandidateActive,
windowsGpuCompositorEnabled: options.experimentalWindowsGpuCompositor === true,
wrapperPath,
hasNvidiaGpu,
audioMode,
audioSkipReason,
stallTimeoutMs: getNvidiaCudaAutoStallTimeoutMs(packagedAutoCandidateActive),
skipReason,
};
}
async function probeExperimentalWindowsD3D11ExportCapability(
options: NativeStaticLayoutExportOptions,
): Promise<WindowsD3D11ExportCapabilityProbe> {
const helperPath =
process.platform === "win32" && options.experimentalWindowsGpuCompositor === true
? await resolveExperimentalWindowsGpuExporterPath()
: null;
let skipReason: string | null = null;
if (process.platform !== "win32") {
skipReason = "not-windows";
} else if (options.experimentalWindowsGpuCompositor !== true) {
skipReason = "windows-gpu-compositor-disabled";
} else if (!helperPath) {
skipReason = "windows-gpu-helper-unavailable";
}
return {
platform: process.platform,
windowsGpuCompositorEnabled: options.experimentalWindowsGpuCompositor === true,
helperPath,
adapterIndexOverride: getWindowsGpuAdapterIndexOverride(),
preferHighPerformanceAdapter: shouldPreferHighPerformanceWindowsGpuAdapter(),
nvencSdkRequested: isWindowsGpuNvencSdkRequested(),
skipReason,
};
}
async function planNativeStaticLayoutRoutes(
options: NativeStaticLayoutExportOptions,
source: NativeStaticLayoutSourceInput,
): Promise<NativeStaticLayoutRoutePlan> {
const cuda = await probeExperimentalNvidiaCudaExportCapability(options);
const d3d11 = await probeExperimentalWindowsD3D11ExportCapability(options);
const decisions: NativeStaticLayoutRouteDecision[] = [];
if (!cuda.skipReason) {
decisions.push({
route: "nvidia-cuda-compositor",
status: "selected",
reasons: ["cuda-wrapper-and-nvidia-gpu-available"],
});
decisions.push({
route: "windows-d3d11-compositor",
status: d3d11.skipReason ? "rejected" : "fallback",
reasons: d3d11.skipReason
? [d3d11.skipReason]
: ["documented-fallback-if-cuda-runtime-fails"],
});
decisions.push({
route: "ffmpeg-static-layout",
status: "fallback",
reasons: ["native-gpu-runtime-fallback"],
});
return {
selectedRoute: "nvidia-cuda-compositor",
decisions,
cuda,
d3d11,
source: {
inputCodec: source.sourceCodec,
proxyCodec: source.proxyCodec,
proxyCreated: source.proxyCreated,
},
};
}
decisions.push({
route: "nvidia-cuda-compositor",
status: "rejected",
reasons: [cuda.skipReason],
});
if (!d3d11.skipReason) {
decisions.push({
route: "windows-d3d11-compositor",
status: "selected",
reasons: [`documented-fallback-after-cuda-skip:${cuda.skipReason}`],
});
decisions.push({
route: "ffmpeg-static-layout",
status: "fallback",
reasons: ["windows-d3d11-runtime-fallback"],
});
return {
selectedRoute: "windows-d3d11-compositor",
decisions,
cuda,
d3d11,
source: {
inputCodec: source.sourceCodec,
proxyCodec: source.proxyCodec,
proxyCreated: source.proxyCreated,
},
};
}
decisions.push({
route: "windows-d3d11-compositor",
status: "rejected",
reasons: [d3d11.skipReason],
});
decisions.push({
route: "ffmpeg-static-layout",
status: "selected",
reasons: ["native-gpu-routes-unavailable"],
});
return {
selectedRoute: "ffmpeg-static-layout",
decisions,
cuda,
d3d11,
source: {
inputCodec: source.sourceCodec,
proxyCodec: source.proxyCodec,
proxyCreated: source.proxyCreated,
},
};
}
export async function resolveExperimentalNvidiaCudaExportScriptPath() {
@@ -2159,6 +2385,15 @@ export function buildExperimentalWindowsGpuStaticLayoutArgs(
"--surface-pool-size",
String(surfacePoolSize),
];
const adapterIndexOverride = getWindowsGpuAdapterIndexOverride();
if (adapterIndexOverride !== null) {
args.push("--adapter-index", String(adapterIndexOverride));
} else if (shouldPreferHighPerformanceWindowsGpuAdapter()) {
args.push("--prefer-high-performance-adapter");
}
if (isWindowsGpuNvencSdkRequested()) {
args.push("--nvenc-sdk");
}
if (options.backgroundImagePath) {
args.push("--background-image", options.backgroundImagePath);
@@ -3201,6 +3436,44 @@ export async function exportNativeStaticLayoutVideo(
timelineMapPath,
};
}
const nativeRoutePlan = await planNativeStaticLayoutRoutes(options, sourceInput);
console.info("[native-static-layout-export] Native route plan", {
selectedRoute: nativeRoutePlan.selectedRoute,
decisions: nativeRoutePlan.decisions,
cuda: {
platform: nativeRoutePlan.cuda.platform,
appPackaged: nativeRoutePlan.cuda.appPackaged,
explicitEnabled: nativeRoutePlan.cuda.explicitEnabled,
explicitDisabled: nativeRoutePlan.cuda.explicitDisabled,
packagedAutoCandidateEnabled: nativeRoutePlan.cuda.packagedAutoCandidateEnabled,
packagedAutoCandidateActive: nativeRoutePlan.cuda.packagedAutoCandidateActive,
windowsGpuCompositorEnabled: nativeRoutePlan.cuda.windowsGpuCompositorEnabled,
wrapperPath: nativeRoutePlan.cuda.wrapperPath,
hasNvidiaGpu: nativeRoutePlan.cuda.hasNvidiaGpu,
audioMode: nativeRoutePlan.cuda.audioMode,
audioSkipReason: nativeRoutePlan.cuda.audioSkipReason,
stallTimeoutMs: nativeRoutePlan.cuda.stallTimeoutMs,
skipReason: nativeRoutePlan.cuda.skipReason,
},
d3d11: nativeRoutePlan.d3d11,
source: nativeRoutePlan.source,
output: {
width: options.width,
height: options.height,
frameRate: options.frameRate,
durationSec: options.durationSec,
},
features: {
timelineMap: Boolean(options.timelineMapPath),
webcamOverlay: Boolean(options.webcamInputPath),
cursorOverlay: Boolean(options.cursorTelemetry?.length),
zoomOverlay: Boolean(options.zoomTelemetry?.length),
sourceCrop: hasNativeStaticLayoutSourceCrop(options),
backgroundImage: Boolean(options.backgroundImagePath),
backgroundBlurPx: options.backgroundBlurPx ?? 0,
audioMode: options.audioOptions?.audioMode ?? "none",
},
});
const fullConfig: NativeStaticLayoutExportArgsConfig = {
inputPath: options.inputPath,
outputPath: videoOnlyPath,
@@ -3293,9 +3566,17 @@ export async function exportNativeStaticLayoutVideo(
}
}
let experimentalNvidiaCudaOptions = experimentalGpuOptions;
const nvidiaCudaSkipReason =
await getExperimentalNvidiaCudaExportSkipReason(options);
let shouldTryNvidiaCuda = nvidiaCudaSkipReason === null;
let shouldTryNvidiaCuda =
nativeRoutePlan.selectedRoute === "nvidia-cuda-compositor";
let windowsD3D11FallbackReason =
nativeRoutePlan.decisions.find(
(decision) =>
decision.route === "windows-d3d11-compositor" &&
decision.status === "selected",
)?.reasons[0] ??
(nativeRoutePlan.cuda.skipReason
? `nvidia-cuda-skipped:${nativeRoutePlan.cuda.skipReason}`
: undefined);
if (
shouldTryNvidiaCuda &&
(isPackagedNvidiaCudaExportAutoCandidateActive() ||
@@ -3316,24 +3597,20 @@ export async function exportNativeStaticLayoutVideo(
},
);
}
const shouldLogNvidiaCudaSkip =
isExplicitNvidiaCudaExportEnabled() ||
(isPackagedNvidiaCudaExportAutoCandidateEnabled() &&
nvidiaCudaSkipReason !== "env-disabled");
if (
!shouldTryNvidiaCuda &&
shouldLogNvidiaCudaSkip &&
nvidiaCudaSkipReason !== "env-disabled"
(nativeRoutePlan.cuda.explicitEnabled ||
nativeRoutePlan.cuda.packagedAutoCandidateEnabled) &&
nativeRoutePlan.cuda.skipReason !== "env-disabled"
) {
console.warn(
"[native-static-layout-export] Skipping NVIDIA CUDA compositor; falling back to Windows GPU compositor",
{
reason: nvidiaCudaSkipReason,
audioMode: options.audioOptions?.audioMode ?? "none",
overrideEnv: NVIDIA_CUDA_ALLOW_AUDIO_EXPORT_ENV,
packagedAutoCandidate: isPackagedNvidiaCudaExportAutoCandidateEnabled(),
},
);
console.warn("[native-static-layout-export] Skipping NVIDIA CUDA compositor", {
reason: nativeRoutePlan.cuda.skipReason,
audioMode: nativeRoutePlan.cuda.audioMode,
overrideEnv: NVIDIA_CUDA_ALLOW_AUDIO_EXPORT_ENV,
wrapperPath: nativeRoutePlan.cuda.wrapperPath,
hasNvidiaGpu: nativeRoutePlan.cuda.hasNvidiaGpu,
selectedFallback: nativeRoutePlan.selectedRoute,
});
}
if (shouldTryNvidiaCuda && options.cursorTelemetry?.length) {
const cursorTelemetryPath = await prepareNvidiaCudaCursorTelemetry(
@@ -3355,6 +3632,12 @@ export async function exportNativeStaticLayoutVideo(
};
}
}
if (
!shouldTryNvidiaCuda &&
nativeRoutePlan.selectedRoute === "nvidia-cuda-compositor"
) {
windowsD3D11FallbackReason = "nvidia-cuda-cursor-assets-unavailable";
}
if (shouldTryNvidiaCuda) {
try {
@@ -3439,6 +3722,10 @@ export async function exportNativeStaticLayoutVideo(
throw error;
}
metrics.fallbackChunkCount++;
windowsD3D11FallbackReason =
error instanceof Error
? `nvidia-cuda-runtime-failed:${error.message}`
: "nvidia-cuda-runtime-failed";
console.warn(
"[native-static-layout-export] Experimental NVIDIA CUDA compositor failed or produced invalid output; falling back to Windows GPU compositor:",
error,
@@ -3447,7 +3734,18 @@ export async function exportNativeStaticLayoutVideo(
}
}
if (!didRenderVideo) {
if (!didRenderVideo && !nativeRoutePlan.d3d11.skipReason) {
console.info(
"[native-static-layout-export] Starting Windows D3D11 compositor fallback",
{
fallbackReason: windowsD3D11FallbackReason,
helperPath: nativeRoutePlan.d3d11.helperPath,
adapterIndexOverride: nativeRoutePlan.d3d11.adapterIndexOverride,
preferHighPerformanceAdapter:
nativeRoutePlan.d3d11.preferHighPerformanceAdapter,
nvencSdkRequested: nativeRoutePlan.d3d11.nvencSdkRequested,
},
);
const gpuResult = await runExperimentalWindowsGpuStaticLayoutExport(
experimentalGpuOptions,
videoOnlyPath,
@@ -3464,6 +3762,12 @@ export async function exportNativeStaticLayoutVideo(
);
}
await validateRenderedVideoOutput();
const verifiedNvidiaAdapter = isNvidiaVendorId(
gpuResult.summary.adapterVendorId,
);
const verifiedNvencBackend = /nvenc/i.test(
gpuResult.summary.encoderBackend ?? "",
);
console.info("[native-static-layout-export] Windows GPU compositor completed", {
elapsedMs: gpuResult.elapsedMs,
width: gpuResult.summary.width,
@@ -3474,7 +3778,15 @@ export async function exportNativeStaticLayoutVideo(
surfacePoolSize: gpuResult.summary.surfacePoolSize,
gpuDecodeSurface: gpuResult.summary.gpuDecodeSurface,
adapterIndex: gpuResult.summary.adapterIndex,
adapterVendorId: gpuResult.summary.adapterVendorId,
adapterDeviceId: gpuResult.summary.adapterDeviceId,
adapterDedicatedVideoMemoryMB:
gpuResult.summary.adapterDedicatedVideoMemoryMB,
encoderBackend: gpuResult.summary.encoderBackend,
verifiedNvidiaAdapter,
verifiedNvencBackend,
documentedFallback: !verifiedNvidiaAdapter || !verifiedNvencBackend,
fallbackReason: windowsD3D11FallbackReason,
encoderTuningApplied: gpuResult.summary.encoderTuningApplied,
readMs: gpuResult.summary.readMs,
videoProcessMs: gpuResult.summary.videoProcessMs,
@@ -3496,9 +3808,15 @@ export async function exportNativeStaticLayoutVideo(
backend: "windows-d3d11-compositor",
elapsedMs: gpuResult.elapsedMs,
outputBytes: outputStat.size,
fallbackReason: windowsD3D11FallbackReason,
windowsGpuSummary: gpuResult.summary,
});
didRenderVideo = true;
} else if (!didRenderVideo && nativeRoutePlan.d3d11.skipReason) {
console.warn("[native-static-layout-export] Skipping Windows D3D11 fallback", {
reason: nativeRoutePlan.d3d11.skipReason,
fallbackReason: windowsD3D11FallbackReason,
});
}
} catch (error) {
if (session.terminating) {
+52
View File
@@ -3,7 +3,9 @@ import { describe, expect, it } from "vitest";
import {
getDefaultLightningRenderBackend,
normalizeLightningRuntimePlatform,
planLightningExportRoutes,
shouldPreferNativeAutoBackend,
shouldPreferNativeStaticLayoutBeforeBreeze,
} from "./backendPolicy";
describe("backendPolicy", () => {
@@ -24,4 +26,54 @@ describe("backendPolicy", () => {
it("keeps Lightning exports on the stable WebGL renderer by default", () => {
expect(getDefaultLightningRenderBackend()).toBe("webgl");
});
it("puts visually compatible Windows auto exports on native static layout before Breeze", () => {
expect(shouldPreferNativeStaticLayoutBeforeBreeze("win32", "auto")).toBe(true);
expect(shouldPreferNativeStaticLayoutBeforeBreeze("darwin", "auto")).toBe(false);
expect(
planLightningExportRoutes({
backendPreference: "auto",
platform: "win32",
nativeStaticLayoutAvailable: true,
}),
).toMatchObject({
selectedRoute: "native-static-layout",
decisions: [
{ route: "native-static-layout", status: "selected" },
{ route: "breeze-stream", status: "fallback" },
{ route: "webcodecs", status: "fallback" },
],
});
});
it("documents the Breeze fallback when Windows static native is rejected", () => {
expect(
planLightningExportRoutes({
backendPreference: "auto",
platform: "win32",
nativeStaticLayoutAvailable: true,
nativeStaticLayoutSkipReasons: ["unsupported-frame-overlay"],
}),
).toEqual({
selectedRoute: "breeze-stream",
decisions: [
{
route: "native-static-layout",
status: "rejected",
reasons: ["unsupported-frame-overlay"],
},
{
route: "breeze-stream",
status: "selected",
reasons: ["windows-native-static-fallback"],
},
{
route: "webcodecs",
status: "fallback",
reasons: ["breeze-unavailable-fallback"],
},
],
});
});
});
+106 -1
View File
@@ -1,4 +1,4 @@
import type { ExportRenderBackend } from "./types";
import type { ExportBackendPreference, ExportRenderBackend } from "./types";
export type LightningRuntimePlatform = "darwin" | "win32" | "linux" | "unknown";
@@ -28,6 +28,111 @@ export function shouldPreferNativeAutoBackend(_platform: LightningRuntimePlatfor
return _platform === "darwin" || _platform === "win32";
}
export type LightningExportRoute = "native-static-layout" | "breeze-stream" | "webcodecs";
export interface LightningExportRouteDecision {
route: LightningExportRoute;
status: "selected" | "fallback" | "rejected";
reasons: string[];
}
export interface LightningExportRoutePlan {
selectedRoute: LightningExportRoute;
decisions: LightningExportRouteDecision[];
}
export function shouldPreferNativeStaticLayoutBeforeBreeze(
platform: LightningRuntimePlatform,
backendPreference: ExportBackendPreference,
): boolean {
return backendPreference === "auto" && platform === "win32";
}
export function planLightningExportRoutes(options: {
backendPreference: ExportBackendPreference;
platform: LightningRuntimePlatform;
nativeStaticLayoutAvailable: boolean;
nativeStaticLayoutSkipReasons?: string[];
}): LightningExportRoutePlan {
const decisions: LightningExportRouteDecision[] = [];
const nativeStaticLayoutSkipReasons = options.nativeStaticLayoutSkipReasons ?? [];
const canUseNativeStaticLayout =
options.nativeStaticLayoutAvailable && nativeStaticLayoutSkipReasons.length === 0;
const addNativeStaticLayoutDecision = (status: LightningExportRouteDecision["status"]) => {
decisions.push({
route: "native-static-layout",
status,
reasons: canUseNativeStaticLayout
? ["visually-compatible"]
: nativeStaticLayoutSkipReasons.length > 0
? nativeStaticLayoutSkipReasons
: ["native-static-unavailable"],
});
};
if (options.backendPreference === "webcodecs") {
decisions.push({
route: "webcodecs",
status: "selected",
reasons: ["user-selected-webcodecs"],
});
return { selectedRoute: "webcodecs", decisions };
}
const preferStaticFirst =
options.backendPreference === "breeze" ||
shouldPreferNativeStaticLayoutBeforeBreeze(options.platform, options.backendPreference);
if (preferStaticFirst) {
addNativeStaticLayoutDecision(canUseNativeStaticLayout ? "selected" : "rejected");
decisions.push({
route: "breeze-stream",
status: canUseNativeStaticLayout ? "fallback" : "selected",
reasons: [
options.backendPreference === "breeze"
? "user-selected-breeze"
: "windows-native-static-fallback",
],
});
decisions.push({
route: "webcodecs",
status: "fallback",
reasons: ["breeze-unavailable-fallback"],
});
return {
selectedRoute: canUseNativeStaticLayout ? "native-static-layout" : "breeze-stream",
decisions,
};
}
if (options.backendPreference === "auto" && shouldPreferNativeAutoBackend(options.platform)) {
decisions.push({
route: "breeze-stream",
status: "selected",
reasons: ["platform-prefers-native-streaming"],
});
decisions.push({
route: "webcodecs",
status: "fallback",
reasons: ["breeze-unavailable-fallback"],
});
return { selectedRoute: "breeze-stream", decisions };
}
decisions.push({
route: "webcodecs",
status: "selected",
reasons: ["default-webcodecs-first"],
});
decisions.push({
route: "breeze-stream",
status: "fallback",
reasons: ["webcodecs-software-or-unavailable-fallback"],
});
return { selectedRoute: "webcodecs", decisions };
}
export function getDefaultLightningRenderBackend(): ExportRenderBackend {
return "webgl";
}
+34 -4
View File
@@ -8,8 +8,8 @@ import type {
CursorStyle,
CursorTelemetryPoint,
Padding,
SpeedRegion,
SourceAudioTrackSettings,
SpeedRegion,
TrimRegion,
WebcamOverlaySettings,
ZoomMotionBlurTuning,
@@ -49,7 +49,12 @@ import {
isVideoWallpaperSource,
} from "@/lib/wallpapers";
import { AudioProcessor, isAacAudioEncodingSupported } from "./audioEncoder";
import { normalizeLightningRuntimePlatform, shouldPreferNativeAutoBackend } from "./backendPolicy";
import {
normalizeLightningRuntimePlatform,
planLightningExportRoutes,
shouldPreferNativeAutoBackend,
shouldPreferNativeStaticLayoutBeforeBreeze,
} from "./backendPolicy";
import { buildEditedTrackSourceSegments, classifyEditedTrackStrategy } from "./editedTrackStrategy";
import {
type ExportBackpressureProfile,
@@ -358,13 +363,18 @@ export class ModernVideoExporter {
this.totalExportStartTimeMs = this.getNowMs();
const backendPreference = this.config.backendPreference ?? "auto";
const runtimePlatform = this.getRuntimePlatform();
const preferNativeStaticLayoutBeforeBreeze = shouldPreferNativeStaticLayoutBeforeBreeze(
runtimePlatform,
backendPreference,
);
let useNativeEncoder = false;
let triedNativeStaticLayoutWithProbe = false;
let shouldDeferNativeEncoderStart = backendPreference === "breeze";
let shouldDeferNativeEncoderStart =
backendPreference === "breeze" || preferNativeStaticLayoutBeforeBreeze;
this.lastNativeExportError = null;
let stageStartedAt = this.getNowMs();
if (backendPreference === "breeze") {
if (backendPreference === "breeze" || preferNativeStaticLayoutBeforeBreeze) {
// Defer the streaming native encoder until after metadata is known.
// Static-layout exports can then use the faster Windows D3D compositor
// instead of unnecessarily rendering every frame through JS first.
@@ -2145,6 +2155,26 @@ export class ModernVideoExporter {
const skipReasons = skipReason
? this.getNativeStaticLayoutSkipReasons(audioPlan, videoInfo, effectiveDuration)
: [];
const routePlan = planLightningExportRoutes({
backendPreference: this.config.backendPreference ?? "auto",
platform: this.getRuntimePlatform(),
nativeStaticLayoutAvailable: true,
nativeStaticLayoutSkipReasons: skipReasons,
});
console.info("[VideoExporter] Lightning route plan", {
selectedRoute: routePlan.selectedRoute,
decisions: routePlan.decisions,
audioMode: audioPlan.audioMode,
sourceCodec: videoInfo.codec,
sourceHasAudio: videoInfo.hasAudio,
width: this.config.width,
height: this.config.height,
frameRate: this.config.frameRate,
zoomRegions: this.config.zoomRegions?.length ?? 0,
speedRegions: this.config.speedRegions?.length ?? 0,
audioRegions: this.config.audioRegions?.length ?? 0,
experimentalNativeExport: this.config.experimentalNativeExport === true,
});
if (skipReason) {
this.nativeStaticLayoutSkipReason = skipReason;
this.nativeStaticLayoutSkipReasons = skipReasons;