Files
oak-editor/docs/zh/modularization-and-multiprocess-render-plan.md
T

38 KiB
Raw Blame History

Olive/Oak 模块化与多进程渲染架构方案

状态:设计文档(Design Doc
范围:仅制定方案,不涉及代码变更。
目标:将当前单体架构拆分为多个动态库,并将渲染引擎改造为独立进程,通过 stdio 进行 IPC 通信。


1. 概述

当前 Olive/Oak 采用单体编译模型:所有业务代码被聚合到 libolive-editorOBJECT 库),最终链接为单个 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)中定义:

// 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.hFrameHashCache 等)
  • 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 为例:

# 改造前:仅收集源文件到 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 平台注意事项

  • WindowsQ_DECL_EXPORT/Q_DECL_IMPORT 会自动处理 __declspec(dllexport/dllimport)。需确保 olive-editor.exe 与所有 .dll 在同一目录,或通过 PATH 找到。
  • macOS:动态库后缀为 .dylib。若打包为 .app Bundle,需使用 install_name_tool 或 CMake 的 @rpath 设置确保加载路径正确。
  • Linux:使用 RPATHLD_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_id0
... 类型相关 见下表。

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 中传输(实时预览需要 2430fps,stdio 带宽和 CPU 编解码开销均不可接受)。

4.3.1 方案:POSIX / Windows 共享内存

POSIXLinux/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 策略:引用 + 增量更新

  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}
    
  2. 后续引用:渲染帧请求通过 "node_graph_ref": "a3f7b2d9" 引用已缓存的图,无需重复传输 XML。

  3. 增量更新:当用户调整某个节点的参数时,主进程发送 update_graph 消息,仅携带变更的节点 ID 和参数字段。

    {"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 启动与就绪

// 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 崩溃恢复

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 类结构调整

// 新增:轻量级 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.mdbuild-macos-zh.md,说明新的运行时依赖和动态库加载路径配置。

6. 风险与对策

风险 影响 对策
Node/Render 解耦工作量超预期 采用"接口抽象 + 前向声明"的轻量解耦,不追求完全消除逻辑耦合,只消除编译期头文件依赖。若实在无法解耦,可将 node/ + render/ 暂时合并为一个 libolive-engine.so,后续再拆分。
共享内存跨平台兼容性 封装抽象层 SharedMemoryBufferPOSIX 和 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;   // 完整的项目 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 响应消息(子进程 → 主进程)

// 基础响应结构
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 层无需感知底层架构变化。