Camera metadata

news2024/11/16 13:41:11

目录

背景

CameraMetadata基本概念

Google Metadata

Google—Metadata结构

官方注释

Aandroid API

cameraMetadata头部 : struct camera_metadata

camera_metadata_buffer_entry

struct camera_metadata_entry

data区为什么是一个联合体?

camera metadata头部、entry区、data区关系

CameraMetadata增删改查

allocate_camera_metadata (分配metadata)

find_camera_metadata_entry(从metadata中根据tag查找value)

add_camera_metadata_entry(增加tag和value到metadata)

add_camera_metadata_entry_raw实现

add_camera_metadata_entry用法

delete_camera_metadata_entry(删除tag)

update_camera_metadata_entry(更新tag的value值)

update_camera_metadata_entry实现

append_camera_metadata

clone_camera_metadata

metadata tag分类

android tag

vendor tag

All Tags

Metadata分类

Per Camera Device

Per Session

Per Request

Per Result

Qcom—Metadata整体架构

content

MetaBuffer

Metadata Property

MetadataPool整体架构

为什么引入MetadataPool机制?

MetadataSlot与MetaBuffer的绑定

Camx对meta的管理

InitializeVendorTagInfo

VendorTagManager::GetAllTags

Property

meta的统一管理


背景

首先提出几个关于meta的问题:

  • meta是什么?
  • tagName,tagID,location,data他们是什么?怎么联系起来的?
  • 用什么数据结构管理meta,怎么实现建、增、删、查、改的?
  • 根据需求不同,都有哪些meta,他们有什么区别?
  • framework meta与hal meta怎么交互,是否存在内存copy, 如何实现的(包括request和result)?
  • meta owner是谁?buffer在哪里分配的?生命周期如何?内存大小如何决定?
  • pipeline间,node间,meta怎么传递?
  • 多帧合成pipeline的meta是怎么处理?
  • input meta与output meta的关系? input merge到output?

CameraMetadata基本概念

  1. Metadata 是整个 Android camera 中类似于高楼大厦中的管道系统一样,贯穿了整个 Camera 的 Framework 和 vendor HAL。Metadata 里面包含了所有的控制、参数、返回值等等。
  2. Metadata分为Android原生的Metadata和芯片商或者用户自定义的Metadata,即Android Tag和Vendor Tag。Vendor Tag在Hal中定义,并在camx和chi目录中使用。
  3. 如果是高通平台的话,一般至少存在三类metadata,一类是 Andriod metadata、Qcom metadata、手机厂商自己定义的 metadata。

camera meta简单来说就是用来描述图像特征的参数,比如常见的曝光、ISO等,不难理解他的数据结构应该是由N个key和value组成的。

camera参数多起来可能上百个,用什么样的数据结构来封装这些参数,封装成什么操作,在什么情况下使用这些操作?

如果让你实现对meta的管理,你会怎么实现?

先来看看google是怎么做的:

Google Metadata

Google Metadata相关代码位置:

/system/media/camera/include/system/camera_metadata_tags.h /system/media/camera/src/camera_metadata.c /system/media/camera/src/camera_metadata_tag_info.c

Google—Metadata结构

                        Metadata: An Array of Entries

主要分为: 1.cameraMetadata头部 2.camera_metadata_buffer_entry区 3.data区

官方注释

/**

 * A packet of metadata. This is a list of entries, each of which may point to

 * its values stored at an offset in data.

 *

 * It is assumed by the utility functions that the memory layout of the packet

 * is as follows:

 *   |-----------------------------------------------|

 *   | camera_metadata_t                             |  区域一 :何存camera_metadata_t  结构体定义

 *   |                                               |

 *   |-----------------------------------------------|

 *   | reserved for future expansion                 |  区域二 :保留区,供未来使用

 *   |-----------------------------------------------|

 *   | camera_metadata_buffer_entry_t #0             |  区域三 :何存所有 Tag 结构体定义

 *   |-----------------------------------------------|          TAG[0]、TAG[1]、.....、TAG[entry_count-1]

 *   | ....                                          |

 *   |-----------------------------------------------|

 *   | camera_metadata_buffer_entry_t #entry_count-1 |

 *   |-----------------------------------------------|

 *   | free space for                                |  区域四 :剩余未使用的 Tag 结构体的内存保留,

 *   | (entry_capacity-entry_count) entries          |          该区域大小为 (entry_capacity - entry_count) 个TAG 

 *   |-----------------------------------------------|

 *   | start of camera_metadata.data                 |  区域五 : 所有 Tag对应的具体 metadata 数据

 *   |                                               |

 *   |-----------------------------------------------|

 *   | free space for                                |  区域六 : 剩余未使用的 Tag 占用的内存

 *   | (data_capacity-data_count) bytes              |

 *   |-----------------------------------------------|

 *

 * With the total length of the whole packet being camera_metadata.size bytes.

 *

 * In short, the entries and data are contiguous in memory after the metadata

 * header.

 */

Aandroid API

cameraMetadata头部 : struct camera_metadata

camera_metadata_buffer_entry

struct camera_metadata_entry

typedef struct camera_metadata_entry {
    size_t   index;//该entry在当前metadata里面的index(0~entry_count-1)
    uint32_t tag;//tag的key值
    uint8_t  type;//TYPE_BYTE、 TYPE_INT32、TYPE_FLOAT、TYPE_INT64、TYPE_DOUBLE、TYPE_RATIONAL
    size_t   count;//tag的value对应的data的数量。比如data的类型为uint8_t,count为100。则总共为100个字节。
    union {
        uint8_t *u8;
        int32_t *i32;
        float   *f;
        int64_t *i64;
        double  *d;
        camera_metadata_rational_t *r;
    } data;// tag的value对应的data值
} camera_metadata_entry_t;

data区为什么是一个联合体?

最重要的是data。data是一个联合体,为什么是一个联合体呢?目的就是为了节省空间:比如当类型为枚举时,或者一个整形,那么只需要放在value中存储就行。就没有必要用uint32_t来存储它。和MetaBuffer中的Content是一样的原理。在MetaBufer中,当要存储的内存大小是小于或者等于8个字节时,是直接存储在BYTE m_data[MaxInplaceTagSize];,当存储的内存大小是大于8个字节的,是存放在MemoryRegion中的,这个是高通自己实现的一套metadata框架,相对于Google metadata来说有自己的优势,比较灵活。

camera metadata头部、entry区、data区关系

