docs:Add plan of spilting into multi-library

This commit is contained in:
2026-05-21 20:31:17 +08:00
parent 295c5eaa24
commit 6b94ceea14
13 changed files with 5651 additions and 0 deletions
@@ -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`,主进程通过 **stdiostdin/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_<MODULE>_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
$<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/app>
$<INSTALL_INTERFACE:include>
)
# 方案 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_OBJECTS:olive-version-obj>
)
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 JSONNDJSON**,每行一条完整 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 中传输(实时预览需要 2430fps,stdio 带宽和 CPU 编解码开销均不可接受)。
#### 4.3.1 方案:POSIX / Windows 共享内存
**POSIXLinux/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":"...<Project>...</Project>..."}
{"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<QProcess::ProcessError>::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 &params, const QString &graph_ref);
int RequestRenderAudio(const RenderManager::RenderAudioParams &params, 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<int, RenderTicketPtr> ticket_map_; // ticket_id -> RenderTicket
// ...
};
// 改造后的 RenderManager(对外接口不变)
class RenderManager : public QObject {
Q_OBJECT
public:
// 原有接口 100% 保留
RenderTicketPtr RenderFrame(const RenderVideoParams &params);
RenderTicketPtr RenderAudio(const RenderAudioParams &params);
bool RemoveTicket(RenderTicketPtr ticket);
private:
// 旧实现:RenderThread *video_thread_; ...
// 新实现:
RenderServiceClient *client_;
QHash<Node*, QString> 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; // 完整的项目 XMLkOnlyNodes 模式)
}
interface UpdateGraphRequest extends Request {
type: "update_graph";
graph_ref: string;
updates: Array<{
node_id: string;
params: Record<string, any>;
}>;
}
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 层无需感知底层架构变化。
+466
View File
@@ -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` 的基础类型)。
- 无项目内其他模块依赖。
- 主要是 PODPlain 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 <stdint.h>
#include <stddef.h>
#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 <olive/core/core.h>
#include <olive/core/rational.h>
#include <olive/core/color.h>
#include <olive/core/timerange.h>
#include <olive/core/samplebuffer.h>
#include <olive/core/pixelformat.h>
#include <olive/core/timecodefunctions.h>
#include <cstring>
#include <cstdlib>
// 线程局部错误状态
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<olive::Timecode::Display>(display_mode)
);
QByteArray utf8 = str.toUtf8();
char* result = static_cast<char*>(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
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
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 改为 SHARED1 天)
- [ ] 修改 `ext/core/CMakeLists.txt``STATIC``SHARED`,添加 `CXX_VISIBILITY_PRESET hidden`
- [ ] 为需要导出的类/函数添加导出宏。
- [ ] 验证所有平台编译通过。
**验收标准**`olivecore` 编译为 `.so`/`.dylib`/`.dll`,单元测试通过显式加载运行。
### Step 2: 编写 core C API23 天)
- [ ] 编写 `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<OliveRational(*)(int64_t,int64_t)>("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
```
@@ -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
$<INSTALL_INTERFACE:include>
)
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`。 |
@@ -0,0 +1,305 @@
# liboliveplugin.so — OFX 插件宿主支持
> **依赖**`libolivecore.so`
> **外部依赖**`third_party/openfx/HostSupport``OfxHost` 静态库),expatQt::Core
> **包含源码**`app/pluginSupport/`
> **当前状态**:单体 OBJECT 库的一部分,通过 `target_link_libraries(olive-editor PUBLIC OfxHost)` 隐式链接
> **改造难度**:⭐⭐(较简单,接口相对独立)
---
## 1. 当前状态分析
`app/pluginSupport/` 实现 OFXOpenFX)标准的 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
$<INSTALL_INTERFACE:include>
)
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: 隔离 PluginNode1 天)
- [ ]`app/node/plugins/PluginNode` 移动到 `app/node/` 下(或保持原位,但确保它编译进 `libolivenode.so` 而非 `liboliveplugin.so`)。
- [ ] 确认 `PluginNode``pluginSupport/` 的依赖方向:PluginNode 使用 pluginSupport 的类,而非相反。
**验收标准**`liboliveplugin.so` 编译时不包含任何 `node/` 下的源文件。
### Step 1: 构建 liboliveplugin.so1 天)
- [ ] 创建 `app/pluginSupport/CMakeLists.txt`(若尚无)。
- [ ]`pluginSupport/` 的源文件从主 OBJECT 库中移出,单独构建为 `oliveplugin SHARED`
- [ ] 确保 `OfxHost` 静态库先被构建(`third_party/openfx/HostSupport`)。
**验收标准**`liboliveplugin.so` 编译成功,能通过 `dlsym` 找到 `olive_plugin_api_version`
### Step 2: 最小 C API2 天)
- [ ] 先实现最必需的接口:
- `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 实例句柄,状态完全隔离。 |
@@ -0,0 +1,267 @@
# liboliveaudio.so — 音频播放与处理
> **依赖**`libolivecore.so`
> **外部依赖**PortAudioQt::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
$<INSTALL_INTERFACE:include>
)
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.so1 天)
- [ ]`app/audio/` 从主 OBJECT 库移出,单独构建为 `oliveaudio SHARED`
- [ ] 确保 `AudioManager` 单例的初始化顺序正确(Qt 的 `Q_GLOBAL_STATIC` 或延迟初始化)。
**验收标准**`liboliveaudio.so` 编译成功。
### Step 2: 最小 C API1 天)
- [ ] 实现播放控制:`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` 中完成。 |
+413
View File
@@ -0,0 +1,413 @@
# libolivenode.so — 节点图系统
> **依赖**`libolivecore.so`, `libolivecodec.so`
> **外部依赖**Qt::CoreQObject, 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
$<INSTALL_INTERFACE:include>
)
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.so1 天)
- [ ]`node/`, `timeline/`, `undo/`, `config/` 的源文件聚合,构建为 `olivenode SHARED`
- [ ] 处理 `node/` 下的 `add_subdirectory` 嵌套,确保所有源文件被正确收集。
**验收标准**`libolivenode.so` 编译成功,`nm -D libolivenode.so | grep olive_node` 能看到导出的 C 符号。
### Step 2: 最小 C API2 天)
- [ ] 先实现项目级接口:
- `olive_project_create/destroy/load_file/save_file`
- `olive_node_graph_create/destroy/load_xml/save_xml`
- [ ] 这些接口是渲染子进程最需要的:子进程需要加载 XML 节点图并渲染。
**验收标准**:可以用 C API 创建一个项目、保存为 XML、再加载回来,内容一致。
### Step 3: 节点操作 API2 天)
- [ ] 实现节点增删查改:`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 API1 天)
- [ ] 实现 `olive_undo_stack_*` 系列函数。
- [ ] C API 的 undo 采用函数指针回调模式,避免暴露 C++ 的 `UndoCommand` 类。
### Step 5: ViewerOutput 和 Sequence1 天)
- [ ] 实现 `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` 的文件路径设置/获取。 |
@@ -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_frameOliveFrame*,定义在 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_bufferOliveSampleBuffer*,定义在 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<olive::NodeGraph*>(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<olive::RenderTicket>();
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<olive::FramePtr>();
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
$<INSTALL_INTERFACE:include>
)
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 API2 天)
- [ ] 实现 `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`。 |
+256
View File
@@ -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
$<INSTALL_INTERFACE:include>
)
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 实现(按需,23 天)
- [ ] 实现 `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。 |
@@ -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=<backend> 渲染后端:opengl(默认), dummy
--shader-path=<path> 着色器资源目录(默认:可执行文件同级目录下的 shaders/)
--ocio-config=<path> OpenColorIO 配置文件路径
--verbose 输出详细日志到 stderr
单帧模式(--mode=frame):
--mode=frame
--node-graph=<path> 节点图 XML 文件路径
--output-node=<id> 输出节点 ID(默认:图中的第一个 ViewerOutput
--time=<rational> 渲染时间点,如 "1001/30000"
--video-params=<json> 视频参数 JSON,如 '{"width":1920,"height":1080,"format":"rgba32f"}'
--audio-params=<json> 音频参数 JSON
--color-ref=<space> 参考色彩空间
--color-display=<space> 显示色彩空间
--output-shm=<name> POSIX 共享内存名称(如 "/olive_r_123")或临时文件路径
--output-shm-size=<bytes> 共享内存大小
--output-stdout 将帧数据 base64 编码输出到 stdout(仅小帧/测试用)
批处理模式(--mode=batch):
--mode=batch
--node-graph=<path>
--output-node=<id>
--times=<csv> 逗号分隔的时间点列表,如 "0/30,1/30,2/30"
--video-params=<json>
--audio-params=<json>
--output-shm=<name>
--output-shm-size=<bytes>
音频模式(--mode=audio):
--mode=audio
--node-graph=<path>
--output-node=<id>
--start=<rational> 起始时间
--duration=<rational> 持续时间
--audio-params=<json>
--output-shm=<name>
--output-shm-size=<bytes>
```
### 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 <QCoreApplication>
#include <QCommandLineParser>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QElapsedTimer>
#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, &params, &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<qint64>(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 <stddef.h>
#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 <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
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 <windows.h>
void* olive_shm_create(const char* name, size_t size) {
HANDLE hMap = CreateFileMappingA(INVALID_HANDLE_VALUE, nullptr,
PAGE_READWRITE, 0, static_cast<DWORD>(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 <QString>
#include <QProcess>
#include <QJsonObject>
#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<OliveNode*>(viewer_output_)),
time,
video_params_,
audio_params_,
OLIVE_RENDER_MODE_ONLINE
);
// 4. 连接完成信号
connect(proc, QOverload<int, QProcess::ExitStatus>::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: 集成到 ViewerWidget2 天)
- [ ] 修改 `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_<pid>_<uuid>`。 |
| 共享内存泄漏(子进程崩溃后未 unlink) | 主进程在启动子进程前注册一个定时器,若子进程异常退出,5 秒后自动 `shm_unlink`。 |
| 磁盘空间不足(临时文件方案) | 渲染前检查磁盘空间,不足时返回错误码 6。 |
| 节点图 XML 过大导致解析慢 | 启用增量序列化:只序列化自上次以来变更的节点和参数。 |
| OFX 插件需要持久化状态 | OFX 插件实例不跨帧持久化,每帧重新创建。若某些插件初始化极慢,在 C API 中提供 `olive_plugin_instance_serialize_state` 接口,将状态快照传给下一帧的进程。 |
@@ -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_<module>_`
- 参数和返回值仅使用 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_<module>_<snake_case>` | `olive_node_graph_create`, `olive_codec_decoder_open` |
| 枚举名 | `Olive<Module><PascalCase>` | `OliveCodecResultOk`, `OliveRenderModeOffline` |
| 常量宏 | `OLIVE_<MODULE>_UPPER_SNAKE` | `OLIVE_NODE_OK`, `OLIVE_CODEC_ERROR_NOT_FOUND` |
| 版本宏 | `OLIVE_<MODULE>_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
// 模式 ACreate/Destroy(堆分配)
OliveNodeGraph* olive_node_graph_create(void);
void olive_node_graph_destroy(OliveNodeGraph* obj);
// 模式 BInit/Cleanup(栈分配或外部缓冲区)
int olive_frame_init(OliveFrame* frame, int w, int h, OlivePixelFormat fmt);
void olive_frame_cleanup(OliveFrame* frame);
// 模式 CRef/Unref(引用计数)
void olive_frame_ref(OliveFrame* frame);
void olive_frame_unref(OliveFrame* frame);
```
优先使用 **模式 ACreate/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 <QString>
#include <QHash>
#include <functional>
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<typename FuncPtr>
FuncPtr GetFunction(const QString& module_name, const char* func_name) {
return reinterpret_cast<FuncPtr>(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<QString, ModuleHandle> modules_;
QString last_error_;
};
// 便捷宏:从指定模块获取函数并调用
#define OLIVE_LOAD_FUNC(loader, module, name, type) \
auto name = (loader).GetFunction<type>(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 <dlfcn.h>
#include <QDebug>
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 <windows.h>
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<HMODULE>(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<int(*)()>("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
$<INSTALL_INTERFACE:include>
)
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 <gtest/gtest.h>
#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()`。 |
@@ -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** 在 CIGitHub 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 2libolivecore.so1 人主责)
### 目标
`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<OliveRational(*)(int64_t,int64_t)>("core", "olive_rational_make");
auto add = loader.GetFunction<OliveRational(*)(OliveRational,OliveRational)>("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 3libolivecodec.so1 人主责)
### 目标
`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 4libolivenode.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 5libolivenode.so C API + liboliveplugin.so2 人并行)
### 目标
完成节点图系统的 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 APINodeGraph 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 6olive-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 7ViewerWidget 集成 + 用完即弃验证(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 8liboliveaudio.so + liboliveui.so2 人并行)
### 目标
完成音频库和 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 人 audio1 人 ui1 人优化 renderer)。
---
## 回退总策略
| 场景 | 回退方案 |
|---|---|
| 某周任务无法按时完成 | 将该周剩余任务移到下一周,当前周只交付已完成部分。 |
| `node/` 解耦完全不可行 | 将 `node/` + `render/` 合并为 `libolive-engine.so`,后续再拆分。 |
| "用完即弃"性能完全不可接受 | 切换为"批处理模式"(每进程渲染 N 帧),或"进程池模式"(预启动 N 个进程)。 |
| 动态库在某平台加载失败 | 该平台暂时保持静态链接,其他平台先用动态库。 |
| C API 维护成本过高 | 保留 C API 用于子进程通信,主进程内部恢复直接 C++ 链接(但库仍编译为动态库,由操作系统隐式加载)。 |
| 项目期限紧张 | 优先完成 `olive-renderer` 多进程(核心价值),动态库拆分可以延后。 |
@@ -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 | 否 | `<exe_dir>/shaders` | 着色器资源目录 |
| `--ocio-config` | path | 否 | — | OCIO 配置文件路径 |
| `--verbose` | flag | 否 | false | 详细日志输出到 stderr |
| `--version` | flag | 否 | false | 输出版本信息并退出 |
| `--help` | flag | 否 | false | 输出帮助信息并退出 |
### 2.2 参数值格式
**Rational**`"<numerator>/<denominator>"`,如 `"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 <stdint.h>
#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` | 收到 SIGINTCtrl+C / kill -2 | 视为取消,正常丢弃 |
| 137 | `EXIT_SIGKILL` | 收到 SIGKILLkill -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_<uuid>`,必须以 `/` 开头,长度 < 255。
- Windows: `Local\\olive_<uuid>``Global\\olive_<uuid>`
- 临时文件: `/tmp/olive_<pid>_<uuid>.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_<uuid>`(用户会话内可见)。
- 注意:句柄管理。子进程 `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)
```
+114
View File
@@ -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/<module>_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`**:渲染子进程的通信协议细节。