diff --git a/src/lib/exporter/backendPolicy.test.ts b/src/lib/exporter/backendPolicy.test.ts index 7c532a18..9a33701c 100644 --- a/src/lib/exporter/backendPolicy.test.ts +++ b/src/lib/exporter/backendPolicy.test.ts @@ -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"], + }, + ], + }); + }); }); diff --git a/src/lib/exporter/backendPolicy.ts b/src/lib/exporter/backendPolicy.ts index 60f4f509..85be5209 100644 --- a/src/lib/exporter/backendPolicy.ts +++ b/src/lib/exporter/backendPolicy.ts @@ -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"; }