873 lines
38 KiB
Markdown
873 lines
38 KiB
Markdown
# 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 ¶ms, const QString &graph_ref);
|
||
int RequestRenderAudio(const RenderManager::RenderAudioParams ¶ms, const QString &graph_ref);
|
||
void CancelTicket(int ticket_id);
|
||
|
||
signals:
|
||
void ResultReceived(int ticket_id, const QJsonObject &result);
|
||
void ErrorReceived(int ticket_id, const QString &message);
|
||
void ProcessCrashed();
|
||
void ProcessRecovered();
|
||
|
||
private:
|
||
QProcess *process_;
|
||
QHash<int, RenderTicketPtr> ticket_map_; // ticket_id -> RenderTicket
|
||
// ...
|
||
};
|
||
|
||
// 改造后的 RenderManager(对外接口不变)
|
||
class RenderManager : public QObject {
|
||
Q_OBJECT
|
||
public:
|
||
// 原有接口 100% 保留
|
||
RenderTicketPtr RenderFrame(const RenderVideoParams ¶ms);
|
||
RenderTicketPtr RenderAudio(const RenderAudioParams ¶ms);
|
||
bool RemoveTicket(RenderTicketPtr ticket);
|
||
|
||
private:
|
||
// 旧实现:RenderThread *video_thread_; ...
|
||
// 新实现:
|
||
RenderServiceClient *client_;
|
||
QHash<Node*, 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 层无需感知底层架构变化。
|