CameraMetadata增删改查

allocate_camera_metadata (分配metadata)

camera_metadata_t *allocate_camera_metadata(size_ttagkaifa entry_capacity, size_t data_capacity) //申请一块内存给metadata,并且赋初始值

step1:计算metadata需要申请的内存字节数: memory_needed = sizeof(camera_metadata_t) + sizeof(camera_metadata_buffer_entry_t[entry_capacity]) + sizeof(uint8_t[data_capacity])

step2: 使用calloc函数申请内存:void *buffer = calloc(1, memory_needed);

step3: 调用place_camera_metadata() 函数为头部的机构体camera_metadata_t内部变量赋值,camera_metadata_t *metadata = place_camera_metadata(buffer, memory_needed, entry_capacity, data_capacity);

问题1:entry_capacity是怎么计算出来的?怎么知道当前一共有多少entry?data_capacity是怎么计算出来的?

entry_capacity就是预留的entry的数量,一般比可能用到的entry多预留一些。

data_capacity就是预留的data的大小,一般比所有data的总和多预留一些。

问题2:典型用法有哪些?什么时候用?

需要对已有meta做一些腾挪,或者自己创建的pipeline,尤其HAL层以上的pipeline需要新建对应的meta。

// Allocate replaced metadata

m_pReplacedMetadata    = allocate_camera_metadata(ReplacedMetadataEntryCapacity, ReplacedMetadataDataCapacity);

find_camera_metadata_entry(从metadata中根据tag查找value)

int find_camera_metadata_entry(camera_metadata_t *src, uint32_t tag, camera_metadata_entry_t *entry)

step1:从src中找到tag对应的entry的index

step2:根据index将entry拿出来

int find_camera_metadata_entry(camera_metadata_t *src,  uint32_t tag, camera_metadata_entry_t *entry)
{
    uint32_t index;
    if (src->flags & FLAG_SORTED)  //有排序,二分法查找
    {
        camera_metadata_buffer_entry_t *search_entry = NULL;
        camera_metadata_buffer_entry_t key;
        key.tag = tag;
        search_entry = bsearch(&key,  get_entries(src), src->entry_count, sizeof(camera_metadata_buffer_entry_t), compare_entry_tags);
        if (search_entry == NULL) return NOT_FOUND;
        index = search_entry - get_entries(src);
    }
    else
    { //线性查找
        camera_metadata_buffer_entry_t *search_entry = get_entries(src);
        for (index = 0; index < src->entry_count; index++, search_entry++) {
            if (search_entry->tag == tag) {
                break;
            }
        }
        if (index == src->entry_count) return NOT_FOUND;
    }
    return get_camera_metadata_entry(src, index, entry);
}
  
// 根据index, 将对应的entry 拿出来
int get_camera_metadata_entry(camera_metadata_t *src,  size_t index,  camera_metadata_entry_t *entry)
 {
    camera_metadata_buffer_entry_t *buffer_entry = get_entries(src) + index;
  
    entry->index = index;
    entry->tag = buffer_entry->tag;
    entry->type = buffer_entry->type;
    entry->count = buffer_entry->count;
    if (buffer_entry->count * camera_metadata_type_size[buffer_entry->type] > 4)  // 大于4字节
    {
        entry->data.u8 = get_data(src) + buffer_entry->data.offset;
    }
    else
    {
        entry->data.u8 = buffer_entry->data.value;  // // 小于4字节
    }
}

add_camera_metadata_entry(增加tag和value到metadata)

int add_camera_metadata_entry(camera_metadata_t *dst, uint32_t tag, const void *data, size_t data_count)

调用下面的add_camera_metadata_entry_raw(camera_metadata_t *dst, uint32_t tag, uint8_t type, const void *data, size_t data_count)

step1:从dst的entry末尾取出一个entry,*entry = get_entriesget_entries(dst) + dst→entry_count

step2:计算entry数据总字节数,如果总字节数 < 4, 直接使用memcpy把data复制到entry→data.value

step3:否则 就给entry->data.offset赋值为dst→data_count, 然后使用memcpy将数据拷贝到entry→data.offset位置

add_camera_metadata_entry_raw实现

static int add_camera_metadata_entry_raw(camera_metadata_t *dst, uint32_t tag, uint8_t  type, const void *data, size_t data_count)

{

    size_t data_bytes = calculate_camera_metadata_entry_data_size(type, data_count);  //计算entry数据总字节数,如果小于4字节就返回0

  

    size_t data_payload_bytes = data_count * camera_metadata_type_size[type]; //计算entry数据总字节数,不考虑对齐

    camera_metadata_buffer_entry_t *entry = get_entries(dst) + dst->entry_count;

    memset(entry, 0, sizeof(camera_metadata_buffer_entry_t));

    entry->tag = tag;

    entry->type = type;

    entry->count = data_count;

  

    if (data_bytes == 0)

    {

        memcpy(entry->data.value, data, data_payload_bytes);   //总字节数 <= 4, 直接使用memcpy把data复制到entry→data.value

    }

    else

    {

        entry->data.offset = dst->data_count;    //总字节数 > 4, 给entry->data.offset赋值为dst→data_count,  然后使用memcpy将数据拷贝到entry->data.offset位置

        memcpy(get_data(dst) + entry->data.offset, data, data_payload_bytes);

        dst->data_count += data_bytes; //更新data_count

    }

    dst->entry_count++;

    dst->flags &= ~FLAG_SORTED; //不排序

}

add_camera_metadata_entry用法

CamxResult HAL3MetadataUtil::MergeMetadata(

    Metadata*       pDstMetadata,

    const Metadata* pSrcMetadata)

{

    CamxResult         result             = CamxResultSuccess;

    INT                status             = 0;

    camera_metadata_t* pMergeSrcMetadata  =

        // NOWHINE CP036a: Non-const required by standard function

        const_cast<camera_metadata_t*>(static_cast<const camera_metadata_t*>(pSrcMetadata));

    camera_metadata_t* pMergeDstMetadata  =

        // NOWHINE CP036a: Non-const required by standard function

        const_cast<camera_metadata_t*>(static_cast<const camera_metadata_t*>(pDstMetadata));

    SIZE_T totalEntries = get_camera_metadata_entry_count(pMergeSrcMetadata);

    for (UINT i = 0; i < totalEntries; i++)

    {

        camera_metadata_entry_t srcEntry;

        camera_metadata_entry_t dstEntry;

        camera_metadata_entry_t updatedEntry;

        get_camera_metadata_entry(pMergeSrcMetadata, i, &srcEntry);

        status = find_camera_metadata_entry(pMergeDstMetadata, srcEntry.tag, &dstEntry);

        if (MetadataUtilOK != status)

        {

            status = add_camera_metadata_entry(GetMetadataType(pMergeDstMetadata),

                                               srcEntry.tag,

                                               srcEntry.data.i32,

                                               srcEntry.count);

        }

        else

        {

            status = update_camera_metadata_entry(GetMetadataType(pMergeDstMetadata),

                                                  dstEntry.index,

                                                  srcEntry.data.i32,

                                                  srcEntry.count,

                                                  &updatedEntry);

        }

        if (MetadataUtilOK != status)

        {

            result = CamxResultEFailed;

            break;

        }

    }

    return result;

}

