mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-25 15:25:44 +00:00
Route Windows Lightning exports through NVIDIA native planner
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user