← 返回课程

Session与Pipeline

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

第 13 章:Session 与 Pipeline — 请求编排引擎 ⭐⭐⭐


本章导读

如果你只能认真学一章,那就是这章。

Session 和 Pipeline 是 CamX 的骨架。理解了这章,你就理解了 CamX 80% 的架构设计。

我们从一个问题开始:当你按下拍照按钮,到照片出来,中间发生了什么?

这个问题可以一直往下拆:

这章我们就走一遍这条路。


13.1 Session 和 Pipeline 是什么?先说直觉

CamX 的架构层次很有意思,越往下越"细":

Usecase         →  这次要做什么?(预览?拍照?HDR?)
Session         →  谁来管这件事?
Pipeline        →  需要哪些处理流水线?
Node            →  每个流水线上有哪些工序?
Port / Link     →  工序之间怎么传东西?

可以这样类比:

  • Usecase 是项目经理:决定做预览还是拍照
  • Session 是项目负责人:管着几条生产线(Pipeline)
  • Pipeline 是生产线:一条生产线上多台机器(Node)协作
  • Node 是机器:每台机器干一件事(IFE 接收 RAW,BPS 处理 Bayer……)
  • Port / Link 是传送带:机器之间的连接

它们之间的数量关系

1 个 Usecase 管 1~N 个 Session
1 个 Session 管 1~N 个 Pipeline
1 个 Pipeline 管 1~N 个 Node
1 个 Node 有 1~N 个 Port(输入/输出)

💡 一个关键的直觉:这么多层不是为了复杂而复杂,而是为了灵活

假如你想在"预览"和"拍照"之外加一个"HDR 拍照":

  • 如果你把预览和拍照写死在一个模块里 → 你得改整个模块
  • 如果你分了 Usecase / Session / Pipeline → 复用预览的 Pipeline,加一个 HDR Merge 的 Pipeline 就行

这就是"正交分解"的设计思想。

13.1.1 🧩 一个小陷阱:CHI Session ≠ CamX Session

翻开源码你会看到两个 Session:

chi-cdk/core/chiframework/chxsession.h      → CHI Session(OEM 定制层)
camx/src/core/camxsession.h                 → CamX Session(核心层)

它们的关系是封装

// chi-cdk/core/chiframework/chxsession.h
// CHI Session 是对 CamX Session 的一层包装
class Session {
private:
    CamX::Session* m_pCamxSession;   // ← 内部持有一个 CamX Session
    ChiCallBacks    m_chiCallbacks;   // ← CHI 层的回调函数
};

为什么要有两层?

CamX Session: 负责"机制"——管理 Pipeline 状态机、收发 Request、控制硬件
CHI Session:  负责"策略"——决定 Request 怎么分发、用什么 Pipeline、回调怎么处理

CHI Session 把 CamX Session 的能力"包装"成 OEM 友好的接口。
你在 chi-cdk/ 中写的大部分代码,面对的都是 CHI Session,不是 CamX Session。

💡 实践中的感受

如果你在做 OEM 定制(改 Usecase、加 Feature),你会打交道的主要是 chxsession.h 里的 CHI Session。但当你深入调试 Pipeline 状态机问题的时候,最终还是要打开 camxsession.h 看 CamX Session。

这两层的关系有点像 Linux 的系统调用和内核函数——你平时调的是系统调用(CHI Session),但出了问题得看内核代码(CamX Session)。

13.1.2 RT Pipeline vs Offline Pipeline

这是 CamX Pipeline 设计中最重要的一个二分法

维度 RT Pipeline(实时管线) Offline Pipeline(离线管线)
用途 预览、视频 JPEG 编码、多帧融合
截止时间 严格(每 16ms@60fps 必须完成) 宽松(多几 ms 也可以)
Buffer 数量 IFE=4, 其他=2 通常 2
优先级
状态机 完整 8 状态 可按需简化
是否必须 否(只在需要时激活)
典型例子 PreviewPipeline JpegPipeline, MFNRPipeline

一个典型场景:预览 + 拍照

UsecaseDefault 内部管理两个 Pipeline:

Pipeline 0 (RT):  Sensor → IFE → IPE → Display
  └─ 状态: STREAM_ON(持续工作)
  └─ 优先级: 高(保证预览流畅)

Pipeline 1 (Offline): IPE → JPEG Node → JPEG 编码
  └─ 状态: 平时 STREAM_OFF,拍照时才 STREAM_ON
  └─ 优先级: 低(可以慢慢编)

💡 为什么要分开?

在早期(mm-camera 时代),JPEG 编码和预览在同一个流水线上。结果就是:拍照时预览会卡一帧——因为 ISP 要分带宽给 JPEG。