delete_camera_metadata_entry(删除tag)

int delete_camera_metadata_entry(camera_metadata_t *dst, size_t index)

step1:如果entry大于4字节时,先删除旧的数据:把下个entry(当前的data.offset + entry_bytes)的data到后面的整个data区,向前移动entry_bytes个字节,覆盖掉当前entry的数据, 然后更新后面的entry的data.offset

step2:向前移动entry数组覆盖掉要删除的entry

int delete_camera_metadata_entry(camera_metadata_t *dst, size_t index)
{
    camera_metadata_buffer_entry_t *entry = get_entries(dst) + index;
    size_t data_bytes = calculate_camera_metadata_entry_data_size(entry->type,  entry->count);
  
    if (data_bytes > 0) //entry大于4字节
    {
        // 移动data数据,覆盖要删除的数据
        uint8_t *start = get_data(dst) + entry->data.offset;
        uint8_t *end = start + data_bytes;
        size_t length = dst->data_count - entry->data.offset - data_bytes;
        memmove(start, end, length);
  
        // 更新entry的数据偏移
        camera_metadata_buffer_entry_t *e = get_entries(dst);
        for (i = 0; i < dst->entry_count; i++) {
            if (calculate_camera_metadata_entry_data_size(e->type, e->count) > 0 && e->data.offset > entry->data.offset) {
                e->data.offset -= data_bytes;
            }
            ++e;
        }
        dst->data_count -= data_bytes;
    }
    // 移动entry数组
    memmove(entry, entry + 1,  sizeof(camera_metadata_buffer_entry_t) * (dst->entry_count - index - 1) );
    dst->entry_count -= 1;
}

update_camera_metadata_entry(更新tag的value值)

int update_camera_metadata_entry(camera_metadata_t *dst, size_t index, const void *data, size_t data_count, camera_metadata_entry_t *updated_entry)

step1:当要插入的数据和原来entry的数据长度不相等时,如果entry大于4字节时,先删除旧的数据:

把下个entry(当前的data.offset + entry_bytes)的data到后面的整个data区,向前移动entry_bytes个字节,

覆盖掉当前entry的数据, 然后更新后面的entry的data.offset,最后把新的tag数据追加到整个data区的后面

step2:当要插入的数据和原来entry的数据长度相等时,重复利用原来的data内存, 把新的tag数据拷贝到原来的entry的数据区

step3: 如果entry小于等于4字节,直接拷贝到entry→data.value中即可

update_camera_metadata_entry实现

int update_camera_metadata_entry(camera_metadata_t *dst,
        size_t index,
        const void *data,
        size_t data_count,
        camera_metadata_entry_t *updated_entry) {
    if (dst == NULL) return ERROR;
    if (index >= dst->entry_count) return ERROR;
 
    camera_metadata_buffer_entry_t *entry = get_entries(dst) + index;
 
    size_t data_bytes =
            calculate_camera_metadata_entry_data_size(entry->type,
                    data_count);
    size_t data_payload_bytes =
            data_count * camera_metadata_type_size[entry->type];
 
    size_t entry_bytes =
            calculate_camera_metadata_entry_data_size(entry->type,
                    entry->count);
    if (data_bytes != entry_bytes) {
        // May need to shift/add to data array
        if (dst->data_capacity < dst->data_count + data_bytes - entry_bytes) {
            // No room
            return ERROR;
        }
        if (entry_bytes != 0) {
            // Remove old data
            uint8_t *start = get_data(dst) + entry->data.offset;
            uint8_t *end = start + entry_bytes;
            size_t length = dst->data_count - entry->data.offset - entry_bytes;
            memmove(start, end, length);
            dst->data_count -= entry_bytes;
 
            // Update all entry indices to account for shift
            camera_metadata_buffer_entry_t *e = get_entries(dst);
            size_t i;
            for (i = 0; i < dst->entry_count; i++) {
                if (calculate_camera_metadata_entry_data_size(
                        e->type, e->count) > 0 &&
                        e->data.offset > entry->data.offset) {
                    e->data.offset -= entry_bytes;
                }
                ++e;
            }
        }
 
        if (data_bytes != 0) {
            // Append new data
            entry->data.offset = dst->data_count;
 
            memcpy(get_data(dst) + entry->data.offset, data, data_payload_bytes);
            dst->data_count += data_bytes;
        }
    } else if (data_bytes != 0) {
        // data size unchanged, reuse same data location
        memcpy(get_data(dst) + entry->data.offset, data, data_payload_bytes);
    }
 
    if (data_bytes == 0) {
        // Data fits into entry
        memcpy(entry->data.value, data,
                data_payload_bytes);
    }
 
    entry->count = data_count;
 
    if (updated_entry != NULL) {
        get_camera_metadata_entry(dst,
                index,
                updated_entry);
    }
 
    assert(validate_camera_metadata_structure(dst, NULL) == OK);
    return OK;
}

append_camera_metadata

int append_camera_metadata(camera_metadata_t *dst, const camera_metadata_t *src) //将src的metadata中的entry和data追加在dst后

step1:将src的entry_count个entry和data都拷贝到dst的entry后面和data后面

step2:更新dst的entry→data.offset和其他的成员

