# olive-renderer — 多进程渲染可执行文件 > **类型**:独立可执行文件(非动态库) > **依赖**:`libolivecore.so`, `libolivecodec.so`, `libolivenode.so`, `liboliverender.so`(可静态或动态链接) > **外部依赖**:Qt::Core, OpenGL, FFmpeg, OpenColorIO > **核心特征**:**"用完即弃"**——每帧(或每几帧)启动一个新进程,渲染完成后立即退出 > **改造难度**:⭐⭐⭐(中等,逻辑复杂但隔离清晰) --- ## 1. 设计哲学:用完即弃(Fire-and-Forget) 传统多进程渲染架构通常维护一个**常驻子进程**,通过复杂的 IPC 协议进行状态同步、心跳检测、崩溃恢复。这种架构虽然成熟,但引入了以下复杂性: - **状态同步**:主进程和子进程的节点图、缓存、参数必须保持一致。 - **锁与并发**:共享内存的读写需要锁或原子操作。 - **崩溃恢复**:子进程崩溃后需要重建状态、重同步节点图。 - **生命周期管理**:启动、握手、心跳、优雅退出、强制杀死的完整状态机。 本方案采用**激进的简化策略**: > **每一帧渲染任务 = 一个全新的操作系统进程**。进程接收完整的渲染输入,执行渲染,输出结果,然后 `exit(0)`。 ### 1.1 优势 | 优势 | 说明 | |---|---| | **零锁** | 进程之间不共享任何可变状态(除了只读的共享内存输出区),无需任何互斥锁、信号量、条件变量。 | | **零状态同步** | 每帧的输入都是自包含的(节点图 XML + 渲染参数),子进程无需维护任何跨帧状态。 | | **自动崩溃隔离** | 某一帧的渲染崩溃(OFX 插件 segfault、GPU 驱动错误)只会影响该进程,主进程和其他帧完全不受影响。 | | **资源自动回收** | 进程退出后,操作系统自动回收其所有资源(内存、GPU 上下文、文件句柄、解码器实例),无需显式清理。 | | **可预测性** | 没有状态泄漏、没有内存碎片累积、没有僵尸缓存,每一帧都在干净的环境中渲染。 | | **易于调试** | 可以单独运行 `olive-renderer` 命令行重放某一帧的渲染,无需启动完整 GUI。 | ### 1.2 挑战与回退 | 挑战 | 分析 | 回退策略 | |---|---|---| | **进程启动开销** | `QProcess::start()` + OpenGL 上下文初始化可能需要 50–200ms | 若实测开销过高,采用 **"批处理模式"**:一个进程渲染 N 帧(如 5 帧),然后退出。或预启动一个进程池,但每个进程仍只服务一个批次后自杀。 | | **节点图序列化开销** | 每帧都序列化完整节点图可能耗时 | 节点图 XML 在主进程缓存,相同图只序列化一次;仅参数变化时发送增量更新(即使进程用完即弃,输入数据仍可复用)。 | | **GPU 上下文反复创建** | OpenGL 上下文创建/销毁开销较大 | 采用 **批处理模式** 摊销开销;或在支持的平台使用 EGL/GLES 的轻量上下文。 | | **共享内存创建开销** | 每帧创建新的 shm 对象 | 使用 **内存映射临时文件** 替代 POSIX shm,创建开销更低;或主进程预分配一组循环缓冲区。 | | **音频连续性** | 音频需要连续播放,逐帧进程可能导致间隙 | 音频采用 **批处理模式**:一个进程渲染 0.5–1 秒的音频块,而非每帧一个进程。 | --- ## 2. 进程模型详解 ### 2.1 单帧模式(默认) ``` 主进程(UI 线程或工作线程) │ │ 1. 序列化节点图(若未缓存则生成 XML) │ 2. 创建共享内存 / 临时文件 │ 3. 组装渲染参数 │ ▼ ┌─────────────────────────────────────┐ │ QProcess::start("olive-renderer", │ │ ["--mode=frame", │ │ "--node-graph=/tmp/g_123.xml", │ │ "--time=1001/30000", │ │ "--output-shm=/olive_r_123"]) │ └─────────────────────────────────────┘ │ │ 4. 等待子进程结束(阻塞或异步) │ QProcess::waitForFinished(timeout_ms) │ ▼ 子进程启动 ──────────────────────────────► 子进程退出 │ │ │ a. 解析命令行参数 │ │ b. 加载节点图 XML │ │ c. 初始化 Qt Core + OpenGL │ │ d. 执行 RenderProcessor │ │ e. 将帧写入共享内存 │ │ f. 输出结果 JSON 到 stdout │ │ g. exit(0) │ ▼ ▼ 主进程读取共享内存 ──────────────────────► 主进程释放共享内存 │ │ 5. 将帧数据上传到 GPU Texture 或显示 ▼ ViewerWidget 更新 ``` ### 2.2 批处理模式(性能回退) 当实测单帧模式开销过高时,启用批处理: ``` 主进程 │ │ 渲染帧 #1, #2, #3, #4, #5 ▼ 启动 olive-renderer --mode=batch --frames=5 --times=0/30,1/30,2/30,3/30,4/30 --output-shm=/olive_r_batch_1 --output-shm-size=165888000 (5 * 33MB) │ ▼ 子进程依次渲染 5 帧,全部写入同一块共享内存的不同偏移 输出 JSON 数组包含 5 个结果 exit(0) ``` --- ## 3. 命令行接口 ### 3.1 参数定义 ``` olive-renderer [选项] 全局选项: --backend= 渲染后端:opengl(默认), dummy --shader-path= 着色器资源目录(默认:可执行文件同级目录下的 shaders/) --ocio-config= OpenColorIO 配置文件路径 --verbose 输出详细日志到 stderr 单帧模式(--mode=frame): --mode=frame --node-graph= 节点图 XML 文件路径 --output-node= 输出节点 ID(默认:图中的第一个 ViewerOutput) --time= 渲染时间点,如 "1001/30000" --video-params= 视频参数 JSON,如 '{"width":1920,"height":1080,"format":"rgba32f"}' --audio-params= 音频参数 JSON --color-ref= 参考色彩空间 --color-display= 显示色彩空间 --output-shm= POSIX 共享内存名称(如 "/olive_r_123")或临时文件路径 --output-shm-size= 共享内存大小 --output-stdout 将帧数据 base64 编码输出到 stdout(仅小帧/测试用) 批处理模式(--mode=batch): --mode=batch --node-graph= --output-node= --times= 逗号分隔的时间点列表,如 "0/30,1/30,2/30" --video-params= --audio-params= --output-shm= --output-shm-size= 音频模式(--mode=audio): --mode=audio --node-graph= --output-node= --start= 起始时间 --duration= 持续时间 --audio-params= --output-shm= --output-shm-size= ``` ### 3.2 使用示例 ```bash # 单帧渲染 olive-renderer \ --mode=frame \ --node-graph=/tmp/project_graph.xml \ --output-node=ViewerOutput1 \ --time=1001/30000 \ --video-params='{"width":1920,"height":1080,"format":"rgba32f","channel_count":4}' \ --output-shm=/olive_frame_12345 \ --output-shm-size=33177600 # 批处理渲染 5 帧 olive-renderer \ --mode=batch \ --node-graph=/tmp/project_graph.xml \ --times="0/24,1/24,2/24,3/24,4/24" \ --output-shm=/olive_batch_67890 \ --output-shm-size=165888000 ``` --- ## 4. 输出格式 ### 4.1 标准输出(stdout) 子进程将渲染结果以 **单行 JSON** 输出到 stdout,然后退出。 **单帧成功**: ```json {"status":"ok","mode":"frame","time":"1001/30000","width":1920,"height":1080,"format":"rgba32f","pixel_format_id":28,"shm_name":"/olive_frame_12345","data_offset":256,"data_size":33177600,"linesize":7680,"render_time_ms":42} ``` **批处理成功**: ```json {"status":"ok","mode":"batch","frame_count":5,"frames":[{"time":"0/24","data_offset":256,"data_size":33177600},{"time":"1/24","data_offset":33178056,"data_size":33177600},...],"render_time_ms":180} ``` **错误**: ```json {"status":"error","error_code":"decoder_failure","message":"Failed to open decoder for footage 'clip001.mp4': codec not found","time":"1001/30000"} ``` **被取消**(主进程 kill 时不会收到,因为进程已死;但对于批处理中的内部取消): ```json {"status":"cancelled","frames_completed":3,"frames_total":5} ``` ### 4.2 标准错误(stderr) - `--verbose` 模式下,详细的调试日志输出到 stderr。 - 错误信息同时出现在 stderr(人类可读)和 stdout JSON(机器可读)中。 ### 4.3 退出码 | 退出码 | 含义 | |---|---| | 0 | 渲染成功 | | 1 | 通用错误 | | 2 | 无效参数 | | 3 | 初始化失败(OpenGL/OCIO 等) | | 4 | 节点图加载失败 | | 5 | 渲染过程中出错(解码失败、着色器编译失败等) | | 6 | 输出写入失败(共享内存不足等) | | 130 | 被信号中断(SIGINT,即主进程 kill) | | 137 | 被 SIGKILL 终止 | --- ## 5. 内部架构 ``` olive-renderer (main.cpp) │ ▼ ┌─────────────────────────────┐ │ 1. 解析命令行参数 │ │ (QCommandLineParser) │ └─────────────┬───────────────┘ │ ┌─────────────▼───────────────┐ │ 2. 初始化 QCoreApplication │ │ (无 GUI,无事件循环) │ └─────────────┬───────────────┘ │ ┌─────────────▼───────────────┐ │ 3. 加载节点图 XML │ │ ProjectSerializer::Load() │ │ 找到目标 ViewerOutput │ └─────────────┬───────────────┘ │ ┌─────────────▼───────────────┐ │ 4. 初始化渲染后端 │ │ OpenGL: QOffscreenSurface │ │ + QOpenGLContext │ │ Dummy: 空实现 │ └─────────────┬───────────────┘ │ ┌─────────────▼───────────────┐ │ 5. 执行渲染 │ │ RenderProcessor::Process()│ │ 遍历节点图 → 生成帧 │ └─────────────┬───────────────┘ │ ┌─────────────▼───────────────┐ │ 6. 写入共享内存 │ │ mmap/shm_open/MapViewOfFile│ │ 写入 ShmHeader + 像素数据 │ └─────────────┬───────────────┘ │ ┌─────────────▼───────────────┐ │ 7. 输出 JSON 结果到 stdout │ │ 8. 清理(可选,因即将 exit) │ │ 9. return status; │ └─────────────────────────────┘ ``` ### 5.1 主函数伪代码 ```cpp // app/render/renderer_main.cpp #include #include #include #include #include #include #include #include "olive/render_api.h" #include "olive/node_api.h" int main(int argc, char** argv) { QCoreApplication app(argc, argv); QCommandLineParser parser; // 定义命令行选项 parser.addOption({"mode", "Render mode: frame, batch, audio", "mode", "frame"}); parser.addOption({"node-graph", "Node graph XML file", "path"}); parser.addOption({"output-node", "Output node ID", "id"}); parser.addOption({"time", "Frame time (rational)", "time"}); parser.addOption({"times", "Batch frame times (csv)", "csv"}); parser.addOption({"video-params", "Video params JSON", "json"}); parser.addOption({"audio-params", "Audio params JSON", "json"}); parser.addOption({"output-shm", "Output shared memory name", "name"}); parser.addOption({"output-shm-size", "Output shared memory size", "bytes"}); parser.addOption({"backend", "Render backend", "backend", "opengl"}); parser.addOption({"shader-path", "Shader directory path", "path"}); parser.addOption({"verbose", "Verbose logging"}); parser.process(app); // 验证必需参数 if (!parser.isSet("node-graph") || !parser.isSet("output-shm")) { OutputError("Missing required arguments: --node-graph and --output-shm"); return 2; } // 1. 加载节点图 OliveNodeGraph* graph = olive_node_graph_create(); QFile xml_file(parser.value("node-graph")); if (!xml_file.open(QIODevice::ReadOnly)) { OutputError("Failed to open node graph file"); return 4; } QByteArray xml_data = xml_file.readAll(); if (olive_node_graph_load_xml(graph, xml_data.constData(), xml_data.size()) != OLIVE_OK) { OutputError("Failed to parse node graph XML"); return 4; } // 2. 找到输出节点 const char* output_node_id = parser.value("output-node").toUtf8().constData(); OliveNode* output_node = olive_node_graph_find_node(graph, output_node_id); if (!output_node) { // 如果没指定,找第一个 ViewerOutput // ... } // 3. 初始化渲染上下文 OliveRenderBackend backend = parser.value("backend") == "dummy" ? OLIVE_RENDER_BACKEND_DUMMY : OLIVE_RENDER_BACKEND_OPENGL; OliveRenderContext* ctx = olive_render_context_create(backend); if (!ctx) { OutputError("Failed to create render context"); return 3; } if (olive_render_context_init(ctx) != OLIVE_OK) { OutputError("Failed to initialize render backend"); return 3; } // 4. 准备共享内存 QString shm_name = parser.value("output-shm"); size_t shm_size = parser.value("output-shm-size").toULongLong(); void* shm_ptr = MapSharedMemory(shm_name, shm_size); if (!shm_ptr) { OutputError("Failed to map shared memory"); return 6; } // 5. 执行渲染 QElapsedTimer timer; timer.start(); QString mode = parser.value("mode"); QJsonObject result; if (mode == "frame") { // 单帧渲染 OliveRenderFrameParams params = ParseFrameParams(parser, graph, output_node); void* frame_data = nullptr; size_t frame_size = 0; int w, h; OlivePixelFormat fmt; int err = olive_render_frame_sync(ctx, ¶ms, &frame_data, &frame_size, &w, &h, &fmt); if (err == OLIVE_OK) { // 写入共享内存 WriteFrameToShm(shm_ptr, frame_data, frame_size, w, h, fmt); olive_core_free(frame_data); result["status"] = "ok"; result["mode"] = "frame"; result["width"] = w; result["height"] = h; result["shm_name"] = shm_name; result["data_offset"] = 256; result["data_size"] = static_cast(frame_size); } else { result["status"] = "error"; result["message"] = olive_core_last_error_string(); } } else if (mode == "batch") { // 批处理渲染... } result["render_time_ms"] = timer.elapsed(); // 6. 输出 JSON std::cout << QJsonDocument(result).toJson(QJsonDocument::Compact).toStdString() << std::endl; // 7. 清理 UnmapSharedMemory(shm_ptr, shm_size); olive_render_context_destroy(ctx); olive_node_graph_destroy(graph); return result["status"].toString() == "ok" ? 0 : 5; } ``` --- ## 6. 共享内存实现 ### 6.1 跨平台封装 ```cpp // app/render/shared_memory.h #pragma once #include #ifdef __cplusplus extern "C" { #endif void* olive_shm_create(const char* name, size_t size); void* olive_shm_open(const char* name, size_t size); void olive_shm_close(void* ptr, size_t size); void olive_shm_unlink(const char* name); #ifdef __cplusplus } #endif ``` ### 6.2 POSIX 实现 ```cpp #include "shared_memory.h" #include #include #include #include #include void* olive_shm_create(const char* name, size_t size) { int fd = shm_open(name, O_RDWR | O_CREAT, 0666); if (fd < 0) return nullptr; if (ftruncate(fd, size) < 0) { close(fd); return nullptr; } void* ptr = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); close(fd); return ptr; } void* olive_shm_open(const char* name, size_t size) { int fd = shm_open(name, O_RDWR, 0666); if (fd < 0) return nullptr; void* ptr = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); close(fd); return ptr; } void olive_shm_close(void* ptr, size_t size) { if (ptr) munmap(ptr, size); } void olive_shm_unlink(const char* name) { shm_unlink(name); } ``` ### 6.3 Windows 实现 ```cpp #include void* olive_shm_create(const char* name, size_t size) { HANDLE hMap = CreateFileMappingA(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, static_cast(size), name); if (!hMap) return nullptr; return MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, size); // 注意:句柄需要保存以便后续关闭,此处简化 } void olive_shm_close(void* ptr, size_t size) { (void)size; if (ptr) UnmapViewOfFile(ptr); } ``` --- ## 7. 主进程中的集成 ### 7.1 渲染一帧的封装 ```cpp // app/render/render_process_launcher.h #pragma once #include #include #include #include "olive/core_api.h" #include "olive/node_api.h" namespace olive { class RenderProcessLauncher { public: struct FrameResult { bool success = false; QString error_message; int width = 0; int height = 0; OlivePixelFormat format = OLIVE_PIXEL_FMT_INVALID; QString shm_name; size_t data_offset = 0; size_t data_size = 0; int render_time_ms = 0; }; // 渲染单帧,阻塞直到子进程结束 static FrameResult RenderFrameSync(const QString& nodeGraphXmlPath, const QString& outputNodeId, OliveRational time, const OliveVideoParams& videoParams, const OliveAudioParams& audioParams, OliveRenderMode mode, int timeoutMs = 30000); // 渲染单帧,异步(返回 QProcess*,调用方连接 finished 信号) static QProcess* RenderFrameAsync(const QString& nodeGraphXmlPath, const QString& outputNodeId, OliveRational time, const OliveVideoParams& videoParams, const OliveAudioParams& audioParams, OliveRenderMode mode); private: static QString BuildShmName(); static bool CreateShm(const QString& name, size_t size, void** outPtr); static void DestroyShm(const QString& name, void* ptr, size_t size); }; } // namespace olive ``` ### 7.2 使用示例 ```cpp // ViewerWidget 中请求渲染当前帧 void ViewerWidget::RequestFrameAtTime(OliveRational time) { // 1. 确保节点图 XML 已缓存 if (cached_graph_xml_path_.isEmpty()) { cached_graph_xml_path_ = SerializeNodeGraphToTempFile(viewer_output_); } // 2. 创建共享内存 QString shm_name = RenderProcessLauncher::BuildShmName(); size_t shm_size = CalculateFrameSize(video_params_); // 3. 启动子进程(异步) QProcess* proc = RenderProcessLauncher::RenderFrameAsync( cached_graph_xml_path_, olive_node_get_id(reinterpret_cast(viewer_output_)), time, video_params_, audio_params_, OLIVE_RENDER_MODE_ONLINE ); // 4. 连接完成信号 connect(proc, QOverload::of(&QProcess::finished), this, [this, proc, shm_name, shm_size](int exitCode, QProcess::ExitStatus status) { OnRenderProcessFinished(proc, shm_name, shm_size, exitCode, status); }); } void ViewerWidget::OnRenderProcessFinished(QProcess* proc, const QString& shm_name, size_t shm_size, int exitCode, QProcess::ExitStatus status) { if (status == QProcess::CrashExit) { qWarning() << "Renderer process crashed for frame"; // 不需要恢复状态,直接丢弃这一帧,UI 保持上一帧 proc->deleteLater(); return; } // 解析 stdout JSON QByteArray stdout_data = proc->readAllStandardOutput(); QJsonObject result = QJsonDocument::fromJson(stdout_data).object(); if (result["status"].toString() == "ok") { // 从共享内存读取帧 void* shm_ptr = olive_shm_open(shm_name.toUtf8().constData(), shm_size); if (shm_ptr) { DisplayFrameFromShm(shm_ptr, result); olive_shm_close(shm_ptr, shm_size); olive_shm_unlink(shm_name.toUtf8().constData()); } } else { qWarning() << "Render error:" << result["message"].toString(); } proc->deleteLater(); } ``` --- ## 8. 小步快跑实施步骤 ### Step 0: 验证进程启动开销(1 天) - [ ] 编写一个最小测试程序 `test_process_spawn.cpp`,只测量 `QProcess::start()` + `waitForStarted()` 的耗时。 - [ ] 加上 OpenGL 上下文初始化(`QOffscreenSurface` + `QOpenGLContext`)测量总耗时。 - [ ] 在目标平台(开发机)上测试,记录数据。 **验收标准**:得到精确的单进程启动耗时数据,作为是否采用"用完即弃"或"批处理模式"的依据。 ### Step 1: 创建 olive-renderer 可执行文件目标(1 天) - [ ] 新增 `app/render/renderer_main.cpp`。 - [ ] 在 `app/CMakeLists.txt` 中新增 `add_executable(olive-renderer ...)`。 - [ ] 链接 `oliverender`, `olivenode`, `olivecodec`, `olivecore`, `Qt::Core`。 **验收标准**:`olive-renderer` 编译成功,运行 `olive-renderer --help` 输出用法信息。 ### Step 2: 实现命令行解析与节点图加载(1 天) - [ ] 使用 `QCommandLineParser` 解析所有参数。 - [ ] 实现节点图 XML 加载(调用 `olive_node_graph_load_xml`)。 **验收标准**:`olive-renderer --node-graph=test.xml --output-shm=/test` 能成功加载节点图并找到 ViewerOutput。 ### Step 3: 实现共享内存读写(1 天) - [ ] 实现 `olive_shm_create/open/close/unlink`(POSIX + Windows)。 - [ ] 定义 `ShmHeader` 二进制布局。 **验收标准**:主进程创建 SHM,子进程写入数据,主进程读取并校验 CRC。 ### Step 4: 单帧端到端渲染(2 天) - [ ] 在 `olive-renderer` 中初始化 OpenGL 上下文。 - [ ] 调用 `olive_render_frame_sync` 渲染一帧。 - [ ] 将结果写入共享内存。 - [ ] 输出 JSON 到 stdout。 - [ ] 在主进程中编写 `RenderProcessLauncher::RenderFrameSync` 测试。 **验收标准**:主进程可以成功渲染一帧纯色/测试图,并在 Viewer 中显示。 ### Step 5: 集成到 ViewerWidget(2 天) - [ ] 修改 `ViewerWidget` 的帧请求逻辑,从直接调用 `RenderManager` 改为启动 `olive-renderer`。 - [ ] 处理异步完成信号,从共享内存读取帧并显示。 - [ ] 处理子进程崩溃(忽略该帧,保持上一帧显示)。 **验收标准**:拖动时间线时,Viewer 能实时显示渲染结果(可能有延迟,但功能正确)。 ### Step 6: 批处理模式(2 天,按需) - [ ] 若 Step 0 的测试显示单帧开销过高,实现 `--mode=batch`。 - [ ] 修改 `RenderProcessLauncher` 支持批处理。 ### Step 7: 导出集成(2 天) - [ ] 修改导出任务(`task/export/`),使用 `olive-renderer` 子进程逐帧渲染,然后编码。 - [ ] 导出天然适合批处理模式(可以一次性渲染 10–50 帧)。 --- ## 9. 风险与回退 | 风险 | 对策 | |---|---| | 进程启动开销导致实时预览 < 10fps | 启用批处理模式,每 3–5 帧一个进程;或预启动一个进程池(但每个进程仍只服务一个批次后自杀)。 | | OpenGL 驱动不支持离屏渲染 | 在 Linux 上使用 `EGL` 替代 `QOffscreenSurface`;在 Windows 上使用 `WGL` pbuffer;在 macOS 上使用 `CGL` pixel buffer。 | | 共享内存名称冲突 | 使用 `QUuid::createUuid()` 生成唯一名称,格式为 `/olive__`。 | | 共享内存泄漏(子进程崩溃后未 unlink) | 主进程在启动子进程前注册一个定时器,若子进程异常退出,5 秒后自动 `shm_unlink`。 | | 磁盘空间不足(临时文件方案) | 渲染前检查磁盘空间,不足时返回错误码 6。 | | 节点图 XML 过大导致解析慢 | 启用增量序列化:只序列化自上次以来变更的节点和参数。 | | OFX 插件需要持久化状态 | OFX 插件实例不跨帧持久化,每帧重新创建。若某些插件初始化极慢,在 C API 中提供 `olive_plugin_instance_serialize_state` 接口,将状态快照传给下一帧的进程。 |