为什么相机生产链坚持单一 Owner,并拒绝静默 Runtime Fallback
相机系统最危险的架构不是某条链路性能差,而是两条链路都“似乎能工作”。当采集、畸变校正、ROS 发布和算法接入分别保留自己的 owner、buffer pool 与恢复逻辑后,系统会进入一种很难证明正确的状态:资源是谁占的、时间戳来自哪里、失败后切到了哪条路,都不再清楚。
所以这次架构改造的核心不是换一个更快的 API,而是把生产链收敛为单一 owner,并让失败明确发生。
旧思路的问题:每个 consumer 都能拿到图
早期系统里,相机数据可以经过 ROS topic、共享内存、兼容 publisher 或独立采集节点到达算法。每条路径单独看都合理,但组合起来会产生几个问题:
- 多个进程可能争抢同一视频设备;
- 相同像素被重复转换和复制;
- 不同路径各自解释曝光、PTS 和 group timestamp;
- 恢复时无法确认旧进程是否仍持有设备或 buffer;
- A/B 测量可能把两个版本的进程混在一起。
这类问题往往不会在编译阶段出现,而是在重启、降级或高负载下变成偶发故障。
最终架构:一个 owner,一次采集,多路消费
生产架构被收敛为:
V4L2 / native capture owner
→ typed frame bundle
→ trigger / exposure timing matcher
→ hardware fisheye / transform
→ NV12 stereo atlas
├─ direct algorithm consumers
├─ encoder / SDK consumers
└─ compatibility ROS adapterruntime 是唯一相机 owner。Core 不依赖 ROS,负责 FrameHandle、BufferLease、时间和布局合同;RK3588 adapter 持有 V4L2、DMA-BUF、RGA、OpenCL 与 MPP 的平台细节;ROS bridge 只做兼容发布,不拥有采集链。
这让“谁负责什么”变得可验证:
- capture owner 决定 buffer 何时进入和离开驱动;
- timing matcher 决定哪些帧属于同一组;
- executor 产出带 layout 的执行结果;
- consumer 只消费 typed contract;
- compatibility adapter 可以关闭,不反向定义生产主链。
为什么不做静默 runtime fallback
“OpenCL 失败就自动切 CPU”听起来很稳健,但生产上会制造更隐蔽的问题:
- 服务仍然 active,实际性能和延迟已经完全改变;
- CPU fallback 可能让温度、帧率和其他实时任务一起退化;
- 指标只看到业务仍有输出,却看不到系统已进入非目标架构;
- 故障无法复现,因为重启后又可能回到硬件路径。
因此生产主链坚持:平台能力或配置不满足时显式失败。需要回滚时,部署旧版本或切换明确的 legacy mode,而不是在同一个进程里悄悄换执行路径。
Runtime fallback 和版本回滚不是一回事。 前者改变运行语义却可能隐藏故障,后者是可观测、可审计的运维动作。
一次踩坑:把目标能力写成了已完成能力
架构文档第一次整理时,direct consumer 已经存在,很容易顺手写成“ROS BGR 路径已经移除”。但源码检查发现兼容 publisher 仍然会做 NV12 到 BGR、mmap/read 和消息复制。
这不是措辞问题,而是证据边界问题。修正后的文档明确区分:
- 已实现:typed graph、单一 owner、DMA-BUF fan-out、direct consumer;
- 兼容路径仍存在:ROS BGR publisher;
- 待验证:subscriber-aware 关闭策略和新的板端 CPU A/B。
架构说明如果把目标状态写成当前事实,后续性能分析就会从错误基线出发。
队列为什么也是架构合同
共享 buffer 不代表所有 consumer 必须同步阻塞。不同支路采用不同队列语义:
- source 到 matcher 使用有界队列,满时丢最旧帧;
- 实时 consumer 使用
Latest(1),保留一帧执行中和一帧最新待处理; - 每条支路有独立 worker,慢 consumer 不阻塞整个 graph。
这不是简单的性能参数。队列策略决定系统在过载时是增加延迟、丢旧数据,还是拖死生产者,必须成为显式合同。
停机顺序同样不能靠感觉:
Quiesce producer
→ Drain in-flight work
→ Disconnect consumers
→ Stop platform backend只等 queue empty 就停 backend 会留下竞态,因为 producer 仍可能继续投递;直接 disconnect 也不能强杀正在执行的 callback。
为什么这种设计更容易迁移平台
跨平台复用的不是 V4L2 或 OpenCL 代码,而是 frame、buffer、time、graph 和 consumer contract。迁移到另一颗 SoC 时,应该替换 capture、cache/fence、图像加速、编码和 NPU adapter,而不是复制整套 runtime。
迁移第一步也不是写 backend,而是 capability probe:
- 能否导出或导入共享 buffer;
- cache sync 与 fence 怎样表达;
- 多相机同步由谁保证;
- pool 深度和 release route 是否可控;
- 硬件畸变校正与 native tensor 能否接入。
能力不成立就明确失败,重新评估架构;不能先写一个 CPU fallback,把平台差异藏起来。
结论
单一 owner 的价值不是“代码更漂亮”,而是让设备、buffer、时间和失败都只有一份权威解释。拒绝静默 fallback,也不是追求激进,而是避免系统在最需要诊断时继续假装健康。
相机生产链最终要证明的,不只是有图,而是这张图从哪里来、经过什么硬件、由谁持有、何时可复用,以及能力不足时为什么会明确失败。