int append_camera_metadata(camera_metadata_t *dst,  const camera_metadata_t *src)
 {
    memcpy(get_entries(dst) + dst->entry_count,  get_entries(src),  sizeof(camera_metadata_buffer_entry_t[src->entry_count]));
    memcpy(get_data(dst) + dst->data_count, get_data(src), sizeof(uint8_t[src->data_count]));
    //更新dst中新加入的entry->data.offset
    if (dst->data_count != 0) {
        camera_metadata_buffer_entry_t *entry = get_entries(dst) + dst->entry_count; //新增的src的entry 起始地址
        for (size_t i = 0; i < src->entry_count; i++, entry++) {
            if ( calculate_camera_metadata_entry_data_size(entry->type, entry->count) > 0 ) //data 大于4字节
            {
                entry->data.offset += dst->data_count;    // entry->data.offset在src中本身是有偏移的,所以只需要对每个偏移加上 dst->data_count就可以了
            }
        }
    }
    if (dst->entry_count == 0) {
        dst->flags |= src->flags & FLAG_SORTED; //dst为空,使用src的存储方式
    } else if (src->entry_count != 0) {
        dst->flags &= ~FLAG_SORTED; //dst和src都不为空,使用无排序方式
    }
    dst->entry_count += src->entry_count;
    dst->data_count += src->data_count;
    dst->vendor_id = src->vendor_id;
}

clone_camera_metadata

camera_metadata_t *clone_camera_metadata(const camera_metadata_t *src) /

step1:申请一个camera_metadata_t的内存,大小和src一样大

step2:将src append到这块内存中


camera_metadata_t *clone_camera_metadata(const camera_metadata_t *src)
{
    camera_metadata_t *clone = allocate_camera_metadata(get_camera_metadata_entry_count(src),  get_camera_metadata_data_count(src));
    if (clone != NULL)
    {
       append_camera_metadata(clone, src);
    }
    return clone;
}

metadata tag分类

tag从归属方可以被分类两类:

  1. android平台原生tag。如ANDROID_CONTROL_AE_MODE用于控制AE曝光方式(auto、manual等)
  2. vendor tag(platfrom如Qcom/SumSung/MTK新增tag)。

android tag

tag是通过section的方式来进行分类的,如下:

typedef enum camera_metadata_section {
    ANDROID_COLOR_CORRECTION,
    ANDROID_CONTROL,
    ANDROID_DEMOSAIC,
    ANDROID_EDGE,
    ANDROID_FLASH,
    ANDROID_FLASH_INFO,
    ANDROID_HOT_PIXEL,
    ANDROID_JPEG,
    ANDROID_LENS,
    ANDROID_LENS_INFO,
    ANDROID_NOISE_REDUCTION,
    ANDROID_QUIRKS,
    ANDROID_REQUEST,
    ANDROID_SCALER,
    ANDROID_SENSOR,
    ANDROID_SENSOR_INFO,
    ANDROID_SHADING,
    ANDROID_STATISTICS,
    ANDROID_STATISTICS_INFO,
    ANDROID_TONEMAP,
    ANDROID_LED,
    ANDROID_INFO,
    ANDROID_BLACK_LEVEL,
    ANDROID_SYNC,
    ANDROID_REPROCESS,
    ANDROID_DEPTH,
    ANDROID_LOGICAL_MULTI_CAMERA,
    ANDROID_DISTORTION_CORRECTION,
    ANDROID_HEIC,
    ANDROID_HEIC_INFO,
    ANDROID_SECTION_COUNT,

    VENDOR_SECTION = 0x8000
} camera_metadata_section_t;

上面都是android原生tag的section,每一个section支持的tag总数最大是65536(1<<16)。

vendor tag

vendor tag必须从0x8000000开始使用

All Tags

Metadata分类

Per Camera Device

  1. 每个相机设备一个,设备数量基于get_number_of_camera
  2. 启动阶段获取到Provider,且只获取一次

Provider启动的时候通过dlopen和dlsym获得HAL层的::HAL_MODULE_INFO_SYM指针,类型为camera_module_t。 通过调用HAL层module的get_camera_info方法,从底层获得camera_info类型的变量, camera_info内部包含一个camera_metadata_t类型的成员变量

Per Session

  1. 每次配流时下发
  2. 一个场景下只下发一次配置参数

在Configure Stream时,在camera3_stream_configuration_t结构体中有camera_metadata_t类型的成员变量,HAL需要检查metadata中的参数值,并相应地配置内部camera pipeline。

Per Request

  1. 每次下发request时下发到HAL

在下发request时,在camera3_capture_request_t结构体中有camera_metadata_t类型的成员变量。在hardware.h下camera3.h 下定义。

一个camera id,可能对应多个物理camera,camera3_capture_request_t中包含物理camera的数量,对应多个物理camera的情况下,会对应多个metadata

Per Result

  1. 每次返回result时上传到APP

在收到result时,在camera3_capture_result_t结构体中有camera_metadata_t类型的成员变量,在hardware camera3.h中定义。

一个camera id,可能对应多个物理camera, camera3_capture_result_t中包含物理camera的数量,对应多个物理camera的情况下,会对应多个metadata

Qcom—Metadata整体架构

content

MetaBuffer

Metadata Property

MetadataPool整体架构

  1. MetaPool机制,各部分是怎么组合关联的?
  2. MetadataPool各组件的创建(代码流程,configstream时)
  3. MetadataSlot与MetaBuffer绑定(代码流程,request)
  4. 通知机制的实现(怎么知道已经准备好了,准备好了之后怎么通知)

  1. MetadataPool处理多个request,以及node之间存在meta依赖的情况。
  2. DeferredRequestQueue是和Session一对一的,在session中的成员变量是m_pDeferredRequestQueue
  3. MetadataPool和Pipeline是多对一,一个Pipeline有多个不同种类的Pool,一种一个
  4. MetadataPool和MetadataSlot是一对多的关系
  5. MetadataSlot和MetaBuffer是一对一的关系

为什么引入MetadataPool机制?

MetadataPool处理多个request,以及node之间存在meta依赖的情况。

  1. 当metadata为Per request 和 Per result的时候,上面会发多个request下来。因为ProcessCaptureRequest是异步的。多个request是存放在队列中的。在pipeline中,会有outstanding多个request在排队中。所以metadatapool需要解决的问题就是,这样的事情。

metadatapool 和 metadataSlot是一对多的关系。

MetadataSlot* m_pSlots[MaxPerFramePoolWindowSize];

static const UINT32 MaxPerFramePoolWindowSize = (RequestQueueDepth + MaxExtraHALBuffer) * 2;

一般每一个node依赖前一个Node的Buffer,和前一个Node的产生的metadata。

2.实现Observer pattern 。在pipelie中,下一个node正常运行,需要上一个Node的metadata,这个时候就有依赖关系存在。怎么样通知下一个node已经准备好了,可以继续往下一个node往下传数据呢?