CamX 把 JPEG 编码放到 Offline Pipeline,等于说:

  • 预览走专用的 RT Pipeline,带宽保住了
  • JPEG 编码走 Offline Pipeline,慢慢编不着急
  • 两者通过 PipelineInfo.pDependentOn 建立依赖(JPEG 等 IPE 的输出)

这就是"正交分解"在实践中的体现。

13.1.3 一个 Usecase 怎么组织多个 Session 和 Pipeline

UsecaseDefault
  │
  ├── Session 0(主摄会话)
  │     ├── Pipeline 0 (RT):  IFE → IPE → Display
  │     └── Pipeline 1 (Offline): IPE → JPEG
  │
  └── [Session 1](可选,比如多摄场景)
        └── Pipeline 0 (RT): IFE → IPE → Display(广角)

当你调用 createCaptureSession() 时,UsecaseSelector 决定了用几个 Session、几个 Pipeline、哪些是 RT 哪些是 Offline。


13.2 Session 的核心数据结构

直接打开 camxsession.h,这是 Session 管理 Pipeline 的核心结构。下面展示关键设计的简化视图(实际源码中结构名称和字段略有不同,但逻辑关系一致):

Session
  ├── PerPipelineInfo[]   — 每个 Pipeline 的描述(节点数、节点链表)
  ├── PipelineDescriptor  — Pipeline 的完整描述
  │     ├── PerPipelineInfo     — 节点信息
  │     ├── PipelineOutputData[] — 输出端口描述
  │     ├── PipelineInputData[]  — 输入端口描述
  │     └── PipelineDescriptorFlags — 标志位(isRealTime/isSecureMode等)
  └── SessionPerPipelineData[] — 每个 Pipeline 的运行时数据

几个值得注意的常量

static const UINT32 MaxRealTimePipelines        = 4;   // 最多 4 个实时 Pipeline
static const UINT32 MaxActiveRealtimePipelines  = 2;   // 同时只能激活 2 个
static const UINT32 InvalidPipelineIndex        = 0xFFFFFFFF;

🤔 为什么 MaxRealTimePipelines 是 4,但 MaxActiveRealtimePipelines 只有 2?

这是功耗和性能的权衡。可以有 4 个 Pipeline 实例(比如主摄 + 广角 + 前摄 + 景深,都配置好),但同一时间只能有 2 个在跑(比如主摄和广角同时工作)。

为什么是 2 个?因为高通 ISP 硬件带宽限制——同一时刻的实时流数受限于 ISP 处理能力。如果你同时跑 3 路实时流,ISP 带宽不够,帧率就保不住了。

做 Camera HAL 的人,脑子里始终要有一根弦:ISP 带宽是有限资源。


13.3 Pipeline 是怎么创建的?PipelineCreateInputData

Pipeline 创建时,Usecase 传入一个配置结构体。源码 camxpipeline.h

struct PipelineCreateInputData
{
    ChiContext*               pChiContext;        // 全局上下文
    const PipelineDescriptor* pPipelineDescriptor; // Pipeline 完整描述
    UINT                      pipelineIndex;      // Pipeline 编号
    BOOL                      isRealTime;         // RT vs Offline
    BOOL                      isSecureMode;       // 安全模式
};

PipelineDescriptor 才是定义 Pipeline 拓扑的关键——它包含节点信息(PerPipelineInfo)、输出端口(PipelineOutputData)、输入端口(PipelineInputData)、标志位(PipelineDescriptorFlags)。

概念上,一个 Pipeline 的创建需要这些核心信息:Pipeline 编号、实时/离线标识、以及描述拓扑的 Descriptor。拓扑中可以理解为"哪些 Node 按什么顺序连接"。


13.4 Pipeline 的 8 状态状态机

这是理解 CamX 的第一道硬门槛。Pipeline 有 8 个状态,从出生到死亡走一遍。

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

enum class PipelineStatus
{
    UNINITIALIZED,              ///< 初始状态
    INITIALIZED,                ///< Pipeline 初始化完成
    FINALIZED,                  ///< Pipeline 最终化完成
    RESOURCES_RELEASED,         ///< 流资源已释放
    STREAM_OFF,                 ///< 流已关闭
    RESOURCES_ACQUIRED,         ///< 流资源已获取
    PARTIAL_STREAM_ON,          ///< 部分流已开启
    STREAM_ON,                  ///< 流已开启(正常工作)
    MAX_PIPELINE_CREATE_STATUS  ///< 边界值
};

对应的状态名字符串(同文件 camxpipeline.h):

static const CHAR* PipelineStatusStrings[] = {
    "PipelineStatus::UNINITIALIZED",
    "PipelineStatus::INITIALIZED",
    "PipelineStatus::FINALIZED",
    "PipelineStatus::RESOURCES_RELEASED",
    "PipelineStatus::STREAM_OFF",
    "PipelineStatus::RESOURCES_ACQUIRED",
    "PipelineStatus::PARTIAL_STREAM_ON",
    "PipelineStatus::STREAM_ON",
    "PipelineStatus::MAX_PIPELINE_CREATE_STATUS",
};

