(一)简介

为什么要用CHI模块?

android开发了相机硬件接口,使OEM可以为用户生产高质量的相机解决方案,camera2 API和HAL3接口的结合提供了足够的灵活性,可以支持大多数的使用案例。

但是当java层负责控制执行流程是,在其他引擎(GPU,CPU,DSP)上的延迟或潜在的低效率处理可能是不可接受的,OEM可以通过绕过HAL3接口并直接修改摄像头驱动程序来更有效的实现自定义的功能。

CHI API是建立在HAL3接口的基础上,HAL3是谷歌围绕camera pipeline精确的每个请求设计的,为了给用户提供一个完整的图像处理控制,但是这个请求是有限的。而CHI旨在提供更加精细的控制,以及可以访问ISP中的处理引擎。这个接口可以让照相和通用的图像处理APP提供低延时的软硬件处理。

什么时候要用到CHI模块?

当camera2/HAL3 接口有如下限制的时候:

  1. 没有接口可以访问ISP里面各个固定功能的引擎,ISP不是固定一块的,是由各个硬件和软件模块组成的,我利用CHI可以调用ISP里面的单独的一个模块
  2. 没有接口可以精确地请求一个处理流程,利用CHI可以控制图像处理的每个节点,从而控制图像的每个处理流程
  3. 用例的定义比较松散,预期的处理可能和想像中不同
  4. 管道深度是基于预期的用例调试的,为了实现一个复杂的处理流程,对于大多数的HAL3相机驱动来说,特别的请求数量太多了。
  5. 需要最少的处理和最小的延迟

CHI模块是什么?

CHI 是为了分离Camera2/HAL3接口,并提供一个完整灵活的图像处理驱动。它可以作为独立的实体进行访问。它能够使用来自sensor或者内存的图像数据(这个惊呆了,不仅仅可以处理来自camera数据,也可以处理来自应用的数据)。

为了让高通的spectra能够和现有的camera2/HAL3接口保持向后兼容,将HAL3驱动转换成了恰当的CHI调用。也就是说HAL3的所有实现,都是通过调用CHI接口实现的。(这一点是和老平台的mm-camera是一样的吧)

CHI提供了一个overrides的接口,允许OEM修改高通默认的camera用例实现(HAL3的实现),不仅仅可以正常的使用Camera2/HAL3的应用程序,同时还可以同时获得CHI提供的灵活性。这些代替的操作(overrides)可以让OEM在任何可获得的引擎上实现各种各样的图像处理,具有低延时,不需要在多个引擎之间协调同步的特点。

CHI的五个关键自定义组件

  1. CHI override:可以对HAL3接口进行补充,为任何一个兼容HAL3的APP提供精细的图像处理流程,精确的引擎选择,以及多帧图像的控制。
  2. CHI pipeline :可以自定义ISP pipeline来处理图像,pipeline由固定功能的ISP功能块(FF-ISP)和拓展的节点(node)组成,这部分可以由xml文件进行配置。当然了直接调用CHI也可以以编程的方式生成pipeline(不过谁会这么傻呢)。
  3. CHI node extensions:它是图像的处理节点,可以进行扩展,提供了方便的hooks来简化CPU,GPU(openCL, openGL ES或Vulkan)或者DSP(OpenDSP,,Qualcomm® Computer Vision SDK, 或自定义编程)
  4. CHI stats overrides:允许重写高通默认的统计算法(包括3A),而无需更改驱动程序。外部统计算法可以存储私有的数据,自定义的节点也可以访问这些数据。
  5. CHI sensor XML:通过xml配置不同的硬件,包括图像传感器,马达,EEPROM,闪光灯等组件。

