libaom encoder.c 深度源码分析

AV1 编码器的"总指挥部"------从帧输入到比特流输出的完整调度枢纽

项目 内容
文件路径 av1/encoder/encoder.c
代码规模 5,430 行,约 214 KB
函数数量 83 个(含 static 内部函数)
角色定位 AV1 编码器的高层编排核心,负责初始化、帧输入、编码循环、码率控制、环路滤波、参考帧管理和比特流输出
关键结构体 AV1_PRIMARYAV1_COMPAV1_COMMONAV1EncoderConfig
编码模式 1-pass / 2-pass / 3-pass、ALLINTRA、REALTIME、SVC、FPMT

目录

  1. [介绍:为什么 encoder.c 是"总指挥部"](#介绍:为什么 encoder.c 是"总指挥部")
  2. 核心数据结构体系
  3. 主控制流与函数关系图
  4. 重编码循环 (Recode Loop)
  5. [SuperRes 自动搜索算法](#SuperRes 自动搜索算法)
  6. 环路滤波管线
  7. [RTCD 函数指针调度](#RTCD 函数指针调度)
  8. 自适应量化 (AQ)
  9. [多 Pass 编码体系](#多 Pass 编码体系)
  10. [C 语言工程技巧集锦](#C 语言工程技巧集锦)
  11. 总结

一、介绍:为什么 encoder.c 是"总指挥部"

如果你把 libaom 编码器想象成一支军队,那么 encoder.c 就是总司令的指挥帐篷。它不亲自上一线"拼刺刀"------不会直接做运动估计、变换量化、熵编码;但它决定每一帧的命运:什么时候编码、用什么类型、给多少 bit、要不要重新编码、用不用超分辨率、参考谁不参考谁

核心职责: encoder.c 的工作就是将 aomenc 管道中的"输入帧"(YUV 原始数据)编排为一连串的编码操作,最终吐出 OBU(Open Bitstream Units)比特流。它是整个编码器对外暴露的 API 接口层与内部算法层之间的"粘合剂"。

这个文件包含 83 个函数 ,覆盖了从编码器生命周期管理(创建/销毁)到帧级编码调度、码率控制决策、环路滤波、参考帧池管理、多线程并行等全部高层逻辑。但有趣的是,encoder.c 本身不定义任何新结构体 ------所有数据结构都来自 av1/encoder/encoder.h 等头文件,它只是"消费"这些类型来完成编排。

通读这 5430 行代码,你会发现几个非常有意思的工程特征:

  • 大量中文注释标注关键逻辑(SuperRes 搜索、编码核心函数等)
  • 通过 X-Macro 模式驱动 RTCD 函数指针表的批量填充
  • setjmp / longjmp 错误恢复机制
  • do-while loop 实现的 recode 循环,最多重试 N 次直到 bytes 达标
  • 精细的 32 字节对齐内存分配以适配 SIMD

二、核心数据结构体系

要读懂 encoder.c,必须先理解四层嵌套的数据上下关系:

复制代码
                    ┌─────────────────────────────────────────────────┐
                    │  AV1_PRIMARY (ppi)                              │
                    │  跨帧共享:参考帧池、twopass统计、lookahead、    │
                    │  gf_group                                           │
                    └────────────────┬────────────────────────────────┘
                                     │  ppi->cpi
                                     ▼
      ┌──────────────┐    ┌─────────────────────────────────────────────────┐
      │ EncoderConfig │◄───│  AV1_COMP (cpi)                                │
      │   (oxcf)      │    │  单帧实例:编码配置(oxcf)、码控(rc)、线程(mt)     │
      ├──────────────┤    └────────────────┬────────────────────────────────┘
      │ RATE_CONTROL  │                    │  cpi->common
      │   (rc)        │                    ▼
      ├──────────────┤    ┌─────────────────────────────────────────────────┐
      │ SPEED_FEATURES│    │  AV1_COMMON (cm)                               │
      │   (sf)        │    │  编解码共享:cur_frame、ref_frame、seq_params、   │
      └──────────────┘    │  quant_params                                    │
                          └────────────────┬────────────────────────────────┘
                                           │  td.mb
                                           ▼
                          ┌─────────────────────────────────────────────────┐
                          │  MACROBLOCK (td.mb)                             │
                          │  块级状态:e_mbd(解码端)、rdmult、变换系数缓存      │
                          └─────────────────────────────────────────────────┘

结构体关系速查

结构体 典型别名 生命周期 核心职责
AV1_PRIMARY ppi 整个编码序列 参考帧池 (BufferPool)、两遍统计、lookahead 队列、GF group、RTC ref 配置
AV1_COMP cpi 单帧(FPMT 可有多个) 编码配置 (oxcf)、码率控制 (rc)、运动估计状态、线程信息 (mt_info)、TD 上下文
AV1_COMMON cm 与 AV1_COMP 绑定 当前帧/参考帧缓冲区、序列头、量化参数、分割特征、loop filter 信息
AV1EncoderConfig oxcf 整个编码序列 所有可配置项:RC 参数、量化配置、Tile 配置、SuperRes 配置、motion mode 配置
RATE_CONTROL rc 与 AV1_COMP 绑定 buffer level、target size、q index 搜索范围、over/undershoot 追踪
GF_GROUP gf_group 一个 GOP 帧类型序列 (update_type)、层深 (layer_depth)、并行级别 (frame_parallel_level)

三、主控制流与函数关系图

encoder.c 的主控制流可以用一句话概括:输入 → 决策 → 编码 → 滤波 → 输出 → 更新
#mermaid-svg-m8wAGKfo6fDZGCyt{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-m8wAGKfo6fDZGCyt .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-m8wAGKfo6fDZGCyt .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-m8wAGKfo6fDZGCyt .error-icon{fill:#552222;}#mermaid-svg-m8wAGKfo6fDZGCyt .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-m8wAGKfo6fDZGCyt .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-m8wAGKfo6fDZGCyt .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-m8wAGKfo6fDZGCyt .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-m8wAGKfo6fDZGCyt .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-m8wAGKfo6fDZGCyt .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-m8wAGKfo6fDZGCyt .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-m8wAGKfo6fDZGCyt .marker{fill:#333333;stroke:#333333;}#mermaid-svg-m8wAGKfo6fDZGCyt .marker.cross{stroke:#333333;}#mermaid-svg-m8wAGKfo6fDZGCyt svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-m8wAGKfo6fDZGCyt p{margin:0;}#mermaid-svg-m8wAGKfo6fDZGCyt .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-m8wAGKfo6fDZGCyt .cluster-label text{fill:#333;}#mermaid-svg-m8wAGKfo6fDZGCyt .cluster-label span{color:#333;}#mermaid-svg-m8wAGKfo6fDZGCyt .cluster-label span p{background-color:transparent;}#mermaid-svg-m8wAGKfo6fDZGCyt .label text,#mermaid-svg-m8wAGKfo6fDZGCyt span{fill:#333;color:#333;}#mermaid-svg-m8wAGKfo6fDZGCyt .node rect,#mermaid-svg-m8wAGKfo6fDZGCyt .node circle,#mermaid-svg-m8wAGKfo6fDZGCyt .node ellipse,#mermaid-svg-m8wAGKfo6fDZGCyt .node polygon,#mermaid-svg-m8wAGKfo6fDZGCyt .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-m8wAGKfo6fDZGCyt .rough-node .label text,#mermaid-svg-m8wAGKfo6fDZGCyt .node .label text,#mermaid-svg-m8wAGKfo6fDZGCyt .image-shape .label,#mermaid-svg-m8wAGKfo6fDZGCyt .icon-shape .label{text-anchor:middle;}#mermaid-svg-m8wAGKfo6fDZGCyt .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-m8wAGKfo6fDZGCyt .rough-node .label,#mermaid-svg-m8wAGKfo6fDZGCyt .node .label,#mermaid-svg-m8wAGKfo6fDZGCyt .image-shape .label,#mermaid-svg-m8wAGKfo6fDZGCyt .icon-shape .label{text-align:center;}#mermaid-svg-m8wAGKfo6fDZGCyt .node.clickable{cursor:pointer;}#mermaid-svg-m8wAGKfo6fDZGCyt .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-m8wAGKfo6fDZGCyt .arrowheadPath{fill:#333333;}#mermaid-svg-m8wAGKfo6fDZGCyt .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-m8wAGKfo6fDZGCyt .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-m8wAGKfo6fDZGCyt .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-m8wAGKfo6fDZGCyt .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-m8wAGKfo6fDZGCyt .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-m8wAGKfo6fDZGCyt .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-m8wAGKfo6fDZGCyt .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-m8wAGKfo6fDZGCyt .cluster text{fill:#333;}#mermaid-svg-m8wAGKfo6fDZGCyt .cluster span{color:#333;}#mermaid-svg-m8wAGKfo6fDZGCyt div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-m8wAGKfo6fDZGCyt .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-m8wAGKfo6fDZGCyt rect.text{fill:none;stroke-width:0;}#mermaid-svg-m8wAGKfo6fDZGCyt .icon-shape,#mermaid-svg-m8wAGKfo6fDZGCyt .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-m8wAGKfo6fDZGCyt .icon-shape p,#mermaid-svg-m8wAGKfo6fDZGCyt .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-m8wAGKfo6fDZGCyt .icon-shape .label rect,#mermaid-svg-m8wAGKfo6fDZGCyt .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-m8wAGKfo6fDZGCyt .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-m8wAGKfo6fDZGCyt .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-m8wAGKfo6fDZGCyt :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Yes
Yes
av1_receive_raw_frame

接收原始帧
av1_get_compressed_data

主入口
av1_encode_strategy

编码策略决策
av1_encode

编码分发
统计收集?

is_stat_generation_stage
1-pass 或 2-pass?
av1_first_pass

第一遍统计收集
encode_frame_to_data_rate

帧级编码
帧类型判断 / drop / SSIM / delta-Q
encode_with_and_without_superres

SuperRes 决策
encode_with_recode_loop_and_filter

Recode Loop + 滤波
encode_with_recode_loop

重编码循环
encode_without_recode

无重编码
av1_encode_frame

块级编码核心
loopfilter_frame

环路滤波管线
① DBF

去块效应滤波
② CDEF

方向增强滤波
③ SuperRes

超分辨率后处理
④ LoopRestore

Wiener / SGR
av1_pack_bitstream

OBU 比特流输出
av1_post_encode_updates

编码后更新
refresh_reference_frames

刷新参考帧
av1_rc_postencode_update

码控状态更新
av1_twopass_postencode_update

两遍统计更新
包数据返回 → 下一帧

关键观察: 从图中可以看出 encoder.c 的核心哲学------"分层决策,逐步细化" 。从最外层的 av1_get_compressed_data 到最内层的 av1_encode_frame,每一层都在做不同粒度的决策,而 encoder.c 刚好位于"宏观调度"层,不触及像素级别的细节。

编码路径分叉逻辑

av1_encode() 中有一个清晰的分叉点,决定了走哪种编码路径:

c 复制代码
int av1_encode(AV1_COMP *const cpi, ...) {
  // 2pass 编码的第一遍:收集帧统计信息
  if (is_stat_generation_stage(cpi)) {
    av1_first_pass(cpi, frame_input->ts_duration); // 统计收集
  }
  // 1pass 或 2pass 的第二遍:真正的编码
  else if (cpi->oxcf.pass == AOM_RC_ONE_PASS ||
           cpi->oxcf.pass >= AOM_RC_SECOND_PASS) {
    encode_frame_to_data_rate(cpi, &frame_results->size, dest);
  }
}

四、重编码循环 (Recode Loop)

码率控制中最核心的机制之一就是 Recode Loop ------如果第一次编码产出的 bit 数偏离目标太远,就调整 QP 重新编码,直到 bit 数收敛到可接受范围。这在 encode_with_recode_loop() 中实现。

算法原理

Recode Loop 的核心思想类似于二分查找:

Qnext = adjust(Qcurrent, projected_bits, target_bits, q_low, q_high)

其中 q_low(undershoot 边界)和 q_high(overshoot 边界)逐步收紧,形成一个 QP 搜索窗口。调整逻辑在 recode_loop_update_q() 中实现(位于 rc_utils.c)。

完整的 do-while 循环结构

c 复制代码
static int encode_with_recode_loop(AV1_COMP *cpi, ...) {
  int loop = 0;
  int loop_count = 0;
  int q_low = 0, q_high = 0;
  int overshoot_seen = 0, undershoot_seen = 0;
  cpi->num_frame_recode = 0;

  av1_set_size_dependent_vars(cpi, &q, &bottom_index, &top_index);
  q_low = bottom_index;
  q_high = top_index;

  do {
    loop = 0;

    // 1. 缩放源帧和参考帧
    cpi->source = av1_realloc_and_scale_if_required(...);
    // 2. 缩放参考帧
    av1_scale_references(cpi, ...);
    // 3. 设置量化器
    av1_set_quantizer(cm, ..., q, ...);
    // 4. 自适应量化设置
    if (q_cfg->aq_mode == VARIANCE_AQ)
      av1_vaq_frame_setup(cpi);
    else if (q_cfg->aq_mode == COMPLEXITY_AQ)
      av1_setup_in_frame_q_adj(cpi);
    // 5. 编码核心函数
    av1_encode_frame(cpi);  // 编码核心函数
    // 6. Dummy pack 获取预估 bit 数
    av1_pack_bitstream(cpi, dest, size, ...);
    rc->projected_frame_size = (int)(*size) << 3;
    // 7. 决定是否重编码
    recode_loop_update_q(cpi, &loop, &q, &q_low, &q_high, ...);
    if (loop) {
      ++loop_count;
      cpi->num_frame_recode = AOMMIN(
          cpi->num_frame_recode + 1, NUM_RECODES_PER_FRAME - 1);
    }
  } while (loop);

  return AOM_CODEC_OK;
}

关键细节:

  • loop 是一个局部变量,在每次迭代开头重置为 0;recode_loop_update_q 通过指针将它设回 1 表示需要继续循环。
  • loop_count 用于追踪重编码次数(loop_count==0 是真首次),首次有特殊逻辑(如 av1_setup_frame 只在首次调用)。
  • 每次迭代中缩放操作和参考帧缩放是完全重做的,因为 QP 变化可能导致分辨率变化。
  • NUM_RECODES_PER_FRAME 限制了最大重编码次数,防止死循环。

Dummy Pack 技巧

一个值得注意的工程技巧是 Dummy Pack:在 recode loop 中,编码完成后先做一次"假打包"(dummy pack)来精确估算帧大小,再决定是否需要调整 QP。这避免了在不需要重编码时做真正的比特流输出。

c 复制代码
// Dummy pack of the bitstream using up to date stats to get an
// accurate estimate of output frame size to determine if we need
// to recode.
const int do_dummy_pack =
    (cpi->sf.hl_sf.recode_loop >= ALLOW_RECODE_KFARFGF &&
     oxcf->rc_cfg.mode != AOM_Q) ||
    oxcf->rc_cfg.min_cr > 0;
if (do_dummy_pack) {
  av1_finalize_encoded_frame(cpi);
  av1_pack_bitstream(cpi, dest, size, &largest_tile_id);
  rc->projected_frame_size = (int)(*size) << 3;  // bits
}

⚠️ 踩坑点:AOM_Q(固定 QP)模式下跳过 dummy pack,因为这种模式下不需要基于 bit 数反馈来调整 QP。但如果设置了 min_cr > 0(最小压缩比),即使 AOM_Q 模式也需要 dummy pack 来检查是否满足压缩比要求。


五、SuperRes 自动搜索算法

encode_with_and_without_superres() 是 encoder.c 中最复杂的算法之一,包含显式中文注释,体现了开发者对该算法的特别关注。这个算法回答了:"对这帧画面,用超分辨率缩放来编码是否划算?"

两种搜索模式

模式 策略 适用场景
SUPERRES_AUTO_ALL 遍历所有分母(SCALE_NUMERATOR+1 ~ 2×SCALE_NUMERATOR),每个都编码一遍并比较 RD cost 非 overlay 帧
SUPERRES_AUTO_DUAL 只比较"有超分"和"无超分"两种配置 快速模式

SUPER RES_AUTO_ALL 算法流程

#mermaid-svg-Fm6bjGC5emcM16Xx{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Fm6bjGC5emcM16Xx .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Fm6bjGC5emcM16Xx .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Fm6bjGC5emcM16Xx .error-icon{fill:#552222;}#mermaid-svg-Fm6bjGC5emcM16Xx .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Fm6bjGC5emcM16Xx .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Fm6bjGC5emcM16Xx .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Fm6bjGC5emcM16Xx .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Fm6bjGC5emcM16Xx .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Fm6bjGC5emcM16Xx .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Fm6bjGC5emcM16Xx .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Fm6bjGC5emcM16Xx .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Fm6bjGC5emcM16Xx .marker.cross{stroke:#333333;}#mermaid-svg-Fm6bjGC5emcM16Xx svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Fm6bjGC5emcM16Xx p{margin:0;}#mermaid-svg-Fm6bjGC5emcM16Xx .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Fm6bjGC5emcM16Xx .cluster-label text{fill:#333;}#mermaid-svg-Fm6bjGC5emcM16Xx .cluster-label span{color:#333;}#mermaid-svg-Fm6bjGC5emcM16Xx .cluster-label span p{background-color:transparent;}#mermaid-svg-Fm6bjGC5emcM16Xx .label text,#mermaid-svg-Fm6bjGC5emcM16Xx span{fill:#333;color:#333;}#mermaid-svg-Fm6bjGC5emcM16Xx .node rect,#mermaid-svg-Fm6bjGC5emcM16Xx .node circle,#mermaid-svg-Fm6bjGC5emcM16Xx .node ellipse,#mermaid-svg-Fm6bjGC5emcM16Xx .node polygon,#mermaid-svg-Fm6bjGC5emcM16Xx .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Fm6bjGC5emcM16Xx .rough-node .label text,#mermaid-svg-Fm6bjGC5emcM16Xx .node .label text,#mermaid-svg-Fm6bjGC5emcM16Xx .image-shape .label,#mermaid-svg-Fm6bjGC5emcM16Xx .icon-shape .label{text-anchor:middle;}#mermaid-svg-Fm6bjGC5emcM16Xx .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Fm6bjGC5emcM16Xx .rough-node .label,#mermaid-svg-Fm6bjGC5emcM16Xx .node .label,#mermaid-svg-Fm6bjGC5emcM16Xx .image-shape .label,#mermaid-svg-Fm6bjGC5emcM16Xx .icon-shape .label{text-align:center;}#mermaid-svg-Fm6bjGC5emcM16Xx .node.clickable{cursor:pointer;}#mermaid-svg-Fm6bjGC5emcM16Xx .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Fm6bjGC5emcM16Xx .arrowheadPath{fill:#333333;}#mermaid-svg-Fm6bjGC5emcM16Xx .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Fm6bjGC5emcM16Xx .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Fm6bjGC5emcM16Xx .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Fm6bjGC5emcM16Xx .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Fm6bjGC5emcM16Xx .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Fm6bjGC5emcM16Xx .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Fm6bjGC5emcM16Xx .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Fm6bjGC5emcM16Xx .cluster text{fill:#333;}#mermaid-svg-Fm6bjGC5emcM16Xx .cluster span{color:#333;}#mermaid-svg-Fm6bjGC5emcM16Xx div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Fm6bjGC5emcM16Xx .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Fm6bjGC5emcM16Xx rect.text{fill:none;stroke-width:0;}#mermaid-svg-Fm6bjGC5emcM16Xx .icon-shape,#mermaid-svg-Fm6bjGC5emcM16Xx .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Fm6bjGC5emcM16Xx .icon-shape p,#mermaid-svg-Fm6bjGC5emcM16Xx .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Fm6bjGC5emcM16Xx .icon-shape .label rect,#mermaid-svg-Fm6bjGC5emcM16Xx .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Fm6bjGC5emcM16Xx .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Fm6bjGC5emcM16Xx .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Fm6bjGC5emcM16Xx :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Yes
No
保存编码上下文

av1_save_all_coding_context(cpi)
遍历所有超分比例

denom in SCALE_NUMERATOR+1 .. 2×SCALE_NUMERATOR
用超分编码

encode_with_recode_loop_and_filter()
记录 ssedenom, ratedenom
恢复编码上下文

restore_all_coding_context(cpi)
无超分编码 (full-res)

encode_with_recode_loop_and_filter()
记录 sse2, rate2
遍历所有比例,计算 RDCOST

RDCOST = rdmult × rate + sse
SR RDCost < Full-Res RDCost?
用 best_denom 再次编码

assert(sse1 == sse3) 验证一致性
循环结束 / 使用全分辨率结果

⚠️ 踩坑点: 这个算法有一个性能上的"伤疤"------当超分被选中时,需要用最优分母再完整编码一次 (第三次编码),因为前两次循环中编码器状态已经被后续操作破坏了。代码中的 TODO 注释也坦承了这一点:"We should avoid rerunning the recode loop by saving previous output+state" 。好在代码用 assert(sse1 == sse3) 验证了重编码结果的一致性。

RD Cost 公式

RDCOST = rdmult × Rate + SSE_distortion

其中 rdmult 通过以下函数根据 base qindex 计算,并且超分和无超分两种情况使用同一个 rdmult------这是因为两者都以全分辨率下的 base qindex 为基准。

c 复制代码
const int64_t rdmult = av1_compute_rd_mult_based_on_qindex(
    bit_depth, update_type, cm->quant_params.base_qindex);
const double this_rdcost = RDCOST_DBL_WITH_NATIVE_BD_DIST(
    rdmult, this_rate, this_sse, bit_depth);

六、环路滤波管线

AV1 的环路滤波(In-Loop Filter)是编码端的质量关键环节。encoder.c 中的 loopfilter_frame()cdef_restoration_frame() 按固定顺序驱动四个滤波阶段:

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────┐
│                              环路滤波管线 (In-Loop Filter Pipeline)                      │
├───────────────┬───────────────────┬──────────────────────┬──────────────────────────────┤
│     ① DBF     │      ② CDEF       │     ③ SuperRes       │       ④ LoopRestore           │
│  去块效应滤波   │   方向增强滤波      │   超分辨率后处理        │   Wiener / SGR / 自导引        │
├───────────────┼───────────────────┼──────────────────────┼──────────────────────────────┤
│  av1_pick     │  av1_cdef_search  │  av1_superres_scaled  │  av1_pick_filter_             │
│  _filter_level│  av1_cdef_frame   │  av1_superres_post    │  restoration                  │
│  av1_loop_    │  av1_cdef_frame_mt│  _encode              │  av1_loop_restoration_        │
│  filter_frame │                   │                       │  filter_frame_mt              │
│  _mt          │                   │                       │                              │
├───────────────┼───────────────────┼──────────────────────┼──────────────────────────────┤
│  lf_row_sync  │    cdef_sync      │        ---             │      lr_row_sync              │
│  (DBF行同步)   │  (CDEF行同步)     │                       │   (LR行同步)                  │
└───────────────┴───────────────────┴──────────────────────┴──────────────────────────────┘
              │                   │                      │                              │
              └───────────────────┴──────────────────────┴──────────────────────────────┘
                                    这些滤波器的输出会作为后续帧的参考帧,形成"环路"

多线程架构: 每个滤波阶段都有独立的多线程基础设施------lf_row_sync(DBF 行同步)、cdef_sync(CDEF 同步)、lr_row_sync(LR 行同步)。编码器根据 mt_info->num_mod_workers[MOD_xxx] 决定每个阶段使用多少线程。

还需要注意 skip_apply_postproc_filters 位掩码------它允许对非参考帧跳过某些后处理步骤以节省计算:

c 复制代码
const unsigned int skip_apply_postproc_filters =
    derive_skip_apply_postproc_filters(cpi, use_loopfilter,
                                       use_cdef, use_superres, use_restoration);
// 各 flag 含义:
// SKIP_APPLY_LOOPFILTER, SKIP_APPLY_CDEF,
// SKIP_APPLY_SUPERRES, SKIP_APPLY_RESTORATION

七、RTCD 函数指针调度

encoder.c 中最具冲击力的 C 语言模式就是 X-Macro 驱动的 RTCD (Run-Time CPU Detection) 函数指针表填充。这是 AV1 编码器性能的核心------通过编译时宏展开 + 运行时 CPU 检测,用一套代码适配 SSE、AVX、NEON 等多种 SIMD 指令集。

BFP 宏:标准块大小函数注册

c 复制代码
#define BFP(BT, SDF, SDAF, VF, SVF, SVAF, SDX4DF, SDX3DF, JSDAF, JSVAF) \
  ppi->fn_ptr[BT].sdf    = SDF;      /* SAD             */              \
  ppi->fn_ptr[BT].sdaf   = SDAF;     /* SAD + avg       */              \
  ppi->fn_ptr[BT].vf     = VF;       /* Variance        */              \
  ppi->fn_ptr[BT].svf    = SVF;      /* Sub-pixel Var   */              \
  ppi->fn_ptr[BT].svaf   = SVAF;     /* Sub-pixel Avg V */              \
  ppi->fn_ptr[BT].sdx4df = SDX4DF;   /* 4-ref SAD       */              \
  ppi->fn_ptr[BT].jsdaf  = JSDAF;    /* Dist-wtd SADAvg */              \
  ppi->fn_ptr[BT].jsvaf  = JSVAF;    /* Dist-wtd SVAF   */              \
  ppi->fn_ptr[BT].sdx3df = SDX3DF;   /* 3-ref SAD       */

使用它只需要一行------以 BLOCK_8X8 为例:

c 复制代码
BFP(BLOCK_8X8,
     aom_sad8x8,                     // sdf
     aom_sad8x8_avg,                 // sdaf
     aom_variance8x8,                // vf
     aom_sub_pixel_variance8x8,      // svf
     aom_sub_pixel_avg_variance8x8,  // svaf
     aom_sad8x8x4d,                  // sdx4df
     aom_sad8x8x3d,                  // sdx3df
     aom_dist_wtd_sad8x8_avg,        // jsdaf
     aom_dist_wtd_sub_pixel_avg_variance8x8)  // jsvaf

四类宏对比

用途 覆盖块大小
BFP 标准 SAD / Variance / Sub-pixel / 4-ref / 3-ref / Dist-wtd 全部 22 个方形+矩形块
OBFP OBMC (Overlapped Block Motion Compensation) SAD / Var ≥ 8×8 的 22 个块(无 4×8/8×4/4×4)
MBFP Masked SAD / Sub-pixel Var(复合帧) ≥ 8×8 的 22 个块
SDSFP Skip-mode SAD 部分大块

💡 C 语言技巧: 这是典型的 X-Macro 模式(也叫 Macro-driven Code Generation)。通过宏抽象出"对每种块大小重复做同样绑定"的样板代码,将 10 个函数指针 × 22 种块大小 = 220 行赋值语句压缩到 22 行宏调用。更妙的是,这些符号在链接/RTCD 阶段会被解析为对应 CPU 架构的最优实现。


八、自适应量化 (AQ)

encoder.c 中 AQ 不是直接实现的,而是作为"调度者"在合适的时机调用对应模块的函数。AV1 支持三种 AQ 模式:

复制代码
┌─────────────────────────────────┬──────────────────────────────────┬──────────────────────────────────┐
│        VARIANCE_AQ              │         COMPLEXITY_AQ             │       CYCLIC_REFRESH_AQ            │
│       方差自适应量化              │        复杂度自适应量化             │       循环刷新量化                  │
├─────────────────────────────────┼──────────────────────────────────┼──────────────────────────────────┤
│  av1_vaq_frame_setup(cpi)       │  av1_setup_in_frame_q_adj(cpi)   │  av1_cyclic_refresh_setup(cpi)    │
│  高方差的"平坦区域"使用更精细量化   │  基于纹理复杂度分配 QP 偏移          │  周期性地对不同区域使用不同 QP       │
│                                 │                                  │                                  │
│  适用场景:质量优先编码            │  适用场景:通用编码                 │  适用场景:RTC / 视频会议            │
└─────────────────────────────────┴──────────────────────────────────┴──────────────────────────────────┘

Delta-Q 模式

除了块级 AQ,encoder.c 还管理更精细的 Delta-Q 机制:

Delta-Q 模式 调用位置 机制
DELTA_Q_PERCEPTUAL encode_frame_to_data_rate 之前的 Haar 能量计算 用整个帧的 Haar 小波能量(AC 系数)作为感知复杂度指标
DELTA_Q_PERCEPTUAL_AI encode_frame_to_data_rate 全帧内模式:基于 Wiener 方差的块级 QP 偏移
DELTA_Q_USER_RATING_BASED encode_frame_to_data_rate 用户评分驱动:外部提供 per-block 质量权重

Haar 能量计算

calculate_frame_avg_haar_energy() 在 2-pass 编码的第二遍中计算每帧的平均 Haar 小波能量,用于感知 Delta-Q 调整:

c 复制代码
static void calculate_frame_avg_haar_energy(AV1_COMP *cpi) {
  // 仅用于 2-pass DELTA_Q_PERCEPTUAL 模式
  if (is_one_pass_rt_params(cpi) ||
      (cpi->oxcf.q_cfg.deltaq_mode != DELTA_Q_PERCEPTUAL) ||
      (is_fp_wavelet_energy_invalid(total_stats) == 0))
    return;

  // 计算整帧的 Haar AC 能量
  int64_t frame_avg_wavelet_energy = av1_haar_ac_sad_mxn_uint8_input(
      src, stride, hbd, num_8x8_rows, num_8x8_cols);

  // log1p 归一化
  cpi->twopass_frame.frame_avg_haar_energy =
      log1p((double)frame_avg_wavelet_energy / num_mbs);
}

💡 为什么用 Haar 小波? Haar 变换是最简单的离散小波变换,它本质上就是相邻像素的差。在 AV1 中,Haar AC 能量被用作"帧复杂度"的轻量级代理指标------计算快,且与人类主观感知复杂度有良好的相关性。log1p 归一化使分布更接近正态,便于后续的 Delta-Q 映射。


九、多 Pass 编码体系

encoder.c 对多 Pass 编码的支持是全方位的:从 1-pass 到 3-pass,每个阶段有不同的职责。

复制代码
  ┌───────────────────────┐      ┌───────────────────────┐      ┌────────────────────────┐
  │    First Pass         │      │    Second Pass         │      │    Third Pass           │
  │    (统计收集)           │ ───► │    (正式编码)           │ ───► │    (VBR 精调)            │
  ├───────────────────────┤      ├───────────────────────┤      ├────────────────────────┤
  │  av1_first_pass()     │      │  av1_init_second_pass()│      │  VBR RC 模型离线优化     │
  │  收集帧内/帧间误差      │      │  基于 first-pass 统计   │      │  av1_pop_third_pass_    │
  │  MV统计、小波能量       │      │  做 GOP/帧类型决策      │      │  info() 获取每帧 QP      │
  └───────────────────────┘      └───────────────────────┘      └────────────────────────┘
                  │                          │                           │
                  └──────────────────────────┴───────────────────────────┘
              统计数据流转:twopass.total_stats → twopass.total_left_stats → subtract_stats

统计减法技巧 (subtract_stats)

在 2-pass 编码中,subtract_stats 实现了一个巧妙的设计:统计信息存储在 total_left_stats 中,每编码一帧就从其中减去该帧的统计信息。这样后续帧做决策时看到的是"剩余帧"的统计特征,而不是已经编码完成的帧的。

c 复制代码
static void subtract_stats(FIRSTPASS_STATS *section,
                           const FIRSTPASS_STATS *frame) {
  section->frame          -= frame->frame;
  section->weight         -= frame->weight;
  section->intra_error    -= frame->intra_error;
  section->coded_error    -= frame->coded_error;
  section->pcnt_inter     -= frame->pcnt_inter;
  section->pcnt_motion    -= frame->pcnt_motion;
  section->MVr            -= frame->MVr;
  section->MVc            -= frame->MVc;
  // ... 总共 18 个统计字段
}

💡 C 语言技巧: 直接在结构体字段上做逐字段减法,而不是写一个通用的 memcpy+循环。这样做的优势有两个:(1) 编译器可以进行更好的优化(连续内存访问模式清晰);(2) 代码自文档化,读代码时清楚地知道哪些字段被追踪。


十、C 语言工程技巧集锦

encoder.c 是学习工业级 C 语言工程实践的绝佳教材。以下是我从中提炼的 8 个关键技巧:

1. setjmp / longjmp 错误恢复

AV1 编码器在内存分配失败、参数非法等场景使用 setjmp/longjmp 做非局部跳转,而非层层检查返回值:

c 复制代码
int av1_get_compressed_data(AV1_COMP *cpi, ...) {
  // 设置"安全绳"
  if (setjmp(cm->error->jmp)) {
    cm->error->setjmp = 0;  // 关键:退出前清零
    return cm->error->error_code;
  }
  cm->error->setjmp = 1;  // 标记为"可以接受跳转"
  // ... 编码逻辑 ...
  cm->error->setjmp = 0;  // 正常退出前清零
}

⚠️ 注意: 注释明确说明 "The jmp_buf is valid only for the duration of the function that calls setjmp()"------意味着不能在函数返回后继续使用 jmp_buf。这是初学者容易踩的坑。

2. const 别名模式

encoder.c 中随处可见这样的模式:

c 复制代码
const AV1EncoderConfig *const oxcf = &cpi->oxcf;
const QuantizationCfg *const q_cfg = &oxcf->q_cfg;
const AV1_COMMON *const cm = &cpi->common;

这不仅是风格问题,更是编译器优化提示*const 告诉编译器这个指针本身不会被修改,const 前缀告诉编译器指向的内容不应被修改,因此可以做激进的常量传播和死代码消除。

3. 位掩码操作密集

参考帧管理大量使用位掩码:

c 复制代码
// 检查哪些参考帧被刷新
for (int i = 0; i < REF_FRAMES; i++)
  if ((current_frame->refresh_frame_flags >> i) & 1)
    // 刷新该参考帧

// XOR 切换标志位
if (flags & AOM_EFLAG_NO_REF_LAST)
  ref ^= AOM_LAST_FLAG;

4. 对齐内存分配

c 复制代码
// 所有关键结构都是 32 字节对齐
AV1_PRIMARY *ppi = (AV1_PRIMARY *)aom_memalign(32, sizeof(AV1_PRIMARY));
AV1_COMP *cpi    = (AV1_COMP *)aom_memalign(32, sizeof(AV1_COMP));

32 字节对齐是为了适配 AVX2 的 256-bit 寄存器宽度------一次 load 可以对齐到 cache line 边界。

5. 条件编译的天罗地网

encoder.c 中 #if CONFIG_xxx 非常多,涵盖:

编译宏 功能
CONFIG_REALTIME_ONLY 仅保留 RTC 模式,剪除 VOD 编码逻辑
CONFIG_DENOISE 2D 降噪 + 胶片颗粒参数估计
CONFIG_TUNE_VMAF VMAF 感知编码调优
CONFIG_TUNE_BUTTERAUGLI Butteraugli 感知调优
CONFIG_INTERNAL_STATS 内部统计(PSNR/SSIM/blockiness)
CONFIG_BITRATE_ACCURACY 比特率精度测试(关闭 recode loop)
CONFIG_THREE_PASS 第三遍 VBR 编码
CONFIG_RD_COMMAND 外部 RD 命令覆盖(研究/实验用)

6. AOM_INLINE 的语义

encoder.c 中有 static INLINEstatic AOM_INLINE 两种内联标记:

  • INLINE → 标准 C99 inline 关键字
  • AOM_INLINE → libaom 扩展,可能加 __attribute__((always_inline))__forceinline,强制内联而不依赖编译器启发式

7. IMPLIES 宏:逻辑蕴含

c 复制代码
// 如果 min_cr > 0,则必须允许 recode
assert(IMPLIES(oxcf->rc_cfg.min_cr > 0, allow_recode));

IMPLIES(A, B) 等价于 !A || B(如果 A 成立则 B 必须成立),是逻辑蕴含的标准写法,比 assert(!A || B) 语义更清晰。

8. 帧的"show"与"non-show"

AV1 中有一些特殊帧------如 ALTREF 和 OVERLAY 帧------它们被编码但不立即显示 。encoder.c 中的 show_frameshow_existing_frame 是该逻辑的核心:

  • show_existing_frame:显示一个之前编码好的帧,不产生新比特流(除了 header)
  • update_counters_for_show_frame:统计"真正显示"的帧计数
  • 层级预测结构中的 B 帧和 ARF 可能 show_frame = 0

十一、总结

通读完 encoder.c 的 5,430 行代码,我有以下几点最深感触:

1. 分层架构清晰但"胖": encoder.c 作为高层编排,本身不沉入具体算法,但通过函数调用将各个模块串联起来。问题是某些函数(如 encode_frame_to_data_rate 约 400 行)过长,虽然逻辑上没有深层嵌套,但阅读时需要在大量条件编译间跳跃。
2. 中英文混注的设计意图: 文件中的中文注释集中在 SuperRes 自动搜索和编码核心函数周围------说明这些是最容易让人困惑、最需要额外说明的地方,开发者有意识地增加了母语标注。
3. Recode Loop 是码率控制的核心引擎: 通过二分 QP 搜索 + Dummy Pack 预估 bit 数,对外暴露一个"自动收敛"的接口。但每次 recode 都要完整重编码(包括参考帧缩放),代价高昂。
4. X-Macro 模式值得学习: BFP/OBFP/MBFP/SDSFP 四类宏展示了如何优雅地处理"对多种类型重复相同操作"的 C 语言场景,且与 RTCD 的分发机制完美配合。
5. 条件编译是双刃剑: #if CONFIG_xxx 使代码体积可控(一个文件支持多种编译配置),但严重损害了代码可读性和跳转体验。如果目标只是理解核心流程,建议关注 CONFIG_REALTIME_ONLY 关闭后的路径(默认开启的路径通常是最完整的)。

最后,encoder.c 向我们展示了一个成熟的视频编码器是如何将"复杂"装进"有序"的框架的------从帧的输入、决策、编码、滤波到输出,每个环节的职责边界虽然偶有交叉,但整体上保持了一条从宏观到微观、从调度到执行的清晰路径。希望这篇分析能帮助你在阅读 AV1 源码时少走弯路。