MetadataSlot与MetaBuffer的绑定

Session::ProcessCaptureRequest会把pipeline中的几个pool拿出来,根据requestid取模,用余数找到对应的slot,再把meta和slot进行attach。

Camx对meta的管理

完整的TagID = (section << 16) + tag

sectionOffset为 (0x8000 << 16), section按遍历顺序,基于sectionOffset递增,tag按定义顺序递增

假如vendorTag的第一个tag为“org.codeaurora.qcamera3.exposure_metering”,“exposure_metering_mode”,

则section = 0x8000 + 1, tag = 0;tagID = 0x80010000

第二个tag为“org.codeaurora.qcamera3.exposure_metering”,“available_modes”,

则section = 0x8000 + 1, tag = 1;tagID =0x8001000

tagID什么时候,怎么确定的?

进程启动时,收集所有的vendor Tag:

从HW中获取vendor Tag 信息

CHIVENDORTAGINFO hwVendorTagInfo ;

pHwEnvironment->GetHwStaticEntry()->QueryVendorTagsInfo(&hwVendorTagInfo); 

QueryVendorTagsInfo是在Camxtitan17xcontext.cpp (vendor\qcom\proprietary\camx\src\hwl\titan17x)文件中实现的。

hwVendorTagInfo(g_HwVendorTagSections)的信息定义在Camxvendortagdefines.h (vendor\qcom\proprietary\camx\src\api\vendortag文件中。

获取core的vendorTag信息

CHIVENDORTAGINFO coreVendorTagInfo = { g_CamXVendorTagSections, CAMX_ARRAY_SIZE(g_CamXVendorTagSections) }; 

获取core的compomentVendorTag信息

从external compoment包括( 库的命名规则:VendorName.CategoryName.ModuleName.TargetName.Extension     CategoryName: node, stats,hvx,chi) 获取vendor Tag。

InitializeVendorTagInfo


// VendorTagManager::AppendVendorTagInfo

CamxResult VendorTagManager::AppendVendorTagInfo(
    const CHIVENDORTAGINFO* pVendorTagInfoToAppend)
{
    CamxResult result = CamxResultSuccess;
 
    CAMX_ASSERT(NULL != pVendorTagInfoToAppend);
 
    // This function merge the incoming vendor sections with existing ones. here we allocate a new array of sections and copy
    // the existing and incoming sections to the new array. After the successful merge, we will free the existing section
    // array and assign the new array to m_vendorTagInfo.
 
    if ((NULL != pVendorTagInfoToAppend) && (pVendorTagInfoToAppend->numSections > 0))
    {
        CHIVENDORTAGSECTIONDATA* pSectionData =
            static_cast<CHIVENDORTAGSECTIONDATA*>(CAMX_CALLOC(sizeof(CHIVENDORTAGSECTIONDATA) *
            (m_vendorTagInfo.numSections + pVendorTagInfoToAppend->numSections)));
        if (NULL == pSectionData)
        {
            CAMX_LOG_ERROR(CamxLogGroupCore, "Out of memory!");
            result = CamxResultENoMemory;
        }
        else
        {
            UINT32 sectionCount = 0;
 
            for (UINT32 i = 0; i < m_vendorTagInfo.numSections; i++)
            {
                result = CopyVendorTagSectionData(&pSectionData[i], &m_vendorTagInfo.pVendorTagDataArray[i]);
                if (CamxResultSuccess != result)
                {
                    CAMX_LOG_ERROR(CamxLogGroupCore, "Failed to copy existing vendor tag section!");
                    break;
                }
                else
                {
                    sectionCount++;
                }
            }
 
            if (CamxResultSuccess == result)
            {
                CAMX_ASSERT(sectionCount == m_vendorTagInfo.numSections);
                for (UINT32 i = 0; i < pVendorTagInfoToAppend->numSections; i++)
                {
                    result = CopyVendorTagSectionData(&pSectionData[m_vendorTagInfo.numSections + i],
                                                      &pVendorTagInfoToAppend->pVendorTagDataArray[i]);
                    if (CamxResultSuccess != result)
                    {
                        CAMX_LOG_ERROR(CamxLogGroupCore, "Failed to copy pass in vendor tag section!");
                        break;
                    }
                    else
                    {
                        sectionCount++;
                    }
                }
            }
 
            if (CamxResultSuccess == result)
            {
                CAMX_ASSERT(sectionCount == (pVendorTagInfoToAppend->numSections + m_vendorTagInfo.numSections));
 
                FreeVendorTagInfo(&m_vendorTagInfo);
 
                m_vendorTagInfo.pVendorTagDataArray = pSectionData;
                m_vendorTagInfo.numSections         = sectionCount; //此section有几个tag
            }
            else
            {
                CAMX_ASSERT(sectionCount != (pVendorTagInfoToAppend->numSections + m_vendorTagInfo.numSections));
                CAMX_LOG_ERROR(CamxLogGroupCore, "Failed to append vendor tag section!");
 
                for (UINT32 i = 0; i < sectionCount; i++)
                {
                    FreeVendorTagSectionData(&pSectionData[i]);
                }
 
                CAMX_FREE(pSectionData);
                pSectionData = NULL;
            }
        }
    }
 
    return result;
}

VendorTagManager::GetAllTags

获取所有vendorTag,整合成一张table,可方便的通过index访问所有vendorTag。


// VendorTagManager::GetAllTags

VOID VendorTagManager::GetAllTags(
    VendorTag*           pVendorTags,
    CHITAGSECTIONVISIBILITY visibility)
{
    const CHIVENDORTAGINFO* pVendorTagInfo = VendorTagManager::GetInstance()->GetVendorTagInfo();
    UINT32                  size           = 0;
 
    if (NULL != pVendorTagInfo)
    {
        for (UINT32 i = 0; i < pVendorTagInfo->numSections; i++)
        {
            if ((pVendorTagInfo->pVendorTagDataArray[i].visbility == ChiTagSectionVisibleToAll) ||
                    ChiTagSectionVisibleToAll == visibility ||
                    Utils::IsBitMaskSet(pVendorTagInfo->pVendorTagDataArray[i].visbility,
                    static_cast<UINT32>(visibility)))
            {
                for (UINT32 j = 0; j < pVendorTagInfo->pVendorTagDataArray[i].numTags; j++)
                {
                    pVendorTags[size + j] = pVendorTagInfo->pVendorTagDataArray[i].firstVendorTag + j; //所有vendorTag的完整tagID
                }
 
                size += pVendorTagInfo->pVendorTagDataArray[i].numTags;
            }
        }
    }
}
 
//维护[tagName, tagID]的哈希表,为什么?
VendorTagManager::QueryVendorTagLocation(
        for (UINT32 i = 0; i < pVendorTagInfo->numSections; i++)
        {
            if (0 == OsUtils::StrCmp(pVendorTagInfo->pVendorTagDataArray[i].pVendorTagSectionName, pSectionName))
            {
                for (UINT32 j = 0; j < pVendorTagInfo->pVendorTagDataArray[i].numTags; j++)
                {
                    if (0 == OsUtils::StrCmp(pVendorTagInfo->pVendorTagDataArray[i].pVendorTagaData[j].pVendorTagName,
                                             pTagName))
                    {
                        *pTagLocation = pVendorTagInfo->pVendorTagDataArray[i].firstVendorTag + j;
                        if (TRUE == validKeyStr)
                        {
                            VendorTagManager::GetInstance()->AddTagToHashMap(hash, pTagLocation);
                        }
                        result = CamxResultSuccess;
                        break;
                    }
                }
 
                break;
            }
        }

Property

property是高通私有的tag,一般由高通定义,不希望OEM做修改,他们的section分布如下:

/// @brief Additional classification of tags/properties
enum class PropertyGroup : UINT32
{
    Result    = 0x3000, ///< Properties found in the result pool
    Internal  = 0x4000, ///< Properties found in the internal pool
    Usecase   = 0x5000, ///< Properties found in the usecase pool
    DebugData = 0x6000, ///< Properties found in the debug data pool
};
 
static const PropertyID PropertyIDPerFrameResultBegin    = static_cast<UINT32>(PropertyGroup::Result)    << 16;
static const PropertyID PropertyIDPerFrameInternalBegin  = static_cast<UINT32>(PropertyGroup::Internal)  << 16;
static const PropertyID PropertyIDUsecaseBegin           = static_cast<UINT32>(PropertyGroup::Usecase)   << 16;
static const PropertyID PropertyIDPerFrameDebugDataBegin = static_cast<UINT32>(PropertyGroup::DebugData) << 16;

他们的定义如下:

/* Beginning of MainProperty */
static const PropertyID PropertyIDAECFrameControl                     = PropertyIDPerFrameResultBegin + 0x00;
static const PropertyID PropertyIDAECFrameInfo                        = PropertyIDPerFrameResultBegin + 0x01;
static const PropertyID PropertyIDAWBFrameControl                     = PropertyIDPerFrameResultBegin + 0x02;
static const PropertyID PropertyIDAWBFrameInfo                        = PropertyIDPerFrameResultBegin + 0x03;
static const PropertyID PropertyIDAFFrameControl                      = PropertyIDPerFrameResultBegin + 0x04;
static const PropertyID PropertyIDAFFrameInfo                         = PropertyIDPerFrameResultBegin + 0x05;
static const PropertyID PropertyIDAFPDFrameInfo                       = PropertyIDPerFrameResultBegin + 0x06;
static const PropertyID PropertyIDAFBAFDependencyMet                  = PropertyIDPerFrameResultBegin + 0x07;
static const PropertyID PropertyIDAFStatsProcessingDone               = PropertyIDPerFrameResultBegin + 0x08;
...
 
 
/* Beginning of InternalProperty */
static const PropertyID PropertyIDAECInternal                         = PropertyIDPerFrameInternalBegin + 0x00;
static const PropertyID PropertyIDAFInternal                          = PropertyIDPerFrameInternalBegin + 0x01;
static const PropertyID PropertyIDASDInternal                         = PropertyIDPerFrameInternalBegin + 0x02;
static const PropertyID PropertyIDAWBInternal                         = PropertyIDPerFrameInternalBegin + 0x03;
static const PropertyID PropertyIDAFDInternal                         = PropertyIDPerFrameInternalBegin + 0x04;
static const PropertyID PropertyIDBasePDInternal                      = PropertyIDPerFrameInternalBegin + 0x05;
static const PropertyID PropertyIDISPAECBG                            = PropertyIDPerFrameInternalBegin + 0x06;
static const PropertyID PropertyIDISPAWBBGConfig                      = PropertyIDPerFrameInternalBegin + 0x07;
static const PropertyID PropertyIDISPBFConfig                         = PropertyIDPerFrameInternalBegin + 0x08;
...
 
/* Beginning of UsecaseProperty */
static const PropertyID PropertyIDUsecaseSensorModes                  = PropertyIDUsecaseBegin + 0x00;
static const PropertyID PropertyIDUsecaseBatch                        = PropertyIDUsecaseBegin + 0x01;
static const PropertyID PropertyIDUsecaseFPS                          = PropertyIDUsecaseBegin + 0x02;
static const PropertyID PropertyIDUsecaseLensInfo                     = PropertyIDUsecaseBegin + 0x03;
static const PropertyID PropertyIDUsecaseCameraModuleInfo             = PropertyIDUsecaseBegin + 0x04;
static const PropertyID PropertyIDUsecaseSensorCurrentMode            = PropertyIDUsecaseBegin + 0x05;
static const PropertyID PropertyIDUsecaseSensorTemperatureInfo        = PropertyIDUsecaseBegin + 0x06;
static const PropertyID PropertyIDUsecaseAWBFrameControl              = PropertyIDUsecaseBegin + 0x07;
static const PropertyID PropertyIDUsecaseAECFrameControl              = PropertyIDUsecaseBegin + 0x08;
...
 
/* Beginning of DebugDaGetAndroidMetaIDDebugDataAEC                        = PropertyIDPerFrameDebugDataBegin + 0x01;
static const PropertyID PropertyIDDebugDataAWB                        = PropertyIDPerFrameDebugDataBegin + 0x02;
static const PropertyID PropertyIDDebugDataAF                         = PropertyIDPerFrameDebugDataBegin + 0x03;
static const PropertyID PropertyIDDebugDataAFD                        = PropertyIDPerFrameDebugDataBegin + 0x04;
static const PropertyID PropertyIDTuningDataIFE                       = PropertyIDPerFrameDebugDataBegin + 0x05;
static const PropertyID PropertyIDTuningDataIPE                       = PropertyIDPerFrameDebugDataBegin + 0x06;
static const PropertyID PropertyIDTuningDataBPS                       = PropertyIDPerFrameDebugDataBegin + 0x07;
static const PropertyID PropertyIDTuningDataTFE                       = PropertyIDPerFrameDebugDataBegin + 0x08;
...

问题1:以上Property有什么区别,分别在什么case下用,怎么用?

后面的metaPool会提到

问题2:PropertyID怎么跟tagName匹配的?

Property只有mainProperty也就是resultProperty才对外暴露,存放在PrivatePropertyTag

///< org.codeaurora.qcamera3.internal_private section
s_VendorTagManagerSingleton.m_cachedVendorTags[VendorTagIndex::PrivatePropertyTag].pSectionName
= "org.codeaurora.qcamera3.internal_private";
s_VendorTagManagerSingleton.m_cachedVendorTags[VendorTagIndex::PrivatePropertyTag].pTagName
= "private_property";

问题3:mainProperty在PrivatePropertyTag是怎么存放的?

MetaBuffer::LinearMap::GetAndroidMeta时,将所有对android可暴露的tag从camxMetaBuffer解析出来,
包含camera meta,vendorTag,Property,具体哪些tag暴露可根据策略调整
 
//1.遍历部分mainProperty的tagID和data地址
for (UINT32 tagIndex = propertyStartIndex; tagIndex < propertyEndIndex; ++tagIndex)
{
    const MetadataInfo* pInfo   = HAL3MetadataUtil::GetMetadataInfoByIndex(tagIndex);
    Content&            rContent = m_pMetadataOffsetTable[tagIndex];
 
    if (TRUE == rContent.IsValid())
    {
        result = HAL3MetadataUtil::AppendPropertyPackingInfo(
            rContent.m_tag,
            rContent.m_pVaddr,
            &packingInfo);
    }
}
 
//2.打包以上mainProperty的tagID和data值,头部4字节存放一共多少个Property
//后面4字节为tagID,紧接着是dataValue;
//下一个4字节为tagID,紧接着继续是dataValue...
//dataValue长度是在camx预先定义好的,所以不需要一起打包进去
HAL3MetadataUtil::PackPropertyInfoToBlob(&packingInfo, pPropertyBlob);
 
//3.更新打包后的Property集合到PrivatePropertyTag
CamxResult resultLocal = HAL3MetadataUtil::UpdateMetadata(
    pAndroidMeta,
    propertyBlobId,
    pPropertyBlob,
    PropertyBlobSize,
    TRUE);
 
 
CamxResult HAL3MetadataUtil::PackPropertyInfoToBlob(
    PropertyPackingInfo* pPackingInfo,
    VOID*                pPropertyBlob)
{
    CamxResult result = CamxResultSuccess;
 
    if (NULL != pPropertyBlob)
    {
        UINT32* pCount = reinterpret_cast<UINT32*>(pPropertyBlob);
        *pCount = pPackingInfo->count;
        VOID* pWriteOffset = Utils::VoidPtrInc(pPropertyBlob, sizeof(pPackingInfo->count));
 
        for (UINT32 index = 0; index < pPackingInfo->count; index++)
        {
            // Write TagId
            SIZE_T tagSize = GetPropertyTagSize(pPackingInfo->tagId[index]);
            Utils::Memcpy(pWriteOffset, &pPackingInfo->tagId[index], sizeof(UINT32));
            pWriteOffset = Utils::VoidPtrInc(pWriteOffset, sizeof(UINT32));
 
            // Write tag address
            Utils::Memcpy(pWriteOffset, pPackingInfo->pAddress[index], tagSize);
            pWriteOffset = Utils::VoidPtrInc(pWriteOffset, tagSize);
        }
    }
    else
    {
        CAMX_LOG_ERROR(CamxLogGroupMeta, "Property blob is null");
        result = CamxResultEFailed;
    }
 
    return result;
}
 
CamxResult HAL3MetadataUtil::UnPackBlobToPropertyInfo(
    PropertyPackingInfo* pPackingInfo,
    VOID*                pPropertyBlob)
{
    // First 4 bytes correspoint to number of tags
    CamxResult result = CamxResultSuccess;
    if (NULL != pPropertyBlob)
    {
        UINT32* pCount = reinterpret_cast<UINT32*>(pPropertyBlob);
        pPackingInfo->count = *pCount;
 
        VOID* pReadOffset = Utils::VoidPtrInc(pPropertyBlob, sizeof(pPackingInfo->count));
        for (UINT32 i = 0; i < pPackingInfo->count; i++)
        {
            // First 4 bytes correspond to tagId followed by the tagValue
            pPackingInfo->tagId[i] = *reinterpret_cast<UINT32*>(pReadOffset);
            pReadOffset = Utils::VoidPtrInc(pReadOffset, sizeof(UINT32));
 
            SIZE_T tagSize = GetPropertyTagSize(pPackingInfo->tagId[i]);
            pPackingInfo->pAddress[i] = reinterpret_cast<BYTE*>(pReadOffset);
            pReadOffset = Utils::VoidPtrInc(pReadOffset, tagSize);
        }
    }
 
    else
    {
        CAMX_LOG_ERROR(CamxLogGroupMeta, "Property blob is null");
        result = CamxResultEFailed;
    }
    return result;
}

meta的统一管理

//未完待续....,后续跟新...

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1074243.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

什么是Fetch API?与传统的AJAX相比,有什么优势?

聚沙成塔每天进步一点点 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 欢迎来到前端入门之旅&#xff01;感兴趣的可以订阅本专栏哦&#xff01;这个专栏是为那些对Web开发感兴趣、刚刚踏入前端领域的朋友们量身打造的。无论你是完全的新手还是有一些基础的开发…

3分钟学会批量查询快递秘籍

随着网购的普及&#xff0c;我们经常需要查询快递来了解自己的包裹状态。然而&#xff0c;如果一个个手动查询&#xff0c;不仅费时而且麻烦。这时候&#xff0c;一款能够批量查询快递的软件就变得尤为重要。今天&#xff0c;我将向大家介绍一款名为“固乔快递查询助手”的软件…

Linux知识点 -- 网络基础 -- 数据链路层

Linux知识点 – 网络基础 – 数据链路层 文章目录 Linux知识点 -- 网络基础 -- 数据链路层一、数据链路层1.以太网2.以太网帧格式3.重谈局域网原理4.MAC地址5.MTU6.查看硬件地址和MTU的命令7.ARP协议 二、其他重要协议或技术1.DNS&#xff08;Domain Name System&#xff09;2.…

2008-2020年中国区域二氧化氮产品

简介&#xff1a; 针对OMI 2008-2020年数据中因行异常或者云造成缺失的对流程NO2数据进行填补&#xff0c;利用面积权重法将OMI和GOME数据统一重采样至0.25度的空间分辨率。前言 – 人工智能教程 二氧化氮&#xff08;Nitrogen Dioxide&#xff0c;简称NO2&#xff09;是一种…

Java每日笔试题错题分析(1)

Java每日笔试题错题分析&#xff08;1&#xff09; 一、错题知识点前瞻第26题第29题第34题第41题第50题 二、错题展示及其解析第26题第29题第34题第41题第50题 一、错题知识点前瞻 第26题 多个catch块中子类异常放在前面&#xff0c;父类异常放在后面&#xff0c;否则会产生编…

ssm173基于SSM的养老院老人健康监护平台设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用Vue技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…

保姆级微服务部署教程

大家好&#xff0c;我是鱼皮。 项目上线是每位学编程同学必须掌握的基本技能。之前我已经给大家分享过很多种上线单体项目的方法了&#xff0c;今天再出一期微服务项目的部署教程&#xff0c;用一种最简单的方法&#xff0c;带大家轻松部署微服务项目。 开始之前&#xff0c;…

《安富莱嵌入式周报》第324期:单对以太网技术实战,IROS2023迪士尼逼真机器人展示,数百万模具CAD文件下载,闭环步进电机驱动器,CANopen全解析

周报汇总地址&#xff1a;嵌入式周报 - uCOS & uCGUI & emWin & embOS & TouchGFX & ThreadX - 硬汉嵌入式论坛 - Powered by Discuz! 更新一期视频教程&#xff1a; 第8期ThreadX视频教程&#xff1a;应用实战&#xff0c;将裸机工程移植到RTOS的任务划分…

自助建站系统,一建建站系统api版,自动建站

安装推荐php7.2或7.2以下都行 可使用虚拟主机或者服务器进行搭建。 分站进入网站后台 域名/admin 初始账号123456qq.com密码123456 找到后台的网站设置 将主站域名及你在主站的通信secretId和通信secretKey填进去。 即可正常使用 通信secretId和通信secretKey在主站的【账号…

【Proteus仿真】【STM32单片机】病床呼叫系统设计

文章目录 一、功能简介二、软件设计三、实验现象联系作者 一、功能简介 本项目使用Proteus8仿真STM32单片机控制器&#xff0c;使用LCD1602液晶、按键、蜂鸣器LED、双机通信等。 主要功能&#xff1a; 系统运行后&#xff0c;LCD1602显示开机界面&#xff0c;当从机病床按键按…

ExoPlayer架构详解与源码分析(4)——整体架构

系列文章目录 ExoPlayer架构详解与源码分析&#xff08;1&#xff09;——前言 ExoPlayer架构详解与源码分析&#xff08;2&#xff09;——Player ExoPlayer架构详解与源码分析&#xff08;3&#xff09;——Timeline ExoPlayer架构详解与源码分析&#xff08;4&#xff09;—…

0-1背包理论基础详解

0-1背包问题&#xff1a;有 n 种物品&#xff0c;每种物品只有1个&#xff0c;求解将哪些物品装入背包里物品价值总和最大。 图片来自&#xff1a;代码随想录 0-1背包问题举例&#xff1a; 题目描述&#xff1a; 使用二维dp数组解决背包问题 动规五部曲&#xff1a; dp[i][j…

智能工厂MES系统,终端设备支持手机、PDA、工业平板、PC

一、开源项目简介 源计划智能工厂MES系统(开源版) 功能包括销售管理&#xff0c;仓库管理&#xff0c;生产管理&#xff0c;质量管理&#xff0c;设备管理&#xff0c;条码追溯&#xff0c;财务管理&#xff0c;系统集成&#xff0c;移动端APP。 二、开源协议 使用GPL-3.0开…

【python海洋专题十四】读取多个盐度nc数据画盐度季节变化图

本期内容 读取多个盐度文件&#xff1b;拼接数据在画盐度的季节分布图Part01. 使用数据 IAP 网格盐度数据集 数据详细介绍&#xff1a; 见文件附件&#xff1a; pages/file/dl?fid378649712527544320 全球温盐格点数据.pdf IAP_Global_ocean_gridded_product.pdf 全球温…

Android多线程学习:线程池(一)

一、概念 线程池&#xff1a;创建并维护一定数量的空闲线程&#xff0c;当有需要执行的任务&#xff0c;就交付给线程池中的一个线程&#xff0c;任务执行结束后&#xff0c;该线程也不会死亡&#xff0c;而是回到线程池中重新变为空闲状态。 线程池优点&#xff1a; 1、重用…

Linux系列---【查看mac地址】

查看mac地址命令 查看所有网卡命令 nmcli connection show 查看物理网卡mac地址 ifconfig 删除网卡 nmcli connection delete virbr0 禁用libvirtd.service systemctl disable libvirtd.service 启用libvirtd.service systemctl enable libvirtd.service

使用css 与 js 两种方式实现导航栏吸顶效果

position的属性我们一般认为有 position:absolute postion: relative position:static position:fixed position:inherit; position:initial; position:unset; 但是我最近发现了一个定位position:sticky 这个可以称为粘性定位。 这个粘性定位的元素会始终在那个位置 <st…

firefox的主题文件位置在哪?记录以防遗忘

这篇文章写点轻松的 最近找到了一个自己喜欢的firefox主题,很想把主题的背景图片找到,所以找了下主题文件所在位置 我的firefox版本:版本: 118.0.1 (64 位)主题名称: Sora Kawai 我的位置在 C:\Users\mizuhokaga\AppData\Roaming\Mozilla\Firefox\Profiles\w0e4e24v.default…

可视大盘 + 健康分机制,火山引擎 DataLeap 为企业降低资源优化门槛!

更多技术交流、求职机会&#xff0c;欢迎关注字节跳动数据平台微信公众号&#xff0c;回复【1】进入官方交流群 随着数仓及研发技术团队维护的数据量大、资源使用量大、成本越高、优化压力越大。如何主动发现无效或低效使用的资源&#xff0c;并且可以周期性高效的进行主动治理…

js 气泡上升和鼠标点击事件

效果图 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta name"viewport" content"widthdevice-width, initial-scale1.0" /><title>Document</title><style>bod…