重要术语

  1. Use case:用例,是camera pipeline的特定配置,为某个特定的场景使用。比如说2K的预览的同时可以20MP的抓拍,这就是一个use case。同一个用例可以有多条camera pipeline。在底层硬件的限制类指定任何可能的用例,上层不需要修改驱动程序就可以适应各种场景。

  2. topology:拓扑图,由一系列处理节点和一组链接组成。

  3. Engine:引擎,可用于处理数据的硬件。 如Spectra ISP,Snapdragon CPU,Adreno和DSP。

  4. Node:节点,是camera pipeline的逻辑功能块,运行在单个引擎上。节点连起来就是拓扑图。

  5. Pipeline:管道, 包含了一个拓扑图。驱动程序利用pipeline来了解说用到的引擎,以及输出处理的流程。

  6. session:会话,具有1-n个管道。会话管理所有的硬件资源,以及连接到它的所有管道中的未决请求。支持多个并行,每个会话在多个请求中都保持自己的状态,不受其他会话的影响。停用会话,才释放分配给它的所有资源。

  7. request:请求,触发管道处理数据的动作。 这可以是处理从图像传感器提取的数据帧或处理来自内存的数据的请求。 驱动程序必须为每个请求返回结果。HAL3和CHI请求是分开管理的。(这两个请求是不一样的)

  8. sub-request: 子请求,将单个HAL3请求分解为多个CHI请求的操作。子请求的结构不会在驱动程序(CHI)外部反馈,而是将它们合并成一个结果,该结果与原始请求像对应。子请求用于启用 Camera2功能,如HDR,请求多个不同曝光时间的图像,最终产生单个图像。再比如多帧图像后处理,将多个图像合并以创建单个图像输出。子请求是用例管理的一部分,可以在CHI override模块使用。

  9. Stream:流,具有相同大小和格式的缓冲区序列,用于处理图像数据。可以将多个不同类型的流指定为camera pipeline的输入和输出。这组流是定义Camera2 / HAL3用例的关键组件。

  10. Per-session settings:单个会话设置,在会话生存期内影响处理管道的设置。会话开始后就无法更改这些设置。例如,允许图像防抖。

  11. Per-request settings:单个请求的设置。例如,手动曝光值。

  12. Statistics: 包括3A在内的算法。这些领域特定的算法被视为CHI API的专用部分。

  13. Live or realtime stream:配置来自图像传感器的数据处理,不能够修改之前请求的数据。当处理速率跟不上sensor的输出速率,应该放到离线流中。

  14. Offline stream:处理不从图像传感器接受的数据。

(二)CHI 拓扑图的XML配置

硬件和软件的图像处理节点(node)产生所需的输出,这些节点之间的连接确定数据将如何流过相机子系统。 这组节点和连接称为拓扑。

用例由一组要处理的目标和一组会话设置定义,这些设置进一步定义了应如何处理数据。 每个用例都由拓扑图表示,拓扑图是传递到HAL3 API的信息与如何处理数据的具体定义之间的联系。

所有用例及其对应拓扑的列表都编码在XML文件中。 一个用例是在configure_streams的时候被选择的,基于XML的两个部分:< Targets >和< SystemwideSettings >。

提供了一个XSD模式,该模式定义了XML的结构(这句话没太懂)。 提供了一些工具来将XML文件打包为二进制文件,以供CHI驱动程序使用。

基本概念

节点是拓扑图中的硬件或软件处理组件。 节点的定义包括了高通默认的节点和自定义的节点。一个节点有一组输入和输出,被称作输入和输出端口。

输出到HAL3 图像buffer的输出端口,被称作sinkbuffer 输出端口。还有一些节点的输出端口不输出到任何图像缓冲区,用于指示节点正在使用中。这些叫SinkNoBuffer。

通过将一个节点的输入端口连接到另一个节点的输出端口来形成拓扑图中的DAG,或者在存在反馈环路的情况下,一个节点的输出端口可以连接到其自己的输入端口。输入端口和输出端口之间的连接称为链接(link),链接还包含有关节点之间使用的必要缓冲区的信息。

节点,端口,链接的关系

