From c87acf4b6d4534861ce2acabb15dadcf44ff5de0 Mon Sep 17 00:00:00 2001 From: wiiiii123 Date: Fri, 22 May 2026 18:27:10 +0700 Subject: [PATCH 1/2] refactor(export): add lightning route policy plan --- src/lib/exporter/backendPolicy.test.ts | 52 ++++++++++++ src/lib/exporter/backendPolicy.ts | 107 ++++++++++++++++++++++++- 2 files changed, 158 insertions(+), 1 deletion(-) diff --git a/src/lib/exporter/backendPolicy.test.ts b/src/lib/exporter/backendPolicy.test.ts index 7c532a18f..9a33701cf 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 60f4f5097..85be52091 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"; } From cee6a12ce4151ff84724f1a49c0abf4490346113 Mon Sep 17 00:00:00 2001 From: wiiiii123 Date: Fri, 22 May 2026 18:38:56 +0700 Subject: [PATCH 2/2] test(export): cover lightning route policy decisions --- src/lib/exporter/backendPolicy.test.ts | 117 +++++++++++++++++++++++++ src/lib/exporter/backendPolicy.ts | 5 ++ 2 files changed, 122 insertions(+) diff --git a/src/lib/exporter/backendPolicy.test.ts b/src/lib/exporter/backendPolicy.test.ts index 9a33701cf..e2c3827f6 100644 --- a/src/lib/exporter/backendPolicy.test.ts +++ b/src/lib/exporter/backendPolicy.test.ts @@ -76,4 +76,121 @@ describe("backendPolicy", () => { ], }); }); + + it("documents the native static layout path when Breeze is selected explicitly", () => { + expect( + planLightningExportRoutes({ + backendPreference: "breeze", + platform: "win32", + nativeStaticLayoutAvailable: true, + }), + ).toEqual({ + selectedRoute: "native-static-layout", + decisions: [ + { + route: "native-static-layout", + status: "selected", + reasons: ["visually-compatible"], + }, + { + route: "breeze-stream", + status: "fallback", + reasons: ["user-selected-breeze"], + }, + { + route: "webcodecs", + status: "fallback", + reasons: ["breeze-unavailable-fallback"], + }, + ], + }); + }); + + it("keeps explicit Breeze selected when native static layout is unavailable", () => { + expect( + planLightningExportRoutes({ + backendPreference: "breeze", + platform: "win32", + nativeStaticLayoutAvailable: false, + }), + ).toEqual({ + selectedRoute: "breeze-stream", + decisions: [ + { + route: "native-static-layout", + status: "rejected", + reasons: ["native-static-unavailable"], + }, + { + route: "breeze-stream", + status: "selected", + reasons: ["user-selected-breeze"], + }, + { + route: "webcodecs", + status: "fallback", + reasons: ["breeze-unavailable-fallback"], + }, + ], + }); + }); + + it("records explicit Breeze native static layout skip reasons", () => { + expect( + planLightningExportRoutes({ + backendPreference: "breeze", + platform: "win32", + nativeStaticLayoutAvailable: true, + nativeStaticLayoutSkipReasons: ["unsupported-audio-mix"], + }), + ).toEqual({ + selectedRoute: "breeze-stream", + decisions: [ + { + route: "native-static-layout", + status: "rejected", + reasons: ["unsupported-audio-mix"], + }, + { + route: "breeze-stream", + status: "selected", + reasons: ["user-selected-breeze"], + }, + { + route: "webcodecs", + status: "fallback", + reasons: ["breeze-unavailable-fallback"], + }, + ], + }); + }); + + it("documents why macOS auto skips native static layout", () => { + expect( + planLightningExportRoutes({ + backendPreference: "auto", + platform: "darwin", + nativeStaticLayoutAvailable: true, + }), + ).toEqual({ + selectedRoute: "breeze-stream", + decisions: [ + { + route: "native-static-layout", + status: "rejected", + reasons: ["platform-does-not-use-native-static-layout"], + }, + { + route: "breeze-stream", + status: "selected", + reasons: ["platform-prefers-native-streaming"], + }, + { + route: "webcodecs", + status: "fallback", + reasons: ["breeze-unavailable-fallback"], + }, + ], + }); + }); }); diff --git a/src/lib/exporter/backendPolicy.ts b/src/lib/exporter/backendPolicy.ts index 85be52091..ecfaa11d7 100644 --- a/src/lib/exporter/backendPolicy.ts +++ b/src/lib/exporter/backendPolicy.ts @@ -107,6 +107,11 @@ export function planLightningExportRoutes(options: { } if (options.backendPreference === "auto" && shouldPreferNativeAutoBackend(options.platform)) { + decisions.push({ + route: "native-static-layout", + status: "rejected", + reasons: ["platform-does-not-use-native-static-layout"], + }); decisions.push({ route: "breeze-stream", status: "selected",