💡 状态字符串的存在意义

注意看这个 PipelineStatusStrings 数组。它用来把枚举值转成字符串——日志输出时能看到 "PipelineStatus::STREAM_ON" 而不是数字 7

源码中状态迁移时会在 SetPipelineStatus() 函数中打印日志:

CAMX_LOG_CONFIG(CamxLogGroupCore, "%s status is now %s",
    GetPipelineIdentifierString(),
    PipelineStatusStrings[static_cast<UINT>(status)]);

这就是你在 logcat 中看到 Pipeline status is now PipelineStatus::STREAM_ON 的源头。

8 个状态,按功能分成三组:

"准备阶段"
UNINITIALIZED → INITIALIZED → FINALIZED → RESOURCES_RELEASED
                                          → 资源准备完成,等待启动

"启动阶段"
STREAM_OFF → RESOURCES_ACQUIRED → PARTIAL_STREAM_ON → STREAM_ON
                                                       → 正常工作

"关闭阶段"
STREAM_ON → STREAM_OFF → UNINITIALIZED(释放所有资源)

每个状态在做什么?

① UNINITIALIZED(未初始化)

Pipeline 刚 new 出来时的状态。内存分配了,但里面是空的:

[UNINITIALIZED]
pipeline 对象刚构造 → 还没有任何 Node → 没有任何 Port → 啥也干不了

触发函数:Pipeline 构造函数


② INITIALIZED(已初始化)

// Pipeline::Create() 中做的事情:
// 1. 创建所有 Node(根据 PipelineCreateInputData 中的 nodeConfig)
// 2. 为每个 Node 创建输入/输出 Port
// 3. 设置每个 Port 的 BufferProperties(格式、大小、队列深度)

// 创建完成后:
// - Node 列表形成了
// - Port 列表形成了
// - 但 Node 之间的依赖关系还没建立

此时的状态:零件摆好了,但还没组装。


③ FINALIZED(已完成)

这是最关键的一个状态迁移,因为这里是 Dependency Graph 建立的地方

// Pipeline::Finalize() 的核心工作:
void Pipeline::Finalize() {
    // 1. 遍历所有 Node,收集每个 Node 的 DependencyUnit
    //    比如 IFE 说:我不需要输入 Buffer,我需要 AE Property
    //    BPS 说:我需要 IFE 的输出 Buffer,我需要 IFE 的 Fence

    // 2. 构建有向无环图 (DAG)
    //    确定 Node 的执行顺序:IFE → IPE

    // 3. 通知每个 Node 进行 FinalizeInitialization
    //    Node 在这里创建 BufferManager(申请 buffer)
    for (auto& node : m_nodes) {
        node->FinalizeInitialization();
    }
}

💡 Dependency Graph 为什么在 FINALIZED 阶段建而不是 INITIALIZED?

INITIALIZED 阶段 Node 刚创建,Port/Link 还没完全建立。到了 FINALIZED 阶段,Port 之间的 Link 已经连接好了,DependencyUnit 才能正确解析出 Node 之间的上下游关系。

如果你在 INITIALIZED 就建 Dependency Graph——你只知道"每个 Node 需要什么",但不知道"这个 Node 的上游是谁"。


④ RESOURCES_RELEASED(资源已释放)

申请了 Buffer,发现这是"准备阶段"的中间过渡状态,释放掉临时资源:

void Pipeline::ReleaseResources() {
    // 1. 释放 FINALIZED 阶段申请的临时资源
    // 2. 进入等待 Stream On 的状态
}

为什么要有这个状态?

因为 Buffer 的分配策略是:先准备好,等真正要用的时候再分配。FINALIZED 阶段分配的是一些配置数据(metadata 池等),到 ACQUIRED 阶段才分配真正的图像 Buffer。中间的 RESOURCES_RELEASED 就是"配置完了,等开干"的状态。


⑤ STREAM_OFF(Stream 已关闭)

Pipeline 在这个状态意味着"随时可以启动"。

STREAM_OFF ↔ ACQUIRED → PARTIAL → STREAM_ON → STREAM_OFF

正常流程:OFF → ACQUIRED → ... → ON
异常恢复:ON → OFF → ACQUIRED → ... → ON(重启)
完全销毁:ON → OFF → UNINITIALIZED(释放一切)

⑥ RESOURCES_ACQUIRED(资源已获取)

void Pipeline::AcquireResources() {
    // 1. 分配所有图像 Buffer
    //    IFE: 4 个 buffer,BPS/IPE: 2 个 buffer
    // 2. 准备硬件资源(ISP 寄存器配置)
    // 3. 准备 CSL 资源(与 Kernel 的通信通道)
}

