简介:本资源是一份面向Android相机驱动开发工程师与高通平台Camera框架研究者的深度技术解析文档,聚焦高通Chi-CDK Feature2架构设计与运行机制,解决Camera HAL层多pipeline协同调度、缓冲区高效管理及实时图像处理链路构建等核心难题。文档以PDF格式呈现,共1个文件,大小约1000KB,内容涵盖FeatureGraph与FeatureNode建模逻辑、CHITargetBufferManager缓冲区管理策略、Request-Execute-Result三阶段处理流程、RealTime/RTBay2YUV/RTMFSRYUV实时数据流组件作用、Camx XML pipeline定义方式、FeatureInternalLink跨节点连接机制及Link状态机实现细节,并辅以Simple Model图解与完整执行时序分析。目前已有8067人学习下载,读者可直接掌握Feature2框架的全局架构视图、关键组件职责划分、Stage依赖配置方法及典型场景(如RTBayer2YUV)的端到端执行路径,为定制化Camera功能开发与性能调优提供扎实理论支撑与可复用的设计参考。
1. Feature2 不是“更高级的 CamX”,而是把 pipeline 的控制权从 XML 静态描述里硬生生抢回来的一次架构重构
你写完一个camxoverrides.xml,定义好 sensor → IFE → IPE → FD → Stats 的链路,编译进镜像,重启生效——这曾是高通 Camera 开发者最熟悉的“确定性”。但当某天需求变成“同一帧里,Preview 走 Bayer2YUV + DENOISE,ZSL 回写走 MFSR + TONEMAP,而 Metadata 又要单独走一条低延迟路径”,XML 就卡死了:它无法在 runtime 动态拆分、复用、重组 pipeline 片段。Feature2 正是为解决这个根本矛盾而生:它把原本固化在 XML 中的 pipeline 拓扑,抽象成可编程的FeatureGraph;把每个处理单元(如 Bayer2YUV)从CamX Node升级为可独立生命周期管理的FeatureNode;最关键的是,它让Link不再是 XML 里<link src="ife0" dst="ipe0"/>这种静态字符串,而是一个带状态机(NotVisited/Disabled/OutputPending)、可被 runtime 动态遍历与裁剪的有向边。这不是语法糖升级,而是把 camera pipeline 从“电路板布线”推进到“可编程逻辑阵列”阶段。理解 Feature2,本质是理解高通如何用 C++ 对象模型,在底层驱动层实现一套轻量级的、面向图像流的 DAG 调度引擎——它不替代 CamX,而是架在 CamX 之上,做 CamX 做不了的动态决策。
2. FeatureGraph:不是容器,而是带状态传播能力的拓扑调度器
FeatureGraph 表面看是 FeatureNode 的集合,但它的核心价值远不止于此。它不存储节点实例本身,而是通过全局m_linkData维护所有 Link 的元数据,并赋予 Link 三种语义明确的类型:ExtSrcLink(上层 App 输入)、ExtSinkLink(上层 App 输出)、InternalLink(FeatureNode 间数据流转)。这种设计让 Graph 具备了两个关键能力:一是状态可传播——Link 状态(如 OutputPending)会沿 InternalLink 向上游反向触发WalkBackFromLink,自动标记依赖未满足的节点为InputResourcePending;二是拓扑可裁剪——当某个 SinkPort 未被上层请求时,其对应的 ExtSinkLink 置为 Disabled,整条下游链路(包括所有关联的 InternalLink)会被递归禁用,避免无谓的 buffer 分配与 pipeline 提交。这才是 Feature2 实现“按需激活”的底层机制。
2.1 Link 状态机:从 NotVisited 到 OutputPending 的语义跃迁
Link 的状态不是简单的布尔开关,而是承载了明确的调度语义:
NotVisited:初始状态,表示该 Link 尚未被任何 Request 扫描到,此时pSrcGraphNode和pSinkGraphNode均不参与本次调度。Disabled:显式禁用,可能因上层未请求该输出端口,或 FeatureNode 被整体 bypass。此时portToInputLinkIndexMap和portToOutputLinkIndexMap中对应索引被置空,WalkBackFromLink会跳过此 Link。OutputPending:最关键的活跃态,表示该 Link 已被当前 Request 选中,其pSinkGraphNode的 output port 必须产出数据,且该数据将作为pSrcGraphNode的 input dependency。此时pSinkGraphNode进入ReadyToExecute状态的前置条件之一。
提示:
OutputPending并不保证数据已就绪,只表示“该 Link 被激活,等待数据生产”。真正的数据就绪由ProcessResultMessage触发状态跃迁。
2.2 WalkAllExtSinkLinks:从输出端反向驱动的拓扑扫描引擎
整个 FeatureGraph 的调度起点不是入口(Sensor),而是出口(SinkPort)。WalkAllExtSinkLinks()函数执行以下三步原子操作:
- 标记 SinkLink:遍历所有
ExtSinkLink,若其portId匹配上层传入的m_extSinkPortIdToChiStreamMap.portId或为 Metadata 类型,则置linkState = OutputPending; - 注册上游请求:对每个
OutputPendingLink,调用ProcessInputDepsForUpstreamFeatureRequest(),将requestIndex和该 Link 的portId写入pSrcGraphNode->requestTable[requestIndex],同时递增numRequestOutputs; - 触发反向遍历:调用
WalkBackFromLink(pOutputLinkData),从pSrcGraphNode开始,检查其所有output link是否全为Disabled;若否,则继续向上游递归,直至找到首个CheckAllOutputLinksReadyToProcessRequest()返回 true 的节点,为其创建FeatureRequestObject并启动ProcessRequest()。
这段逻辑彻底颠覆了传统 pipeline 的正向流水线思维——它让调度决策从“我有什么就推什么”,变成“我要什么才拉什么”。
// chi_feature2_graph.cpp 关键片段 VOID ChiFeature2Graph::WalkAllExtSinkLinks( ChiFeature2UsecaseRequestObject* pUsecaseRequestObj) { for (UINT32 extSinkLinkIndex = 0; extSinkLinkIndex < m_numExtSinkLinks; ++extSinkLinkIndex) { ChiFeature2GraphLinkData* pExtSinkLinkData = &m_pLinkData[extSinkLinkIndex]; pExtSinkLinkData->linkState = ChiFeature2GraphLinkState::Disabled; // Step 1: Mark OutputPending for matched SinkPorts if (IsSinkPortMatched(pExtSinkLinkData, pUsecaseRequestObj)) { pExtSinkLinkData->linkState = ChiFeature2GraphLinkState::OutputPending; // Step 2: Register upstream request ProcessInputDepsForUpstreamFeatureRequest(pExtSinkLinkData, 0); // Step 3: Trigger backward walk WalkBackFromLink(pExtSinkLinkData); } } }这段代码后必须跟一句机制说明:IsSinkPortMatched()的判定逻辑直接决定了 FeatureGraph 的“可见性边界”——只有被上层明确请求的 SinkPort 才能激活整条链路,这是 Feature2 支持多路并发、ZSL 与 Preview 解耦的底层保障。ProcessInputDepsForUpstreamFeatureRequest()写入的requestTable是后续ChiFeature2RequestObject::Create()分配内存的唯一依据,requestTable.size()直接决定m_numRequests,而requestTable[i].size()决定第 i 个 Request 的numRequestOutputs。没有这一步,整个 Graph 就是死的。
2.3 FeatureNode 的 Ready 状态判定:一个被严重低估的“就绪门控”
FeatureNode的ReadyToExecute状态并非简单地“所有 input dependency 满足”,而是基于其所有output link的状态组合判定:
BOOL ChiFeature2Graph::CheckAllOutputLinksReadyToProcessRequest( ChiFeature2GraphNode* pGraphNode) { BOOL isReady = FALSE; UINT32 numOutputLinks = pGraphNode->numOutputLinks; // 至少有一个 output link 处于 OutputPending 状态 BOOL hasOutputPending = FALSE; for (UINT32 i = 0; i < numOutputLinks; ++i) { ChiFeature2GraphLinkData* pOutputLinkData = GetOutputLinkData(pGraphNode, i); if (pOutputLinkData->linkState == ChiFeature2GraphLinkState::OutputPending) { hasOutputPending = TRUE; break; } } // 所有 output link 都不能是 NotVisited(即必须被扫描过) BOOL allVisited = TRUE; for (UINT32 i = 0; i < numOutputLinks; ++i) { ChiFeature2GraphLinkData* pOutputLinkData = GetOutputLinkData(pGraphNode, i); if (pOutputLinkData->linkState == ChiFeature2GraphLinkState::NotVisited) { allVisited = FALSE; break; } } isReady = hasOutputPending && allVisited; return isReady; }这个判定逻辑暴露了 Feature2 的核心设计哲学:Node 的就绪,取决于它“能输出什么”,而非“需要输入什么”。只要有一个OutputPendingLink,且所有 Link 都已被扫描(即拓扑已明确),该 Node 就具备了被调度的资格。它的 input dependency 是否满足,是在Sequence_-1阶段由GetDependency()动态查询的——这正是 Feature2 支持跨 FeatureNode 数据复用(如 RealTimeNode 的 output 直接作为下游 input)的前提。如果误以为Ready等价于input ready,就会在调试时陷入“为什么 Node 已 Ready 却卡在 InputResourcePending”的困惑。
3. CHITargetBufferManager:不是缓冲区分配器,而是跨 FeatureNode 的引用计数协处理器
CHITargetBufferManager(CTBM)常被误解为一个简单的 buffer pool 管理器,但它真正的角色是 Feature2 架构中实现“零拷贝数据共享”的信任锚点。它不负责物理内存分配(那是ChiBufferManager或上层ImportExternalTargetBuffer的事),而是通过ImportList/ProducerList/ConsumerList三个队列和精确到字节的refCount,为每个TargetBuffer建立全生命周期的引用关系图。当一个FeatureNode的 output port 产出 buffer,CTBM 将其 node 从ProducerList移至ConsumerList,refCount--;当下游FeatureNode将其作为 input port 使用时,refCount++。只有当refCount归零且队列中 buffer 数量超过minNumOfBuffers时,才真正释放物理内存。这种设计让RealTimeDisplay_OutRaw_OutMeta_OutRDI_InMeta_InMeta_OutYUV_Outyuvmetadata这类复杂链路中的 buffer 复用成为可能——同一块 raw buffer,既被 RealTimeNode 读取,又被 MFSRNode 读取,CTBM 用refCount精确跟踪着每一处引用。
3.1 三队列模型:Import/Producer/Consumer 的语义隔离
CTBM 的三个队列不是性能优化技巧,而是语义契约:
ImportList:存放上层 App 通过ImportExternalTargetBuffer()传入的 buffer。这些 buffer 的生命周期完全由 App 控制,CTBM 只负责登记(pData指向外部内存)和引用计数。refCount初始为 1,表示 App 持有一份引用。ProducerList:存放由 CTBM 自身通过ChiBufferManager申请的 internal buffer。这些 buffer 的物理内存由 CamX 管理,CTBM 仅负责逻辑归属。refCount初始为 1,表示 Producer(当前 FeatureNode)持有一份引用。ConsumerList:存放已被下游 Consumer(其他 FeatureNode)获取的 buffer。每次UpdateTarget()调用,都会将 node 从ProducerList移出,加入ConsumerList,并refCount--。refCount在此处代表“还有多少个 Consumer 未完成消费”。
注意:
refCount的增减严格遵循“谁使用谁增加,谁释放谁减少”原则。SetupTargetBuffer()调用时refCount++(Producer 增加自身引用),ReleaseTargetBuffer()调用时refCount--(Producer 释放自身引用)。下游GetInputDependency()获取 buffer 时,CTBM 内部会refCount++(Consumer 增加引用),而ProcessResultMessage()中ReleaseInputDependencyMessagge则refCount--(Consumer 释放引用)。
3.2 Buffer 生命周期的四个关键钩子
CTBM 的 buffer 管理完全由四个函数驱动,每个都对应明确的状态跃迁:
| 函数 | 触发时机 | 队列操作 | refCount 变化 | 语义 |
|---|---|---|---|---|
ImportExternalTargetBuffer() | 上层传入 sink buffer | node 加入ImportList | refCount = 1 | App 声明对该 buffer 的所有权 |
SetupTargetBuffer() | FeatureNode 准备 output port | node 从ImportList移至ProducerList | refCount++ | Producer 接管 buffer,准备写入 |
UpdateTarget() | result 返回,buffer 成为下游 input | node 从ProducerList移至ConsumerList | refCount-- | Producer 完成写入,buffer 可被下游读取 |
ReleaseTargetBuffer() | FeatureNode 显式释放 buffer | node 从ConsumerList移除,pData释放 | refCount-- | Producer 放弃对该 buffer 的所有权 |
// chi_target_buffer_manager.cpp 关键逻辑 CDKResult CHITargetBufferManager::UpdateTarget( ChiTargetBufferInfo* pTargetBufferInfo, ChiFeature2RequestObject* pRequestObject) { // 1. 从 ProducerList 移除 node DoublyLinkedNode* pNode = FindNodeInProducerList(pTargetBufferInfo->hBuffer); RemoveNodeFromProducerList(pNode); // 2. 加入 ConsumerList AddNodeToConsumerList(pNode); // 3. refCount 减一:Producer 完成生产 pNode->refCount--; // 4. 若下游需要,refCount 加一(内部隐式) if (pRequestObject->IsDownstreamConsumer()) { pNode->refCount++; } return CDKResultSuccess; }这段代码揭示了一个关键细节:UpdateTarget()不仅是 Producer 的收尾动作,更是 Consumer 的启始动作。IsDownstreamConsumer()的判断逻辑,决定了该 buffer 是否会被下游 FeatureNode 的GetInputDependency()获取——这正是RTBayer2YUV中RealTimeNode的 output buffer 能直接作为Bayer2YUV的 input buffer 的技术基础。如果refCount在UpdateTarget()后变为 0,意味着该 buffer 已无任何 Consumer,CTBM 会立即触发ReleaseTargetBuffer()清理资源,避免内存泄漏。
3.3 避坑:FeatureNode 缓冲区管理的五大血泪经验
Feature2 的 buffer 管理是高频翻车区,以下是某开发者在调试RTMFSRYUV链路时踩过的五个典型坑,每条都附带可验证的排查路径:
现象:
ProcessResultMessage()中ProcessBufferCallback(yuv)被调用,但yuvbuffer 数据全黑。
原因:Bayer2YUVFeatureNode 的output port在SetupTargetBuffer()时未正确设置bufferFormat和bufferSize,导致 CamX 分配的 internal buffer 尺寸错误,实际写入越界。
解决:在OnProcessRequest()的Sequence_0阶段,检查pRequestInfo->pRequestOutput[i].bufferMetadataInfo.bufferFormat是否与ChiFeature2Descriptor::StageDescriptor中定义的outputPortFormat严格一致;用ChiBufferManager::GetBufferSize()校验bufferSize。现象:
RealTimeDisplay正常显示,但MFSR处理帧率骤降,log 显示CHITargetBufferManager::Allocate failed。
原因:RealTimeNode的output port被多个 downstream FeatureNode(如 Display + MFSR)同时引用,但minNumOfBuffers设置过小(如设为 2),导致ConsumerList中 buffer 不足,Allocate时无法复用。
解决:在CHITargetBufferManager::Create()初始化时,将minNumOfBuffers设为max(downstream_count, 3);或在ProbeChiFeature2Features()阶段,根据FeatureDescriptor中numDownstreamNodes动态计算。现象:
Preview流正常,但ZSL回写 buffer 偶发花屏,且仅在高帧率下出现。
原因:ZSL的ExtSinkLink与Preview的ExtSinkLink共享同一SinkTarget,但WalkAllExtSinkLinks()未对ZSLLink 设置OutputPending,导致其requestTable为空,FeatureRequestObject未创建,buffer 未被SetupTargetBuffer()预分配。
解决:检查IsSinkPortMatched()的判定逻辑,确保ZSL的portId在m_extSinkPortIdToChiStreamMap中有独立映射;在configure_streams()阶段,为ZSL显式调用ImportExternalTargetBuffer()。现象:
Metadatacallback 频繁丢失,ProcessMetadataCallback(meta)调用次数远少于预期。
原因:Metadata的ExtSinkLink被误设为Disabled,因为portId.portType比较时未区分ChiMetadataPortType::Stats与ChiMetadataPortType::App,导致IsSinkPortMatched()返回 false。
解决:在IsSinkPortMatched()中,对MetaData类型 Link,增加portId.portType == ChiMetadataPortType::Stats的精确匹配;打印pExtSinkLinkData->portId.portType与m_extSinkPortIdToChiStreamMap.portId.portType进行比对。现象:
RTBay2YUV链路中,RealTimeNode的output portbuffer 地址在ProcessResultMessage()中与GetInputDependency()获取的地址不一致。
原因:RealTimeNode的output port未在FeatureDescriptor的StageDescriptor中声明为isRealTime = TRUE,导致 CTBM 未启用virtual InternalLink机制,而是为每个 Request 重新分配新 buffer。
解决:检查ChiFeature2Descriptor::StageDescriptor结构体,确认isRealTime字段为TRUE;在DoFeatureCreate()的PrepareStageData()阶段,校验pStageData->isRealTime是否被正确赋值。
4. ProcessRequest 状态机:Sequence_-1 与 Sequence_0 的双阶段解耦,是 Feature2 支持动态依赖的核心
Feature2 的ProcessRequest()不是一个线性函数,而是一个由FeatureRequestObject(FRO)状态机驱动的双阶段协议。Sequence_-1(Dependency Setup)与Sequence_0(Buffer Submit)的严格分离,使得 Feature2 能在 runtime 动态解析跨 Node 依赖,这是它区别于 CamX 静态 pipeline 的本质特征。Sequence_-1阶段只做两件事:1)遍历所有output link,收集上游FeatureNode的output port信息,构建dependency list;2)根据dependency list的完备性,决定该 FRO 是进入Executing状态(无依赖),还是InputResourcePending状态(有依赖)。Sequence_0阶段则纯粹是 buffer 绑定与 pipeline 提交,不涉及任何依赖决策。这种解耦让RealTimeNode的 output 可以在Sequence_-1中被识别为dependency,而在Sequence_0中直接复用其物理地址,无需 memcpy。
4.1 FRO 状态机:从 Initialized 到 OutputResourcePending 的七步闭环
FeatureRequestObject的状态流转严格遵循以下路径,每一步都对应明确的函数调用与资源操作:
Initialized→ReadyToExecute:OnPrepareRequest()被调用,初始化requestTable,为每个OutputPendingLink 分配RequestOutputInfo;ReadyToExecute→InputResourcePending:GetDependency()发现上游output port未就绪,SetCurRequestState(InputResourcePending);InputResourcePending→Executing:ProcessDependency()收到上游ProcessResultMessage(),dependency满足,SetCurRequestState(Executing);Executing→OutputResourcePending:OnExecuteProcessRequest()完成SubmitRequestToSession(),SetCurRequestState(OutputResourcePending);OutputResourcePending→Initialized:HandleOutputResourcePending()调用ReleaseTargetBuffer(),清理本 Node 的 buffer 引用,SetCurRequestState(Initialized);ReadyToExecute→Executing:GetDependency()无依赖,直接SetCurRequestState(Executing);Executing→OutputResourcePending:同第 4 步。
提示:
CanRequestContinue()函数只返回TRUE给Initialized、ReadyToExecute、Executing三种状态,这意味着InputResourcePending和OutputResourcePending是阻塞态,FRO 在此状态下不会被ProcessRequest()循环再次处理,直到外部事件(如ProcessResultMessage())触发状态跃迁。
4.2 Sequence_-1:GetDependency() 如何动态发现上游 output port
GetDependency()的核心逻辑是“向上游 FeatureNode 查询其output port的 handle”,而非静态配置。它通过pUpstreamGraphNode->requestTable查找对应requestIndex的RequestOutputInfo,从中提取hBuffer:
CDKResult ChiFeature2Base::GetDependency( ChiFeature2RequestObject* pRequestObject, ChiFeature2GraphNode* pUpstreamGraphNode, UINT8 requestIndex, ChiTargetBufferInfo* pTargetBufferInfo) { // 1. 从上游 Node 的 requestTable 中获取 output info std::vector<std::vector<ChiFeature2RequestOutputInfo>>& requestTable = pUpstreamGraphNode->requestTable; if (requestIndex >= requestTable.size()) { return CDKResultEFailed; } std::vector<ChiFeature2RequestOutputInfo>& requestOutputs = requestTable[requestIndex]; if (requestOutputs.empty()) { return CDKResultEFailed; } // 2. 取第一个 output port(实际可能有多个,需遍历) ChiFeature2RequestOutputInfo& requestOutputInfo = requestOutputs[0]; pTargetBufferInfo->hBuffer = requestOutputInfo.bufferMetadataInfo.hBuffer; // 3. 关键:增加 refCount,表示本 Node 开始引用该 buffer CHITargetBufferManager::IncrementRefCount(pTargetBufferInfo->hBuffer); return CDKResultSuccess; }这段代码的关键在于IncrementRefCount()调用——它确保了上游RealTimeNode的 output buffer 在被下游Bayer2YUV引用期间,不会被ReleaseTargetBuffer()提前释放。requestTable的存在,使得GetDependency()不需要知道上游 Node 的具体类型(RealTime / Bayer2YUV / MFSR),只需按requestIndex索引即可,这是 Feature2 实现“功能无关依赖”的基石。
4.3 Sequence_0:SubmitRequestToSession() 如何绑定 CamX pipeline
Sequence_0阶段的SubmitRequestToSession()是 Feature2 与 CamX 的胶水层。它不创建新 pipeline,而是将已存在的CamX Session与当前 FRO 的 buffer 绑定:
CDKResult ChiFeature2Base::SubmitRequestToSession( ChiFeature2RequestObject* pRequestObject, ChiFeature2GraphNode* pGraphNode) { // 1. 为每个 output port 设置 buffer info for (UINT32 i = 0; i < pRequestObject->GetNumRequestOutputs(); ++i) { ChiFeature2RequestOutputInfo* pOutputInfo = pRequestObject->GetRequestOutputInfo(i); SetBufferInfo(pOutputInfo); // 调用 CHITargetBufferManager::SetBufferInfo() } // 2. 为每个 input port 获取 dependency buffer for (UINT32 i = 0; i < pGraphNode->numInputPorts; ++i) { ChiFeature2PortDescriptor* pPortDesc = &pGraphNode->inputPorts[i]; ChiTargetBufferInfo targetBufferInfo; GetInputDependency(pPortDesc, &targetBufferInfo); // 内部调用 GetDependency() SetInputBufferInfo(&targetBufferInfo); } // 3. 提交到 CamX Session return m_pSession->SubmitRequest(pRequestObject->GetCamXRequest()); }SetBufferInfo()和SetInputBufferInfo()的作用,是将 CTBM 管理的hBufferhandle 注入 CamX 的CamXRequest结构体。m_pSession->SubmitRequest()最终调用 CamX 的CamXSession::SubmitRequest(),至此,Feature2 的调度完成,控制权移交 CamX。Sequence_0的纯粹性(只做绑定,不做决策)保证了 Feature2 的轻量与高效。
5. FeatureInternalLink:虚拟链路与物理链路的二象性,是 ZSL 和多路并发的底层支点
FeatureInternalLink是 Feature2 中最具迷惑性的概念。它表面是FeatureNode间的连接线,实则是两种截然不同机制的统一体:物理 InternalLink对应 CamX 中真实存在的 pipeline 间数据通路(如 IFE 的 RDI 输出连到 IPE 的 Raw 输入),而虚拟 InternalLink(virtual InternalLink)则完全由 Feature2 在内存中模拟,不消耗任何硬件资源,专为RealTimeNode这类“不走 CamX pipeline”的特殊节点设计。RTBayer2YUV链路中,RealTimeNode的output port与Bayer2YUV的input port之间就是虚拟 Link——RealTimeNode直接将 buffer 地址写入requestTable,Bayer2YUV在GetDependency()时读取该地址,全程无硬件传输。这种二象性让 Feature2 既能兼容传统 CamX pipeline,又能无缝接入自定义算法模块,是其实现“软硬协同”的关键设计。
5.1 物理 InternalLink:CamX pipeline 的拓扑投影
物理InternalLink的存在,是为了将 CamX 的 pipeline 拓扑“投影”到 FeatureGraph 中。ChiFeature2Descriptor::InternalLinkDesc结构体中,srcPipelineId和dstPipelineId必须与camxoverrides.xml中定义的 pipeline ID 严格一致。FeatureGraph在CreateFeatureGraph()阶段,会遍历所有InternalLinkDesc,为每个 Link 创建ChiFeature2GraphLinkData,并将其pSrcGraphNode和pSinkGraphNode指向对应的FeatureNode。此时,WalkBackFromLink()沿物理 Link 的遍历,等价于 CamX 中DRQ队列的 dependency 传递——IFE的 output pending,会触发IPE的 input dependency 检查。
5.2 虚拟 InternalLink:RealTimeNode 的零拷贝魔法
RealTimeNode的 magic 在于它不提交任何 CamX Request,却能产出output port数据。其output port的 buffer 由CHITargetBufferManager::ImportExternalTargetBuffer()从上层 App 获取,ProcessResultMessage()中ProcessBufferCallback()直接将该 buffer 的hBufferhandle 写入requestTable。下游FeatureNode的GetDependency()读取此 handle,SetInputBufferInfo()将其注入 CamX Request。整个过程绕过了 CamX 的DRQ与ready/deferred队列,实现了真正的零拷贝。virtual InternalLink的存在,让RealTimeNode成为 FeatureGraph 中的“第一公民”,而非 CamX 的附属品。
5.3 Link 状态同步:物理 Link 与虚拟 Link 的统一调度
无论物理还是虚拟,InternalLink的linkState都由WalkAllExtSinkLinks()统一管理。OutputPending状态的传播不区分 Link 类型——当RealTimeNode的output port被标记为OutputPending,其下游Bayer2YUV的input port就会进入InputResourcePending状态,触发GetDependency()查询。这种统一调度,抹平了硬件 pipeline 与软件算法模块的界限,让RTMFSRYUV这样的混合链路成为可能:RealTimeNode(虚拟)→MFSRNode(物理)→TonemapNode(物理)→SinkPort(虚拟)。
6. 从 Chi-cdk 到 Feature2:一次对“谁该为调度负责”的权力重定义
Chi-cdk(Camera Hardware Interface - Common Development Kit)在 Feature2 架构中,早已不是早期文档里那个“封装 CamX API 的工具包”。它的核心职责已演变为FeatureGraph 的编译器与加载器。configure_streams()阶段,ChiFeaturePoolManager::Create()会扫描/vendor/lib64/camera/feature2/下所有com.qti.feature2.*.so,通过DoFeatureCreate()加载每个 SO 的ChiFeature2Descriptor,并将其SessionDescriptor、StageDescriptor、InternalLinkDesc解析为内存中的FeatureGraph元数据。Chi-cdk不执行任何调度逻辑,它只提供ChiFeature2Graph::Create()、ChiFeature2Graph::WalkAllExtSinkLinks()等纯虚函数接口,真正的调度引擎是Feature2Wrapper中的ExecuteProcessRequest()。这种分层,让 OEM 可以在不修改 Chi-cdk 的前提下,通过编写新的feature2.*.so来扩展功能,而无需触碰 CamX 底层。
6.1 ChiFeature2Descriptor:Feature2 的“中间语言”
ChiFeature2Descriptor是 Feature2 的 ABI 契约,其结构体定义了 Feature2 的全部语义:
struct ChiFeature2Descriptor { ChiFeature2SessionDescriptor sessionDescriptor; // 所有 pipeline 的 ID、port 信息 ChiFeature2StageDescriptor* pStageDescriptors; // 每个 stage 使用哪些 pipeline、port、dependency ChiFeature2InternalLinkDesc* pInternalLinkDescs; // pipeline 间 link 描述 UINT32 numStageDescriptors; UINT32 numInternalLinkDescs; };sessionDescriptor是 Feature2 的“符号表”,pStageDescriptors是“指令集”,pInternalLinkDescs是“控制流图”。DoFeatureCreate()加载 SO 时,Chi-cdk会校验sessionDescriptor.pipelineId是否存在于当前 CamX Session 中,pStageDescriptors[i].inputPortId是否在sessionDescriptor中定义——这是 Feature2 实现“安全加载”的关键校验。Chi-cdk本身不包含任何算法逻辑,它只是一个运行时链接器。
6.2 UsecaseRequestObject(URO)与 FeatureRequestObject(FRO):跨层请求的桥接器
URO是 App 层请求的镜像,FRO是 FeatureGraph 层的执行单元,二者通过CreateUsecaseRequestObject()与CreateFeatureRequestObject()一一映射。URO中的curProcessSequenceId决定FRO的maxProcessSequence,URO的numRequests决定FRO的m_numRequests。这种映射,让 App 的“一帧多请求”(如 Preview + ZSL + Video)能被 Feature2 精确拆解为多个FRO,每个FRO独立走自己的状态机。URO是 Feature2 与上层的契约,FRO是 Feature2 与 CamX 的契约,Chi-cdk是这两者之间的翻译官。
6.3 设计取舍的终极思考:为什么 Feature2 不直接取代 CamX?
Feature2 的存在,本质上是对“调度权归属”的一次妥协式创新。CamX 的优势在于对硬件 pipeline 的极致优化与稳定,劣势在于静态拓扑无法适应复杂用例;Feature2 的优势在于 runtime 动态调度与灵活组合,劣势在于无法直接操作硬件寄存器。高通选择让 Feature2 架在 CamX 之上,而非重写 CamX,是经过深思熟虑的工程权衡:
- 稳定性优先:CamX 经过数代手机验证,替换风险极高。Feature2 作为上层框架,即使出错,影响范围可控。
- 渐进式演进:OEM 可以逐步将旧 XML pipeline 迁移为 Feature2 Graph,无需一次性重写所有模块。
- 分工明确:CamX 专注“如何高效执行单个 pipeline”,Feature2 专注“何时、何地、以何种组合执行 pipeline”。
这种分层,也带来了调试复杂度的上升——当你看到RTBayer2YUV链路异常时,问题可能在Chi-cdk的 descriptor 解析、FeatureGraph的 link 状态传播、CHITargetBufferManager的 refCount 错误、CamX Session的 buffer 绑定,甚至camxoverrides.xml的 pipeline ID 拼写错误。从那以后我每次遇到InputResourcePending卡死,都强制走一遍WalkAllExtSinkLinks()的日志,逐行核对pExtSinkLinkData->linkState的变化,再顺藤摸瓜查requestTable的 size 和内容。Feature2 的强大,恰恰藏在它层层抽象的缝隙里——只有亲手拨开m_linkData、requestTable、refCount这三重迷雾,你才算真正握住了高通 camera 的调度权。
希望帮到你。
本文还有配套的精品资源,点击获取