AV1 编码器的"总指挥部"------从帧输入到比特流输出的完整调度枢纽
| 项目 | 内容 |
|---|---|
| 文件路径 | av1/encoder/encoder.c |
| 代码规模 | 5,430 行,约 214 KB |
| 函数数量 | 83 个(含 static 内部函数) |
| 角色定位 | AV1 编码器的高层编排核心,负责初始化、帧输入、编码循环、码率控制、环路滤波、参考帧管理和比特流输出 |
| 关键结构体 | AV1_PRIMARY、AV1_COMP、AV1_COMMON、AV1EncoderConfig |
| 编码模式 | 1-pass / 2-pass / 3-pass、ALLINTRA、REALTIME、SVC、FPMT |
目录
- [介绍:为什么 encoder.c 是"总指挥部"](#介绍:为什么 encoder.c 是"总指挥部")
- 核心数据结构体系
- 主控制流与函数关系图
- 重编码循环 (Recode Loop)
- [SuperRes 自动搜索算法](#SuperRes 自动搜索算法)
- 环路滤波管线
- [RTCD 函数指针调度](#RTCD 函数指针调度)
- 自适应量化 (AQ)
- [多 Pass 编码体系](#多 Pass 编码体系)
- [C 语言工程技巧集锦](#C 语言工程技巧集锦)
- 总结
一、介绍:为什么 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 INLINE 和 static AOM_INLINE 两种内联标记:
INLINE→ 标准 C99inline关键字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_frame 和 show_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 源码时少走弯路。