🤔 为什么 IFE 要 4 个 buffer,其他 Node 只要 2 个?

IFE 是 Pipeline 入口,下面挂着多个下游——Preview 时给 IPE + Stats,Snapshot 时还要加 BPS。多下游并行消费 → 需要更多 buffer 才不会堵。

IPE 的下游单一(只给 Display 或 JPEG),2 个 buffer 交替足够。

设计原则:Pipeline 入口的 Node,buffer 数量要留足。


⑦ PARTIAL_STREAM_ON(部分 Stream 已开启)

void Pipeline::PartialStreamOn() {
    // 启动"能够尽早启动"的 Node
    // 比如 IFE:一旦 Sensor 有帧进来,IFE 就要开始处理
    // 而 JPEG Node 可以等(它只在拍照时激活)
}

为什么要有"部分启动"这个概念?

有些 Node 是"实时"的(IFE/IPE),Sensor 每来一帧都要处理。有些 Node 是"按需"的(BPS/JPEG),只有拍照时才激活。

如果等所有 Node 都准备好了再 Stream On,那第一帧的延迟就大了。所以先启动实时 Node,后面的 Node 准备好了再加入。

这是低延迟设计的体现。


⑧ STREAM_ON(Stream 已开启)

唯一的"正常工作"状态

在这个状态下:

STREAM_ON 状态下的每帧循环:

① 接收 Request
② Pipeline 解析 Request,分给各 Node
③ Node 检查依赖 → 依赖满足 → 处理
④ Node 处理完成 → 写回 Result
⑤ Pipeline 收集所有 Node 的 Result
⑥ Session 汇聚 → 回调给 App

第 ①~⑥ 步每帧重复。

13.5 一个完整的生命周期示例

现在,我们把从打开相机到关闭相机的完整 Pipeline 状态迁移走一遍:

① App 打开相机 → open()

                            Pipeline 创建
                              │
                              ▼
                        UNINITIALIZED
                              │ Pipeline::Create()
                              │   创建 Node 和 Port
                              ▼
                        INITIALIZED
                              │ Pipeline::Finalize()
                              │   建立 Dependency Graph
                              ▼
                        FINALIZED
                              │ ReleaseResources()
                              ▼
                        RESOURCES_RELEASED
                              │
                              ▼
                        STREAM_OFF            ← 此时配置完成,只等启动

② App 创建 Session → configure_streams()

                        STREAM_OFF
                              │ AcquireResources()
                              │   分配 Buffer
                              ▼
                        RESOURCES_ACQUIRED
                              │ PartialStreamOn()
                              │   启动实时 Node
                              ▼
                        PARTIAL_STREAM_ON
                              │ StreamOn()
                              │   启动所有 Node
                              ▼
                        STREAM_ON              ← 开始预览!

③ 每帧处理

                        STREAM_ON
                              │
                    ProcessRequest 循环
                              │
                        STREAM_ON              ← 保持不变

④ App 关闭相机 → close()

                        STREAM_ON
                              │ StreamOff()
                              ▼
                        STREAM_OFF
                              │ Destroy()
                              ▼
                        UNINITIALIZED          ← 回到起点

💡 实际工程中的一个小插曲

你在工作中可能会遇到这种情况:调用 StreamOff() 后,Pipeline 没有正常回到 STREAM_OFF,而是卡住了。常见原因是某个 Node 的 HW 还在忙,没有响应 StreamOff 命令。

这时怎么处理?CamX 的实现中有一个 watchdog 机制——如果 StreamOff 后等了一段时间(比如 500ms)状态还没变,就强制把 Pipeline 置为 STREAM_OFF,然后报错。

用 logcat 抓的话,你会看到:

[CamX] [CORE] camxpipeline.cpp: StreamOff timeout! pipelineId=0
[CamX] [CORE] camxpipeline.cpp: Force reset pipeline state to STREAM_OFF

看到这个日志,说明有 Node 的 HW 卡住了。


13.6 ProcessCaptureRequest — Request 在 Session 中的旅程

前面说了 Pipeline 的状态,现在是时候看看一个 Request 进来后怎么被处理

① camxhaldevice.cpp → camera3_device_t.process_capture_request()
    │
② HALDevice → CHI ExtensionModule → Usecase
    │   m_ChiAppCallbacks.chi_override_process_request()
    │
③ Usecase::SubmitChiRequest()
    │
④ Session::ProcessCaptureRequest()
    │
⑤ 根据 PipelineIndex 分发到对应的 Pipeline
    │
⑥ Pipeline::ProcessRequest()

