← 返回课程

Node机制

Camera全栈开发(Qcom Camx) 第 16 / 30 节

第 14 章:Node 机制 — 最小的处理单元 ⭐⭐⭐


本章导读

Pipeline 是一条生产线。生产线上的每一台机器,就是 Node

Node 是 CamX 中真正干活的单元。IFE Node 接收 RAW 数据、BPS Node 做 Bayer 处理、IPE Node 做 YUV 处理、JPEG Node 做编码……每个 Node 各司其职。

但 Node 之间不直接调用。它们通过 DependencyUnit 声明自己的依赖,由 Pipeline 统一调度。这是 CamX 实现异步并行的关键机制。


14.1 Node 是什么

Pipeline = 一条生产线
  Node A → Node B → Node C → ...

Node = 生产线上的机器
  IFE Node:   接收 MIPI RAW → 预处理 → 输出 Bayer RAW
  BPS Node:   Bayer → Demosaic → 去噪 → 输出 RGB/YUV
  IPE Node:   YUV → 色彩增强 → 锐化 → 输出最终 YUV
  JPEG Node:  YUV → JPEG 编码

Port = 机器的输入/输出口
  IFE 有 1 个输入 Port(MIPI)和 2 个输出 Port(Bayer RAW + Stats)

Link = 机器之间的传送带
  IFE.OutputPort0 → BPS.InputPort0

Node 的类型

类型 加速方式 源码位置 例子
HW Node ISP 硬件 camx/src/hwl/ IFE/BPS/IPE/JPEG
SW Node CPU 处理 camx/src/swl/ EIS/SWMF/Stats

HW Node 封装 ISP 硬件模块的寄存器操作,处理速度快,几毫秒完成。
SW Node 在 CPU 上运行算法,处理时间取决于 CPU 频率。

如果你在调试帧率问题,先看 SW Node 的处理时间——它们通常是瓶颈。


14.2 Node 的生命周期——以 IFE Node 为例

每个 Node 都遵循 6 个阶段的生命周期。这里以 IFE Node 为具体例子,展示它从创建到销毁经历的全过程。

阶段 1:Create — 出生

触发时机: Pipeline::Create() 中,为每个 Node 调用

IFE Node 的 Create 做了:
  ① 注册 Node 名称 "IFE" + 分配 Node ID
  ② 创建 1 个输入 Port(MIPI CSI → IFE)
  ③ 创建 2 个输出 Port(IFE → BPS, IFE → Stats Node)
  ④ 设置每个 Port 的 BufferProperties:
     输入 Port: format=RAW10, size=4000x3000, queueDepth=0
     输出 Port 1: format=Bayer RAW, queueDepth=4 (供 BPS 读)
     输出 Port 2: format=Stats, queueDepth=2 (供 Stats Node 读)
  ⑤ 声明 DependencyUnit:
     hasInputBuffersReadyDependency = false  (IFE 没有上游,不需要等 Buffer)
     hasPropertyDependency = true            (需要 AE/AWB 参数)
     hasFenceDependency = false

此时 IFE Node 只是"纸面上存在",没有任何硬件资源。

阶段 2:Initialize — 加载配置

触发时机: Pipeline::Initialize() 中

IFE Node 的 Initialize 做了:
  ① 从 chromatix 加载 IFE 模块参数:
     - BLS (黑电平值)
     - BPC (坏点表)
     - LSC (镜头阴影增益表)
  ② 注册需要的 Property(告诉 Metadata 系统: 我需要 AE_MODE、EXPOSURE_TIME 等)
  ③ 分配内部所需的配置结构体内存

此时 IFE 配置已加载,但硬件还没启动。

阶段 3:Activate — 启动硬件

触发时机: Pipeline::PartialStreamOn() 或 StreamOn()

IFE Node 的 Activate 做了:
  ① 调用 CSL 层: CSLStreamOn(IFE) → ioctl → Kernel 启动 IFE 硬件
  ② 设置 IFE 的 MIPI 接收参数(Lane 数量、速率)
  ③ HW Node: 写 ISP 寄存器,告诉 IFE"开始干活"
  ④ SW Node: 算法初始化(IFE 是 HW Node,此步为空)

