代碼與標(biāo)準(zhǔn)如何對(duì)應(yīng)
總是有人說自己把代碼和標(biāo)準(zhǔn)對(duì)應(yīng)不起來。其實(shí)是因?yàn)槟阋床恢罉?biāo)準(zhǔn)各個(gè)章節(jié)講的什么,要么不知道代碼中各個(gè)函數(shù)的功能,或者兩者都不知道。今天再以 X264 的幀內(nèi)編碼為例讓大家體會(huì)一下讀代碼時(shí)該如何與標(biāo)準(zhǔn)對(duì)應(yīng)。此貼是帖子“如何閱讀代碼”的延續(xù),因此采用的代碼與編譯環(huán)境設(shè)置與其一樣,此處不再贅述。
上貼說過 Encode_frame 函數(shù)包含最核心的編碼代碼,那么我們現(xiàn)在就 F11 進(jìn)去看看。遇到的第一個(gè)函數(shù)是 x264_encoder_encode,再 F11 進(jìn)去,執(zhí)行到 x264_reference_update,它在干什么呢?顧名思義猜測(cè)一定是更新幀間參考要用到的一些內(nèi)存空間,因?yàn)槲覀儸F(xiàn)在還沒有編碼,所以 F11 進(jìn)去后沒執(zhí)行什么操作就出來了。
繼續(xù) F10,執(zhí)行到 x264_frame_pop_unused。F11 跟進(jìn),然后 F10,發(fā)現(xiàn)它走了 x264_frame_new 的分支(x264_frame_pop 分支干什么用的呢?暫時(shí)先別管。跟著流程走,管多了就迷茫了),F(xiàn)11 跟進(jìn) x264_frame_new 發(fā)現(xiàn)通篇都是對(duì)變量結(jié)構(gòu)體指針 frame 里的成員變量執(zhí)行 CHECKED_MALLOC,由此我們可以初步判斷它是在為幀結(jié)構(gòu)體分配內(nèi)存空間。
step out 跳出 x264_frame_pop_unused,F(xiàn)10 到 x264_frame_copy_picture,F(xiàn)11 進(jìn)去讀讀代碼我們就知道這個(gè)函數(shù)的功能是將待編碼圖像從 pic_in 復(fù)制到 fenc->plane。繼續(xù) F10,到了 x264_frame_push,通過閱讀該函數(shù)的代碼我們知道它的功能是將當(dāng)前幀結(jié)構(gòu)體從 fenc 移到 h->frames.next 中。后面的函數(shù) x264_frame_init_lowres、x264_adaptive_quant_frame、x264_encoder_frame_end 都未被執(zhí)行。既然沒被執(zhí)行,那我們現(xiàn)在暫時(shí)就不管它們。
F10 到 x264_stack_align( x264_slicetype_decide, h ); x264_stack_align 顧名思義無非就是平臺(tái)優(yōu)化方面考慮的對(duì)齊操作,因此這里我們要關(guān)心的是函數(shù) x264_slicetype_decide,F(xiàn)11 我們會(huì)發(fā)現(xiàn)進(jìn)不到 x264_slicetype_decide 里。怎么辦呢?見下面第一個(gè)截圖,將光標(biāo)點(diǎn)到 x264_slicetype_decide 上,點(diǎn)鼠標(biāo)右鍵選擇 go to definition,然后先在里面的第一行代碼下斷點(diǎn)(見下面第二個(gè)截圖),然后再按 F10 就可以進(jìn)入到 x264_slicetype_decide 函數(shù)了。該函數(shù)顧名思義是來決定當(dāng)前 slice 的編碼類型的,即到底是 I 片還是 P 片或 B 片。通過瀏覽其代碼,我們也會(huì)發(fā)現(xiàn)代碼所做也正是這樣。
step out 跳出 x264_slicetype_decide,繼續(xù) F10 執(zhí)行到 x264_frame_push,這里實(shí)際要執(zhí)行兩個(gè)函數(shù),因?yàn)?x264_frame_push 的第二個(gè)參數(shù)是函數(shù) x264_frame_shift,所以會(huì)先執(zhí)行它。F11 首先進(jìn)入的就是 x264_frame_shift,然后 step out 跳出 x264_frame_shift,繼續(xù) F11 就進(jìn)入了 x264_frame_push,通過閱讀這兩個(gè)簡(jiǎn)短的函數(shù)的代碼,我們知道它們執(zhí)行的操作是將當(dāng)前編碼幀結(jié)構(gòu)體從 h->frames.next 移到 h->frames.current。
繼續(xù) F10,又到了一個(gè) x264_frame_shift 函數(shù),F(xiàn)11 進(jìn)去通過閱讀代碼我們可以知道該函數(shù)的功能將當(dāng)前幀結(jié)構(gòu)體從 h->frames.current 移到 h->fenc(我有點(diǎn)奇怪,為什么 X264 要這么麻煩地把一個(gè)變量移來移去呢?一次搞定不行么?)。繼續(xù) F10,到了 x264_reference_reset,其功能顧名思義,也有英文注釋,具體有什么用,現(xiàn)在我還不知道,暫時(shí)不管吧。
繼續(xù) F10,到了 x264_reference_build_list,顧名思義,參考列表構(gòu)建,在 JM 里叫做參考列表初始化(JM86 對(duì)應(yīng)的函數(shù)是 init_lists)。參考列表初始化的作用即構(gòu)建幀間編碼圖像所需要用到的參考圖像列表。那么如何初始化呢?如果大家記得 H.264 標(biāo)準(zhǔn)的各個(gè)章節(jié)的功能,那么就該知道 200503 版的 8.2.4 小節(jié)正是講的這部分內(nèi)容。這樣這個(gè)函數(shù)就與標(biāo)準(zhǔn)的內(nèi)容對(duì)應(yīng)起來了。至于通過代碼是如何實(shí)現(xiàn)的,先看懂了標(biāo)準(zhǔn)的這個(gè)部分再來讀這個(gè)函數(shù)的代碼吧。
繼續(xù) F10,到了 x264_ratecontrol_start,顧名思義進(jìn)行碼率控制的一些準(zhǔn)備工作。
繼續(xù) F10,到了 x264_slice_init,顧名思義片初始化,做了哪些工作呢?F11 進(jìn)去執(zhí)行了分支 x264_slice_header_init,通過瀏覽其代碼,我們發(fā)現(xiàn)通篇都是對(duì)結(jié)構(gòu)體指針 sh 內(nèi)的成員變量的賦值操作。后面的 x264_macroblock_slice_init 函數(shù)在干什么,大家自己 F11 進(jìn)去看,看不懂沒關(guān)系,反正就是給一些變量賦初值嘛。繼續(xù) F10,到了 bs_init,顧名思義是對(duì)碼流相關(guān)的變量進(jìn)行初始化,因?yàn)?bs 就是 bit stream 嘛。
繼續(xù) F10,到了 if( i_nal_type == NAL_SLICE_IDR && h->param.b_repeat_headers ),注意前面的英文注釋 /* Write SPS and PPS */,意思就是這里在向碼流中寫 SPS 和 PPS。這里的三組函數(shù),顧名思義第一組是在寫 SEI、第二組是在寫 SPS、第三組是在寫 PPS。那么如何寫碼流呢?當(dāng)然是要遵循語(yǔ)法表了。下面以寫 SPS 為例簡(jiǎn)要說明一下,如果大家記得 H.264 標(biāo)準(zhǔn)的各個(gè)章節(jié)的功能,那么就該知道 200503 版的 7.3.2.1 小節(jié)就是 SPS 語(yǔ)法表。因?yàn)?7.3.2.1 規(guī)定了 SPS 在碼流中的第一個(gè)語(yǔ)法元素是 profile_idc,因此當(dāng)我們 F11 進(jìn)入 x264_sps_write 的時(shí)候會(huì)發(fā)現(xiàn)該函數(shù)第一行代碼正是在寫 sps->i_profile_idc,標(biāo)準(zhǔn)規(guī)定 SPS 第二個(gè)語(yǔ)法元素是 constraint_set0_flag,因此該函數(shù)的第二行代碼就是在寫 sps->b_constraint_set0,其他同理。這里說的是寫的順序,那么寫的方式是什么呢?H.264 中的熵編碼方式細(xì)分起來有很多,每個(gè)語(yǔ)法表的最后一列 descriptor 規(guī)定的就是對(duì)應(yīng)的語(yǔ)法元素采用哪種熵編碼方法。例如:profile_idc 是 u(8),因此它采用 8 位無符號(hào)整數(shù)編碼;constraint_set0_flag 是 u(1),因此它采用 1 比特?zé)o符號(hào)整數(shù)編碼。各種熵編碼方法在 200503 版 7.2 小節(jié)最后都有說明,此處不再贅述。好了,我們知道了各個(gè)語(yǔ)法元素采用什么方式編碼,自然也就知道了代碼中各個(gè)熵編碼函數(shù)對(duì)應(yīng)的是什么編碼方式。例如:對(duì) profile_idc 編碼采用的是 bs_write 函數(shù),當(dāng)然這個(gè)函數(shù)的功能就是無符號(hào)熵編碼了,對(duì) i_id 編碼采用的是 bs_write_ue 函數(shù),當(dāng)然這個(gè)函數(shù)的功能就是 ue(v)——無符號(hào)哥倫布編碼。其他同理。其實(shí)這些我在帖子“ 如何讀標(biāo)準(zhǔn)和代碼”中已經(jīng)講過了。
順便提一下,對(duì)碼流的讀寫操作都要依據(jù)語(yǔ)法表所定義的語(yǔ)法元素順序和熵編碼類型。上面講的是編碼的具體例子,解碼的具體例子我以前用 JM 講過,參考帖子“如何結(jié)合標(biāo)準(zhǔn)看JM代碼(JM86)”。 好了,繼續(xù) F10,到了 x264_slices_write。F11 進(jìn)入,再 F10,到了 x264_stack_align( x264_slice_write, h );我們關(guān)心的是 x264_slice_write,進(jìn)入該函數(shù),方法在上面已經(jīng)說過了。x264_slice_write 第一個(gè)函數(shù)為系統(tǒng)函數(shù) memset,下一個(gè)為 x264_nal_start,其功能看下代碼就知道是在設(shè)置將要寫入碼流的 NALU 的第一個(gè)字節(jié)的值。第二個(gè)函數(shù) x264_slice_header_write 顧名思義是在向碼流中寫入片頭。如果大家記得 H.264 標(biāo)準(zhǔn)的各個(gè)章節(jié)的功能,那么就該知道 200503 版的 7.3.3 小節(jié)就是片頭的語(yǔ)法表。寫碼流的過程與上面 SPS 的過程同理,此處不再贅述。
F10 到了 while 循環(huán),顧名思義根據(jù) while 循環(huán)的循環(huán)條件猜測(cè)一下該 while 循環(huán)的功能,肯定就是循環(huán)對(duì)整個(gè)圖像的每個(gè)宏塊一次編碼了。要驗(yàn)證一下猜測(cè)很簡(jiǎn)單,在 while 循環(huán)體的第一行下斷點(diǎn),按一次 F5 就觀察一下 mb_xy 變量的值的變化情況。另外還有個(gè)信息說明了這一點(diǎn),h->sh.i_last_mb 變量的值剛好等于待編碼圖像的總宏塊數(shù)。
F10 到了 x264_fdec_filter_row,顧名思義猜測(cè)該函數(shù)的功能是去塊濾波。如果大家記得 H.264 標(biāo)準(zhǔn)的各個(gè)章節(jié)的功能,那么就該知道 200503 版的 8.7 小節(jié)正是講的這部分內(nèi)容。要讀懂這個(gè)函數(shù)的代碼就先學(xué)習(xí)一下 8.7 小節(jié)吧。
F10 到了 x264_macroblock_cache_load,通過瀏覽代碼我們知道是在對(duì)一些變量賦值,各個(gè)變量的含義顧名思義。這也屬于編碼前的準(zhǔn)備工作。 繼續(xù) F10 到了 x264_macroblock_analyse,看見英文注釋了吧?不用我們顧名思義就知道它的功能了,是在進(jìn)行模式選擇。F11 進(jìn)入該函數(shù)。第一個(gè)被調(diào)用的函數(shù)是 x264_ratecontrol_qp,顧名思義獲取當(dāng)前宏塊 QP。第二個(gè)被調(diào)用的函數(shù)是 x264_mb_analyse_init,F(xiàn)11 進(jìn)去后發(fā)現(xiàn)只有非 I 片才進(jìn)行一些操作,那暫時(shí)就不管它。
F10 到了 x264_mb_cache_fenc_satd,F(xiàn)11 進(jìn)去。一開始是個(gè) 4*4 的雙重循環(huán)。我們現(xiàn)在是在對(duì)一個(gè)宏塊進(jìn)行操作,這里又出現(xiàn) 4*4 的循環(huán),那么很明顯了這個(gè)雙重循環(huán)肯定是在計(jì)算每個(gè) 4*4 的塊,下面的 2*2 的雙重循環(huán)肯定是在計(jì)算 8*8 的塊。因?yàn)楹陦K的尺寸是 16*16 嘛,寬高分成 4 份不正好是 4*4,分成 2 份不正好是 8*8 么?做視頻的人應(yīng)該對(duì) 4、8、16 等常用的數(shù)字敏感。先分析第一個(gè) 4*4 的雙重循環(huán)。注意,for 循環(huán)里的 h->pixf.satd 和 h->pixf.sad 都是函數(shù)指針,因此要用 F11 跟進(jìn)。h->pixf.satd 的兩個(gè)輸入是 zero 和 fenc,跟進(jìn)之后的函數(shù) pixel_satd_wxh 在計(jì)算他們之差,然后作 Hadamard 變換,然后計(jì)算 SATD。由此可以猜測(cè) fenc 里存放的是原始待編碼宏塊(到底是不是呢?讀者自己反回去找到 h->mb.pic.p_fenc[0] 被賦值的地方看看就知道了)。后面代碼的功能類似了,不重復(fù)敘述??偟膩碚f,x264_mb_cache_fenc_satd 這個(gè)函數(shù)就是計(jì)算原始待編碼宏塊 4*4 和 8*8 的 STAD。算來做什么?暫時(shí)還不知道。
step out,跳出 x264_mb_cache_fenc_satd 函數(shù),繼續(xù) F10,到了 x264_mb_analyse_intra,F(xiàn)11 進(jìn)入。F10 到了 predict_16x16_mode_available,顧名思義并結(jié)合該函數(shù)代碼,可以確定它是在檢查當(dāng)前宏塊有幾種可用的 16*16 幀內(nèi)預(yù)測(cè)模式。繼續(xù) F10 到了 for 循環(huán) for( i = 0; i < i_max; i++ ),其循環(huán)條件 i_max 是函數(shù) predict_16x16_mode_available 的返回值,那么很顯然這個(gè) for 循環(huán)是在循環(huán)計(jì)算可用預(yù)測(cè)模式了。繼續(xù) F10,進(jìn)循環(huán)體到了 h->predict_16x16[i_mode],這又是個(gè)函數(shù)指針,顧名思義并結(jié)合改函數(shù)代碼,可以確定它是在取得 16*16 塊當(dāng)前預(yù)測(cè)模式下的幀內(nèi)預(yù)測(cè)塊。如果大家記得 H.264 標(biāo)準(zhǔn)的各個(gè)章節(jié)的功能,那么就該知道 200503 版的 8.3.3 小節(jié)正是講了 16*16 塊的各種預(yù)測(cè)模式下如何進(jìn)行幀內(nèi)預(yù)測(cè)的。要讀懂這個(gè)函數(shù)的代碼就先學(xué)習(xí)一下 8.3.3 小節(jié)吧。繼續(xù) F10,到了 h->pixf.mbcmp[PIXEL_16x16],又是個(gè)函數(shù)指針,其功能大家自己跟進(jìn)吧。該 for 循環(huán)完成后就把 16*16 塊的最佳預(yù)測(cè)模式計(jì)算出來并存儲(chǔ)起來了。
繼續(xù) F10,到了幀內(nèi) 4*4 的預(yù)測(cè)模式選擇部分。for 循環(huán) for( idx = 0;; idx++ ),idx 是什么?因?yàn)檫@是幀內(nèi) 4*4 預(yù)測(cè),所以我們很自然應(yīng)該聯(lián)想到 idx 就應(yīng)該是 16 個(gè) 4*4 塊的編號(hào),這個(gè)決定了 16 個(gè) 4*4 塊的處理順序,這個(gè)順序可不是亂來的哦,200503 版標(biāo)準(zhǔn)/圖 6-10 對(duì)順序做了規(guī)定。繼續(xù) F10,到了 x264_mb_predict_intra4x4_mode 顧名思義并結(jié)合該函數(shù)代碼可以確定它是在獲得最可能預(yù)測(cè)模式,如果大家記得 H.264 標(biāo)準(zhǔn)的各個(gè)章節(jié)的功能,那么就該知道 200503 版的 8.3.1.1 小節(jié)正是講的這部分內(nèi)容。要讀懂這個(gè)函數(shù)的代碼就先學(xué)習(xí)一下 8.3.1.1 小節(jié)吧。繼續(xù) F10 到了 predict_4x4_mode_available 跟上面 16*16 塊類似,功能顧名思義就不多說了。繼續(xù) F10,進(jìn)入第二個(gè) for 循環(huán) for( ; i<i_max; i++ ),一看就知道該 for 循環(huán)跟上面 16*16 塊同理是在計(jì)算當(dāng)前 4*4 塊的最佳預(yù)測(cè)模式。繼續(xù) F10,進(jìn)入循環(huán)體到了 h->predict_4x4[i_mode] 顧名思義并結(jié)合該函數(shù)代碼可以確定它是在取得當(dāng)前 4*4 塊在當(dāng)前可用預(yù)測(cè)模式下的幀內(nèi)預(yù)測(cè)塊,如果大家記得 H.264 標(biāo)準(zhǔn)的各個(gè)章節(jié)的功能,那么就該知道 200503 版的 8.3.1.2 小節(jié)正是講的這部分內(nèi)容。要讀懂這個(gè)函數(shù)的代碼就先學(xué)習(xí)一下 8.3.1.2 小節(jié)吧。繼續(xù) F10,到了 h->pixf.mbcmp[PIXEL_4x4],也跟上面 16*16 塊類似,功能顧名思義。
繼續(xù) F10,第二個(gè) for 循環(huán)執(zhí)行完后就把當(dāng)前 4*4 塊的最佳預(yù)測(cè)模式計(jì)算出來并存儲(chǔ)起來了,到了函數(shù)指針 h->predict_4x4[a->i_predict4x4[idx]],很顯然是在取得當(dāng)前 4*4 塊的最佳預(yù)測(cè)模式下的預(yù)測(cè)塊了。算來干什么?從 H.264 幀內(nèi)宏塊編碼的原理上我們知道幀內(nèi)預(yù)測(cè)要以相鄰塊的重建值為參考,不先計(jì)算預(yù)測(cè)塊,殘差從哪里來?不得到殘差,又哪里得到重建呢?(所以這里也體現(xiàn)了,讀 代碼前要對(duì)編碼原理和框架熟悉,否則你咋能明白這里為什么要取得預(yù)測(cè)塊呢?)。繼續(xù) F10,到了 x264_mb_encode_i4x4,顧名思義并聯(lián)想幀內(nèi)編碼原理和框架,我們猜測(cè)它是在進(jìn)行當(dāng)前 4*4 塊的重建。F11 進(jìn)去驗(yàn)證一下我們的猜測(cè)是否正確。x264_mb_encode_i4x4 函數(shù)里依次執(zhí)行了 h->dctf.sub4x4_dct、x264_quant_4x4、h->zigzagf.scan_4x4、 h->quantf.dequant_4x4、h->dctf.add4x4_idct,各函數(shù)功能顧名思義,的確驗(yàn)證了我們對(duì) x264_mb_encode_i4x4 這個(gè)函數(shù)的功能的猜測(cè)。那么這些函數(shù)為什么要以這些順序調(diào)用呢?因?yàn)榫幋a原理和框架就是這樣(這也再次體現(xiàn)了,讀代碼前要對(duì)編碼原理和框架熟悉)。
step out,跳出 x264_mb_analyse_intra 函數(shù),繼續(xù) F10,到了 x264_intra_rd,F(xiàn)11 跟進(jìn)。繼續(xù) F10,到了函數(shù) x264_analyse_update_cache,顧名思義無法猜測(cè)其功能,F(xiàn)11 跟進(jìn)之后發(fā)現(xiàn)它只調(diào)用了一個(gè)函數(shù) x264_mb_analyse_intra_chroma,這個(gè)函數(shù)又是什么功能呢?留給讀者自己去跟進(jìn)吧。step out,跳出 x264_analyse_update_cache 函數(shù),繼續(xù) F10,到了 x264_rd_cost_mb,顧名思義猜測(cè)是進(jìn)行 RDO 模式選擇。這種方法的失真測(cè)度通常是使用 SSD,即原始像素與重建像素的誤差平方和。那么如果我們對(duì) x264_rd_cost_mb 的功能猜測(cè)正確,其函數(shù)中必然有編碼宏塊的代碼和計(jì)算 SSD 的代碼。F11 跟進(jìn)去驗(yàn)證我們的猜測(cè),x264_rd_cost_mb 里的確調(diào)用了 x264_macroblock_encode 和 ssd_mb。這兩個(gè)函數(shù)是否是在執(zhí)行編碼和計(jì)算 SSD 的功能呢?留給讀者自己去驗(yàn)證吧。提醒一句,X264 在這里用的失真測(cè)度不僅僅是 SSD,另外還有什么成分,讀者自己去跟蹤 ssd_mb 函數(shù)。x264_rd_cost_mb 函數(shù)最后執(zhí)行的函數(shù)是 x264_macroblock_size_cavlc,顧名思義是在對(duì)當(dāng)前宏塊進(jìn)行熵編碼了。為什么要熵編碼,因?yàn)?RDO 的率失真準(zhǔn)則中要用到編碼比特?cái)?shù)啊。
step out,跳出 x264_rd_cost_mb 函數(shù),后面的代碼不說大家也知道了。step out,跳出 x264_intra_rd 函數(shù),該函數(shù)下面的 6 行代碼(見下圖)的功能大家得弄清楚。因?yàn)樗懔诉@么多模式,這么多代價(jià),最后編碼到底選哪個(gè)模式呢?答案就這里了。
step out,跳出 x264_macroblock_analyse 函數(shù),到了 x264_macroblock_encode,顧名思義并結(jié)合編碼流程可以確定這里調(diào)用這個(gè)函數(shù)就是在用最終選定的那個(gè)最優(yōu)的模式對(duì)當(dāng)前宏塊進(jìn)行實(shí)際編 碼了。繼續(xù) F10,到了 x264_bitstream_check_buffer,顧名思義猜測(cè)是進(jìn)行寫碼流前的一些準(zhǔn)備工作。繼續(xù) F10,到了 x264_macroblock_write_cavlc,顧名思義并聯(lián)想編碼流程,很明顯是在將最后的編碼結(jié)果寫入碼流了。
至此,一個(gè)宏塊幀內(nèi)編碼的過程就剖析完了。相信大家看完這么長(zhǎng)的帖子之后,應(yīng)該對(duì)我以前提出的學(xué)習(xí)建議中的兩點(diǎn)有了深刻體會(huì):1、讀代碼前一定要熟悉編碼 原理和框架;2、弄清楚標(biāo)準(zhǔn)各個(gè)章節(jié)講的什么內(nèi)容。當(dāng)然這也是怎么看標(biāo)準(zhǔn),怎么用標(biāo)準(zhǔn)的問題——先很粗略地了解各個(gè)章節(jié)是講的什么,等到需要詳細(xì)了解其內(nèi) 容時(shí)候再去細(xì)讀相關(guān)章節(jié)。當(dāng)然,C 語(yǔ)言功底在讀代碼過程中也是必須的,否則像函數(shù)指針這些東西你都搞不清楚怎么回事。
有了這個(gè)實(shí)際的例子,切身的體驗(yàn),再回頭去看看我和別人以前總結(jié)的學(xué)習(xí)方法的帖子,相信你會(huì)有更深的體會(huì)。最后還要說一句:別人不可能為你做所有的事,很多還是要靠自己努力!