diff --git a/electron/ipc/export/nativeStaticLayoutRoutePlan.test.ts b/electron/ipc/export/nativeStaticLayoutRoutePlan.test.ts new file mode 100644 index 000000000..36bcaf064 --- /dev/null +++ b/electron/ipc/export/nativeStaticLayoutRoutePlan.test.ts @@ -0,0 +1,167 @@ +import { describe, expect, it } from "vitest"; +import { + type NativeStaticLayoutRouteSource, + type NvidiaCudaExportCapabilityProbe, + planNativeStaticLayoutRoutes, + type WindowsD3D11ExportCapabilityProbe, +} from "./nativeStaticLayoutRoutePlan"; + +const source: NativeStaticLayoutRouteSource = { + inputCodec: "h264", + proxyCodec: "h264", + proxyCreated: false, +}; + +const cudaProbe: NvidiaCudaExportCapabilityProbe = { + platform: "win32", + appPackaged: true, + explicitEnabled: false, + explicitDisabled: false, + packagedAutoCandidateEnabled: true, + packagedAutoCandidateActive: true, + windowsGpuCompositorEnabled: true, + wrapperPath: "C:\\Recordly\\run-mp4-pipeline.mjs", + hasNvidiaGpu: true, + audioMode: "copy-source", + audioSkipReason: null, + stallTimeoutMs: 120_000, + skipReason: null, +}; + +const d3d11Probe: WindowsD3D11ExportCapabilityProbe = { + platform: "win32", + windowsGpuCompositorEnabled: true, + helperPath: "C:\\Recordly\\recordly-gpu-export.exe", + adapterIndexOverride: null, + preferHighPerformanceAdapter: true, + nvencSdkRequested: false, + skipReason: null, +}; + +describe("planNativeStaticLayoutRoutes", () => { + it("selects CUDA first and records D3D11 plus FFmpeg fallbacks", () => { + expect( + planNativeStaticLayoutRoutes({ + cuda: cudaProbe, + d3d11: d3d11Probe, + source, + }), + ).toEqual({ + selectedRoute: "nvidia-cuda-compositor", + decisions: [ + { + route: "nvidia-cuda-compositor", + status: "selected", + reasons: ["cuda-wrapper-and-nvidia-gpu-available"], + }, + { + route: "windows-d3d11-compositor", + status: "fallback", + reasons: ["documented-fallback-if-cuda-runtime-fails"], + }, + { + route: "ffmpeg-static-layout", + status: "fallback", + reasons: ["native-gpu-runtime-fallback"], + }, + ], + cuda: cudaProbe, + d3d11: d3d11Probe, + source, + }); + }); + + it("selects D3D11 when CUDA is unavailable", () => { + const cuda = { + ...cudaProbe, + hasNvidiaGpu: false, + skipReason: "nvidia-gpu-unavailable", + } satisfies NvidiaCudaExportCapabilityProbe; + + expect( + planNativeStaticLayoutRoutes({ + cuda, + d3d11: d3d11Probe, + source, + }), + ).toMatchObject({ + selectedRoute: "windows-d3d11-compositor", + decisions: [ + { + route: "nvidia-cuda-compositor", + status: "rejected", + reasons: ["nvidia-gpu-unavailable"], + }, + { + route: "windows-d3d11-compositor", + status: "selected", + reasons: ["documented-fallback-after-cuda-skip:nvidia-gpu-unavailable"], + }, + { + route: "ffmpeg-static-layout", + status: "fallback", + reasons: ["windows-d3d11-runtime-fallback"], + }, + ], + }); + }); + + it("selects FFmpeg when both native GPU routes are unavailable", () => { + const cuda = { + ...cudaProbe, + wrapperPath: null, + skipReason: "cuda-wrapper-unavailable", + } satisfies NvidiaCudaExportCapabilityProbe; + const d3d11 = { + ...d3d11Probe, + helperPath: null, + skipReason: "windows-gpu-helper-unavailable", + } satisfies WindowsD3D11ExportCapabilityProbe; + + expect( + planNativeStaticLayoutRoutes({ + cuda, + d3d11, + source, + }), + ).toEqual({ + selectedRoute: "ffmpeg-static-layout", + decisions: [ + { + route: "nvidia-cuda-compositor", + status: "rejected", + reasons: ["cuda-wrapper-unavailable"], + }, + { + route: "windows-d3d11-compositor", + status: "rejected", + reasons: ["windows-gpu-helper-unavailable"], + }, + { + route: "ffmpeg-static-layout", + status: "selected", + reasons: ["native-gpu-routes-unavailable"], + }, + ], + cuda, + d3d11, + source, + }); + }); + + it("preserves proxy source metadata for route diagnostics", () => { + const proxiedSource = { + inputCodec: "vp9", + proxyCodec: "h264", + proxyCreated: true, + }; + + expect( + planNativeStaticLayoutRoutes({ + cuda: cudaProbe, + d3d11: d3d11Probe, + source: proxiedSource, + }).source, + ).toEqual(proxiedSource); + }); +}); diff --git a/electron/ipc/export/nativeStaticLayoutRoutePlan.ts b/electron/ipc/export/nativeStaticLayoutRoutePlan.ts new file mode 100644 index 000000000..3ee6c4e73 --- /dev/null +++ b/electron/ipc/export/nativeStaticLayoutRoutePlan.ts @@ -0,0 +1,132 @@ +import type { NativeVideoExportAudioMode } from "../nativeVideoExport"; + +export type NativeStaticLayoutRoute = + | "nvidia-cuda-compositor" + | "windows-d3d11-compositor" + | "ffmpeg-static-layout"; + +export interface NativeStaticLayoutRouteDecision { + route: NativeStaticLayoutRoute; + status: "selected" | "fallback" | "rejected"; + reasons: string[]; +} + +export interface 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; +} + +export interface WindowsD3D11ExportCapabilityProbe { + platform: NodeJS.Platform; + windowsGpuCompositorEnabled: boolean; + helperPath: string | null; + adapterIndexOverride: number | null; + preferHighPerformanceAdapter: boolean; + nvencSdkRequested: boolean; + skipReason: string | null; +} + +export interface NativeStaticLayoutRouteSource { + inputCodec: string; + proxyCodec?: string; + proxyCreated: boolean; +} + +export interface NativeStaticLayoutRoutePlan { + selectedRoute: NativeStaticLayoutRoute; + decisions: NativeStaticLayoutRouteDecision[]; + cuda: NvidiaCudaExportCapabilityProbe; + d3d11: WindowsD3D11ExportCapabilityProbe; + source: NativeStaticLayoutRouteSource; +} + +export function planNativeStaticLayoutRoutes(options: { + cuda: NvidiaCudaExportCapabilityProbe; + d3d11: WindowsD3D11ExportCapabilityProbe; + source: NativeStaticLayoutRouteSource; +}): NativeStaticLayoutRoutePlan { + const { cuda, d3d11, source } = 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, + }; + } + + 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, + }; + } + + 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, + }; +}