480 lines
18 KiB
Markdown
480 lines
18 KiB
Markdown
# 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`)
|
||
|
||
**成功:**
|
||
```json
|
||
{
|
||
"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
|
||
}
|
||
```
|
||
|
||
**失败:**
|
||
```json
|
||
{
|
||
"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`)
|
||
|
||
**成功:**
|
||
```json
|
||
{
|
||
"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`)
|
||
|
||
**成功:**
|
||
```json
|
||
{
|
||
"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 结构)
|
||
|
||
```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 创建
|
||
|
||
```cpp
|
||
// 主进程创建
|
||
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 清理策略
|
||
|
||
**正常路径**:
|
||
1. 子进程成功渲染,写入数据,退出。
|
||
2. 主进程读取数据。
|
||
3. 主进程 `olive_shm_close(ptr, size)` + `olive_shm_unlink(name)`。
|
||
|
||
**异常路径(子进程崩溃)**:
|
||
1. 主进程检测到 `QProcess::CrashExit`。
|
||
2. 主进程立即 `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。
|
||
|
||
```cpp
|
||
// 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 手动运行子进程
|
||
|
||
```bash
|
||
# 直接运行 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)保存到日志目录,可以精确重放某一帧的渲染:
|
||
|
||
```bash
|
||
# 主进程日志目录:~/.local/share/oak/renderer_logs/
|
||
# 每个渲染任务保存为:
|
||
# frame_12345.params (命令行参数)
|
||
# frame_12345.xml (节点图)
|
||
|
||
# 重放
|
||
./olive-renderer $(cat ~/.local/share/oak/renderer_logs/frame_12345.params)
|
||
```
|