XML的组成

  1. UsecaseDef是根标记,在此标记中定义了各个用例Usecase。

  2. Usecase是用例标记,它包括了匹配用例的关键,以及拓补图。里面有UsecaseName、Targets、streamconfigmode、systemwidesetting、pipeline

  3. Targets是流的列表,包括用例执行的格式和大小范围,此处列出的流与传递到HAL3 configure_streams API的流相对应.

    Target是单个流或目标的描述,会将本节的各个标记值(target和systemwideSettings)与传递到configure_streams中的流配置进行比较,以找到匹配的Usecase。

    1. range,缓冲区的分辨率和configure_streams()中缓冲区的传入分辨率必须在此范围内,以便Usecase被选择为输入流配置的匹配用例
    2. systemwideSettings,单个会话的设置组,定义此用例的key,这些设置对应安卓的property。
    3. TargetDirection: 流的方向,可以是输入或输出,也可以是双向
    4. Formats:流的数据格式
  4. pipeline是这个用例的pipeline,有一组节点和链接组成。pipelineName是这个pipeline的名字。

    1. nodesList是节点的列表。node是单个节点,告诉驱动程序如何处理一个请求。

      1. NodeProperty,可选的,允许安卓的几个property属性。

      2. NodeName,节点名字

      3. NodeId,驱动程序通过解释nodeId以了解节点类型,自定义的节点,Id都是255,这个值与NodeProperty一起通知驱动程序,确切的自定义节点类型。

      4. NodeInstance,节点实例

      5. NodeInstanceId ,用于表示节点的唯一实例ID,这些id用于将节点的端口彼此连接起来

        <Node> 
        <NodeProperty>
        <NodePropertyName>CustomNodeLibraryNodePropertyName>
        <NodePropertyId>1NodePropertyId>
        <NodePropertyDataType>STRINGNodePropertyDataType>
        <NodePropertyValue>com.oem.node.libnameNodePropertyValue>
        NodeProperty>
        <NodeName>CustomNodeNodeName>
        <NodeId>255NodeId>
        <NodeInstance>CustomNodeInstanceName0NodeInstance>
        <NodeInstanceId>0NodeInstanceId>
        Node>
    2. portlinkages,节点间的链接列表。link,是节点间的单个链接,可以一对多

      1. srcPort是节点的输出端口,主要通过端口以及节点的名字和id来确定。
      2. dstPort是节点的输入端口
      3. bufferProperties描述了缓冲区的属性,主要有格式(BufferFormat),大小,初始分配的缓冲区数量(BufferImmediateAllocCount),最大缓冲区深度(BufferQueueDepth),从堆中分配分配缓冲区(bufferHeap),用于允许分配在摄像机ISP外部可见以及缓冲区的使用信息(BufferFlags)
      4. linkProperties就只有BatchMode,是否需要批处理,通常只有在HFR模式才需要这样做,以允许ISP在单个硬件提交中处理多个帧。它只适用于无法脱机操作的链接。
      <Link> 
      <SrcPort>
      <PortName>IFEOutputPortFullPortName>
      <PortId>0PortId>
      <NodeName>IFENodeName>
      <NodeId>65536NodeId>
      <NodeInstance>IFEInstanceName0NodeInstance>
      <NodeInstanceId>0NodeInstanceId>
      SrcPort>
      <DstPort>
      <PortName>IPEInputPortFullPortName>
      <PortId>0PortId>
      <NodeName>IPENodeName>
      <NodeId>65538NodeId>
      <NodeInstance>IPEInstanceName0NodeInstance>
      <NodeInstanceId>0NodeInstanceId>
      DstPort>
      <BufferProperties>
      <BufferFormat>ChiFormatYUV420NV12BufferFormat>
      <BufferImmediateAllocCount>8BufferImmediateAllocCount>
      <BufferQueueDepth>8BufferQueueDepth>
      <BufferHeap>IonBufferHeap>
      <BufferFlags>BufferMemFlagHwBufferFlags>
      BufferProperties>
      Link>

(三)CHI 架构模型

overview

CHI框架

流程

初始化

JAVA层通过open的函数获得了CHI的所有操作函数,这是通过chi_hal_override_entry()方法获取的。

接着用configure_stream配置流,CHI框架在其中的操作是初始化了会话session,找到配置对应的pipeline,并返回了一个pipeline的句柄。(框架为自定义会话和后处理预留了接口,返回NULL是用高通默认的pipeline)

请求与返回流程

用process_capture_request去请求图像数据(包括抓拍,录像和预览),这个请求会提交到pipeline中,接着pipeline中的各个节点都会得到请求的通知,进行对应的执行。当然这一步也可以重写请求,比如说为多帧特性创建额外的请求。

每个管道都会异步执行,来自任何管道的结果,包括实时流和离线流,都会通知CHI层。如果CHI为JAVA层的请求生成了多个子请求,那么需要等到所有结果都触发之后再调用chi_override_process_pipeline_result,把结果转发到HAL,最终转发给camera框架中的process_capture_result。 高通框架支持重写结果通知部分。

close 流程

当HAL收到camera框架的close调用时,它调用chi_teardown_override_session,然后必须清理所有的自定义管道和会话。

元数据metadata

CHI中的沟通渠道分为以下几个:

  • 传给APP的数据

  • 传给后处理pipeline的数据

  • 使用发布和订阅机制的节点通信(这是很有名的一种设计模式)

    • 原生节点之间用android tags/chiVendorTags
    • 原生节点和自定义节点,以及自定义节点之间用android tags/chiVendorTags/ExtCompVendorTags

元数据标签可以是预定义的Android标签,也可以是定制的厂商标签。元数据标签使内部和外部的组件可以相互通信,也可以与面向应用程序的摄像头API通信。在CHI中,Android元数据标签是预定义的,只接收不可变的值。厂商标记不能静态地与固定的绝对值关联。根据目标上扩展组件的数量和类型,CHI使用厂商标记的动态索引(base + offset)使组件能够相互通信。

