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`,主进程通过 **stdio(stdin/stdout)** 进行 IPC 控制,**共享内存/内存映射文件**传输大帧数据。
---
## 2. 总体架构目标
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ olive-editor(主进程,GUI) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌─────────────────┐ │
│ │ liboliveui │ │ libolivenode │ │libolivecodec │ │ liboliverender │ │
│ │ (widget/ │ │ (node/ │ │ (codec/ │ │ -client (轻量) │ │
│ │ panel/ │ │ timeline/ │ │ common/) │ │ IPC 封装层 │ │
│ │ window/) │ │ undo/) │ │ │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └────────┬────────┘ │
│ ▲ ▲ ▲ │ │
│ └─────────────────┴─────────────────┘ │ QProcess │
│ 动态链接 │ stdin/stdout│
├──────────────────────────────────────────────────────────────────┼───────────┤
│ │ │
│ ┌───────────────────────────────────────────────────────────────┘ │
│ │ olive-renderer(子进程,无 GUI) │
│ │ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ RenderService (IPC 服务端,监听 stdin,输出 stdout) │ │
│ │ └──────────────────────────┬──────────────────────────────────────────┘ │
│ │ │ │
│ │ ┌──────────────────────────▼──────────────────────────────────────────┐ │
│ │ │ RenderProcessor + OpenGLRenderer + DecoderCache + ShaderCache │ │
│ │ │ (原有渲染逻辑,在独立地址空间运行,崩溃不影响主进程) │ │
│ │ └─────────────────────────────────────────────────────────────────────┘ │
│ └────────────────────────────────────────────────────────────────────────────┘
│
│ 基础依赖(所有模块共享):
│ ┌─────────────┐ ┌─────────────┐ ┌──────────────────┐
│ │ libolivecore│ │liboliveaudio│ │ liboliveplugin │
│ │ (ext/core) │ │ (audio/) │ │ (pluginSupport/) │
│ └─────────────┘ └─────────────┘ └──────────────────┘
└─────────────────────────────────────────────────────────────────────────────┘
```
---
## 3. 动态库拆分方案
### 3.1 拆分原则
- **低侵入性**:优先拆分依赖关系清晰、接口明确的模块;对耦合严重的 `node/` ↔ `render/` 暂不强行物理分割,而是通过**接口抽象 + 动态链接**降低耦合。
- **分层依赖**:严格遵循 `上层 → 下层` 的依赖方向,禁止循环依赖。
- **符号可控**:引入 `OLIVE_<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 JSON(NDJSON)**,每行一条完整 JSON 消息,以 `\n` 分隔。理由:
- 基于文本,易于调试(`echo '{...}' | olive-renderer` 可手动测试)。
- 结构化,易于扩展新字段。
- 帧边界天然由换行符确定,无需额外的长度前缀或帧同步协议。
#### 4.2.2 消息定义
**请求消息(主进程 → 子进程,写入子进程 stdin)**:
| 字段 | 类型 | 说明 |
|---|---|---|
| `type` | string | 消息类型:`init`, `render_frame`, `render_audio`, `cancel`, `shutdown`, `ping` |
| `req_id` | int | 请求唯一标识,用于响应匹配。 |
| `...` | 类型相关 | 见下表。 |
**响应消息(子进程 → 主进程,写入子进程 stdout)**:
| 字段 | 类型 | 说明 |
|---|---|---|
| `type` | string | 消息类型:`ready`, `result`, `error`, `cancelled`, `heartbeat`, `pong` |
| `req_id` | int | 对应请求的 `req_id`。对于主动推送(如 `heartbeat`),`req_id` 为 `0`。 |
| `...` | 类型相关 | 见下表。 |
#### 4.2.3 详细消息格式
```json
// === 初始化 ===
// 主进程 -> 子进程
{"type":"init","req_id":1,"backend":"opengl","shader_path":"/usr/share/olive/shaders","ocio_config_path":"/path/to/config.ocio"}
// 子进程 -> 主进程
{"type":"ready","req_id":1,"status":"ok","backend_version":"4.6 (Core Profile)"}
{"type":"error","req_id":1,"status":"error","message":"Failed to create OpenGL context"}
// === 渲染视频帧 ===
// 主进程 -> 子进程
{
"type": "render_frame",
"req_id": 2,
"ticket_id": 101,
"node_graph_ref": "a3f7b2d9",
"time": "1001/30000",
"video_params": {
"width": 1920,
"height": 1080,
"depth": 1,
"format": "rgba32f",
"channel_count": 4,
"pixel_aspect": "1/1"
},
"audio_params": {
"sample_rate": 48000,
"channel_layout": "stereo"
},
"mode": "offline",
"color_manager": {
"reference_space": "ACES - ACES2065-1",
"display_space": "Rec.709"
},
"shm_name": "/olive_r_12345_2",
"shm_size": 33177600
}
// 子进程 -> 主进程(成功)
{
"type": "result",
"req_id": 2,
"ticket_id": 101,
"status": "ok",
"shm_name": "/olive_r_12345_2",
"width": 1920,
"height": 1080,
"format": "rgba32f",
"pixel_format_id": 28,
"timestamp_ms": 45
}
// 子进程 -> 主进程(失败)
{
"type": "error",
"req_id": 2,
"ticket_id": 101,
"status": "error",
"category": "decoder",
"message": "Failed to decode frame at time 1001/30000: codec not found"
}
// === 渲染音频 ===
// 主进程 -> 子进程
{
"type": "render_audio",
"req_id": 3,
"ticket_id": 102,
"node_graph_ref": "a3f7b2d9",
"range": {"in": "0/1", "out": "48000/48000"},
"audio_params": {
"sample_rate": 48000,
"channel_layout": "stereo",
"format": "flt_planar"
},
"mode": "offline",
"shm_name": "/olive_r_12345_3",
"shm_size": 384000
}
// === 取消任务 ===
{"type":"cancel","req_id":4,"ticket_id":101}
{"type":"cancelled","req_id":4,"ticket_id":101}
// === 心跳与探测 ===
{"type":"ping","req_id":5}
{"type":"pong","req_id":5}
// 子进程主动心跳(每 3 秒)
{"type":"heartbeat","req_id":0,"timestamp":1716288000}
// === 优雅退出 ===
{"type":"shutdown","req_id":6}
{"type":"result","req_id":6,"status":"ok"}
```
#### 4.2.4 通信时序示例
```
主进程 子进程 (olive-renderer)
│ │
│── QProcess::start() ─────────────────>│
│ │ 初始化 Qt Core
│ │ 初始化 OpenGL 上下文
│<── stdout: {"type":"ready",...} ─────│
│ │
│── stdin: render_frame (ticket #1) ──>│
│── stdin: render_frame (ticket #2) ──>│ 入队渲染
│ │
│<── stdout: result (ticket #1) ───────│ 完成帧 #1
│── 读取共享内存帧数据 │
│── shm_unlink() │
│ │
│<── stdout: result (ticket #2) ───────│ 完成帧 #2
│ │
│── stdin: cancel (ticket #3) ────────>│ 取消正在进行的 #3
│<── stdout: cancelled (ticket #3) ────│
│ │
│── stdin: shutdown ──────────────────>│ 清理资源,退出事件循环
│<── stdout: result (shutdown) ────────│
│── QProcess::waitForFinished() ──────>│ 进程结束
```
### 4.3 数据平面:共享内存帧传输
NDJSON 仅适合传输控制命令和元数据。**视频帧(RGBA32F, 1920×1080 ≈ 33MB)和音频块**不能通过 base64 编码在 JSON 中传输(实时预览需要 24–30fps,stdio 带宽和 CPU 编解码开销均不可接受)。
#### 4.3.1 方案:POSIX / Windows 共享内存
**POSIX(Linux/macOS)**:
```cpp
// 主进程创建
int fd = shm_open("/olive_r_12345_2", O_RDWR | O_CREAT, 0666);
ftruncate(fd, shm_size);
void *ptr = mmap(nullptr, shm_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
// 子进程打开(通过 shm_name 从 JSON 中读取)
int fd = shm_open("/olive_r_12345_2", O_RDWR, 0666);
void *ptr = mmap(nullptr, shm_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
// 使用完毕后,主进程负责 unlink
shm_unlink("/olive_r_12345_2");
```
**Windows**:
```cpp
// 主进程创建
HANDLE hMap = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE,
0, shm_size, L"Local\\olive_r_12345_2");
void *ptr = MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, shm_size);
// 子进程打开
HANDLE hMap = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, L"Local\\olive_r_12345_2");
void *ptr = MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, shm_size);
// 清理
UnmapViewOfFile(ptr);
CloseHandle(hMap);
```
**帧数据布局**:共享内存前 256 字节保留为**元数据头**(Magic、版本、实际数据偏移、行间距 linesize、校验和),后续为原始像素/采样数据。
```
┌─────────────────────┬──────────────────────────────────────┐
│ Header (256 B) │ Pixel Data │
│ magic | offset | │ row 0 │ row 1 │ ... │ row H-1 │
│ linesize | ... │ (width * channels * sizeof(float)) │
└─────────────────────┴──────────────────────────────────────┘
```
#### 4.3.2 备选方案:内存映射临时文件
若共享内存 API 在不同平台间行为不一致,可退而求其次使用**内存映射临时文件**:
```cpp
QTemporaryFile tmp;
tmp.setFileTemplate("olive_render_XXXXXX.raw");
tmp.open();
tmp.resize(shm_size);
// 内存映射
tmp.seek(0);
uchar *ptr = tmp.map(0, shm_size);
// 将文件路径(而非 shm_name)通过 JSON 传递
{"shm_path": "/tmp/olive_render_a1b2c3.raw", ...}
```
此方案兼容性最好,但性能略低于纯共享内存(因可能触发文件系统页缓存回写)。
### 4.4 节点图序列化与缓存
渲染请求的核心输入是**节点图(Node Graph)**。直接每次传输完整 XML 序列化在实时预览场景下(24–30fps)不可接受。
#### 4.4.1 策略:引用 + 增量更新
1. **首次传输**:当某个 `ViewerOutput` 需要渲染时,主进程将其关联的节点图通过 `type: init_graph` 消息完整序列化发送给子进程,子进程缓存并返回一个 `graph_ref`(如 SHA-256 前 8 位)。
```json
{"type":"init_graph","req_id":10,"graph_ref":"a3f7b2d9","node_graph_xml":"...<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; // 完整的项目 XML(kOnlyNodes 模式)
}
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 层无需感知底层架构变化。