在第 ⑥ 步开始之前,Pipeline 是 STREAM_ON 状态
在第 ⑥ 步之后,Pipeline 内部做这些事情:
// 伪代码:Session::ProcessCaptureRequest
CamxResult Session::ProcessCaptureRequest(CaptureRequest* pRequest)
{
    // 1. 拿到这个 Request 应该发给哪个 Pipeline
    UINT32 pipelineIndex = pRequest->GetPipelineIndex();

    if (pipelineIndex == USE_DEFAULT_PIPELINE) {
        // 2. 发给所有激活的 Pipeline
        for (UINT32 i = 0; i < m_numActivePipelines; i++) {
            Pipeline* pPipeline = m_pipelineInfo[i].pPipeline;

            // 2.1 检查 Pipeline 是不是 STREAM_ON
            if (pPipeline->GetStatus() != PipelineStatus::STREAM_ON) {
                // 如果 Pipeline 还没准备好,这个 Request 要等待
                CAMX_LOG_WARN("Pipeline %d not ready, status=%d",
                              i, pPipeline->GetStatus());
                return CamxResultEFailed;
            }

            // 2.2 Pipeline 处理这个 Request
            pPipeline->ProcessRequest(pRequest);
        }
    } else {
        // 只发给指定的 Pipeline(如离线 JPEG Pipeline)
        m_pipelineInfo[pipelineIndex].pPipeline->ProcessRequest(pRequest);
    }

    return CamxResultSuccess;
}

💡 这里有个容易被忽略的细节

ProcessCaptureRequest 的返回值是 CamxResult。但它返回的是"请求是否被正确接收",而不是"请求是否处理完成"。真正的处理结果是异步回调回来的。

所以 ProcessCaptureRequest 返回 Success 只意味着"我收到了,开始处理了"。如果你在这里返回 Failed,App 层会立即收到一个错误回调。


13.7 Result 怎么回来的?反向路径

前面讲了 Request 怎么进去,现在讲 Result 怎么出来。这是反向路径

Node ProcessRequestResult()
  → Pipeline::ProcessPipelineResult()
    → Session::ProcessPipelineResult()
      → m_chiCallbacks.ChiProcessCaptureResult()
        → Usecase::ProcessCaptureResult()
          → HAL3 process_capture_result()
            → App onCaptureCompleted()
// 伪代码:Pipeline 完成一批结果 → 通知 Session
void Pipeline::ProcessPipelineResult(CaptureResult* pResult)
{
    // 1. 收集本 Pipeline 中所有 Node 的 result metadata
    MergeNodeResults(pResult);

    // 2. 通知所属的 Session
    Session* pSession = GetSession();
    pSession->ProcessPipelineResult(this, pResult);
}

// 伪代码:Session 收集所有 Pipeline 的结果 → 汇聚 → 回调
void Session::ProcessPipelineResult(Pipeline* pPipeline, CaptureResult* pResult)
{
    // 1. 找到这个 Pipeline 在 Session 中的索引
    UINT32 idx = FindPipelineIndex(pPipeline);

    // 2. 把结果暂存
    m_pendingResults[idx] = pResult;

    // 3. 检查是不是所有激活的 Pipeline 都完成了
    if (AllActivePipelinesCompleted()) {
        // 4. 汇聚所有 Pipeline 的 metadata(合并)
        CaptureResult* pMerged = MergeAllResults(m_pendingResults);

        // 5. 通过 CHI Callback 通知 Usecase
        m_chiCallbacks.ChiProcessCaptureResult(pMerged);

        // 6. 清理
        ClearPendingResults();
    }
}

Partial Result —— 结果不是一次性回来的

一个很有意思的细节:Result 不是一次性回来的。Node 可以分段上报结果

时间轴 →

Node IFE 处理完成
  └─→ ProcessPartialMetadataDone(IFE, partialMeta)
       └─→ Pipeline::NotifyNodePartialMetadataDone()
            └─→ Session::NotifyResult(partial)
                 └─→ ChiProcessPartialCaptureResult()
                      └─→ Usecase 可以提前处理部分数据

Node BPS 处理完成
  └─→ ProcessPartialMetadataDone(BPS, partialMeta)
       └─→ Pipeline::NotifyNodePartialMetadataDone()
            └─→ Session::NotifyResult(partial)
                 └─→ ChiProcessPartialCaptureResult()

Node IPE 处理完成
  └─→ ProcessMetadataDone(IPE, fullMeta)
  └─→ SinkPortFenceSignaled(IPE, buffer)   ← 图像数据也到了
       └─→ Pipeline::ProcessPipelineResult()
            └─→ Session::ProcessPipelineResult()
                 └─→ ChiProcessCaptureResult(full)   ← 完整结果

💡 为什么需要 Partial Result?

这样做是为了降低延迟。有些 metadata(比如 AE 的统计数据)在 IFE 处理完就出来了,不需要等 BPS 和 IPE。

Usecase 收到 partial result 后可以提前做下一帧的 AE 计算——等完整的 result 回来时,下一帧的参数已经算好了。

