diff --git a/docs/zh/modularization-and-multiprocess-render-plan.md b/docs/zh/modularization-and-multiprocess-render-plan.md new file mode 100644 index 000000000..cd3bea941 --- /dev/null +++ b/docs/zh/modularization-and-multiprocess-render-plan.md @@ -0,0 +1,872 @@ +# Olive/Oak 模块化与多进程渲染架构方案 + +> **状态**:设计文档(Design Doc) +> **范围**:仅制定方案,不涉及代码变更。 +> **目标**:将当前单体架构拆分为多个动态库,并将渲染引擎改造为独立进程,通过 stdio 进行 IPC 通信。 + +--- + +## 1. 概述 + +当前 Olive/Oak 采用**单体编译模型**:所有业务代码被聚合到 `libolive-editor`(OBJECT 库),最终链接为单个 `olive-editor` 可执行文件。这种架构在项目规模较小时工作良好,但随着 OFX 插件、节点图复杂度、多轨道高清/超高清处理的加入,单体架构面临以下问题: + +- **编译-链接耗时**:任何小改动都触发大规模重编译和重链接。 +- **渲染崩溃导致编辑器全崩**:OpenGL/OFX/FFmpeg 的崩溃会直接拖垮整个 GUI 进程,用户未保存的工作全部丢失。 +- **插件隔离性差**:OFX 插件与主程序共享地址空间,恶意或 buggy 插件可任意破坏内存。 +- **可扩展性受限**:未来如要支持分布式渲染、云渲染、独立批处理工具,均需先打破单体边界。 + +本方案提出**两阶段架构演进**: + +1. **动态库拆分**:按功能层次将代码拆分为若干共享库(`.so`/`.dylib`/`.dll`),明确模块边界与符号可见性。 +2. **渲染器多进程化**:将 `render/` 相关逻辑从主进程剥离为独立可执行文件 `olive-renderer`,主进程通过 **stdio(stdin/stdout)** 进行 IPC 控制,**共享内存/内存映射文件**传输大帧数据。 + +--- + +## 2. 总体架构目标 + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ olive-editor(主进程,GUI) │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌─────────────────┐ │ +│ │ liboliveui │ │ libolivenode │ │libolivecodec │ │ liboliverender │ │ +│ │ (widget/ │ │ (node/ │ │ (codec/ │ │ -client (轻量) │ │ +│ │ panel/ │ │ timeline/ │ │ common/) │ │ IPC 封装层 │ │ +│ │ window/) │ │ undo/) │ │ │ │ │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ └────────┬────────┘ │ +│ ▲ ▲ ▲ │ │ +│ └─────────────────┴─────────────────┘ │ QProcess │ +│ 动态链接 │ stdin/stdout│ +├──────────────────────────────────────────────────────────────────┼───────────┤ +│ │ │ +│ ┌───────────────────────────────────────────────────────────────┘ │ +│ │ olive-renderer(子进程,无 GUI) │ +│ │ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ │ RenderService (IPC 服务端,监听 stdin,输出 stdout) │ │ +│ │ └──────────────────────────┬──────────────────────────────────────────┘ │ +│ │ │ │ +│ │ ┌──────────────────────────▼──────────────────────────────────────────┐ │ +│ │ │ RenderProcessor + OpenGLRenderer + DecoderCache + ShaderCache │ │ +│ │ │ (原有渲染逻辑,在独立地址空间运行,崩溃不影响主进程) │ │ +│ │ └─────────────────────────────────────────────────────────────────────┘ │ +│ └────────────────────────────────────────────────────────────────────────────┘ +│ +│ 基础依赖(所有模块共享): +│ ┌─────────────┐ ┌─────────────┐ ┌──────────────────┐ +│ │ libolivecore│ │liboliveaudio│ │ liboliveplugin │ +│ │ (ext/core) │ │ (audio/) │ │ (pluginSupport/) │ +│ └─────────────┘ └─────────────┘ └──────────────────┘ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 3. 动态库拆分方案 + +### 3.1 拆分原则 + +- **低侵入性**:优先拆分依赖关系清晰、接口明确的模块;对耦合严重的 `node/` ↔ `render/` 暂不强行物理分割,而是通过**接口抽象 + 动态链接**降低耦合。 +- **分层依赖**:严格遵循 `上层 → 下层` 的依赖方向,禁止循环依赖。 +- **符号可控**:引入 `OLIVE__API` 宏,显式导出公共接口,隐藏内部符号(`-fvisibility=hidden`)。 +- **Qt 元对象系统兼容**:跨动态库的 Qt 信号/槽需确保 `moc` 生成的元对象信息可被正确链接,推荐在公共头文件中完整声明信号/槽。 + +### 3.2 库划分 + +| 动态库 | 包含源码 | 外部依赖 | 说明 | +|---|---|---|---| +| `libolivecore.so` | `ext/core/` | FFmpeg::avutil, OpenGL::GL, Imath | 已有独立库,仅将构建类型由 `STATIC` 改为 `SHARED`。 | +| `libolivecodec.so` | `app/codec/`, `app/common/` | olivecore, FFmpeg, OpenImageIO, OpenEXR | 编解码 + 通用工具。`common/` 因被 `codec/` 重度依赖且不含 UI,故合并。 | +| `liboliveplugin.so` | `app/pluginSupport/`, `third_party/openfx/HostSupport` | olivecore, Qt::Core, expat | OFX 宿主支持,相对独立。 | +| `liboliveaudio.so` | `app/audio/` | olivecore, PortAudio, Qt::Core | 音频播放管理。 | +| `libolivenode.so` | `app/node/`, `app/timeline/`, `app/undo/`, `app/config/` | olivecore, olivecodec, Qt::Core | **核心数据层**。节点图、时间线模型、Undo、配置。注意:当前 `Node.h` 包含部分 `render/` 头文件(缓存类型、作业枚举),需先进行**头文件解耦**(见 3.4)。 | +| `liboliverender.so` | `app/render/`(不含 OpenGL 具体后端) | olivenode, olivecodec, olivecore, Qt::Core, OpenColorIO | 渲染抽象层:`Renderer`, `RenderProcessor`, `RenderTicket`, `Job` 体系。 | +| `liboliveui.so` | `app/widget/`, `app/panel/`, `app/window/`, `app/dialog/`, `app/tool/`, `app/ui/` | olivenode, oliverender, olivecodec, olivecore, oliveaudio, Qt::Widgets, KDDockWidgets | **UI 层**。所有 Qt Widget 相关代码。 | +| `libolivetask.so` | `app/task/` | olivenode, olivecodec, oliverender, olivecore, Qt::Core | 任务调度系统。可独立成库,也可在初期并入 `liboliveui.so`。 | +| `olive-editor` | `main.cpp`, `core.cpp/h` | 上述全部 | 主可执行文件,仅保留入口和全局生命周期管理。 | + +### 3.3 依赖关系图 + +``` + ┌─────────────┐ + │ olive-editor│ + └──────┬──────┘ + │ links all + ┌──────────────────────┼──────────────────────┐ + │ │ │ + ▼ ▼ ▼ +┌─────────┐ ┌─────────────┐ ┌─────────────┐ +│liboliveui│ │libolivetask │ │liboliverender│ +└────┬────┘ └──────┬──────┘ └──────┬──────┘ + │ │ │ + └────────────────────┼──────────────────────┘ + │ + ▼ + ┌─────────────┐ + │ libolivenode│ + └──────┬──────┘ + │ + ┌───────────────┼───────────────┐ + │ │ │ + ▼ ▼ ▼ + ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ + │liboliveaudio│ │libolivecodec│ │liboliveplugin│ + └──────┬──────┘ └──────┬──────┘ └─────────────┘ + │ │ + └───────────────┼───────────────┘ + │ + ▼ + ┌─────────────┐ + │libolivecore │ + └─────────────┘ +``` + +**依赖规则**: +- 禁止任何下层库依赖上层库。 +- `libolivenode.so` 当前依赖 `render/` 的部分类型(`FrameHashCache`, `ShaderJob` 等枚举),需通过**前向声明(forward declare)**或**接口抽象**解耦。 + +### 3.4 接口与符号可见性 + +#### 3.4.1 导出宏定义 + +在每个模块的公共头目录(如 `app/node/api.h`)中定义: + +```cpp +// app/node/api.h +#pragma once + +#include "common/define.h" + +#ifdef OLIVE_BUILDING_NODE +# define OLIVE_NODE_API Q_DECL_EXPORT +#else +# define OLIVE_NODE_API Q_DECL_IMPORT +#endif +``` + +所有需要跨库使用的类/函数均标记: + +```cpp +// app/node/node.h +class OLIVE_NODE_API Node : public QObject { ... }; +``` + +#### 3.4.2 Node 与 Render 的解耦 + +当前 `Node.h` 包含以下 render 头文件(经代码分析): +- `render/rendercache.h`(`FrameHashCache` 等) +- `render/job/shaderjob.h` 等(作业类型) + +**解耦策略**: + +1. **枚举与前置声明**:将 `RenderTicket::ReturnType`, `RenderMode::Mode`, `PixelFormat` 等移到 `ext/core/` 或 `common/` 中,使其不依赖 `render/`。 +2. **接口回调**:`Node` 中需要通知缓存失效的逻辑,改为通过 `NodeCacheInterface` 纯虚接口注入,而非直接引用 `FrameHashCache`。 +3. **Job 类型**:`Node` 仅需要知道 `ShaderJob` 等类型的存在以支持虚函数分发,可以将 `ProcessShader` 等虚函数的参数从具体类型改为更抽象的 `const void *` 或基类指针,或把 `render/job/*.h` 中仅含数据定义的头文件移动到 `common/`。 + +### 3.5 CMake 改造要点 + +当前 `app/` 下的子模块通过修改 `PARENT_SCOPE` 变量 `OLIVE_SOURCES` 来汇报源文件,最终由 `app/CMakeLists.txt` 统一创建 `libolive-editor` OBJECT 库。改造后,每个子模块应自行产出库目标。 + +#### 3.5.1 子模块 CMakeLists.txt 改造示例 + +以 `app/node/CMakeLists.txt` 为例: + +```cmake +# 改造前:仅收集源文件到 PARENT_SCOPE +# set(OLIVE_SOURCES ${OLIVE_SOURCES} node.cpp node.h PARENT_SCOPE) + +# 改造后:创建本模块的 OBJECT/SHARED 库片段 +add_subdirectory(project) +add_subdirectory(output) +# ... 其他子目录 + +set(NODE_SOURCES + node.cpp + node.h + traverser.cpp + traverser.h + # ... 所有 node/ 下源文件 +) + +# 方案 A:本模块建 SHARED 库(推荐) +add_library(olivenode SHARED ${NODE_SOURCES}) +target_link_libraries(olivenode + PUBLIC + olivecore + olivecodec + Qt${QT_VERSION_MAJOR}::Core +) +target_compile_definitions(olivenode PRIVATE OLIVE_BUILDING_NODE) +target_include_directories(olivenode + PUBLIC + $ + $ +) + +# 方案 B:本模块建 OBJECT 库,由上层组合为 SHARED 库 +# add_library(olivenode-obj OBJECT ${NODE_SOURCES}) +# ... 在 app/CMakeLists.txt 中组合 +``` + +#### 3.5.2 顶层 app/CMakeLists.txt 改造 + +```cmake +# 各子模块自行创建库目标 +add_subdirectory(audio) # -> oliveaudio +add_subdirectory(codec) # -> olivecodec +add_subdirectory(common) # -> 并入 olivecodec 或单独 common +add_subdirectory(config) # -> 并入 olivenode +add_subdirectory(node) # -> olivenode +add_subdirectory(render) # -> oliverender +add_subdirectory(task) # -> olivetask +add_subdirectory(timeline) # -> 并入 olivenode +add_subdirectory(undo) # -> 并入 olivenode +add_subdirectory(widget) # -> oliveui +add_subdirectory(panel) # -> oliveui +add_subdirectory(window) # -> oliveui +add_subdirectory(dialog) # -> oliveui +add_subdirectory(tool) # -> oliveui +add_subdirectory(ui) # -> oliveui +add_subdirectory(pluginSupport) # -> oliveplugin + +# 版本对象保持 OBJECT +add_library(olive-version-obj OBJECT version.cpp version.h) +target_link_libraries(olive-version-obj PRIVATE Qt${QT_VERSION_MAJOR}::Core) + +# 主可执行文件 +add_executable(olive-editor + main.cpp + core.cpp + core.h + $ +) + +target_link_libraries(olive-editor PRIVATE + oliveui + olivetask + oliverender + olivenode + oliveaudio + olivecodec + oliveplugin + olivecore + # ... 外部依赖 +) +``` + +#### 3.5.3 平台注意事项 + +- **Windows**:`Q_DECL_EXPORT`/`Q_DECL_IMPORT` 会自动处理 `__declspec(dllexport/dllimport)`。需确保 `olive-editor.exe` 与所有 `.dll` 在同一目录,或通过 `PATH` 找到。 +- **macOS**:动态库后缀为 `.dylib`。若打包为 `.app` Bundle,需使用 `install_name_tool` 或 CMake 的 `@rpath` 设置确保加载路径正确。 +- **Linux**:使用 `RPATH` 或 `LD_LIBRARY_PATH`。打包时可用 `patchelf` 或 AppImage 工具。 + +--- + +## 4. 渲染器多进程化方案 + +### 4.1 进程模型 + +| 进程 | 职责 | 技术栈 | +|---|---|---| +| `olive-editor`(主进程) | GUI、项目数据管理、时间线编辑、用户交互 | Qt Widgets, KDDockWidgets | +| `olive-renderer`(子进程) | 节点图遍历、GPU/OpenGL 渲染、FFmpeg 解码、OFX 插件执行 | Qt Core(非 GUI), OpenGL, FFmpeg, OCIO | + +**启动方式**:主进程通过 `QProcess` 启动 `olive-renderer`,并捕获其 `stdin/stdout` 作为通信管道。子进程不使用任何 GUI 模块,仅初始化 `QCoreApplication` 和 OpenGL 离屏上下文。 + +### 4.2 IPC 通信协议(stdio) + +#### 4.2.1 传输格式:NDJSON + +采用 **Newline Delimited JSON(NDJSON)**,每行一条完整 JSON 消息,以 `\n` 分隔。理由: +- 基于文本,易于调试(`echo '{...}' | olive-renderer` 可手动测试)。 +- 结构化,易于扩展新字段。 +- 帧边界天然由换行符确定,无需额外的长度前缀或帧同步协议。 + +#### 4.2.2 消息定义 + +**请求消息(主进程 → 子进程,写入子进程 stdin)**: + +| 字段 | 类型 | 说明 | +|---|---|---| +| `type` | string | 消息类型:`init`, `render_frame`, `render_audio`, `cancel`, `shutdown`, `ping` | +| `req_id` | int | 请求唯一标识,用于响应匹配。 | +| `...` | 类型相关 | 见下表。 | + +**响应消息(子进程 → 主进程,写入子进程 stdout)**: + +| 字段 | 类型 | 说明 | +|---|---|---| +| `type` | string | 消息类型:`ready`, `result`, `error`, `cancelled`, `heartbeat`, `pong` | +| `req_id` | int | 对应请求的 `req_id`。对于主动推送(如 `heartbeat`),`req_id` 为 `0`。 | +| `...` | 类型相关 | 见下表。 | + +#### 4.2.3 详细消息格式 + +```json +// === 初始化 === +// 主进程 -> 子进程 +{"type":"init","req_id":1,"backend":"opengl","shader_path":"/usr/share/olive/shaders","ocio_config_path":"/path/to/config.ocio"} + +// 子进程 -> 主进程 +{"type":"ready","req_id":1,"status":"ok","backend_version":"4.6 (Core Profile)"} +{"type":"error","req_id":1,"status":"error","message":"Failed to create OpenGL context"} + +// === 渲染视频帧 === +// 主进程 -> 子进程 +{ + "type": "render_frame", + "req_id": 2, + "ticket_id": 101, + "node_graph_ref": "a3f7b2d9", + "time": "1001/30000", + "video_params": { + "width": 1920, + "height": 1080, + "depth": 1, + "format": "rgba32f", + "channel_count": 4, + "pixel_aspect": "1/1" + }, + "audio_params": { + "sample_rate": 48000, + "channel_layout": "stereo" + }, + "mode": "offline", + "color_manager": { + "reference_space": "ACES - ACES2065-1", + "display_space": "Rec.709" + }, + "shm_name": "/olive_r_12345_2", + "shm_size": 33177600 +} + +// 子进程 -> 主进程(成功) +{ + "type": "result", + "req_id": 2, + "ticket_id": 101, + "status": "ok", + "shm_name": "/olive_r_12345_2", + "width": 1920, + "height": 1080, + "format": "rgba32f", + "pixel_format_id": 28, + "timestamp_ms": 45 +} + +// 子进程 -> 主进程(失败) +{ + "type": "error", + "req_id": 2, + "ticket_id": 101, + "status": "error", + "category": "decoder", + "message": "Failed to decode frame at time 1001/30000: codec not found" +} + +// === 渲染音频 === +// 主进程 -> 子进程 +{ + "type": "render_audio", + "req_id": 3, + "ticket_id": 102, + "node_graph_ref": "a3f7b2d9", + "range": {"in": "0/1", "out": "48000/48000"}, + "audio_params": { + "sample_rate": 48000, + "channel_layout": "stereo", + "format": "flt_planar" + }, + "mode": "offline", + "shm_name": "/olive_r_12345_3", + "shm_size": 384000 +} + +// === 取消任务 === +{"type":"cancel","req_id":4,"ticket_id":101} +{"type":"cancelled","req_id":4,"ticket_id":101} + +// === 心跳与探测 === +{"type":"ping","req_id":5} +{"type":"pong","req_id":5} + +// 子进程主动心跳(每 3 秒) +{"type":"heartbeat","req_id":0,"timestamp":1716288000} + +// === 优雅退出 === +{"type":"shutdown","req_id":6} +{"type":"result","req_id":6,"status":"ok"} +``` + +#### 4.2.4 通信时序示例 + +``` +主进程 子进程 (olive-renderer) + │ │ + │── QProcess::start() ─────────────────>│ + │ │ 初始化 Qt Core + │ │ 初始化 OpenGL 上下文 + │<── stdout: {"type":"ready",...} ─────│ + │ │ + │── stdin: render_frame (ticket #1) ──>│ + │── stdin: render_frame (ticket #2) ──>│ 入队渲染 + │ │ + │<── stdout: result (ticket #1) ───────│ 完成帧 #1 + │── 读取共享内存帧数据 │ + │── shm_unlink() │ + │ │ + │<── stdout: result (ticket #2) ───────│ 完成帧 #2 + │ │ + │── stdin: cancel (ticket #3) ────────>│ 取消正在进行的 #3 + │<── stdout: cancelled (ticket #3) ────│ + │ │ + │── stdin: shutdown ──────────────────>│ 清理资源,退出事件循环 + │<── stdout: result (shutdown) ────────│ + │── QProcess::waitForFinished() ──────>│ 进程结束 +``` + +### 4.3 数据平面:共享内存帧传输 + +NDJSON 仅适合传输控制命令和元数据。**视频帧(RGBA32F, 1920×1080 ≈ 33MB)和音频块**不能通过 base64 编码在 JSON 中传输(实时预览需要 24–30fps,stdio 带宽和 CPU 编解码开销均不可接受)。 + +#### 4.3.1 方案:POSIX / Windows 共享内存 + +**POSIX(Linux/macOS)**: + +```cpp +// 主进程创建 +int fd = shm_open("/olive_r_12345_2", O_RDWR | O_CREAT, 0666); +ftruncate(fd, shm_size); +void *ptr = mmap(nullptr, shm_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + +// 子进程打开(通过 shm_name 从 JSON 中读取) +int fd = shm_open("/olive_r_12345_2", O_RDWR, 0666); +void *ptr = mmap(nullptr, shm_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + +// 使用完毕后,主进程负责 unlink +shm_unlink("/olive_r_12345_2"); +``` + +**Windows**: + +```cpp +// 主进程创建 +HANDLE hMap = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, + 0, shm_size, L"Local\\olive_r_12345_2"); +void *ptr = MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, shm_size); + +// 子进程打开 +HANDLE hMap = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, L"Local\\olive_r_12345_2"); +void *ptr = MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, shm_size); + +// 清理 +UnmapViewOfFile(ptr); +CloseHandle(hMap); +``` + +**帧数据布局**:共享内存前 256 字节保留为**元数据头**(Magic、版本、实际数据偏移、行间距 linesize、校验和),后续为原始像素/采样数据。 + +``` +┌─────────────────────┬──────────────────────────────────────┐ +│ Header (256 B) │ Pixel Data │ +│ magic | offset | │ row 0 │ row 1 │ ... │ row H-1 │ +│ linesize | ... │ (width * channels * sizeof(float)) │ +└─────────────────────┴──────────────────────────────────────┘ +``` + +#### 4.3.2 备选方案:内存映射临时文件 + +若共享内存 API 在不同平台间行为不一致,可退而求其次使用**内存映射临时文件**: + +```cpp +QTemporaryFile tmp; +tmp.setFileTemplate("olive_render_XXXXXX.raw"); +tmp.open(); +tmp.resize(shm_size); + +// 内存映射 +tmp.seek(0); +uchar *ptr = tmp.map(0, shm_size); + +// 将文件路径(而非 shm_name)通过 JSON 传递 +{"shm_path": "/tmp/olive_render_a1b2c3.raw", ...} +``` + +此方案兼容性最好,但性能略低于纯共享内存(因可能触发文件系统页缓存回写)。 + +### 4.4 节点图序列化与缓存 + +渲染请求的核心输入是**节点图(Node Graph)**。直接每次传输完整 XML 序列化在实时预览场景下(24–30fps)不可接受。 + +#### 4.4.1 策略:引用 + 增量更新 + +1. **首次传输**:当某个 `ViewerOutput` 需要渲染时,主进程将其关联的节点图通过 `type: init_graph` 消息完整序列化发送给子进程,子进程缓存并返回一个 `graph_ref`(如 SHA-256 前 8 位)。 + + ```json + {"type":"init_graph","req_id":10,"graph_ref":"a3f7b2d9","node_graph_xml":"........."} + {"type":"result","req_id":10,"graph_ref":"a3f7b2d9","node_count":42} + ``` + +2. **后续引用**:渲染帧请求通过 `"node_graph_ref": "a3f7b2d9"` 引用已缓存的图,无需重复传输 XML。 + +3. **增量更新**:当用户调整某个节点的参数时,主进程发送 `update_graph` 消息,仅携带变更的节点 ID 和参数字段。 + + ```json + {"type":"update_graph","req_id":11,"graph_ref":"a3f7b2d9","updates":[{"node_id":"Transform1","params":{"position":{"x":100,"y":200}}}]} + ``` + +4. **序列化复用**:直接复用现有的 `ProjectSerializer`,以 `kOnlyNodes` 模式序列化目标 `ViewerOutput` 及其上游依赖节点。 + +#### 4.4.2 子进程中的节点图重建 + +子进程收到 `init_graph` 后: +1. 使用 `ProjectSerializer::Load()` 将 XML 反序列化为临时 `Project` 对象。 +2. 提取目标 `ViewerOutput` 节点,构建本地 `NodeValueDatabase`。 +3. 将图对象存入 `graph_ref → Project` 的映射表中。 +4. 后续 `render_frame` 直接使用缓存的图,避免重复解析 XML。 + +### 4.5 渲染进程生命周期管理 + +#### 4.5.1 启动与就绪 + +```cpp +// RenderManager::CreateInstance() +render_process_ = new QProcess(this); +render_process_->setProgram(QCoreApplication::applicationDirPath() + "/olive-renderer"); +render_process_->setArguments({"--backend", "opengl"}); +render_process_->start(); + +// 等待 ready 消息(带超时) +connect(render_process_, &QProcess::readyReadStandardOutput, this, &RenderManager::ReadStdout); +``` + +#### 4.5.2 心跳与卡死检测 + +- 子进程每 **3 秒**主动输出 `heartbeat`。 +- 主进程每 **5 秒**发送 `ping`,若 **10 秒**内未收到 `pong`,认为子进程卡死。 +- 卡死处理: + 1. `render_process_->kill()` 强制终止。 + 2. 清理所有未完成的 `RenderTicket`,标记为错误状态。 + 3. 自动重启子进程。 + 4. 重新发送所有活跃的 `graph_ref` 对应的节点图。 + +#### 4.5.3 崩溃恢复 + +```cpp +connect(render_process_, QOverload::of(&QProcess::errorOccurred), + this, [this](QProcess::ProcessError error) { + if (error == QProcess::Crashed) { + qWarning() << "Renderer process crashed. Restarting..."; + RestartRendererProcess(); + // 通知 UI 显示"渲染器已崩溃并恢复"的提示 + } +}); +``` + +**关键收益**:即使 OpenGL 驱动崩溃、OFX 插件 segfault、FFmpeg 解码器触发未处理异常,也只会导致 `olive-renderer` 子进程终止,主进程的 GUI、项目数据、Undo 栈均完好无损。 + +#### 4.5.4 优雅退出 + +主进程析构时: +1. 发送 `shutdown` 请求,等待子进程返回 `result`(超时 5 秒)。 +2. 若子进程未退出,调用 `terminate()`,再等 3 秒。 +3. 若仍未退出,`kill()` 强制结束。 + +### 4.6 主进程 RenderManager 适配 + +当前 `RenderManager` 直接创建并管理 `RenderThread`。改造后,`RenderManager` 转型为 **IPC 客户端管理器**,对外接口保持**完全不变**,以最小化 UI 层侵入。 + +#### 4.6.1 类结构调整 + +```cpp +// 新增:轻量级 IPC 客户端 +class RenderServiceClient : public QObject { + Q_OBJECT +public: + explicit RenderServiceClient(QObject *parent = nullptr); + bool Start(const QString &renderer_executable); + void Shutdown(); + + // 异步发送渲染请求,返回内部 ticket_id + int RequestRenderFrame(const RenderManager::RenderVideoParams ¶ms, const QString &graph_ref); + int RequestRenderAudio(const RenderManager::RenderAudioParams ¶ms, const QString &graph_ref); + void CancelTicket(int ticket_id); + +signals: + void ResultReceived(int ticket_id, const QJsonObject &result); + void ErrorReceived(int ticket_id, const QString &message); + void ProcessCrashed(); + void ProcessRecovered(); + +private: + QProcess *process_; + QHash ticket_map_; // ticket_id -> RenderTicket + // ... +}; + +// 改造后的 RenderManager(对外接口不变) +class RenderManager : public QObject { + Q_OBJECT +public: + // 原有接口 100% 保留 + RenderTicketPtr RenderFrame(const RenderVideoParams ¶ms); + RenderTicketPtr RenderAudio(const RenderAudioParams ¶ms); + bool RemoveTicket(RenderTicketPtr ticket); + +private: + // 旧实现:RenderThread *video_thread_; ... + // 新实现: + RenderServiceClient *client_; + QHash node_graph_refs_; // ViewerOutput -> graph_ref +}; +``` + +#### 4.6.2 渲染流程映射 + +``` +UI/Widget 层 + │ RenderManager::RenderFrame(params) + │ + ▼ +RenderManager + │ 1. 检查 params.node 对应的 graph_ref 是否已在子进程缓存 + │ 2. 若未缓存,序列化节点图 -> init_graph -> 获取 graph_ref + │ 3. 创建共享内存 -> shm_name + │ 4. 将 params + graph_ref + shm_name 打包为 NDJSON + │ + ▼ +RenderServiceClient -> QProcess::write() -> 子进程 stdin + │ + │<── stdout: NDJSON result + ▼ +RenderManager 从共享内存读取帧数据 + │ 构造 FramePtr/TexturePtr + │ 调用 RenderTicket::Finish(result) + ▼ +UI 层收到 RenderTicketWatcher::Finished 信号,更新显示 +``` + +#### 4.6.3 兼容性保留 + +- 保留 `RenderTicket`, `RenderTicketWatcher`, `RenderTicketPtr` 的完整语义。 +- 保留 `PreviewAutoCacher` 的接口,其内部调用 `RenderManager` 的方式无需修改。 +- 保留 `RenderMode::Mode`, `ReturnType` 等枚举定义位置,或在 `ext/core/` 中建立同义定义。 + +--- + +## 5. 实施路线图 + +### 5.1 第一阶段:动态库基础拆分(预估 2–3 周) + +**目标**:完成低耦合模块的动态库化,建立符号导出规范和 CMake 新范式。 + +| 任务 | 说明 | +|---|---| +| T1.1 | 将 `ext/core/` 的构建类型由 `STATIC` 改为 `SHARED`,验证所有平台加载正常。 | +| T1.2 | 创建 `libolivecodec.so`:合并 `app/codec/` + `app/common/`,处理跨平台符号导出。 | +| T1.3 | 创建 `liboliveplugin.so`:将 `pluginSupport/` + `OfxHost` 独立,验证 OFX 插件加载。 | +| T1.4 | 创建 `liboliveaudio.so`:将 `app/audio/` 独立。 | +| T1.5 | 引入 `OLIVE_API` 宏体系,为每个模块定义导出/导入宏。 | +| T1.6 | 在 CI 中增加动态库加载路径测试,确保 `olive-editor` 能在干净环境中启动。 | + +**里程碑**:`olive-editor` 可正常启动,所有原有功能不变,但内部已由 1 个 OBJECT 库变为 4+ 个动态库。 + +### 5.2 第二阶段:核心层拆分与渲染进程化(预估 4–6 周) + +**目标**:完成 `node/` 与 `render/` 的解耦,并实现 `olive-renderer` 子进程。 + +| 任务 | 说明 | +|---|---| +| T2.1 | **Node/Render 解耦**:将 `Node.h` 中对 `render/` 的包含移除,迁移依赖类型到 `common/` 或 `ext/core/`;将 `FrameHashCache` 交互改为接口注入。 | +| T2.2 | 创建 `libolivenode.so`:包含 `node/`, `timeline/`, `undo/`, `config/`。 | +| T2.3 | 创建 `liboliverender.so`:包含 `render/`(不含 OpenGL 后端具体平台代码),依赖 `olivenode` + `olivecodec`。 | +| T2.4 | 创建 `olive-renderer` 可执行文件目标,复用 `liboliverender.so` + `libolivenode.so` + `libolivecodec.so` + `libolivecore.so`。 | +| T2.5 | 实现 **NDJSON IPC 协议**:在子进程中实现 `RenderService`(基于 `QSocketNotifier` 监听 `stdin`);在主进程中实现 `RenderServiceClient`。 | +| T2.6 | 实现 **共享内存帧传输**:封装 `SharedMemoryBuffer` 类,支持 POSIX + Windows API,统一为 `Create/Attach/Detach/Destroy` 接口。 | +| T2.7 | 实现 **节点图序列化缓存**:在 `RenderServiceClient` 中维护 `graph_ref` 映射表;在子进程中维护反序列化后的节点图缓存。 | +| T2.8 | 改造 `RenderManager`:将内部 `RenderThread` 调度替换为 `RenderServiceClient` IPC 调用,对外接口保持不变。 | +| T2.9 | 实现 **崩溃恢复与心跳**:子进程卡死/崩溃检测,自动重启,重新同步节点图缓存。 | +| T2.10 | 全面回归测试:预览、导出、音频回放、OFX 插件、色彩管理。 | + +**里程碑**:渲染在子进程中稳定运行,手动触发子进程崩溃(如 `kill -9`)后,主进程可自动恢复且 GUI 不闪退。 + +### 5.3 第三阶段:优化与稳定化(预估 2–3 周) + +| 任务 | 说明 | +|---|---| +| T3.1 | **增量节点图更新**:实现 `update_graph` 消息,避免参数微调时重复传输完整 XML。 | +| T3.2 | **多渲染进程**:支持同时启动多个 `olive-renderer` 进程(如一个用于预览,一个用于后台导出),提升并行度。 | +| T3.3 | **性能基准测试**:对比单进程 vs 多进程的帧渲染延迟、内存占用、CPU 开销,优化共享内存拷贝次数。 | +| T3.4 | **打包适配**:更新 macOS `.app` Bundle、Windows Installer、Linux AppImage 的打包脚本,确保动态库和 `olive-renderer` 被正确包含。 | +| T3.5 | 文档更新:更新 `build.md`、`build-macos-zh.md`,说明新的运行时依赖和动态库加载路径配置。 | + +--- + +## 6. 风险与对策 + +| 风险 | 影响 | 对策 | +|---|---|---| +| **Node/Render 解耦工作量超预期** | 高 | 采用"接口抽象 + 前向声明"的轻量解耦,不追求完全消除逻辑耦合,只消除编译期头文件依赖。若实在无法解耦,可将 `node/` + `render/` 暂时合并为一个 `libolive-engine.so`,后续再拆分。 | +| **共享内存跨平台兼容性** | 中 | 封装抽象层 `SharedMemoryBuffer`,POSIX 和 Windows 分别实现。若某平台支持不佳,自动降级为内存映射临时文件。 | +| **NDJSON 协议性能瓶颈** | 中 | 控制消息数据量极小(<1KB),不会是瓶颈。若未来需要更高吞吐,可无损升级为 **MessagePack**(二进制 JSON 兼容格式),无需改协议语义。 | +| **子进程启动延迟影响首帧** | 中 | 采用**预启动策略**:主进程启动后立即在后台启动 `olive-renderer`,用户打开项目时渲染器已就绪。 | +| **GPU/OpenGL 上下文跨进程问题** | 中 | 子进程独立创建离屏 OpenGL 上下文(`QOffscreenSurface`),主进程不再直接操作 GL。主进程 UI 显示通过共享内存获取 CPU 帧数据,或使用平台特定的 GL 共享纹理(进阶优化,初期不做)。 | +| **OFX 插件在子进程中的稳定性** | 高 | 这正是多进程架构的收益点。OFX 插件崩溃仅影响子进程,主进程安全。需确保 OFX 插件资源路径通过 `--resource-path` 参数传递给子进程。 | +| **Qt 信号/槽跨动态库** | 低 | Qt 的元对象系统原生支持跨动态库,只要确保 `moc` 编译了含 `Q_OBJECT` 宏的公共头文件,且动态库被正确链接。 | + +--- + +## 7. 附录:完整消息协议定义 + +### 7.1 请求消息(主进程 → 子进程) + +```typescript +// 基础请求结构 +interface Request { + type: string; + req_id: number; // >0 +} + +interface InitRequest extends Request { + type: "init"; + backend: "opengl" | "dummy"; + shader_path: string; + ocio_config_path?: string; +} + +interface InitGraphRequest extends Request { + type: "init_graph"; + graph_ref: string; // 主进程生成的图引用 ID + node_graph_xml: string; // 完整的项目 XML(kOnlyNodes 模式) +} + +interface UpdateGraphRequest extends Request { + type: "update_graph"; + graph_ref: string; + updates: Array<{ + node_id: string; + params: Record; + }>; +} + +interface RenderFrameRequest extends Request { + type: "render_frame"; + ticket_id: number; + graph_ref: string; + time: string; // 有理数字符串,如 "1001/30000" + video_params: VideoParams; + audio_params: AudioParams; + mode: "offline" | "online"; + color_manager?: ColorManagerInfo; + force_size?: { width: number; height: number }; + force_format?: string; + shm_name: string; + shm_size: number; +} + +interface RenderAudioRequest extends Request { + type: "render_audio"; + ticket_id: number; + graph_ref: string; + range: { in: string; out: string }; + audio_params: AudioParams; + mode: "offline" | "online"; + generate_waveforms: boolean; + shm_name: string; + shm_size: number; +} + +interface CancelRequest extends Request { + type: "cancel"; + ticket_id: number; +} + +interface ShutdownRequest extends Request { + type: "shutdown"; +} + +interface PingRequest extends Request { + type: "ping"; +} +``` + +### 7.2 响应消息(子进程 → 主进程) + +```typescript +// 基础响应结构 +interface Response { + type: string; + req_id: number; // 对应请求,0 表示主动推送 +} + +interface ReadyResponse extends Response { + type: "ready"; + status: "ok" | "error"; + backend_version?: string; + message?: string; // 当 status=error 时 +} + +interface ResultResponse extends Response { + type: "result"; + ticket_id?: number; + status: "ok"; + // 对于渲染结果 + shm_name?: string; + width?: number; + height?: number; + format?: string; + pixel_format_id?: number; + timestamp_ms?: number; // 渲染耗时 +} + +interface ErrorResponse extends Response { + type: "error"; + ticket_id?: number; + status: "error"; + category?: "decoder" | "shader" | "plugin" | "system" | "unknown"; + message: string; +} + +interface CancelledResponse extends Response { + type: "cancelled"; + ticket_id: number; +} + +interface PongResponse extends Response { + type: "pong"; +} + +interface HeartbeatResponse extends Response { + type: "heartbeat"; + timestamp: number; // Unix timestamp (seconds) +} +``` + +### 7.3 共享内存布局(二进制) + +```c +#define OLIVE_SHM_MAGIC 0x4F4C4956 // 'OLIV' +#define OLIVE_SHM_VERSION 1 + +struct ShmHeader { + uint32_t magic; // OLIVE_SHM_MAGIC + uint32_t version; // OLIVE_SHM_VERSION + uint32_t data_offset; // 像素/采样数据起始偏移(通常 256) + uint32_t width; // 帧宽(视频)或采样数(音频) + uint32_t height; // 帧高 + uint32_t depth; // 3D 纹理深度 + uint32_t channel_count; // 通道数 + uint32_t pixel_format; // PixelFormat 枚举值 + uint32_t linesize; // 每行字节数(可能包含 padding) + uint64_t data_size; // 实际数据字节数 + uint64_t checksum; // CRC64(可选校验) + uint8_t reserved[256 - 48]; // 填充至 256 字节 +}; +// 紧接着 ShmHeader 之后为原始数据 +``` + +--- + +## 8. 结论 + +本方案通过**动态库分层拆分**将 Olive/Oak 从单体编译单元演进为模块化架构,显著提升编译效率和代码边界清晰度;通过**渲染器多进程化**将最易崩溃的 GPU/OFX/FFmpeg 逻辑隔离到独立进程,利用 **stdio + NDJSON + 共享内存** 实现低延迟 IPC,从根本上解决"渲染崩溃导致编辑器闪退丢工作"的痛点。 + +实施上采用**渐进式路线**:先拆分外围低耦合模块建立规范,再攻克核心 Node/Render 解耦与进程化。对外接口(`RenderManager::RenderFrame` 等)保持完全兼容,UI 层无需感知底层架构变化。 diff --git a/docs/zh/modularization-plan/01-olivecore.md b/docs/zh/modularization-plan/01-olivecore.md new file mode 100644 index 000000000..4dbb83e61 --- /dev/null +++ b/docs/zh/modularization-plan/01-olivecore.md @@ -0,0 +1,466 @@ +# libolivecore.so — 基础数据类型库 + +> **依赖**:无(不依赖其他 Olive 模块) +> **外部依赖**:FFmpeg::avutil, OpenGL::GL, Imath::Imath +> **当前状态**:`ext/core/` 目录,已作为独立静态库编译 +> **改造难度**:⭐(最简单) + +--- + +## 1. 当前状态分析 + +`ext/core/` 是项目中最为独立的模块,包含纯数据类型和数学工具: + +| 文件/类 | 职责 | +|---|---| +| `rational.h` | 有理数(帧率、时间) | +| `color.h` | 颜色表示与运算 | +| `timecodefunctions.h` | 时间码格式化 | +| `timerange.h` | 时间范围 `[in, out)` | +| `samplebuffer.h` | 音频采样缓冲区 | +| `pixelformat.h` | 像素格式枚举 | +| `videoparams.h` / `audioparams.h` | 视频/音频参数 | +| `bezier.h` / `math.h` | 数学工具 | +| `stringutils.h` / `value.h` | 字符串与通用值 | + +**优势**: +- 无 Qt GUI 依赖(仅用 `Qt::Core` 的基础类型)。 +- 无项目内其他模块依赖。 +- 主要是 POD(Plain Old Data)类型和纯函数。 + +--- + +## 2. C API 设计 + +### 2.1 头文件:`c_api/include/olive/core_api.h` + +```c +#ifndef OLIVE_CORE_API_H +#define OLIVE_CORE_API_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#define OLIVE_CORE_API_VERSION 1 + +/* ========== 导出宏 ========== */ +#ifdef OLIVE_BUILDING_CORE +# define OLIVE_CORE_API __attribute__((visibility("default"))) +#else +# define OLIVE_CORE_API +#endif + +/* ========== 枚举 ========== */ + +typedef enum { + OLIVE_PIXEL_FMT_INVALID = 0, + OLIVE_PIXEL_FMT_RGBA8, + OLIVE_PIXEL_FMT_RGBA16, + OLIVE_PIXEL_FMT_RGBA32F, + OLIVE_PIXEL_FMT_RGB8, + OLIVE_PIXEL_FMT_YUV420P, + OLIVE_PIXEL_FMT_YUV422P, + OLIVE_PIXEL_FMT_YUV444P, + OLIVE_PIXEL_FMT_COUNT +} OlivePixelFormat; + +typedef enum { + OLIVE_SAMPLE_FMT_INVALID = 0, + OLIVE_SAMPLE_FMT_U8, + OLIVE_SAMPLE_FMT_S16, + OLIVE_SAMPLE_FMT_S32, + OLIVE_SAMPLE_FMT_FLT, + OLIVE_SAMPLE_FMT_DBL, + OLIVE_SAMPLE_FMT_U8P, + OLIVE_SAMPLE_FMT_S16P, + OLIVE_SAMPLE_FMT_S32P, + OLIVE_SAMPLE_FMT_FLTP, + OLIVE_SAMPLE_FMT_DBLP, + OLIVE_SAMPLE_FMT_COUNT +} OliveSampleFormat; + +typedef enum { + OLIVE_OK = 0, + OLIVE_ERROR_GENERIC = -1, + OLIVE_ERROR_INVALID = -2, + OLIVE_ERROR_NOMEM = -3, + OLIVE_ERROR_NOT_FOUND = -4, + OLIVE_ERROR_IO = -5, + OLIVE_ERROR_CANCELLED = -6, + OLIVE_ERROR_UNSUPPORTED = -7, +} OliveResult; + +/* ========== POD 结构体 ========== */ + +typedef struct { + int64_t num; + int64_t den; +} OliveRational; + +typedef struct { + double r; + double g; + double b; + double a; +} OliveColor; + +typedef struct { + int width; + int height; +} OliveSize; + +typedef struct { + int width; + int height; + int depth; + int channel_count; + OlivePixelFormat format; + double pixel_aspect_num; + double pixel_aspect_den; +} OliveVideoParams; + +typedef struct { + int sample_rate; + int64_t channel_layout; // FFmpeg AV_CH_LAYOUT_* 值 + OliveSampleFormat format; +} OliveAudioParams; + +/* ========== API 版本 ========== */ +OLIVE_CORE_API int olive_core_api_version(void); + +/* ========== 内存管理 ========== */ +OLIVE_CORE_API void olive_core_free(void* ptr); +OLIVE_CORE_API void* olive_core_alloc(size_t size); +OLIVE_CORE_API void* olive_core_realloc(void* ptr, size_t size); + +/* ========== 错误处理 ========== */ +OLIVE_CORE_API int olive_core_last_error_code(void); +OLIVE_CORE_API const char* olive_core_last_error_string(void); + +/* ========== Rational ========== */ +OLIVE_CORE_API OliveRational olive_rational_make(int64_t num, int64_t den); +OLIVE_CORE_API OliveRational olive_rational_add(OliveRational a, OliveRational b); +OLIVE_CORE_API OliveRational olive_rational_sub(OliveRational a, OliveRational b); +OLIVE_CORE_API OliveRational olive_rational_mul(OliveRational a, OliveRational b); +OLIVE_CORE_API OliveRational olive_rational_div(OliveRational a, OliveRational b); +OLIVE_CORE_API double olive_rational_to_double(OliveRational r); +OLIVE_CORE_API OliveRational olive_rational_from_double(double v, int64_t max_den); +OLIVE_CORE_API int olive_rational_cmp(OliveRational a, OliveRational b); +OLIVE_CORE_API int olive_rational_is_valid(OliveRational r); +OLIVE_CORE_API void olive_rational_reduce(OliveRational* r); + +/* ========== Color ========== */ +OLIVE_CORE_API OliveColor olive_color_make(double r, double g, double b, double a); +OLIVE_CORE_API OliveColor olive_color_add(OliveColor a, OliveColor b); +OLIVE_CORE_API OliveColor olive_color_mul_scalar(OliveColor c, double s); + +/* ========== TimeRange ========== */ +typedef struct OliveTimeRange OliveTimeRange; + +OLIVE_CORE_API OliveTimeRange* olive_time_range_create(OliveRational in, OliveRational out); +OLIVE_CORE_API void olive_time_range_destroy(OliveTimeRange* tr); +OLIVE_CORE_API OliveRational olive_time_range_in(OliveTimeRange* tr); +OLIVE_CORE_API OliveRational olive_time_range_out(OliveTimeRange* tr); +OLIVE_CORE_API OliveRational olive_time_range_length(OliveTimeRange* tr); +OLIVE_CORE_API int olive_time_range_contains(OliveTimeRange* tr, OliveRational t); +OLIVE_CORE_API int olive_time_range_overlaps(OliveTimeRange* a, OliveTimeRange* b); + +/* ========== Timecode ========== */ +OLIVE_CORE_API char* olive_timecode_from_rational(OliveRational time, + OliveRational timebase, + int display_mode); +OLIVE_CORE_API OliveRational olive_timecode_to_rational(const char* timecode, + OliveRational timebase); + +/* ========== PixelFormat ========== */ +OLIVE_CORE_API int olive_pixel_format_bytes_per_channel(OlivePixelFormat fmt); +OLIVE_CORE_API int olive_pixel_format_channel_count(OlivePixelFormat fmt); +OLIVE_CORE_API size_t olive_pixel_format_frame_size(OlivePixelFormat fmt, int width, int height); +OLIVE_CORE_API const char* olive_pixel_format_name(OlivePixelFormat fmt); + +/* ========== SampleBuffer ========== */ +typedef struct OliveSampleBuffer OliveSampleBuffer; + +OLIVE_CORE_API OliveSampleBuffer* olive_sample_buffer_create(OliveAudioParams params, + int sample_count); +OLIVE_CORE_API void olive_sample_buffer_destroy(OliveSampleBuffer* buf); +OLIVE_CORE_API int olive_sample_buffer_sample_count(OliveSampleBuffer* buf); +OLIVE_CORE_API int olive_sample_buffer_channel_count(OliveSampleBuffer* buf); +OLIVE_CORE_API void* olive_sample_buffer_channel_data(OliveSampleBuffer* buf, int channel); +OLIVE_CORE_API size_t olive_sample_buffer_channel_data_size(OliveSampleBuffer* buf); +OLIVE_CORE_API OliveAudioParams olive_sample_buffer_params(OliveSampleBuffer* buf); +OLIVE_CORE_API OliveSampleBuffer* olive_sample_buffer_silence(OliveAudioParams params, + int sample_count); + +/* ========== VideoParams 辅助 ========== */ +OLIVE_CORE_API size_t olive_video_params_frame_size(OliveVideoParams params); +OLIVE_CORE_API int olive_video_params_is_valid(OliveVideoParams params); + +#ifdef __cplusplus +} +#endif + +#endif // OLIVE_CORE_API_H +``` + +### 2.2 实现:`c_api/src/core_api.cpp` + +```cpp +#include "olive/core_api.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// 线程局部错误状态 +thread_local int g_last_error_code = OLIVE_OK; +thread_local char g_last_error_string[1024]; + +static void SetError(int code, const char* msg) { + g_last_error_code = code; + strncpy(g_last_error_string, msg, sizeof(g_last_error_string) - 1); + g_last_error_string[sizeof(g_last_error_string) - 1] = '\0'; +} + +extern "C" { + +int olive_core_api_version(void) { return OLIVE_CORE_API_VERSION; } + +void olive_core_free(void* ptr) { free(ptr); } +void* olive_core_alloc(size_t size) { return malloc(size); } +void* olive_core_realloc(void* ptr, size_t size) { return realloc(ptr, size); } + +int olive_core_last_error_code(void) { return g_last_error_code; } +const char* olive_core_last_error_string(void) { return g_last_error_string; } + +OliveRational olive_rational_make(int64_t num, int64_t den) { + return {num, den}; +} + +OliveRational olive_rational_add(OliveRational a, OliveRational b) { + olive::Rational ra(a.num, a.den); + olive::Rational rb(b.num, b.den); + auto rc = ra + rb; + return {rc.numerator(), rc.denominator()}; +} + +// ... 其他 rational 运算类似封装 ... + +double olive_rational_to_double(OliveRational r) { + return olive::Rational(r.num, r.den).toDouble(); +} + +OliveColor olive_color_make(double r, double g, double b, double a) { + return {r, g, b, a}; +} + +OliveTimeRange* olive_time_range_create(OliveRational in, OliveRational out) { + try { + auto* tr = new OliveTimeRange(); + // 内部持有 olive::TimeRange 指针 + // tr->impl = new olive::TimeRange(...); + return tr; + } catch (...) { + SetError(OLIVE_ERROR_NOMEM, "Failed to create TimeRange"); + return nullptr; + } +} + +void olive_time_range_destroy(OliveTimeRange* tr) { + if (tr) { + delete tr; + } +} + +OliveRational olive_time_range_in(OliveTimeRange* tr) { + // auto r = tr->impl->in(); + // return {r.numerator(), r.denominator()}; + return {0, 1}; // 占位 +} + +// ... 其他 TimeRange 封装 ... + +char* olive_timecode_from_rational(OliveRational time, + OliveRational timebase, + int display_mode) { + try { + olive::Rational t(time.num, time.den); + olive::Rational tb(timebase.num, timebase.den); + QString str = olive::Timecode::time_to_string( + t, tb, + static_cast(display_mode) + ); + QByteArray utf8 = str.toUtf8(); + char* result = static_cast(malloc(utf8.size() + 1)); + memcpy(result, utf8.constData(), utf8.size() + 1); + return result; + } catch (...) { + SetError(OLIVE_ERROR_GENERIC, "Timecode conversion failed"); + return nullptr; + } +} + +OliveSampleBuffer* olive_sample_buffer_create(OliveAudioParams params, int sample_count) { + try { + // olive::AudioParams cpp_params = ...; + auto* buf = new OliveSampleBuffer(); + // buf->impl = new olive::SampleBuffer(cpp_params, sample_count); + return buf; + } catch (...) { + SetError(OLIVE_ERROR_NOMEM, "Failed to create SampleBuffer"); + return nullptr; + } +} + +void olive_sample_buffer_destroy(OliveSampleBuffer* buf) { + if (buf) { + delete buf; + } +} + +// ... 其他 SampleBuffer 封装 ... + +} // extern "C" +``` + +--- + +## 3. CMake 改造 + +### 3.1 `ext/core/CMakeLists.txt` + +```cmake +# 改造前 +# add_library(olivecore STATIC ...) + +# 改造后 +set(CORE_SOURCES + src/rational.cpp + src/color.cpp + src/timerange.cpp + src/samplebuffer.cpp + src/pixelformat.cpp + src/timecodefunctions.cpp + # ... 其他源文件 +) + +# C API 封装层 +set(CORE_API_SOURCES + ${CMAKE_SOURCE_DIR}/c_api/src/core_api.cpp +) + +add_library(olivecore SHARED + ${CORE_SOURCES} + ${CORE_API_SOURCES} +) + +target_compile_definitions(olivecore PRIVATE OLIVE_BUILDING_CORE) + +target_include_directories(olivecore + PUBLIC + $ + $ + PRIVATE + ${CMAKE_SOURCE_DIR}/c_api/include +) + +target_link_libraries(olivecore + PUBLIC + FFmpeg::avutil + OpenGL::GL + Imath::Imath +) + +# 默认隐藏符号,只有标记 OLIVE_CORE_API 的才导出 +set_target_properties(olivecore PROPERTIES + CXX_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN YES +) + +# 安装 +install(TARGETS olivecore DESTINATION lib) +install(DIRECTORY include/olive DESTINATION include) +install(FILES ${CMAKE_SOURCE_DIR}/c_api/include/olive/core_api.h DESTINATION include/olive) +``` + +--- + +## 4. 小步快跑实施步骤 + +### Step 0: 准备基础设施(1 天) + +- [ ] 创建 `c_api/` 目录结构(`include/olive/`, `src/`, `tests/`)。 +- [ ] 编写 `ModuleLoader` 类(`c_api/src/module_loader.h/cpp`),支持 POSIX + Windows。 +- [ ] 在 CMake 中新增 `OLIVE_DYNAMIC_MODULES` 选项(默认 OFF)。 +- [ ] 编写最小测试动态库,验证 `ModuleLoader` 可以正确加载和调用。 + +**验收标准**:`ModuleLoader` 可以成功 `dlopen` 一个测试 SO 并调用其中的函数。 + +### Step 1: 将 olivecore 改为 SHARED(1 天) + +- [ ] 修改 `ext/core/CMakeLists.txt`:`STATIC` → `SHARED`,添加 `CXX_VISIBILITY_PRESET hidden`。 +- [ ] 为需要导出的类/函数添加导出宏。 +- [ ] 验证所有平台编译通过。 + +**验收标准**:`olivecore` 编译为 `.so`/`.dylib`/`.dll`,单元测试通过显式加载运行。 + +### Step 2: 编写 core C API(2–3 天) + +- [ ] 编写 `c_api/include/olive/core_api.h`(先只包含最常用的类型:Rational, Color, TimeRange, PixelFormat)。 +- [ ] 编写 `c_api/src/core_api.cpp`,用 C++ 封装现有类的调用,导出纯 C 函数。 +- [ ] **原则**:不改变 `ext/core/` 下的任何现有源文件,只在 `c_api/src/` 中新增封装代码。 +- [ ] 编写单元测试 `tests/c_api/test_core_api.cpp`。 + +**验收标准**: +```cpp +ModuleLoader loader; +loader.Load("core", "./libolivecore.so"); +auto make = loader.GetFunction("core", "olive_rational_make"); +ASSERT_EQ(make(1, 2).num, 1); +``` + +### Step 3: 主进程加载验证(1 天) + +- [ ] 在 `Core::Start()` 中新增代码:尝试显式加载 `libolivecore.so`,若失败则回退到静态链接模式。 +- [ ] 验证主程序启动时 `olivecore` 被正确加载。 + +**验收标准**:主程序日志输出 `Loaded module: core from /path/to/libolivecore.so`。 + +### Step 4: 扩展 C API 覆盖度(按需) + +- [ ] 根据其他模块(codec, node)的需要,逐步在 `core_api.h` 中增加类型(SampleBuffer, VideoParams 等)。 + +--- + +## 5. 风险与回退 + +| 风险 | 对策 | +|---|---| +| `ext/core` 的某些类依赖 Qt 模板,导出 C 接口繁琐 | 优先封装 POD 和简单类,复杂类(如 `SampleBuffer` 的音频重采样)暂时不对外暴露,留在内部使用。 | +| Windows 上 `__declspec(dllexport)` 与 `__attribute__((visibility))` 混用 | 定义统一的 `OLIVE_API` 宏,根据平台自动选择。 | +| 性能担忧:C 封装层增加函数调用开销 | `core` 中的操作(有理数运算)本身极快,C 封装的开销(一次函数调用)可忽略。若发现瓶颈,可将热点路径内联到 C API 头文件中(但保持 ABI 稳定)。 | + +--- + +## 6. 与后续模块的协作 + +`libolivecore.so` 是最底层库,所有其他动态库(`olivecodec`, `olivenode`, `oliverender` 等)都隐式或显式依赖它。 + +- **隐式依赖**:`libolivecodec.so` 在编译时链接 `libolivecore.so`,运行时由操作系统加载器自动解析。 +- **显式依赖**:主进程需要显式加载 `libolivecore.so`,然后才能加载依赖它的上层库(虽然操作系统加载器会自动处理 `DT_NEEDED`,但主进程仍需要显式 `dlopen` 以确保错误处理可控)。 + +**加载顺序**: +```cpp +loader.Load("core", path); // 必须先加载 +loader.Load("codec", path); // 依赖 core,但操作系统会自动解析 +loader.Load("node", path); // 依赖 codec + core +``` diff --git a/docs/zh/modularization-plan/02-olivecodec.md b/docs/zh/modularization-plan/02-olivecodec.md new file mode 100644 index 000000000..21ee449a1 --- /dev/null +++ b/docs/zh/modularization-plan/02-olivecodec.md @@ -0,0 +1,359 @@ +# libolivecodec.so — 编解码库 + +> **依赖**:`libolivecore.so` +> **外部依赖**:FFmpeg (avcodec, avformat, avutil, swscale, swresample, avfilter), OpenImageIO, OpenEXR +> **包含源码**:`app/codec/`, `app/common/` +> **当前状态**:单体 OBJECT 库的一部分 +> **改造难度**:⭐⭐(较简单) + +--- + +## 1. 当前状态分析 + +`app/codec/` 负责媒体文件的读取与写入,`app/common/` 提供通用工具(FFmpeg 辅助、XML 工具、文件操作等)。两者紧密耦合,且 `common/` 被 `codec/` 重度依赖,因此合并为一个动态库。 + +| 组件 | 说明 | +|---|---| +| `decoder.h/cpp` | 解码器抽象基类 | +| `ffmpeg/ffmpegdecoder` / `ffmpegencoder` | FFmpeg 视频/音频解码编码 | +| `oiio/oiiodecoder` / `oiioencoder` | OpenImageIO 图像序列解码编码 | +| `frame.h/cpp` | CPU 帧数据(`FramePtr`) | +| `stream.h` | 媒体流信息 | +| `conformmanager.h/cpp` | 音频格式统一转换 | +| `common/ffmpegutils.h` | FFmpeg 辅助函数 | +| `common/xmlutils.h` | XML 序列化辅助 | +| `common/filefunctions.h` | 文件操作 | + +--- + +## 2. C API 设计 + +### 2.1 头文件:`c_api/include/olive/codec_api.h` + +```c +#ifndef OLIVE_CODEC_API_H +#define OLIVE_CODEC_API_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "core_api.h" + +#define OLIVE_CODEC_API_VERSION 1 + +#ifdef OLIVE_BUILDING_CODEC +# define OLIVE_CODEC_API __attribute__((visibility("default"))) +#else +# define OLIVE_CODEC_API +#endif + +/* ========== 类型前向声明 ========== */ +typedef struct OliveDecoder OliveDecoder; +typedef struct OliveEncoder OliveEncoder; +typedef struct OliveFrame OliveFrame; +typedef struct OliveStream OliveStream; +typedef struct OliveMediaInfo OliveMediaInfo; + +/* ========== API 版本 ========== */ +OLIVE_CODEC_API int olive_codec_api_version(void); + +/* ========== MediaInfo(媒体文件信息) ========== */ +OLIVE_CODEC_API OliveMediaInfo* olive_media_info_probe(const char* filename); +OLIVE_CODEC_API void olive_media_info_destroy(OliveMediaInfo* info); + +OLIVE_CODEC_API int olive_media_info_stream_count(OliveMediaInfo* info); +OLIVE_CODEC_API int olive_media_info_stream_type(OliveMediaInfo* info, int stream_index); // 0=video, 1=audio, 2=subtitle +OLIVE_CODEC_API OliveVideoParams olive_media_info_video_params(OliveMediaInfo* info, int stream_index); +OLIVE_CODEC_API OliveAudioParams olive_media_info_audio_params(OliveMediaInfo* info, int stream_index); +OLIVE_CODEC_API OliveRational olive_media_info_duration(OliveMediaInfo* info); +OLIVE_CODEC_API const char* olive_media_info_codec_name(OliveMediaInfo* info, int stream_index); + +/* ========== Decoder ========== */ +OLIVE_CODEC_API OliveDecoder* olive_decoder_create(const char* codec_id); +OLIVE_CODEC_API void olive_decoder_destroy(OliveDecoder* decoder); + +OLIVE_CODEC_API int olive_decoder_open(OliveDecoder* decoder, + const char* filename, + int stream_index); +OLIVE_CODEC_API void olive_decoder_close(OliveDecoder* decoder); + +// 视频解码:解码指定时间的帧 +OLIVE_CODEC_API int olive_decoder_decode_video(OliveDecoder* decoder, + OliveRational time, + OliveFrame** out_frame); + +// 音频解码:解码指定时间范围的采样 +OLIVE_CODEC_API int olive_decoder_decode_audio(OliveDecoder* decoder, + OliveRational start, + OliveRational duration, + OliveSampleBuffer** out_buffer); + +// 获取解码器支持的流参数 +OLIVE_CODEC_API OliveVideoParams olive_decoder_video_params(OliveDecoder* decoder); +OLIVE_CODEC_API OliveAudioParams olive_decoder_audio_params(OliveDecoder* decoder); + +/* ========== Frame ========== */ +OLIVE_CODEC_API void olive_frame_destroy(OliveFrame* frame); + +OLIVE_CODEC_API int olive_frame_width(OliveFrame* frame); +OLIVE_CODEC_API int olive_frame_height(OliveFrame* frame); +OLIVE_CODEC_API int olive_frame_linesize(OliveFrame* frame); +OLIVE_CODEC_API OlivePixelFormat olive_frame_format(OliveFrame* frame); +OLIVE_CODEC_API void* olive_frame_data(OliveFrame* frame); // 指向像素数据的指针 +OLIVE_CODEC_API size_t olive_frame_data_size(OliveFrame* frame); + +// 将 Frame 转换为指定的像素格式(内部使用 swscale) +OLIVE_CODEC_API int olive_frame_convert(OliveFrame* src, + OlivePixelFormat dst_format, + OliveFrame** out_frame); + +// 从原始数据创建 Frame(用于渲染结果回传) +OLIVE_CODEC_API OliveFrame* olive_frame_from_data(int width, + int height, + OlivePixelFormat format, + const void* data, + int linesize); + +/* ========== Encoder ========== */ +OLIVE_CODEC_API OliveEncoder* olive_encoder_create(const char* format_name, + const char* codec_name); +OLIVE_CODEC_API void olive_encoder_destroy(OliveEncoder* encoder); + +OLIVE_CODEC_API int olive_encoder_open(OliveEncoder* encoder, + const char* filename, + OliveVideoParams vparams, + OliveAudioParams aparams); +OLIVE_CODEC_API int olive_encoder_write_video(OliveEncoder* encoder, OliveFrame* frame); +OLIVE_CODEC_API int olive_encoder_write_audio(OliveEncoder* encoder, OliveSampleBuffer* buffer); +OLIVE_CODEC_API int olive_encoder_close(OliveEncoder* encoder); + +/* ========== Conform(音频格式统一) ========== */ +OLIVE_CODEC_API int olive_audio_conform(const char* input_filename, + const char* output_filename, + OliveAudioParams target_params); + +#ifdef __cplusplus +} +#endif + +#endif // OLIVE_CODEC_API_H +``` + +### 2.2 实现要点 + +```cpp +// c_api/src/codec_api.cpp + +#include "olive/codec_api.h" +#include "codec/decoder.h" +#include "codec/ffmpeg/ffmpegdecoder.h" +#include "codec/frame.h" +#include "codec/encoder.h" +#include "codec/ffmpeg/ffmpegencoder.h" +#include "common/ffmpegutils.h" + +struct OliveDecoder { + olive::DecoderPtr impl; +}; + +struct OliveFrame { + olive::FramePtr impl; +}; + +// ... 其他不透明指针定义 ... + +extern "C" { + +OliveDecoder* olive_decoder_create(const char* codec_id) { + try { + auto* d = new OliveDecoder(); + // 根据 codec_id 创建对应的解码器实例 + // 若 codec_id 为 nullptr 或 "auto",则自动探测 + d->impl = olive::Decoder::CreateFromID(QString::fromUtf8(codec_id)); + return d; + } catch (...) { + return nullptr; + } +} + +void olive_decoder_destroy(OliveDecoder* decoder) { + delete decoder; +} + +int olive_decoder_open(OliveDecoder* decoder, const char* filename, int stream_index) { + if (!decoder || !filename) return OLIVE_ERROR_INVALID; + try { + bool ok = decoder->impl->Open(QString::fromUtf8(filename), stream_index); + return ok ? OLIVE_OK : OLIVE_ERROR_GENERIC; + } catch (...) { + return OLIVE_ERROR_GENERIC; + } +} + +int olive_decoder_decode_video(OliveDecoder* decoder, OliveRational time, OliveFrame** out_frame) { + if (!decoder || !out_frame) return OLIVE_ERROR_INVALID; + try { + olive::Rational t(time.num, time.den); + olive::FramePtr frame = decoder->impl->RetrieveVideo(t); + if (!frame) return OLIVE_ERROR_NOT_FOUND; + auto* f = new OliveFrame(); + f->impl = frame; + *out_frame = f; + return OLIVE_OK; + } catch (...) { + return OLIVE_ERROR_GENERIC; + } +} + +// ... 其他函数类似封装 ... + +} // extern "C" +``` + +--- + +## 3. CMake 改造 + +### 3.1 `app/codec/CMakeLists.txt` + +```cmake +# 收集 codec/ 内部源文件 +set(CODEC_INTERNAL_SOURCES + decoder.cpp decoder.h + encoder.cpp encoder.h + frame.cpp frame.h + stream.cpp stream.h + conformmanager.cpp conformmanager.h + ffmpeg/ffmpegdecoder.cpp ffmpeg/ffmpegdecoder.h + ffmpeg/ffmpegencoder.cpp ffmpeg/ffmpegencoder.h + oiio/oiiodecoder.cpp oiio/oiiodecoder.h + oiio/oiioencoder.cpp oiio/oiioencoder.h + # ... +) + +# 收集 common/ 源文件(并入 codec 库) +set(COMMON_INTERNAL_SOURCES + ../common/ffmpegutils.cpp ../common/ffmpegutils.h + ../common/xmlutils.cpp ../common/xmlutils.h + ../common/filefunctions.cpp ../common/filefunctions.h + ../common/qtutils.cpp ../common/qtutils.h + ../common/debug.cpp ../common/debug.h + # ... +) + +# C API 封装层 +set(CODEC_API_SOURCES + ${CMAKE_SOURCE_DIR}/c_api/src/codec_api.cpp +) + +add_library(olivecodec SHARED + ${CODEC_INTERNAL_SOURCES} + ${COMMON_INTERNAL_SOURCES} + ${CODEC_API_SOURCES} +) + +target_compile_definitions(olivecodec PRIVATE OLIVE_BUILDING_CODEC) + +target_include_directories(olivecodec + PRIVATE + ${CMAKE_SOURCE_DIR}/app + ${CMAKE_SOURCE_DIR}/c_api/include + PUBLIC + $ +) + +target_link_libraries(olivecodec + PUBLIC + olivecore + FFMPEG::avcodec + FFMPEG::avformat + FFMPEG::avutil + FFMPEG::swscale + FFMPEG::swresample + FFMPEG::avfilter + ${OIIO_LIBRARIES} + ${OPENEXR_LIBRARIES} +) + +set_target_properties(olivecodec PROPERTIES + CXX_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN YES +) + +install(TARGETS olivecodec DESTINATION lib) +install(FILES ${CMAKE_SOURCE_DIR}/c_api/include/olive/codec_api.h DESTINATION include/olive) +``` + +--- + +## 4. 小步快跑实施步骤 + +### Step 0: 分析依赖关系(半天) + +- [ ] 梳理 `app/codec/` 和 `app/common/` 中的所有文件。 +- [ ] 确认 `common/` 中不包含任何 Qt GUI 相关代码(若有,移出到 `liboliveui.so`)。 +- [ ] 列出 `codec/` 和 `common/` 对 `node/` 的反向依赖(理论上不应有,若有需先解耦)。 + +**验收标准**:确认 `codec/` + `common/` 的依赖图只包含 `ext/core/`、FFmpeg、OIIO、Qt::Core。 + +### Step 1: 合并 common 到 codec 库(1 天) + +- [ ] 修改 `app/codec/CMakeLists.txt`,将 `app/common/` 的源文件并入。 +- [ ] 将 `app/common/CMakeLists.txt` 改为空文件(或删除,保留 add_subdirectory 空壳以兼容)。 +- [ ] 确保编译产物为 `libolivecodec.so`(或 `.dylib`/`.dll`)。 + +**验收标准**:`libolivecodec.so` 编译成功,原有单元测试通过。 + +### Step 2: 设计 C API 的最小子集(1 天) + +- [ ] 先只实现渲染流程**最必需**的接口: + - `olive_decoder_create/open/destroy` + - `olive_decoder_decode_video` + - `olive_frame_width/height/data/destroy` + - `olive_media_info_probe` +- [ ] 暂不实现:Encoder、Conform、音频解码的复杂场景。 + +**验收标准**:可以用 C API 打开一个视频文件并解码出一帧。 + +### Step 3: 编写 C API 实现(2 天) + +- [ ] 编写 `c_api/include/olive/codec_api.h`(最小子集)。 +- [ ] 编写 `c_api/src/codec_api.cpp`。 +- [ ] 每个函数用 `try/catch(...)` 包裹,异常转换为 `OLIVE_ERROR_GENERIC`。 +- [ ] 在 `c_api/tests/test_codec_api.cpp` 中编写测试。 + +**验收标准**: +```cpp +OliveDecoder* d = olive_decoder_create(nullptr); +olive_decoder_open(d, "test.mp4", 0); +OliveFrame* f = nullptr; +olive_decoder_decode_video(d, olive_rational_make(0, 1), &f); +assert(f != nullptr); +assert(olive_frame_width(f) > 0); +olive_frame_destroy(f); +olive_decoder_destroy(d); +``` + +### Step 4: 显式加载验证(1 天) + +- [ ] 在主进程中通过 `ModuleLoader` 加载 `libolivecodec.so`。 +- [ ] 验证可以成功解码测试视频并显示帧尺寸。 + +**验收标准**:主进程日志输出成功加载 `codec`,并能获取测试视频的宽和高。 + +### Step 5: 扩展 C API(按需迭代) + +- [ ] 根据 `node/` 和 `render/` 的需要,逐步增加 Encoder、音频解码、Conform 等接口。 +- [ ] 每次增加后运行编解码单元测试。 + +--- + +## 5. 风险与回退 + +| 风险 | 对策 | +|---|---| +| `FramePtr` 是 `std::shared_ptr`,C API 中需要管理引用计数 | `OliveFrame` 不透明指针内部持有 `std::shared_ptr`,销毁时自动减引用计数。若需要延长生命周期,可新增 `olive_frame_ref/unref`。 | +| `Decoder::Open` 是异步/多线程的 | C API 层面先做同步封装(等待 Open 完成)。若性能不满足,后续可新增异步回调接口。 | +| `common/` 中的 `xmlutils.h` 依赖 Qt XML | 这是允许的(Qt::Core 的一部分),但需注意 `common/` 中若混入 GUI 相关代码(如 `QMessageBox`),必须移出。 | +| FFmpeg 的 `AVFrame` 到 `olive::Frame` 转换在 C API 边界 | 保持内部实现不变,C API 只操作 `olive::Frame`。 | diff --git a/docs/zh/modularization-plan/03-oliveplugin.md b/docs/zh/modularization-plan/03-oliveplugin.md new file mode 100644 index 000000000..dd896dd90 --- /dev/null +++ b/docs/zh/modularization-plan/03-oliveplugin.md @@ -0,0 +1,305 @@ +# liboliveplugin.so — OFX 插件宿主支持 + +> **依赖**:`libolivecore.so` +> **外部依赖**:`third_party/openfx/HostSupport`(`OfxHost` 静态库),expat,Qt::Core +> **包含源码**:`app/pluginSupport/` +> **当前状态**:单体 OBJECT 库的一部分,通过 `target_link_libraries(olive-editor PUBLIC OfxHost)` 隐式链接 +> **改造难度**:⭐⭐(较简单,接口相对独立) + +--- + +## 1. 当前状态分析 + +`app/pluginSupport/` 实现 OFX(OpenFX)标准的 Host 端接口,使 Olive 能够加载第三方插件(如 Sapphire、Neat Video 等)。 + +| 组件 | 说明 | +|---|---| +| `OliveHost` | OFX Host 接口主实现 | +| `PluginInstance` | 单个插件实例管理 | +| `OliveClip` / `OliveClipInstance` | OFX Clip 接口封装 | +| `OliveParam` / `OliveParamInstance` | OFX 参数接口封装 | +| `node/plugins/PluginNode` | OFX 插件在节点图中的封装节点 | + +**特点**: +- `pluginSupport/` 与 `node/plugins/PluginNode` 存在双向依赖。 +- OFX Host 支持库(`third_party/openfx/HostSupport`)是第三方代码,不应修改其接口。 +- 插件渲染需要 OpenGL 上下文,因此 `liboliveplugin.so` 需要与渲染层协作。 + +**决策**:由于 `PluginNode` 继承自 `Node`(在 `libolivenode.so` 中),`PluginNode` 应留在 `libolivenode.so` 中。`liboliveplugin.so` 只包含纯 Host 支持代码(`pluginSupport/`),通过 C API 向 `libolivenode.so` 暴露插件加载和管理能力。 + +--- + +## 2. C API 设计 + +### 2.1 头文件:`c_api/include/olive/plugin_api.h` + +```c +#ifndef OLIVE_PLUGIN_API_H +#define OLIVE_PLUGIN_API_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "core_api.h" + +#define OLIVE_PLUGIN_API_VERSION 1 + +#ifdef OLIVE_BUILDING_PLUGIN +# define OLIVE_PLUGIN_API __attribute__((visibility("default"))) +#else +# define OLIVE_PLUGIN_API +#endif + +/* ========== 类型前向声明 ========== */ +typedef struct OlivePluginHost OlivePluginHost; +typedef struct OlivePlugin OlivePlugin; +typedef struct OlivePluginInstance OlivePluginInstance; +typedef struct OlivePluginParam OlivePluginParam; + +/* ========== API 版本 ========== */ +OLIVE_PLUGIN_API int olive_plugin_api_version(void); + +/* ========== Host 生命周期 ========== */ +OLIVE_PLUGIN_API OlivePluginHost* olive_plugin_host_create(void); +OLIVE_PLUGIN_API void olive_plugin_host_destroy(OlivePluginHost* host); + +// 设置插件搜索路径(可多次调用添加多个路径) +OLIVE_PLUGIN_API int olive_plugin_host_add_path(OlivePluginHost* host, const char* path); + +// 扫描所有路径,加载可用插件 +OLIVE_PLUGIN_API int olive_plugin_host_rescan(OlivePluginHost* host); + +// 获取已加载插件数量 +OLIVE_PLUGIN_API int olive_plugin_host_plugin_count(OlivePluginHost* host); + +// 获取指定索引的插件 +OLIVE_PLUGIN_API OlivePlugin* olive_plugin_host_get_plugin(OlivePluginHost* host, int index); + +/* ========== Plugin 信息 ========== */ +OLIVE_PLUGIN_API const char* olive_plugin_get_id(OlivePlugin* plugin); +OLIVE_PLUGIN_API const char* olive_plugin_get_name(OlivePlugin* plugin); +OLIVE_PLUGIN_API const char* olive_plugin_get_group(OlivePlugin* plugin); // 分类,如 "Filter/Blur" +OLIVE_PLUGIN_API int olive_plugin_is_hardware_rendering_supported(OlivePlugin* plugin); + +/* ========== Plugin Instance ========== */ +OLIVE_PLUGIN_API OlivePluginInstance* olive_plugin_instance_create(OlivePlugin* plugin, + int width, + int height); +OLIVE_PLUGIN_API void olive_plugin_instance_destroy(OlivePluginInstance* instance); + +// 参数操作(通过字符串名称) +OLIVE_PLUGIN_API int olive_plugin_instance_set_param_int(OlivePluginInstance* instance, + const char* param_name, + int value); +OLIVE_PLUGIN_API int olive_plugin_instance_set_param_double(OlivePluginInstance* instance, + const char* param_name, + double value); +OLIVE_PLUGIN_API int olive_plugin_instance_set_param_string(OlivePluginInstance* instance, + const char* param_name, + const char* value); + +// 渲染一帧(输入/输出均为 Frame) +OLIVE_PLUGIN_API int olive_plugin_instance_render(OlivePluginInstance* instance, + OliveRational time, + OliveFrame* input_frame, + OliveFrame** output_frame); + +/* ========== Param 枚举(用于 UI 构建控件) ========== */ +OLIVE_PLUGIN_API int olive_plugin_instance_param_count(OlivePluginInstance* instance); +OLIVE_PLUGIN_API OlivePluginParam* olive_plugin_instance_get_param(OlivePluginInstance* instance, + int index); + +OLIVE_PLUGIN_API const char* olive_plugin_param_get_name(OlivePluginParam* param); +OLIVE_PLUGIN_API const char* olive_plugin_param_get_label(OlivePluginParam* param); +OLIVE_PLUGIN_API int olive_plugin_param_get_type(OlivePluginParam* param); // 0=int, 1=double, 2=string, 3=bool, 4=color, 5=choice +OLIVE_PLUGIN_API int olive_plugin_param_get_int_min(OlivePluginParam* param); +OLIVE_PLUGIN_API int olive_plugin_param_get_int_max(OlivePluginParam* param); +OLIVE_PLUGIN_API double olive_plugin_param_get_double_min(OlivePluginParam* param); +OLIVE_PLUGIN_API double olive_plugin_param_get_double_max(OlivePluginParam* param); +OLIVE_PLUGIN_API int olive_plugin_param_get_choice_count(OlivePluginParam* param); +OLIVE_PLUGIN_API const char* olive_plugin_param_get_choice_label(OlivePluginParam* param, int index); + +#ifdef __cplusplus +} +#endif + +#endif // OLIVE_PLUGIN_API_H +``` + +### 2.2 实现要点 + +```cpp +// c_api/src/plugin_api.cpp + +#include "olive/plugin_api.h" +#include "pluginSupport/olivehost.h" +#include "pluginSupport/plugininstance.h" +#include "pluginSupport/oliveparam.h" +#include "codec/frame.h" + +struct OlivePluginHost { + olive::OliveHost* impl; +}; + +struct OlivePlugin { + olive::Plugin* impl; // 内部插件描述对象 +}; + +struct OlivePluginInstance { + olive::PluginInstance* impl; +}; + +// ... + +extern "C" { + +OlivePluginHost* olive_plugin_host_create(void) { + try { + auto* h = new OlivePluginHost(); + h->impl = new olive::OliveHost(); + return h; + } catch (...) { + return nullptr; + } +} + +void olive_plugin_host_destroy(OlivePluginHost* host) { + if (host) { + delete host->impl; + delete host; + } +} + +int olive_plugin_host_add_path(OlivePluginHost* host, const char* path) { + if (!host || !path) return OLIVE_ERROR_INVALID; + try { + host->impl->AddPath(QString::fromUtf8(path)); + return OLIVE_OK; + } catch (...) { + return OLIVE_ERROR_GENERIC; + } +} + +int olive_plugin_host_rescan(OlivePluginHost* host) { + if (!host) return OLIVE_ERROR_INVALID; + try { + host->impl->RescanPlugins(); + return OLIVE_OK; + } catch (...) { + return OLIVE_ERROR_GENERIC; + } +} + +// ... 其他封装类似 ... + +} // extern "C" +``` + +--- + +## 3. CMake 改造 + +### 3.1 `app/pluginSupport/CMakeLists.txt` + +```cmake +set(PLUGIN_INTERNAL_SOURCES + olivehost.cpp olivehost.h + plugininstance.cpp plugininstance.h + oliveclip.cpp oliveclip.h + oliveclipinstance.cpp oliveclipinstance.h + oliveparam.cpp oliveparam.h + oliveparaminstance.cpp oliveparaminstance.h + # ... +) + +set(PLUGIN_API_SOURCES + ${CMAKE_SOURCE_DIR}/c_api/src/plugin_api.cpp +) + +add_library(oliveplugin SHARED + ${PLUGIN_INTERNAL_SOURCES} + ${PLUGIN_API_SOURCES} +) + +target_compile_definitions(oliveplugin PRIVATE OLIVE_BUILDING_PLUGIN) + +target_include_directories(oliveplugin + PRIVATE + ${CMAKE_SOURCE_DIR}/app + ${CMAKE_SOURCE_DIR}/third_party/openfx/include + ${CMAKE_SOURCE_DIR}/third_party/openfx/HostSupport/include + ${CMAKE_SOURCE_DIR}/c_api/include + PUBLIC + $ +) + +target_link_libraries(oliveplugin + PUBLIC + olivecore + OfxHost # third_party/openfx/HostSupport 构建的目标 + EXPAT::EXPAT + Qt${QT_VERSION_MAJOR}::Core +) + +set_target_properties(oliveplugin PROPERTIES + CXX_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN YES +) + +install(TARGETS oliveplugin DESTINATION lib) +install(FILES ${CMAKE_SOURCE_DIR}/c_api/include/olive/plugin_api.h DESTINATION include/olive) +``` + +--- + +## 4. 小步快跑实施步骤 + +### Step 0: 隔离 PluginNode(1 天) + +- [ ] 将 `app/node/plugins/PluginNode` 移动到 `app/node/` 下(或保持原位,但确保它编译进 `libolivenode.so` 而非 `liboliveplugin.so`)。 +- [ ] 确认 `PluginNode` 对 `pluginSupport/` 的依赖方向:PluginNode 使用 pluginSupport 的类,而非相反。 + +**验收标准**:`liboliveplugin.so` 编译时不包含任何 `node/` 下的源文件。 + +### Step 1: 构建 liboliveplugin.so(1 天) + +- [ ] 创建 `app/pluginSupport/CMakeLists.txt`(若尚无)。 +- [ ] 将 `pluginSupport/` 的源文件从主 OBJECT 库中移出,单独构建为 `oliveplugin SHARED`。 +- [ ] 确保 `OfxHost` 静态库先被构建(`third_party/openfx/HostSupport`)。 + +**验收标准**:`liboliveplugin.so` 编译成功,能通过 `dlsym` 找到 `olive_plugin_api_version`。 + +### Step 2: 最小 C API(2 天) + +- [ ] 先实现最必需的接口: + - `olive_plugin_host_create/destroy/add_path/rescan` + - `olive_plugin_host_plugin_count/get_plugin` + - `olive_plugin_get_id/name` +- [ ] 暂不实现:渲染接口(`olive_plugin_instance_render`)、参数枚举。 + +**验收标准**:主进程可以扫描 OFX 插件目录并列出所有插件名称。 + +### Step 3: 扩展渲染接口(2 天) + +- [ ] 实现 `olive_plugin_instance_create/destroy`。 +- [ ] 实现 `olive_plugin_instance_render`(输入输出 `OliveFrame*`)。 +- [ ] 此步骤需要 `libolivecodec.so` 的 `OliveFrame` 定义已就绪。 + +**验收标准**:可以创建一个 OFX 插件实例,传入一帧,获取处理后的一帧。 + +### Step 4: 参数枚举(2 天) + +- [ ] 实现参数枚举接口,使 UI 层可以通过 C API 自动构建参数控件。 +- [ ] 编写测试:加载一个已知插件(如 OFX 示例插件),验证参数数量与类型正确。 + +--- + +## 5. 风险与回退 + +| 风险 | 对策 | +|---|---| +| `OfxHost` 静态库中的符号与动态库导出冲突 | `OfxHost` 保持静态链接进 `liboliveplugin.so`,其符号不对外导出(`hidden` 可见性)。 | +| OFX 插件需要 OpenGL 上下文 | 渲染接口 `olive_plugin_instance_render` 需要传入或绑定 GL 上下文。在"用完即弃"的渲染子进程模型中,这天然解决:子进程自己创建 GL 上下文,插件在其上渲染。 | +| `PluginNode` 需要 `PluginInstance` 的 C++ 类 | `PluginNode` 在 `libolivenode.so` 内部,可以直接包含 `pluginSupport/` 的 C++ 头文件(因为 node 库可以在编译时访问 pluginSupport 源码)。只有跨库边界才需要 C API。 | +| OFX 插件多实例状态管理复杂 | C API 中每个 `OlivePluginInstance*` 对应一个独立的 OFX 实例句柄,状态完全隔离。 | diff --git a/docs/zh/modularization-plan/04-oliveaudio.md b/docs/zh/modularization-plan/04-oliveaudio.md new file mode 100644 index 000000000..bb4100fab --- /dev/null +++ b/docs/zh/modularization-plan/04-oliveaudio.md @@ -0,0 +1,267 @@ +# liboliveaudio.so — 音频播放与处理 + +> **依赖**:`libolivecore.so` +> **外部依赖**:PortAudio,Qt::Core +> **包含源码**:`app/audio/` +> **当前状态**:单体 OBJECT 库的一部分 +> **改造难度**:⭐⭐(较简单) + +--- + +## 1. 当前状态分析 + +`app/audio/` 负责音频播放管理、音频处理管线和波形可视化数据。 + +| 组件 | 说明 | +|---|---| +| `audiomanager.h/cpp` | 音频播放管理器(单例),基于 PortAudio | +| `audioprocessor.h/cpp` | 音频处理管线 | +| `audiovisualwaveform.h/cpp` | 音频波形数据(用于 UI 显示) | +| `audiohybriddevice.h/cpp` | 音频混合设备 | + +**特点**: +- 相对独立,不直接依赖 `node/` 或 `render/`(通过回调或数据缓冲区交互)。 +- `AudioManager` 是单例,C API 中需要妥善处理单例的生命周期。 +- 音频数据量较小,实时性要求高。 + +--- + +## 2. C API 设计 + +### 2.1 头文件:`c_api/include/olive/audio_api.h` + +```c +#ifndef OLIVE_AUDIO_API_H +#define OLIVE_AUDIO_API_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "core_api.h" + +#define OLIVE_AUDIO_API_VERSION 1 + +#ifdef OLIVE_BUILDING_AUDIO +# define OLIVE_AUDIO_API __attribute__((visibility("default"))) +#else +# define OLIVE_AUDIO_API +#endif + +/* ========== 类型前向声明 ========== */ +typedef struct OliveAudioManager OliveAudioManager; +typedef struct OliveAudioProcessor OliveAudioProcessor; +typedef struct OliveAudioWaveform OliveAudioWaveform; + +/* ========== API 版本 ========== */ +OLIVE_AUDIO_API int olive_audio_api_version(void); + +/* ========== AudioManager(播放控制) ========== */ +OLIVE_AUDIO_API OliveAudioManager* olive_audio_manager_get_instance(void); +OLIVE_AUDIO_API void olive_audio_manager_release_instance(OliveAudioManager* mgr); + +OLIVE_AUDIO_API int olive_audio_manager_init(OliveAudioManager* mgr, OliveAudioParams params); +OLIVE_AUDIO_API void olive_audio_manager_shutdown(OliveAudioManager* mgr); + +// 播放控制 +OLIVE_AUDIO_API int olive_audio_manager_play(OliveAudioManager* mgr); +OLIVE_AUDIO_API int olive_audio_manager_pause(OliveAudioManager* mgr); +OLIVE_AUDIO_API int olive_audio_manager_stop(OliveAudioManager* mgr); +OLIVE_AUDIO_API int olive_audio_manager_is_playing(OliveAudioManager* mgr); + +// 推入待播放的音频缓冲区(主进程渲染后推入) +OLIVE_AUDIO_API int olive_audio_manager_push_buffer(OliveAudioManager* mgr, + OliveSampleBuffer* buffer); + +// 获取当前播放时间 +OLIVE_AUDIO_API OliveRational olive_audio_manager_get_playback_time(OliveAudioManager* mgr); + +/* ========== AudioProcessor(处理管线) ========== */ +OLIVE_AUDIO_API OliveAudioProcessor* olive_audio_processor_create(OliveAudioParams params); +OLIVE_AUDIO_API void olive_audio_processor_destroy(OliveAudioProcessor* proc); + +// 处理一帧音频(应用音量、声像等) +OLIVE_AUDIO_API int olive_audio_processor_process(OliveAudioProcessor* proc, + OliveSampleBuffer* input, + OliveSampleBuffer** output); + +/* ========== AudioWaveform(波形数据) ========== */ +OLIVE_AUDIO_API OliveAudioWaveform* olive_audio_waveform_create(OliveAudioParams params, + OliveRational duration); +OLIVE_AUDIO_API void olive_audio_waveform_destroy(OliveAudioWaveform* wf); + +// 从采样缓冲区生成波形数据 +OLIVE_AUDIO_API int olive_audio_waveform_generate(OliveAudioWaveform* wf, + OliveSampleBuffer* buffer, + OliveRational start_time); + +// 获取指定时间点的波形峰值(用于 UI 绘制) +OLIVE_AUDIO_API float olive_audio_waveform_get_peak(OliveAudioWaveform* wf, + OliveRational time, + int channel); + +// 获取波形数据数组(用于批量绘制) +OLIVE_AUDIO_API const float* olive_audio_waveform_get_peaks(OliveAudioWaveform* wf, + int channel, + int* out_count); + +#ifdef __cplusplus +} +#endif + +#endif // OLIVE_AUDIO_API_H +``` + +### 2.2 实现要点 + +```cpp +// c_api/src/audio_api.cpp + +#include "olive/audio_api.h" +#include "audio/audiomanager.h" +#include "audio/audioprocessor.h" +#include "audio/audiovisualwaveform.h" + +struct OliveAudioManager { + // AudioManager 是单例,此处不持有所有权,只作为句柄 + olive::AudioManager* impl; +}; + +extern "C" { + +OliveAudioManager* olive_audio_manager_get_instance(void) { + static OliveAudioManager mgr; + mgr.impl = olive::AudioManager::instance(); + return &mgr; +} + +void olive_audio_manager_release_instance(OliveAudioManager* mgr) { + // 单例不在这里销毁 + (void)mgr; +} + +int olive_audio_manager_init(OliveAudioManager* mgr, OliveAudioParams params) { + if (!mgr || !mgr->impl) return OLIVE_ERROR_INVALID; + try { + olive::AudioParams cpp_params; + cpp_params.set_sample_rate(params.sample_rate); + cpp_params.set_channel_layout(params.channel_layout); + // ... 转换 format ... + mgr->impl->SetParameters(cpp_params); + return OLIVE_OK; + } catch (...) { + return OLIVE_ERROR_GENERIC; + } +} + +int olive_audio_manager_push_buffer(OliveAudioManager* mgr, OliveSampleBuffer* buffer) { + if (!mgr || !mgr->impl || !buffer) return OLIVE_ERROR_INVALID; + try { + mgr->impl->PushBuffer(buffer->impl); + return OLIVE_OK; + } catch (...) { + return OLIVE_ERROR_GENERIC; + } +} + +// ... 其他函数类似封装 ... + +} // extern "C" +``` + +--- + +## 3. CMake 改造 + +```cmake +# app/audio/CMakeLists.txt + +set(AUDIO_INTERNAL_SOURCES + audiomanager.cpp audiomanager.h + audioprocessor.cpp audioprocessor.h + audiovisualwaveform.cpp audiovisualwaveform.h + audiohybriddevice.cpp audiohybriddevice.h +) + +set(AUDIO_API_SOURCES + ${CMAKE_SOURCE_DIR}/c_api/src/audio_api.cpp +) + +add_library(oliveaudio SHARED + ${AUDIO_INTERNAL_SOURCES} + ${AUDIO_API_SOURCES} +) + +target_compile_definitions(oliveaudio PRIVATE OLIVE_BUILDING_AUDIO) + +target_include_directories(oliveaudio + PRIVATE + ${CMAKE_SOURCE_DIR}/app + ${CMAKE_SOURCE_DIR}/c_api/include + PUBLIC + $ +) + +target_link_libraries(oliveaudio + PUBLIC + olivecore + ${PORTAUDIO_LIBRARIES} + Qt${QT_VERSION_MAJOR}::Core +) + +set_target_properties(oliveaudio PROPERTIES + CXX_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN YES +) + +install(TARGETS oliveaudio DESTINATION lib) +install(FILES ${CMAKE_SOURCE_DIR}/c_api/include/olive/audio_api.h DESTINATION include/olive) +``` + +--- + +## 4. 小步快跑实施步骤 + +### Step 0: 分析音频数据流(半天) + +- [ ] 梳理 `AudioManager` 的数据流:谁调用 `PushBuffer`?谁消费? +- [ ] 确认 `audio/` 对 `node/` 的依赖情况(`AudioProcessor` 是否直接操作 `Node`?)。 + +**验收标准**:确认 `audio/` 可以独立于 `node/` 编译(可能只需要 `SampleBuffer` 和 `AudioParams` 类型)。 + +### Step 1: 独立编译 liboliveaudio.so(1 天) + +- [ ] 将 `app/audio/` 从主 OBJECT 库移出,单独构建为 `oliveaudio SHARED`。 +- [ ] 确保 `AudioManager` 单例的初始化顺序正确(Qt 的 `Q_GLOBAL_STATIC` 或延迟初始化)。 + +**验收标准**:`liboliveaudio.so` 编译成功。 + +### Step 2: 最小 C API(1 天) + +- [ ] 实现播放控制:`init`, `play`, `pause`, `stop`。 +- [ ] 实现 `push_buffer`(关键接口,主进程渲染音频后推入播放队列)。 + +**验收标准**:可以通过 C API 初始化音频、播放一段静音缓冲区。 + +### Step 3: 波形数据接口(1 天) + +- [ ] 实现 `olive_audio_waveform_create/generate/get_peak`。 +- [ ] 此接口供 UI 层调用以绘制音频波形。 + +**验收标准**:给定一个 `OliveSampleBuffer*`,可以生成波形并查询任意时间点的峰值。 + +### Step 4: 处理器接口(1 天) + +- [ ] 实现 `olive_audio_processor_create/process`。 +- [ ] 用于节点图中的音频处理链(音量、声像)。 + +--- + +## 5. 风险与回退 + +| 风险 | 对策 | +|---|---| +| `AudioManager` 单例在动态库卸载后仍被引用 | 主进程退出前先停止播放并释放 `AudioManager`,再卸载动态库。 | +| PortAudio 回调线程与主线程的交互 | C API 中 `push_buffer` 是线程安全的(内部用 `QMutex` 保护队列),C API 调用者无需额外同步。 | +| 实时音频延迟要求 | C API 不增加额外拷贝:`push_buffer` 内部直接传递 `SampleBuffer` 的共享指针。 | +| 音频处理需要节点图信息 | `AudioProcessor` 的参数(如音量值)通过 C API 直接设置,不涉及节点图遍历。节点图到音频参数的映射在 `libolivenode.so` 中完成。 | diff --git a/docs/zh/modularization-plan/05-olivenode.md b/docs/zh/modularization-plan/05-olivenode.md new file mode 100644 index 000000000..e70893e98 --- /dev/null +++ b/docs/zh/modularization-plan/05-olivenode.md @@ -0,0 +1,413 @@ +# libolivenode.so — 节点图系统 + +> **依赖**:`libolivecore.so`, `libolivecodec.so` +> **外部依赖**:Qt::Core(QObject, QString, XML) +> **包含源码**:`app/node/`, `app/timeline/`, `app/undo/`, `app/config/` +> **当前状态**:整个项目的**核心枢纽**,被 `render/`, `widget/`, `panel/`, `task/` 等几乎所有上层模块依赖 +> **改造难度**:⭐⭐⭐⭐⭐(最困难,耦合最深) + +--- + +## 1. 当前状态分析 + +`app/node/` 是整个编辑器的**数据与计算模型核心**,采用节点图范式组织所有处理。其特点是: + +1. **被几乎所有模块依赖**:`render/` 遍历节点图,`widget/` 绘制节点连接,`panel/` 包装节点编辑器,`task/` 在导出时读取节点图。 +2. **头文件耦合严重**:`Node.h` 直接 `#include` 了 `codec/frame.h` 和 `render/` 下的多个头文件(缓存类型、作业类型等)。 +3. **Qt 深度集成**:`Node` 继承 `QObject`,使用信号槽、元对象系统、`QVariant`。 +4. **序列化内建**:`ProjectSerializer` 支持将节点图保存/加载为 XML。 + +### 1.1 关键耦合点与解耦策略 + +| 耦合点 | 当前状态 | 解耦策略 | +|---|---|---| +| `Node.h` 包含 `render/rendercache.h` | `Node` 直接操作 `FrameHashCache` | 将缓存失效抽象为虚函数 `InvalidateCache()`,或注入 `NodeCacheCallbacks` 接口指针。移除 `rendercache.h` 的包含。 | +| `Node.h` 包含 `render/job/*.h` | `Node::ProcessShader()` 等虚函数使用具体 Job 类型 | 将 `ProcessShader` 等改为接受 `const void* job_data` + `JobType` 枚举,内部再 `static_cast`。或前向声明 Job 类(若已是不透明指针)。 | +| `Footage`(`node/project/footage/`)依赖 `Decoder` | `Footage` 需要解码器信息预览 | 保留此依赖,`libolivenode.so` 链接 `libolivecodec.so` 是合理的。 | +| `ViewerOutput` 被 UI 直接引用 | `ViewerOutput` 是节点图与 UI 的桥梁 | `ViewerOutput` 保留在 `node/` 中,C API 暴露 `OliveViewerOutput*` 句柄。 | +| `timeline/` 依赖 `node/` | `TimelineMarker` 等引用 `Node` | `timeline/` 并入 `libolivenode.so`,不单独拆分。 | +| `undo/` 依赖 `node/` | `UndoCommand` 操作 `Node` 对象 | `undo/` 并入 `libolivenode.so`。 | + +--- + +## 2. C API 设计 + +### 2.1 头文件:`c_api/include/olive/node_api.h` + +```c +#ifndef OLIVE_NODE_API_H +#define OLIVE_NODE_API_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "core_api.h" + +#define OLIVE_NODE_API_VERSION 1 + +#ifdef OLIVE_BUILDING_NODE +# define OLIVE_NODE_API __attribute__((visibility("default"))) +#else +# define OLIVE_NODE_API +#endif + +/* ========== 不透明类型 ========== */ +typedef struct OliveNodeGraph OliveNodeGraph; +typedef struct OliveNode OliveNode; +typedef struct OliveNodeInput OliveNodeInput; +typedef struct OliveNodeOutput OliveNodeOutput; +typedef struct OliveParam OliveParam; +typedef struct OliveKeyframe OliveKeyframe; +typedef struct OliveProject OliveProject; +typedef struct OliveSequence OliveSequence; +typedef struct OliveTrack OliveTrack; +typedef struct OliveClip OliveClip; +typedef struct OliveViewerOutput OliveViewerOutput; + +/* ========== 枚举 ========== */ +typedef enum { + OLIVE_NODE_TYPE_UNKNOWN = 0, + OLIVE_NODE_TYPE_INPUT, + OLIVE_NODE_TYPE_OUTPUT, + OLIVE_NODE_TYPE_FILTER, + OLIVE_NODE_TYPE_DISTORT, + OLIVE_NODE_TYPE_GENERATOR, + OLIVE_NODE_TYPE_COLOR, + OLIVE_NODE_TYPE_AUDIO, + OLIVE_NODE_TYPE_TRANSITION, + OLIVE_NODE_TYPE_PLUGIN, + OLIVE_NODE_TYPE_GROUP, +} OliveNodeType; + +typedef enum { + OLIVE_PARAM_TYPE_INT = 0, + OLIVE_PARAM_TYPE_DOUBLE, + OLIVE_PARAM_TYPE_STRING, + OLIVE_PARAM_TYPE_RATIONAL, + OLIVE_PARAM_TYPE_COLOR, + OLIVE_PARAM_TYPE_BOOL, + OLIVE_PARAM_TYPE_VECTOR2, + OLIVE_PARAM_TYPE_VECTOR3, + OLIVE_PARAM_TYPE_VECTOR4, +} OliveParamType; + +/* ========== API 版本 ========== */ +OLIVE_NODE_API int olive_node_api_version(void); + +/* ========== NodeGraph ========== */ +OLIVE_NODE_API OliveNodeGraph* olive_node_graph_create(void); +OLIVE_NODE_API void olive_node_graph_destroy(OliveNodeGraph* g); + +// 序列化 +OLIVE_NODE_API int olive_node_graph_load_xml(OliveNodeGraph* g, + const char* xml_data, + size_t xml_len); +OLIVE_NODE_API char* olive_node_graph_save_xml(OliveNodeGraph* g, + size_t* out_len); + +// 节点增删查 +OLIVE_NODE_API OliveNode* olive_node_graph_add_node(OliveNodeGraph* g, + const char* node_type_id, + const char* node_id); +OLIVE_NODE_API int olive_node_graph_remove_node(OliveNodeGraph* g, OliveNode* node); +OLIVE_NODE_API OliveNode* olive_node_graph_find_node(OliveNodeGraph* g, + const char* node_id); +OLIVE_NODE_API int olive_node_graph_node_count(OliveNodeGraph* g); +OLIVE_NODE_API OliveNode* olive_node_graph_get_node(OliveNodeGraph* g, int index); + +// 连接管理 +OLIVE_NODE_API int olive_node_connect(OliveNode* from_node, + int output_index, + OliveNode* to_node, + int input_index); +OLIVE_NODE_API int olive_node_disconnect(OliveNode* node, int input_index); +OLIVE_NODE_API OliveNode* olive_node_get_connected_node(OliveNode* node, + int input_index); + +/* ========== Node 属性 ========== */ +OLIVE_NODE_API const char* olive_node_get_id(OliveNode* node); +OLIVE_NODE_API const char* olive_node_get_label(OliveNode* node); +OLIVE_NODE_API OliveNodeType olive_node_get_type(OliveNode* node); +OLIVE_NODE_API const char* olive_node_get_type_id(OliveNode* node); + +OLIVE_NODE_API int olive_node_input_count(OliveNode* node); +OLIVE_NODE_API int olive_node_output_count(OliveNode* node); + +/* ========== Param 操作 ========== */ +OLIVE_NODE_API int olive_node_param_count(OliveNode* node); +OLIVE_NODE_API OliveParam* olive_node_get_param(OliveNode* node, int index); +OLIVE_NODE_API OliveParam* olive_node_find_param(OliveNode* node, + const char* param_name); + +OLIVE_NODE_API const char* olive_param_get_name(OliveParam* param); +OLIVE_NODE_API OliveParamType olive_param_get_type(OliveParam* param); + +OLIVE_NODE_API int olive_param_set_int(OliveParam* param, int64_t value); +OLIVE_NODE_API int olive_param_set_double(OliveParam* param, double value); +OLIVE_NODE_API int olive_param_set_rational(OliveParam* param, OliveRational value); +OLIVE_NODE_API int olive_param_set_color(OliveParam* param, OliveColor value); +OLIVE_NODE_API int olive_param_set_string(OliveParam* param, const char* value); + +OLIVE_NODE_API int64_t olive_param_get_int(OliveParam* param); +OLIVE_NODE_API double olive_param_get_double(OliveParam* param); +OLIVE_NODE_API OliveRational olive_param_get_rational(OliveParam* param); +OLIVE_NODE_API OliveColor olive_param_get_color(OliveParam* param); + +/* ========== Keyframe ========== */ +OLIVE_NODE_API int olive_param_add_keyframe(OliveParam* param, + OliveRational time, + double value); +OLIVE_NODE_API int olive_param_remove_keyframe(OliveParam* param, + OliveRational time); +OLIVE_NODE_API int olive_param_keyframe_count(OliveParam* param); + +/* ========== Project ========== */ +OLIVE_NODE_API OliveProject* olive_project_create(const char* name); +OLIVE_NODE_API void olive_project_destroy(OliveProject* proj); +OLIVE_NODE_API int olive_project_load_file(OliveProject* proj, const char* filename); +OLIVE_NODE_API int olive_project_save_file(OliveProject* proj, const char* filename); +OLIVE_NODE_API OliveNodeGraph* olive_project_get_graph(OliveProject* proj); + +/* ========== Sequence / Timeline ========== */ +OLIVE_NODE_API OliveSequence* olive_sequence_create(const char* name, + OliveVideoParams vparams, + OliveAudioParams aparams); +OLIVE_NODE_API OliveViewerOutput* olive_sequence_get_viewer_output(OliveSequence* seq); + +/* ========== ViewerOutput(渲染目标) ========== */ +OLIVE_NODE_API const char* olive_viewer_output_get_node_id(OliveViewerOutput* viewer); +OLIVE_NODE_API OliveVideoParams olive_viewer_output_get_video_params(OliveViewerOutput* viewer); +OLIVE_NODE_API OliveAudioParams olive_viewer_output_get_audio_params(OliveViewerOutput* viewer); + +/* ========== Undo ========== */ +typedef struct OliveUndoStack OliveUndoStack; + +OLIVE_NODE_API OliveUndoStack* olive_undo_stack_create(void); +OLIVE_NODE_API void olive_undo_stack_destroy(OliveUndoStack* stack); +OLIVE_NODE_API void olive_undo_stack_push(OliveUndoStack* stack, + const char* action_name, + void* undo_data, + void (*undo_fn)(void*), + void (*redo_fn)(void*), + void (*free_fn)(void*)); +OLIVE_NODE_API int olive_undo_stack_can_undo(OliveUndoStack* stack); +OLIVE_NODE_API int olive_undo_stack_can_redo(OliveUndoStack* stack); +OLIVE_NODE_API void olive_undo_stack_undo(OliveUndoStack* stack); +OLIVE_NODE_API void olive_undo_stack_redo(OliveUndoStack* stack); +OLIVE_NODE_API void olive_undo_stack_clear(OliveUndoStack* stack); + +#ifdef __cplusplus +} +#endif + +#endif // OLIVE_NODE_API_H +``` + +### 2.2 关键解耦实现:NodeCacheCallbacks + +```cpp +// app/node/node.h(改造后,移除 render/ 头文件包含) + +// 前向声明 +class NodeCacheCallbacks; + +class Node : public QObject { + // ... + void SetCacheCallbacks(NodeCacheCallbacks* callbacks); + +protected: + virtual void InvalidateCacheInternal(const TimeRange& range); + +private: + NodeCacheCallbacks* cache_callbacks_ = nullptr; +}; + +// app/node/nodecachecallbacks.h(新增) +class NodeCacheCallbacks { +public: + virtual ~NodeCacheCallbacks() = default; + virtual void InvalidateCache(const QString& cache_id, const TimeRange& range) = 0; + virtual void InvalidateAllCaches() = 0; +}; +``` + +`RenderManager`(在 `liboliverender.so` 中)实现 `NodeCacheCallbacks`,并在创建节点时注入: + +```cpp +class RenderCacheCallbacks : public NodeCacheCallbacks { + void InvalidateCache(const QString& cache_id, const TimeRange& range) override { + // 原有 FrameHashCache 的失效逻辑 + } + // ... +}; +``` + +这样 `Node.h` 不再需要包含 `render/rendercache.h`,编译期依赖被打破。 + +### 2.3 关键解耦实现:RenderJob 虚函数参数抽象 + +当前 `Node` 有虚函数: + +```cpp +// 改造前 +virtual void ProcessShader(TexturePtr destination, const Node* node, const ShaderJob* job); +``` + +改造后: + +```cpp +// app/node/jobtypes.h(新增,只含枚举和基类,无 render/ 依赖) +enum class NodeJobType { + kShader, + kGenerate, + kFootage, + kColorTransform, + kSample, + kCache, +}; + +struct NodeJobData { + NodeJobType type; + void* data; // 实际数据由 render/ 中的具体类解释 +}; + +// app/node/node.h +virtual void ProcessJob(TexturePtr destination, const NodeJobData& job); +``` + +`RenderProcessor`(在 `liboliverender.so` 中)调用时: + +```cpp +ShaderJob job = ...; +NodeJobData data{NodeJobType::kShader, &job}; +node->ProcessJob(destination, data); +``` + +--- + +## 3. CMake 改造 + +```cmake +# app/node/CMakeLists.txt + +set(NODE_INTERNAL_SOURCES + node.cpp node.h + traverser.cpp traverser.h + traverserproxy.cpp traverserproxy.h + nodevalue.cpp nodevalue.h + # ... 所有 node/ 子目录源文件 +) + +set(TIMELINE_SOURCES + ../timeline/timelinecoordinate.cpp ../timeline/timelinecoordinate.h + ../timeline/timelinemarker.cpp ../timeline/timelinemarker.h + ../timeline/timelineworkarea.cpp ../timeline/timelineworkarea.h + ../timeline/undo/*.cpp ../timeline/undo/*.h +) + +set(UNDO_SOURCES + ../undo/undocommand.cpp ../undo/undocommand.h + ../undo/undostack.cpp ../undo/undostack.h +) + +set(CONFIG_SOURCES + ../config/config.cpp ../config/config.h +) + +set(NODE_API_SOURCES + ${CMAKE_SOURCE_DIR}/c_api/src/node_api.cpp +) + +add_library(olivenode SHARED + ${NODE_INTERNAL_SOURCES} + ${TIMELINE_SOURCES} + ${UNDO_SOURCES} + ${CONFIG_SOURCES} + ${NODE_API_SOURCES} +) + +target_compile_definitions(olivenode PRIVATE OLIVE_BUILDING_NODE) + +target_include_directories(olivenode + PRIVATE + ${CMAKE_SOURCE_DIR}/app + ${CMAKE_SOURCE_DIR}/c_api/include + PUBLIC + $ +) + +target_link_libraries(olivenode + PUBLIC + olivecore + olivecodec + Qt${QT_VERSION_MAJOR}::Core +) + +set_target_properties(olivenode PROPERTIES + CXX_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN YES +) + +install(TARGETS olivenode DESTINATION lib) +install(FILES ${CMAKE_SOURCE_DIR}/c_api/include/olive/node_api.h DESTINATION include/olive) +``` + +--- + +## 4. 小步快跑实施步骤 + +### Step 0: 头文件解耦(3–4 天,最关键) + +- [ ] 创建 `app/node/nodecachecallbacks.h`,定义 `NodeCacheCallbacks` 接口。 +- [ ] 修改 `Node.h`:移除 `render/rendercache.h` 包含,添加 `NodeCacheCallbacks*` 成员和 `SetCacheCallbacks()` 方法。 +- [ ] 创建 `app/node/jobtypes.h`,定义 `NodeJobType` 枚举和 `NodeJobData` 结构体。 +- [ ] 修改 `Node.h`:将所有 `ProcessXxx` 虚函数合并为 `ProcessJob(TexturePtr, const NodeJobData&)`,或保留原签名但将参数类型改为前向声明。 +- [ ] 修改 `RenderProcessor`:适配新的 `NodeCacheCallbacks` 和 `NodeJobData`。 + +**验收标准**:`app/node/` 目录可以独立编译,不直接或间接包含 `app/render/` 下的任何头文件。 + +### Step 1: 独立编译 libolivenode.so(1 天) + +- [ ] 将 `node/`, `timeline/`, `undo/`, `config/` 的源文件聚合,构建为 `olivenode SHARED`。 +- [ ] 处理 `node/` 下的 `add_subdirectory` 嵌套,确保所有源文件被正确收集。 + +**验收标准**:`libolivenode.so` 编译成功,`nm -D libolivenode.so | grep olive_node` 能看到导出的 C 符号。 + +### Step 2: 最小 C API(2 天) + +- [ ] 先实现项目级接口: + - `olive_project_create/destroy/load_file/save_file` + - `olive_node_graph_create/destroy/load_xml/save_xml` +- [ ] 这些接口是渲染子进程最需要的:子进程需要加载 XML 节点图并渲染。 + +**验收标准**:可以用 C API 创建一个项目、保存为 XML、再加载回来,内容一致。 + +### Step 3: 节点操作 API(2 天) + +- [ ] 实现节点增删查改:`add_node`, `remove_node`, `find_node`, `connect`, `disconnect`。 +- [ ] 实现参数读写:`set_param_double`, `get_param_double`, `set_param_rational` 等。 + +**验收标准**:可以用 C API 构建一个简单的节点图(如 Generator -> ViewerOutput),并序列化为 XML。 + +### Step 4: Undo API(1 天) + +- [ ] 实现 `olive_undo_stack_*` 系列函数。 +- [ ] C API 的 undo 采用函数指针回调模式,避免暴露 C++ 的 `UndoCommand` 类。 + +### Step 5: ViewerOutput 和 Sequence(1 天) + +- [ ] 实现 `OliveSequence*` 和 `OliveViewerOutput*` 的 C API。 +- [ ] 这是渲染的入口:渲染子进程需要知道哪个 `ViewerOutput` 是输出目标。 + +--- + +## 5. 风险与回退 + +| 风险 | 对策 | +|---|---| +| Node.h 解耦工作量过大,影响面太广 | **分阶段**:第一阶段只做"编译期解耦"(移除 include),不改虚函数签名。若仍然困难,允许 `libolivenode.so` 和 `liboliverender.so` 暂时合并为 `libolive-engine.so`,后续再拆分。 | +| `QObject` 信号槽跨动态库 | Qt 信号槽跨动态库在正确链接 Qt 的情况下工作正常。确保所有含 `Q_OBJECT` 的类在动态库内被 `moc` 处理。 | +| `NodeValueTable` 等模板类难以导出 C 接口 | 不在 C API 中暴露模板类。`NodeTraverser` 的遍历结果(`NodeValueTable`)在 C++ 内部处理,C API 只提供高阶函数如 `olive_node_graph_evaluate_at_time`。 | +| 序列化 XML 格式变更 | C API 中的 `load_xml`/`save_xml` 直接使用现有的 `ProjectSerializer`,XML 格式完全不变,向下兼容。 | +| `Footage` 节点持有 `Decoder` | `Footage` 内部持有 `DecoderPtr`,C API 不暴露 Decoder 细节,只暴露 `Footage` 的文件路径设置/获取。 | diff --git a/docs/zh/modularization-plan/06-oliverender.md b/docs/zh/modularization-plan/06-oliverender.md new file mode 100644 index 000000000..21b83f729 --- /dev/null +++ b/docs/zh/modularization-plan/06-oliverender.md @@ -0,0 +1,406 @@ +# liboliverender.so — 渲染引擎抽象 + +> **依赖**:`libolivecore.so`, `libolivecodec.so`, `libolivenode.so` +> **外部依赖**:Qt::Core, Qt::OpenGL, OpenColorIO, OpenGL +> **包含源码**:`app/render/`(不含 OpenGL 具体后端平台代码的抽象层) +> **当前状态**:单体 OBJECT 库的一部分,直接管理 `RenderThread`、`RenderProcessor`、`OpenGLRenderer` +> **改造难度**:⭐⭐⭐⭐(困难,与 Node 耦合深) + +--- + +## 1. 当前状态分析 + +`app/render/` 是渲染系统的核心,负责将节点图转换为可显示的帧/音频。当前架构: + +| 组件 | 说明 | +|---|---| +| `rendermanager.h/cpp` | 渲染管理单例,管理 `RenderThread` 和缓存 | +| `renderprocessor.h/cpp` | 节点图遍历 + 渲染作业生成(继承 `NodeTraverser`) | +| `renderer.h/cpp` | 渲染器抽象基类(`Renderer`) | +| `opengl/openglrenderer.h/cpp` | OpenGL 渲染后端 | +| `job/*.h` | 各种渲染作业类型(ShaderJob, FootageJob, GenerateJob 等) | +| `previewautocacher.h/cpp` | 预览自动缓存 | +| `rendercache.h/cpp` | 渲染缓存框架 | + +**关键设计决策**: + +本方案中,**实际的 GPU 渲染发生在 `olive-renderer` 子进程中**,不在主进程的 `liboliverender.so` 中。因此 `liboliverender.so` 的角色需要重新定位: + +- **在主进程中**:`liboliverender.so` 提供轻量的 **渲染客户端** 功能:节点图序列化、渲染参数打包、共享内存创建、子进程启动协调。 +- **在子进程中**:`olive-renderer` 可执行文件链接 `liboliverender.so`(或静态链接其代码),执行实际的 `RenderProcessor` + `OpenGLRenderer`。 + +也就是说,`liboliverender.so` 既服务于主进程(IPC 客户端),也服务于子进程(渲染服务端)。但通过编译选项或子目录拆分,可以在主进程中只包含轻量客户端代码。 + +**简化方案**:`liboliverender.so` 包含完整的渲染逻辑(包括 `RenderProcessor` 和 `Renderer` 抽象),但主进程中的 `RenderManager` 不再直接调用它,而是通过 C API 启动 `olive-renderer` 子进程。子进程自身可以静态链接或动态链接 `liboliverender.so` 来执行渲染。 + +--- + +## 2. C API 设计 + +### 2.1 头文件:`c_api/include/olive/render_api.h` + +```c +#ifndef OLIVE_RENDER_API_H +#define OLIVE_RENDER_API_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "core_api.h" +#include "node_api.h" + +#define OLIVE_RENDER_API_VERSION 1 + +#ifdef OLIVE_BUILDING_RENDER +# define OLIVE_RENDER_API __attribute__((visibility("default"))) +#else +# define OLIVE_RENDER_API +#endif + +/* ========== 枚举 ========== */ +typedef enum { + OLIVE_RENDER_MODE_OFFLINE = 0, // 最高质量(导出) + OLIVE_RENDER_MODE_ONLINE, // 实时预览(允许降低精度) +} OliveRenderMode; + +typedef enum { + OLIVE_RENDER_BACKEND_OPENGL = 0, + OLIVE_RENDER_BACKEND_DUMMY, +} OliveRenderBackend; + +/* ========== 不透明类型 ========== */ +typedef struct OliveRenderContext OliveRenderContext; +typedef struct OliveRenderTicket OliveRenderTicket; +typedef struct OliveRenderParams OliveRenderParams; + +/* ========== 渲染参数结构体 ========== */ +typedef struct { + OliveNodeGraph* node_graph; + const char* output_node_id; // 通常为 ViewerOutput 的 ID + OliveRational time; + OliveVideoParams video_params; + OliveAudioParams audio_params; + OliveRenderMode mode; + OliveRenderBackend backend; + const char* color_reference_space; // 可为 nullptr + const char* color_display_space; // 可为 nullptr + OliveSize force_size; // {0,0} 表示不强制 + OlivePixelFormat force_format; // INVALID 表示不强制 +} OliveRenderFrameParams; + +typedef struct { + OliveNodeGraph* node_graph; + const char* output_node_id; + OliveRational start; + OliveRational duration; + OliveAudioParams audio_params; + OliveRenderMode mode; +} OliveRenderAudioParams; + +/* ========== API 版本 ========== */ +OLIVE_RENDER_API int olive_render_api_version(void); + +/* ========== 渲染上下文(用于本地/同进程渲染,或子进程内部) ========== */ +OLIVE_RENDER_API OliveRenderContext* olive_render_context_create(OliveRenderBackend backend); +OLIVE_RENDER_API void olive_render_context_destroy(OliveRenderContext* ctx); +OLIVE_RENDER_API int olive_render_context_init(OliveRenderContext* ctx); + +/* ========== 同步渲染(单帧) ========== */ +// 渲染视频帧,结果写入 out_frame(OliveFrame*,定义在 codec_api.h) +OLIVE_RENDER_API int olive_render_frame_sync(OliveRenderContext* ctx, + const OliveRenderFrameParams* params, + void** out_frame_data, // 原始像素数据,需 olive_core_free + size_t* out_frame_size, + int* out_width, + int* out_height, + OlivePixelFormat* out_format); + +// 渲染音频,结果写入 out_buffer(OliveSampleBuffer*,定义在 core_api.h) +OLIVE_RENDER_API int olive_render_audio_sync(OliveRenderContext* ctx, + const OliveRenderAudioParams* params, + OliveSampleBuffer** out_buffer); + +/* ========== 异步渲染接口(用于子进程模型中的本地队列) ========== */ +OLIVE_RENDER_API OliveRenderTicket* olive_render_frame_async(OliveRenderContext* ctx, + const OliveRenderFrameParams* params); +OLIVE_RENDER_API OliveRenderTicket* olive_render_audio_async(OliveRenderContext* ctx, + const OliveRenderAudioParams* params); +OLIVE_RENDER_API int olive_render_ticket_wait(OliveRenderTicket* ticket, int timeout_ms); +OLIVE_RENDER_API int olive_render_ticket_get_result_frame(OliveRenderTicket* ticket, + void** out_frame_data, + size_t* out_frame_size, + int* out_width, + int* out_height, + OlivePixelFormat* out_format); +OLIVE_RENDER_API void olive_render_ticket_destroy(OliveRenderTicket* ticket); +OLIVE_RENDER_API void olive_render_cancel_ticket(OliveRenderTicket* ticket); + +/* ========== 节点图序列化辅助(供子进程使用) ========== */ +// 将节点图序列化为适合渲染子进程消费的紧凑格式 +OLIVE_RENDER_API char* olive_render_serialize_graph_for_render(OliveNodeGraph* graph, + const char* output_node_id, + size_t* out_len); + +#ifdef __cplusplus +} +#endif + +#endif // OLIVE_RENDER_API_H +``` + +### 2.2 实现要点 + +```cpp +// c_api/src/render_api.cpp + +#include "olive/render_api.h" +#include "render/rendermanager.h" +#include "render/renderprocessor.h" +#include "render/renderer.h" +#include "render/opengl/openglrenderer.h" +#include "render/job/shaderjob.h" +#include "render/job/generatejob.h" +#include "render/job/footagejob.h" +#include "render/job/colortransformjob.h" +#include "render/job/samplejob.h" +#include "codec/frame.h" +#include "node/output/viewer/viewer.h" +#include "node/project.h" + +struct OliveRenderContext { + olive::Renderer* renderer = nullptr; + olive::DecoderCache* decoder_cache = nullptr; + olive::ShaderCache* shader_cache = nullptr; +}; + +struct OliveRenderTicket { + olive::RenderTicketPtr impl; +}; + +extern "C" { + +OliveRenderContext* olive_render_context_create(OliveRenderBackend backend) { + try { + auto* ctx = new OliveRenderContext(); + if (backend == OLIVE_RENDER_BACKEND_OPENGL) { + ctx->renderer = new olive::OpenGLRenderer(); + } else { + // ctx->renderer = new olive::DummyRenderer(); + } + ctx->decoder_cache = new olive::DecoderCache(); + ctx->shader_cache = new olive::ShaderCache(); + return ctx; + } catch (...) { + return nullptr; + } +} + +void olive_render_context_destroy(OliveRenderContext* ctx) { + if (!ctx) return; + delete ctx->shader_cache; + delete ctx->decoder_cache; + if (ctx->renderer) { + ctx->renderer->Destroy(); + delete ctx->renderer; + } + delete ctx; +} + +int olive_render_context_init(OliveRenderContext* ctx) { + if (!ctx || !ctx->renderer) return OLIVE_ERROR_INVALID; + try { + if (!ctx->renderer->Init()) return OLIVE_ERROR_GENERIC; + ctx->renderer->PostInit(); + return OLIVE_OK; + } catch (...) { + return OLIVE_ERROR_GENERIC; + } +} + +int olive_render_frame_sync(OliveRenderContext* ctx, + const OliveRenderFrameParams* params, + void** out_frame_data, + size_t* out_frame_size, + int* out_width, + int* out_height, + OlivePixelFormat* out_format) { + if (!ctx || !params) return OLIVE_ERROR_INVALID; + try { + // 1. 找到输出节点 + olive::Node* output_node = nullptr; + { + auto* cpp_graph = static_cast(params->node_graph); // 需要内部转换 + // ... 查找 output_node_id 对应的节点 ... + } + + // 2. 构造 RenderVideoParams + olive::RenderManager::RenderVideoParams vparams( + output_node, + ConvertToCpp(params->video_params), + ConvertToCpp(params->audio_params), + olive::Rational(params->time.num, params->time.den), + nullptr, // ColorManager,需从 graph 获取或传入 + params->mode == OLIVE_RENDER_MODE_OFFLINE ? olive::RenderMode::kOffline : olive::RenderMode::kOnline + ); + + // 3. 创建 ticket 并执行 + auto ticket = std::make_shared(); + ticket->Start(); + olive::RenderProcessor::Process(ticket, ctx->renderer, ctx->decoder_cache, ctx->shader_cache); + + // 4. 等待结果 + ticket->WaitForFinished(); + if (!ticket->HasResult()) return OLIVE_ERROR_GENERIC; + + // 5. 提取帧数据 + olive::FramePtr frame = ticket->Get().value(); + if (!frame) return OLIVE_ERROR_GENERIC; + + *out_width = frame->width(); + *out_height = frame->height(); + *out_format = ConvertToC(frame->format()); + + size_t data_size = frame->allocated_size(); + void* data = malloc(data_size); + memcpy(data, frame->data(), data_size); + *out_frame_data = data; + *out_frame_size = data_size; + + return OLIVE_OK; + } catch (...) { + return OLIVE_ERROR_GENERIC; + } +} + +// ... 其他函数类似封装 ... + +} // extern "C" +``` + +--- + +## 3. CMake 改造 + +```cmake +# app/render/CMakeLists.txt + +set(RENDER_INTERNAL_SOURCES + rendermanager.cpp rendermanager.h + renderprocessor.cpp renderprocessor.h + renderer.cpp renderer.h + renderticket.cpp renderticket.h + rendercache.cpp rendercache.h + previewautocacher.cpp previewautocacher.h + colorprocessor.cpp colorprocessor.h + colorprocessorcache.cpp colorprocessorcache.h + diskmanager.cpp diskmanager.h + # ... job/ 目录下的所有文件 + job/shaderjob.cpp job/shaderjob.h + job/generatejob.cpp job/generatejob.h + job/footagejob.cpp job/footagejob.h + job/colortransformjob.cpp job/colortransformjob.h + job/samplejob.cpp job/samplejob.h + job/cachejob.cpp job/cachejob.h + job/pluginjob.cpp job/pluginjob.h + job/acceleratedjob.cpp job/acceleratedjob.h + # ... opengl/ 目录 + opengl/openglrenderer.cpp opengl/openglrenderer.h + opengl/openglshader.cpp opengl/openglshader.h + opengl/opengltexture.cpp opengl/opengltexture.h + # ... plugin/ 目录(OFX 插件专用渲染器) + plugin/pluginrenderer.cpp plugin/pluginrenderer.h +) + +set(RENDER_API_SOURCES + ${CMAKE_SOURCE_DIR}/c_api/src/render_api.cpp +) + +add_library(oliverender SHARED + ${RENDER_INTERNAL_SOURCES} + ${RENDER_API_SOURCES} +) + +target_compile_definitions(oliverender PRIVATE OLIVE_BUILDING_RENDER) + +target_include_directories(oliverender + PRIVATE + ${CMAKE_SOURCE_DIR}/app + ${CMAKE_SOURCE_DIR}/third_party/openfx/include + ${CMAKE_SOURCE_DIR}/third_party/openfx/HostSupport/include + ${CMAKE_SOURCE_DIR}/c_api/include + PUBLIC + $ +) + +target_link_libraries(oliverender + PUBLIC + olivenode + olivecodec + olivecore + oliveplugin + Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::OpenGL + ${OCIO_LIBRARIES} + OpenGL::GL +) + +set_target_properties(oliverender PROPERTIES + CXX_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN YES +) + +install(TARGETS oliverender DESTINATION lib) +install(FILES ${CMAKE_SOURCE_DIR}/c_api/include/olive/render_api.h DESTINATION include/olive) +``` + +--- + +## 4. 小步快跑实施步骤 + +### Step 0: 分离渲染客户端与服务端代码(2 天) + +- [ ] 分析 `render/` 中哪些代码主进程需要(RenderManager 的排队/协调逻辑),哪些仅子进程需要(OpenGLRenderer、RenderProcessor)。 +- [ ] 创建 `app/render/client/` 子目录,放置主进程专用的轻量代码(如 `RenderProcessLauncher`)。 +- [ ] 确保 `render/` 的现有代码仍然可以编译为完整的库(子进程使用)。 + +**验收标准**:`liboliverender.so` 编译成功,包含完整的渲染逻辑。 + +### Step 1: 将 render/ 独立为动态库(1 天) + +- [ ] 修改 `app/render/CMakeLists.txt`,将 `render/` 从主 OBJECT 库移出,构建为 `oliverender SHARED`。 +- [ ] 处理 `shaders/` 目录的资源文件路径问题(子进程需要知道着色器文件位置)。 + +**验收标准**:`liboliverender.so` 编译成功。 + +### Step 2: 最小 C API(2 天) + +- [ ] 实现 `olive_render_context_create/destroy/init`。 +- [ ] 实现 `olive_render_frame_sync`(同步渲染单帧)。 +- [ ] 此 C API 主要供 `olive-renderer` 子进程内部使用(子进程加载 `liboliverender.so` 后调用)。 + +**验收标准**:可以编写一个命令行测试程序,加载 `liboliverender.so`,初始化 OpenGL,渲染一帧纯色。 + +### Step 3: 节点图序列化辅助(1 天) + +- [ ] 实现 `olive_render_serialize_graph_for_render`。 +- [ ] 此函数供主进程调用,将目标 `ViewerOutput` 及其上游节点序列化为紧凑 XML。 + +**验收标准**:给定一个包含 ViewerOutput 的图,序列化后的 XML 可以被 `ProjectSerializer` 重新加载。 + +### Step 4: 异步 Ticket 接口(2 天) + +- [ ] 实现 `olive_render_frame_async`, `olive_render_ticket_wait`, `olive_render_ticket_get_result_frame`。 +- [ ] 此接口用于子进程内部的并发渲染(一个子进程内可同时渲染多帧)。 + +--- + +## 5. 风险与回退 + +| 风险 | 对策 | +|---|---| +| `RenderProcessor` 深度依赖 `NodeTraverser`,C API 难以表达遍历逻辑 | `olive_render_frame_sync` 是高阶封装,内部直接使用原有的 C++ `RenderProcessor`,C API 调用者无需了解遍历细节。 | +| OpenGL 上下文初始化在不同平台差异大 | 在子进程中处理平台差异(子进程使用 `QOffscreenSurface` + `QOpenGLContext`)。C API 中 `backend` 参数暂时只支持 `"opengl"`。 | +| `PreviewAutoCacher` 的复杂缓存逻辑 | `PreviewAutoCacher` 保留在主进程中(或完全移除,因为"用完即弃"的渲染模型下,缓存策略由主进程重新设计)。 | +| 子进程需要访问 `app/shaders/` 下的 GLSL 文件 | 通过命令行参数 `--shader-path` 将资源路径传递给子进程。打包时确保着色器文件与可执行文件一同分发。 | +| `ColorManager` 和 OCIO 配置 | 通过 C API 参数 `color_reference_space` / `color_display_space` 传递,子进程内部重建 `ColorManager`。 | diff --git a/docs/zh/modularization-plan/07-oliveui.md b/docs/zh/modularization-plan/07-oliveui.md new file mode 100644 index 000000000..a0eef662f --- /dev/null +++ b/docs/zh/modularization-plan/07-oliveui.md @@ -0,0 +1,256 @@ +# liboliveui.so — UI 层 + +> **依赖**:`libolivecore.so`, `libolivecodec.so`, `libolivenode.so`, `liboliverender.so`, `liboliveaudio.so` +> **外部依赖**:Qt::Widgets, Qt::OpenGL, Qt::OpenGLWidgets, KDDockWidgets +> **包含源码**:`app/widget/`, `app/panel/`, `app/window/`, `app/dialog/`, `app/tool/`, `app/ui/` +> **当前状态**:单体 OBJECT 库的一部分 +> **改造难度**:⭐⭐⭐(中等,但代码量大) + +--- + +## 1. 当前状态分析 + +UI 层是代码量最大的模块,但**耦合方向是单向的**:UI 依赖下层(node/render/codec),下层不依赖 UI。这使得 UI 层的拆分相对直接。 + +| 组件 | 说明 | +|---|---| +| `widget/` | 40+ 自定义 Qt Widget(节点视图、时间线、播放控制、颜色轮等) | +| `panel/` | 基于 KDDockWidgets 的可停靠面板包装 | +| `window/mainwindow/` | 主窗口 | +| `dialog/` | 模态对话框(导出、首选项、项目属性等) | +| `tool/` | 工具枚举 | +| `ui/` | 图标、光标、样式表、翻译资源 | + +**关键问题**: +- `Core` 类(`core.h/cpp`)混合了业务逻辑和 UI 逻辑(`StartGUI()`, `main_window_` 等)。 +- `RenderManager` 的 `RenderTicketWatcher` 使用 Qt 信号通知 UI。 +- 大量 UI 类直接包含 `node/` 和 `render/` 的 C++ 头文件。 + +**策略**: +- `liboliveui.so` 的 C API 不需要非常完善,因为 UI 层**大概率仍然与主进程一同编译**(UI 是主进程的核心)。 +- 但为了保持架构一致性,仍定义 C API 用于: + 1. 第三方脚本/插件通过 C API 操作 UI(未来扩展)。 + 2. 单元测试通过 C API 驱动 UI(自动化测试)。 +- **主进程中的 UI 代码可以继续使用 C++ 直接包含下层头文件**,不必全部改为 C API 调用。这是因为 UI 层在最顶层,不需要被其他模块依赖。 + +**修正策略**:`liboliveui.so` 的拆分重点在于: +1. 将 UI 代码从单体 OBJECT 库移出,编译为独立的 `liboliveui.so`。 +2. 主进程显式加载 `liboliveui.so`。 +3. UI 层内部继续使用 C++ 直接调用下层(node/render 等),只在跨库边界处遵循 ABI 规则。 + +--- + +## 2. C API 设计(精简版) + +UI 层的 C API 不需要覆盖所有 Widget,只需提供应用级入口和关键面板操作: + +### 2.1 头文件:`c_api/include/olive/ui_api.h` + +```c +#ifndef OLIVE_UI_API_H +#define OLIVE_UI_API_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "core_api.h" +#include "node_api.h" + +#define OLIVE_UI_API_VERSION 1 + +#ifdef OLIVE_BUILDING_UI +# define OLIVE_UI_API __attribute__((visibility("default"))) +#else +# define OLIVE_UI_API +#endif + +/* ========== 不透明类型 ========== */ +typedef struct OliveApplication OliveApplication; +typedef struct OliveMainWindow OliveMainWindow; +typedef struct OliveViewerPanel OliveViewerPanel; +typedef struct OliveTimelinePanel OliveTimelinePanel; +typedef struct OliveNodeEditorPanel OliveNodeEditorPanel; + +/* ========== API 版本 ========== */ +OLIVE_UI_API int olive_ui_api_version(void); + +/* ========== 应用生命周期 ========== */ +OLIVE_UI_API OliveApplication* olive_ui_app_create(int argc, char** argv); +OLIVE_UI_API int olive_ui_app_exec(OliveApplication* app); +OLIVE_UI_API void olive_ui_app_quit(OliveApplication* app); +OLIVE_UI_API void olive_ui_app_destroy(OliveApplication* app); + +/* ========== 主窗口 ========== */ +OLIVE_UI_API OliveMainWindow* olive_ui_main_window_create(OliveApplication* app); +OLIVE_UI_API void olive_ui_main_window_destroy(OliveMainWindow* win); +OLIVE_UI_API void olive_ui_main_window_show(OliveMainWindow* win); +OLIVE_UI_API void olive_ui_main_window_set_fullscreen(OliveMainWindow* win, int fullscreen); + +/* ========== 项目操作 ========== */ +OLIVE_UI_API int olive_ui_open_project(OliveMainWindow* win, const char* filename); +OLIVE_UI_API int olive_ui_save_project(OliveMainWindow* win, const char* filename); +OLIVE_UI_API int olive_ui_import_footage(OliveMainWindow* win, const char** filenames, int count); + +/* ========== 查看器(Viewer) ========== */ +OLIVE_UI_API OliveViewerPanel* olive_ui_get_active_viewer(OliveMainWindow* win); +OLIVE_UI_API void olive_ui_viewer_set_time(OliveViewerPanel* viewer, OliveRational time); +OLIVE_UI_API void olive_ui_viewer_play(OliveViewerPanel* viewer); +OLIVE_UI_API void olive_ui_viewer_pause(OliveViewerPanel* viewer); +OLIVE_UI_API void olive_ui_viewer_stop(OliveViewerPanel* viewer); + +/* ========== 时间线 ========== */ +OLIVE_UI_API OliveTimelinePanel* olive_ui_get_active_timeline(OliveMainWindow* win); +OLIVE_UI_API void olive_ui_timeline_set_time(OliveTimelinePanel* timeline, OliveRational time); +OLIVE_UI_API void olive_ui_timeline_set_work_area(OliveTimelinePanel* timeline, + OliveRational in, + OliveRational out); + +/* ========== 节点编辑器 ========== */ +OLIVE_UI_API OliveNodeEditorPanel* olive_ui_get_node_editor(OliveMainWindow* win); +OLIVE_UI_API void olive_ui_node_editor_set_graph(OliveNodeEditorPanel* editor, + OliveNodeGraph* graph); + +/* ========== 导出对话框 ========== */ +OLIVE_UI_API int olive_ui_show_export_dialog(OliveMainWindow* win, + OliveViewerOutput* viewer_output, + const char* default_filename); + +/* ========== 状态栏消息 ========== */ +OLIVE_UI_API void olive_ui_show_status_message(OliveMainWindow* win, + const char* message, + int timeout_ms); + +#ifdef __cplusplus +} +#endif + +#endif // OLIVE_UI_API_H +``` + +--- + +## 3. CMake 改造 + +```cmake +# 由于 UI 层代码分散在 widget/, panel/, window/, dialog/, tool/, ui/ 多个目录, +# 需要在 app/CMakeLists.txt 中统一聚合。 + +set(UI_INTERNAL_SOURCES + # widget/ + widget/viewer/viewerwidget.cpp widget/viewer/viewerwidget.h + widget/timelinewidget/timelinewidget.cpp widget/timelinewidget/timelinewidget.h + widget/nodeview/nodeview.cpp widget/nodeview/nodeview.h + # ... 所有 widget 源文件 + + # panel/ + panel/viewer/viewerpanel.cpp panel/viewer/viewerpanel.h + panel/timeline/timelinepanel.cpp panel/timeline/timelinepanel.h + panel/node/nodepanel.cpp panel/node/nodepanel.h + # ... 所有 panel 源文件 + + # window/ + window/mainwindow/mainwindow.cpp window/mainwindow/mainwindow.h + + # dialog/ + dialog/export/exportdialog.cpp dialog/export/exportdialog.h + dialog/preferences/preferencesdialog.cpp dialog/preferences/preferencesdialog.h + # ... 所有 dialog 源文件 + + # tool/ + tool/tool.cpp tool/tool.h + + # ui/ 资源(.qrc 等) + # ... +) + +set(UI_API_SOURCES + ${CMAKE_SOURCE_DIR}/c_api/src/ui_api.cpp +) + +add_library(oliveui SHARED + ${UI_INTERNAL_SOURCES} + ${UI_API_SOURCES} +) + +target_compile_definitions(oliveui PRIVATE OLIVE_BUILDING_UI) + +target_include_directories(oliveui + PRIVATE + ${CMAKE_SOURCE_DIR}/app + ${CMAKE_SOURCE_DIR}/c_api/include + ${CMAKE_SOURCE_DIR}/ext/KDDockWidgets/src + PUBLIC + $ +) + +target_link_libraries(oliveui + PUBLIC + olivenode + oliverender + olivecodec + oliveaudio + olivecore + oliveplugin + Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Gui + Qt${QT_VERSION_MAJOR}::Widgets + Qt${QT_VERSION_MAJOR}::OpenGL + Qt${QT_VERSION_MAJOR}::OpenGLWidgets + KDAB::kddockwidgets +) + +set_target_properties(oliveui PROPERTIES + CXX_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN YES +) + +install(TARGETS oliveui DESTINATION lib) +install(FILES ${CMAKE_SOURCE_DIR}/c_api/include/olive/ui_api.h DESTINATION include/olive) +``` + +--- + +## 4. 小步快跑实施步骤 + +### Step 0: 解耦 Core 类(2 天) + +- [ ] 分析 `core.h/cpp` 中哪些属于 UI 逻辑(`StartGUI()`, `main_window_`, `ImportFiles()` 等),哪些属于业务逻辑。 +- [ ] 将 UI 相关逻辑迁移到 `liboliveui.so` 内部的一个 `UiCore` 类中。 +- [ ] 保留 `Core` 类中纯业务逻辑(如 `FootageFileDialogFilter`, `CreateNewSequenceForProject`)。 + +**验收标准**:`core.h` 不再包含 `mainwindow.h` 或 `projectexplorer.h` 等 UI 头文件。 + +### Step 1: 聚合 UI 源码(1 天) + +- [ ] 在 `app/CMakeLists.txt` 中聚合所有 UI 相关源文件(widget/, panel/, window/, dialog/, tool/, ui/)。 +- [ ] 确保 `liboliveui.so` 可以编译。 + +**验收标准**:`liboliveui.so` 编译成功。 + +### Step 2: 主进程加载 UI 库(1 天) + +- [ ] 修改 `main.cpp`:先通过 `ModuleLoader` 加载 `liboliveui.so`,然后调用 `olive_ui_app_create` 和 `olive_ui_main_window_create`。 +- [ ] 若动态加载失败,回退到静态链接模式。 + +**验收标准**:主程序启动时日志显示成功加载 `ui` 模块,并正常显示主窗口。 + +### Step 3: C API 实现(按需,2–3 天) + +- [ ] 实现 `olive_ui_app_create/exec/quit/destroy`。 +- [ ] 实现 `olive_ui_main_window_create/show`。 +- [ ] 实现项目操作:`open_project`, `save_project`, `import_footage`。 +- [ ] 其他 UI C API 根据测试/脚本需求逐步实现。 + +**验收标准**:可以通过一个外部测试程序加载 `liboliveui.so` 并打开主窗口。 + +--- + +## 5. 风险与回退 + +| 风险 | 对策 | +|---|---| +| UI 代码量巨大,聚合时容易遗漏源文件 | 编写脚本自动收集 `widget/`, `panel/`, `window/`, `dialog/` 下的所有 `.cpp`/`.h` 文件,或在 CMake 中保持原有的 `add_subdirectory` 结构,只是最终输出为 SHARED 而非 OBJECT。 | +| KDDockWidgets 的符号跨动态库 | KDDockWidgets 以静态库形式链接进 `liboliveui.so`,其符号不外泄。确保 `liboliveui.so` 的 `CXX_VISIBILITY_PRESET hidden`。 | +| Qt 资源文件(.qrc)在动态库中的加载 | 将 `.qrc` 编译进 `liboliveui.so`,Qt 的资源系统在动态库中工作正常。确保 `Q_INIT_RESOURCE()` 在库加载时被调用。 | +| `Core` 类的信号槽跨模块 | `Core` 保留在主进程,`UiCore` 在 `liboliveui.so` 中。两者通过 C API 或 Qt 的跨进程信号(如果未来需要)通信。初期保持简单:主进程直接调用 UI 的 C API。 | diff --git a/docs/zh/modularization-plan/08-olive-renderer.md b/docs/zh/modularization-plan/08-olive-renderer.md new file mode 100644 index 000000000..d6a7c573a --- /dev/null +++ b/docs/zh/modularization-plan/08-olive-renderer.md @@ -0,0 +1,692 @@ +# olive-renderer — 多进程渲染可执行文件 + +> **类型**:独立可执行文件(非动态库) +> **依赖**:`libolivecore.so`, `libolivecodec.so`, `libolivenode.so`, `liboliverender.so`(可静态或动态链接) +> **外部依赖**:Qt::Core, OpenGL, FFmpeg, OpenColorIO +> **核心特征**:**"用完即弃"**——每帧(或每几帧)启动一个新进程,渲染完成后立即退出 +> **改造难度**:⭐⭐⭐(中等,逻辑复杂但隔离清晰) + +--- + +## 1. 设计哲学:用完即弃(Fire-and-Forget) + +传统多进程渲染架构通常维护一个**常驻子进程**,通过复杂的 IPC 协议进行状态同步、心跳检测、崩溃恢复。这种架构虽然成熟,但引入了以下复杂性: + +- **状态同步**:主进程和子进程的节点图、缓存、参数必须保持一致。 +- **锁与并发**:共享内存的读写需要锁或原子操作。 +- **崩溃恢复**:子进程崩溃后需要重建状态、重同步节点图。 +- **生命周期管理**:启动、握手、心跳、优雅退出、强制杀死的完整状态机。 + +本方案采用**激进的简化策略**: + +> **每一帧渲染任务 = 一个全新的操作系统进程**。进程接收完整的渲染输入,执行渲染,输出结果,然后 `exit(0)`。 + +### 1.1 优势 + +| 优势 | 说明 | +|---|---| +| **零锁** | 进程之间不共享任何可变状态(除了只读的共享内存输出区),无需任何互斥锁、信号量、条件变量。 | +| **零状态同步** | 每帧的输入都是自包含的(节点图 XML + 渲染参数),子进程无需维护任何跨帧状态。 | +| **自动崩溃隔离** | 某一帧的渲染崩溃(OFX 插件 segfault、GPU 驱动错误)只会影响该进程,主进程和其他帧完全不受影响。 | +| **资源自动回收** | 进程退出后,操作系统自动回收其所有资源(内存、GPU 上下文、文件句柄、解码器实例),无需显式清理。 | +| **可预测性** | 没有状态泄漏、没有内存碎片累积、没有僵尸缓存,每一帧都在干净的环境中渲染。 | +| **易于调试** | 可以单独运行 `olive-renderer` 命令行重放某一帧的渲染,无需启动完整 GUI。 | + +### 1.2 挑战与回退 + +| 挑战 | 分析 | 回退策略 | +|---|---|---| +| **进程启动开销** | `QProcess::start()` + OpenGL 上下文初始化可能需要 50–200ms | 若实测开销过高,采用 **"批处理模式"**:一个进程渲染 N 帧(如 5 帧),然后退出。或预启动一个进程池,但每个进程仍只服务一个批次后自杀。 | +| **节点图序列化开销** | 每帧都序列化完整节点图可能耗时 | 节点图 XML 在主进程缓存,相同图只序列化一次;仅参数变化时发送增量更新(即使进程用完即弃,输入数据仍可复用)。 | +| **GPU 上下文反复创建** | OpenGL 上下文创建/销毁开销较大 | 采用 **批处理模式** 摊销开销;或在支持的平台使用 EGL/GLES 的轻量上下文。 | +| **共享内存创建开销** | 每帧创建新的 shm 对象 | 使用 **内存映射临时文件** 替代 POSIX shm,创建开销更低;或主进程预分配一组循环缓冲区。 | +| **音频连续性** | 音频需要连续播放,逐帧进程可能导致间隙 | 音频采用 **批处理模式**:一个进程渲染 0.5–1 秒的音频块,而非每帧一个进程。 | + +--- + +## 2. 进程模型详解 + +### 2.1 单帧模式(默认) + +``` +主进程(UI 线程或工作线程) + │ + │ 1. 序列化节点图(若未缓存则生成 XML) + │ 2. 创建共享内存 / 临时文件 + │ 3. 组装渲染参数 + │ + ▼ +┌─────────────────────────────────────┐ +│ QProcess::start("olive-renderer", │ +│ ["--mode=frame", │ +│ "--node-graph=/tmp/g_123.xml", │ +│ "--time=1001/30000", │ +│ "--output-shm=/olive_r_123"]) │ +└─────────────────────────────────────┘ + │ + │ 4. 等待子进程结束(阻塞或异步) + │ QProcess::waitForFinished(timeout_ms) + │ + ▼ +子进程启动 ──────────────────────────────► 子进程退出 + │ │ + │ a. 解析命令行参数 │ + │ b. 加载节点图 XML │ + │ c. 初始化 Qt Core + OpenGL │ + │ d. 执行 RenderProcessor │ + │ e. 将帧写入共享内存 │ + │ f. 输出结果 JSON 到 stdout │ + │ g. exit(0) │ + ▼ ▼ +主进程读取共享内存 ──────────────────────► 主进程释放共享内存 + │ + │ 5. 将帧数据上传到 GPU Texture 或显示 + ▼ +ViewerWidget 更新 +``` + +### 2.2 批处理模式(性能回退) + +当实测单帧模式开销过高时,启用批处理: + +``` +主进程 + │ + │ 渲染帧 #1, #2, #3, #4, #5 + ▼ +启动 olive-renderer + --mode=batch + --frames=5 + --times=0/30,1/30,2/30,3/30,4/30 + --output-shm=/olive_r_batch_1 + --output-shm-size=165888000 (5 * 33MB) + │ + ▼ +子进程依次渲染 5 帧,全部写入同一块共享内存的不同偏移 +输出 JSON 数组包含 5 个结果 +exit(0) +``` + +--- + +## 3. 命令行接口 + +### 3.1 参数定义 + +``` +olive-renderer [选项] + +全局选项: + --backend= 渲染后端:opengl(默认), dummy + --shader-path= 着色器资源目录(默认:可执行文件同级目录下的 shaders/) + --ocio-config= OpenColorIO 配置文件路径 + --verbose 输出详细日志到 stderr + +单帧模式(--mode=frame): + --mode=frame + --node-graph= 节点图 XML 文件路径 + --output-node= 输出节点 ID(默认:图中的第一个 ViewerOutput) + --time= 渲染时间点,如 "1001/30000" + --video-params= 视频参数 JSON,如 '{"width":1920,"height":1080,"format":"rgba32f"}' + --audio-params= 音频参数 JSON + --color-ref= 参考色彩空间 + --color-display= 显示色彩空间 + --output-shm= POSIX 共享内存名称(如 "/olive_r_123")或临时文件路径 + --output-shm-size= 共享内存大小 + --output-stdout 将帧数据 base64 编码输出到 stdout(仅小帧/测试用) + +批处理模式(--mode=batch): + --mode=batch + --node-graph= + --output-node= + --times= 逗号分隔的时间点列表,如 "0/30,1/30,2/30" + --video-params= + --audio-params= + --output-shm= + --output-shm-size= + +音频模式(--mode=audio): + --mode=audio + --node-graph= + --output-node= + --start= 起始时间 + --duration= 持续时间 + --audio-params= + --output-shm= + --output-shm-size= +``` + +### 3.2 使用示例 + +```bash +# 单帧渲染 +olive-renderer \ + --mode=frame \ + --node-graph=/tmp/project_graph.xml \ + --output-node=ViewerOutput1 \ + --time=1001/30000 \ + --video-params='{"width":1920,"height":1080,"format":"rgba32f","channel_count":4}' \ + --output-shm=/olive_frame_12345 \ + --output-shm-size=33177600 + +# 批处理渲染 5 帧 +olive-renderer \ + --mode=batch \ + --node-graph=/tmp/project_graph.xml \ + --times="0/24,1/24,2/24,3/24,4/24" \ + --output-shm=/olive_batch_67890 \ + --output-shm-size=165888000 +``` + +--- + +## 4. 输出格式 + +### 4.1 标准输出(stdout) + +子进程将渲染结果以 **单行 JSON** 输出到 stdout,然后退出。 + +**单帧成功**: +```json +{"status":"ok","mode":"frame","time":"1001/30000","width":1920,"height":1080,"format":"rgba32f","pixel_format_id":28,"shm_name":"/olive_frame_12345","data_offset":256,"data_size":33177600,"linesize":7680,"render_time_ms":42} +``` + +**批处理成功**: +```json +{"status":"ok","mode":"batch","frame_count":5,"frames":[{"time":"0/24","data_offset":256,"data_size":33177600},{"time":"1/24","data_offset":33178056,"data_size":33177600},...],"render_time_ms":180} +``` + +**错误**: +```json +{"status":"error","error_code":"decoder_failure","message":"Failed to open decoder for footage 'clip001.mp4': codec not found","time":"1001/30000"} +``` + +**被取消**(主进程 kill 时不会收到,因为进程已死;但对于批处理中的内部取消): +```json +{"status":"cancelled","frames_completed":3,"frames_total":5} +``` + +### 4.2 标准错误(stderr) + +- `--verbose` 模式下,详细的调试日志输出到 stderr。 +- 错误信息同时出现在 stderr(人类可读)和 stdout JSON(机器可读)中。 + +### 4.3 退出码 + +| 退出码 | 含义 | +|---|---| +| 0 | 渲染成功 | +| 1 | 通用错误 | +| 2 | 无效参数 | +| 3 | 初始化失败(OpenGL/OCIO 等) | +| 4 | 节点图加载失败 | +| 5 | 渲染过程中出错(解码失败、着色器编译失败等) | +| 6 | 输出写入失败(共享内存不足等) | +| 130 | 被信号中断(SIGINT,即主进程 kill) | +| 137 | 被 SIGKILL 终止 | + +--- + +## 5. 内部架构 + +``` +olive-renderer (main.cpp) + │ + ▼ +┌─────────────────────────────┐ +│ 1. 解析命令行参数 │ +│ (QCommandLineParser) │ +└─────────────┬───────────────┘ + │ +┌─────────────▼───────────────┐ +│ 2. 初始化 QCoreApplication │ +│ (无 GUI,无事件循环) │ +└─────────────┬───────────────┘ + │ +┌─────────────▼───────────────┐ +│ 3. 加载节点图 XML │ +│ ProjectSerializer::Load() │ +│ 找到目标 ViewerOutput │ +└─────────────┬───────────────┘ + │ +┌─────────────▼───────────────┐ +│ 4. 初始化渲染后端 │ +│ OpenGL: QOffscreenSurface │ +│ + QOpenGLContext │ +│ Dummy: 空实现 │ +└─────────────┬───────────────┘ + │ +┌─────────────▼───────────────┐ +│ 5. 执行渲染 │ +│ RenderProcessor::Process()│ +│ 遍历节点图 → 生成帧 │ +└─────────────┬───────────────┘ + │ +┌─────────────▼───────────────┐ +│ 6. 写入共享内存 │ +│ mmap/shm_open/MapViewOfFile│ +│ 写入 ShmHeader + 像素数据 │ +└─────────────┬───────────────┘ + │ +┌─────────────▼───────────────┐ +│ 7. 输出 JSON 结果到 stdout │ +│ 8. 清理(可选,因即将 exit) │ +│ 9. return status; │ +└─────────────────────────────┘ +``` + +### 5.1 主函数伪代码 + +```cpp +// app/render/renderer_main.cpp + +#include +#include +#include +#include +#include +#include +#include +#include "olive/render_api.h" +#include "olive/node_api.h" + +int main(int argc, char** argv) { + QCoreApplication app(argc, argv); + QCommandLineParser parser; + + // 定义命令行选项 + parser.addOption({"mode", "Render mode: frame, batch, audio", "mode", "frame"}); + parser.addOption({"node-graph", "Node graph XML file", "path"}); + parser.addOption({"output-node", "Output node ID", "id"}); + parser.addOption({"time", "Frame time (rational)", "time"}); + parser.addOption({"times", "Batch frame times (csv)", "csv"}); + parser.addOption({"video-params", "Video params JSON", "json"}); + parser.addOption({"audio-params", "Audio params JSON", "json"}); + parser.addOption({"output-shm", "Output shared memory name", "name"}); + parser.addOption({"output-shm-size", "Output shared memory size", "bytes"}); + parser.addOption({"backend", "Render backend", "backend", "opengl"}); + parser.addOption({"shader-path", "Shader directory path", "path"}); + parser.addOption({"verbose", "Verbose logging"}); + + parser.process(app); + + // 验证必需参数 + if (!parser.isSet("node-graph") || !parser.isSet("output-shm")) { + OutputError("Missing required arguments: --node-graph and --output-shm"); + return 2; + } + + // 1. 加载节点图 + OliveNodeGraph* graph = olive_node_graph_create(); + QFile xml_file(parser.value("node-graph")); + if (!xml_file.open(QIODevice::ReadOnly)) { + OutputError("Failed to open node graph file"); + return 4; + } + QByteArray xml_data = xml_file.readAll(); + if (olive_node_graph_load_xml(graph, xml_data.constData(), xml_data.size()) != OLIVE_OK) { + OutputError("Failed to parse node graph XML"); + return 4; + } + + // 2. 找到输出节点 + const char* output_node_id = parser.value("output-node").toUtf8().constData(); + OliveNode* output_node = olive_node_graph_find_node(graph, output_node_id); + if (!output_node) { + // 如果没指定,找第一个 ViewerOutput + // ... + } + + // 3. 初始化渲染上下文 + OliveRenderBackend backend = parser.value("backend") == "dummy" + ? OLIVE_RENDER_BACKEND_DUMMY + : OLIVE_RENDER_BACKEND_OPENGL; + OliveRenderContext* ctx = olive_render_context_create(backend); + if (!ctx) { + OutputError("Failed to create render context"); + return 3; + } + if (olive_render_context_init(ctx) != OLIVE_OK) { + OutputError("Failed to initialize render backend"); + return 3; + } + + // 4. 准备共享内存 + QString shm_name = parser.value("output-shm"); + size_t shm_size = parser.value("output-shm-size").toULongLong(); + void* shm_ptr = MapSharedMemory(shm_name, shm_size); + if (!shm_ptr) { + OutputError("Failed to map shared memory"); + return 6; + } + + // 5. 执行渲染 + QElapsedTimer timer; + timer.start(); + + QString mode = parser.value("mode"); + QJsonObject result; + + if (mode == "frame") { + // 单帧渲染 + OliveRenderFrameParams params = ParseFrameParams(parser, graph, output_node); + void* frame_data = nullptr; + size_t frame_size = 0; + int w, h; + OlivePixelFormat fmt; + int err = olive_render_frame_sync(ctx, ¶ms, &frame_data, &frame_size, &w, &h, &fmt); + + if (err == OLIVE_OK) { + // 写入共享内存 + WriteFrameToShm(shm_ptr, frame_data, frame_size, w, h, fmt); + olive_core_free(frame_data); + + result["status"] = "ok"; + result["mode"] = "frame"; + result["width"] = w; + result["height"] = h; + result["shm_name"] = shm_name; + result["data_offset"] = 256; + result["data_size"] = static_cast(frame_size); + } else { + result["status"] = "error"; + result["message"] = olive_core_last_error_string(); + } + } else if (mode == "batch") { + // 批处理渲染... + } + + result["render_time_ms"] = timer.elapsed(); + + // 6. 输出 JSON + std::cout << QJsonDocument(result).toJson(QJsonDocument::Compact).toStdString() << std::endl; + + // 7. 清理 + UnmapSharedMemory(shm_ptr, shm_size); + olive_render_context_destroy(ctx); + olive_node_graph_destroy(graph); + + return result["status"].toString() == "ok" ? 0 : 5; +} +``` + +--- + +## 6. 共享内存实现 + +### 6.1 跨平台封装 + +```cpp +// app/render/shared_memory.h + +#pragma once +#include + +#ifdef __cplusplus +extern "C" { +#endif + +void* olive_shm_create(const char* name, size_t size); +void* olive_shm_open(const char* name, size_t size); +void olive_shm_close(void* ptr, size_t size); +void olive_shm_unlink(const char* name); + +#ifdef __cplusplus +} +#endif +``` + +### 6.2 POSIX 实现 + +```cpp +#include "shared_memory.h" +#include +#include +#include +#include +#include + +void* olive_shm_create(const char* name, size_t size) { + int fd = shm_open(name, O_RDWR | O_CREAT, 0666); + if (fd < 0) return nullptr; + if (ftruncate(fd, size) < 0) { + close(fd); + return nullptr; + } + void* ptr = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + close(fd); + return ptr; +} + +void* olive_shm_open(const char* name, size_t size) { + int fd = shm_open(name, O_RDWR, 0666); + if (fd < 0) return nullptr; + void* ptr = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + close(fd); + return ptr; +} + +void olive_shm_close(void* ptr, size_t size) { + if (ptr) munmap(ptr, size); +} + +void olive_shm_unlink(const char* name) { + shm_unlink(name); +} +``` + +### 6.3 Windows 实现 + +```cpp +#include + +void* olive_shm_create(const char* name, size_t size) { + HANDLE hMap = CreateFileMappingA(INVALID_HANDLE_VALUE, nullptr, + PAGE_READWRITE, 0, static_cast(size), name); + if (!hMap) return nullptr; + return MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, size); + // 注意:句柄需要保存以便后续关闭,此处简化 +} + +void olive_shm_close(void* ptr, size_t size) { + (void)size; + if (ptr) UnmapViewOfFile(ptr); +} +``` + +--- + +## 7. 主进程中的集成 + +### 7.1 渲染一帧的封装 + +```cpp +// app/render/render_process_launcher.h + +#pragma once +#include +#include +#include +#include "olive/core_api.h" +#include "olive/node_api.h" + +namespace olive { + +class RenderProcessLauncher { +public: + struct FrameResult { + bool success = false; + QString error_message; + int width = 0; + int height = 0; + OlivePixelFormat format = OLIVE_PIXEL_FMT_INVALID; + QString shm_name; + size_t data_offset = 0; + size_t data_size = 0; + int render_time_ms = 0; + }; + + // 渲染单帧,阻塞直到子进程结束 + static FrameResult RenderFrameSync(const QString& nodeGraphXmlPath, + const QString& outputNodeId, + OliveRational time, + const OliveVideoParams& videoParams, + const OliveAudioParams& audioParams, + OliveRenderMode mode, + int timeoutMs = 30000); + + // 渲染单帧,异步(返回 QProcess*,调用方连接 finished 信号) + static QProcess* RenderFrameAsync(const QString& nodeGraphXmlPath, + const QString& outputNodeId, + OliveRational time, + const OliveVideoParams& videoParams, + const OliveAudioParams& audioParams, + OliveRenderMode mode); + +private: + static QString BuildShmName(); + static bool CreateShm(const QString& name, size_t size, void** outPtr); + static void DestroyShm(const QString& name, void* ptr, size_t size); +}; + +} // namespace olive +``` + +### 7.2 使用示例 + +```cpp +// ViewerWidget 中请求渲染当前帧 +void ViewerWidget::RequestFrameAtTime(OliveRational time) { + // 1. 确保节点图 XML 已缓存 + if (cached_graph_xml_path_.isEmpty()) { + cached_graph_xml_path_ = SerializeNodeGraphToTempFile(viewer_output_); + } + + // 2. 创建共享内存 + QString shm_name = RenderProcessLauncher::BuildShmName(); + size_t shm_size = CalculateFrameSize(video_params_); + + // 3. 启动子进程(异步) + QProcess* proc = RenderProcessLauncher::RenderFrameAsync( + cached_graph_xml_path_, + olive_node_get_id(reinterpret_cast(viewer_output_)), + time, + video_params_, + audio_params_, + OLIVE_RENDER_MODE_ONLINE + ); + + // 4. 连接完成信号 + connect(proc, QOverload::of(&QProcess::finished), + this, [this, proc, shm_name, shm_size](int exitCode, QProcess::ExitStatus status) { + OnRenderProcessFinished(proc, shm_name, shm_size, exitCode, status); + }); +} + +void ViewerWidget::OnRenderProcessFinished(QProcess* proc, + const QString& shm_name, + size_t shm_size, + int exitCode, + QProcess::ExitStatus status) { + if (status == QProcess::CrashExit) { + qWarning() << "Renderer process crashed for frame"; + // 不需要恢复状态,直接丢弃这一帧,UI 保持上一帧 + proc->deleteLater(); + return; + } + + // 解析 stdout JSON + QByteArray stdout_data = proc->readAllStandardOutput(); + QJsonObject result = QJsonDocument::fromJson(stdout_data).object(); + + if (result["status"].toString() == "ok") { + // 从共享内存读取帧 + void* shm_ptr = olive_shm_open(shm_name.toUtf8().constData(), shm_size); + if (shm_ptr) { + DisplayFrameFromShm(shm_ptr, result); + olive_shm_close(shm_ptr, shm_size); + olive_shm_unlink(shm_name.toUtf8().constData()); + } + } else { + qWarning() << "Render error:" << result["message"].toString(); + } + + proc->deleteLater(); +} +``` + +--- + +## 8. 小步快跑实施步骤 + +### Step 0: 验证进程启动开销(1 天) + +- [ ] 编写一个最小测试程序 `test_process_spawn.cpp`,只测量 `QProcess::start()` + `waitForStarted()` 的耗时。 +- [ ] 加上 OpenGL 上下文初始化(`QOffscreenSurface` + `QOpenGLContext`)测量总耗时。 +- [ ] 在目标平台(开发机)上测试,记录数据。 + +**验收标准**:得到精确的单进程启动耗时数据,作为是否采用"用完即弃"或"批处理模式"的依据。 + +### Step 1: 创建 olive-renderer 可执行文件目标(1 天) + +- [ ] 新增 `app/render/renderer_main.cpp`。 +- [ ] 在 `app/CMakeLists.txt` 中新增 `add_executable(olive-renderer ...)`。 +- [ ] 链接 `oliverender`, `olivenode`, `olivecodec`, `olivecore`, `Qt::Core`。 + +**验收标准**:`olive-renderer` 编译成功,运行 `olive-renderer --help` 输出用法信息。 + +### Step 2: 实现命令行解析与节点图加载(1 天) + +- [ ] 使用 `QCommandLineParser` 解析所有参数。 +- [ ] 实现节点图 XML 加载(调用 `olive_node_graph_load_xml`)。 + +**验收标准**:`olive-renderer --node-graph=test.xml --output-shm=/test` 能成功加载节点图并找到 ViewerOutput。 + +### Step 3: 实现共享内存读写(1 天) + +- [ ] 实现 `olive_shm_create/open/close/unlink`(POSIX + Windows)。 +- [ ] 定义 `ShmHeader` 二进制布局。 + +**验收标准**:主进程创建 SHM,子进程写入数据,主进程读取并校验 CRC。 + +### Step 4: 单帧端到端渲染(2 天) + +- [ ] 在 `olive-renderer` 中初始化 OpenGL 上下文。 +- [ ] 调用 `olive_render_frame_sync` 渲染一帧。 +- [ ] 将结果写入共享内存。 +- [ ] 输出 JSON 到 stdout。 +- [ ] 在主进程中编写 `RenderProcessLauncher::RenderFrameSync` 测试。 + +**验收标准**:主进程可以成功渲染一帧纯色/测试图,并在 Viewer 中显示。 + +### Step 5: 集成到 ViewerWidget(2 天) + +- [ ] 修改 `ViewerWidget` 的帧请求逻辑,从直接调用 `RenderManager` 改为启动 `olive-renderer`。 +- [ ] 处理异步完成信号,从共享内存读取帧并显示。 +- [ ] 处理子进程崩溃(忽略该帧,保持上一帧显示)。 + +**验收标准**:拖动时间线时,Viewer 能实时显示渲染结果(可能有延迟,但功能正确)。 + +### Step 6: 批处理模式(2 天,按需) + +- [ ] 若 Step 0 的测试显示单帧开销过高,实现 `--mode=batch`。 +- [ ] 修改 `RenderProcessLauncher` 支持批处理。 + +### Step 7: 导出集成(2 天) + +- [ ] 修改导出任务(`task/export/`),使用 `olive-renderer` 子进程逐帧渲染,然后编码。 +- [ ] 导出天然适合批处理模式(可以一次性渲染 10–50 帧)。 + +--- + +## 9. 风险与回退 + +| 风险 | 对策 | +|---|---| +| 进程启动开销导致实时预览 < 10fps | 启用批处理模式,每 3–5 帧一个进程;或预启动一个进程池(但每个进程仍只服务一个批次后自杀)。 | +| OpenGL 驱动不支持离屏渲染 | 在 Linux 上使用 `EGL` 替代 `QOffscreenSurface`;在 Windows 上使用 `WGL` pbuffer;在 macOS 上使用 `CGL` pixel buffer。 | +| 共享内存名称冲突 | 使用 `QUuid::createUuid()` 生成唯一名称,格式为 `/olive__`。 | +| 共享内存泄漏(子进程崩溃后未 unlink) | 主进程在启动子进程前注册一个定时器,若子进程异常退出,5 秒后自动 `shm_unlink`。 | +| 磁盘空间不足(临时文件方案) | 渲染前检查磁盘空间,不足时返回错误码 6。 | +| 节点图 XML 过大导致解析慢 | 启用增量序列化:只序列化自上次以来变更的节点和参数。 | +| OFX 插件需要持久化状态 | OFX 插件实例不跨帧持久化,每帧重新创建。若某些插件初始化极慢,在 C API 中提供 `olive_plugin_instance_serialize_state` 接口,将状态快照传给下一帧的进程。 | diff --git a/docs/zh/modularization-plan/09-c-api-design.md b/docs/zh/modularization-plan/09-c-api-design.md new file mode 100644 index 000000000..03766efc6 --- /dev/null +++ b/docs/zh/modularization-plan/09-c-api-design.md @@ -0,0 +1,677 @@ +# C API 设计规范总纲 + +> **必读**:本文件定义了所有 Olive/Oak 动态库的 C API 设计约定。`01-` 到 `07-` 各模块的 API 均遵循此规范。 + +--- + +## 1. 设计原则 + +### 1.1 不透明指针(Opaque Pointer) + +所有 C++ 对象在 C 接口中均隐藏实现,仅暴露为 `struct` 的前向声明: + +```c +// 公共头文件(.h)中 +typedef struct OliveNodeGraph OliveNodeGraph; // 只有声明,无定义 + +// 实现文件(.cpp)中 +struct OliveNodeGraph { + olive::NodeGraph* impl; // 实际的 C++ 对象 +}; +``` + +外部代码只能操作指针,无法解引用或 sizeof。 + +### 1.2 纯 C 接口 + +- 函数名使用 `snake_case`,前缀为 `olive__`。 +- 参数和返回值仅使用 C 基础类型、结构体、不透明指针。 +- 禁止使用 C++ 特性:类、引用、重载、模板、异常、`std::string`、`QString`。 +- 字符串使用 `const char*`(UTF-8 编码)。 +- 布尔值使用 `int`(0 = false,非 0 = true)。 + +### 1.3 动态库自身可以用 C++ + +动态库的实现文件(`.cpp`)内部可以继续使用: +- Qt(`QObject`, `QString`, `QList`, 信号槽等) +- C++ STL +- 虚函数、模板、Lambda +- 异常(但不得穿透 C 接口边界) + +C 接口层只是薄薄的封装胶合层。 + +--- + +## 2. 命名规范 + +| 元素 | 规范 | 示例 | +|---|---|---| +| 类型名 | `Olive` + `PascalCase` | `OliveNodeGraph`, `OliveFrame` | +| 函数名 | `olive__` | `olive_node_graph_create`, `olive_codec_decoder_open` | +| 枚举名 | `Olive` | `OliveCodecResultOk`, `OliveRenderModeOffline` | +| 常量宏 | `OLIVE__UPPER_SNAKE` | `OLIVE_NODE_OK`, `OLIVE_CODEC_ERROR_NOT_FOUND` | +| 版本宏 | `OLIVE__API_VERSION` | `OLIVE_NODE_API_VERSION 1` | + +--- + +## 3. 内存管理约定 + +### 3.1 谁创建,谁释放 + +- **库创建的对象**,必须由库的对应 `destroy`/`free` 函数释放。 +- **主进程分配并传入的缓冲区**(如 `char*` 参数),由主进程管理,库内部只读或复制。 +- **库返回的字符串/缓冲区**,必须使用库提供的 `free` 函数释放,不能用 C 标准 `free()`(因为库的堆和主进程的堆可能是分离的,尤其是在 Windows 上)。 + +```c +// 正确:库分配,库释放 +char* xml = olive_node_graph_save_xml(graph, &len); +// ... 使用 xml ... +olive_core_free(xml); // 使用库提供的释放函数 + +// 错误: +free(xml); // 危险!堆可能不一致 +``` + +### 3.2 通用释放函数 + +每个模块提供一个通用释放函数: + +```c +void olive_core_free(void* ptr); // 释放字符串/二进制缓冲区 +void olive_core_mem_free(void* ptr, size_t size); // 带大小的释放(用于安全擦除) +``` + +### 3.3 对象生命周期模式 + +```c +// 模式 A:Create/Destroy(堆分配) +OliveNodeGraph* olive_node_graph_create(void); +void olive_node_graph_destroy(OliveNodeGraph* obj); + +// 模式 B:Init/Cleanup(栈分配或外部缓冲区) +int olive_frame_init(OliveFrame* frame, int w, int h, OlivePixelFormat fmt); +void olive_frame_cleanup(OliveFrame* frame); + +// 模式 C:Ref/Unref(引用计数) +void olive_frame_ref(OliveFrame* frame); +void olive_frame_unref(OliveFrame* frame); +``` + +优先使用 **模式 A(Create/Destroy)**,因为不透明指针天然适合堆分配。 + +--- + +## 4. 错误处理 + +### 4.1 返回码约定 + +所有可能失败的函数返回 `int`: + +```c +#define OLIVE_OK 0 // 成功 +#define OLIVE_ERROR_GENERIC -1 // 通用错误 +#define OLIVE_ERROR_INVALID -2 // 无效参数 +#define OLIVE_ERROR_NOMEM -3 // 内存不足 +#define OLIVE_ERROR_NOT_FOUND -4 // 找不到对象/文件 +#define OLIVE_ERROR_IO -5 // IO 错误 +#define OLIVE_ERROR_CANCELLED -6 // 操作被取消 +#define OLIVE_ERROR_UNSUPPORTED -7 // 不支持的操作 +``` + +### 4.2 详细错误信息 + +提供线程局部的错误信息获取函数: + +```c +int olive_core_last_error_code(void); +const char* olive_core_last_error_string(void); // 线程安全,返回静态缓冲区或 TLS +``` + +实现方式: + +```cpp +// .cpp 中 +thread_local int g_last_error_code = OLIVE_OK; +thread_local char g_last_error_string[1024]; + +static void SetError(int code, const char* fmt, ...) { + g_last_error_code = code; + va_list args; + va_start(args, fmt); + vsnprintf(g_last_error_string, sizeof(g_last_error_string), fmt, args); + va_end(args); +} +``` + +--- + +## 5. 字符串处理 + +### 5.1 输入字符串 + +- 所有 `const char*` 参数均视为 **UTF-8 编码**。 +- 库内部在边界处转换为 `QString`: + +```cpp +// 封装层内部 +QString qstr = QString::fromUtf8(cstr); +``` + +### 5.2 输出字符串 + +- 返回 `char*` 的函数,使用 `olive_core_free()` 释放。 +- 如果只需读取而不持有,提供 `const char*` 返回版本: + +```c +const char* olive_node_get_type_name(OliveNode* node); // 生命周期与 node 绑定 +char* olive_node_graph_save_xml(OliveNodeGraph* graph, size_t* out_len); // 需释放 +``` + +--- + +## 6. 显式加载器(ModuleLoader) + +### 6.1 设计目标 + +主进程通过一个统一的 `ModuleLoader` 类显式加载所有动态库,将 `dlopen`/`dlsym` 的细节隐藏。 + +### 6.2 C++ 封装类 + +```cpp +// app/moduleloader.h +#pragma once + +#include +#include +#include + +namespace olive { + +class ModuleLoader { +public: + ModuleLoader(); + ~ModuleLoader(); + + // 加载指定路径的动态库 + bool Load(const QString& module_name, const QString& library_path); + + // 卸载 + void Unload(const QString& module_name); + + // 获取函数指针(模板封装,内部调用 dlsym) + template + FuncPtr GetFunction(const QString& module_name, const char* func_name) { + return reinterpret_cast(GetFunctionRaw(module_name, func_name)); + } + + // 检查是否已加载 + bool IsLoaded(const QString& module_name) const; + + // 获取加载错误信息 + QString LastError() const; + +private: + void* GetFunctionRaw(const QString& module_name, const char* func_name); + + struct ModuleHandle { + void* handle; // dlopen handle + QString path; + }; + QHash modules_; + QString last_error_; +}; + +// 便捷宏:从指定模块获取函数并调用 +#define OLIVE_LOAD_FUNC(loader, module, name, type) \ + auto name = (loader).GetFunction(module, #name); \ + if (!name) { qFatal("Failed to load function: " #name " from module: " #module); } + +} // namespace olive +``` + +### 6.3 实现(POSIX) + +```cpp +// app/moduleloader.cpp +#include "moduleloader.h" +#include +#include + +namespace olive { + +bool ModuleLoader::Load(const QString& module_name, const QString& library_path) { + if (modules_.contains(module_name)) return true; + + void* handle = dlopen(library_path.toUtf8().constData(), RTLD_NOW | RTLD_LOCAL); + if (!handle) { + last_error_ = QString::fromUtf8(dlerror()); + qWarning() << "Failed to load" << library_path << ":" << last_error_; + return false; + } + + modules_.insert(module_name, {handle, library_path}); + qInfo() << "Loaded module:" << module_name << "from" << library_path; + return true; +} + +void ModuleLoader::Unload(const QString& module_name) { + auto it = modules_.find(module_name); + if (it != modules_.end()) { + dlclose(it->handle); + modules_.erase(it); + } +} + +void* ModuleLoader::GetFunctionRaw(const QString& module_name, const char* func_name) { + auto it = modules_.find(module_name); + if (it == modules_.end()) return nullptr; + dlerror(); // 清除之前的错误 + void* func = dlsym(it->handle, func_name); + return func; +} + +} // namespace olive +``` + +### 6.4 实现(Windows) + +```cpp +#include + +bool ModuleLoader::Load(const QString& module_name, const QString& library_path) { + HMODULE handle = LoadLibraryW(library_path.toStdWString().c_str()); + if (!handle) { + last_error_ = QString::number(GetLastError()); + return false; + } + modules_.insert(module_name, {handle, library_path}); + return true; +} + +void* ModuleLoader::GetFunctionRaw(const QString& module_name, const char* func_name) { + auto it = modules_.find(module_name); + if (it == modules_.end()) return nullptr; + return GetProcAddress(static_cast(it->handle), func_name); +} +``` + +### 6.5 使用示例 + +```cpp +// core.cpp 中初始化 +module_loader_ = new ModuleLoader(); +module_loader_->Load("core", FindLibraryPath("libolivecore.so")); +module_loader_->Load("codec", FindLibraryPath("libolivecodec.so")); +module_loader_->Load("node", FindLibraryPath("libolivenode.so")); + +// 获取函数 +OLIVE_LOAD_FUNC(*module_loader_, "core", olive_rational_make, OliveRational(*)(int64_t, int64_t)); + +OliveRational r = olive_rational_make(1001, 30000); +``` + +--- + +## 7. 类型映射表 + +| C++ 类型(内部) | C 接口类型(公共) | 说明 | +|---|---|---| +| `olive::Rational` | `OliveRational` | `struct { int64_t num, den; }` | +| `olive::Color` | `OliveColor` | `struct { double r, g, b, a; }` | +| `olive::TimeRange` | `OliveTimeRange*` | 不透明指针 | +| `olive::Frame` | `OliveFrame*` | 不透明指针 | +| `olive::SampleBuffer` | `OliveSampleBuffer*` | 不透明指针 | +| `olive::PixelFormat` | `OlivePixelFormat` | `enum` | +| `olive::VideoParams` | `OliveVideoParams` | 公开结构体(POD) | +| `olive::AudioParams` | `OliveAudioParams` | 公开结构体(POD) | +| `olive::Node*` | `OliveNode*` | 不透明指针 | +| `olive::NodeGraph*` | `OliveNodeGraph*` | 不透明指针 | +| `olive::RenderTicketPtr` | `OliveRenderTicket*` | 不透明指针(引用计数内部管理) | +| `QString` | `const char*` | UTF-8 编码 | +| `QSize` | `struct { int width; int height; }` | `OliveSize` | +| `QMatrix4x4` | `float[16]` | 列优先 | + +### 7.1 POD 结构体定义示例 + +```c +// olivecore_api.h + +typedef struct { + int64_t num; + int64_t den; +} OliveRational; + +typedef struct { + double r; + double g; + double b; + double a; +} OliveColor; + +typedef struct { + int width; + int height; + int depth; + int channel_count; + OlivePixelFormat format; + double pixel_aspect_num; + double pixel_aspect_den; +} OliveVideoParams; + +typedef struct { + int sample_rate; + int64_t channel_layout; // FFmpeg AV_CH_LAYOUT_* 值 + OliveSampleFormat format; +} OliveAudioParams; + +typedef struct { + int width; + int height; +} OliveSize; +``` + +--- + +## 8. 线程安全 + +### 8.1 API 层面 + +- **默认不保证线程安全**。除非文档明确标注 `thread-safe`,否则每个 `OliveXxx*` 对象只能在创建它的线程中使用。 +- 这是刻意的设计:由于渲染进程是"用完即弃"的,不存在多线程共享渲染状态的问题。 + +### 8.2 主进程中的线程使用 + +- `ModuleLoader` 本身是线程安全的(只读查找,加载/卸载在初始化/退出时串行执行)。 +- UI 对象在主线程操作。 +- IO/解码可以在工作线程中通过 C API 操作独立的 `OliveDecoder*` 实例。 + +--- + +## 9. 版本与 ABI 兼容性 + +### 9.1 API 版本号 + +每个模块的 C API 有一个主版本号: + +```c +#define OLIVE_NODE_API_VERSION 1 + +int olive_node_api_version(void); // 返回 OLIVE_NODE_API_VERSION +``` + +### 9.2 加载时版本检查 + +```cpp +bool LoadNodeModule(ModuleLoader* loader, const QString& path) { + if (!loader->Load("node", path)) return false; + auto version_fn = loader->GetFunction("node", "olive_node_api_version"); + if (!version_fn || version_fn() != EXPECTED_NODE_API_VERSION) { + qFatal("Incompatible libolivenode.so version"); + return false; + } + return true; +} +``` + +### 9.3 ABI 兼容性规则 + +- **允许**:新增函数、新增枚举值(在末尾)、新增结构体字段(在末尾,且文档标注"v2 起可用")。 +- **不允许**:删除函数、修改函数签名、修改已有字段含义、改变枚举值顺序。 +- **结构体扩展**:POD 结构体新增字段时,提供初始化宏确保旧代码不会未初始化新字段: + +```c +#define OLIVE_VIDEO_PARAMS_DEFAULT { \ + .width = 1920, .height = 1080, .depth = 1, \ + .channel_count = 4, .format = OLIVE_PIXEL_FMT_RGBA32F, \ + .pixel_aspect_num = 1.0, .pixel_aspect_den = 1.0 \ +} +``` + +--- + +## 10. 头文件组织 + +### 10.1 目录结构 + +``` +c_api/ +├── include/ +│ ├── olive/ # 公共 C API 头文件(安装时发布) +│ │ ├── core_api.h +│ │ ├── codec_api.h +│ │ ├── node_api.h +│ │ ├── render_api.h +│ │ ├── audio_api.h +│ │ ├── plugin_api.h +│ │ ├── ui_api.h +│ │ └── olive_api.h # 总入口,包含所有模块 +│ └── olivecpp/ # 主进程内部使用的 C++ 辅助封装 +│ ├── module_loader.h +│ ├── core_wrapper.h // RAII 包装类 +│ ├── node_wrapper.h +│ └── ... +└── src/ + ├── core_api.cpp // 对应各模块的 C 封装实现 + ├── codec_api.cpp + ├── node_api.cpp + ├── render_api.cpp + ├── audio_api.cpp + ├── plugin_api.cpp + └── ui_api.cpp +``` + +### 10.2 C API 头文件示例 + +```c +// c_api/include/olive/node_api.h +#ifndef OLIVE_NODE_API_H +#define OLIVE_NODE_API_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "core_api.h" + +#define OLIVE_NODE_API_VERSION 1 + +/* ========== 类型前向声明 ========== */ +typedef struct OliveNodeGraph OliveNodeGraph; +typedef struct OliveNode OliveNode; +typedef struct OliveParam OliveParam; + +/* ========== 函数导出宏 ========== */ +#ifdef OLIVE_BUILDING_NODE +# define OLIVE_NODE_API __attribute__((visibility("default"))) +#else +# define OLIVE_NODE_API +#endif + +/* ========== API 版本 ========== */ +OLIVE_NODE_API int olive_node_api_version(void); + +/* ========== NodeGraph ========== */ +OLIVE_NODE_API OliveNodeGraph* olive_node_graph_create(void); +OLIVE_NODE_API void olive_node_graph_destroy(OliveNodeGraph* g); + +OLIVE_NODE_API int olive_node_graph_load_xml(OliveNodeGraph* g, + const char* xml_data, + size_t xml_len); +OLIVE_NODE_API char* olive_node_graph_save_xml(OliveNodeGraph* g, + size_t* out_len); + +OLIVE_NODE_API OliveNode* olive_node_graph_find_node(OliveNodeGraph* g, + const char* node_id); +OLIVE_NODE_API int olive_node_graph_add_node(OliveNodeGraph* g, + const char* node_type, + const char* node_id); + +/* ========== Node ========== */ +OLIVE_NODE_API const char* olive_node_get_id(OliveNode* node); +OLIVE_NODE_API const char* olive_node_get_type_name(OliveNode* node); + +OLIVE_NODE_API int olive_node_connect(OliveNode* from_node, + int from_output_index, + OliveNode* to_node, + int to_input_index); + +/* ========== Param ========== */ +OLIVE_NODE_API int olive_node_set_param_int(OliveNode* node, + const char* param_name, + int64_t value); +OLIVE_NODE_API int olive_node_set_param_double(OliveNode* node, + const char* param_name, + double value); +OLIVE_NODE_API int olive_node_set_param_rational(OliveNode* node, + const char* param_name, + OliveRational value); +OLIVE_NODE_API int olive_node_set_param_string(OliveNode* node, + const char* param_name, + const char* value); + +/* ========== Project ========== */ +OLIVE_NODE_API OliveNodeGraph* olive_project_create(const char* name); +OLIVE_NODE_API int olive_project_load_file(OliveNodeGraph* project, + const char* filename); +OLIVE_NODE_API int olive_project_save_file(OliveNodeGraph* project, + const char* filename); + +#ifdef __cplusplus +} +#endif + +#endif // OLIVE_NODE_API_H +``` + +--- + +## 11. CMake 中的 C API 编译 + +### 11.1 为模块添加 C API 目标 + +```cmake +# app/node/CMakeLists.txt + +# 原有 C++ 源码(内部实现,不暴露头文件) +set(NODE_INTERNAL_SOURCES + node.cpp node.h + traverser.cpp traverser.h + project/project.cpp project/project.h + # ... 其他内部文件 +) + +# C API 封装层源码 +set(NODE_API_SOURCES + ${CMAKE_SOURCE_DIR}/c_api/src/node_api.cpp +) + +# 模块对外头文件(安装时发布) +set(NODE_API_HEADERS + ${CMAKE_SOURCE_DIR}/c_api/include/olive/node_api.h +) + +# 创建动态库 +add_library(olivenode SHARED + ${NODE_INTERNAL_SOURCES} + ${NODE_API_SOURCES} +) + +target_compile_definitions(olivenode PRIVATE OLIVE_BUILDING_NODE) +target_include_directories(olivenode + PRIVATE + ${CMAKE_SOURCE_DIR}/app + ${CMAKE_SOURCE_DIR}/c_api/include + PUBLIC + $ +) + +target_link_libraries(olivenode + PRIVATE + olivecore + olivecodec + Qt${QT_VERSION_MAJOR}::Core +) + +# 设置符号可见性:默认隐藏,只有标记 OLIVE_NODE_API 的才导出 +set_target_properties(olivenode PROPERTIES + CXX_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN YES +) + +# 安装 C API 头文件 +install(FILES ${NODE_API_HEADERS} DESTINATION include/olive) +install(TARGETS olivenode DESTINATION lib) +``` + +### 11.2 主可执行文件不链接业务库 + +```cmake +# app/CMakeLists.txt(改造后) + +add_executable(olive-editor + main.cpp + core.cpp + core.h + ${CMAKE_SOURCE_DIR}/c_api/src/module_loader.cpp # 显式加载器实现 +) + +# 主程序只链接 Qt 和系统库,不链接 olivecore/olivecodec 等业务库! +target_link_libraries(olive-editor PRIVATE + Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Gui + Qt${QT_VERSION_MAJOR}::Widgets + # ... 其他 UI 依赖 +) + +target_include_directories(olive-editor PRIVATE + ${CMAKE_SOURCE_DIR}/c_api/include +) +``` + +--- + +## 12. 测试策略 + +### 12.1 C API 单元测试 + +为每个 C API 函数编写独立测试: + +```cpp +// tests/c_api/test_node_api.cpp +#include +#include "olive/node_api.h" + +TEST(NodeAPITest, CreateDestroy) { + OliveNodeGraph* g = olive_node_graph_create(); + ASSERT_NE(g, nullptr); + olive_node_graph_destroy(g); +} + +TEST(NodeAPITest, AddNodeAndParam) { + OliveNodeGraph* g = olive_node_graph_create(); + ASSERT_EQ(OLIVE_OK, olive_node_graph_add_node(g, "Transform", "T1")); + OliveNode* n = olive_node_graph_find_node(g, "T1"); + ASSERT_NE(n, nullptr); + ASSERT_EQ(OLIVE_OK, olive_node_set_param_double(n, "position_x", 100.0)); + olive_node_graph_destroy(g); +} +``` + +### 12.2 ABI 稳定性测试 + +在 CI 中: +1. 编译当前版本的动态库。 +2. 用上一个发布版本的测试可执行文件加载当前动态库运行。 +3. 验证所有测试通过(确保未破坏 ABI)。 + +--- + +## 13. 常见陷阱 + +| 陷阱 | 说明 | 对策 | +|---|---|---| +| **异常穿透 C 边界** | C++ 异常抛出到 C 调用方是 UB。 | 所有 C API 函数用 `try/catch(...)` 包裹,捕获所有异常并转换为错误码。 | +| **RTTI 跨边界** | `dynamic_cast` 在不同动态库间可能失败。 | C 接口不使用 RTTI,内部若必须 `dynamic_cast`,确保类型定义在同一个库内。 | +| **Qt 元对象跨库** | `qobject_cast` 依赖 moc 生成的静态元对象数据,跨库时可能失效。 | 不在 C API 中暴露 Qt 对象,所有 Qt 对象封装在库内部。 | +| **全局静态变量** | 多个动态库各有一份全局静态变量。 | 避免在 C API 头文件中定义全局静态变量,使用函数内 static + 首次调用初始化。 | +| **堆不一致(Windows)** | A 库 `malloc`,B 库 `free` 导致崩溃。 | 严格遵循"谁分配谁释放",使用库提供的 `olive_core_free()`。 | diff --git a/docs/zh/modularization-plan/10-implementation-roadmap.md b/docs/zh/modularization-plan/10-implementation-roadmap.md new file mode 100644 index 000000000..12a5e9eb1 --- /dev/null +++ b/docs/zh/modularization-plan/10-implementation-roadmap.md @@ -0,0 +1,345 @@ +# 10 周小步快跑实施路线图 + +> 本路线图为**渐进式、可回退、可并行**的实施计划。每一周都有明确的交付物和验收标准。任何一周的任务如果超时或遇到阻碍,都可以独立回退或跳过,不影响其他周的进度。 + +--- + +## 关键原则 + +1. **不改现有代码,只增代码**:每一周的改造都通过新增文件(`c_api/`、`app/render/renderer_main.cpp` 等)完成,现有源文件尽量不动。只有必须解耦的地方才修改现有头文件。 +2. **编译开关控制**:通过 CMake 选项 `-DOLIVE_DYNAMIC_MODULES=ON` 控制是否走动态库路径。默认 OFF,确保主干始终可编译可运行。 +3. **每周可独立验证**:每周结束都有一个可运行的版本,即使后续周不开始,当前成果也是有价值的。 +4. **先易后难**:从耦合最低的模块(core、codec)开始,积累经验和工具链,最后攻克最难的 node/render。 + +--- + +## 人员分工建议(假设 2–3 人) + +| 角色 | 负责内容 | +|---|---| +| **基础设施工程师** | ModuleLoader、CMake 改造、CI 适配、C API 规范执行 | +| **编解码工程师** | `libolivecore.so`、`libolivecodec.so`、`liboliveaudio.so` | +| **渲染工程师** | `libolivenode.so` 解耦、`liboliverender.so`、`olive-renderer` 多进程 | +| **UI 工程师** | `liboliveui.so`、主进程集成、ViewerWidget 改造 | + +**注意**:初期阶段(Week 1–3)只需 1 人负责基础设施 + 1 人负责编解码即可。渲染和 UI 改造在 Week 4 之后全面展开。 + +--- + +## Week 1:基础设施与工具链(1 人主责) + +### 目标 +建立 C API 基础设施,验证显式加载工具链在所有目标平台正常工作。 + +### 任务清单 + +- [ ] **T1.1** 创建目录结构 `c_api/include/olive/`, `c_api/src/`, `c_api/tests/`。 +- [ ] **T1.2** 编写 `ModuleLoader` 类(`c_api/src/module_loader.h/cpp`),支持 POSIX + Windows。 +- [ ] **T1.3** 定义全局 C API 规范文件:`c_api/include/olive/core_api.h` 的基础部分(`OliveResult`, `OliveRational`, 内存管理函数)。 +- [ ] **T1.4** 编写最小测试动态库 `c_api/tests/test_module/`(只导出一个 `int test_add(int, int)`),验证 `ModuleLoader` 可以正确加载和调用。 +- [ ] **T1.5** 在 CMake 中新增 `OLIVE_DYNAMIC_MODULES` 选项(默认 OFF)。 +- [ ] **T1.6** 在 CI(GitHub Actions)中增加一个 job:开启 `OLIVE_DYNAMIC_MODULES=ON` 编译,验证 Linux/macOS/Windows 三平台。 + +### 验收标准 + +```bash +# 运行测试 +./tests/c_api/test_module_loader +# 输出: +# [PASS] Load test_module.so +# [PASS] Call test_add(2, 3) = 5 +# [PASS] Unload test_module.so +``` + +### 回退策略 + +若 `ModuleLoader` 在某平台工作异常,该周可仅完成 POSIX 平台,Windows 平台延后处理。 + +--- + +## Week 2:libolivecore.so(1 人主责) + +### 目标 +将 `ext/core/` 从静态库改造为显式加载的动态库,建立第一个完整的 C API。 + +### 任务清单 + +- [ ] **T2.1** 修改 `ext/core/CMakeLists.txt`:`add_library(olivecore SHARED ...)`,添加 `CXX_VISIBILITY_PRESET hidden`。 +- [ ] **T2.2** 为 `ext/core/` 中需要跨库使用的类添加导出宏(或保持 C API 头文件中的 `OLIVE_CORE_API`)。 +- [ ] **T2.3** 完成 `c_api/include/olive/core_api.h`:Rational, Color, TimeRange, Timecode, PixelFormat, SampleBuffer, VideoParams, AudioParams。 +- [ ] **T2.4** 编写 `c_api/src/core_api.cpp`,封装所有上述类型。 +- [ ] **T2.5** 编写单元测试 `tests/c_api/test_core_api.cpp`。 +- [ ] **T2.6** 在主进程 `Core::Start()` 中尝试显式加载 `libolivecore.so`,失败时打印警告但不阻塞启动。 + +### 验收标准 + +```cpp +ModuleLoader loader; +loader.Load("core", "./libolivecore.so"); +auto make = loader.GetFunction("core", "olive_rational_make"); +auto add = loader.GetFunction("core", "olive_rational_add"); +OliveRational r = add(make(1,2), make(1,3)); +assert(r.num == 5 && r.den == 6); +``` + +### 回退策略 + +若 C API 封装工作量超预期,本周可只完成 Rational + TimeRange 的最小子集,其余类型后续补充。 + +--- + +## Week 3:libolivecodec.so(1 人主责) + +### 目标 +将 `app/codec/` + `app/common/` 封装为显式加载动态库,实现解码器的 C API。 + +### 任务清单 + +- [ ] **T3.1** 将 `app/common/` 的源文件并入 `app/codec/CMakeLists.txt`。 +- [ ] **T3.2** 创建 `libolivecodec.so` 的 SHARED 目标。 +- [ ] **T3.3** 编写 `c_api/include/olive/codec_api.h`(最小子集):MediaInfo, Decoder, Frame。 +- [ ] **T3.4** 编写 `c_api/src/codec_api.cpp`。 +- [ ] **T3.5** 编写测试:加载视频文件 → 解码第一帧 → 验证宽高 > 0。 +- [ ] **T3.6** 在主进程中显式加载 `libolivecodec.so`。 + +### 验收标准 + +```cpp +auto decoder = olive_decoder_create(nullptr); +olive_decoder_open(decoder, "test.mp4", 0); +OliveFrame* frame = nullptr; +olive_decoder_decode_video(decoder, olive_rational_make(0,1), &frame); +assert(olive_frame_width(frame) == 1920); +olive_frame_destroy(frame); +olive_decoder_destroy(decoder); +``` + +### 回退策略 + +若 `common/` 中有代码依赖 `node/` 或 `render/`,先将这些代码移回主库,再继续。 + +--- + +## Week 4:libolivenode.so 解耦(2 人并行,最关键的一周) + +### 目标 +解决 `Node.h` 对 `render/` 的头文件依赖,为 `libolivenode.so` 的独立编译扫清障碍。 + +### 任务清单 + +- [ ] **T4.1** 创建 `app/node/nodecachecallbacks.h`,定义 `NodeCacheCallbacks` 纯虚接口。 +- [ ] **T4.2** 修改 `app/node/node.h`: + - 移除 `#include "render/rendercache.h"`。 + - 添加 `NodeCacheCallbacks* cache_callbacks_` 和 `SetCacheCallbacks()`。 + - 将 `InvalidateCache` 相关逻辑改为调用 `cache_callbacks_->InvalidateCache()`。 +- [ ] **T4.3** 创建 `app/node/jobtypes.h`,定义 `NodeJobType` 和 `NodeJobData`。 +- [ ] **T4.4** 修改 `Node.h` 中的 `ProcessXxx` 虚函数签名,使用 `NodeJobData`。 +- [ ] **T4.5** 修改 `RenderProcessor`,适配新的 `NodeCacheCallbacks` 和 `NodeJobData`。 +- [ ] **T4.6** 验证 `app/node/` 目录可以独立编译(写一个临时 CMake 测试)。 + +### 验收标准 + +```bash +cd /tmp && cmake /path/to/oak/app/node && make +# 编译成功,不报错 +``` + +### 回退策略 + +**若解耦工作量超预期**:允许 `node/` 和 `render/` 暂时合并为 `libolive-engine.so`。这是最重要的回退策略——宁可合并也不阻塞进度。合并后仍可继续封装 C API,后续再拆分。 + +--- + +## Week 5:libolivenode.so C API + liboliveplugin.so(2 人并行) + +### 目标 +完成节点图系统的 C API,并将 OFX 插件宿主独立为动态库。 + +### 任务清单(节点图工程师) + +- [ ] **T5.1** 创建 `libolivenode.so`,聚合 `node/`, `timeline/`, `undo/`, `config/`。 +- [ ] **T5.2** 编写 `c_api/include/olive/node_api.h` 和 `c_api/src/node_api.cpp`。 +- [ ] **T5.3** 实现最小 C API:NodeGraph create/destroy, load_xml/save_xml, add_node, connect, param set/get。 +- [ ] **T5.4** 编写测试:用 C API 构建一个 Generator -> ViewerOutput 的图,序列化后反序列化验证。 + +### 任务清单(插件工程师) + +- [ ] **T5.5** 将 `pluginSupport/` 从主 OBJECT 库移出,创建 `liboliveplugin.so`。 +- [ ] **T5.6** 编写 `c_api/include/olive/plugin_api.h`(最小子集):host create/destroy, add_path, rescan, plugin count/get。 +- [ ] **T5.7** 验证主进程可以扫描 OFX 插件目录并列出插件名称。 + +### 验收标准 + +```cpp +// 节点图测试 +OliveNodeGraph* g = olive_node_graph_create(); +olive_node_graph_add_node(g, "SolidGenerator", "Solid1"); +olive_node_graph_add_node(g, "ViewerOutput", "Viewer1"); +OliveNode* solid = olive_node_graph_find_node(g, "Solid1"); +OliveNode* viewer = olive_node_graph_find_node(g, "Viewer1"); +olive_node_connect(solid, 0, viewer, 0); +size_t len; +char* xml = olive_node_graph_save_xml(g, &len); +assert(len > 0); +olive_core_free(xml); +``` + +### 回退策略 + +若 `node/` C API 工作量过大,优先保证 `load_xml` / `save_xml` / `find_node` 三个函数(这是渲染子进程最需要的),其余延后。 + +--- + +## Week 6:olive-renderer 单帧端到端(2 人并行) + +### 目标 +实现第一个可用的 `olive-renderer` 可执行文件,能渲染一帧测试图。 + +### 任务清单 + +- [ ] **T6.1** 编写 `app/render/renderer_main.cpp`,实现命令行解析。 +- [ ] **T6.2** 实现 `olive_shm_create/open/close/unlink`(POSIX + Windows)。 +- [ ] **T6.3** 在 `olive-renderer` 中集成:加载 XML → 初始化 OpenGL → 渲染 → 写入 SHM → 输出 JSON。 +- [ ] **T6.4** 编写主进程中的 `RenderProcessLauncher::RenderFrameSync`。 +- [ ] **T6.5** 端到端测试:主进程启动 `olive-renderer` 渲染一帧纯色,验证 SHM 中的像素值正确。 + +### 验收标准 + +```bash +# 命令行直接测试 +olive-renderer --mode=frame --node-graph=test_solid.xml --time=0/1 \ + --video-params='{"width":100,"height":100,"format":"rgba32f"}' \ + --output-shm=/olive_test --output-shm-size=160000 +# 输出: +# {"status":"ok","width":100,"height":100,"format":"rgba32f",...} +``` + +### 回退策略 + +若 OpenGL 离屏上下文初始化在某平台失败,该平台暂时使用 `--backend=dummy`(只测试进程模型,不测试实际渲染)。 + +--- + +## Week 7:ViewerWidget 集成 + 用完即弃验证(2 人并行) + +### 目标 +将 `olive-renderer` 集成到主进程的 Viewer 中,验证"用完即弃"模型在实际场景中的可行性。 + +### 任务清单 + +- [ ] **T7.1** 修改 `ViewerWidget`:从 `RenderManager::RenderFrame()` 改为启动 `olive-renderer`。 +- [ ] **T7.2** 实现异步完成回调:`OnRenderProcessFinished()` 读取 SHM 并更新 Texture。 +- [ ] **T7.3** 处理子进程崩溃:崩溃时忽略该帧,保持上一帧显示,记录日志。 +- [ ] **T7.4** 测量实际性能:拖动时间线时的帧率、CPU 占用、进程启动耗时。 +- [ ] **T7.5** 若性能不达标,实现批处理模式(`--mode=batch`)。 + +### 验收标准 + +- 打开一个简单项目(单轨道 + 纯色生成器),拖动时间线,Viewer 实时更新。 +- `kill -9` 一个渲染子进程,主进程不崩溃,Viewer 保持显示。 + +### 回退策略 + +若"用完即弃"性能完全不可接受(如帧率 < 5fps),立即切换为**批处理模式**或**进程池模式**(预启动 N 个进程,循环使用,每个进程渲染一批后自杀)。 + +--- + +## Week 8:liboliveaudio.so + liboliveui.so(2 人并行) + +### 目标 +完成音频库和 UI 库的动态库拆分。 + +### 任务清单(音频工程师) + +- [ ] **T8.1** 创建 `liboliveaudio.so`。 +- [ ] **T8.2** 编写 `audio_api.h/cpp`(最小子集):manager init/play/pause/push_buffer。 +- [ ] **T8.3** 验证音频播放通过 C API 正常工作。 + +### 任务清单(UI 工程师) + +- [ ] **T8.1** 聚合所有 UI 源文件,创建 `liboliveui.so`。 +- [ ] **T8.2** 解耦 `Core` 类中的 UI 逻辑(`StartGUI()` 迁移到 `liboliveui.so` 内部)。 +- [ ] **T8.3** 主进程显式加载 `liboliveui.so`,成功启动主窗口。 +- [ ] **T8.4** 编写 `ui_api.h`(最小子集):app create/exec, main_window show, open_project。 + +### 验收标准 + +- 主程序通过显式加载 `liboliveui.so` 启动,界面正常。 +- 音频播放正常(可听到声音)。 + +--- + +## Week 9:导出集成 + 稳定性打磨(2 人并行) + +### 目标 +将导出流程集成到多进程渲染模型,全面稳定性测试。 + +### 任务清单 + +- [ ] **T9.1** 修改导出任务(`task/export/`),使用 `olive-renderer` 逐帧/逐批渲染。 +- [ ] **T9.2** 导出天然适合批处理:一次性发送 10–50 帧给子进程。 +- [ ] **T9.3** 编写压力测试:连续渲染 100 帧,验证无内存泄漏、无共享内存泄漏。 +- [ ] **T9.4** 测试 OFX 插件在子进程中的渲染(选择几个免费 OFX 插件测试)。 +- [ ] **T9.5** 测试崩溃场景:`kill -9` 随机子进程,验证主进程稳定。 + +### 验收标准 + +- 成功导出一个 10 秒视频(300 帧),画面正确。 +- 连续启动 100 个渲染子进程,系统无共享内存泄漏(`ls /dev/shm/` 检查)。 + +--- + +## Week 10:打包适配 + 文档 + 性能优化(2 人并行) + +### 目标 +完成打包脚本适配,编写用户文档,进行最终性能优化。 + +### 任务清单 + +- [ ] **T10.1** 更新 macOS 打包脚本:确保 `libolive*.dylib` 和 `olive-renderer` 被打入 `.app` Bundle,`@rpath` 设置正确。 +- [ ] **T10.2** 更新 Windows 打包脚本:确保 `.dll` 和 `olive-renderer.exe` 在_installer 中。 +- [ ] **T10.3** 更新 Linux AppImage 打包:确保动态库在 AppImage 内可加载。 +- [ ] **T10.4** 更新 `docs/build.md` 和 `docs/build-macos-zh.md`,说明新的运行时依赖。 +- [ ] **T10.5** 性能优化: + - 节点图 XML 缓存(相同图只序列化一次)。 + - 共享内存预分配池(避免反复创建/销毁)。 + - 批处理大小动态调整(根据上一批的渲染时间调整下一批的帧数)。 +- [ ] **T10.6** 全面回归测试:导入、编辑、预览、导出、Undo/Redo、保存/加载项目。 + +### 验收标准 + +- 在三平台上都能通过 `make install` 或打包脚本生成可分发包。 +- 新用户按照 `build.md` 可以成功编译并运行。 +- 与 Week 0(改造前)相比,导出速度不差于 90%,预览帧率不差于 70%。 + +--- + +## 并行工作流 + +``` +Week 1: [基础设施] +Week 2: [core] (依赖 W1) +Week 3: [codec] (依赖 W2) +Week 4: [node 解耦] (可并行 W3, 但建议 W3 后启动) +Week 5: [node C API] + [plugin] (依赖 W4) +Week 6: [renderer] (依赖 W5) +Week 7: [Viewer 集成] (依赖 W6) +Week 8: [audio] + [ui] (可并行 W7) +Week 9: [导出集成] (依赖 W7) +Week 10: [打包/文档/优化] (依赖 W9) +``` + +**最大并行度**:Week 8 时可以有 3 人同时工作(1 人 audio,1 人 ui,1 人优化 renderer)。 + +--- + +## 回退总策略 + +| 场景 | 回退方案 | +|---|---| +| 某周任务无法按时完成 | 将该周剩余任务移到下一周,当前周只交付已完成部分。 | +| `node/` 解耦完全不可行 | 将 `node/` + `render/` 合并为 `libolive-engine.so`,后续再拆分。 | +| "用完即弃"性能完全不可接受 | 切换为"批处理模式"(每进程渲染 N 帧),或"进程池模式"(预启动 N 个进程)。 | +| 动态库在某平台加载失败 | 该平台暂时保持静态链接,其他平台先用动态库。 | +| C API 维护成本过高 | 保留 C API 用于子进程通信,主进程内部恢复直接 C++ 链接(但库仍编译为动态库,由操作系统隐式加载)。 | +| 项目期限紧张 | 优先完成 `olive-renderer` 多进程(核心价值),动态库拆分可以延后。 | diff --git a/docs/zh/modularization-plan/11-ipc-protocol.md b/docs/zh/modularization-plan/11-ipc-protocol.md new file mode 100644 index 000000000..78838d7f2 --- /dev/null +++ b/docs/zh/modularization-plan/11-ipc-protocol.md @@ -0,0 +1,479 @@ +# IPC 协议规范 + +> 本文件定义主进程与 `olive-renderer` 子进程之间的全部通信方式。由于采用**"用完即弃"**模型,协议被设计为**极简、无状态、单向请求-响应**。 + +--- + +## 1. 通信通道概览 + +| 通道 | 方向 | 用途 | 格式 | +|---|---|---|---| +| **命令行参数** | 主进程 → 子进程 | 传递渲染配置(模式、路径、参数) | POSIX 风格长选项 | +| **stdin** | 主进程 → 子进程 | 传递节点图 XML(当 XML 过大超出命令行长度限制时) | 原始 XML 字符串 | +| **stdout** | 子进程 → 主进程 | 返回渲染结果元数据 | 单行 JSON | +| **stderr** | 子进程 → 主进程 | 日志和详细错误信息 | 纯文本 | +| **共享内存 / 内存映射文件** | 双向 | 传输大帧数据(像素/采样) | 二进制(ShmHeader + 原始数据) | +| **退出码** | 子进程 → 主进程 | 快速判断成功/失败/异常 | 整数 (0–255) | + +**重要**:由于子进程渲染完成后立即退出,不存在**长期状态同步**、**心跳**、**取消信号**(主进程直接 `kill` 即可)。 + +--- + +## 2. 命令行参数 + +### 2.1 参数总表 + +| 参数 | 类型 | 必需 | 默认值 | 说明 | +|---|---|---|---|---| +| `--mode` | string | 否 | `frame` | 渲染模式:`frame`, `batch`, `audio` | +| `--node-graph` | path | 是 | — | 节点图 XML 文件路径 | +| `--node-graph-stdin` | flag | 否 | false | 从 stdin 读取节点图 XML,而非文件 | +| `--output-node` | string | 否 | 自动探测 | 输出节点 ID | +| `--time` | rational | 条件 | — | 单帧时间点(`--mode=frame` 时必需) | +| `--times` | csv | 条件 | — | 多帧时间点列表(`--mode=batch` 时必需) | +| `--start` | rational | 条件 | — | 音频起始时间(`--mode=audio` 时必需) | +| `--duration` | rational | 条件 | — | 音频持续时间(`--mode=audio` 时必需) | +| `--video-params` | json | 条件 | — | 视频参数(`--mode=frame`/`batch` 时必需) | +| `--audio-params` | json | 条件 | — | 音频参数(`--mode=audio` 时必需) | +| `--color-ref` | string | 否 | — | 参考色彩空间名称 | +| `--color-display` | string | 否 | — | 显示色彩空间名称 | +| `--force-size` | json | 否 | — | 强制输出尺寸,如 `{"width":1920,"height":1080}` | +| `--force-format` | string | 否 | — | 强制像素格式,如 `rgba32f` | +| `--output-shm` | string | 是 | — | 输出共享内存名称或临时文件路径 | +| `--output-shm-size` | int | 是 | — | 输出缓冲区大小(字节) | +| `--output-stdout` | flag | 否 | false | 将帧数据 base64 编码输出到 stdout(仅小帧/测试) | +| `--backend` | string | 否 | `opengl` | 渲染后端:`opengl`, `dummy` | +| `--shader-path` | path | 否 | `/shaders` | 着色器资源目录 | +| `--ocio-config` | path | 否 | — | OCIO 配置文件路径 | +| `--verbose` | flag | 否 | false | 详细日志输出到 stderr | +| `--version` | flag | 否 | false | 输出版本信息并退出 | +| `--help` | flag | 否 | false | 输出帮助信息并退出 | + +### 2.2 参数值格式 + +**Rational**:`"/"`,如 `"1001/30000"`, `"0/1"`。 + +**JSON**:紧凑格式,键用双引号。例如: +``` +--video-params='{"width":1920,"height":1080,"format":"rgba32f","channel_count":4,"depth":1}' +``` + +**CSV**:逗号分隔的有理数字符串。例如: +``` +--times="0/24,1/24,2/24,3/24,4/24" +``` + +--- + +## 3. 标准输入(stdin) + +当 `--node-graph-stdin` 标志存在时,子进程从 stdin 读取节点图 XML,而不是从 `--node-graph` 指定的文件。 + +### 3.1 使用场景 + +- 节点图 XML 非常大(> 100KB),超出命令行长度限制。 +- 主进程不想在磁盘上创建临时文件。 +- 安全考虑:敏感项目数据不写入磁盘。 + +### 3.2 协议 + +``` +主进程 子进程 + │ │ + │── XML 数据 ──>│(子进程读取 stdin 直到 EOF) + │ │ +``` + +子进程读取 stdin 的全部内容,视为节点图 XML 字符串。XML 结束后不需要特殊分隔符(EOF 即结束)。 + +**注意**:由于子进程使用 `QCoreApplication` 且不使用 Qt 的事件循环读取 stdin,应使用阻塞式 `QTextStream(stdin).readAll()` 或 `std::cin`。 + +--- + +## 4. 标准输出(stdout) + +子进程将渲染结果以**单行 JSON** 输出到 stdout,以换行符 `\n` 结尾。主进程读取第一行后即视为响应完成。 + +### 4.1 单帧模式输出(`--mode=frame`) + +**成功:** +```json +{ + "status": "ok", + "mode": "frame", + "time": "1001/30000", + "width": 1920, + "height": 1080, + "format": "rgba32f", + "pixel_format_id": 28, + "channel_count": 4, + "shm_name": "/olive_frame_abc123", + "data_offset": 256, + "data_size": 33177600, + "linesize": 7680, + "render_time_ms": 42 +} +``` + +**失败:** +```json +{ + "status": "error", + "mode": "frame", + "time": "1001/30000", + "error_code": "decoder_failure", + "message": "Failed to open decoder for footage 'clip001.mp4'", + "render_time_ms": 5 +} +``` + +### 4.2 批处理模式输出(`--mode=batch`) + +**成功:** +```json +{ + "status": "ok", + "mode": "batch", + "frame_count": 5, + "frames": [ + {"time": "0/24", "data_offset": 256, "data_size": 33177600, "render_time_ms": 45}, + {"time": "1/24", "data_offset": 33178056, "data_size": 33177600, "render_time_ms": 38}, + {"time": "2/24", "data_offset": 66356112, "data_size": 33177600, "render_time_ms": 41}, + {"time": "3/24", "data_offset": 99534168, "data_size": 33177600, "render_time_ms": 39}, + {"time": "4/24", "data_offset": 132712224, "data_size": 33177600, "render_time_ms": 42} + ], + "total_render_time_ms": 205 +} +``` + +### 4.3 音频模式输出(`--mode=audio`) + +**成功:** +```json +{ + "status": "ok", + "mode": "audio", + "start": "0/1", + "duration": "48000/48000", + "sample_rate": 48000, + "channels": 2, + "sample_count": 48000, + "shm_name": "/olive_audio_abc123", + "data_offset": 256, + "data_size": 384000, + "render_time_ms": 15 +} +``` + +### 4.4 字段说明 + +| 字段 | 类型 | 出现条件 | 说明 | +|---|---|---|---| +| `status` | string | 总是 | `"ok"`, `"error"`, `"cancelled"` | +| `mode` | string | 总是 | `"frame"`, `"batch"`, `"audio"` | +| `time` / `times` | string | frame/batch | 渲染时间点 | +| `width` | int | frame/batch | 帧宽 | +| `height` | int | frame/batch | 帧高 | +| `format` | string | frame/batch | 像素格式名称 | +| `pixel_format_id` | int | frame/batch | 像素格式枚举值 | +| `channel_count` | int | frame/batch | 通道数 | +| `shm_name` | string | 总是 | 共享内存名称 | +| `data_offset` | int | 总是 | 实际数据在共享内存中的偏移(跳过 ShmHeader) | +| `data_size` | int | 总是 | 实际数据大小(字节) | +| `linesize` | int | frame/batch | 每行字节数(含 padding) | +| `frame_count` | int | batch | 批处理帧数 | +| `frames` | array | batch | 每帧的元数据 | +| `sample_rate` | int | audio | 采样率 | +| `channels` | int | audio | 通道数 | +| `sample_count` | int | audio | 采样数 | +| `error_code` | string | error | 错误分类码 | +| `message` | string | error | 人类可读错误信息 | +| `render_time_ms` | int | 总是 | 纯渲染耗时(不含进程启动) | + +--- + +## 5. 共享内存二进制布局 + +### 5.1 整体结构 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 共享内存区域 │ +├────────────────────────┬────────────────────────────────────────┤ +│ ShmHeader (256 B) │ Payload Data │ +│ │ │ +│ magic │ 帧像素数据 / 音频采样数据 │ +│ version │ │ +│ data_offset │ 大小 = data_size │ +│ data_size │ │ +│ width │ │ +│ height │ │ +│ format │ │ +│ linesize │ │ +│ checksum │ │ +│ reserved[...] │ │ +└────────────────────────┴────────────────────────────────────────┘ +``` + +### 5.2 ShmHeader 定义(C 结构) + +```c +#include + +#define OLIVE_SHM_MAGIC 0x4F4C4956 // 'OLIV' 大端序 +#define OLIVE_SHM_VERSION 1 +#define OLIVE_SHM_HEADER_SIZE 256 + +typedef struct { + uint32_t magic; // OLIVE_SHM_MAGIC + uint32_t version; // OLIVE_SHM_VERSION + uint32_t data_offset; // Payload 数据起始偏移(>= 256) + uint64_t data_size; // Payload 实际数据大小(字节) + uint32_t width; // 帧宽 或 采样数 + uint32_t height; // 帧高 或 0(音频) + uint32_t depth; // 3D 纹理深度(通常 1) + uint32_t channel_count; // 通道数 + uint32_t pixel_format; // OlivePixelFormat 枚举值 + uint32_t linesize; // 每行字节数(视频)或 0(音频) + uint64_t checksum; // CRC64 校验和(可选,0 表示未校验) + uint8_t reserved[256 - 48]; // 填充至 256 字节,未来扩展用 +} OliveShmHeader; + +// 辅助:计算 CRC64 +uint64_t olive_shm_checksum(const void* data, size_t size); +``` + +### 5.3 校验和(Checksum) + +- 默认启用 CRC64 校验。 +- `checksum` 字段覆盖 **Payload Data 区域**(从 `data_offset` 开始的 `data_size` 字节)。 +- 若主进程设置 `checksum = 0`,表示跳过校验(用于调试或性能敏感场景)。 + +### 5.4 多帧批处理布局 + +批处理模式下,所有帧连续存储在同一块共享内存中: + +``` +Offset 0: ShmHeader (256 B) +Offset 256: Frame 0 data (frame_0_size B, 按 linesize 对齐) +Offset 256+N0: Frame 1 data (frame_1_size B) +Offset 256+N0+N1: Frame 2 data +... +``` + +每帧的 `data_offset` 在 stdout JSON 中单独指定。 + +--- + +## 6. 标准错误(stderr) + +### 6.1 日志级别 + +当 `--verbose` 启用时,stderr 输出结构化日志: + +``` +[2024-05-21T10:30:15.123Z] [INFO] 初始化 OpenGL 上下文 +[2024-05-21T10:30:15.245Z] [INFO] OpenGL 版本: 4.6.0 NVIDIA 535.104 +[2024-05-21T10:30:15.310Z] [INFO] 加载节点图: 42 个节点 +[2024-05-21T10:30:15.412Z] [INFO] 开始渲染帧 @ 1001/30000 +[2024-05-21T10:30:15.454Z] [INFO] 渲染完成, 耗时 42ms +[2024-05-21T10:30:15.455Z] [INFO] 写入共享内存: /olive_frame_abc123, 33177600 bytes +``` + +### 6.2 错误日志 + +错误同时输出到 stderr 和 stdout JSON: + +``` +[2024-05-21T10:30:15.456Z] [ERROR] Decoder 初始化失败: codec not found for 'hevc' +``` + +--- + +## 7. 退出码 + +| 退出码 | 名称 | 含义 | 主进程应对 | +|---|---|---|---| +| 0 | `EXIT_OK` | 渲染成功 | 正常处理结果 | +| 1 | `EXIT_GENERIC_ERROR` | 通用错误 | 记录错误,丢弃该帧 | +| 2 | `EXIT_INVALID_ARGS` | 命令行参数无效 | 检查主进程参数组装逻辑 | +| 3 | `EXIT_INIT_FAILED` | 初始化失败(OpenGL/OCIO) | 尝试 `dummy` 后端或提示用户 | +| 4 | `EXIT_GRAPH_LOAD_FAILED` | 节点图加载/解析失败 | 检查 XML 序列化逻辑 | +| 5 | `EXIT_RENDER_FAILED` | 渲染过程中出错 | 记录具体错误,丢弃该帧 | +| 6 | `EXIT_OUTPUT_FAILED` | 输出写入失败(SHM 不足) | 清理 SHM,重试或报错 | +| 130 | `EXIT_SIGINT` | 收到 SIGINT(Ctrl+C / kill -2) | 视为取消,正常丢弃 | +| 137 | `EXIT_SIGKILL` | 收到 SIGKILL(kill -9) | 视为取消,正常丢弃 | +| 139 | `EXIT_SEGFAULT` | 段错误(未捕获信号) | 视为崩溃,记录日志 | +| 其他 | — | 未知错误 | 记录日志,丢弃该帧 | + +--- + +## 8. 主进程与子进程交互时序 + +### 8.1 单帧完整时序 + +``` +时间轴 ──────────────────────────────────────────────────────────────> + +主进程: [准备参数] [创建SHM] [QProcess::start()] [等待] [读JSON] [读SHM] [unlink SHM] + │ │ │ │ │ │ │ +子进程: [启动] [解析参数] [加载XML] [初始化GL] [渲染] [写SHM] [写stdout] [exit] + │ │ │ │ │ │ │ + └────────┴────────┴───────────┴────────┴────────┴──────────┘ + 进程生命周期 +``` + +### 8.2 异常情况时序 + +**子进程崩溃(segfault)**: +``` +主进程: [start] ── [wait] ── [finished信号] ── [exitStatus == CrashExit] ── [忽略该帧] +子进程: [启动] ── [崩溃] ──────────────────── [操作系统回收资源] +``` + +**主进程取消(kill)**: +``` +主进程: [start] ── [用户操作/超时] ── [QProcess::kill()] ── [finished信号] ── [unlink SHM] +子进程: [启动] ── [渲染中] ───────── [SIGKILL] ──────────── [立即终止] +``` + +--- + +## 9. 共享内存生命周期管理 + +### 9.1 创建 + +```cpp +// 主进程创建 +QString shm_name = "/olive_" + QUuid::createUuid().toString(QUuid::WithoutBraces); +size_t shm_size = CalculateFrameSize(params) + OLIVE_SHM_HEADER_SIZE; +void* shm_ptr = olive_shm_create(shm_name.toUtf8().constData(), shm_size); +``` + +### 9.2 命名规范 + +- POSIX: `/olive_`,必须以 `/` 开头,长度 < 255。 +- Windows: `Local\\olive_` 或 `Global\\olive_`。 +- 临时文件: `/tmp/olive__.raw`(内存映射临时文件回退方案)。 + +### 9.3 清理策略 + +**正常路径**: +1. 子进程成功渲染,写入数据,退出。 +2. 主进程读取数据。 +3. 主进程 `olive_shm_close(ptr, size)` + `olive_shm_unlink(name)`。 + +**异常路径(子进程崩溃)**: +1. 主进程检测到 `QProcess::CrashExit`。 +2. 主进程立即 `olive_shm_unlink(name)`(即使数据未读取)。 + +**双重保险**: +- 主进程在创建 SHM 时启动一个 `QTimer`(5 秒后触发)。 +- 若 5 秒后 SHM 仍未被清理(异常路径未执行到 unlink),定时器自动 `olive_shm_unlink(name)`。 +- 防止子进程崩溃后主进程也崩溃导致的 SHM 泄漏。 + +--- + +## 10. 平台差异 + +### 10.1 Linux + +- 共享内存:`/dev/shm/` 下的 tmpfs 文件。 +- 最大名称长度:255 字节(含 null)。 +- 权限:`shm_open` 使用 `0666`,确保子进程可以打开。 +- 系统限制:`/proc/sys/kernel/shmmax` 通常足够大(> 1GB)。 + +### 10.2 macOS + +- 共享内存:`shm_open` 创建的 POSIX 共享内存对象。 +- 注意:macOS 的 `shm_open` 名称长度限制为 31 字符(包括开头的 `/`)! +- **解决方案**:使用 **内存映射临时文件** 替代 POSIX shm。 + +```cpp +// macOS 专用:使用临时文件替代 shm_open +int fd = mkstemp("/tmp/olive_XXXXXX.raw"); +ftruncate(fd, size); +void* ptr = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); +// 将文件路径传递给子进程 +``` + +### 10.3 Windows + +- 共享内存:`CreateFileMapping` + `MapViewOfFile`。 +- 名称:`Local\\olive_`(用户会话内可见)。 +- 注意:句柄管理。子进程 `MapViewOfFile` 后需要保留 `HANDLE` 以便后续 `UnmapViewOfFile` 和 `CloseHandle`。 +- 简化方案:子进程只负责写入,不关闭映射。进程退出后操作系统自动回收。 + +--- + +## 11. 性能调优建议 + +### 11.1 减少进程启动开销 + +| 技术 | 效果 | 复杂度 | +|---|---|---| +| 静态链接 `olive-renderer` | 避免动态库加载开销 | 低 | +| 预加载 Qt 插件 | 减少 `QCoreApplication` 初始化时间 | 低 | +| 批处理模式 | 摊销 OpenGL 上下文创建开销 | 中 | +| 使用 `EGL` 替代 `GLX`/`WGL` | EGL 上下文创建更快 | 中 | + +### 11.2 减少序列化开销 + +| 技术 | 效果 | 复杂度 | +|---|---|---| +| 节点图 XML 缓存 | 相同图只序列化一次 | 低 | +| 增量参数更新 | 仅发送变更的参数 | 中 | +| 二进制序列化格式 | 替代 XML,解析更快 | 高 | + +### 11.3 减少共享内存开销 + +| 技术 | 效果 | 复杂度 | +|---|---|---| +| 共享内存池 | 预分配 N 块循环使用 | 中 | +| 内存映射临时文件 | 避免 `shm_open` 系统调用 | 低 | +| 零拷贝(GPU 纹理共享) | 跨进程直接共享 GPU 纹理 | 高(平台相关) | + +--- + +## 12. 调试工具 + +### 12.1 手动运行子进程 + +```bash +# 直接运行 olive-renderer,独立于主进程 +./olive-renderer \ + --mode=frame \ + --node-graph=/tmp/debug_graph.xml \ + --time=0/1 \ + --video-params='{"width":100,"height":100,"format":"rgba32f"}' \ + --output-shm=/olive_debug \ + --output-shm-size=160000 \ + --verbose + +# 查看 stdout 输出 +# 查看 stderr 日志 +# 用另一个程序读取 /olive_debug 验证像素数据 +``` + +### 12.2 环境变量 + +| 变量 | 作用 | +|---|---| +| `OLIVE_RENDERER_BACKEND=dummy` | 强制使用 dummy 后端(跳过 OpenGL) | +| `OLIVE_RENDERER_TIMEOUT=60000` | 子进程超时时间(毫秒) | +| `OLIVE_RENDERER_KEEP_SHM=1` | 子进程退出后不删除共享内存(调试) | +| `OLIVE_RENDERER_LOG_FILE=/path/to.log` | 将日志写入文件 | + +### 12.3 重放渲染 + +将主进程发送给子进程的所有输入(命令行参数 + XML)保存到日志目录,可以精确重放某一帧的渲染: + +```bash +# 主进程日志目录:~/.local/share/oak/renderer_logs/ +# 每个渲染任务保存为: +# frame_12345.params (命令行参数) +# frame_12345.xml (节点图) + +# 重放 +./olive-renderer $(cat ~/.local/share/oak/renderer_logs/frame_12345.params) +``` diff --git a/docs/zh/modularization-plan/README.md b/docs/zh/modularization-plan/README.md new file mode 100644 index 000000000..cdd804315 --- /dev/null +++ b/docs/zh/modularization-plan/README.md @@ -0,0 +1,114 @@ +# Olive/Oak 模块化与多进程渲染:显式加载 + 纯 C 接口方案 + +> **状态**:详细实施计划 +> **范围**:仅制定方案,不涉及代码变更。 +> **核心目标**: +> 1. 所有业务模块编译为动态库,主进程**显式加载**(`dlopen`/`LoadLibrary`),通过**纯 C 接口**交互。 +> 2. 渲染引擎拆分为独立可执行文件 `olive-renderer`,采用**"用完即弃"**的进程模型:每帧(或每几帧)启动一个新进程,渲染完成后立即退出,最大限度避免锁和状态同步问题。 + +--- + +## 目录索引 + +| 文件 | 内容 | +|---|---| +| `README.md` | 本文档:总体架构、设计哲学、目录索引 | +| `09-c-api-design.md` | **先读此文件**:C API 设计规范、显式加载器、内存管理、错误处理、类型映射总纲。所有其他库的 C API 都遵循此规范。 | +| `01-olivecore.md` | `libolivecore.so`:基础数据类型库(Rational, Color, TimeRange, PixelFormat, SampleBuffer 等) | +| `02-olivecodec.md` | `libolivecodec.so`:编解码库(Decoder, Encoder, Frame, Stream) | +| `03-oliveplugin.md` | `liboliveplugin.so`:OFX 插件宿主支持 | +| `04-oliveaudio.md` | `liboliveaudio.so`:音频播放与处理 | +| `05-olivenode.md` | `libolivenode.so`:节点图系统(Node, NodeGraph, Project, Param, Keyframe, Timeline, Undo) | +| `06-oliverender.md` | `liboliverender.so`:渲染引擎抽象(RenderContext, RenderJob, RenderResult) | +| `07-oliveui.md` | `liboliveui.so`:UI 层(Widget, Panel, Window, Dialog) | +| `08-olive-renderer.md` | `olive-renderer` 可执行文件:多进程渲染的"用完即弃"模型详细设计 | +| `10-implementation-roadmap.md` | 10 周小步快跑实施路线图,含每周任务、验收标准、回退策略 | +| `11-ipc-protocol.md` | 渲染子进程的 IPC 协议(命令行参数、stdin JSON、stdout NDJSON、共享内存) | + +--- + +## 总体架构图 + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ olive-editor(主进程,GUI) │ +│ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ ModuleLoader(显式加载管理器) │ │ +│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ +│ │ │dlopen │ │dlopen │ │dlopen │ │dlopen │ │dlopen │ │ │ +│ │ │olivecore │ │olivecodec│ │olivenode │ │oliverender││oliveui │ │ │ +│ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │ +│ │ │ │ │ │ │ │ │ +│ │ └────────────┴────────────┴────────────┘ │ │ │ +│ │ 纯 C 接口交互 │ │ │ +│ │ ▼ │ │ +│ │ ┌──────────────┐ │ │ +│ │ │ QProcess │ │ │ +│ │ │ 启动/等待/回收 │ │ │ +│ │ └──────┬───────┘ │ │ +│ └───────────────────────────────────────────────────────────┼───────────┘ │ +│ │ │ +├──────────────────────────────────────────────────────────────┼───────────────┤ +│ │ │ +│ ┌───────────────────────────────────────────────────────────┘ │ +│ │ olive-renderer(子进程,无 GUI,"用完即弃") │ +│ │ │ +│ │ 启动参数:--node-graph=/tmp/g.xml --time=1001/30000 --output-shm=/o_123 │ +│ │ │ +│ │ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ │ 1. 初始化 Qt Core │ │ +│ │ │ 2. 初始化 OpenGL 离屏上下文 │ │ +│ │ │ 3. 加载节点图 XML │ │ +│ │ │ 4. 执行渲染(RenderProcessor + OpenGLRenderer) │ │ +│ │ │ 5. 将帧数据写入共享内存 │ │ +│ │ │ 6. 输出 JSON 结果到 stdout │ │ +│ │ │ 7. 清理并退出(return 0) │ │ +│ │ └─────────────────────────────────────────────────────────────────────┘ │ +│ └────────────────────────────────────────────────────────────────────────────┘ +│ +│ 所有动态库共享的基础依赖:QtCore, FFmpeg, OpenColorIO 等(由操作系统加载器解析) +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 核心设计哲学 + +### 1. 显式加载 + 纯 C 接口 + +- **主进程不链接任何业务动态库**。`olive-editor` 的 `main.cpp` 和 `core.cpp` 中没有任何对业务模块的 `#include`(除 C API 头文件外)。 +- 所有跨模块边界的交互通过**纯 C 函数**完成,使用**不透明指针(Opaque Pointer)**封装 C++ 对象。 +- 动态库内部可以继续使用 C++、Qt、STL、虚函数、模板等任意特性,但对外仅暴露 C 接口。 + +**为什么用 C 接口而非 C++ 类?** +- C++ 的 ABI(虚表布局、name mangling、异常传播)在不同编译器/版本间不兼容。 +- C 接口的符号名干净(无 mangling),`dlsym` 可直接查找。 +- 未来如果需要,C 接口可被 Python、Rust、C# 等语言直接绑定。 + +### 2. "用完即弃"的渲染进程 + +- **每一帧(或每 N 帧)渲染任务 = 一个独立的操作系统进程**。 +- 进程启动时接收完整的渲染参数和节点图,渲染完成后立即 `exit(0)`。 +- **不存在常驻渲染进程**,因此不需要:心跳检测、崩溃恢复、状态同步、读写锁、graph_ref 缓存、复杂的取消机制。 +- 主进程如果需要取消渲染,直接 `QProcess::kill()` 即可。 + +**可能的问题与回退**: +- 若进程启动开销(`QProcess::start()` + OpenGL 上下文初始化)导致实时预览帧率不足,可回退为**"批处理模式"**:每 3–5 帧共享一个进程,或预先启动一个进程池(但进程池内的进程仍不共享状态,每个进程只处理一个批次后自杀)。详见 `08-olive-renderer.md`。 + +### 3. 小步快跑 + +- 每个模块的改造都是**独立、可回退、可并行**的。 +- 不改现有 C++ 类定义,只在其上层**新增 C 封装层**(`api/_api.cpp`)。 +- 保留原有 OBJECT 库编译方式,通过 CMake 选项 `-DOLIVE_DYNAMIC_MODULES=ON` 切换。 +- 每完成一个模块,就通过该模块的单元测试验证,再进入下一个模块。 + +--- + +## 快速开始(阅读顺序) + +1. **先读 `09-c-api-design.md`**:理解 C API 的约定、显式加载器、内存管理规则。 +2. **再读 `08-olive-renderer.md`**:理解最核心的架构变革——多进程渲染。 +3. **然后按任意顺序阅读 `01-` 到 `07-`**:各业务模块的具体 C API 设计和实施步骤。 +4. **最后读 `10-implementation-roadmap.md`**:10 周实施计划,了解如何排期和验收。 +5. **参考 `11-ipc-protocol.md`**:渲染子进程的通信协议细节。