元数据标记ID是一个32位的值,它被限制在一个特定的部分中。每个部分的起始偏移量为0x1_0000。标记空间的范围从0x0000_0000到0x8000_0000是为Android元数据标记保留的。供应商部分应该在0x8000_0000之后开始。

在初始化时,当CHI扫描目标系统中的可用组件时,它为每个发布自定义厂商标记的组件分配一个厂商部分的开始位置。组件需要枚举标记为Base + Offset, Base由CHI分配

metadata 存储位置

节点可以通过设置初始化时枚举过的标记来发布元数据。CHI会根据特定标记的依赖关系,通知所有其他提交的节点。这个机制对于Android标签、ChiVendorTags或其他ExtCompVendorTags都是一样的。

节点间通信流程图

(四)代码细节

1. 加载外部二进制库

每个节点都必须在一起单独的动态库里面,按照com.< cendor >, < category >, < algorithm > .so的方式命名。

< vendor >:公司名

< category >:模块的类型,可以是node或者stats

< algorithm > : 算法独特的名字,如果说节点,名字必须和xml指定的名称匹配。如果是stats,只能是af, aec, awb, asd, afd里面的一个。

所有的动态库都放在 /sys/data/camx/components/

相机服务的初始化,发生在设备启动的时候,加载HAL3模块的CHI实现。在CHI HAL3的启动过程中,驱动城市查询并加载位于这个路径下的所有动态库。

2. CHI 接口

CHI向HAL3接口添加了一些关键元素,以支持更细粒度的控制,以下函数用于访问CHI驱动程序,他们对Camera2/HAL3没有任何依赖。

ChiEntry()是CHI驱动程序的主要入口点,所有CHI功能都需要。

其中使用了ChiContextOps 结构体指向所有的CHI函数。客户端需要使用有效的CHIIHandle句柄来调用这些函数,这个结构体永远不会缩小,现有字段也不会在结构更改时重新排序或者重新使用。所有字段默认值是0,0代表了这个特性不可用。

CHI 接口的函数中比较重要的是pOpenContext。通过这个函数打开CHI的上下文,并将句柄返回给CHI。这个函数必须在任何其他CHI函数之前调用。

这个函数是用来创建chicontext的唯一实例,chicontext是会话中所有状态都保留的地方,预计每个进程中将会有一个Chicontext,当然,一个进程中有多个chicontext也是支持的。在chicontext之间没有交互信息的机制,会话和pipeline只能被创建它的chicontext使用。

3. HAL3到CHI 重写接口(override)

如果CHI功能用于HAL3相机应用程序,这些功能必须在OEM重写代码中实现。这些函数必须被编译到一个名为com.< vendor > .chi.override.so的文件中。

chi_hal_override_entry()

由驱动程序调用来初始化HAL重写模块的入口点。为了让驱动程序初始化override接口,com.< vendor > .chi.override.so必须导出这个函数。该函数在相机服务器初始化期间调用,初始化发生在设备引导期间。

4. CHI 节点接口

ChiNodeEntry() :由CHI驱动程序调用来初始化自定义节点的入口点。

ChiNodeCallbacks结构体:

size 结构的大小,这个值框架会自己计算的
majorVersion/minorVersion 版本号
pGetCapabilities 必填,能力集,mask列表在chinode.h中查找
pQueryVendorTag 如果节点需要到处vendor标记才填充它
pCreate 必填,当拓扑图创建时创建节点实例
pDestory 必填,当拓扑图销毁是销毁节点实例
pQueryBufferInfo 必填,CHI调用这个函数从节点查询特定输出格式的输入缓冲区格式(分辨率和类型)
pSetBufferInfo 必填,CHI设置缓冲区格式
pProcessRequest 必填,执行请求处理。
pChiNodeSetNodeInterface 必填,设置节点中的预定义CHI接口
pPipelineCreated 可选,CHI通知节点创建了一个特定的pipeline
pPostPipelineCreate 后管道创建的通知
pPrepareStreamOn/pOnStreamOn/pOnStreamOff 事件通知
pQueryMetadataPublishList 必须调用此函数来查询节点发布的元数据列表。
pFlushRequest 释放为请求分配的节点资源。
pGetFlushResponse 获取刷新调用的最坏情况响应时间
pFillHwdata 填充每个请求的硬件数据

5. CHI stats 重写

有AE, AWB, AF, HAF(混合自动对焦),PDAF,Auto Flicker Detection(水波纹检测),Auto Scene Detection(场景识别),Tracker (对象触摸跟踪)算法的接口,可以自己实现(高通的算法还是很复杂和高级的,正常人不会修改)