高通Camera chi-cdk feature2框架解析:DAG拓扑与HAL自定义节点实践
简介面向高通Camera驱动、算法及多媒体框架开发者的技术专题PDF以Chi-CDK Feature2框架为对象系统梳理其整体架构与核心运行机制覆盖FeatureGraph/FeatureNode管理、CHITargetBufferManager缓冲区管理、Request/Execute/Result三阶段流程、RealTime/RTBay2YUV/RTMFSRYUV实时链路、Camx XML pipeline定义、FeatureInternalLink跨pipeline连接以及Link状态机等关键技术点并详细展开FeatureGraph从简单模型到实际实现的推导过程帮助读者理解camera pipeline从配置到buffer提交的完整逻辑。资源为一份PDF文档共1个文件大小约1000KB以图文和流程图辅助讲解适合作为源码阅读前的体系化预习资料。已有8049人学习。内容后半部分深入FeatureGraph实现细节包括m_linkData的visit机制、Link状态变化、FeatureNode ready判定、request Sequence(-1)/Sequence(0)执行、dependency获取与buffer释放等关键点也可用于实战排错时的快速查阅。1. 高通 Camera chi-cdk 的 feature2 框架为什么 HAL 层要养一张“处理图”调试高通 Camera 的 chi-cdk 时feature2 是绕不开的一块。只要你想在预览或者拍照链路上插一个自己的处理节点比如做美颜、多帧降噪、景深虚化最终都会落到 feature2 的拓扑里。它本质上是把相机处理流程拆成一个个带输入输出端口的节点再用一张有向无环图把这些节点串起来。和早期的 feature1 那种一条流水线写死的做法相比feature2 最大的变化是加一个效果节点不再需要把主链路从头改一遍而是改一张“接线图”。这篇笔记就围绕这个框架讲清楚它是什么、怎么用、参数怎么设、以及最容易踩的坑适合刚接手 chi-cdk 的 HAL 工程师也适合被拉到平台项目里做效果优化的应用层同学。我把 feature2 讲明白的方式很直接先看它要解决什么痛点再拆它的核心设计然后按“实现一个自己的节点”走一遍最小可跑通的流程最后集中说排查思路。这样你回去对着代码能很快定位到该改哪里而不是在几个目录里互相翻。2. feature2 的核心设计节点、端口和一张有向图2.1 从 feature1 到 feature2为什么“图”比“流水线”好用早期的 feature1 框架更像一条固定的流水线预览是预览拍照是拍照每个场景写死一串处理模块模块之间靠硬编码的顺序衔接。想在某两个模块之间插一个新的处理步骤得改主流程代码改动链路长而且不同项目的 usecase 差异一大代码就很容易长出各种分支。feature2 把这个结构改成了有向无环图DAG。每个处理单元叫一个 “feature”代码里往往是Feature类或者对应的FeatureRequestfeature 之间通过端口连接。一条完整链路比如“sensor 出帧 → 降噪 → 美颜 → 变焦 → 编码器”就被表示成 5 个节点和 4 条边的一幅拓扑。某个项目想省掉美颜不删代码只改拓扑描述想加一个超分节点也只需要在拓扑里新挂一个 feature 并把前后两个端口连起来。这种抽象带来的实际收益在调试期就能感受到。我调试某个项目的夜景多帧合成时需要把“帧选择 合成”拆开单独验证。在 feature1 里我得临时复制一条完整链路再删掉一半节点feature2 里我只需要构造一个两节点的拓扑一个做帧选择一个做合成直接提交到 chi 节点的 usecase 上验证完再回去改正式拓扑。整个闭环从改代码、编译、push 到看效果时间能缩短一半以上。2.2 节点与端口数据往里进处理后往外出在 feature2 里一个 feature 节点至少要有一对端口输入端口和输出端口。端口之间传输的是 buffer 和 metadata。buffer 是图像数据本体metadata 是伴随这一帧的辅助信息比如曝光时间、增益、人脸坐标、AWB 状态。节点从输入端口拿到 buffer自己的算法处理完之后把结果写到输出端口对应的 buffer 里再通知下一个节点。端口有几个属性直接影响你做事的粒度。一个是端口对应的 buffer 需求比如宽高、格式、数量另一个是端口的工作模式常见有bypass和copy。bypass 意味着这个节点处理时直接使用上游 buffer不做额外拷贝copy 模式是复制一份再处理。刚上手时很多问题出在这里某些上游节点输出的格式是 NV12你的算法要 YUV420 半平面于是不得不在端口配置里把格式转掉或者自己申请一块新 buffer。搞不清楚端口属性后面配置拓扑时就会面对各种“负优化”。通常一个节点会有一个主输入和一个主输出但也可以有多个端口。比如一个“画质增强”节点可以同时接收主图输入和深度图输入输出一张增强后的图。多端口的存在让图结构比简单的链式多了一层表达能力代价是拓扑解析和 buffer 申请的逻辑变复杂你得显式地指定每个端口关联的是哪条边。2.3 节点权重的意义让框架知道你的算法有多重feature2 的设计里有一个常被忽略的参数节点的权重processing cost。框架调度时并不只是做拓扑排序它还要考虑当前硬件的计算压力。有的节点是 GPU 算子有的是 DSP 算子有的纯 CPU甚至有的节点要占用单独的硬件资源比如某个专用加速器。节点权重就是告诉框架“我这个算法大约要消耗多少算力”。平台在用例运行前会做资源预算。如果预算超了它可能在运行时选择降级比如切到一个更轻的算法路径或者挂起某些非关键节点。你自定义的节点如果权重给得过高会出现一种很玄学的现象单帧处理没问题连拍或连续预览时帧率掉得厉害甚至直接丢帧。而把权重调成符合实现水平的数值调度就平顺了。我一般会先做一次单节点耗时测量把单位帧的耗时除以帧间隔时间作为权重数量级参考再留一点余量。权重也不要给太低太低会让框架以为负载很轻同时塞进多个重负载节点结果一起超预算。2.4 buffer 流转与引用计数谁负责释放内存在用 feature2 的时候最容易出问题的是 buffer 谁申请、谁释放、谁引用。平台里 buffer 通常来自ChiBufferManager节点拿到 buffer 后不自己随意释放而是通过引用计数来管理。一个 buffer 可能同时被下游节点引用比如你要做帧对齐也可能被多个节点共享读。在实际代码里一个比较规范的做法是输入端口给你 buffer你用完马上把引用还回去如果你还要留作后用比如做多帧缓存就把它的引用计数加一存到自己管理的队列里等真正用完再一次一次减。如果不加引用直接存下游节点可能已经把这个 buffer 回收到池子里下一帧的同一个地址写入了新数据你手里的老帧就被“悄悄改掉”了。这是多帧类功能最常见的翻车原因。3. 实现一个自定义 feature2 节点从接口到回调3.1 代码骨架头文件定义两个核心结构新增一个 feature2 节点通常要做三件小事定义一个ChiFeatureType的扩展定义节点自己的请求处理类以及定义端口和拓扑。代码上先落一个头文件声明你的 feature 类型、端口数量和端口属性。下面是一个常见骨架示意// my_beauty_feature.h #include chi.h #include chifeature2.h // 1. 扩展 feature type在现有枚举后追加你自己的类型 enum ChiFeatureTypeMyBeauty { ChiFeatureTypeMyBeauty 0x1001, // 选一个平台没用到的值 }; // 2. 定义这个 feature 的端口数量与配置 static const ChiFeaturePortInfo MyBeautyInputPort { .portId 0, .portType ChiFeaturePortTypeInput, .portProperty ChiFeaturePortPropertyDefault, }; static const ChiFeaturePortInfo MyBeautyOutputPort { .portId 0, .portType ChiFeaturePortTypeOutput, .portProperty ChiFeaturePortPropertyDefault, }; // 3. 定义 feature 的请求处理类 class MyBeautyFeature : public ChiFeature2Base { public: MyBeautyFeature(ChiFeature2RequestInfo reqInfo, ChiFeature2Session* session) : ChiFeature2Base(reqInfo, session) {} // 处理输入buffer产出输出buffer ChiFeature2StatusCode ProcessRequest(ChiFeature2RequestInfo reqInfo, ChiFeature2Buffer* inBuf, ChiFeature2Buffer* outBuf) override; };这里的关键是继承ChiFeature2Base并实现ProcessRequest。平台框架会按拓扑顺序调用节点每个节点只关心自己的输入输出不需要管上游是谁、下游是谁。3.2 注册与加载把节点挂到 chi-cdk 的用例上节点类写完只是第一步你还需要让 chi-cdk 在解析拓扑时认识你这个 feature。常见做法是把这个 feature 的构造入口注册到一个 feature 工厂里面同时在用例描述文件或代码里把 topology 中对应的节点类型指向你的类型。注册的方式各平台大同小异一般是在ChiFeature2Case初始化的时候把自己的构造器塞进一个表里后续解析到 type 时按表查出来创建实例。这一步最常见的坑是枚举值冲突。如果你的 feature type 数值和平台预留或已有的枚举重叠解析会直接失败日志里会看到“unknown feature type”之类的报错而且位置还不一定好找。3.3 最简拓扑两个节点也能跑一个用例为了验证你的 feature 实现是否正常可以先把链路缩到最小模拟一个“输入源节点 → 你的处理节点 → 输出节点”。输入源用现成的测试帧输出节点直接把 buffer 丢给 preview 或者 dump 到文件。下面是一个构造拓扑的示意// build_my_topology.cpp ChiFeature2Topology topo; topo.portNum 4; // 两个节点一共4个端口 // 输入源节点只有输出端口 topo.nodes[0].featureType ChiFeatureTypeTestSource; topo.nodes[0].outputPort[0].portId 0; topo.nodes[0].outputPort[0].type ChiFeaturePortTypeOutput; // 你的节点一个输入一个输出 topo.nodes[1].featureType ChiFeatureTypeMyBeauty; topo.nodes[1].inputPort[0].portId 0; topo.nodes[1].outputPort[0].portId 0; // 节点间连线source 的 0 号输出 - mybeauty 的 0 号输入 topo.links[0].srcFeatureId 0; topo.links[0].srcPortId 0; topo.links[0].dstFeatureId 1; topo.links[0].dstPortId 0;构造拓扑时最重要的两点一是每个节点的 featureType 和端口号对得上二是连线必须是有向的而且不能成环。出现环路时拓扑校验会失败日志一般会打印“cycle detected”。3.4 参数下发与效果回调控制端怎么找到节点节点跑通之后下一步是让上层能动态调参。feature2 里一般会暴露一个属性接口控制端通过这个接口把参数传进去。常见的做法是定义一个 request 的 private data 结构把你的参数比如美颜等级、磨皮半径填进去然后随 request 一起下发。我在实现时通常会预留一个SetParameter的方法供外部调用参数存进节点自己的状态对象。ProcessRequest 每次执行时读取状态对象组装成算法需要的参数格式。需要注意的是这个状态对象可能会被多个线程读写下发参数的线程和处理帧的线程不是同一个因此读写的临界区要用锁保护否则会出现某一帧突然很丑的偶发问题——这是典型的数据竞争。回调则用来告诉上层这一帧处理完了把结果 buffer 的句柄和该帧的 timestamp 返回。调用回调时不要在锁里面直接做耗时操作否则会拖慢整个 request 处理线程导致后面的节点全部排队。正确姿势是把回调里要做的事尽量精简成一个通知真正的内容放到外部线程去消化。4. feature2 的配置参数与移植从 demo 到真机用例4.1 端口属性配置和 buffer 池设计端口属性决定这个节点申请什么样的 buffer。你需要关注三个维度尺寸、格式和 buffer 数量。尺寸不一定要和 sensor 输出一致有时为了性能会先用一个较小的中间尺寸做分析再放大到全尺寸输出。格式则要和你的算法输入对齐不匹配就得依赖内部转换这一层转换如果放在 ProcessRequest 里就会吃掉不少耗时。buffer 数量是另一个容易让人犹豫的配置。数量太少上下游都会等 buffer帧率直接掉数量太多内存占用大而且某些硬件模块需要连续的内存碎片化会引发分配失败。经验上多帧类算法比如 3 帧降噪的中间 buffer 至少要比帧数多 2预览类算法用 3 到 4 个来回流转比较稳妥。具体可以压测时再调。4.2 拓扑和用例绑定上层如何选中你的图feature2 的拓扑要绑定到具体的 usecase平台才能在有预览请求时把这条链路跑起来。常见的做法是给某个预览/拍照用例指定默认拓扑 ID根据 ID 找到对应的节点图。从移植角度看替换链路时最怕的是“拓扑描述雷同但语义不同”。比如两个项目都叫预览拓扑一个要求先美颜再降噪另一个要求先降噪再美颜。如果只改了节点连线顺序而没改 buffer 格式就会出现一种别扭情况算法没报错但效果明显不对。遇到这类问题我建议先把节点顺序和格式变化列成一张对照表每次改动拓扑时同时核一遍两列能省下很多盲目排查的时间。4.3 多相机也要走同一张图feature2 的并发与同步双摄或者多摄项目里多个 sensor 的数据会同时进入 feature2。这时你需要考虑拓扑里多个输入节点的时间同步。很多实现里有一个同步节点负责对齐两个输入源的时间戳然后把对齐后的帧交给后端算法。同步判断热门路是以主摄时间为基准从副摄的环形缓冲里找时间戳最接近的一帧。如果两帧时间差超过阈值就丢弃或等待。feature2 下的同步节点本质上也是一个 feature它的端口比较多配置时要显式说明“哪个端口是主、哪个端口是辅”否则对齐逻辑会拿错帧。常见现象是虚化算法出来的边缘忽好忽坏多半就是同步没对齐。4.4 性能评估先看帧耗时分布再谈优化把 feature2 链路跑起来后性能评估不要只看整条链路的平均耗时。因为节点多的时候某些节点是并行处理的平均帧耗时看不出瓶颈位置。我习惯在每个节点的入口和出口各打一个时间戳汇总统计每个节点的耗时占比再结合帧率看整体。统计表大概是这个形式节点耗时ms备注输入源8含 sensor 等待降噪节点4占用 DSP我的美颜节点3CPU 算子编码器节点6含 JPEG 编码如果发现耗时集中在输入等待问题大概率不在算法身上而在 buffer 配置不够导致排队如果耗时集中在某个算子那就得从算法内部优化或者换硬件加速。feature2 的图结构在这里的优势是你可以在不改链路的前提下临时把某个节点设成 bypass马上就能对比出它到底占了多少开销。5. feature2 使用中的常见问题排查与避坑5.1 帧率掉一半查出来是 buffer 数量不足现象预览链路加了自己的节点后帧率从 30 掉到 15CPU 占用并不高但日志里反复出现 buffer 等待超时。原因新节点从池子里申请 buffer 的时机晚持有时间长上游要等它归还 buffer 才能继续填帧等效地把整条链路卡成半速。解决先给中间 buffer 加数量比如从 2 加到 4再观察帧率是否恢复。如果恢复了再逐步往下减直到找到临界值。注意不要一次加太多内存占用会涨。5.2 输出图是绿的或者花的多半是格式没对上现象自己的节点输出后预览画面整体偏色或者出现马赛克一样的噪点但节点内部算法自测没问题。原因输出端口声明的 buffer 格式和你实际写入的数据格式不一致。比如你写入了 NV12但端口属性写的是 YUYV下游按 YUYV 解析自然花掉。解决把端口的 buffer format 改为实际写入的格式。如果算法指定只能输出一种格式而下游必须用另一种格式就在两个节点之间插入一个转换节点或者在端口上配置转换开关让框架帮你做一次内部转换。5.3 回调里直接做重活把整个图卡死现象画面一卡一卡的尤其是多帧连拍时特别明显看起来像内存不够但实测占用很低。原因回调用来处理结果通知的那个函数体里直接做了写文件、拷贝大 buffer、或者上锁等待等其他操作。这个回调运行在框架处理线程上一旦阻塞后续节点全部排队。解决把回调函数体重构成轻量级只塞一个 event 到队列里立刻返回。具体耗时的目标如写 jpg、上传、统计放到另外的工作线程里消费 event。5.4 改了拓扑配置却不生效现象在配置里新增了一条边重新编译跑起来效果还是老样子打印拓扑信息也显示新节点在但实际路径根本没走。原因usecase 选择拓扑的逻辑是按某个条件匹配的比如按相机 ID、按场景模式。你改的是默认拓扑而运行时的实际场景走的是另一条拓扑。解决在 feature2 初始化的日志里确认当前选中的拓扑 ID 和节点列表。如果确定没选中你改的那条去查用例与拓扑的映射表把新配置挂到正确的场景上。5.5 玄学崩溃不同帧率下偶发 crash现象跑长时间压力测试几分钟到几十分钟不定期崩溃崩溃栈在 buffer release 附近抓 log 稳定复现不了。原因某个分支路径里没有遵守引用计数规则提前把 buffer 释放了另一条路径还在用。解决把所有对 buffer 的获取和释放都打上断点配合 log 比对每一帧的引用计数变化。重点检查异常分支比如算法判断“当前帧不处理”时有没有把原本该 hold 住的引用清掉。6. 进阶用 feature2 做动态降级与效果优先级控制到了产品阶段feature2 除了跑通链路还要处理“某些时刻性能不够用”的问题。这时候我会用拓扑里的动态切边来做降级。比如平时走“美颜 降噪 超分”三节点当检测到温升或者帧率掉到阈值以下就切到“降噪 美颜”两节点甚至只保留降噪。切换时只需要在运行时重新提交一组连线不需要改传感器配置。实现动态降级前先给节点排好优先级谁可以丢、谁不能丢。比如降噪关系到基本画质美颜是效果卖点超分是加分项。我会把降级的优先级从低到高设为“超分 → 美颜 → 降噪”这样即便切到最简链路出图也不至于没法看。每一步降级后要重新评估一次帧耗时确保没有反向波动。效果优先级控制还有一个常见场景3A 和效果节点的竞争。某些算法会修改曝光或增益如果和 3A 抢控制权画面会闪。我的做法是在 feature2 节点里做一个“效果预算”上限超过预算就不允许覆盖 3A 参数这样算是一种软约束。调试这类问题时我会同时打印 3A 的最终曝光值和算法的期望曝光值比对看是谁赢了。最后把个人习惯总结一下。我每次新接手一个 feature2 相关项目会先做三件事读一遍当前 usecase 的拓扑定义、打印一次运行时拓扑、跑一个最小链路的 demo把这三件事做完再往正式链路上加东西。凭经验直接改大拓扑往往会在无关的环节浪费大量时间。这个习惯帮我避开了不少“改半天发现没走到那一条链路”的尴尬希望帮到你。本文还有配套的精品资源点击获取