这就叫"Pipeline"——数据在流水线上流过去,每一站都能先把结果送回来一部分,不等全部做完。

关键日志

// Node 的 partial metadata 回来了
[CamX] [CORE] camxnode.cpp: ProcessPartialMetadataDone()
       node=IFE req=42

// Pipeline 通知 Session 结果就绪
[CamX] [CORE] camxsession.cpp: ProcessPipelineResult()
       pipelineId=0 req=42

// Session 通过 CHI 回调 Usecase
[CHI] chxusecase.cpp: ProcessCaptureResult()
       requestId=42

// 最终回调到 App
[CamX] [HAL ] camxhal3entry.cpp: process_capture_result()
       frame_number=42

13.8 PipelineFlags — 一个 Pipeline 藏着的信息

每个 Pipeline 有一些标志位,记录了它的"基因"。

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

struct PipelineFlags
{
    union
    {
        struct
        {
            BIT hasStatsNode            : 1;  ///< 是否有 3A Stats Node
            BIT isSecureMode            : 1;  ///< 是否是安全模式(DRM 保护)
            BIT enableQTimer            : 1;  ///< 使用 Qtimer(硬件时间同步)
            BIT isInitialConfigPending  : 1;  ///< 初始配置是否待处理
            BIT isHFRMode               : 1;  ///< 是否高帧率模式(>60fps)
            BIT hasIFENode              : 1;  ///< 是否包含 IFE
            BIT hasJPEGNode             : 1;  ///< 是否包含 JPEG
            BIT isCameraRunningOnBPS    : 1;  ///< 是否在 BPS 而非 IFE 上运行
            BIT reserved                : 24; ///< 保留位
        };
        UINT32 allFlagsValue;     ///< 整体标志值(可以一次性比较)
    };
};

💡 union { struct { BIT }; UINT32; } 的设计

这种设计让你既可以按位访问每个标志(pipelineFlags.hasIFENode),也可以一次性读取所有标志的值(pipelineFlags.allFlagsValue)。

后者在调试时特别有用——看到 allFlagsValue = 0x23,立刻知道哪些位被置了。

另外注意 BIT 类型和 BOOL 不同——BIT 是 1 位字段,BOOL 是 4 字节。用 BIT 可以把 8 个标志压缩到 1 个 UINT32 里。

除了这些静态标志,camxpipeline.h 中还有几个关键方法:

// 判断 Pipeline 是否 stream on
BOOL IsStreamedOn() {
    return PipelineStatus::STREAM_ON == GetPipelineStatus();
}

// 状态迁移时打印日志
VOID SetPipelineStatus(PipelineStatus status) {
    CAMX_LOG_CONFIG(CamxLogGroupCore, "%s status is now %s",
        m_pPipelineName,
        PipelineStatusStrings[static_cast<UINT>(status)]);
    m_currentPipelineStatus = status;
}

这些标志位的作用:

用途 1: 日志和调试
  → 快速知道这个 Pipeline 由哪些 Node 组成

用途 2: 优化调度
  → 离线 Pipeline 不需要实时优先级
  → hasStatsNode 的 Pipeline 需要分配 Stats Buffer

用途 3: 兼容性检查
  → isSecureMode 的 Pipeline 需要特殊的内存分配策略

13.9 实际调试:从 Session Dump 看 Pipeline 状态

当你遇到问题,第一步通常是看 Session Dump

获取方式

# 方法 1: dumpsys(最方便)
adb shell dumpsys media.camera > session_dump.txt

# 方法 2: 配置自动 dump
echo "enableSessionDump=1" >> camxoverridesettings.txt

从 Session Dump 可以读到什么

=== Pipeline Info ===
  Pipeline ID: 0
  Status: STREAM_ON                    ← 正常工作状态
  Node Count: 3
  Flags: IFE=1 IPE=1 BPS=0 JPEG=0  (preview, 不过 BPS)

=== Node Info ===
  Node ID: IFE (0x10000)
    State: PROCESSING                  ← 正在处理帧
    ProcessingTime: 2.1ms             ← 每帧耗时(正常值)
    InputPorts: 1
    OutputPorts: 2
    Dependency: satisfied              ← 依赖已满足

  Node ID: BPS (0x20000)
    State: WAITING                     ← 在等什么?
    ProcessingTime: 0ms
    Dependency: waiting(InputBuffer, 15ms)  ← 等了 15ms 还没等到!
    ↑ 这里是个明显的瓶颈

=== Buffer Info ===
  Buffer Pool:
    Total: 4
    InUse: 4
    Free: 0                             ← Buffer 全部被占用!

问题分析

上面的 dump 告诉我们什么?

BPS 在等 IFE 的输入 Buffer(等了 15ms)
而且 Buffer Pool 全部被占用了

