Files
oak-editor/docs/zh/modularization-plan/08-olive-renderer.md
T

26 KiB
Raw Blame History

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 上下文初始化可能需要 50200ms 若实测开销过高,采用 "批处理模式":一个进程渲染 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=<backend>        渲染后端:opengl(默认), dummy
  --shader-path=<path>       着色器资源目录(默认:可执行文件同级目录下的 shaders/
  --ocio-config=<path>       OpenColorIO 配置文件路径
  --verbose                  输出详细日志到 stderr

单帧模式(--mode=frame):
  --mode=frame
  --node-graph=<path>        节点图 XML 文件路径
  --output-node=<id>         输出节点 ID(默认:图中的第一个 ViewerOutput
  --time=<rational>          渲染时间点,如 "1001/30000"
  --video-params=<json>      视频参数 JSON,如 '{"width":1920,"height":1080,"format":"rgba32f"}'
  --audio-params=<json>      音频参数 JSON
  --color-ref=<space>        参考色彩空间
  --color-display=<space>    显示色彩空间
  --output-shm=<name>        POSIX 共享内存名称(如 "/olive_r_123")或临时文件路径
  --output-shm-size=<bytes>  共享内存大小
  --output-stdout            将帧数据 base64 编码输出到 stdout(仅小帧/测试用)

批处理模式(--mode=batch):
  --mode=batch
  --node-graph=<path>
  --output-node=<id>
  --times=<csv>              逗号分隔的时间点列表,如 "0/30,1/30,2/30"
  --video-params=<json>
  --audio-params=<json>
  --output-shm=<name>
  --output-shm-size=<bytes>

音频模式(--mode=audio):
  --mode=audio
  --node-graph=<path>
  --output-node=<id>
  --start=<rational>         起始时间
  --duration=<rational>      持续时间
  --audio-params=<json>
  --output-shm=<name>
  --output-shm-size=<bytes>

3.2 使用示例

# 单帧渲染
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,然后退出。

单帧成功

{"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}

批处理成功

{"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}

错误

{"status":"error","error_code":"decoder_failure","message":"Failed to open decoder for footage 'clip001.mp4': codec not found","time":"1001/30000"}

被取消(主进程 kill 时不会收到,因为进程已死;但对于批处理中的内部取消):

{"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 主函数伪代码

// app/render/renderer_main.cpp

#include <QCoreApplication>
#include <QCommandLineParser>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QElapsedTimer>
#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, &params, &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<qint64>(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 跨平台封装

// app/render/shared_memory.h

#pragma once
#include <stddef.h>

#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 实现

#include "shared_memory.h"
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

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 实现

#include <windows.h>

void* olive_shm_create(const char* name, size_t size) {
    HANDLE hMap = CreateFileMappingA(INVALID_HANDLE_VALUE, nullptr,
                                      PAGE_READWRITE, 0, static_cast<DWORD>(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 渲染一帧的封装

// app/render/render_process_launcher.h

#pragma once
#include <QString>
#include <QProcess>
#include <QJsonObject>
#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 使用示例

// 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<OliveNode*>(viewer_output_)),
        time,
        video_params_,
        audio_params_,
        OLIVE_RENDER_MODE_ONLINE
    );

    // 4. 连接完成信号
    connect(proc, QOverload<int, QProcess::ExitStatus>::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/unlinkPOSIX + Windows)。
  • 定义 ShmHeader 二进制布局。

验收标准:主进程创建 SHM,子进程写入数据,主进程读取并校验 CRC。

Step 4: 单帧端到端渲染(2 天)

  • olive-renderer 中初始化 OpenGL 上下文。
  • 调用 olive_render_frame_sync 渲染一帧。
  • 将结果写入共享内存。
  • 输出 JSON 到 stdout。
  • 在主进程中编写 RenderProcessLauncher::RenderFrameSync 测试。

验收标准:主进程可以成功渲染一帧纯色/测试图,并在 Viewer 中显示。

Step 5: 集成到 ViewerWidget2 天)

  • 修改 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_<pid>_<uuid>
共享内存泄漏(子进程崩溃后未 unlink) 主进程在启动子进程前注册一个定时器,若子进程异常退出,5 秒后自动 shm_unlink
磁盘空间不足(临时文件方案) 渲染前检查磁盘空间,不足时返回错误码 6。
节点图 XML 过大导致解析慢 启用增量序列化:只序列化自上次以来变更的节点和参数。
OFX 插件需要持久化状态 OFX 插件实例不跨帧持久化,每帧重新创建。若某些插件初始化极慢,在 C API 中提供 olive_plugin_instance_serialize_state 接口,将状态快照传给下一帧的进程。