18 KiB
18 KiB
IPC 协议规范
本文件定义主进程与
olive-renderer子进程之间的全部通信方式。由于采用**"用完即弃"模型,协议被设计为极简、无状态、单向请求-响应**。
1. 通信通道概览
| 通道 | 方向 | 用途 | 格式 |
|---|---|---|---|
| 命令行参数 | 主进程 → 子进程 | 传递渲染配置(模式、路径、参数) | POSIX 风格长选项 |
| stdin | 主进程 → 子进程 | 传递节点图 XML(当 XML 过大超出命令行长度限制时) | 原始 XML 字符串 |
| stdout | 子进程 → 主进程 | 返回渲染结果元数据 | 单行 JSON |
| stderr | 子进程 → 主进程 | 日志和详细错误信息 | 纯文本 |
| 共享内存 / 内存映射文件 | 双向 | 传输大帧数据(像素/采样) | 二进制(ShmHeader + 原始数据) |
| 退出码 | 子进程 → 主进程 | 快速判断成功/失败/异常 | 整数 (0–255) |
重要:由于子进程渲染完成后立即退出,不存在长期状态同步、心跳、取消信号(主进程直接 kill 即可)。
2. 命令行参数
2.1 参数总表
| 参数 | 类型 | 必需 | 默认值 | 说明 |
|---|---|---|---|---|
--mode |
string | 否 | frame |
渲染模式:frame, batch, audio |
--node-graph |
path | 是 | — | 节点图 XML 文件路径 |
--node-graph-stdin |
flag | 否 | false | 从 stdin 读取节点图 XML,而非文件 |
--output-node |
string | 否 | 自动探测 | 输出节点 ID |
--time |
rational | 条件 | — | 单帧时间点(--mode=frame 时必需) |
--times |
csv | 条件 | — | 多帧时间点列表(--mode=batch 时必需) |
--start |
rational | 条件 | — | 音频起始时间(--mode=audio 时必需) |
--duration |
rational | 条件 | — | 音频持续时间(--mode=audio 时必需) |
--video-params |
json | 条件 | — | 视频参数(--mode=frame/batch 时必需) |
--audio-params |
json | 条件 | — | 音频参数(--mode=audio 时必需) |
--color-ref |
string | 否 | — | 参考色彩空间名称 |
--color-display |
string | 否 | — | 显示色彩空间名称 |
--force-size |
json | 否 | — | 强制输出尺寸,如 {"width":1920,"height":1080} |
--force-format |
string | 否 | — | 强制像素格式,如 rgba32f |
--output-shm |
string | 是 | — | 输出共享内存名称或临时文件路径 |
--output-shm-size |
int | 是 | — | 输出缓冲区大小(字节) |
--output-stdout |
flag | 否 | false | 将帧数据 base64 编码输出到 stdout(仅小帧/测试) |
--backend |
string | 否 | opengl |
渲染后端:opengl, dummy |
--shader-path |
path | 否 | <exe_dir>/shaders |
着色器资源目录 |
--ocio-config |
path | 否 | — | OCIO 配置文件路径 |
--verbose |
flag | 否 | false | 详细日志输出到 stderr |
--version |
flag | 否 | false | 输出版本信息并退出 |
--help |
flag | 否 | false | 输出帮助信息并退出 |
2.2 参数值格式
Rational:"<numerator>/<denominator>",如 "1001/30000", "0/1"。
JSON:紧凑格式,键用双引号。例如:
--video-params='{"width":1920,"height":1080,"format":"rgba32f","channel_count":4,"depth":1}'
CSV:逗号分隔的有理数字符串。例如:
--times="0/24,1/24,2/24,3/24,4/24"
3. 标准输入(stdin)
当 --node-graph-stdin 标志存在时,子进程从 stdin 读取节点图 XML,而不是从 --node-graph 指定的文件。
3.1 使用场景
- 节点图 XML 非常大(> 100KB),超出命令行长度限制。
- 主进程不想在磁盘上创建临时文件。
- 安全考虑:敏感项目数据不写入磁盘。
3.2 协议
主进程 子进程
│ │
│── XML 数据 ──>│(子进程读取 stdin 直到 EOF)
│ │
子进程读取 stdin 的全部内容,视为节点图 XML 字符串。XML 结束后不需要特殊分隔符(EOF 即结束)。
注意:由于子进程使用 QCoreApplication 且不使用 Qt 的事件循环读取 stdin,应使用阻塞式 QTextStream(stdin).readAll() 或 std::cin。
4. 标准输出(stdout)
子进程将渲染结果以单行 JSON 输出到 stdout,以换行符 \n 结尾。主进程读取第一行后即视为响应完成。
4.1 单帧模式输出(--mode=frame)
成功:
{
"status": "ok",
"mode": "frame",
"time": "1001/30000",
"width": 1920,
"height": 1080,
"format": "rgba32f",
"pixel_format_id": 28,
"channel_count": 4,
"shm_name": "/olive_frame_abc123",
"data_offset": 256,
"data_size": 33177600,
"linesize": 7680,
"render_time_ms": 42
}
失败:
{
"status": "error",
"mode": "frame",
"time": "1001/30000",
"error_code": "decoder_failure",
"message": "Failed to open decoder for footage 'clip001.mp4'",
"render_time_ms": 5
}
4.2 批处理模式输出(--mode=batch)
成功:
{
"status": "ok",
"mode": "batch",
"frame_count": 5,
"frames": [
{"time": "0/24", "data_offset": 256, "data_size": 33177600, "render_time_ms": 45},
{"time": "1/24", "data_offset": 33178056, "data_size": 33177600, "render_time_ms": 38},
{"time": "2/24", "data_offset": 66356112, "data_size": 33177600, "render_time_ms": 41},
{"time": "3/24", "data_offset": 99534168, "data_size": 33177600, "render_time_ms": 39},
{"time": "4/24", "data_offset": 132712224, "data_size": 33177600, "render_time_ms": 42}
],
"total_render_time_ms": 205
}
4.3 音频模式输出(--mode=audio)
成功:
{
"status": "ok",
"mode": "audio",
"start": "0/1",
"duration": "48000/48000",
"sample_rate": 48000,
"channels": 2,
"sample_count": 48000,
"shm_name": "/olive_audio_abc123",
"data_offset": 256,
"data_size": 384000,
"render_time_ms": 15
}
4.4 字段说明
| 字段 | 类型 | 出现条件 | 说明 |
|---|---|---|---|
status |
string | 总是 | "ok", "error", "cancelled" |
mode |
string | 总是 | "frame", "batch", "audio" |
time / times |
string | frame/batch | 渲染时间点 |
width |
int | frame/batch | 帧宽 |
height |
int | frame/batch | 帧高 |
format |
string | frame/batch | 像素格式名称 |
pixel_format_id |
int | frame/batch | 像素格式枚举值 |
channel_count |
int | frame/batch | 通道数 |
shm_name |
string | 总是 | 共享内存名称 |
data_offset |
int | 总是 | 实际数据在共享内存中的偏移(跳过 ShmHeader) |
data_size |
int | 总是 | 实际数据大小(字节) |
linesize |
int | frame/batch | 每行字节数(含 padding) |
frame_count |
int | batch | 批处理帧数 |
frames |
array | batch | 每帧的元数据 |
sample_rate |
int | audio | 采样率 |
channels |
int | audio | 通道数 |
sample_count |
int | audio | 采样数 |
error_code |
string | error | 错误分类码 |
message |
string | error | 人类可读错误信息 |
render_time_ms |
int | 总是 | 纯渲染耗时(不含进程启动) |
5. 共享内存二进制布局
5.1 整体结构
┌─────────────────────────────────────────────────────────────────┐
│ 共享内存区域 │
├────────────────────────┬────────────────────────────────────────┤
│ ShmHeader (256 B) │ Payload Data │
│ │ │
│ magic │ 帧像素数据 / 音频采样数据 │
│ version │ │
│ data_offset │ 大小 = data_size │
│ data_size │ │
│ width │ │
│ height │ │
│ format │ │
│ linesize │ │
│ checksum │ │
│ reserved[...] │ │
└────────────────────────┴────────────────────────────────────────┘
5.2 ShmHeader 定义(C 结构)
#include <stdint.h>
#define OLIVE_SHM_MAGIC 0x4F4C4956 // 'OLIV' 大端序
#define OLIVE_SHM_VERSION 1
#define OLIVE_SHM_HEADER_SIZE 256
typedef struct {
uint32_t magic; // OLIVE_SHM_MAGIC
uint32_t version; // OLIVE_SHM_VERSION
uint32_t data_offset; // Payload 数据起始偏移(>= 256)
uint64_t data_size; // Payload 实际数据大小(字节)
uint32_t width; // 帧宽 或 采样数
uint32_t height; // 帧高 或 0(音频)
uint32_t depth; // 3D 纹理深度(通常 1)
uint32_t channel_count; // 通道数
uint32_t pixel_format; // OlivePixelFormat 枚举值
uint32_t linesize; // 每行字节数(视频)或 0(音频)
uint64_t checksum; // CRC64 校验和(可选,0 表示未校验)
uint8_t reserved[256 - 48]; // 填充至 256 字节,未来扩展用
} OliveShmHeader;
// 辅助:计算 CRC64
uint64_t olive_shm_checksum(const void* data, size_t size);
5.3 校验和(Checksum)
- 默认启用 CRC64 校验。
checksum字段覆盖 Payload Data 区域(从data_offset开始的data_size字节)。- 若主进程设置
checksum = 0,表示跳过校验(用于调试或性能敏感场景)。
5.4 多帧批处理布局
批处理模式下,所有帧连续存储在同一块共享内存中:
Offset 0: ShmHeader (256 B)
Offset 256: Frame 0 data (frame_0_size B, 按 linesize 对齐)
Offset 256+N0: Frame 1 data (frame_1_size B)
Offset 256+N0+N1: Frame 2 data
...
每帧的 data_offset 在 stdout JSON 中单独指定。
6. 标准错误(stderr)
6.1 日志级别
当 --verbose 启用时,stderr 输出结构化日志:
[2024-05-21T10:30:15.123Z] [INFO] 初始化 OpenGL 上下文
[2024-05-21T10:30:15.245Z] [INFO] OpenGL 版本: 4.6.0 NVIDIA 535.104
[2024-05-21T10:30:15.310Z] [INFO] 加载节点图: 42 个节点
[2024-05-21T10:30:15.412Z] [INFO] 开始渲染帧 @ 1001/30000
[2024-05-21T10:30:15.454Z] [INFO] 渲染完成, 耗时 42ms
[2024-05-21T10:30:15.455Z] [INFO] 写入共享内存: /olive_frame_abc123, 33177600 bytes
6.2 错误日志
错误同时输出到 stderr 和 stdout JSON:
[2024-05-21T10:30:15.456Z] [ERROR] Decoder 初始化失败: codec not found for 'hevc'
7. 退出码
| 退出码 | 名称 | 含义 | 主进程应对 |
|---|---|---|---|
| 0 | EXIT_OK |
渲染成功 | 正常处理结果 |
| 1 | EXIT_GENERIC_ERROR |
通用错误 | 记录错误,丢弃该帧 |
| 2 | EXIT_INVALID_ARGS |
命令行参数无效 | 检查主进程参数组装逻辑 |
| 3 | EXIT_INIT_FAILED |
初始化失败(OpenGL/OCIO) | 尝试 dummy 后端或提示用户 |
| 4 | EXIT_GRAPH_LOAD_FAILED |
节点图加载/解析失败 | 检查 XML 序列化逻辑 |
| 5 | EXIT_RENDER_FAILED |
渲染过程中出错 | 记录具体错误,丢弃该帧 |
| 6 | EXIT_OUTPUT_FAILED |
输出写入失败(SHM 不足) | 清理 SHM,重试或报错 |
| 130 | EXIT_SIGINT |
收到 SIGINT(Ctrl+C / kill -2) | 视为取消,正常丢弃 |
| 137 | EXIT_SIGKILL |
收到 SIGKILL(kill -9) | 视为取消,正常丢弃 |
| 139 | EXIT_SEGFAULT |
段错误(未捕获信号) | 视为崩溃,记录日志 |
| 其他 | — | 未知错误 | 记录日志,丢弃该帧 |
8. 主进程与子进程交互时序
8.1 单帧完整时序
时间轴 ──────────────────────────────────────────────────────────────>
主进程: [准备参数] [创建SHM] [QProcess::start()] [等待] [读JSON] [读SHM] [unlink SHM]
│ │ │ │ │ │ │
子进程: [启动] [解析参数] [加载XML] [初始化GL] [渲染] [写SHM] [写stdout] [exit]
│ │ │ │ │ │ │
└────────┴────────┴───────────┴────────┴────────┴──────────┘
进程生命周期
8.2 异常情况时序
子进程崩溃(segfault):
主进程: [start] ── [wait] ── [finished信号] ── [exitStatus == CrashExit] ── [忽略该帧]
子进程: [启动] ── [崩溃] ──────────────────── [操作系统回收资源]
主进程取消(kill):
主进程: [start] ── [用户操作/超时] ── [QProcess::kill()] ── [finished信号] ── [unlink SHM]
子进程: [启动] ── [渲染中] ───────── [SIGKILL] ──────────── [立即终止]
9. 共享内存生命周期管理
9.1 创建
// 主进程创建
QString shm_name = "/olive_" + QUuid::createUuid().toString(QUuid::WithoutBraces);
size_t shm_size = CalculateFrameSize(params) + OLIVE_SHM_HEADER_SIZE;
void* shm_ptr = olive_shm_create(shm_name.toUtf8().constData(), shm_size);
9.2 命名规范
- POSIX:
/olive_<uuid>,必须以/开头,长度 < 255。 - Windows:
Local\\olive_<uuid>或Global\\olive_<uuid>。 - 临时文件:
/tmp/olive_<pid>_<uuid>.raw(内存映射临时文件回退方案)。
9.3 清理策略
正常路径:
- 子进程成功渲染,写入数据,退出。
- 主进程读取数据。
- 主进程
olive_shm_close(ptr, size)+olive_shm_unlink(name)。
异常路径(子进程崩溃):
- 主进程检测到
QProcess::CrashExit。 - 主进程立即
olive_shm_unlink(name)(即使数据未读取)。
双重保险:
- 主进程在创建 SHM 时启动一个
QTimer(5 秒后触发)。 - 若 5 秒后 SHM 仍未被清理(异常路径未执行到 unlink),定时器自动
olive_shm_unlink(name)。 - 防止子进程崩溃后主进程也崩溃导致的 SHM 泄漏。
10. 平台差异
10.1 Linux
- 共享内存:
/dev/shm/下的 tmpfs 文件。 - 最大名称长度:255 字节(含 null)。
- 权限:
shm_open使用0666,确保子进程可以打开。 - 系统限制:
/proc/sys/kernel/shmmax通常足够大(> 1GB)。
10.2 macOS
- 共享内存:
shm_open创建的 POSIX 共享内存对象。 - 注意:macOS 的
shm_open名称长度限制为 31 字符(包括开头的/)! - 解决方案:使用 内存映射临时文件 替代 POSIX shm。
// macOS 专用:使用临时文件替代 shm_open
int fd = mkstemp("/tmp/olive_XXXXXX.raw");
ftruncate(fd, size);
void* ptr = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
// 将文件路径传递给子进程
10.3 Windows
- 共享内存:
CreateFileMapping+MapViewOfFile。 - 名称:
Local\\olive_<uuid>(用户会话内可见)。 - 注意:句柄管理。子进程
MapViewOfFile后需要保留HANDLE以便后续UnmapViewOfFile和CloseHandle。 - 简化方案:子进程只负责写入,不关闭映射。进程退出后操作系统自动回收。
11. 性能调优建议
11.1 减少进程启动开销
| 技术 | 效果 | 复杂度 |
|---|---|---|
静态链接 olive-renderer |
避免动态库加载开销 | 低 |
| 预加载 Qt 插件 | 减少 QCoreApplication 初始化时间 |
低 |
| 批处理模式 | 摊销 OpenGL 上下文创建开销 | 中 |
使用 EGL 替代 GLX/WGL |
EGL 上下文创建更快 | 中 |
11.2 减少序列化开销
| 技术 | 效果 | 复杂度 |
|---|---|---|
| 节点图 XML 缓存 | 相同图只序列化一次 | 低 |
| 增量参数更新 | 仅发送变更的参数 | 中 |
| 二进制序列化格式 | 替代 XML,解析更快 | 高 |
11.3 减少共享内存开销
| 技术 | 效果 | 复杂度 |
|---|---|---|
| 共享内存池 | 预分配 N 块循环使用 | 中 |
| 内存映射临时文件 | 避免 shm_open 系统调用 |
低 |
| 零拷贝(GPU 纹理共享) | 跨进程直接共享 GPU 纹理 | 高(平台相关) |
12. 调试工具
12.1 手动运行子进程
# 直接运行 olive-renderer,独立于主进程
./olive-renderer \
--mode=frame \
--node-graph=/tmp/debug_graph.xml \
--time=0/1 \
--video-params='{"width":100,"height":100,"format":"rgba32f"}' \
--output-shm=/olive_debug \
--output-shm-size=160000 \
--verbose
# 查看 stdout 输出
# 查看 stderr 日志
# 用另一个程序读取 /olive_debug 验证像素数据
12.2 环境变量
| 变量 | 作用 |
|---|---|
OLIVE_RENDERER_BACKEND=dummy |
强制使用 dummy 后端(跳过 OpenGL) |
OLIVE_RENDERER_TIMEOUT=60000 |
子进程超时时间(毫秒) |
OLIVE_RENDERER_KEEP_SHM=1 |
子进程退出后不删除共享内存(调试) |
OLIVE_RENDERER_LOG_FILE=/path/to.log |
将日志写入文件 |
12.3 重放渲染
将主进程发送给子进程的所有输入(命令行参数 + XML)保存到日志目录,可以精确重放某一帧的渲染:
# 主进程日志目录:~/.local/share/oak/renderer_logs/
# 每个渲染任务保存为:
# frame_12345.params (命令行参数)
# frame_12345.xml (节点图)
# 重放
./olive-renderer $(cat ~/.local/share/oak/renderer_logs/frame_12345.params)