很长一段时间没有写博客了,原因很多。SVC相关的研究和代码推进都暂时停止了, 因为有太多的其它学习和开发。SVC在视频的灵活传送和错误隐藏当面是有很大潜力的,但并非意味着它能适合于真正的网络工程应用。最近随着“阿凡达”的热播,很多人对3D视频的兴趣都热情起来,立体视频时代由这样的错觉3D开始,也将打开它的大幕了。 3D视频的制作超出了编解码范畴,更重要的利用视差进行渲染和视频特技制作,希望内行的朋友去看“阿发达”的时候,不仅仅是看热闹。 不要紧跟这些热闹的话题,今年还是踏踏实实的去探索一些基础知识的研究。打算继续对X264和FFMPEG相关内容的深入探讨,打算重新开展MVC 和机器视觉结合学习,打算把基础图像处理的算法重新复习,并且写作小结博客。等待一切的浮华被拂去,视频技术还将艰难的像更高级的目标,全息视频迈进。 这一次的文章将分析X264的多线程过程,也可以说是并行编码过程。 1. 编译并行编码的x264 从X264的帮助命令行可以看到,添加--threads项可以调整运行的线程数,可是当我完成X264编译,视图对手头的YUV进行编码的时候, 发现在自己的双核计算机上,只能发挥50%的效率,即使使用--threads n 也无济于事,提示就是没有打开pthread支持。Pthreads定义了一套 C程序语言类型、函数与常量,它以 pthread.h 头文件和一个线程库实现。【1】 下面就把我在windows上实现pthread版本的X264编译过程写作如下: 2009年3月的66版本 1. 从http://sourceware.org/pthreads-win32/ 下载pthread的win32版本,把其中的include和lib加入到VC++的引用目录中去。 2. 在项目属性的“C/C++ -> 预处理器 ->预处理器”中加入HAVE_PTHREAD。 3. 在osdep.h文件,紧接着#ifdef USE_REAL_PTHREAD加入 #pragma comment(lib, "pthreadVC2.lib") 引用pthreadVC2.lib,重新编译。 2009年10月的77版本 4. 在项目属性的“C/C++ -> 预处理器 ->预处理器”中加入SYS_MINGW。 其它版本请自己根据可能的编译错误随机应变。调整项目属性意味着同时调整libx264和x264两处的属性。 经过如上调整编译出的X264就可以在--threads n //n>=2的时候用完CPU的潜力了。 2. X264的编码基本流程 (1)接口变更 以前曾经写过文章介绍X264的编程架构并且分析了它的接口,现在进一步看看x264是怎么把YUV图像编程H.264编码的。在代码分析中,最 容易让人头疼的是X264代码随处充斥着的多线程处理和码率控制两方面的代码,所以,这里将先简化过程,忽略掉这些非主体代码。需要说明的是,本文分析的 是版本77,2009年10月的版本。 这里的API比版本66少了x264_nal_encode(...),该函数是将码率封装成NAL,现在它被放到static int x264_encoder_encapsulate_nals( x264_t *h )中,不再作为单独API出现。而x264_encoder_encapsulate_nals(...)分别被 x264_encoder_headers(...)和x264_encoder_frame_end(...)所调用,分别用于封装参数 (sps,pps)和其它数据的码流。 (2)main函数 从代码的main()函数开始, 这个函数很简单,就是读取参数,然后编码。到了版本77,相对于66版本而已,增加了参数--preset,用于定义一些预设的参数,究竟是哪个版本引入 的可自行考证。在调试程序的时候,可以根据需要选择预设参数值,如果采用默认状态,编码的FPS会比较慢。 现在重点考察编码函数static int Encode( x264_param_t *param, cli_opt_t *opt ), 在这个函数里,将会使用到X264的API,从代码带注释直接装贴过来,就不解释了。 首先,代码通过x264_encoder_open( param ) 和 x264_picture_alloc( )来初始化编码器和分配内存功输入YUV图像使用。接下来可以看到由两个注释隔开的代码块,它们的功能如下 /* Encode frames */ while(输入图像中的正常编码帧){ 编码正常的码流 } /* Flush delayed frames */ 编码因为B帧而残余的码流(在B帧编码中,需要参考最后一个P帧的那些B帧,这时,输入帧已经结束,而编码帧尚未结束) Encode()最后的代码是进行编码器关闭和内存謇恚?⑼臣票嗦胫∈?亢虵PS等。 (3)帧编码函数Encode_frame() 在上面的两个编码代码块中,主体函数是 static int Encode_frame( x264_t *h, hnd_t hout, x264_picture_t *pic ) 这个函数将输入每帧的YUV数据,然后谕?涑鰊al包。编码码流的具体工作交由API int x264_encoder_encode( x264_t *h,x264_nal_t **pp_nal, int *pi_nal,x264_picture_t *pic_in, 来完成,它应该是X264中最重要的函数了。 (4)分析x264_encoder_encode() 首先遇到参考帧调整好书如下, static inline int x264_reference_update( x264_t *h ) 它会在h->frames.reference 保留需要的参考帧,然后根据参考帧队列的大小限制,移除不使用的参考帧。 然后根据注释把代码块逐个往下分析: /* ------------------- Setup new frame from picture -------------------- */ /* 1: Copy the picture to a frame and move it to a buffer */ 把帧输入的YUV数据传入 x264_frame_t *fenc中,然后进行一些码率控制方式的初始化。 /* 2: Place the frame into the queue for its slice type decision */ 把fenc放到slice决定队列中,也输入码率控制的一部分 /* 3: The picture is analyzed in the lookahead */ 分析slice类型,具体的类型决定工作将在函数void x264_slicetype_decide( x264_t *h )中处理。 后面做码率控制分析的时候再详述。 /* ------------------- Get frame to be encoded ------------------------- */ 去处编码帧,放置在h->fenc中,并重新设置编码参数。 /* ------------------- Setup frame context ----------------------------- */ 根据帧类型设置i_nal_type,i_nal_ref_idc,h->sh.i_type ,如果是IDR帧,重置参考帧队列。 /* ------------------- Init ----------------------------- */ 根据当前帧建立参考帧队列,当前参考帧按编码帧类型分别写在h->fref0和h->fref1中。并整理好他们的排列顺序,h->fref0按poc从高到低,h->fref1反之。 /* ---------------------- Write the bitstream -------------------------- */ 写NAL码流 /* Write SPS and PPS */ 写参数集 /* ------------------------ Create slice header ----------------------- */ 初始化slice header参数 /* Write frame */ 输出slice header和slice data
函数最后调用 static int x264_encoder_frame_end( x264_t *h, x264_t *thread_current,x264_nal_t **pp_nal, int *pi_nal, x264_picture_t *pic_out ) 来做NAL装,并且调整编码器状态和输出本帧编码的统计数据。 (5)static void *x264_slices_write( x264_t *h ) 这个函数被x264_encoder_encode()调用作为处理slice header和slice data的编码,这个函数主要是分出slice group中的一个slice,具体做slice编码则在 static int x264_slice_write( x264_t *h ) 这个函数的代码块划分如下: step1. 初始化NAL,调用x264_slice_header_write()根据前面的参数设置输出slice header码流, step2. 如果是用CABAC,则初始化其上下文。 step3. 进入宏块,逐个宏块编码: 宏块编码重要的是以下两个函数: 其之前的代码是做宏块数据的导入,其后的代码是对编码数据进行熵编码,根据slicedata协议写入码流,更新coded_block_pattern,处理码率控制状态和更新CABAC上下文数据等。代码分析到宏块级了,就看看这个基本的编码单位是怎么被处理的吧。 (6)x264_macroblock_analyse( h ) 这个函数就是分析宏块以确定其宏块分区模式,对I帧进行帧内预测和对P/B帧进行运动估计就发生在此函数,首先进行亮度编码,紧接着是色度。同样来一步步分析其实现。 step2. 根据h->sh.i_type的类型(I,P,B)来分别计算宏块模式的率失真代价,代价计算使用SATD方法,【2】中有相关介绍。通过计算SATD可以大致估计编码码流,作为宏块选择的依据。 随机取h->mb.i_type == I_8x8的情况来分析, if( h->mb.b_lossless ) predict_8x8[i_mode]( p_dst, edge )将进行帧内预测,x264_mb_encode_i8x8( h, i, i_qp )进行DCT编码和量化,同时进行反量化和逆DCT编码,以备重建图像使用。
对于I8x8和I4x4的情况一般会进行分别做3 个或15个块的预测和编码,留下一个块在x264_macroblock_encode( h )中再预测编码,原因是前面的块将作为后面编码块的预测依据。具体说会导致 i_pred_mode = x264_mb_predict_intra4x4_mode( h, 4*idx )的计算值发生变化。
P/B帧的帧间预测将在接下来的代码段发生,具体的运动估计算法不在详述,以后将补充X264运动估计分析。
step3. 根据i_mbrd的不同,做一些后续运算。 (7)x264_macroblock_encode( h ) 在确定了宏块分区模式后,在本函数将对I帧剩余的宏块分区进行预测和编码,而对P/B帧的运动补偿和残差编码主要发生在这里。 基本流程分析到这里已经算结束了,在代码中,会发现宏块的预测和编码会散布在不同的函数发生,原因是对率失真优化的要求(对P/B帧)。所以,在X264中参考帧管理,码率控制,帧间预测和多线程编码都是比较有趣的探索对象。 3. 多线程代码分析 (1)文档解读 分析完X264的基本架构,来看看多线程发挥力量的地方。X264自带的多线程介绍文档是本课题的必读文档,它存放在X264的DOC文件夹下。本 文描述的大意是:当前的X264多线程模式已经放弃基于slice的并行编码,转而采用帧级和宏块级的并行,原因是slice并行需要采用slice group,会引入而外冗余降低编码效率。摘抄一段原文如下: New threading method: frame-based 以上的说明意味着,X264采用B帧在编码时不作为参考帧,所以适宜对其进行并行。 (2)运行状况分析 先来看看x264_pthread_create被调用的地方,只有这些地方才实实在在的创建了线程。 x264_pthread_create( &h->thread_handle, NULL, (void*)x264_slices_write, h ) x264_pthread_create( &look_h->thread_handle, NULL, (void *)x264_lookahead_thread, look_h ) x264_pthread_create( &h->tid, NULL, (void*)read_frame_thread_int, h->next_args )
由上图的运行可以看出,在开启了--threads 4后。x264_slices_write()可以开启4个线程同时编码,而同时存在一个主线程和一个x264_lookahead_thread()线 程。x264_slices_write()的优先级为低,原因是调用了 if( h->param.i_sync_lookahead ) 调低本线程的优先级。read_frame_thread_int()是读磁盘上的流数据信息,因为I/O和内存的不同步,所以应该分开线程处理。 在x264_encoder_open()中可以找到一下代码,可以看到对于x264_slices_write()和x264_lookahead_thread()都有被分配了专有的上下文变量,供单一线程使用。 for( i = 1; i < h->param.i_threads + !!h->param.i_sync_lookahead; i++ ) (3)如何确保按指定线程数来开启线程编码? 按打印实验可以看到,假设使用--threads 4的参数选项,代码会同时开启4个x264_slices_write()线程,然后每编完一个帧(前面的一个线程返回后),一个新的被产生出来,使得 x264_slices_write()线程总数保持在4个,这一过程的相关代码如下: int x264_encoder_encode( x264_t *h,x264_nal_t **pp_nal, int *pi_nal,x264_picture_t *pic_in, ... /* Write frame */ ... } static int x264_encoder_frame_end( x264_t *h, x264_t *thread_current,x264_nal_t **pp_nal, int *pi_nal, x264_picture_t *pic_out ) ... } 从以上两个函数的代码段可以看到,h上下文中保持的线程不会多于4个, x264_pthread_create()根据主线程的调用,创建出x264_slices_write线程,然后thread_oldest被指定并被率控函数判断重设,当前的线程数还不足4的时候,thread_oldest指向新线程,h->b_thread_active为0,不能进入x264_encoder_frame_end()的相关代码,主线程继续循环创建x264_slices_write线程,当线程总数为4,这时thread_oldest指向4个线程中被判断最快返回的那个,这时h->b_thread_active=1将进入x264_pthread_join(),那样,该线程就将主线至于阻塞状态,直至thread_oldest完成,才能重现创建新线程,以此机制,保持指定数码的编码线程数。 (4)x264_lookahead_thread()线程的作用 在分析这个线程之前,来看看两个重要的线程控制函数: //唤醒等待该条件变量的所有线程。如果没有等待的线程,则什么也不做。 #define x264_pthread_cond_broadcast pthread_cond_broadcast //自动解锁互斥量(如同执行了 pthread_unlock_mutex),并等待条件变量触发。这时线程挂起,不占用 CPU 时间,直到条件变量被触发。在调用 pthread_cond_wait 之前,应用程序必须加锁互斥量。pthread_cond_wait 函数返回前,自动重新对互斥量加锁(如同执行了 pthread_lock_mutex)。 以下的代码是X264中x264_lookahead_thread代码经常阻塞的地方, **************************代码段A******************************************** if( h->lookahead->next.i_size <= h->lookahead->i_slicetype_length ) 这里是等待满足!h->lookahead->ifbuf.i_size && !h->lookahead->b_exit_thread 的条件,后一条件在正常编码过程是TRUE,因为不会无故退出线程。那么这里等待的其实是ifbuf.i_size为非0.查找相关代码, 这里的 ifbuf.i_size条件是在x264_synch_frame_list_push()得到满足的,这里在得到一个输入的新编码帧后将发出信号。 slist->list[ slist->i_size++ ] = frame; 在 代码段A中,if( h->lookahead->next.i_size <= h->lookahead->i_slicetype_length )条件中,i_slicetype_length表示为了进行slice type的判断而缓存的帧,它的值有取决于h->frames.i_delay,由代码的初始化设定值决定(默认为40)。也就是说预存40帧的数 值,进行slice type决定用。暂时不详细分析slice type判断的具体实现,它的大概思想是根据码率,GOP和失真状况的权衡,来进行帧类型选择,在类似实时通信场合,不允许B帧的使用,也不可能预存那么多帧,这样的处理没有意义。 回头看这里的处理意义,是阻塞线程,等待后续的输入帧,然后利用处理规则来决定其slice type,为slice编码准备帧。 (5)宏块级别的并行 在数据结构x264_frame_t中,有变量x264_pthread_cond_t cv; 该变量分别在下面的两个函数里被封装了阻塞和唤醒: void x264_frame_cond_broadcast( x264_frame_t *frame, int i_lines_completed ); 考查它们被调用的地方, ************代码B****************from x264_macroblock_analyse( )->x264_mb_analyse_init() int thresh = pix_y + h->param.analyse.i_mv_range_thread; **************************代码C************************************from x264_fdec_filter_row() if( h->param.i_threads > 1 && h->fdec->b_kept_as_ref ) x264_frame_cond_broadcast( h->fdec, mb_y*16 + (b_end ? 10000 : -(X264_THREAD_HEIGHT <<h->sh.b_mbaff)) ); 从 上面的代码段可以看到没完成图像一行的编码,便会使用mb_y*16 -X264_THREAD_HEIGH的值来尝试唤醒x264_pthread_cond_wait( &frame->cv, &frame->mutex ),要判断的条件是 mb_y*16 -X264_THREAD_HEIGH < thresh = pix_y + h->param.analyse.i_mv_range_thread; 后者作为一个设想的阈值,用于确保依赖于本帧的后续帧在编码时,本帧已经编码出若干行宏块,以后续编码帧的基础,那样可以设想的情形如下图,不过X264是以编码完整行为单位的。 本文的分析道这里告一段落,对于帧间多线程分析和宏块的并行优化,或按自己的应用做代码裁剪,可以通过改正上面的(4)(5)代码段来实现,在当前(四核CPU)的X264测试中,已有代码确实能够很好的利用多核资源,并行编码的话题会随硬件的升级而不断探索下去。 【参考文献】 1. 维基百科, http://zh.wikipedia.org/wiki/POSIX_Threads 2. MVC学习的第四周小结(2009.6.1-2009.6.7)-运动估计2 :http://www.rosoo.net/a/201001/8386.html 3. X264 document,threads.txt (jmvc) |