这个症状通常说明:
→ IFE 的处理速度跟不上 Sensor 的输出速度
→ IFE 的 4 个 buffer 全被占满
→ BPS 拿不到新的 buffer → 卡住

解决方案:
  1. 降低 Sensor 帧率(30fps → 25fps)
  2. 降低 IFE 的处理分辨率
  3. 检查 IFE 是不是有什么额外的处理拖慢了

13.10 源码在哪?按图索骥

如果你要自己翻代码,下面是这章涉及的核心文件和关键行号入口:

组件 文件 关键内容
Session 定义 camx/src/core/camxsession.h PipelineInfo, LinkInfo, PortInfo, NodeInfo 结构体
Session 实现 camx/src/core/camxsession.cpp ProcessCaptureRequest, ProcessPipelineResult
Pipeline 定义 camx/src/core/camxpipeline.h PipelineStatus 枚举, PipelineFlags, PipelineCreateInputData
Pipeline 实现 camx/src/core/camxpipeline.cpp 状态迁移, ProcessRequest, CSLMessageHandler
CHI Session chi-cdk/core/chiframework/chxsession.h CHI 层 Session 封装, ChiCallBacks
Node 定义 camx/src/core/camxnode.h DependencyUnit, Node 生命周期

阅读顺序建议

① camxpipeline.h → 先看 PipelineStatus 枚举(8 个状态)
② camxsession.h → 再看 Session 的数据结构(理解组织关系)
③ camxpipeline.cpp → Pipeline::Create() 和 Finalize()(状态迁移逻辑)
④ camxsession.cpp → ProcessCaptureRequest() 和 ProcessPipelineResult()(请求和结果)

13.11 容易混淆的几个概念

在结束这章之前,澄清几个经常被问混淆的问题:

Q: Session 和 Usecase 有什么区别?

Usecase 是"决策者"——它决定用哪个 Session 和哪些 Pipeline
Session 是"执行者"——它管理 Pipeline,收发 Request

一个 Usecase 可以包含多个 Session(比如主摄 Session + 景深 Session)
一个 Session 属于一个 Usecase,但 Usecase 可以换 Session

Q: Pipeline 和 Node 是什么关系?

Pipeline = 流水线(一整条线)
Node = 流水线上的机器(一台机器)

一台机器不是流水线。多台机器连起来才是。
一个 Node 不能在多个 Pipeline 中复用。

Q: 一个 Session 管理多个 Pipeline,那多个 Pipeline 怎么同步?

通常是"异步并行":
  每个 Pipeline 独立处理自己的 Request
  Session 等所有 Pipeline 都完成了,再汇聚 Result

但也有"串行依赖"的情况:
  离线 JPEG Pipeline 必须在 IPE Pipeline 完成后才能开始
  这时候 PipelineInfo.pDependentOn 就起了作用

13.12 实战案例——"预览卡住"的问题排查

下面用一个真实的调试场景来展示这章的知识怎么用。

症状

打开相机预览,5 秒后画面卡住不动。logcat 中有这样的日志:

[CamX] [CORE] camxpipeline.cpp: Pipeline::ProcessRequest() pipelineId=0 req=42
...
[CamX] [CORE] camxnode.cpp: CheckAndSetDependencies() node=BPS satisfied=false
[CamX] [CORE] camxnode.cpp: CheckAndSetDependencies() node=BPS satisfied=false
[CamX] [CORE] camxnode.cpp: CheckAndSetDependencies() node=BPS satisfied=false

BPS 一直在等。等了 3 秒还没好。

第一步:Session Dump 看一下

adb shell dumpsys media.camera > dump.txt

从 dump 中读到:

=== Pipeline Info ===
  Pipeline ID: 0
  Status: STREAM_ON                  ← Pipeline 状态正常
  Node Count: 3

=== Node Info ===
  Node IFE: State=PROCESSING, ProcessingTime=2.1ms  ← IFE 在干活
  Node BPS: State=WAITING, Dependency=waiting(InputBuffer, 3.2s)
                                        ↑ 等了 3.2 秒!
  Node IPE: State=IDLE, Dependency=waiting(InputBuffer)

=== Buffer Info ===
  IFE Buffer Pool: Total=4, InUse=4, Free=0    ← IFE 的 4 个 buffer 全被占满了
  BPS Buffer Pool: Total=2, InUse=0, Free=2    ← BPS 的 buffer 空闲

第二步:分析

IFE 在处理帧(正常),
但 IFE 的 4 个 buffer 全被占满了(InUse=4, Free=0),
BPS 在等 IFE 的输出 Buffer(等了 3.2 秒)。

为什么 IFE 的 buffer 满了?
  → IFE 处理速度正常(2.1ms/帧)
  → 但 BPS 没有拿 IFE 的 buffer
  → 可能是 BPS 在处理之前的一帧时卡住了

第三步:进一步排查 BPS