此后 IFE 硬件开始等待 Sensor 的 MIPI 数据。
一旦有数据进来,立即处理。

阶段 4:ProcessRequest — 干活(循环)

触发时机: 每次 sensor 送来一帧时

IFE Node 的 ProcessRequest 做了:
  ① 从输入 Port 获取 RAW Buffer(Sensor 写入的)
  ② ISP 硬件处理: BLS → Linearization → BPC → LSC
  ③ 提取 Stats(AE/AWB/AF 统计)→ 写回 Stats 输出 Port
  ④ Bayer RAW 结果 → 写回 BPS 输出 Port
  ⑤ 检查 DependencyUnit: 如果设置了 Fence,处理完后发 Fence Signal
  ⑥ CacheOps(clean=TRUE) — 让下游 BPS 读到最新数据

这一阶段是每帧循环的,在 STREAM_ON 状态下不断重复。

阶段 5:Deactivate — 停止硬件

触发时机: Pipeline::StreamOff() 中

IFE Node 的 Deactivate 做了:
  ① CSLStreamOff(IFE) → ioctl → Kernel 停止 IFE 硬件
  ② 等待当前正在处理的帧完成(或超时强制停)

阶段 6:Destroy — 销毁

触发时机: Pipeline::Destroy() 中

IFE Node 的 Destroy 做了:
  ① 释放 Initialize 中分配的资源
  ② 释放 Port
  ③ Node 对象析构

14.3 DependencyUnit — 异步并行的精髓 ⭐⭐⭐

DependencyUnit 是 CamX 异步并行架构的核心。 每个 Node 声明自己的依赖,Pipeline 满足所有依赖后才让 Node 开始处理。

三种依赖类型

源码定义在 camx/src/core/camxnode.h

struct DependencyUnit
{
    union
    {
        struct
        {
            UINT32 hasInputBuffersReadyDependency       :  1;  ///< 输入 Buffer 就绪
            UINT32 hasPropertyDependency                :  1;  ///< Property 满足条件
            UINT32 hasFenceDependency                   :  1;  ///< Fence 信号到达
            UINT32 isPreemptable                        :  1;  ///< 依赖是否可以抢占
            UINT32 hasIOBufferAvailabilityDependency    :  1;  ///< 输入输出 Buffer 是否已分配
            UINT32 isInternalDependency                 :  1;  ///< 是否为 Node 内部依赖
            UINT32 reserved                             : 26;  ///< 保留位
        };
        UINT32 dependencyFlagsMask;    ///< 标志掩码,用于一次性检查
    };
    // ...
};

💡 为什么有 6 个标志位而不是 3 个?

前面我只讲了 3 种依赖,但源码中有 6 个。其中 isPreemptableisInternalDependency 是进阶用法:

  • hasIOBufferAvailabilityDependency:表示 Node 确实需要 Buffer 分配完成才能处理。如果不设这个标志,Node 可能在 Buffer 还没分配好就被调度了——会导致空指针。这主要是为了优化内存——一些 Node 可以"先跑起来,等 Buffer 到了再干活",延迟内存分配。
  • isPreemptable:这个 Node 可以被抢占。高优先级的 Request 可以插队,让这个 Node 暂停当前处理。
  • isInternalDependency:依赖是 Node 内部管理的,不需要 Pipeline 层面的调度。

大多数 Node 只用前 3 个。第 4、5 个是性能优化,第 6 个是内部管理。

① InputBuffersReadyDependency(输入 Buffer 就绪)

含义: 这个 Node 需要上游的输出 Buffer 才能处理
示例: BPS 需要 IFE 输出的 Bayer RAW Buffer
      如果 IFE 还没处理完,BPS 就得等

② PropertyDependency(Property 依赖)

含义: 这个 Node 需要某个 metadata property 满足条件
示例: IFE 需要 AE 的曝光参数
      如果 AE 还没算出曝光值,IFE 就得等

