38 KiB
Olive/Oak 模块化与多进程渲染架构方案
状态:设计文档(Design Doc)
范围:仅制定方案,不涉及代码变更。
目标:将当前单体架构拆分为多个动态库,并将渲染引擎改造为独立进程,通过 stdio 进行 IPC 通信。
1. 概述
当前 Olive/Oak 采用单体编译模型:所有业务代码被聚合到 libolive-editor(OBJECT 库),最终链接为单个 olive-editor 可执行文件。这种架构在项目规模较小时工作良好,但随着 OFX 插件、节点图复杂度、多轨道高清/超高清处理的加入,单体架构面临以下问题:
- 编译-链接耗时:任何小改动都触发大规模重编译和重链接。
- 渲染崩溃导致编辑器全崩:OpenGL/OFX/FFmpeg 的崩溃会直接拖垮整个 GUI 进程,用户未保存的工作全部丢失。
- 插件隔离性差:OFX 插件与主程序共享地址空间,恶意或 buggy 插件可任意破坏内存。
- 可扩展性受限:未来如要支持分布式渲染、云渲染、独立批处理工具,均需先打破单体边界。
本方案提出两阶段架构演进:
- 动态库拆分:按功能层次将代码拆分为若干共享库(
.so/.dylib/.dll),明确模块边界与符号可见性。 - 渲染器多进程化:将
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)中定义:
// 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
所有需要跨库使用的类/函数均标记:
// 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等(作业类型)
解耦策略:
- 枚举与前置声明:将
RenderTicket::ReturnType,RenderMode::Mode,PixelFormat等移到ext/core/或common/中,使其不依赖render/。 - 接口回调:
Node中需要通知缓存失效的逻辑,改为通过NodeCacheInterface纯虚接口注入,而非直接引用FrameHashCache。 - 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 为例:
# 改造前:仅收集源文件到 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 改造
# 各子模块自行创建库目标
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。若打包为.appBundle,需使用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 详细消息格式
// === 初始化 ===
// 主进程 -> 子进程
{"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):
// 主进程创建
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:
// 主进程创建
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 在不同平台间行为不一致,可退而求其次使用内存映射临时文件:
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 策略:引用 + 增量更新
-
首次传输:当某个
ViewerOutput需要渲染时,主进程将其关联的节点图通过type: init_graph消息完整序列化发送给子进程,子进程缓存并返回一个graph_ref(如 SHA-256 前 8 位)。{"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} -
后续引用:渲染帧请求通过
"node_graph_ref": "a3f7b2d9"引用已缓存的图,无需重复传输 XML。 -
增量更新:当用户调整某个节点的参数时,主进程发送
update_graph消息,仅携带变更的节点 ID 和参数字段。{"type":"update_graph","req_id":11,"graph_ref":"a3f7b2d9","updates":[{"node_id":"Transform1","params":{"position":{"x":100,"y":200}}}]} -
序列化复用:直接复用现有的
ProjectSerializer,以kOnlyNodes模式序列化目标ViewerOutput及其上游依赖节点。
4.4.2 子进程中的节点图重建
子进程收到 init_graph 后:
- 使用
ProjectSerializer::Load()将 XML 反序列化为临时Project对象。 - 提取目标
ViewerOutput节点,构建本地NodeValueDatabase。 - 将图对象存入
graph_ref → Project的映射表中。 - 后续
render_frame直接使用缓存的图,避免重复解析 XML。
4.5 渲染进程生命周期管理
4.5.1 启动与就绪
// 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,认为子进程卡死。 - 卡死处理:
render_process_->kill()强制终止。- 清理所有未完成的
RenderTicket,标记为错误状态。 - 自动重启子进程。
- 重新发送所有活跃的
graph_ref对应的节点图。
4.5.3 崩溃恢复
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 优雅退出
主进程析构时:
- 发送
shutdown请求,等待子进程返回result(超时 5 秒)。 - 若子进程未退出,调用
terminate(),再等 3 秒。 - 若仍未退出,
kill()强制结束。
4.6 主进程 RenderManager 适配
当前 RenderManager 直接创建并管理 RenderThread。改造后,RenderManager 转型为 IPC 客户端管理器,对外接口保持完全不变,以最小化 UI 层侵入。
4.6.1 类结构调整
// 新增:轻量级 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 请求消息(主进程 → 子进程)
// 基础请求结构
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 响应消息(子进程 → 主进程)
// 基础响应结构
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 共享内存布局(二进制)
#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 层无需感知底层架构变化。