看 BPS 的 ProcessingTime——没有更新,说明 BPS 可能根本没开始处理。

为什么 BPS 不处理?因为它在等 InputBuffer。但 IFE 说它有帧输出了。

真正的根因:BPS 的前一帧处理还没完成(可能是 BPS 硬件挂了或超时),所以 BPS 还在忙着——它没有释放之前占用的 IFE 的 buffer。新的 IFE buffer 到了,但旧的还没释放,buffer 池满了,IFE 无法继续输出。

时间线:
  帧 N: IFE 处理完 → buffer[N] 给 BPS → BPS 开始处理
  帧 N+1: IFE 处理完 → buffer[N+1] 给 BPS(BPS 还没处理完帧 N)
  帧 N+2: IFE 处理完 → buffer[N+2] 给 BPS(BPS 还在忙)
  帧 N+3: IFE 处理完 → 没有空闲 buffer 了! → IFE 等待

第四步:解决

问题出在 BPS 的处理超时。可能原因:
  1. BPS ISP 硬件时钟异常 → 检查 ISP 时钟配置
  2. BPS 输入的数据量过大 → 降低分辨率试试
  3. BPS Node 内部配置错误 → 检查 chromatix 参数

临时恢复手段:
  在 camxoverridesettings.txt 中设置 enableSnapshot=0
  (关闭拍照功能,绕过会卡住的 BPS 处理分支)

这个案例说明了什么?

  1. Pipeline 状态机告诉你"整体在正常工作"(STREAM_ON),但 Node 级别的 WAITING 暴露了问题
  2. IFE 的 4 个 buffer 全部被占满,说明流水线后端(BPS)堵住了
  3. Buffer 的 Free 数量是最直观的诊断指标——Free=0 通常意味着"哪里卡住了"
  4. 日志中的 satisfied=false 持续超过几秒,才说明真的有问题(短时间的等待是正常的)

13.13 本章总结

Session & Pipeline 的核心要点
────────────────────────────────────────────────

① Session 是 Pipeline 的容器
   负责 Receive Request → Distribute to Pipelines → Collect Results → Return

② 两层 Session 设计
   CHI Session(策略层)封装 CamX Session(机制层)
   OEM 开发面对 CHI Session,调试深入到 CamX Session

③ RT Pipeline vs Offline Pipeline
   RT: 预览/视频,严格截止时间,高优先级
   Offline: JPEG 编码,宽松,低优先级
   分开是为了不阻塞预览

④ Pipeline 有 8 个状态
   正常路径: UNINIT → INIT → FINAL → REL → OFF → ACQ → PARTIAL → ON
   关键迁移: FINAL 阶段建立 Dependency Graph
   正常状态: STREAM_ON(唯一能处理请求的状态)

⑤ 核心常量
   MaxRealTimePipelines = 4, MaxActiveRealtimePipelines = 2

⑥ Result 走反向路径
   Node → Pipeline → Session → CHI → Usecase → HAL3 → App
   支持 Partial Result(提前回调部分 metadata)

⑦ Session Dump 是诊断问题的主要手段
   重点关注: Node 状态、ProcessingTime、Buffer Free 数量

⑧ 关键源码文件
   camxsession.h / .cpp, camxpipeline.h / .cpp, chxsession.h

⑤ Session Dump 是诊断问题的主要手段
重点关注: Node 状态、ProcessingTime、Buffer Free 数量


**学完这章之后,你应该能回答这几个问题:**

1. 从 UNINITIALIZED 到 STREAM_ON,Pipeline 经历了几个状态?
2. Dependency Graph 在哪个阶段建立的?为什么不在 INITIALIZED 阶段?
3. 为什么 IFE 要 4 个 buffer,其他 Node 要 2 个?
4. 如果在 Session Dump 中看到 BPS 在 waiting(InputBuffer),说明什么?

---

---

## 动手验证

**目标**:通过 Session Dump 观察真实设备的 Pipeline 状态。

1. 打开相机 App,保持预览状态。在另一个终端执行:
   ```bash
   adb shell dumpsys media.camera > session_dump.txt
  1. session_dump.txt 中找出:

    • 当前有几个 Pipeline?它们的 Status 分别是什么?
    • 每个 Pipeline 包含哪些 Node?哪里有 PipelineFlags 相关输出?
    • 各 Node 的 Buffer 使用情况(Total / InUse / Free)
    • 有没有 Node 的 Dependency 显示为 waiting
  2. 拍照一次,再执行一次 dumpsys。对比拍照前后的变化:

    • 是否多了一个 Offline Pipeline?
    • Offline Pipeline 的状态是什么?

常见误解


下章预告:第 14 章 Node 机制——DependencyUnit 是 CamX 异步并行架构的精髓。你会发现,Node 之间不直接调用,而是通过依赖声明来协作。这种设计,值得每个做异步系统的人学习。