← 返回课程

Feature2框架

Camera全栈开发(Qcom Camx) 第 18 / 30 节

第 16 章:Feature2 框架 — 灵活的功能组合


本章导读

第 12 章讲了 Usecase。Usecase 决定"当前是预览还是拍照"——选好之后,整个 Session 期间的 Pipeline 拓扑就固定了。

但有个问题:如果 OEM 想给拍照加个"美颜"功能呢? 按传统做法,得写一个新的 Usecase,把预览 + 美颜 + JPEG 的 Pipeline 全部重新配置一遍。每加一个新功能就要新写一个 Usecase。如果有 5 个功能(美颜、HDR、夜景、虚化、超分),所有可能的组合就有几十种——这是"组合爆炸"。

Feature2 解决了这个问题——它把每个功能做成独立的"插件",然后像搭积木一样组合。这章用最简单的例子开始,从 0 到 1 讲清楚 Feature2 到底是个什么东西。


16.1 不用 Feature2 会怎样?用 Feature2 又怎样?

场景 1:只做预览

不用 Feature2(传统 Usecase):
  UsecaseDefault 创建一条 Pipeline: Sensor → IFE → IPE → Display
  完事。

用 Feature2:
  也一样。简单场景不需要 Feature2。

场景 2:预览 + 拍照(JPEG)

不用 Feature2:
  UsecaseDefault 创建两条 Pipeline:
    RT Pipeline:      Sensor → IFE → IPE → Display
    Offline Pipeline: IPE → JPEG Node

用 Feature2:
  也一样。基础拍照不需要 Feature2。

场景 3:预览 + HDR 拍照(关键!)

到这里,区别出现了。

不用 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。


16.2 用一个最简例子走一遍流程

不急着看代码结构。先用只做预览这个最简场景,理解 Feature2 涉及的几个角色。

假设 App 调了 createCaptureSession([previewSurface]),并且设备配了 Feature2。

第一步:Usecase 收到请求

和以前一样,UsecaseSelector 选了 UsecaseDefault。
但 UsecaseDefault 内部启用了 Feature2。

第二步:Feature Graph Manager 看一眼请求

Feature Graph Manager 说: "这个请求只有一个 Preview Surface,走 DefaultPreview Graph"

DefaultPreview Graph 里面只有一个 Feature: Preview Feature

第三步:Feature Graph Manager 创建 FRO

FRO = Feature Request Object
每个请求进来,Graph Manager 为它创建一个 FRO。
FRO 就是这个请求在 Feature 处理过程中的"身份证"——
记录了"现在处理到哪一步了"、"还缺什么数据"。

对于这个简单的预览请求:
  FRO 创建 → Preview Feature 处理 → 输出 YUV → 显示 → FRO 完成

第四步:Preview Feature 干活

Preview Feature 说: "我要从 Sensor 拿 RAW 数据"
→ Feature Graph Manager 查看谁连到 Preview Feature 的输入端
→ 发现它是 Graph 的第一个 Feature(外部输入来自 Sensor)
→ Sensor 数据一到 → Preview Feature 开始处理
→ 输出 Preview YUV → 送到 Display

16.3 再加一个 JPEG——看 Graph 怎么"扩展"

现在 App 重建了 Session,加了 JPEG Surface。

Graph 从"一个 Feature"变成"两个 Feature 串联"

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 的末端往前逐个问"你需要什么",直到所有依赖都满足。


16.4 再来一个 HDR——看 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 只关心"谁给我数据"和"我给谁数据"。这就是"插件"的含义。


16.5 这个过程里涉及的角色——用"餐厅"类比

上面出现了好几个新名词。用一个类比来理解它们的关系:

你(App)去餐厅点了一份"HDR 套餐"。

Feature Graph = 套餐的配方
  "HDR 套餐的配方: 前菜(Preview) → 主菜(HDR) → 甜点(JPEG)"

Feature = 每道菜的厨师
  Preview 厨师: 只管做好前菜,输出给下一个环节
  HDR 厨师:    收到前菜,加工成主菜,输出给甜点
  JPEG 厨师:   收到主菜,做成甜点,端给客人

Feature Graph Manager = 餐厅经理
  拿到订单 → 看配方 → 从后往前问每个厨师"你需要什么原料?"
  → 确保原料齐了 → 让厨师开始做

Feature Graph Selector = 菜单推荐系统
  光照充足 → 推荐 HDR 套餐
  暗光     → 推荐夜景套餐

FRO = 订单跟踪单
  每张订单一个跟踪单
  记录"这道菜做到了第几步"
  做完一道 → 划掉 → 下一道

16.6 代码长什么样——不要求全懂,先有个印象

Feature2 的配置分成两部分:代码侧(定义 Feature 是什么)和 数据侧(定义 Graph 怎么连)。

代码侧:每个 Feature 只需要覆写 3 个函数

// 你的 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 行。

数据侧:用 Descriptor 描述"我是谁、我需要什么"

// 告诉框架: "我叫 HDRMerge, 有 1 个处理阶段, 需要 1 个 Session"
ChiFeature2Descriptor g_hdrFeatureDesc = {
    .featureId    = FEATURE_HDR,
    .pFeatureName = "HDRMerge",
    .numStages    = 1,
    .pStages      = hdrStageDesc,    // 指向 Stage 描述数组
    .numSessions  = 1,
    .pSession     = &hdrSessionDesc, // 指向 Session 描述
};

Graph 配置:Feature 怎么连起来

// 定义 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 代码。


16.7 本章总结

一句话: Feature2 = 把相机功能拆成独立插件, 通过 Graph 像搭积木一样组合

不用 Feature2: 加功能 = 写新 Usecase = 重配所有 Pipeline
用 Feature2:   加功能 = 写新 Feature + 改 Graph 配置

五个角色(用餐厅类比记住):
  Feature               = 厨师(只管自己那道菜)
  Feature Graph         = 套餐配方(菜怎么组合)
  Feature Graph Manager = 经理(拿着配方, 确保原料齐了才开始做)
  Feature Graph Selector= 菜单推荐(光照好 → HDR, 暗光 → 夜景)
  FRO                   = 订单跟踪单(做到第几步了)

核心机制:
  Graph Manager 从 Graph 末端开始, 逐个往前问"你需要什么?"
  所有依赖满足后 → Feature 执行 → 输出给下一个 Feature

动手验证

  1. 在源码中找 chifeature2types.h,对比本章的 ChiFeature2Descriptor 结构。
  2. 搜索 logcat 中 Feature Graph 选择日志:
    adb logcat -d | grep -iE "FeatureGraph|FGM|FGS"
    
  3. 想一想:如果 OEM 要加一个"黑白滤镜"功能,用 Feature2 怎么做?需要改哪些文件?

常见误解