# Flow 架構

URL: https://flow.lanstar.dev/docs/architecture

> 範圍：macOS
> 定位：專為 Apple Silicon 最佳化的原生影片播放器，結合「播放引擎 + 影像處理框架 + 可替換模組」。

---

## 1. 設計目標與原則

- 目標：見 [`REQUIREMENTS.md`](/docs/requirements)。
- 原則：見 [`AGENTS.md`](https://github.com/xlanstar/flow/blob/main/AGENTS.md#principles)。
- **核心／模組邊界**：核心決定幀「怎麼流」（執行緒、佇列、時鐘、generation、何時處理與丟棄）；模組決定幀「長什麼樣」（解析、解碼、像素變換、GPU 編碼、輸出）。核心只把幀當不透明的 IOSurface-backed `CVPixelBuffer` 搬運，不讀寫內容。

---

## 2. 技術選型

| 層級             | 選擇                                                                                      | 模組                                       | 理由                                                                    |
| ---------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------- |
| 語言             | Swift、Metal Shading Language、少量 C（vendored 橋接）                                    | —                                          | C 只用於 vendored 函式庫邊界                                            |
| 最低系統         | macOS 26（以 Xcode 27 建置）                                                              | —                                          | 可直接使用 Metal 4、新版 SwiftUI；免去大量 `#available` 分支            |
| 容器解析（Demux） | FFmpeg `libavformat`（LGPL，動態連結）                                                    | `FlowFFmpeg`                               | 支援 MKV/MP4/TS/WebM/AVI 等幾乎所有容器與串流協定                       |
| 視訊解碼         | VideoToolbox 硬解為主；`dav1d`（AV1）、`libavcodec` 軟解為備援                            | `FlowVideoToolbox`、`FlowDav1d`、`FlowFFmpeg` | 硬解省電；不支援的編碼由軟解補足                                  |
| 音訊解碼         | `libavcodec` + AudioToolbox                                                               | `FlowFFmpeg`                               | 涵蓋 DTS／TrueHD／FLAC／Opus 等                                      |
| 音訊輸出         | AVAudioEngine（`AVAudioSourceNode`）、空間音訊／多聲道                                   | `FlowAVAudio`                              | 系統整合（AirPods 空間音訊、裝置切換）                                  |
| GPU              | Metal 4（`MTL4CommandQueue`、Argument Table）                                             | `FlowMetal4`                               | 低開銷提交                                                              |
| 字幕             | 解碼：`libavcodec`（文字字幕轉 ASS 事件、點陣字幕轉 RGBA）；渲染：libass（ASS）             | `FlowFFmpeg`、`FlowLibass`                 | libavcodec 涵蓋多數字幕格式並統一為 ASS；libass 是 ASS 渲染事實標準     |
| Dolby Vision     | libdovi（RPU 解析）+ 自製 Metal reshaping/tone mapping                                    | `FlowDolbyVision`                          | 硬解只給 base layer，需要自行套用 RPU                                   |
| 神經網路         | Core ML（ANE 優先，GPU 備援），輸入輸出為 IOSurface-backed `CVPixelBuffer`                | `FlowCoreML`                               | ANE 省電且不佔 GPU；zero-copy                                           |
| UI               | SwiftUI；影片顯示層以 `NSViewRepresentable` 包 `CAMetalLayer`                             | `FlowUI`、`FlowMetal4`                     | 顯示層需要精準控制                                                      |
| Shader           | 內建：MSL，建置期編譯為 metallib；使用者：mpv 相容 GLSL（glslang → SPIR-V → SPIRV-Cross 轉 MSL）或 MSL，執行期編譯 | `FlowMetal4`、`FlowGLSL`、`FlowUserShaders` | 內建零編譯成本；GLSL 沿用 mpv 生態，MSL 可用 Metal 專屬功能 |
| 模組             | 僅第一方，靜態連結、in-process；不支援外部載入                                            | `Modules/*`                                | 完整 SwiftUI、熱路徑零開銷；免外部程式碼的簽章與信任問題                |
| 持久化           | SwiftData                                                                                 | `FlowLibrary`                              | 媒體庫、播放紀錄、續播點                                                |
| 建置             | Xcode 專案 + 本地 Swift Packages；vendored 函式庫以 XCFramework 形式提供                  | —                                          | 模組邊界清楚、可單獨測試                                                |
| 發行             | GitHub Release + Homebrew Cask，見 `.agents/skills/release/`                              | —                                          | 不受沙盒與審查限制                                                      |

> **授權**：本體為 Apache-2.0。LGPL 元件（FFmpeg、FriBidi、libsmb2）以動態 framework 隨附，其餘靜態連結；清單與履行方式見 [`THIRD_PARTY_NOTICES.md`](https://github.com/xlanstar/flow/blob/main/THIRD_PARTY_NOTICES.md)。開源授權不含編解碼器專利，專利敏感格式優先交由 VideoToolbox／AudioToolbox。內建 shader 與移植程式碼限寬鬆授權；libplacebo、mpv 只作行為參考，不移植程式碼。

---

## 3. 系統總覽

### 3.1 結構

```mermaid
flowchart TB
    subgraph App["App 層"]
        UI[FlowUI<br/>播放控制／媒體庫／設定]
        Platform[平台整合<br/>Now Playing／PiP／App Intents／MCP／快捷鍵]
        Root[composition root<br/>選用模組]
    end

    subgraph Core["核心：契約 + 控制平面"]
        Cmd[FlowController<br/>PlayerAPI 實作／Profiles]
        Engine[FlowEngine<br/>狀態機／執行緒／時鐘／元件選擇]
        Graph[FlowGraph<br/>Frame／Display Graph 拓撲]
        Host[FlowModuleHost<br/>啟用／停用]
        Reg[FlowRegistry<br/>已註冊實作與撤銷]
        API[[FlowInterfaces<br/>擴充點協定]]
        PAPI[[FlowPlayerAPI<br/>PlayerAPI／Command／Property／Event]]
    end

    subgraph Mods["模組：實作"]
        Media[MediaSource／Demuxer<br/>FFmpeg、Network]
        Dec[Decoder<br/>VideoToolbox、Dav1d、FFmpeg]
        RT[NodeRuntime<br/>Metal4、VapourSynth、CoreML]
        Nodes[Node／前端<br/>Effects、GLSL、UserShaders、DolbyVision]
        Out[Presenter／AudioOutput<br/>Metal4、AVAudio]
        Sub[Subtitle<br/>FFmpeg、Libass]
    end

    UI --> PAPI
    Platform --> PAPI
    Cmd -.->|實作| PAPI
    API --> PAPI
    Root --> Host
    Cmd --> Engine
    Engine --> Graph
    Engine -->|選擇| Reg
    Engine -->|呼叫| API
    Graph -->|呼叫| API
    Host -->|註冊| Reg
    Reg --> API
    Mods -.->|實作| API
```

### 3.2 資料流

```mermaid
flowchart LR
    Src[MediaSource] --> Dmx[Demuxer] --> PR((packet ring))
    PR --> VD[VideoDecoder] --> FS((FrameSink)) --> Seg[Frame Graph 段<br/>FrameRuntime] --> FQ((FrameQueue)) --> DP[DisplayPipeline] --> Pr[Presenter]
    PR --> AD[AudioDecoder] --> AR((PCM ring)) --> AO[AudioOutput] <-->|時間戳／速率| Clk((Clock))
    PR --> SDec[SubtitleDecoder] --> SD[SubtitleRenderer] -->|overlay| DP
    Clk <-->|vsync／顯示時間| Pr
```

> 圓形為核心擁有的佇列與時鐘；方框為模組實作的介面。

---

## 4. 模組劃分（Swift Packages）

```
Flow/
├── Apps/
│   └── FlowMac/                 # App target；只經 FlowComposition 的 FlowPlayer 與 PlayerAPI 操作播放器
├── Benchmarks/                  # package-benchmark：micro、播放情境、size、baseline（docs/BENCHMARK.md）
├── Packages/
│   ├── Core/                    # 不含任何實作的播放器核心；各 target 位於 Sources/<target>/
│   │   │                        # 契約：模組、UI 與自動化編譯時依賴的型別與協定
│   │   ├── FlowBase/            # MediaTime、ColorInfo、FrameGeometry、TrackInfo、SPSC ring、Logger、Signpost、計數器
│   │   ├── FlowPlayerAPI/       # PlayerAPI、Command／Property／Event 與內建名稱；UI 與自動化唯一能碰的介面
│   │   ├── FlowInterfaces/      # 擴充點協定、FlowModule、資料平面型別（Packet、VideoFrame、PCMFormat）
│   │   │                        # 控制平面：只有 Composition、IntegrationTests、Benchmarks 連結
│   │   ├── FlowRegistry/        # ComponentRegistry：依擁有者記錄與撤銷註冊
│   │   ├── FlowGraph/           # Frame／Display Graph 拓撲、分段、規格推導、預讀、bypass
│   │   ├── FlowController/      # PlayerController（PlayerAPI 實作）、按鍵綁定、Profiles
│   │   ├── FlowEngine/          # PlayerSession 狀態機、執行緒、FrameQueue／PCM ring、時鐘與 A/V 同步、Seek、元件選擇與 fallback、Governor
│   │   ├── FlowModuleHost/      # 模組啟用／停用
│   │   │                        # 開發用：App 不連結；Core 自身的測試也用，故留在 Core
│   │   ├── FlowSampling/        # 行程／執行緒／記憶體取樣器；測試與 benchmark 用
│   │   └── FlowTestSupport/     # mock 模組、TestModuleContext、fixture 定位、MemorySource；Core、模組與整合測試共用
│   ├── Composition/             # FlowComposition：composition root，唯一組裝 Modules/* 之處；App 與 Benchmarks 共用
│   ├── IntegrationTests/        # 真實模組 + mock 輸出的端到端播放測試（§11.2）
│   ├── Library/                 # FlowLibrary，App 層：媒體庫、播放紀錄、續播（SwiftData）
│   ├── MCP/                     # FlowMCP，App 層：MCP server（Streamable HTTP，僅 loopback）
│   └── UI/                      # FlowUI，App 層：SwiftUI 元件（控制列、OSD、設定頁）
├── Modules/                     # 實作；模組本身靜態連結（LGPL 函式庫除外，見 §2）
│   ├── File/                    # FileSource：本機檔案 MediaSource
│   ├── FFmpeg/                  # FFmpegDemuxer（含 HLS）、FFmpegVideoDecoder、FFmpegAudioDecoder、FFmpegSubtitleDecoder、點陣字幕渲染
│   ├── VideoToolbox/            # VTVideoDecoder
│   ├── Dav1d/                   # Dav1dVideoDecoder
│   ├── Network/                 # SMB（libsmb2）／WebDAV／HTTP MediaSource
│   ├── AVAudio/                 # AVAudioEngine AudioOutput
│   ├── Libass/                  # ASS 字幕渲染
│   ├── Metal4/                  # Metal4DisplayRuntime、MTL4ShaderNode、Presenter、內建步驟與 fallback 節點
│   │   ├── Plugins/             # MetalLibraryPlugin：建置期把 Shaders/*.metal 編譯為 FlowMetal4.metallib
│   │   └── Shaders/
│   │       ├── Include/Flow/    # Shader ABI（§7.4），使用者 .metal 亦依賴
│   │       └── *.metal          # 色彩轉換、bilinear、基本 tone mapping、dither
│   ├── DolbyVision/             # libdovi RPU 解析 + reshaping 節點（依賴 Metal4）
│   ├── GLSL/                    # GLSL → MSL 前端、GLSLShaderNode（依賴 Metal4）
│   ├── UserShaders/             # mpv `//!` 解析、匯入管理（依賴 Metal4、GLSL）
│   ├── Effects/                 # 高品質縮放、去色帶、銳化、去交錯、動態 tone mapping（依賴 Metal4）
│   ├── VapourSynth/             # VSRuntime、VapourSynthFilterNode
│   └── CoreML/                  # CoreMLRuntime、神經網路超解析／降噪節點
├── Vendor/                      # 由 scripts/ 產生，不入庫
│   ├── libav*.xcframework       # FFmpeg（libavformat、libavcodec、libavutil、libswresample）；LGPL，動態；scripts/build-ffmpeg.sh
│   ├── Licenses/                # 各函式庫授權全文，隨 App 附於 Resources/Licenses/
│   ├── dav1d.xcframework
│   ├── libass.xcframework       # 含 freetype／harfbuzz
│   ├── fribidi.xcframework      # LGPL，動態
│   ├── libsmb2.xcframework      # LGPL，動態；僅 FlowNetwork 使用
│   ├── libdovi.xcframework
│   ├── glslang.xcframework      # 僅 FlowGLSL 使用（C API）
│   └── SPIRV-Cross.xcframework  # 同上
├── Tests/（各 package 內）     # 單元、Golden image、效能測試
└── docs/
```

**命名**：資料夾與 package 名稱為角色（`Core`、`Composition`、`UI`、`FFmpeg`），product 與 Swift module 加 `Flow` 前綴（`FlowUI`、`FlowFFmpeg`），以免與 Apple framework（`AVAudio`、`VideoToolbox`）或其他 module 撞名。`Core` 以外皆為單一 product `Flow<資料夾>`。

**分層**：

| 層 | 位置 | 可依賴 |
| --- | --- | --- |
| 契約 | `FlowBase`、`FlowPlayerAPI`、`FlowInterfaces` | 契約 |
| 控制平面 | `FlowRegistry`、`FlowGraph`、`FlowController`、`FlowEngine`、`FlowModuleHost` | 契約、控制平面 |
| 模組 | `Modules/*` | 契約、runtime 模組 |
| 組裝 | `Composition` | 契約、控制平面、模組 |
| 前端 | `UI`、`MCP`、`Library` | `FlowBase`、`FlowPlayerAPI` |
| App | `Apps/FlowMac` | 前端、`FlowComposition`、`FlowBase`、`FlowPlayerAPI` |
| 工具 | `IntegrationTests`、`Benchmarks`、`FlowSampling`、`FlowTestSupport` | 不限；App 不連結 |

**依賴方向（只能向下）**：

```
Apps → {FlowUI, FlowMCP, FlowLibrary, FlowComposition}
FlowComposition → {FlowModuleHost, FlowEngine, Modules/*}
{FlowUI, FlowMCP} → FlowPlayerAPI
FlowModuleHost → FlowRegistry
FlowEngine → {FlowGraph, FlowRegistry, FlowController}
{FlowGraph, FlowRegistry} → FlowInterfaces → FlowPlayerAPI → FlowBase
FlowController → FlowPlayerAPI
Modules/* → [runtime 模組，如 Metal4] → FlowInterfaces
```

**邊界規則**：

- `Core/` 只定義契約並掌管邏輯、生命週期與控制平面；不得匯入 SwiftUI、AppKit、Network、Metal、VideoToolbox、AVFAudio 與任何 vendored C 函式庫（CoreVideo 僅作為 `CVPixelBuffer`／IOSurface 的不透明載體）。SwiftLint custom rule（`core_banned_import`、`core_banned_link`）檢查 `Packages/Core` 的 import 與連結。
- `Modules/` 只能依賴契約、自身的 vendored 函式庫，以及所需的 runtime 模組；不得依賴控制平面。模組測試另可用 `FlowTestSupport`（`TestModuleContext` 記錄註冊）。
- App、`FlowUI` 與 `FlowMCP` 只依賴 `FlowPlayerAPI`（不依賴 `FlowInterfaces`、`FlowEngine`），經 `PlayerAPI` 操作播放器；`FlowPlayer` 只以 `@_spi(Benchmark)` 開放統計、display runtime、額外 presenter 與 `PresentationSurface` 給 Benchmarks，不暴露 registry 與 `PlayerSession`。
- 依賴由 `Package.swift` 宣告強制，`scripts/check-deps.sh`（prek）依上表檢查各 manifest、`project.yml` 與 App 的 import，並檢查命名；package 的 import 由 explicit module build 限制在依賴閉包內；所有 package 與 App target 啟用 `MemberImportVisibility`（只能使用本檔 import 的模組成員）、`ExistentialAny`、`InternalImportsByDefault`（import 預設 internal，公開 API 用到的型別才 `public import`，vendored C 型別因此無法出現在公開 API），警告視為錯誤。
- 模組只公開 `FlowModule` 型別；其餘實作為 internal，測試以 `@testable import` 存取。Core 與模組中只有 Benchmarks 需要的型別（如 `FrameQueue`、`PlaybackClock`）以 `@_spi(Benchmark)` 開放，不設為 `public`。
- 是否「必要、不可停用」由 App 組裝時標記（§7.1），不由資料夾決定；核心不內建任何實作，沒有模組時無法播放。
- 官方功能用 `FlowInterfaces` 寫不出來即視為介面缺陷。

---

## 5. 播放引擎

### 5.1 執行緒模型

執行緒由 `FlowEngine` 建立與擁有；模組方法只在下表指定的執行緒被呼叫。

| 執行緒       | QoS／優先權                       | 職責                                                           |
| ------------ | ---------------------------------- | -------------------------------------------------------------- |
| Demux        | userInitiated                      | 呼叫 `Demuxer.readPacket`，寫入 packet ring                     |
| Video decode | userInitiated                      | 呼叫 `VideoDecoder.decode`；VT 等非同步解碼器由其 callback 寫入 `FrameSink` |
| Audio decode | userInteractive                    | 解碼並重取樣到輸出格式，寫入 PCM ring                           |
| Audio render | 即時（Core Audio IO 執行緒）       | `AudioOutput` 從 PCM ring 取樣本，**禁止上鎖與配置記憶體**        |
| Frame Graph  | userInitiated（Swift Concurrency） | 依 PTS 順序 pull 各 runtime 段，寫入 `FrameQueue`；非熱路徑     |
| Subtitle     | userInitiated                      | 呼叫 `SubtitleDecoder.decode`；依 `FrameQueue` 中即將顯示的 PTS 預先呼叫 `SubtitleRenderer.render`，寫入 overlay 佇列 |
| Render       | userInteractive，`CAMetalDisplayLink` | 選幀與已備妥的 `OverlayFrame`、呼叫 `DisplayPipeline.render`     |

資料平面元件之間以核心提供的**單一生產者／單一消費者 lock-free ring buffer** 溝通；跨平面以 `AsyncStream` 回報事件（狀態變化、錯誤、統計）。

### 5.2 核心協定

定義於 `FlowInterfaces`；實作以 factory 註冊，由 `FlowEngine` 選擇與建立。

```swift
public protocol MediaSource: Sendable {
    var url: URL { get }
    var isSeekable: Bool { get }
    func read(into buffer: UnsafeMutableRawBufferPointer, at offset: Int64) throws -> Int
    var length: Int64? { get }
}

public protocol DemuxerFactory: Sendable {
    var id: String { get }
    func probe(_ source: any MediaSource) -> ProbeScore   // .unsupported 表示不支援
    /// `opener` 供需另開 URL 的 demuxer（HLS 的 playlist、segment）使用
    func make(_ source: any MediaSource, opener: any MediaOpener) throws -> any Demuxer
}

/// 核心實作；依 URL scheme 選用已註冊的 `MediaSourceFactory`
public protocol MediaOpener: Sendable {
    func open(_ url: URL) throws -> any MediaSource
}

public protocol Demuxer: AnyObject {
    var tracks: [TrackInfo] { get }
    /// 第一次 readPacket 前呼叫一次；其他軌的 packet 可不讀
    func setEnabledTracks(_ indices: Set<Int>)
    func readPacket() throws -> Packet?            // nil = EOF
    func seek(to time: MediaTime, mode: SeekMode) throws
    // 待實作：
    var attachments: [Attachment] { get }          // 內嵌字型等（MKV attachment）
}

public protocol VideoDecoderFactory: Sendable {
    var id: String { get }
    func support(for format: VideoFormat) -> DecoderSupport  // .hardware / .software / .none
    /// 解出的幀寫入 `sink`；佇列由核心擁有
    func make(_ format: VideoFormat, sink: FrameSink) throws -> any VideoDecoder
}

public protocol VideoDecoder: AnyObject {
    /// 每幀的像素格式；make 後固定，讓 Display Graph 在首幀前編譯
    var outputFormat: VideoPixelFormat { get }
    /// 非同步解碼器已收下、尚未產出的 packet 數；核心計入佇列高水位（同步解碼器為 0）
    var framesInFlight: Int { get }
    func decode(_ packet: Packet) throws
    /// EOF 時送出內部剩餘幀（如重排序中的 B 幀）
    func drain() throws
    func flush()
    // 待實作：播放落後時由 `FlowEngine` 在 Video decode 執行緒設定；不支援者可忽略
    var skip: DecodeSkip { get set }   // .none / .nonReference
}

/// 核心實作，不阻塞；背壓由 `FlowEngine` 暫停呼叫 `decode` 達成
public protocol FrameSink: AnyObject, Sendable {
    /// Video decode 執行緒或解碼器 callback 呼叫，一次一個；依 PTS 遞增寫入
    func push(_ frame: VideoFrame)
    /// 解碼器 callback 中 framesInFlight 減少但未 push（丟棄、失敗）時呼叫，讓核心恢復解碼
    func framesInFlightDidDecrease()
}

// CVPixelBuffer 非 Sendable；建立後唯讀故安全。CoreVideo 標註 Sendable 後移除 @unchecked
public struct VideoFrame: @unchecked Sendable {
    public let pixelBuffer: CVPixelBuffer   // 一律 IOSurface-backed
    public let pts: MediaTime
    public let duration: MediaTime
    public let color: ColorInfo             // primaries / transfer / matrix / range / chroma location
    public let geometry: FrameGeometry      // 旋轉（90° 倍數）、鏡像、像素長寬比
    public let generation: UInt32           // seek 世代，用來丟棄過期幀
    // 待實作（VID-02）：
    public let hdr: HDRMetadata?            // mastering display、CLL、DV RPU、HDR10+
}
```

`MediaSource`、`AudioDecoder`、`AudioOutput`、`SubtitleDecoder`、`SubtitleRenderer`、`Presenter` 同樣採 factory + 實例協定（字幕見 §9）。

- **HLS**：屬 demux 層，無法以單一 `MediaSource` 的位元組 offset 表達。playlist 以 `MediaSource` 開啟並經 `probe` 辨識，demuxer 再經 `MediaOpener` 開啟 variant playlist 與 segment；網路 I/O 仍由 `FlowNetwork` 的 HTTP `MediaSource` 負責。`FlowFFmpeg` 以 libavformat 的 HLS demuxer 實作，經 `AVFormatContext.io_open` 接上 `MediaOpener`。直播時總時長未知（§6.3）。
- **顯示幾何**：`FrameGeometry` 由 demuxer 自容器（MP4 display matrix、MKV 欄位）或碼流（SAR）取得，解碼器沿用 packet 上的值填入；像素保持儲存方向，由 Display Graph 的 `scale` 套用（§6.4）。

**生命週期契約**（所有元件適用）：

| 階段  | 方法                                  | 執行緒                     | 規則                                                   |
| ----- | ------------------------------------- | -------------------------- | ------------------------------------------------------ |
| 建立  | `Factory.make`、`configure`           | 控制平面（可 async）       | 可配置記憶體、可拋錯；失敗時 `FlowEngine` 換下一個實作 |
| 處理  | `readPacket`、`decode`、`render` 等   | 資料平面（§5.1）           | 不得 await；上鎖、配置記憶體、阻塞依執行緒分級（硬即時／軟即時／吞吐）限制，見 `swift-realtime-media` Skill |
| Flush | `flush`、`seek`、`reset`              | 該元件的資料平面執行緒     | 只丟棄內部狀態，不釋放資源                             |
| 重設  | `configure`                           | 控制平面，元件暫停時       | `FlowEngine` 保證不與處理並行                          |
| 釋放  | `deinit`                              | 控制平面                   | `FlowEngine` 保證在最後一次處理之後                    |

**輸出契約**：所有解碼器輸出 IOSurface-backed `CVPixelBuffer` 並填妥 `ColorInfo`，**不做色彩轉換**；YUV→RGB 由 Display Graph 掌控以保證品質一致。輸出須依 PTS 遞增，重排序由解碼器模組負責（如 VT：HEVC 即使開啟 temporal processing 仍依解碼順序輸出，由 `FrameReorder` 依 PTS 重排）。

### 5.3 解碼策略

1. `FlowEngine` 依 `VideoFormat`（codec、profile、bit depth、解析度）詢問所有已註冊 factory 的 `support()`：`.hardware` 優先於 `.software`，同級依 App 組裝順序。
2. 建立或解碼發生致命錯誤時，`FlowEngine` 改用下一個 factory，並從前一關鍵幀重新解碼。
3. 實作細節屬各模組，例如 `FlowVideoToolbox` 依內容選輸出格式（8-bit → `420YpCbCr8BiPlanar`；10-bit/HDR → `420YpCbCr10BiPlanar`；`VideoRange`／`FullRange` 依來源 range；ProRes 4444 → `4444AYpCbCr16`），軟解模組寫入 `CVPixelBufferPool` 的 IOSurface buffer。

### 5.4 時鐘與 A/V 同步

時鐘與同步策略屬 `FlowEngine`；`AudioOutput` 與 `Presenter` 只回報時間戳。

**時鐘**

- `PlaybackClock` 為錨點映射 `(hostTime, mediaTime, rate)`，以原子快照發布，其他執行緒無鎖讀取；`display-resample` 由 Render 執行緒依 vsync 推進，`audio` 由 Audio render 執行緒依音訊時間更新。
- 音訊時間＝`AudioOutput` 回報的 (sample time, host time) 扣除輸出延遲（裝置、串流、safety offset、緩衝）；裝置變更時重新查詢延遲。無音軌時以 host time 推進。
- 換裝置、音軌結束、underrun 時重新錨定，媒體時間保持單調、不跳躍。
- `AudioOutput` 停止並重啟後，回報的 host time 須與已消耗的樣本一致；核心另保證「聽到的時間」不早於現在，防止輸出的 host time 停住時時鐘倒退。
- 核心經 `HostClock` 協定取得時間，測試時注入虛擬時鐘。

**同步模式**（Property `video.sync`）

- **`display-resample`（預設）**：vsync 主導，視訊每幀固定 N 個 vsync、速度調整 ≤ ±1%，音訊同比例重取樣並修正漂移；條件為刷新率在 ±1% 內為輸出幀率的整數倍。
- **`audio`**：音訊時鐘主導，視訊丟／重複幀，音訊不調整；用於其他情況。
- 輸出幀率一律取 Frame Graph 輸出的 `StreamSpec.fps`，不用來源幀率。
- 條件不符時自動降為 `audio`；條件恢復且穩定一段時間後才回到 `display-resample`（遲滯）。實際模式顯示於統計 OSD。
- **刷新率**：`FlowEngine` 依輸出幀率設定 `preferredFrameRateRange`（ProMotion／VRR，如 24p → 120Hz 的 5:5 pacing），全螢幕時可請求切換刷新率。刷新率只由輸出幀率決定；依刷新率決定輸出的節點只讀顯示器能力（§6.5），避免兩者互相追逐。

**選幀**（Render 執行緒，每 vsync）

1. 以 display link 的 `targetPresentationTimestamp` 加上量測到的呈現延遲（實際上屏時間 − `targetPresentationTimestamp`），換算該次繪製實際顯示時的媒體時間。
2. `display-resample` 依累計誤差分配每幀的 vsync 數（如 3:2 交替）；`audio` 選 `pts ≤ t` 的最新幀，邊界加遲滯避免 cadence 抖動。
3. 交由 `DisplayPipeline` 繪製；`Presenter` 回報實際顯示時間，用來校正呈現延遲並統計延遲幀與丟幀。
4. vsync 停住（如視窗被遮住）時，Render 執行緒以備援 timer 依時鐘消耗到期的幀，避免解碼卡在高水位。

**落後時降級**（依序、含遲滯，每一步記錄於統計 OSD）

- **無時間窗與 `.retimed` 節點**：降低節點品質層級（Governor，§6.7）→ Frame Graph 前丟幀 → 解碼器略過非參考幀（`VideoDecoder.skip`）→ 顯示端丟幀。
- **有時間窗或 `.retimed` 節點**：降低節點品質層級（Governor，§6.7）→ 降低重定時候選（§6.5，如 120 → 60）→ bypass 該節點 → 顯示端丟幀。

> 丟輸入幀會讓時間窗缺幀，故有時間窗節點時不在 Frame Graph 前丟幀。

**起播、Seek、暫停、倍速**

- 起播與 seek：新 `generation` 的首個音訊樣本與視訊幀都就緒才啟動時鐘；音訊裁掉目標時間前的樣本，視訊先顯示首幀。
- 暫停時 `rate = 0` 並停止 display link，只在畫面需更新（seek、字幕／OSD、顯示目標或參數變更）時繪製單幀；Frame Graph 預讀補滿即停止。`playback.rate` 改變時重新錨定，音訊以保持音高的 time-stretch 播放。
- EOF：音訊先結束改以 host time 推進；視訊結束時停在最後一幀。
- `audio.delay` 只修改音訊時間映射；字幕依實際顯示幀的 `pts` 對齊，丟幀時不失步。

### 5.5 狀態機與 Seek

```
idle → loading → ready ⇄ playing ⇄ paused
                   ↓         ↓
                seeking ←────┘ → (ready | playing | paused)
任何狀態 → error／ended
```

- Seek 時控制平面只遞增 `generation`；各 ring 由消費端丟棄舊世代的幀與樣本（控制平面不清空 SPSC ring，以維持單一生產者、單一消費者），各元件的 `flush` 在其資料平面執行緒呼叫，不需要同步等待。
- Seek 完成條件：新世代的首幀已渲染，或首幀就緒後已等滿 `displayWaitLimit`（display link 被節流時）；完成後回到 seek 前的狀態，暫停中 seek 仍為暫停。
- `SeekMode`：`.keyframe`（拖動時即時預覽）與 `.exact`（放開後精準定位，從前一關鍵幀解碼至目標）。
- **Demux 快取**：核心提供前向快取（依秒數 + 位元組上限）與後向快取，讓短距離倒退無需重新讀取網路。

### 5.6 Presenter

`Presenter` 協定定義於核心，回報顯示目標（尺寸、像素格式、刷新率與可用範圍；EDR headroom 待實作）與每幀實際顯示時間。顯示目標變化時通知 `FlowEngine`：尺寸、像素格式變化才重新編譯 Display Graph（`DisplayPlan` 相同即沿用）；EDR headroom、刷新率等參數只更新 uniform 與 `preferredFrameRateRange`，並重新判斷同步模式（§5.4）。實作在 `FlowMetal4`：

- **`MetalLayerPresenter`**：預設。`CAMetalLayer`，EDR 開啟時 `wantsExtendedDynamicRangeContent = true`、`rgba16Float` + extended linear Display P3。目前（VID-02 前）把 layer colorspace 標成來源色彩空間，drawable 依位元深度用 `bgra8Unorm`／`bgr10a2Unorm`，色域與轉移函數交給系統 compositor；HDR 是否改走 EDR 於 VID-02 決定。
- **`SampleBufferPresenter`**：子母畫面（PiP）。將 Display Graph 輸出寫回 IOSurface `CVPixelBuffer` → `CMSampleBuffer` → `AVSampleBufferDisplayLayer`，搭配 `AVPictureInPictureController.ContentSource(sampleBufferDisplayLayer:)`，讓 PiP 也保有影像處理效果；`controlTimebase` 由 `PlaybackClock` 驅動。

---

## 6. 影像處理框架

### 6.1 雙圖模型

影像處理依時間域分成兩張圖：

```mermaid
flowchart LR
    Dec[Decoder] -->|FrameSink| FG[Frame Graph<br/>使用者排序／預先處理]
    FG --> Q[FrameQueue]
    Q --> DG[Display Graph<br/>每 vsync／固定 hook]
    DG --> P[Presenter]
```

|      | Frame Graph                                                        | Display Graph                    |
| ---- | ------------------------------------------------------------------ | -------------------------------- |
| 驅動 | 依 PTS 順序 pull，領先播放位置執行                                 | 每 vsync 一次，不得阻塞          |
| 節點 | runtime 為 `FrameRuntime` 的節點，可混用多個 runtime               | 僅 `ShaderNode`，且 runtime 為目前的 `DisplayRuntime` |
| 順序 | 使用者任意排序                                                     | 依 hook 固定                     |
| 能力 | 時間窗內前後幀、可改幀率／幀數、延遲不固定                         | 一進一出，可讀已顯示的歷史幀     |
| 限制 | 不得依賴顯示參數（視窗尺寸、EDR headroom、OSD）；需要時由節點訂閱唯讀 Property 自行重設 | 不得依賴未來幀                   |

> 依賴顯示參數的步驟（縮放到輸出尺寸、tone mapping、字幕／OSD、dither）固定在 Display Graph，其餘處理放 Frame Graph。

### 6.2 節點與 Runtime

核心只定義節點階層與 runtime 介面；**節點（做什麼）**、**runtime（怎麼執行）**、**語言前端（怎麼寫）** 三者分離：

```swift
// FlowInterfaces（不依賴任何 GPU API）
public protocol RenderNode: AnyObject, Sendable {
    var descriptor: NodeDescriptor { get }   // id、placement（.frame / .display(hook)）、時間窗、需求色彩空間、品質層級數、是否逐像素
    var runtime: RuntimeID { get }           // 由哪個 runtime 執行
    /// 格式或參數改變時呼叫；回傳輸出規格，讓 FlowGraph 規劃資源與輸出時間
    func configure(input: NodeSpec, context: NodeContext) throws -> NodeSpec
    /// Governor 切換品質層級（0 為最高，§6.7）；由 runtime 在兩次處理之間呼叫
    func setQuality(_ level: Int)
}

/// 非同步，可用 CPU／外部 runtime；只能放 Frame Graph
public protocol FilterNode: RenderNode {}

/// 同步編碼 GPU 指令、不得阻塞；可放的圖由 runtime 能力決定
public protocol ShaderNode: RenderNode {}

/// 只提供身分，供 FlowGraph 依節點的 `runtime` 分段與驗證
public protocol NodeRuntime: AnyObject, Sendable {
    var id: RuntimeID { get }
}

/// 能執行 Frame Graph 段
public protocol FrameRuntime: NodeRuntime {
    /// 把連續同 runtime 的節點編成一段，經 `input` 向上游要幀
    func makeSegment(_ nodes: [any RenderNode], input: any FrameSource) throws -> any FrameSource
}

/// 能執行 Display Graph；同時只有一個
public protocol DisplayRuntime: NodeRuntime {
    /// 依已排序的 hook 計畫建立 pipeline；內建步驟與 fallback 由此 runtime 提供
    func makePipeline(_ plan: DisplayPlan, presenter: any Presenter) throws -> any DisplayPipeline
}

public protocol DisplayPipeline: AnyObject {
    /// Render 執行緒，每 vsync 最多一次；不得阻塞。幀與 plan 都沒變時核心不呼叫，畫面維持上一個 drawable
    func render(_ frame: VideoFrame, at time: HostTime)
    // 待實作（SUB-02）：overlay 由 runtime 擁有的佇列傳入，不在每個 vsync 配置陣列
}
```

- **具體編碼介面屬 runtime 模組**：如 `FlowMetal4` 定義 `MTL4ShaderNode: ShaderNode`（`encode(_:input:output:)`，輸出為 `MTLTexture`）；runtime 以 `as?` 取得自己的節點型別。
- **逐像素節點**：輸出像素只取決於輸入的同一像素、且不改尺寸的節點（YUV→RGB、線性化、tone mapping 等）於 descriptor 標示逐像素，可與相鄰步驟合併成同一個 pass（§6.6）；需讀鄰近像素或改變尺寸者為取樣節點。
- **能力即規則**：runtime 依需要組合 `FrameRuntime`／`DisplayRuntime`（如 Metal 4 runtime 兩者皆實作、`VSRuntime` 只實作前者）。`FlowGraph` 編譯時驗證：Frame Graph 節點的 runtime 須為 `FrameRuntime`；Display Graph 只接受 runtime 為目前 `DisplayRuntime` 的 `ShaderNode`，違反者不得加入。
- **擴充方式**：
  - 節點：實作某 runtime 的節點協定（如 `FlowEffects` 的縮放器實作 `MTL4ShaderNode`）。
  - Shader 語言：前端模組，編譯為 runtime 可執行的形式，節點實作該 runtime 協定（如 `FlowGLSL` → MSL → `GLSLShaderNode: MTL4ShaderNode`）。
  - 執行環境：runtime 模組，實作 `FrameRuntime`／`DisplayRuntime` 與自己的節點協定（如 `FlowVapourSynth`、`FlowCoreML`）。

> 連續同 runtime 的節點不論來源語言都合併進同一段（同一個 command buffer、共用 heap aliasing 與 GPU 計時）；跨 runtime 邊界才有轉換成本。

**職責分工**：

|      | `FlowGraph`（核心）                                        | `NodeRuntime`（模組）                           |
| ---- | ---------------------------------------------------------- | ----------------------------------------------- |
| 拓撲 | 依使用者順序／hook 排序、依 `runtime` 分段、驗證 placement、切分 pass | —                                    |
| 規格 | 呼叫 `configure` 推導 `NodeSpec`、時間窗、輸出時間格       | —                                               |
| 時間 | 驗證 PTS 單調遞增且不偏離輸入；違反時 bypass               | 依時間窗與輸出時間驅動節點                      |
| 資源 | 預讀深度與記憶體上限                                       | 紋理、heap aliasing、buffer pool                |
| 執行 | 排程、generation、逾時 bypass                              | 合併 pass、編碼與提交、pipeline cache、GPU 計時 |
| 轉換 | 決定哪裡需要色彩空間或格式轉換                             | 提供轉換實作                                    |

### 6.3 Frame Graph

一切皆 `FrameSource`：解碼器輸出是根，每個 runtime 段包住上游再成為新的 `FrameSource`。

```swift
public protocol FrameSource: AnyObject, Sendable {
    /// 尺寸、格式、色彩、幾何、名目 fps；總時長可為 nil（直播）
    var spec: StreamSpec { get }
    /// 依 PTS 遞增回傳下一幀；nil = 本 generation 結束
    func next() async throws -> VideoFrame?
    /// seek 或參數變更時由 FlowGraph 呼叫；丟棄內部狀態
    func reset()
}
```

- **串流模型**：以 PTS 而非幀編號定位，總幀數與時長可未知（VFR、直播）。每個 generation 是一段獨立串流，從 seek 落點開始（`.keyframe` 為關鍵幀、`.exact` 為目標幀），以 `nil` 結束。需要隨機存取的 runtime 在段內自行轉換（§7.6）。
- **根**：核心把 `FrameSink` 包成根 `FrameSource`，以有界佇列把 push 轉成 pull：下游 await 至新幀到達；佇列達高水位時 `FlowEngine` 暫停呼叫 `decode`。demux EOF 後呼叫 `drain`，排空即回傳 `nil`。

- **編譯**：依使用者順序，連續同 runtime 的節點合併成一段交給 `FrameRuntime.makeSegment`，各段首尾相接（如連續 `MTL4ShaderNode` 編成單一 command buffer、連續 VS 節點編成單一 VS 圖）。
- **格式**：段間一律傳 IOSurface-backed `VideoFrame`；預設維持原生 YUV（4K `rgba16Float` 一幀約 66 MB，不利預讀），節點可宣告需要線性 RGB，由下游 runtime 轉換。
- **時間**：輸出幀自帶 `pts`／`duration`，改變幀率或幀數（補幀、IVTC）的節點經 `NodeSpec.timing` 宣告（§6.5）；`FlowGraph` 只驗證 PTS 單調遞增且輸出時間不偏離輸入（總時長不變），違反時 bypass 該段，以免音訊失步。`StreamSpec.fps` 為名目值，僅供預讀估算與顯示同步。
- **顯示資訊**：框架不因顯示參數變化重新編譯 Frame Graph；節點若要依顯示參數決定輸出，自行訂閱唯讀 Property 並更新設定，走一般重新編譯流程。補幀等重定時節點例外，輸出幀率見 §6.5。
- **即時性**：前以 `FrameSink`、後以 `FrameQueue` 隔離，以預讀深度（預設約 0.5 s，並受記憶體上限約束）吸收延遲抖動；高輸出幀率下記憶體上限使預讀低於約 0.2 s 時，改觸發 §5.4 降級而非繼續縮短。不屬熱路徑，可使用 Swift Concurrency。
- **Seek／參數變更**：遞增 `generation`、取消進行中的 pull，由根往下呼叫各段 `reset` 後從新位置重新處理，避免已預讀的舊結果延遲生效。
- **失敗**：某段逾時或拋錯時 bypass 該段並通知使用者，播放不中斷。
- **可量測**：每段耗時、預讀水位、跨 runtime 邊界數顯示於統計 OSD。

### 6.4 Display Graph 與 Hook Points

內建步驟（chroma 升取樣、YUV→RGB、線性化）之間開放 **Hook Point**，順序對齊 mpv，以相容 mpv 使用者 shader（§7.5）：

```mermaid
flowchart LR
    A[FrameQueue] --> P[plane] --> CU([chroma 升取樣]) --> PS[plane-scaled] --> MG([合併]) --> N[native]
    N --> RGB([YUV→RGB]) --> M[main] --> LIN([線性化*]) --> L[linear*] --> SIG([sigmoid 化*]) --> SG[sigmoid*]
    SG --> S[scale] --> SD[scaled] --> T[tonemap] --> O[output] --> OV[overlay] --> D[dither]
```

\* 依 `render.scale-light` 啟用。

| Hook            | mpv 對應                              | 色彩空間／格式             | 典型節點                                                    |
| --------------- | ------------------------------------- | --------------------------- | ----------------------------------------------------------- |
| `plane`         | `LUMA`、`CHROMA`、`ALPHA`、`RGB`、`XYZ` | 解碼器原生單一平面          | 亮度預放大（FSRCNNX、RAVU）、chroma 重建（KrigBilateral）   |
| `plane-scaled`  | `CHROMA_SCALED`、`ALPHA_SCALED`       | 單一平面，已縮放到 luma 尺寸 |                                                             |
| `native`        | `NATIVE`                              | 合併平面，4:4:4 原生 YUV    | 去色帶                                                      |
| `main`          | `MAIN`、`MAINPRESUB`                  | 非線性 RGB（來源轉移函數）  | Anime4K                                                     |
| `linear`        | `LINEAR`                              | 顯示線性 RGB，`rgba16Float` |                                                             |
| `sigmoid`       | `SIGMOID`                             | sigmoid 化的顯示線性 RGB    |                                                             |
| `scale`         | `PREKERNEL` → 縮放器 → `POSTKERNEL`   | 依 `render.scale-light`     | 內建縮放器（EWA Lanczos、Bicubic、FSR）                     |
| `scaled`        | `SCALED`                              | 輸出解析度，sigmoid 已還原  | 銳化、Film grain                                            |
| `tonemap`       | —                                     | 顯示線性，依 EDR headroom   | BT.2390、Hable、動態 tone mapping                           |
| `output`        | `OUTPUT`                              | 顯示參照，目標轉移函數編碼  |                                                             |
| `overlay`       | —                                     | 同 `output`                 | 字幕、OSD（`OverlayFrame` 由 runtime 合成）                 |
| `dither`        | —                                     | 目標 drawable 格式          | Dither                                                      |

- **內建步驟**：chroma 升取樣、YUV→RGB、線性化為必填插槽，預設節點由 `DisplayRuntime` 提供；模組可取代（如 `FlowDolbyVision` 於有 RPU 時取代 YUV→RGB 做 reshaping）。線性化在 `render.scale-light` 為 `gamma` 時延到 `tonemap` 前。
- **幾何**：`scale` 依 `VideoFrame.geometry` 與 Property `video.rotate`／`video.aspect` 套用旋轉、鏡像與像素長寬比，並等比縮放置中於輸出（letterbox）；Frame Graph 與 `scale` 之前的 hook 皆處理未旋轉的儲存畫面（同 mpv）。
- **Fallback**：單選 hook 的 fallback 亦由 `DisplayRuntime` 提供（`FlowMetal4`：`scale` 硬體 bilinear、`tonemap` 基本 BT.2390、`dither` ordered），只啟用必要模組時畫面仍正確。
- **Hook 語意**（由 `FlowGraph` 處理）：`scale`、`tonemap`、`dither` 為單選，模組節點取代 fallback；其餘 hook 可疊加，同一 hook 先執行模組節點、再依 `render.user-shaders` 順序執行使用者 shader；`scale` 的 `PREKERNEL`／`POSTKERNEL` 節點包在所選縮放器前後。
- **平面**：`plane`／`plane-scaled` 節點須指定平面，如 `.display(.plane(.luma))`；來源為 RGB 時只有 `RGB` 觸發（同 mpv）。
- **縮放光域**：Property `render.scale-light`（`auto`／`linear`／`sigmoid`／`gamma`）決定 `linear`／`sigmoid` 是否觸發及 `scale`、`scaled` 的光域。`auto` 對齊 mpv 預設：升頻 sigmoid、降頻線性、HDR 降頻 gamma（避免極端亮度差放大 ringing）。
- **尺寸**：任一 hook 的節點都可改變尺寸；chroma 升取樣以 hook 後的 luma 尺寸為目標，已同尺寸則略過（如 KrigBilateral）；`scale` 只處理剩餘倍率。
- **實體化**：hook 只是邏輯位置；只有掛上節點或結果被讀取時才寫出紋理，否則前後步驟合併在同一個 pass（同 mpv，§6.6）。
- **紋理命名空間**：每個 hook 的結果以 mpv 名稱（`LUMA`、`NATIVE`、`MAIN`、`PREKERNEL`…）登記，連同 `//!SAVE` 紋理供後續節點綁定；`FlowGraph` 依最後一個讀取者決定生命週期。
- **座標**：每個紋理帶相對來源的 offset／transform（對應 mpv `NAME_off`／`NAME_rot`），由 `FlowGraph` 沿 hook 傳遞，最後由縮放器補償，避免預放大造成的半像素偏移。
- **線性光約定**：顯示線性（套用來源 EOTF），1.0 = SDR 參考白（203 nits，BT.2408），與 EDR 一致；兩張圖與所有模組共用。
- **`output` 編碼**：一律為目標轉移函數編碼；drawable 為 extended linear 時，由 runtime 在 `dither` 轉換。

> 節點可在 descriptor 宣告需要的色彩空間，`FlowGraph` 決定插入位置，轉換節點由 runtime 提供。

### 6.5 NodeSpec 與時間

```swift
public struct NodeSpec: Sendable {
    public var texture: TextureSpec
    /// 相對來源座標的 offset／transform；對應 mpv `NAME_off`／`NAME_rot`
    public var transform: TextureTransform
    /// `.passthrough` 或 `.retimed(fps:)`；Display Graph 只接受 `.passthrough`
    public var timing: FrameTiming
}
```

- **時間窗**：descriptor 宣告需要前 N／後 M 幀；Display Graph 的 M 必須為 0。generation 起點與 EOF 處不足時以邊界幀補齊並標記；窗內幀由 `FlowGraph` 以滑動視窗保留，計入預讀記憶體上限。
- **重定時**：`.retimed` 節點只宣告輸出幀率，`FlowGraph` 依此產生輸出時間格，runtime 對每個輸出時間驅動一次節點；節點仍是一次一張輸出，不管理 PTS。
- **重定時候選**：節點從 `NodeContext.retimeRates` 選擇輸出幀率（可依自身上限選較小者）。`FlowEngine` 依 `display.max-refresh-rate`、`playback.rate` 與目前降級層級提供 `最高刷新率 ÷ (k × rate)`（k 為正整數）的候選，確保 `display-resample` 成立，也避免與刷新率互相追逐（§5.4）。候選變化視同參數變更（§6.3）。
- **節點輸入**（型別由 runtime 定義，語意由核心規定）：時間窗內的幀（依實際 PTS，VFR 亦正確）、輸出 PTS、輸出時間在前後輸入幀間的相位（0–1）、HDR metadata、使用者參數。

### 6.6 Graph 編譯與資源管理

**`FlowGraph`（核心）**：

1. **編譯**：Frame Graph 依使用者順序分段；Display Graph 依 hook 排序、套用單選語意與 fallback → 建立紋理命名空間，略過綁定不到紋理的節點 → 標記需要的色彩轉換 → 呼叫 `configure` 推導每個節點的輸入／輸出規格 → 切分 pass → 產生 `DisplayPlan`。兩張圖都只在來源格式、輸出尺寸、節點組合變化時才重新編譯；Display Graph 另含 `video.rotate` 與 drawable 像素格式。`DisplayPlan` 與上次相同時不重建 pipeline。
2. **切分 pass**：中間紋理的讀寫是主要成本（4K `rgba16Float` 每多一個 pass 約多讀寫 133 MB），故只在取樣前切開：
   - 逐像素步驟接到目前 pass 的尾端，中間值留在暫存器。
   - 遇到取樣步驟時，結束目前 pass 並寫出紋理，取樣步驟開新 pass。
   - 硬體 bilinear 取樣（如 bilinear chroma 升取樣）視為逐像素，可放在 pass 開頭。
   - `scale` 為 1:1 或硬體 bilinear 時不切開，直接以輸出座標取樣來源平面（同 libplacebo）；先內插再做非線性步驟，屬可接受的近似，只適用於 `render.scale-light` 為 `gamma`（或尚未支援光域的 fallback）；`linear`／`sigmoid` 須在線性化後縮放，`scale` 另開 pass。若 chroma 也是 bilinear 且沒有取樣節點，整條管線只有一個 pass。
   - `overlay`、`dither` 接到最後一個 pass；是否另寫出 `output` 由 runtime 逐幀決定（見下方「重用」）。
   - 使用者 shader 每個 `//!HOOK` 為獨立 pass，不合併（同 mpv）。
3. **監控**：彙整 runtime 回報的耗時供 Governor（§6.7）使用，逾時即 bypass 該節點或段。

**DisplayRuntime（`FlowMetal4`）**：

1. **資源**：暫存紋理由 `MTLHeap` 配置並依生命週期（最後一個讀取者）做 **aliasing**，4K 管線的暫存記憶體可降低 40–60%；Frame Graph 段輸出要進 `FrameQueue`，改由 `CVPixelBufferPool` 配置。
2. **合併 pass**：依 `DisplayPlan` 把同一 pass 的步驟編成單一 kernel；Frame Graph 的 Metal 段同此規則。
   - 只含內建步驟：一個 uber kernel，以 function constant 開關各步驟並特化 pipeline；組合有限，全部預先建立。
   - 含 `MTL4PointwiseNode`：各節點提供 MSL 函式片段與參數，由 runtime 與內建步驟依序串成 kernel 背景編譯，以整串的雜湊快取；就緒前比照下方「Pipeline cache」先 bypass。
3. **只用 compute**：所有 pass 皆為 compute kernel，不用 render pass。驅動依行程用過的 encoder 種類配置 GPU context（M4 Max：compute 160 MB、render 224 MB、兩者並用約 290 MB），單一種類最省且路徑單純。render 專屬功能的替代：
   - mpv fragment 型 hook 編成 kernel；`dFdx` 等微分以 quad shuffle 模擬，隱含 LOD 取樣改為 `level(0)`。
   - 字幕不畫四邊形，改取樣 overlay 紋理（§9）。
   - 不用 MetalFX（內部為 render），縮放器以 kernel 實作。
4. **提交**：一幀一個 command buffer（重複使用 command allocator + argument table），最多 3 幀 in-flight，以 `MTLSharedEvent` 控制。
5. **Pipeline cache**：所有 `MTLComputePipelineState` 由 `MTL4Compiler` 在背景預先建立並快取於記憶體（模組啟用時 prewarm 內建組合），避免首次啟用節點時卡頓；跨啟動的快取以 `MTL4Archive` 持久化（待實作）；首幀不等待，pipeline 未就緒的選配節點先 bypass，就緒後再啟用。
6. **計時**：每個 command buffer 以 `MTL4CommitFeedback` 的 GPU 起訖時間量測；多 pass 時改以 `MTL4CounterHeap` 逐 pass 量測（待實作）。結果回報給 `FlowGraph` 與統計 OSD；合併的步驟共用一個量測值。
7. **收尾**：`overlay`、`dither` 併入最後一個 pass 的 kernel：取樣 overlay 紋理 blend、dither 後寫入 drawable；中間值留在暫存器，且維持 `overlay` → `dither` 順序。
8. **重用**：同一幀可能重繪（跨多個 vsync 或暫停）、有 overlay，且管線不只一個 pass 時，才寫出 `output`，後續重繪只重做 `overlay` 之後；其餘情況重繪時從來源重算（單一 pass 時重算比讀 `output` 省頻寬）。overlay 未變時不提交，`CAMetalLayer` 維持顯示上一個 drawable。

### 6.7 Governor

依處理預算自動切換節點品質層級，讓昂貴節點（神經網路超解析、高階縮放器）負載過高時先降品質而非直接 bypass。由 `FlowEngine` 決策，`FlowGraph` 經 runtime 套用。

- **品質層級**：節點於 `NodeDescriptor` 宣告層級數（如 AI 模型大小、縮放器 kernel），未宣告者只有一級。各層級輸出的 `NodeSpec` 須相同，故切換不重新編譯、不遞增 generation、保留已預讀的幀。
- **預算**：
  - GPU：兩張圖所有 pass 的 GPU 時間佔經過時間的比例 ≤ Property `render.gpu-budget`（預設 0.8），依 runtime 的 GPU 計時（§6.6）。
  - Frame Graph：預讀水位持續下降視同超出預算，涵蓋 CPU、ANE 等非 GPU runtime。
  - `ProcessInfo.thermalState` 達 `.serious` 或啟用低耗電模式時調降預算。
- **決策**：超出預算時，將近期耗時最高且仍可降的節點降一級；持續低於預算一段時間才逐級回升，回升後立即超出則暫時鎖定該層級，避免來回切換。
- **與降級的關係**：為 §5.4 落後降級的第一步；所有節點降至最低層級仍不足才進入後續步驟。
- **控制**：Property `render.governor`（`auto`／`off`）；各節點目前層級顯示於統計 OSD。

---

## 7. 模組

### 7.1 模組模型

所有實作（解碼器、runtime、節點、輸出、功能）皆為第一方模組：原始碼位於 `Modules/`，靜態連結進 App，由 composition root 組裝。不支援外部模組（無 bundle、manifest、下載或安裝）。

```swift
// Packages/Composition/Sources/FlowComposition/Modules.swift：唯一知道用了哪些實作的地方
import FlowAVAudio
import FlowFFmpeg
import FlowFile
import FlowMetal4
import FlowVideoToolbox

public enum FlowModules {
    public static let essential: [ModuleEntry] = [
        .essential(FileModule.self),
        .essential(FFmpegModule.self),       // 替換：改這行與對應 import（另加一行 package 依賴）
        .essential(VideoToolboxModule.self),
        .essential(AVAudioModule.self),
        .essential(Metal4Module.self),
        // .optional(EffectsModule.self),    // 選用模組
    ]
}
```

- **FlowPlayer**：`FlowComposition` 以 `FlowPlayer` 組裝 `ComponentRegistry`、`PlayerController`、`ModuleHost`、`PlayerSession`，四者皆不對外公開。App 只用 `api`（`PlayerAPI`）、`makeVideoView()`、`attach(_:)`；影片 view 由 `PresenterFactory.makeSurface()` 提供，App 與 composition root 都不需知道 presenter 型別。Benchmarks 經同一個 `FlowPlayer` 組裝，量到的就是發行的。

- **替換**：同一介面的實作可互換，改模組名稱與 import 即可；依賴某 runtime 的模組（如 `FlowGLSL` 依賴 `FlowMetal4`）需隨該 runtime 一起替換。
- **必要性**：`.essential` 不可停用；`.optional` 可於執行期停用與重新啟用。
- **in-process**：模組本身不使用 ExtensionKit／XPC；完整 SwiftUI、直接呼叫框架 API、熱路徑零開銷。
- **Library validation**：主程式不載入外部程式碼，Hardened Runtime 下無需 `disable-library-validation`；需執行外部程式碼的模組（如 VapourSynth）一律交由 helper 行程。

> 不提供 Script（JavaScript）：帶來額外 runtime 與維護負擔。

### 7.2 Module API

```swift
public protocol FlowModule: AnyObject, SendableMetatype {
    /// 穩定識別碼；預設為完整型別名稱
    static var id: String { get }
    init()
    /// 只在這裡註冊；停用時由 FlowModuleHost 撤銷所有註冊
    @MainActor func activate(_ context: any ModuleContext) throws
    @MainActor func deactivate()
}

@MainActor
public protocol ModuleContext {
    var media: any MediaRegistry { get }        // MediaSourceFactory、DemuxerFactory、外部字幕來源
    var decoders: any DecoderRegistry { get }   // VideoDecoderFactory / AudioDecoderFactory / SubtitleDecoderFactory
    var outputs: any OutputRegistry { get }     // AudioOutputFactory、PresenterFactory
    var runtimes: any RuntimeRegistry { get }   // FrameRuntime / DisplayRuntime
    var player: any PlayerAPI { get }           // Command / Property / Event（§8）
    // 待實作：
    var subtitles: any SubtitleRegistry { get } // SubtitleRendererFactory
    var nodes: any NodeRegistry { get }         // 可供使用者加入的節點類型
    var ui: any UIRegistry { get }              // 分頁、設定面板、選單、OSD 元件
    var storage: any ModuleStorage { get }      // 模組專屬設定與檔案
}
```

- **能力可任意組合**：一個模組可同時註冊解碼器、runtime、節點、UI 分頁並呼叫 API。
- **UI**：`ui` 接受原生 SwiftUI `View`，與內建畫面無差別。
- **版本**：與 App 一起建置，`FlowInterfaces` 不需維持 ABI 穩定，可隨需求重構。
- **停用**：撤銷所有註冊即生效；播放中的媒體沿用已建立的元件，下次載入時 `FlowEngine` 依 §5.3 改選其他實作。重新啟用後恢復原本的偏好順序。
- **故障**：GPU 或 Frame Graph 段逾時時，立即 bypass 該節點或段並通知使用者，播放不中斷。

### 7.3 範例：解碼器模組（`Modules/FFmpeg`）

- 註冊 `FFmpegDemuxerFactory`、`FFmpegVideoDecoderFactory`（`.software`）、`FFmpegAudioDecoderFactory`、`FFmpegSubtitleDecoderFactory`、`FFmpegBitmapSubtitleRendererFactory`。
- 目前已註冊 `FFmpegDemuxerFactory`、`FFmpegAudioDecoderFactory`。
- 只匯入 libav*／libswresample XCFramework 與 `FlowInterfaces`、`FlowBase`；C 型別不越過模組邊界。
- 新增其他解碼器（如另一個 AV1 實作）＝新增模組實作 `VideoDecoderFactory`，選擇與 fallback 由 `FlowEngine` 處理。

### 7.4 範例：Metal 4 Runtime（`Modules/Metal4`）

- **Runtime**：`Metal4DisplayRuntime` 實作 `DisplayRuntime`（§6.6 資源、提交、pipeline cache、計時）；Frame Graph 上線時一併實作 `FrameRuntime`。
- **節點協定**：`MTL4ShaderNode: ShaderNode`，供其他模組實作；`MSLShaderNode` 為以 MSL 原始碼建立的通用實作。逐像素節點改實作 `MTL4PointwiseNode: ShaderNode`（提供 MSL 函式片段與參數，不自行 dispatch），以便合併 pass（§6.6）。
- **內建節點**：色彩轉換、chroma 升取樣、線性化、fallback 縮放器／tone mapping／dither、overlay 合成。
- **Presenter**：`MetalLayerPresenter`（`MetalVideoView` 由 `makeSurface()` 提供）、`SampleBufferPresenter`（待實作，§5.6）；`OffscreenPresenter` 以 `@_spi(Benchmark)` 開放，僅供 Benchmarks 自行註冊（BENCHMARK §7）。
- **Shader ABI**：`Shaders/Include/Flow/`（`FlowShader.h`：紋理／buffer 索引與 `DisplayUniforms`，使用者 shader 用的 `FlowFrameInfo` 待加入；`Color.h`：轉換函式），供內建 shader 與依賴 `FlowMetal4` 的模組共用，建置期編譯為 metallib。使用者 `.metal` 也依賴此 ABI，故需維持相容：破壞性變更遞增 major，shader 以 `//!FLOW <major>` 宣告，runtime 支援目前與前一個 major。

### 7.5 範例：GLSL 前端與使用者 Shader（`Modules/GLSL`、`Modules/UserShaders`）

`FlowGLSL` 只做語言前端：GLSL → glslang → SPIR-V → SPIRV-Cross → MSL，產生 `GLSLShaderNode: MTL4ShaderNode`，與其他 Metal 節點同段執行。新增語言（如 Slang）只需新增對應的前端模組。

`FlowUserShaders` 讓使用者匯入兩種 shader，共用同一套 `//!` 指示詞與 hook 語意。

- **格式**：
  - `.glsl`：mpv／libplacebo GLSL，產生 `GLSLShaderNode`；常見 shader（FSRCNNX、RAVU、Anime4K、ArtCNN、KrigBilateral、SSimSuperRes）免修改即可使用。
  - `.metal`：MSL，依 Flow Shader ABI（§7.4），產生 `MSLShaderNode`；免轉譯，可用 Metal 專屬功能（SIMD-group、half、threadgroup 調校）。
- **匯入**：開檔或拖放 `.glsl`／`.metal`，複製到 Application Support；不提供網路下載。啟用清單與順序為 Property `render.user-shaders`（對應 mpv `glsl-shaders`，兩種格式可混排），可寫進 Profile。
- **指示詞**：`//!HOOK`、`//!BIND`、`//!SAVE`、`//!DESC`、`//!WIDTH`／`//!HEIGHT`、`//!OFFSET`、`//!COMPONENTS`、`//!WHEN`、`//!COMPUTE`、`//!TEXTURE`，以及 libplacebo 擴充 `//!PARAM`（顯示為節點參數）、`//!BUFFER`。`.metal` 另需 `//!FLOW <major>`。
- **編譯**：
  - `.glsl`：前置 mpv 內建定義（`HOOKED_tex`、`NAME_pos/size/pt/mul/rot/off`、`frame`、`random`、`input_size`、`target_size`、`tex_offset` 等）後交給 `FlowGLSL`。
  - `.metal`：每個 pass 實作 `hook()`（同 mpv）；前置 `Include/Flow/` 內容（執行期編譯無法 `#include`）並產生 entry point，依宣告順序綁定 `HOOKED`／`//!BIND` 紋理、`FlowFrameInfo` 與參數（按 MSL 對齊打包）。
  - 非 `//!COMPUTE` 的 pass 包成 kernel，一個 thread 寫一個輸出像素（§6.6「只用 compute」）。
  - 兩者皆以 `MTL4Compiler` 背景編譯，以原始碼雜湊快取 MSL 與 `MTL4Archive`；失敗只略過該 shader 並提示。
- **節點化**：每個 pass 在每個 `//!HOOK` 目標上各成為一個 `.display(hook)` 節點（hook 對應見 §6.4）；`//!BIND` 依 §6.4 紋理命名空間解析，綁定不到時略過該 pass（同 mpv）；`//!SAVE` 紋理由 runtime 管理並參與 aliasing，與其他節點共用排程、GPU 計時與逾時 bypass。
- **求值**：`//!WHEN`、`//!WIDTH`／`//!HEIGHT` 的 RPN 運算式在 `FlowGraph` 編譯時求值，所依賴的尺寸或參數變化時才重新編譯；`frame`、`random` 每幀更新。
- **與 mpv 的差異**：只掛在 Display Graph；`MAINPRESUB` 等同 `MAIN`（字幕於 `overlay` 合成）；`OUTPUT` 在 `overlay` 之前，不作用於字幕與 OSD（同 vo_gpu；libplacebo 在之後）。
- **未觸發提示**：hook 未觸發的 shader（如來源為 RGB 時的 `LUMA`、`render.scale-light=gamma` 時的 `LINEAR`）標示於統計 OSD。
- **參考實作**：除上列差異外，語意以 libplacebo 的 mpv shader 解析為準（只比對行為，不移植程式碼）。

### 7.6 範例：VapourSynth Runtime（`Modules/VapourSynth`）

- **Runtime**：`VSRuntime` 只實作 `FrameRuntime`；連續 `VapourSynthFilterNode` 經 `makeSegment` 組成單一 VS 圖，以自訂來源 filter 把上游 `FrameSource` 注入為 `video_in`。
- **幀編號**：VS 是隨機存取模型，由 `VSRuntime` 在段內轉換：`n` 為本 generation 起算的輸入序號，總幀數回報極大值；來源 filter 收到 `getFrame(n)` 時向上游 `next()` 直到第 n 幀，並快取時間窗內的幀，超出快取的倒退請求視為錯誤並 bypass。輸出 PTS 在 `.passthrough` 沿用第 n 個輸入幀、`.retimed` 依 §6.5 時間格；`reset` 時重建 VS 圖。
- **節點**：`VapourSynthFilterNode: FilterNode`，含通用「VapourSynth Script」（使用者 `.vpy`）與選配預設節點（QTGMC 等，宣告參數）。
- **UI**：新增分頁（腳本編輯、VS／Python 路徑、VS plugin 管理）。
- **隔離**：VS、Python、VS plugin 與 `.vpy` 一律在 helper 行程執行（IOSurface 經 XPC 傳遞），框架不介入；`disable-library-validation` 只授予 helper，主程式維持 library validation，Python 崩潰也不影響播放。
- **成本**：VS 只接受 planar 格式且無外部記憶體 import，IOSurface 與 `VSFrame` 間需轉換與複製；每個跨 runtime 邊界的成本顯示於統計 OSD，連續 VS 節點應排在一起。

---

## 8. Command／Property 系統

整個播放器的「公開介面」，UI 與模組使用同一套，確保模組能做到內建 UI 能做的任何事。

- **Property**：階層式、型別化、可觀察。例如 `playback.position`、`playback.rate`、`video.params.colorspace`、`render.nodes`、`render.governor`、`render.gpu-budget`、`video.rotate`、`video.aspect`、`audio.volume`、`audio.mute`、`audio.delay`、`sub.track`、`video.sync`、`display.refresh-rate`／`display.max-refresh-rate`（唯讀）。
- **Command**：具名、參數型別化、可非同步。例如 `load`、`seek`、`node.enable`、`screenshot`、`sub.add`。
- **Event**：`file-loaded`、`seek`、`end-file`、`track-switched`、`render-stats` 等。
- 對接：
  - SwiftUI → 以 `PlayerViewModel` 投影 Property。
  - 按鍵綁定 → `input.conf` 風格的對應表；可手改檔案或於設定視窗編輯，兩者讀寫同一份檔案。
  - App Intents／捷徑／AppleScript → 包裝常用 Command。
  - AI agent → MCP tools（`FlowMCP`）包裝 Command 與 Property。
  - 模組 → 經 `ModuleContext.player` 直接使用。

### 設定與 Profiles

設定分層合併：`內建預設 < 使用者設定 < 條件式 Profile < 單檔覆寫 < 執行期`。
條件式 Profile 範例：`video.hdr == true && display.edrHeadroom < 2` 時自動套用較激進的 tone mapping。

---

## 9. 字幕

核心定義 `SubtitleDecoder`（packet → `SubtitleEvent`）與 `SubtitleRenderer`（`SubtitleEvent` → `OverlayFrame`：IOSurface + 位置），並負責字幕軌選擇、延遲與時間對齊。

```swift
public enum SubtitleEvent: Sendable {
    case ass(ASSEvent)                // 文字字幕一律轉為 ASS
    case bitmap(BitmapSubtitle)
}

public struct ASSEvent: Sendable {
    public let pts: MediaTime
    public let duration: MediaTime?   // nil = 直到下一個事件
    /// MKV 格式事件行（ReadOrder,Layer,Style,Name,MarginL,MarginR,MarginV,Effect,Text）：
    /// 即 libavcodec 的輸出與 `ass_process_chunk` 的輸入，兩端免轉換
    public let line: String
}

public struct BitmapSubtitle: Sendable {
    public let pts: MediaTime
    public let duration: MediaTime?   // nil = 直到下一個事件（PGS 一律如此）
    public let canvasSize: PixelSize  // 座標基準（PGS／VobSub 畫面尺寸）
    public let rects: [BitmapRect]    // 位置、尺寸、stride、premultiplied BGRA；空 = 清除
}

public protocol SubtitleDecoder: AnyObject {
    /// 文字字幕軌的 ASS header（styles 等）；點陣字幕為 nil
    var assHeader: String? { get }
    func decode(_ packet: Packet) throws -> [SubtitleEvent]
    func flush()
}

public protocol SubtitleRendererFactory: Sendable {
    var id: String { get }
    func support(for kind: SubtitleKind) -> Bool     // .ass / .bitmap
    func make(assHeader: String?, attachments: [Attachment]) throws -> any SubtitleRenderer
}

public protocol SubtitleRenderer: AnyObject {
    func add(_ event: SubtitleEvent)
    func flush()
    /// Subtitle 執行緒；內容未變時回傳前一個結果
    func render(at time: MediaTime, target: OverlayTarget) -> OverlayFrame?
}
```

| 職責     | 實作                        | 範圍                                                                                     |
| -------- | --------------------------- | ---------------------------------------------------------------------------------------- |
| 解碼     | `FlowFFmpeg`（libavcodec）  | 文字：SRT、WebVTT、ASS/SSA、mov_text 等 → `ass`；點陣：PGS、VobSub、DVB、XSUB → `bitmap` |
| 渲染 ASS | `FlowLibass`                | libass 輸出 glyph bitmap → 合成為單一 RGBA IOSurface；完整支援特效與內嵌字型             |
| 渲染點陣 | `FlowFFmpeg`                | RGBA → IOSurface                                                                         |

- `FlowEngine` 依事件種類選用 `SubtitleRendererFactory`；`FlowLibass` 只依賴 `FlowInterfaces`，不依賴 FFmpeg。
- **執行緒**：解碼與渲染皆在 Subtitle 執行緒（§5.1）；libass 遇複雜特效單次光柵化可達數十 ms，故 Render 執行緒只依顯示 PTS 取用已備妥的 `OverlayFrame`。
- **`OverlayTarget`**：輸出像素尺寸、影片於輸出中的位置、Retina 倍率；供 libass 設定畫面尺寸與邊界。尺寸變更時重新光柵化，完成前沿用舊 overlay 縮放顯示。
- **重新光柵化**：只在內容改變時進行（libass 以 `ass_render_frame` 的變更旗標判斷）。`DisplayRuntime` 在 `overlay` hook 取樣 overlay 紋理合成（§6.6「收尾」）。
- **內嵌字型**：`Demuxer.attachments` 經 `make` 交給 libass（`ass_add_font`）。
- **外部字幕檔**：同樣經 `Demuxer`（libavformat）讀取，開檔時一次讀完並解碼。
- HDR 內容下字幕亮度以可設定的 nits 值（預設 203 nits，依 BT.2408）合成，避免刺眼。
- 支援雙字幕（兩個 `SubtitleRenderer` 實例）、字幕延遲調整、外部字幕自動比對、模組擴充字幕來源。

---

## 10. 平台整合

- **視窗**：多視窗、原生全螢幕、浮動視窗、Spaces。
- **Now Playing／媒體鍵**：`MPNowPlayingInfoCenter`、`MPRemoteCommandCenter`。
- **PiP**：`SampleBufferPresenter`（`FlowMetal4`）。
- **檔案存取**：一般 bookmark（記住最近檔案；不啟用 App Sandbox）。
- **網路來源**：SMB／WebDAV／HTTP 為 `MediaSource`（`FlowNetwork`）；HLS 由 demuxer 處理（§5.2）。SMB 以 `libsmb2` 在行程內實作，讀取可逾時與取消；不掛載（`NetFS` 的核心讀取無法中斷），不用 `libsmbclient`（GPLv3）。
- **自動化**：App Intents、AppleScript、URL scheme、MCP。
- **MCP**：`FlowMCP` 於 `http://127.0.0.1:17996/mcp` 提供 Streamable HTTP（不開 SSE 串流、不用 session），檢查 Host 與 Origin 以防 DNS rebinding；不另做 stdio helper，App 須先開啟。使用方式見 [`MCP.md`](/docs/mcp)。
- **外接顯示**：多顯示器、依顯示器 EDR 能力切換管線。
- **關於**：顯示 `LICENSE` 與 `THIRD_PARTY_NOTICES.md`（建置時複製進 App bundle）。

---

## 11. 可觀測性與測試

### 11.1 可觀測性

- `os.Logger`：依模組分 subsystem／category。
- `OSSignposter`：demux、decode、每個 pass、present 都有 interval，可直接在 Instruments 分析。
- **統計 OSD**（類似 mpv 的 `i` 鍵）：選用的實作、色彩參數、HDR metadata、同步模式、音訊重取樣偏移、降級層級、Governor 品質層級、丟幀／延遲幀、A/V 漂移、各節點 GPU 時間、跨 runtime 邊界成本。

### 11.2 測試策略

- **單元測試**：時鐘（以虛擬時鐘模擬漂移、換裝置、underrun）、佇列、狀態機、模組註冊與撤銷、Property 系統、Governor（以模擬耗時驗證遲滯）。
- **核心整合測試**：`FlowEngine`／`FlowGraph` 以 mock 模組跑完整生命週期（選擇、fallback、seek、bypass），不需 GPU 與 vendored 函式庫。Mock 統一放在 `FlowTestSupport`，不在各測試 target 複製。
- **模組測試**：只依賴自身與 `FlowTestSupport`，不依賴其他模組與控制平面（註冊以 `TestModuleContext` 驗證）；需要真實封包時以系統框架讀 fixture（如 VideoToolbox 測試用 AVFoundation passthrough）。跨模組路徑由 `IntegrationTests` 負責。
- **邊界檢查**：SwiftLint custom rule（`core_banned_import`、`core_banned_link`）確認 `Packages/Core` 未匯入 GPU／解碼 API 與 vendored C 函式庫（§4）。
- **Golden image**：每個內建節點與色彩轉換以參考畫面比對（容許 ΔE／PSNR 閾值）；常見 mpv shader 與 mpv 輸出比對。
- **一致性測試語料**：涵蓋各容器、編碼、HDR 格式、字幕格式的測試影片集（含邊界案例：VFR、損壞檔、超長 GOP）。
- **效能測試**：見 [`BENCHMARK.md`](/docs/benchmark)。
- **幀時間測試**：以 display link 時間戳驗證 pacing：24p@120Hz 每幀恰好 5 個 vsync；23.976p 補幀到 120@120Hz 每幀 1 個 vsync、音訊重取樣約 0.1%；25p@60Hz 降為 `audio`；負載不足時依序降級且不在模式間來回切換。
- **Fuzzing**：Demux 與字幕解析以 libFuzzer 針對畸形輸入。

---

## 12. 關鍵架構決策（ADR 摘要）

| #   | 決策                                                | 替代方案                        | 理由                                                                                            |
| --- | --------------------------------------------------- | ------------------------------- | ----------------------------------------------------------------------------------------------- |
| 1   | 自建播放管線（FFmpeg demux + VT decode + Metal）    | 純 AVFoundation                 | AVFoundation 不支援 MKV／多數字幕／逐幀 GPU 處理管線；自建才能做到完整影像處理與擴充              |
| 2   | YUV→RGB 由 Graph 處理，不交給解碼器                 | VT 輸出 BGRA                    | 控制色彩精度、chroma 升取樣品質，並讓 Frame Graph 可存取原生 YUV                                |
| 3   | 預設 `display-resample`，條件不符退回音訊時鐘       | 只用音訊時鐘／只用視訊時鐘      | 刷新率為幀率整數倍時以微量重取樣換得無 judder 的 pacing（補幀尤其需要）；否則音訊連續優先，因人耳對不連續遠比對丟／重複幀敏感 |
| 4   | Frame Graph（自由排序）+ Display Graph（固定 hook） | 單一固定 hook 圖／單一自由 DAG | 與顯示無關的處理可任意排序並混用 runtime；依賴顯示參數的步驟固定、語意可預期                    |
| 5   | 模組僅第一方、靜態連結                              | 外部 bundle／ExtensionKit + XPC／Script | 完整 SwiftUI、直接呼叫 API、熱路徑零開銷；免除外部程式碼的簽章、信任與崩潰歸因問題 |
| 6   | 資料平面不使用 actor                                | 全面 Swift Concurrency          | 熱路徑需要可預測延遲，避免 executor 排程與 suspension 的抖動                                  |
| 7   | LGPL 元件（FFmpeg、FriBidi、libsmb2）動態連結       | 靜態連結／GPL 組態             | 符合 LGPL 重新連結要求，與 Apache-2.0 本體相容                                                  |
| 8   | 核心只含介面與控制平面，所有實作皆為模組            | 核心內建預設實作                | 實作可替換、可擴充；核心能以 mock 測試；官方實作與擴充走同一套介面                              |
| 9   | 使用者 shader 採 mpv 指示詞格式，本體可為 GLSL 或 MSL | 自訂格式／僅 GLSL／僅 MSL     | GLSL 沿用 mpv 生態現成 shader，MSL 免轉譯且可用 Metal 專屬功能；兩者共用 hook 語意與解析器      |
| 10  | 節點／runtime／語言前端分離                         | 每種語言自帶執行環境            | 不同語言的節點同段執行，共用 command buffer、aliasing 與計時；新語言只需前端                    |
| 11  | 佇列、執行緒、時鐘歸核心                            | 由各實作自行管理                | 生命週期與即時規則可由核心保證，實作不必重複造輪子                                              |
| 12  | 核心不抽象 GPU API                                  | 核心定義跨 API 的紋理／編碼器   | 抽象必然洩漏且破壞 zero-copy；段間只傳 `VideoFrame`，段內用 runtime 原生型別                    |
| 13  | 本體採 Apache-2.0                                   | GPL／MPL-2.0／閉源              | 與所有依賴相容、含貢獻者專利授權                                                                |
| 14  | 僅以 GitHub Release 與 Homebrew Cask 發行          | Mac App Store                   | 免 App Sandbox 與審查限制（使用者 shader、VapourSynth、任意檔案存取）；以 Developer ID + notarization 保障 Gatekeeper |
| 15  | Frame Graph 依 PTS 串流 pull                        | 依幀編號隨機存取（VapourSynth 模型） | 解碼器為 push、VFR／直播總幀數未知、seek 後幀編號無定義；隨機存取只在需要的 runtime 內轉換 |
| 16  | 逐像素步驟合併成單一 pass，只在取樣前切開           | 每個步驟或 hook 一個 pass       | 中間紋理讀寫是頻寬瓶頸，運算量不變；與 mpv／libplacebo 相同                                     |
| 17  | GPU 工作只用 compute                               | render pass／兩者並用           | 驅動每行程 context 只付一種且最小（§6.6）；單一路徑；render 專屬功能皆有 compute 替代            |

---

## 13. 主要風險

| 風險                                  | 影響                    | 緩解                                                                                                      |
| ------------------------------------- | ----------------------- | --------------------------------------------------------------------------------------------------------- |
| Dolby Vision 正確性                   | 色彩錯誤（P5 綠紫色偏） | 早期建立 DV 測試語料；依規格自行實作，以 libplacebo 輸出比對；以 golden image 驗證                        |
| 節點或使用者 shader 拖垮效能或 GPU hang | 卡頓／丟幀           | GPU 時間監控、command buffer 逾時偵測後 bypass 該節點；每個模組與 shader 的 signpost 與統計               |
| 介面邊界劃分不當                      | 實作被迫繞過介面、或介面過度抽象 | 先以兩個以上實作驗證介面（如 VT + FFmpeg、Metal + VS）；介面無 ABI 承諾，可隨時重構              |
| mpv shader 相容性                     | 輸出與 mpv 不同／無法編譯 | 以 libplacebo 語意為準；常見 shader 以 mpv 輸出做 golden image 回歸                                    |
| 藍牙／AirPlay 輸出延遲回報不準        | A/V 失步               | 延遲隨裝置變更重新查詢；提供 `audio.delay` 手動補償；評估以 `AVSampleBufferAudioRenderer` + `AVSampleBufferRenderSynchronizer` 實作另一個 `AudioOutput` |
| FFmpeg 安全性（畸形檔案）             | 崩潰／漏洞             | Fuzzing、跟進上游安全修正、精簡啟用的 demuxer/decoder                                                     |
| 授權違規（依賴、模型、內建 shader）   | 無法散布                | 新增前依 `THIRD_PARTY_NOTICES.md` 審查；禁止 GPL、AGPL、非商用；內建 shader 與移植程式碼限寬鬆授權           |
