如果你只能认真学一章,那就是这章。
Session 和 Pipeline 是 CamX 的骨架。理解了这章,你就理解了 CamX 80% 的架构设计。
我们从一个问题开始:当你按下拍照按钮,到照片出来,中间发生了什么?
这个问题可以一直往下拆:
这章我们就走一遍这条路。
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 就行
这就是"正交分解"的设计思想。
翻开源码你会看到两个 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)。
这是 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 的输出)
这就是"正交分解"在实践中的体现。
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。
直接打开 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 带宽是有限资源。
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 按什么顺序连接"。

源码定义在 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(释放所有资源)
Pipeline 刚 new 出来时的状态。内存分配了,但里面是空的:
[UNINITIALIZED]
pipeline 对象刚构造 → 还没有任何 Node → 没有任何 Port → 啥也干不了
触发函数:Pipeline 构造函数
// Pipeline::Create() 中做的事情:
// 1. 创建所有 Node(根据 PipelineCreateInputData 中的 nodeConfig)
// 2. 为每个 Node 创建输入/输出 Port
// 3. 设置每个 Port 的 BufferProperties(格式、大小、队列深度)
// 创建完成后:
// - Node 列表形成了
// - Port 列表形成了
// - 但 Node 之间的依赖关系还没建立
此时的状态:零件摆好了,但还没组装。
这是最关键的一个状态迁移,因为这里是 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 的上游是谁"。
申请了 Buffer,发现这是"准备阶段"的中间过渡状态,释放掉临时资源:
void Pipeline::ReleaseResources() {
// 1. 释放 FINALIZED 阶段申请的临时资源
// 2. 进入等待 Stream On 的状态
}
为什么要有这个状态?
因为 Buffer 的分配策略是:先准备好,等真正要用的时候再分配。FINALIZED 阶段分配的是一些配置数据(metadata 池等),到 ACQUIRED 阶段才分配真正的图像 Buffer。中间的 RESOURCES_RELEASED 就是"配置完了,等开干"的状态。
Pipeline 在这个状态意味着"随时可以启动"。
STREAM_OFF ↔ ACQUIRED → PARTIAL → STREAM_ON → STREAM_OFF
正常流程:OFF → ACQUIRED → ... → ON
异常恢复:ON → OFF → ACQUIRED → ... → ON(重启)
完全销毁:ON → OFF → UNINITIALIZED(释放一切)
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 数量要留足。
void Pipeline::PartialStreamOn() {
// 启动"能够尽早启动"的 Node
// 比如 IFE:一旦 Sensor 有帧进来,IFE 就要开始处理
// 而 JPEG Node 可以等(它只在拍照时激活)
}
为什么要有"部分启动"这个概念?
有些 Node 是"实时"的(IFE/IPE),Sensor 每来一帧都要处理。有些 Node 是"按需"的(BPS/JPEG),只有拍照时才激活。
如果等所有 Node 都准备好了再 Stream On,那第一帧的延迟就大了。所以先启动实时 Node,后面的 Node 准备好了再加入。
这是低延迟设计的体现。
唯一的"正常工作"状态。
在这个状态下:
STREAM_ON 状态下的每帧循环:
① 接收 Request
② Pipeline 解析 Request,分给各 Node
③ Node 检查依赖 → 依赖满足 → 处理
④ Node 处理完成 → 写回 Result
⑤ Pipeline 收集所有 Node 的 Result
⑥ Session 汇聚 → 回调给 App
第 ①~⑥ 步每帧重复。
现在,我们把从打开相机到关闭相机的完整 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 卡住了。
前面说了 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 层会立即收到一个错误回调。
前面讲了 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();
}
}
一个很有意思的细节: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
每个 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 需要特殊的内存分配策略
当你遇到问题,第一步通常是看 Session Dump。
# 方法 1: dumpsys(最方便)
adb shell dumpsys media.camera > session_dump.txt
# 方法 2: 配置自动 dump
echo "enableSessionDump=1" >> camxoverridesettings.txt
=== 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 是不是有什么额外的处理拖慢了
如果你要自己翻代码,下面是这章涉及的核心文件和关键行号入口:
| 组件 | 文件 | 关键内容 |
|---|---|---|
| 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()(请求和结果)
在结束这章之前,澄清几个经常被问混淆的问题:
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 就起了作用
下面用一个真实的调试场景来展示这章的知识怎么用。
打开相机预览,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 秒还没好。
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 的 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 处理分支)
这个案例说明了什么?
- Pipeline 状态机告诉你"整体在正常工作"(STREAM_ON),但 Node 级别的 WAITING 暴露了问题
- IFE 的 4 个 buffer 全部被占满,说明流水线后端(BPS)堵住了
- Buffer 的 Free 数量是最直观的诊断指标——Free=0 通常意味着"哪里卡住了"
- 日志中的
satisfied=false持续超过几秒,才说明真的有问题(短时间的等待是正常的)
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
在 session_dump.txt 中找出:
PipelineFlags 相关输出?waiting?拍照一次,再执行一次 dumpsys。对比拍照前后的变化:
下章预告:第 14 章 Node 机制——DependencyUnit 是 CamX 异步并行架构的精髓。你会发现,Node 之间不直接调用,而是通过依赖声明来协作。这种设计,值得每个做异步系统的人学习。