简介:面向Android相机底层开发与图像处理方向的技术人员,这份PDF围绕高通CAMX架构中的ChiFeature2框架展开,系统梳理了Feature2涉及的关键结构体、端口缓冲信息、锚点帧选择数据、回调接口及多相机资源管理等核心数据结构,并以图形化关系图呈现庞大而复杂的调用与依赖脉络。内容覆盖请求创建、FeatureGraph管理、数据流处理到最终输出,兼顾单摄与多摄场景下的实时视频流、多帧处理、HDR合成和特征提取需求,也涉及锚点同步、帧选择与实时序列化等模块。压缩包仅含1个PDF文件,约82KB,便于离线检索与查阅。目前已有439人学习,适合具备嵌入式或相机模块经验、希望深入理解Android底层相机框架并优化图像质量与处理效率的开发者,可借助结构体关系图与接口定义理清ChiFeature2各组件的交互方式,为自定义相机应用、增强现实或计算机视觉项目提供底层参考。
1. 从一堆结构体到一张图:ChiFeature2 到底在管什么
第一次打开 CAMX 里 ChiFeature2 的头文件,多数人的反应是"这不可能读完"。ChiFeature2Base、ChiFeature2GraphDesc、ChiFeature2UsecaseRequestObject、ChiFeature2StageDescriptor、ChiFeature2PrunableVariant……几百个结构体层层嵌套,指针套指针,回调里再挂回调。但真正做过多摄并发或者 HDR 合成的人会发现,它们其实只回答三个问题:这一帧要经过哪些特征节点、每个节点吃哪块 buffer、跑完之后谁来回收。把这三点抽出来,整棵类型树就塌缩成一张有向图,这也是 Feature2 里 FeatureGraph 这个名字的由来。
它解决的是高通相机 HAL 从早期 Feature1 单管线模型向多相机、多帧、可裁剪管线演进时暴露出来的资源管理难题。适合谁读?写过 Camera HAL、调过 multi-camera 同步、被 ZSL 或离线重处理卡过吞吐的工程师。新手直接啃会很吃力,建议先把 Request、Port、Metadata 三件套弄清楚再回头。
2. ChiFeature2 数据结构分层与 FeatureGraph 建模原理
2.1 类型树的四层划分
把输入里那一长串结构体按作用归类,基本落在四层:描述层、请求层、运行层、回调层。
| 层级 | 代表结构体 | 生命周期 | 谁创建 |
|---|---|---|---|
| 描述层 | ChiFeature2GraphDesc/ChiFeature2StageDescriptor/ChiFeature2PipelineDescriptor | 进程级,静态 | FeatureGraphManager |
| 请求层 | ChiFeature2UsecaseRequestObject/ChiFeature2RequestObject/ChiFeature2RequestMap | 每帧 | Feature2 框架 |
| 运行层 | ChiFeature2Graph/FeatureGraphManagerSessionData/ChiTargetBufferManager | 会话级 | Usecase |
| 回调层 | ChiFeature2GraphManagerCallbacks/ChiFeature2GraphCallbackData | 会话级,函数指针 | OEM/Usecase |
描述层是"图纸",它不参与每帧分配。ChiFeature2GraphDesc里挂pStages、numStages、pFeatureInstances、numFeatureInstances,描述的是这张图有哪些特征、分成几段、实例属性怎么配。请求层才是"零件",每来一帧 captureRequest 就生成一个ChiFeature2UsecaseRequestObject,把物理相机、输入流、输出流、metadata 引用一起打包。
运行层是把图纸实例化之后的活体,FeatureGraphManagerSessionData持有pFeatureGraph、ppFeatureGraphDesc、m_pSessionSettings,负责把描述层翻译成真正的ChiFeature2Graph。回调层不存数据,只存函数指针和私有上下文,让 OEM 能在 deserialize、serialize、节点处理这几个时刻插自己的逻辑。
2.2 FeatureGraph 的节点、端口与依赖
ChiFeature2Graph是由ChiFeature2GraphNode组成的,节点之间靠ChiFeature2GraphLinkData连接。每个节点有输入端口和输出端口,ChiFeature2PortDescriptor描述端口名、方向、是否 sink,ChiFeature2DependencyConfigDescriptor则描述端口之间的依赖关系和批次索引。
理解依赖的关键是三个字段:requestIndex、batchIndex、dependencyIndex。
requestIndex:这个依赖属于第几次请求,多帧处理时 ZSL 会回吐历史帧,靠它区分。batchIndex:ChiFeature2DependencyBatch里的批次号,一个批次内的依赖可以并行触发。dependencyIndex:同一批次内的序号,用于精确定位是哪一路输入。
ChiFeature2PrunableVariant是 Feature2 里很妙的一个设计。图上每条链路可以挂若干可裁剪变体,字段里有pruneVariantType、pruneVariantMask、groupId、prunableVariant。运行时根据当前场景(比如 bokeh 模式没开、QR 扫描没触发)把不需要的链路整条剪掉,避免白白跑一遍 GPU。ChiFeature2PruneRule进一步给出 minFrameCountForAnchor、logicalRequestPruneMask 这类规则,配合ChiFeature2GraphSelectorOEM做选择。
2.3 用 Graphviz 把结构体关系画出来
手动读完几百个结构体不现实,常见做法是写脚本从预处理后的头文件里抽字段,生成 DOT,再渲染成图。下面是一个可复用的抽取脚本:
# parse_chi_structs.py # 从预处理过的 CAMX 头文件里抽取结构体之间的"包含"关系 import re, sys # 匹配 struct Foo { ... }; 整体 STRUCT_RE = re.compile(r'struct\s+(Chi\w+)\s*\{(.*?)\}\s*;', re.S) # 抓成员:类型名 + 变量名,只保留 Chi 开头的类型 FIELD_RE = re.compile(r'\b(Chi\w*)\s+[\w\*]+\s*;') def parse(path): src = open(path, encoding='utf-8', errors='ignore').read() edges, nodes = [], set() for name, body in STRUCT_RE.findall(src): nodes.add(name) for ftype in FIELD_RE.findall(body): if ftype != name: edges.append((name, ftype)) nodes.add(ftype) return nodes, edges def to_dot(nodes, edges, out): with open(out, 'w', encoding='utf-8') as f: f.write('digraph ChiFeature2 {\n rankdir=LR;\n node [shape=box,fontsize=10];\n') for n in sorted(nodes): f.write(f' "{n}";\n') for a, b in set(edges): f.write(f' "{a}" -> "{b}";\n') # 有向边表示包含关系 f.write('}\n') if __name__ == '__main__': n, e = parse(sys.argv[1]) to_dot(n, e, sys.argv[2]) # 输出 .dot print(f'nodes={len(n)} edges={len(e)}')逻辑说明:先按最外层struct ... { ... };切块,正则用了re.S让.跨行匹配,否则嵌套结构体一多就会切错。FIELD_RE只抓Chi开头的类型,把uint32_t、void*这类基础类型过滤掉,不然图会糊成一片。
参数说明:第一个参数是预处理后的头文件路径,建议先跑gcc -E -P -I<camx_inc> feature2.h -o feature2.i展开宏,否则带宏声明的成员会漏;第二个参数是输出 dot 文件。生成之后直接渲染:
# 大图渲染成 SVG,方便浏览器里缩放着看 dot -Tsvg chi.dot -o chi.svg # 只关心某个子图先看局部,避免几万个节点挤在一起 dot -Tsvg -Gdpi=150 -Kfdp chi.dot -o chi_fdp.svg节点数上千时dot布局会非常慢,换成sfdp或fdp通常几十秒内出图。如果只想看请求层的调用链,可以在to_dot里加白名单keep = {'ChiFeature2RequestObject', ...},把不相关的边剪掉。
2.4 从结构体走向运行时对象的时机
结构体只是静态定义,真正决定行为的时机有三个。
第一是图创建时刻。ChiFeature2GraphCreateInputInfo携带pFeatureGraphName、pStreamDesc、pFeatureDependencies、maxDependencyPorts,FeatureGraphManager 在这一步校验依赖端口数量是否够用。
第二是会话建立时刻。FeatureGraphManagerSessionData里m_numInternalLinks、m_pEnabledFeatures、m_pSessionDescriptor定型,此时决定启用哪些特征、内链怎么连。
第三是每帧请求时刻。ChiFeature2UsecaseRequestObject的m_flags、m_requestState、captureType被打上,然后ChiFeature2RequestObject走m_pPortMap把每个 port 映射到具体 buffer。这三步对应"图纸→生产线→零件",任何一步参数填错,后面全是空指针。
提示:调试 Feature2 时先从
ChiFeature2RequestOutputInfo入手反查,它到ChiFeature2RequestObject的链路最短,比从头翻GraphDesc快得多。
3. 数据结构图形化工具的落地实现与参数调优
3.1 交互式浏览:把 DOT 喂给浏览器
静态 SVG 一旦上千节点就难查,我给的做法是转成 JSON 再配前端渲染,或者直接用vis.js的 network。下面把 DOT 转成 JSON 的小脚本:
# dot_to_json.py —— 把 dot 转成前端可加载的节点/边 JSON import json, re, sys def load_dot(path): nodes, edges = {}, [] with open(path, encoding='utf-8') as f: for line in f: line = line.strip() m = re.match(r'"(\w+)"\s*->\s*"(\w+)"', line) if m: edges.append({'from': m.group(1), 'to': m.group(2)}) continue m = re.match(r'"(\w+)";', line) if m: n = m.group(1) # 按前缀分组,前端据此上色 group = 'req' if 'Request' in n else ('graph' if 'Graph' in n else 'base') nodes[n] = {'id': n, 'group': group} return list(nodes.values()), edges if __name__ == '__main__': n, e = load_dot(sys.argv[1]) json.dump({'nodes': n, 'edges': e}, open(sys.argv[2], 'w'), ensure_ascii=False) print(f'nodes={len(n)} edges={len(e)}')分组字段group是给前端上色用的,请求层、图形层、基础层各一色,肉眼看的时候一眼能分辨层级。边去重很重要,同一个类型被多个结构体引用时会在 DOT 里出现多条同名边。
3.2 大图布局参数对比
不同布局引擎对这类结构体图的适用度差别很大,实测下来:
| 引擎 | 适用节点量 | 特点 | 建议 |
|---|---|---|---|
dot | < 500 | 层次清晰,交叉少 | 画子图 |
fdp | 500 ~ 3000 | 力导向,速度可接受 | 全量图首选 |
sfdp | > 3000 | 最快,边会糊 | 大图预览 |
neato | 中等 | 类似力导向 | 少用 |
命令行示例:
# sfdp 处理上万边的大图,加 overlap 避免节点重叠 sfdp -Tsvg -Goverlap=prism -Gsize=30,30 chi.dot -o chi_big.svg-Goverlap=prism让引擎在布局后做一次去重叠处理,代价是稍慢,但大图上节点不再糊成一团。-Gsize控制画布尺寸,太大浏览器会卡,一般 30 英寸够用。
3.3 输出可交互 HTML
想省掉前端工程,可以直接用pygraphviz生成交互式 HTML,或者套用成熟的 vis-network 模板:
# gen_html.py —— 生成内嵌 JSON 的单文件 HTML import json TEMPLATE = """<!DOCTYPE html><html><head><meta charset="utf-8"> <script src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script> <style>#net{width:100vw;height:100vh;}</style></head> <body><div id="net"></div><script> var data = %s; var nodes = new vis.DataSet(data.nodes); var edges = new vis.DataSet(data.edges); var container = document.getElementById('net'); new vis.Network(container, {nodes:nodes, edges:edges}, { physics:{stabilization:{iterations:200}}, // 布局稳定迭代次数 nodes:{shape:'box', font:{size:12}}, edges:{arrows:'to', smooth:false} }); </script></body></html>""" data = json.load(open('chi.json', encoding='utf-8')) open('chi_view.html', 'w', encoding='utf-8').write(TEMPLATE % json.dumps(data)) print('html written')physics.stabilization.iterations是布局精度的核心参数,数值越大布局越稳、加载越慢;200 是图规模几千时的折中点。smooth:false关掉曲线边,能省不少渲染开销,大图必开。
提示:把生成好的 HTML 和
chi.dot一起放进版本库,头文件一改就重跑脚本 diff,能第一时间发现结构体字段被增删。
4. 多相机实时链路与资源管理实战
4.1 多相机图与单相机图的差异点
ChiFeature2GraphDesc里的isMultiCameraGraph是分水岭。置真时,ChiFeature2UsecaseRequestObject会用到physicalCameraId、m_pPhysicalCameraId,ChiFeature2UsecaseRequestObjectExtSrcStreamData也要按perCamera展开。单相机图不会走ChiFeature2AnchorSync,多相机图一旦启用同步,ChiFeature2AnchorSyncData的anchorFrameIdx、numberOfFrames就会决定是对齐还是丢帧。
常见任务的两条路径:
- 实时预览多摄:
ChiFeature2RealTimeMCX负责,m_pHALRequestSem控制提交节流。 - 离线重处理:
ChiFeature2MCReprocessRT接在实时链路后面,依赖ChiFeature2ZSLData里的lastFrameNumber回捞历史帧。
m_isVideoStreamEnabled、m_isRTCapInputYUVStreamEnabled这两个标志位直接决定以上哪条路径被实例化。
4.2 目标 buffer 与 metadata 池的分配
ChiTargetBufferManagerCreateData是这块的核心,字段含义得一个个对:
| 字段 | 含义 | 典型取值 |
|---|---|---|
pTargetBufferName | buffer 名,对应流名 | "TBM_Preview" |
numOfMetadataBuffers | metadata buffer 总数 | 视帧率与延迟定 |
minMetaBufferCount/maxMetaBufferCount | 上下水位 | 2 / 8 |
pMetadataManager | 元数据管理器句柄 | 外部传入 |
numOfInternalStreamBuffers | 内部流 buffer 数 | 与内链端口数对齐 |
numOfExternalStreamBuffers | 外部流 buffer 数 | 与 sink 端口数对齐 |
isChiFenceEnabled | 是否用 fence 同步 | 通常 true |
isChiGrallocBufferUsed | 是否走 gralloc | 依平台 |
minMetaBufferCount设太小会在高帧率下频繁等 buffer,设太大会撑爆内存。常见做法是按"最大并发请求数 + 2"来配,用ChiFeature2BufferMetadataInfo里的bufferMetaRefCnt观察引用计数,跑到高位就说明缓冲欠配。
metadata 客户端由m_genericMetadataClientId和metadataClientId标识,ChiFeature2MetadataInfo里的pMetadata、pMCCResult是跨节点传递的关键。ChiFeature2FrameInfoStatsRegeneration里的local3ADebugData、tintlessStatsData只在调试时开,线上常关掉避免无谓拷贝。
4.3 用 API 查询图与实例信息
调试阶段先查后跑,别急着改代码:
// 查询 Feature2 能力与已注册的图描述数量 ChiFeature2QueryInfo queryInfo = {}; ChiFeature2GraphManagerCallbacks callbacks = {}; UINT32 numCaps = 0; ChiFeature2Capability* pCaps = nullptr; queryInfo.numCaps = &numCaps; queryInfo.ppCapabilities = &pCaps; // FeatureGraphManager 通过 callbacks 暴露能力查询入口 callbacks.pQueryCaps(&queryInfo); // numCaps 返回可用能力条目数,pCaps 指向能力数组,调用方只读关键点在numCaps与ppCapabilities是出参,调用前必须清零,否则返回值会是野指针。ChiFeature2Descriptor里的pFeatureDesc、numFeatureInstances可以进一步查看每个特征被实例化几次。
再看请求对象创建的输入结构:
// 构造请求对象时最容易被忽略的几个字段 ChiFeature2RequestObjectCreateInputInfo info = {}; info.pTargetName = "Snapshot"; // 目标名,影响流选择 info.instanceId = 0; // 特征实例,多实例时不能写死 info.pGraphPrivateData = nullptr; // OEM 私有数据,可为空 info.m_frameIDMeta = frameNumber; // 帧号,用于 offset 对齐 info.m_lastZSLFrameNumber = lastZSL; // ZSL 回捞边界,离线处理必填 ChiFeature2RequestObject* pReq = nullptr; g_pFeature2Base->pCreateFeature2(&info, &pReq);m_lastZSLFrameNumber填错是离线处理的经典坑:图能建、能跑,但拿回来的永远是最近一帧,因为边界值被当成了无效范围直接退化。instanceId在多相机多实例场景必须按物理相机区分,写死 0 会导致所有请求挤到同一个实例上。
4.4 提交作业与 fence 同步
ChiFeature2RealTime里m_hSubmissionJob、m_pHALRequestSem、m_maxFeatureExecutionTime三个字段配合着看。m_maxFeatureExecutionTime是软超时门限,超过这个时间还没跑完,框架会打印超时告警但未必中止任务。m_hSubmissionJob对应 job 句柄,下游依赖它做序列化提交。
Fence 由isChiFenceEnabled开关,配合Feature2BufferOptionsData里的pPortBufferStatus使用。常见失败模式:fence 未置位就提前读 buffer,表现为随机花屏,日志里通常能看到bufferErrorPresent为真。这时候先查ChiFeature2PortBufferMetadataInfo里的isValidForSnapshot和bufferErrorPresent,比逐节点加打印效率高得多。
5. 裁剪规则与同步调试的几个关键技巧
5.1 PruneVariant 裁剪链路的验证
裁剪配错不会报错,只会静默丢链路,所以必须主动验证。用ChiFeature2PrunableVariant的pruneVariantMask和pruneGroup对齐后,加一条查询确认当前生效的变体:
// 打印当前图中每个可裁剪组实际选中的变体 for (UINT32 i = 0; i < pGraphDesc->numStages; ++i) { ChiFeature2StageDescriptor* pStage = &pGraphDesc->pStages[i]; for (UINT32 j = 0; j < pStage->numPorts; ++j) { ChiFeature2PortDescriptor* pPort = &pStage->pPorts[j]; // pruneVariantMask 为 0 表示该端口无可裁剪变体 CAMX_LOG_INFO(CamxLogGroupChi, "stage=%s port=%s mask=0x%x pruneGroup=%u", pStage->pStageName, pPort->pPortName, pPort->pPruneVariantMask, pPort->pruneGroup); } }mask与groupId一起决定哪条变体被选中,mask为 0 的端口代表该链路无条件保留。日志里如果看到预期该保留的端口 mask 为 0,说明规则写反了。
5.2 AnchorSync 对齐问题定位
多相机同步出问题时,先看ChiFeature2AnchorSyncData的anchorFrameIdx、numberOfFrames和ChiFeature2AnchorFrameSelectionData里的anchorFrameSelectionMode、max、min、numImagesAllowedAsAnchor。同步选帧逻辑在ChiFeature2AnchorPickInputInfo里,focusValue、histogram、minHistrogramBin、maxHistrogramBin参与打分。
排查顺序建议按"输入是否齐 → 锚点是否选出 → 对齐是否生效"来。输入不齐时先看numInputDependency与实际到达数是否一致;锚点选不出通常是打分阈值太严,isFWRaw16CbStream这类回调流未开也会卡住;对齐失效则查ChiFeature2AnchorSyncData的numRealtimeLogicalOutputs有没有超出实际逻辑相机数。
5.3 参数速查与常见误配对照
| 参数 | 误配后果 | 建议做法 |
|---|---|---|
minMetaBufferCount | 高帧率下等 buffer 掉帧 | 按最大并发 + 2 配 |
m_lastZSLFrameNumber | 离线只拿最新帧 | 按用户选择的快门帧填 |
instanceId写死 | 多实例请求挤一个 | 按物理相机区分 |
isChiFenceEnabled关 | 随机花屏 | 线上保持开启 |
pruneVariantMask | 链路静默丢失 | 用日志逐端口核对 |
m_hasFWRaw16CbStream | 回调流不可用 | 与流配置对齐 |
一个实操要点:把第 3 章生成的 HTML 图和第 2 章的抽取脚本一起用,改完裁剪规则后重跑脚本,diff 一下 DOT 就能看出哪些边被剪了。这类结构体图工具最大的价值不在"看一眼",而在于把每次改动的图结构变化变成可回溯的版本差异,出问题时能对着上一版图快速定位是哪个结构体字段被改动了。
本文还有配套的精品资源,点击获取