第 12 章讲了 Usecase。Usecase 决定"当前是预览还是拍照"——选好之后,整个 Session 期间的 Pipeline 拓扑就固定了。
但有个问题:如果 OEM 想给拍照加个"美颜"功能呢? 按传统做法,得写一个新的 Usecase,把预览 + 美颜 + JPEG 的 Pipeline 全部重新配置一遍。每加一个新功能就要新写一个 Usecase。如果有 5 个功能(美颜、HDR、夜景、虚化、超分),所有可能的组合就有几十种——这是"组合爆炸"。
Feature2 解决了这个问题——它把每个功能做成独立的"插件",然后像搭积木一样组合。这章用最简单的例子开始,从 0 到 1 讲清楚 Feature2 到底是个什么东西。
不用 Feature2(传统 Usecase):
UsecaseDefault 创建一条 Pipeline: Sensor → IFE → IPE → Display
完事。
用 Feature2:
也一样。简单场景不需要 Feature2。
不用 Feature2:
UsecaseDefault 创建两条 Pipeline:
RT Pipeline: Sensor → IFE → IPE → Display
Offline Pipeline: IPE → JPEG Node
用 Feature2:
也一样。基础拍照不需要 Feature2。
到这里,区别出现了。
不用 Feature2:
得写一个新的 UsecaseHDR。
在 UsecaseHDR 里重新配所有 Pipeline:
RT Pipeline: Sensor → IFE → IPE → Display
ShortExposure Pipeline: Sensor → IFE → IPE
LongExposure Pipeline: Sensor → IFE → IPE
HDRMerge Pipeline: IPE → Merge → IPE
JPEG Pipeline: IPE → JPEG Node
五个 Pipeline,全部在 UsecaseHDR 里写死。
下次要加"Bokeh"功能?再写一个 UsecaseBokeh。
下次要"HDR + Bokeh"组合?再写一个 UsecaseHDRBokeh。
用 Feature2:
写三个独立的 Feature(每个只管自己的事):
① Preview Feature: "我负责输出预览画面"
② HDR Feature: "我负责拿两帧 YUV,融合成 HDR"
③ JPEG Feature: "我负责把 YUV 编码成 JPEG"
然后配一个 Feature Graph,把它们串起来:
Preview → HDR → JPEG → App
下次要加 Bokeh?
写一个 Bokeh Feature,Graph 改成: Preview → Bokeh → JPEG
下次要 HDR + Bokeh?
Graph 改成: Preview → HDR → Bokeh → JPEG
不写新代码!只改 Graph 配置!
这就是 Feature2 的核心价值:功能 = 独立插件,Graph = 插件的连接方式。加新功能 = 写一个新插件 + 改 Graph 配置。不用重写整个 Usecase。
不急着看代码结构。先用只做预览这个最简场景,理解 Feature2 涉及的几个角色。
假设 App 调了 createCaptureSession([previewSurface]),并且设备配了 Feature2。
和以前一样,UsecaseSelector 选了 UsecaseDefault。
但 UsecaseDefault 内部启用了 Feature2。
Feature Graph Manager 说: "这个请求只有一个 Preview Surface,走 DefaultPreview Graph"
DefaultPreview Graph 里面只有一个 Feature: Preview Feature
FRO = Feature Request Object
每个请求进来,Graph Manager 为它创建一个 FRO。
FRO 就是这个请求在 Feature 处理过程中的"身份证"——
记录了"现在处理到哪一步了"、"还缺什么数据"。
对于这个简单的预览请求:
FRO 创建 → Preview Feature 处理 → 输出 YUV → 显示 → FRO 完成
Preview Feature 说: "我要从 Sensor 拿 RAW 数据"
→ Feature Graph Manager 查看谁连到 Preview Feature 的输入端
→ 发现它是 Graph 的第一个 Feature(外部输入来自 Sensor)
→ Sensor 数据一到 → Preview Feature 开始处理
→ 输出 Preview YUV → 送到 Display
现在 App 重建了 Session,加了 JPEG Surface。
DefaultSnapshot Graph:
Preview Feature → JPEG Feature
Preview Feature 的输出:
- 一路 YUV → Display(和之前一样)
- 一路 YUV → JPEG Feature(新增的)
① Feature Graph Manager 创建 FRO
② Graph Manager 从 Graph 的末端(Sink)开始问:
→ JPEG Feature,你需要什么?
→ JPEG Feature: "我需要一帧 YUV"
③ Graph Manager 沿着 Graph 往回找:
→ 谁连到 JPEG Feature 的输入端?
→ 是 Preview Feature
④ Graph Manager 问 Preview Feature:
→ "能不能输出一帧 YUV 给 JPEG?"
→ Preview Feature: "可以,Sensor 数据到了我就输出"
⑤ Sensor 数据到了:
→ Preview Feature 处理 → 输出 YUV 给 Display(预览)
→ 同时输出 YUV 给 JPEG Feature → JPEG 编码 → 回调 App
关键理解:Graph Manager 不是"让所有 Feature 一起跑",而是从 Graph 的末端往前逐个问"你需要什么",直到所有依赖都满足。
在 Preview 和 JPEG 之间插入 HDR Feature:
HDRCapture Graph:
Preview Feature → HDR Feature → JPEG Feature
↑
(需要两路输入: 短曝光 + 长曝光)
处理流程比之前多一步:
① Graph Manager 从末端问: JPEG → "我需要 YUV"
② 往前找到 HDR Feature
③ HDR Feature: "我需要短曝光 YUV 和长曝光 YUV"
④ 往前找到 Preview Feature
⑤ Preview Feature: "我可以输出两路:
- 正常曝光 → Display (预览)
- 短曝光 RDI → HDR Feature
- 长曝光 RDI → HDR Feature"
⑥ HDR Feature 拿到两路输入 → 融合 → 输出 HDR YUV 给 JPEG
⑦ JPEG 编码 → 回调 App
HDR Feature 对 Preview 和 JPEG 来说是透明的——Preview 不知道 HDR 在它后面,JPEG 不知道 HDR 在它前面。每个 Feature 只关心"谁给我数据"和"我给谁数据"。这就是"插件"的含义。
上面出现了好几个新名词。用一个类比来理解它们的关系:
你(App)去餐厅点了一份"HDR 套餐"。
Feature Graph = 套餐的配方
"HDR 套餐的配方: 前菜(Preview) → 主菜(HDR) → 甜点(JPEG)"
Feature = 每道菜的厨师
Preview 厨师: 只管做好前菜,输出给下一个环节
HDR 厨师: 收到前菜,加工成主菜,输出给甜点
JPEG 厨师: 收到主菜,做成甜点,端给客人
Feature Graph Manager = 餐厅经理
拿到订单 → 看配方 → 从后往前问每个厨师"你需要什么原料?"
→ 确保原料齐了 → 让厨师开始做
Feature Graph Selector = 菜单推荐系统
光照充足 → 推荐 HDR 套餐
暗光 → 推荐夜景套餐
FRO = 订单跟踪单
每张订单一个跟踪单
记录"这道菜做到了第几步"
做完一道 → 划掉 → 下一道
Feature2 的配置分成两部分:代码侧(定义 Feature 是什么)和 数据侧(定义 Graph 怎么连)。
// 你的 Feature(比如 HDR),从高通基类继承
class MyHDRFeature : public ChiFeature2Base {
// ① 准备阶段: Feature Graph Manager 问你"这个请求你要怎么处理?"
CDKResult DoPrepareRequest(FRO* pFRO) {
// 告诉框架: 我需要 2 路 YUV 输入, 1 路 YUV 输出
return CDKResultSuccess;
}
// ② 执行阶段: 框架说"你的输入到了,开始干活"
CDKResult OnExecuteProcessRequest(FRO* pFRO) {
// 这里就是你的 HDR 融合算法
YUVBuffer* pShort = GetInput(0); // 短曝
YUVBuffer* pLong = GetInput(1); // 长曝
YUVBuffer* pOut = GetOutput(0);
HDRMerge(pShort, pLong, pOut); // ← 核心算法
return CDKResultSuccess;
}
// ③ 依赖声明: 框架问你"你现在需要什么?"
CDKResult OnPopulateDependency(FRO* pFRO) {
// 告诉框架: 我需要等上游 Feature 给我两路 YUV
ReportInputDependency(INPUT_PORT_0); // 短曝
ReportInputDependency(INPUT_PORT_1); // 长曝
return CDKResultSuccess;
}
};
💡 以前(xxx)你必须自己管理 buffer 引用计数、request 状态、flush 逻辑。在 xxx 上,ChiFeature2Base 帮你管了所有这些。你只写算法。代码量从 ~2000 行降到 ~500 行。
// 告诉框架: "我叫 HDRMerge, 有 1 个处理阶段, 需要 1 个 Session"
ChiFeature2Descriptor g_hdrFeatureDesc = {
.featureId = FEATURE_HDR,
.pFeatureName = "HDRMerge",
.numStages = 1,
.pStages = hdrStageDesc, // 指向 Stage 描述数组
.numSessions = 1,
.pSession = &hdrSessionDesc, // 指向 Session 描述
};
// 定义 HDRCapture Graph: Preview → HDRMerge → JPEG
ChiFeature2GraphDesc g_hdrGraph = {
.pFeatureGraphName = "HDRCapture",
.numFeatureInstances = 3,
.pFeatureInstances = { &previewInst, &hdrInst, &jpegInst },
// 外部输入: Sensor → Preview Feature 的输入端口 0
.numExtSrcLinks = 1,
.pExtSrcLinks = { { "Sensor", "Preview", 0 } },
// 内部链接:
.numInternalLinks = 2,
.pInternalLinks = {
{ "Preview", 0, "HDRMerge", 0 }, // Preview 输出0 → HDR 输入0
{ "HDRMerge", 0, "JPEG", 0 }, // HDR 输出0 → JPEG 输入0
},
// 外部输出: JPEG → App
.numExtSinkLinks = 1,
.pExtSinkLinks = { { "JPEG", 0, "AppSink" } },
};
改 Graph 只需要改这个 Descriptor,不需要动 Feature 代码。
一句话: Feature2 = 把相机功能拆成独立插件, 通过 Graph 像搭积木一样组合
不用 Feature2: 加功能 = 写新 Usecase = 重配所有 Pipeline
用 Feature2: 加功能 = 写新 Feature + 改 Graph 配置
五个角色(用餐厅类比记住):
Feature = 厨师(只管自己那道菜)
Feature Graph = 套餐配方(菜怎么组合)
Feature Graph Manager = 经理(拿着配方, 确保原料齐了才开始做)
Feature Graph Selector= 菜单推荐(光照好 → HDR, 暗光 → 夜景)
FRO = 订单跟踪单(做到第几步了)
核心机制:
Graph Manager 从 Graph 末端开始, 逐个往前问"你需要什么?"
所有依赖满足后 → Feature 执行 → 输出给下一个 Feature
chifeature2types.h,对比本章的 ChiFeature2Descriptor 结构。adb logcat -d | grep -iE "FeatureGraph|FGM|FGS"