③ FenceDependency(Fence 信号)

含义: 这个 Node 需要等待硬件 fence 信号
示例: IPE 需要 BPS 的硬件完成信号
      BPS 处理完 → 发 fence signal → IPE 开始

典型 Pipeline 的依赖配置

Node A (IFE):
  hasInputBuffersReadyDependency = false  (Sensor 不需要输入)
  hasPropertyDependency = true            (需要 AE 参数)
  hasFenceDependency = false
  → IFE 可以最先启动

Node B (BPS):
  hasInputBuffersReadyDependency = true   (需要 IFE 的输出)
  hasPropertyDependency = true            (需要 IFE 的 metadata)
  hasFenceDependency = true               (需要 IFE 的 fence)
  → IFE 完成后 BPS 才能开始

Node C (IPE):
  hasInputBuffersReadyDependency = true
  hasPropertyDependency = false
  hasFenceDependency = true
  → BPS 完成后 IPE 才能开始

Dependency Graph 解析

解析发生在 Pipeline Finalize 阶段:

① 遍历所有 Node,收集每个 Node 的 DependencyUnit
    ↓
② 分析 Node 间的依赖关系
    ↓
③ 构建有向无环图(DAG)
    ↓
④ 确定处理顺序(拓扑排序)
    ↓
⑤ 运行时:依赖满足 → Node 开始处理
     依赖不满足 → Node 挂起等待

💡 为什么说 DependencyUnit 是"异步并行"的精髓?

考虑一个场景:3 个 Node(A→B→C)是串行依赖,但 Node D(Stats)只依赖 A。

如果不用 DependencyUnit:

A → B → C → D(串行,D 必须等 C 完成)

用了 DependencyUnit:

A → B → C(串行)
A → D(并行!D 可以和 B 同时处理)

这就是 CamX 能实现高效流水线的根本原因——不是"先来后到",而是"依赖满足就开始"。


14.4 源码中找 DependencyUnit

camx/src/core/camxnode.h 中找到它的定义:

struct DependencyUnit {
    BOOL hasInputBuffersReadyDependency : 1;
    BOOL hasPropertyDependency          : 1;
    BOOL hasFenceDependency             : 1;

    BufferDependency    inputBuffers[];
    PropertyDependency  properties[];
    FenceDependency     fences[];
};

struct BufferDependency {
    UINT32      portId;              // 哪个 Port 的 Buffer
    UINT32      nodeId;              // 哪个 Node 的 Port
    BufferStatus requiredStatus;
};

struct PropertyDependency {
    UINT32           propertyTag;     // 依赖哪个 Property
    PropertyCondition condition;      // 满足条件
};

14.5 DRQ(DeferredRequestQueue)——谁在管理"等"

上一节说了每个 Node 声明自己的依赖。但问题来了:谁负责跟踪"这些依赖满足了没有"?这就是 DRQ 的工作。

先理解场景

某一帧到了,Pipeline 要对这帧做处理。

IFE 说: 我需要 AE 曝光值才能开始
BPS 说: 我需要 IFE 完成 + 它的 Buffer 才能开始
IPE 说: 我需要 BPS 完成 + 它的 Buffer 才能开始

现在的问题是:
  AE 曝光值还没算出来 → IFE 不能处理 → 要等
  等到 AE 曝光值出来了 → IFE 可以处理了
  IFE 处理完 → BPS 可以处理了
  BPS 处理完 → IPE 可以处理了

谁来追踪"AE 曝光值到了没有"、"IFE 处理完了没有"?

如果把 DRQ 比作一个快递站

每个 Node 就是一个等待派送的快递包裹。
  包裹上贴着标签: "我需要 X 和 Y 才能派送"

DRQ 的工作:
  ① 收到一个新包裹 → 看标签 → 东西都齐了? → 立刻派送
                                            → 东西不够? → 寄存等待
  ② 收到一个新物品 (property/fence) → 看哪个包裹在等它 → 找到了 → 标记已收到
  ③ 某个包裹的东西收齐了 → 从寄存区取出 → 立刻派送

