[ffmpeg] h.264解碼所用的主要緩衝區介紹
在進行h264解碼過程中,有兩個最重要的結構體,分別為H264Picture、H264SliceContext。
H264Picture
H264Picture用於維護一幀影象以及與該影象相關的語法元素。其中佔用大片記憶體的結構體成員有以下幾個:
typedef struct H264Picture { AVFrame *f; int8_t *qscale_table; int16_t (*motion_val[2])[2]; uint32_t *mb_type; int8_t *ref_index[2]; } H264Picture;
Menber/Size | Description |
f W x H (frame in pixels) x YUV |
維護視訊的一幀,主要的儲存空間由AVBufferRef提供,儲存的是這一幀的畫素資料,由於視訊中每個畫素分有YUV三個分量,因此會有三塊大記憶體,分別儲存這三個分量的畫素資料。如果視訊是以interlaced來進行編碼的,則會對一幀分為上下場進行編碼,不過在解碼的時候這兩個場會被合併,由這一成員維護。 ![]() |
qscale_table W x H (frame in MBs) |
記錄一幀中所有巨集塊的QP。每個巨集塊都有獨立的QP,QP值由SPS、PPS、slice以及巨集塊中的QP相關語法元素計算得來。QP除了用於對殘差係數進行逆量化之外還在去塊濾波中起到判別真假濾波邊界的作用。 ![]() |
motion_val W x H (frame in 4x4 blocks) x 2 x 2 |
記錄一幀中所有4x4塊的運動向量。4x4塊是運動向量作用的最小單位,該表格會記錄inter巨集塊中各個4x4塊的運動向量。如果該塊在進行編碼時採用的是雙向預測,那麼在解碼的時候就會得到前向以及後向共兩個運動向量,因此motion_val是個長度為2的陣列,分別指向前向以及後向運動向量表,表中的每一項表示一個運動向量。一個運動向量分為x與y兩個分量。 ![]() |
mb_type W x H (frame in MBs) |
記錄一幀中所有巨集塊的型別。即每個巨集塊解碼出來的語法元素mb_type。 ![]() |
ref_index W x H (frame in 8x8 blocks) x 2 |
記錄一幀中所有8x8塊的參考影象索引。8x8塊是參考影象作用的最小單位,該表格會記錄inter巨集塊中各個8x8塊的參考影象索引。如果該塊在編碼時採用的是雙向預測,那麼在解碼的時候就會得到前向以及後向共兩個參考影象索引,因此ref_index是個長度為2的陣列,分別指向前向以及後向參考影象索引表,表中的每一項儲存一個索引。 ![]() |
H264SliceContext
h.264解碼時,各個slice之間相對來說較為獨立,因此對於從一個slice解碼出來的各個語法元素,會用一個結構體來進行維護,這個結構體就是H264SliceContext。在對slice解碼過程中涉及到的大多資料的存取都是通過該結構體來完成。其中佔用較大記憶體,並且會被頻繁使用的語法元素相關的結構體成員有以下幾個:
typedef struct H264SliceContext { int8_t intra4x4_pred_mode_cache[5 * 8]; int8_t(*intra4x4_pred_mode); DECLARE_ALIGNED(8, uint8_t, non_zero_count_cache)[15 * 8]; DECLARE_ALIGNED(16, int16_t, mv_cache)[2][5 * 8][2]; DECLARE_ALIGNED(8,int8_t, ref_cache)[2][5 * 8]; DECLARE_ALIGNED(16, uint8_t, mvd_cache)[2][5 * 8][2]; uint8_t direct_cache[5 * 8]; ///< as a DCT coefficient is int32_t in high depth, we need to reserve twice the space. DECLARE_ALIGNED(16, int16_t, mb)[16 * 48 * 2]; DECLARE_ALIGNED(16, int16_t, mb_luma_dc)[3][16 * 2]; uint8_t (*mvd_table[2])[2]; }
Menber | Description |
intra4x4_pred_mode_cache | 儲存當前巨集塊及其Left,Top方向的每個4x4塊的intra4x4預測模式,有如下用途: 1. 對當前巨集塊進行幀內預測,也就是通過intra4x4預測模式來構建巨集塊的畫素資料。 2. 在進行當前巨集塊的intra4x4預測模式的預測時,需要根據每一個4x4塊其A(左)、B(上)塊的intra4x4預測模式來進行當前預測模式的預測。 ![]() |
intra4x4_pred_mode | 儲存當前巨集塊所在的行以及前一行巨集塊的intra4x4預測模式。但是要注意的是,這裡只對每個巨集塊提供8個intra4x4預測模式的儲存位置,而實際所用到的區域只有7個,這7個intra4x4預測模式分別位於當前巨集塊的最底下一行(Bottom)以及最右邊一列(Right)。 當前巨集塊的這7個intra4x4預測模式將會作為後面所解碼的巨集塊的Left、Top方向的intra4x4預測模式使用,即會用intra4x4_pred_mode來填充intra4x4_pred_mode_cache的Left、Top的位置。 ![]() |
non_zero_count_cache | 儲存當前巨集塊及其Left、Top方向的每個4x4塊中非零係數的個數,有YUV三個分量。有如下用途: 1. 在cabac解碼語法元素coded_block_flag時,需要當前塊的A(左)以及B(上)的非零係數數目來選取上下文索引。 2. 在進行去塊濾波時,會根據邊界兩邊的塊是否含有非零係數來確定濾波強度。 3. 4x4塊non_zero_count的值會根據當前巨集塊的CBP的值來進行設定,如果一個4x4塊沒有非零係數,則沒有必要進行係數的逆量化逆變換了。 ![]() |
mv_cache | 儲存當前巨集塊及其Left、Top、Top-Right、Top-Left方向的mv。有如下用途: 1. 在進行mv預測時,會根據當前塊的A、B、C、D的mv來得到當前塊的mvp。 2. 如果當前塊是B_Direct,並且採用的是spatial預測,則會根據當前塊的A、B、C來確定當前塊的ref以及mv。 3. 在進行運動補償時,需要通過當前塊的mv來生成畫素資料。 4. 在進行去塊濾波時,會根據邊界兩邊的mv的差值來確定濾波強度。 ![]() |
ref_cache | 儲存當前巨集塊及其Left、Top、Top-Right、Top-Left方向的ref。有如下用途: 1. 在進行ref的cabac解碼時需要根據其A以及B方塊的ref來選取上下文索引值。 2. 在進行mv預測的時候,會根據解碼出來的當前塊的ref以及A、B、C、D的ref來得到mvp。 3. 在進行運動補償時,需要通過當前塊的ref來生成畫素資料。 4. 在進行去塊濾波時,會根據邊界兩邊是否為同一個ref,或者是否有同樣的參考幀數目來確定濾波強度。 5. 如果當前塊是B_Direct,並且採用的是spatial預測,則會根據A、B、C塊的ref來確定當前塊的ref以及mv。 ![]() |
mvd_cache | 儲存當前巨集塊以及其Left、Top的mvd。有如下用途: 1. 通過當前塊的mvd以及預測所得的mvp得到正確的mv。 2. 在進行當前塊的mvd的cabac解碼時需要根據其A、B塊的mvd來選取上下文索引值。 ![]() |
direct_cache | 儲存當前巨集塊的Left、Top的塊的sub_mb_type(以8x8塊為單位),主要用於判斷這些塊是否為B_Direct。在進行ref的cabac解碼時需要根據當前塊的A、B的塊是否為B_Direct來選取上下文索引值。 ![]() |
mb | 儲存當前巨集塊的YUV的畫素殘差的變換系數。在編碼的時候巨集塊畫素殘差的編碼順序為變換、量化、然後熵編碼就能得到碼流資料;而在解碼時,巨集塊的碼流在經過熵解碼後,然後執行逆量化,會得到巨集塊殘差畫素的變換系數,這些係數會被存在mb當中,對這些殘差係數執行逆變換後,就能得到畫素殘差。 ![]() |
mb_luma_dc | 儲存當前巨集塊的DC係數。在編碼時,如果當前巨集塊採用的預測模式為intra16x16,那麼畫素殘差在進行4x4的變換後會得到16個DC係數以及15x16個AC係數,在進行量化後,這16個DC係數會排列在一起先進行熵編碼,然後熵編碼這16x15個AC係數;那麼在解碼時,如果當前巨集塊的預測模式為intra16x16,那麼在執行熵解碼後會得到16個DC係數,這些DC係數會被寫入mb_luma_dc當中。在mb_luma_dc當中的這些DC係數在進行逆量化後就會被寫入mb,形成16個4x4的像殘差係數。 ![]() |
mvd_table | 儲存當前巨集塊所在的行以及前一行巨集塊的mvd。但是要注意的是,這裡只對每個巨集塊提供8個mvd的儲存位置,而實際所用到的區域只有7個,這7個mvd分別位於當前巨集塊的最底下一行(Bottom)以及最右邊一列(Right)。 當前巨集塊的這7個mvd將會作為後面所解碼的巨集塊的Left、Top方向的mvd使用,即會用於填充mvd_cache的Left、Top的位置。 ![]() |
其中名稱中含有“cache”這一名稱的結構體成員都需要當前巨集塊的周邊塊的資訊,這些資訊都是在 fill_decode_cache 中寫入到成員的陣列中的,而當前巨集塊中的資訊則是在熵解碼後直接或者間接儲存到cache結構體成員中。
這些包含cache欄位的成員中基本都有DECLARE_ALIGNED修飾,這個巨集主要用於向編譯器宣告這些成員為8或者16byte對齊。原因是為了提升處理速度,這些成員大多需要用SIMD指令進行處理,而SIMD指令在執行時,如果記憶體運算元不是對齊的,則有可能會出現效能下降。
這些結構體成員被命名為cache也是有原因的。在計算機原理中,當進行記憶體訪問時,為了提高資料訪問速度,一般都會對所訪問的記憶體及其周邊記憶體區域(即一個cache line)一同取入cache當中,如果某個程式碼段會頻繁訪問資料,並且大部分資料都在cache當中,即cache命中率高,那麼這個程式碼段的執行效率就會得到很好的提升;如果大部分資料不在cache中,即cache命中率低,就會在資料訪問上浪費大量時間。一般的處理器的L1 cache僅幾十k位元組的容量,因此在執行資料處理的時候,如果不是頻繁訪問的記憶體區域,有可能很快就會被從cache中清除。基於這些理論,現在返回來觀察h264頻繁訪問的資料,可以發現:
- 這些以cache命名的結構體成員除了包含當前巨集塊的資料之外,還包含其周邊塊的資料,特別是上一行的資料。在實際進行資料的排列的時候,是以巨集塊行為單位從左到右進行排列的,因此即使巨集塊在空間位置上是上下相鄰,但是在記憶體中也會間隔較遠,很有可能不在同一cache line中。
- 解碼一個巨集塊所需要訪問的資料繁多,解碼器為每一幀的每種資料都分配了各自的記憶體塊,這些記憶體塊都佔用相當大的記憶體空間,因此不同的資料不可能在同一cache line中。
- 解碼一個巨集塊需要多次訪問各個記憶體塊中的不同資料,並且訪問的程式碼段較為分散。由於cache空間有限,如果直接處理記憶體塊內的資料,就有可能會導致cache line被頻繁替換,使得在進行資料訪問的時候cache命中率較低,從而在資料訪問上耗費較多時間。
為了針對上述問題進行優化,ffmpeg把在進行巨集塊解碼時頻繁訪問到的資料集中到了H264SliceContext結構體中,並且用名稱包含cache欄位的成員儲存巨集塊及其周邊的資料。如此一來,就使得巨集塊解碼過程中的資料訪問的記憶體範圍大大縮小,只有在開頭的填充這些成員以及末尾的資料寫回的時候才會訪問到各個分散的記憶體塊,以此來提升記憶體的cache命中率。
還有一些未被介紹的緩衝區,指向這些緩衝區的指標是H264Context結構體的成員,主要在 ff_h264_alloc_tables 中進行記憶體分配。