总是有人说自己把代码和标准对应不起来。其实是因为你要么不知道标准各个章节讲的什么,要么不知道代码中各个函数的功能,或者两者都不知道。今天再以 X264 的帧内编码为例让大家体会一下读代码时该如何与标准对应。此贴是帖子“[原创]如何阅读代码”的延续,因此采用的代码与编译环境设置与其一样,此处不再赘述。
上贴说过 Encode_frame 函数包含最核心的编码代码,那么我们现在就 F11 进去看看。遇到的第一个函数是 x264_encoder_encode,再 F11 进去,执行到 x264_reference_update,它在干什么呢?顾名思义猜测一定是更新帧间参考要用到的一些内存空间,因为我们现在还没有编码,所以 F11 进去后没执行什么操作就出来了。
继续 F10,执行到 x264_frame_pop_unused。F11 跟进,然后 F10,发现它走了 x264_frame_new 的分支(x264_frame_pop 分支干什么用的呢?暂时先别管。跟着流程走,管多了就迷茫了),F11 跟进 x264_frame_new 发现通篇都是对变量结构体指针 frame 里的成员变量执行 CHECKED_MALLOC,由此我们可以初步判断它是在为帧结构体分配内存空间。
step out 跳出 x264_frame_pop_unused,F10 到 x264_frame_copy_picture,F11 进去读读代码我们就知道这个函数的功能是将待编码图像从 pic_in 复制到 fenc->plane。继续 F10,到了 x264_frame_push,通过阅读该函数的代码我们知道它的功能是将当前帧结构体从 fenc 移到 h->frames.next 中。后面的函数 x264_frame_init_lowres、x264_adaptive_quant_frame、x264_encoder_frame_end 都未被执行。既然没被执行,那我们现在暂时就不管它们。
F10 到 x264_stack_align( x264_slicetype_decide, h ); x264_stack_align 顾名思义无非就是平台优化方面考虑的对齐操作,因此这里我们要关心的是函数 x264_slicetype_decide,F11 我们会发现进不到 x264_slicetype_decide 里。怎么办呢?见下面第一个截图,将光标点到 x264_slicetype_decide 上,点鼠标右键选择 go to definition,然后先在里面的第一行代码下断点(见下面第二个截图),然后再按 F10 就可以进入到 x264_slicetype_decide 函数了。该函数顾名思义是来决定当前 slice 的编码类型的,即到底是 I 片还是 P 片或 B 片。通过浏览其代码,我们也会发现代码所做也正是这样。
step out 跳出 x264_slicetype_decide,继续 F10 执行到 x264_frame_push,这里实际要执行两个函数,因为 x264_frame_push 的第二个参数是函数 x264_frame_shift,所以会先执行它。F11 首先进入的就是 x264_frame_shift,然后 step out 跳出 x264_frame_shift,继续 F11 就进入了 x264_frame_push,通过阅读这两个简短的函数的代码,我们知道它们执行的操作是将当前编码帧结构体从 h->frames.next 移到 h->frames.current。
继续 F10,又到了一个 x264_frame_shift 函数,F11 进去通过阅读代码我们可以知道该函数的功能将当前帧结构体从 h->frames.current 移到 h->fenc(我有点奇怪,为什么 X264 要这么麻烦地把一个变量移来移去呢?一次搞定不行么?)。继续 F10,到了 x264_reference_reset,其功能顾名思义,也有英文注释,具体有什么用,现在我还不知道,暂时不管吧。
继续 F10,到了 x264_reference_build_list,顾名思义,参考列表构建,在 JM 里叫做参考列表初始化(JM86 对应的函数是 init_lists)。参考列表初始化的作用即构建帧间编码图像所需要用到的参考图像列表。那么如何初始化呢?如果大家记得 H.264 标准的各个章节的功能,那么就该知道 200503 版的 8.2.4 小节正是讲的这部分内容。这样这个函数就与标准的内容对应起来了。至于通过代码是如何实现的,先看懂了标准的这个部分再来读这个函数的代码吧。
继续 F10,到了 x264_ratecontrol_start,顾名思义进行码率控制的一些准备工作。
继续 F10,到了 x264_slice_init,顾名思义片初始化,做了哪些工作呢?F11 进去执行了分支 x264_slice_header_init,通过浏览其代码,我们发现通篇都是对结构体指针 sh 内的成员变量的赋值操作。后面的 x264_macroblock_slice_init 函数在干什么,大家自己 F11 进去看,看不懂没关系,反正就是给一些变量赋初值嘛。继续 F10,到了 bs_init,顾名思义是对码流相关的变量进行初始化,因为 bs 就是 bit stream 嘛。
继续 F10,到了 if( i_nal_type == NAL_SLICE_IDR && h->param.b_repeat_headers ),注意前面的英文注释 /* Write SPS and PPS */,意思就是这里在向码流中写 SPS 和 PPS。这里的三组函数,顾名思义第一组是在写 SEI、第二组是在写 SPS、第三组是在写 PPS。那么如何写码流呢?当然是要遵循语法表了。下面以写 SPS 为例简要说明一下,如果大家记得 H.264 标准的各个章节的功能,那么就该知道 200503 版的 7.3.2.1 小节就是 SPS 语法表。因为 7.3.2.1 规定了 SPS 在码流中的第一个语法元素是 profile_idc,因此当我们 F11 进入 x264_sps_write 的时候会发现该函数第一行代码正是在写 sps->i_profile_idc,标准规定 SPS 第二个语法元素是 constraint_set0_flag,因此该函数的第二行代码就是在写 sps->b_constraint_set0,其他同理。这里说的是写的顺序,那么写的方式是什么呢?H.264 中的熵编码方式细分起来有很多,每个语法表的最后一列 descriptor 规定的就是对应的语法元素采用哪种熵编码方法。例如:profile_idc 是 u(8),因此它采用 8 位无符号整数编码;constraint_set0_flag 是 u(1),因此它采用 1 比特无符号整数编码。各种熵编码方法在 200503 版 7.2 小节最后都有说明,此处不再赘述。好了,我们知道了各个语法元素采用什么方式编码,自然也就知道了代码中各个熵编码函数对应的是什么编码方式。例如:对 profile_idc 编码采用的是 bs_write 函数,当然这个函数的功能就是无符号熵编码了,对 i_id 编码采用的是 bs_write_ue 函数,当然这个函数的功能就是 ue(v)——无符号哥伦布编码。其他同理。其实这些我在帖子“[原创] 如何读标准和代码”中已经讲过了。
顺便提一下,对码流的读写操作都要依据语法表所定义的语法元素顺序和熵编码类型。上面讲的是编码的具体例子,解码的具体例子我以前用 JM 讲过,参考帖子“如何结合标准看JM代码(JM86)”。好了,继续 F10,到了 x264_slices_write。F11 进入,再 F10,到了 x264_stack_align( x264_slice_write, h );我们关心的是 x264_slice_write,进入该函数,方法在上面已经说过了。x264_slice_write 第一个函数为系统函数 memset,下一个为 x264_nal_start,其功能看下代码就知道是在设置将要写入码流的 NALU 的第一个字节的值。第二个函数 x264_slice_header_write 顾名思义是在向码流中写入片头。如果大家记得 H.264 标准的各个章节的功能,那么就该知道 200503 版的 7.3.3 小节就是片头的语法表。写码流的过程与上面 SPS 的过程同理,此处不再赘述。
F10 到了 while 循环,顾名思义根据 while 循环的循环条件猜测一下该 while 循环的功能,肯定就是循环对整个图像的每个宏块一次编码了。要验证一下猜测很简单,在 while 循环体的第一行下断点,按一次 F5 就观察一下 mb_xy 变量的值的变化情况。另外还有个信息说明了这一点,h->sh.i_last_mb 变量的值刚好等于待编码图像的总宏块数。
F10 到了 x264_fdec_filter_row,顾名思义猜测该函数的功能是去块滤波。如果大家记得 H.264 标准的各个章节的功能,那么就该知道 200503 版的 8.7 小节正是讲的这部分内容。要读懂这个函数的代码就先学习一下 8.7 小节吧。
F10 到了 x264_macroblock_cache_load,通过浏览代码我们知道是在对一些变量赋值,各个变量的含义顾名思义。这也属于编码前的准备工作。继续 F10 到了 x264_macroblock_analyse,看见英文注释了吧?不用我们顾名思义就知道它的功能了,是在进行模式选择。F11 进入该函数。第一个被调用的函数是 x264_ratecontrol_qp,顾名思义获取当前宏块 QP。第二个被调用的函数是 x264_mb_analyse_init,F11 进去后发现只有非 I 片才进行一些操作,那暂时就不管它。
F10 到了 x264_mb_cache_fenc_satd,F11 进去。一开始是个 4*4 的双重循环。我们现在是在对一个宏块进行操作,这里又出现 4*4 的循环,那么很明显了这个双重循环肯定是在计算每个 4*4 的块,下面的 2*2 的双重循环肯定是在计算 8*8 的块。因为宏块的尺寸是 16*16 嘛,宽高分成 4 份不正好是 4*4,分成 2 份不正好是 8*8 么?做视频的人应该对 4、8、16 等常用的数字敏感。先分析第一个 4*4 的双重循环。注意,for 循环里的 h->pixf.satd 和 h->pixf.sad 都是函数指针,因此要用 F11 跟进。h->pixf.satd 的两个输入是 zero 和 fenc,跟进之后的函数 pixel_satd_wxh 在计算他们之差,然后作 Hadamard 变换,然后计算 SATD。由此可以猜测 fenc 里存放的是原始待编码宏块(到底是不是呢?读者自己反回去找到 h->mb.pic.p_fenc[0] 被赋值的地方看看就知道了)。后面代码的功能类似了,不重复叙述。总的来说,x264_mb_cache_fenc_satd 这个函数就是计算原始待编码宏块 4*4 和 8*8 的 STAD。算来做什么?暂时还不知道。
step out,跳出 x264_mb_cache_fenc_satd 函数,继续 F10,到了 x264_mb_analyse_intra,F11 进入。F10 到了 predict_16x16_mode_available,顾名思义并结合该函数代码,可以确定它是在检查当前宏块有几种可用的 16*16 帧内预测模式。继续 F10 到了 for 循环 for( i = 0; i < i_max; i++ ),其循环条件 i_max 是函数 predict_16x16_mode_available 的返回值,那么很显然这个 for 循环是在循环计算可用预测模式了。继续 F10,进循环体到了 h->predict_16x16[i_mode],这又是个函数指针,顾名思义并结合改函数代码,可以确定它是在取得 16*16 块当前预测模式下的帧内预测块。如果大家记得 H.264 标准的各个章节的功能,那么就该知道 200503 版的 8.3.3 小节正是讲了 16*16 块的各种预测模式下如何进行帧内预测的。要读懂这个函数的代码就先学习一下 8.3.3 小节吧。继续 F10,到了 h->pixf.mbcmp[PIXEL_16x16],又是个函数指针,其功能大家自己跟进吧。该 for 循环完成后就把 16*16 块的最佳预测模式计算出来并存储起来了。
继续 F10,到了帧内 4*4 的预测模式选择部分。for 循环 for( idx = 0;; idx++ ),idx 是什么?因为这是帧内 4*4 预测,所以我们很自然应该联想到 idx 就应该是 16 个 4*4 块的编号,这个决定了 16 个 4*4 块的处理顺序,这个顺序可不是乱来的哦,200503 版标准/图 6-10 对顺序做了规定。继续 F10,到了 x264_mb_predict_intra4x4_mode 顾名思义并结合该函数代码可以确定它是在获得最可能预测模式,如果大家记得 H.264 标准的各个章节的功能,那么就该知道 200503 版的 8.3.1.1 小节正是讲的这部分内容。要读懂这个函数的代码就先学习一下 8.3.1.1 小节吧。继续 F10 到了 predict_4x4_mode_available 跟上面 16*16 块类似,功能顾名思义就不多说了。继续 F10,进入第二个 for 循环 for( ; i<i_max; i++ ),一看就知道该 for 循环跟上面 16*16 块同理是在计算当前 4*4 块的最佳预测模式。继续 F10,进入循环体到了 h->predict_4x4[i_mode] 顾名思义并结合该函数代码可以确定它是在取得当前 4*4 块在当前可用预测模式下的帧内预测块,如果大家记得 H.264 标准的各个章节的功能,那么就该知道 200503 版的 8.3.1.2 小节正是讲的这部分内容。要读懂这个函数的代码就先学习一下 8.3.1.2 小节吧。继续 F10,到了 h->pixf.mbcmp[PIXEL_4x4],也跟上面 16*16 块类似,功能顾名思义。
继续 F10,第二个 for 循环执行完后就把当前 4*4 块的最佳预测模式计算出来并存储起来了,到了函数指针 h->predict_4x4[a->i_predict4x4[idx]],很显然是在取得当前 4*4 块的最佳预测模式下的预测块了。算来干什么?从 H.264 帧内宏块编码的原理上我们知道帧内预测要以相邻块的重建值为参考,不先计算预测块,残差从哪里来?不得到残差,又哪里得到重建呢?(所以这里也体现了,读代码前要对编码原理和框架熟悉,否则你咋能明白这里为什么要取得预测块呢?)。继续 F10,到了 x264_mb_encode_i4x4,顾名思义并联想帧内编码原理和框架,我们猜测它是在进行当前 4*4 块的重建。F11 进去验证一下我们的猜测是否正确。x264_mb_encode_i4x4 函数里依次执行了 h->dctf.sub4x4_dct、x264_quant_4x4、h->zigzagf.scan_4x4、h->quantf.dequant_4x4、h->dctf.add4x4_idct,各函数功能顾名思义,的确验证了我们对 x264_mb_encode_i4x4 这个函数的功能的猜测。那么这些函数为什么要以这些顺序调用呢?因为编码原理和框架就是这样(这也再次体现了,读代码前要对编码原理和框架熟悉)。
step out,跳出 x264_mb_analyse_intra 函数,继续 F10,到了 x264_intra_rd,F11 跟进。继续 F10,到了函数 x264_analyse_update_cache,顾名思义无法猜测其功能,F11 跟进之后发现它只调用了一个函数 x264_mb_analyse_intra_chroma,这个函数又是什么功能呢?留给读者自己去跟进吧。step out,跳出 x264_analyse_update_cache 函数,继续 F10,到了 x264_rd_cost_mb,顾名思义猜测是进行 RDO 模式选择。这种方法的失真测度通常是使用 SSD,即原始像素与重建像素的误差平方和。那么如果我们对 x264_rd_cost_mb 的功能猜测正确,其函数中必然有编码宏块的代码和计算 SSD 的代码。F11 跟进去验证我们的猜测,x264_rd_cost_mb 里的确调用了 x264_macroblock_encode 和 ssd_mb。这两个函数是否是在执行编码和计算 SSD 的功能呢?留给读者自己去验证吧。提醒一句,X264 在这里用的失真测度不仅仅是 SSD,另外还有什么成分,读者自己去跟踪 ssd_mb 函数。x264_rd_cost_mb 函数最后执行的函数是 x264_macroblock_size_cavlc,顾名思义是在对当前宏块进行熵编码了。为什么要熵编码,因为 RDO 的率失真准则中要用到编码比特数啊。
step out,跳出 x264_rd_cost_mb 函数,后面的代码不说大家也知道了。step out,跳出 x264_intra_rd 函数,该函数下面的 6 行代码(见下图)的功能大家得弄清楚。因为算了这么多模式,这么多代价,最后编码到底选哪个模式呢?答案就这里了。
step out,跳出 x264_macroblock_analyse 函数,到了 x264_macroblock_encode,顾名思义并结合编码流程可以确定这里调用这个函数就是在用最终选定的那个最优的模式对当前宏块进行实际编码了。继续 F10,到了 x264_bitstream_check_buffer,顾名思义猜测是进行写码流前的一些准备工作。继续 F10,到了 x264_macroblock_write_cavlc,顾名思义并联想编码流程,很明显是在将最后的编码结果写入码流了。
至此,一个宏块帧内编码的过程就剖析完了。相信大家看完这么长的帖子之后,应该对我以前提出的学习建议中的两点有了深刻体会:1、读代码前一定要熟悉编码原理和框架;2、弄清楚标准各个章节讲的什么内容。当然这也是怎么看标准,怎么用标准的问题——先很粗略地了解各个章节是讲的什么,等到需要详细了解其内容时候再去细读相关章节。当然,C 语言功底在读代码过程中也是必须的,否则像函数指针这些东西你都搞不清楚怎么回事。
有了这个实际的例子,切身的体验,再回头去看看我和别人以前总结的学习方法的帖子,相信你会有更深的体会。最后还要说一句:别人不可能为你做所有的事,很多还是要靠自己努力!