没有轮询!每当有新物品到达,DRQ 才被唤醒,检查谁在等它。

DRQ 的核心数据结构

源码 camx/src/core/camxdeferredrequestqueue.h

struct Dependency {
    Node*     pNode;              // 等待派送的 Node
    UINT32    propertyCount;      // 总共需要几个 property(标签上的"目标数")
    UINT32    publishedCount;     // 已经收到几个 property(标签上的"已收数")
    PropertyID properties[];      // 具体需要哪些 property(标签上的"物品清单")

    UINT32    fenceCount;         // 总共需要几个 fence
    UINT32    signaledCount;      // 已经收到几个 fence
    CSLFence* phFences[];         // 具体需要哪些 fence
};

核心是两个计数器

两样都收齐了 → 这个 Node 可以派送了。

一个帧的完整"等待→满足→派送"过程

以第 n 帧为例,追踪 IFE/BPS/IPE 三台机器的调度过程:

═══════════════════════════════════════════
第 1 步: 注册依赖
═══════════════════════════════════════════
帧到 → Pipeline 为每个 Node 调用 DRQ::AddDeferredNode()

IFE Node 注册: 需要 1 个 property (AE_TAG), 0 个 fence
  → Dependency { publishedCount=0, propertyCount=1, signaledCount=0, fenceCount=0 }
  → 寄存 (publishedCount < propertyCount)

BPS Node 注册: 需要 0 个 property, 1 个 fence (IFE 的完成信号)
  → Dependency { publishedCount=0, propertyCount=0, signaledCount=0, fenceCount=1 }
  → 寄存 (signaledCount < fenceCount)

IPE Node 注册: 需要 0 个 property, 1 个 fence (BPS 的完成信号)
  → Dependency { publishedCount=0, propertyCount=0, signaledCount=0, fenceCount=1 }
  → 寄存 (signaledCount < fenceCount)

当前寄存区: [IFE, BPS, IPE]  ← 都在等

═══════════════════════════════════════════
第 2 步: AE 曝光值算出来了
═══════════════════════════════════════════
Stats Node 发布了 AE_TAG property
  → DRQ::OnPropertyUpdate(AE_TAG, requestId)
    → 查寄存区, 发现 IFE 在等这个 property
    → IFE 的 publishedCount: 0 → 1
    → publishedCount(1) == propertyCount(1) → property 收齐了!
    → fenceCount(0) == signaledCount(0) → fence 也收齐了(本来就不需要)
    → IFE 从寄存区取出 → DispatchReadyNodes() → IFE.ProcessRequest()

IFE 开始硬件处理(~2ms)→ 处理完成 → 发 Fence Signal

═══════════════════════════════════════════
第 3 步: IFE 完成了
═══════════════════════════════════════════
IFE Fence Signal 到达
  → DRQ::FenceSignaledCallback(ife_fence, requestId)
    → 查寄存区, 发现 BPS 在等这个 fence
    → BPS 的 signaledCount: 0 → 1
    → signaledCount(1) == fenceCount(1) → fence 收齐了!
    → publishedCount(0) == propertyCount(0) → property 也收齐了(本来就不需要)
    → BPS 从寄存区取出 → DispatchReadyNodes() → BPS.ProcessRequest()

BPS 开始硬件处理(~3.5ms)→ 处理完成 → 发 Fence Signal

═══════════════════════════════════════════
第 4 步: BPS 完成了
═══════════════════════════════════════════
BPS Fence Signal 到达
  → DRQ::FenceSignaledCallback(bps_fence, requestId)
    → 查寄存区, 发现 IPE 在等这个 fence
    → IPE 的 signaledCount: 0 → 1
    → IPE 从寄存区取出 → DispatchReadyNodes() → IPE.ProcessRequest()

═══ 全部完成 ═══
三类 Node, 三种依赖, DRQ 分三步依次派送。
每步之间没有 CPU 空转——靠回调触发。

如果 DRQ 发现依赖永远满足不了?

比如: 某个 fence 等了 500ms 还没来(上游 Node 可能挂了)

DRQ 不会无限等。CamX 内部有超时机制:
  → Fence 超时 → Pipeline 标记这个 Request 为失败
  → 所有等待这个 fence 的 Node 被取消
  → 错误回调给 Usecase → 通知 App

💡 DRQ 一句话总结

DRQ 是一台"计数驱动的自动派件机"——每个 Node 注册时告诉 DRQ"我需要 N 个 property 和 M 个 fence",DRQ 收到一个就 +1,收齐了就自动派送。不轮询,靠回调唤醒,零 CPU 空转。


14.6 HW Node vs SW Node

特性 HW Node SW Node
处理位置 ISP 硬件 CPU/DSP
速度 快(固定几 ms) 慢(取决于 CPU)
典型例子 IFE/BPS/IPE/JPEG EIS/SWMF/Stats
源码目录 camx/src/hwl/ camx/src/swl/

HW Node 示例——IFE:

# 源码路径
camx/src/hwl/ife/camxchinodeife.cpp
camx/src/hwl/xxx/  (ISP 寄存器定义)

SW Node 示例——EIS:

# 源码路径
camx/src/swl/eisv3/    (EIS 稳像算法)
camx/src/swl/eisv2/    (EIS 老版本)

14.7 实战:Watermark 水印

如何在 Node 中叠加水印,可以看下下面相关代码:

// camx/src/core/camxnode.cpp
VOID Node::WatermarkImage(
    const FenceHandlerData* pFenceHandlerData,
    ImageDump* pImageDump, UINT32 numBatchedFrames)
{
    for (UINT i = 0; i < numBatchedFrames; i++) {
        ImageDumpInfo dumpInfo = { 0 };

        dumpInfo.requestId = pFenceHandlerData->requestId;
        dumpInfo.pFormat = pOutputBufferInfo[i].pImageBuffer->GetFormat();
        dumpInfo.pBaseAddr = pOutputBufferInfo[i].pImageBuffer->GetCPUAddress();
        dumpInfo.width  = pOutputPort->bufferProperties.imageFormat.width;
        dumpInfo.height = pOutputPort->bufferProperties.imageFormat.height;

        // 水印图案
        dumpInfo.pWatermarkPattern = m_pWatermarkPattern;

        if (NULL != dumpInfo.pWatermarkPattern) {
            ImageDump::Watermark(&dumpInfo);
        }

        // Cache 操作(CPU 写入 → HW 可见)
        pFenceHandlerData->pOutputBufferInfo[i].pImageBuffer->CacheOps(TRUE, TRUE);
    }
}

触发方式adb shell setprop persist.vendor.camera.watermarkImage 1

这个例子展示了:

  1. Node 在 HW 处理完成后,在输出 Buffer 上叠加额外数据
  2. CacheOps(TRUE, TRUE) 确保 CPU 写入的数据被 ISP 看到
  3. setprop 控制 Feature 开关

14.8 本章总结

Node = 最小处理单元

DependencyUnit = 异步并行的核心机制
  hasInputBuffersReadyDependency — 输入 Buffer 就绪
  hasPropertyDependency          — Property 满足条件
  hasFenceDependency             — Fence 信号到达

解析时机: Pipeline Finalize 阶段
核心思想: 依赖满足就开始,不等前面的全做完

HW Node → 硬件加速(IFE/BPS/IPE)
SW Node → CPU 处理(EIS/SWMF/Stats)

动手验证

  1. 在你的源码 camx/src/core/camxnode.h 中搜索 DependencyUnit,对比本章的描述,确认字段名称和结构。

  2. 开启 Session Dump,观察 Node 的依赖状态:

    adb shell dumpsys media.camera | grep -A 5 "Node\|Dependency"
    

    有没有 Node 显示为 waiting?它在等什么依赖?

  3. 在你的源码中找一个 HW Node(camx/src/hwl/ 下)和一个 SW Node(camx/src/swl/ 下),对比它们的实现差异。

常见误解