C++
Assembler
H.264
01 Nov 2015
Pretty Pull Request
ちょっと前に OpenH264 というエンコーダに小さなプルリクを出した時のメモが以下のように今も残ってた。
この Mac のデフォルトのメモ帳は要素を増やすとそれなりに動作が緩慢になっていくので消そうと思ったけど
ただ消すのも忍びないので書き残しておくことにした。
メモでは色々調べていたけど、作業的には
小さなもの
だった。
ことの発端としてはこういうサイト
を見ながら、小さなSIMDプログラムを書く機会があったが、本当に小さくてすぐ終わってしまったので
他にも何かしらで書けないかと考えた時についでにOSSにプルリク投げてみるか、という機運が高まったことから始まった。
最終的にSIMDコード書けてないわけだけど…。
さらに、SIMDは intrinsics
で書きたかったのだけれど、
SIMDはどこもアセンブラで、メモを見返してみるとアセンブラについて調べた部分がいくらかあった。
書いてて思い出してきたが AVX2の開発も受け付けてるという記述があり、書こうと思ったら
そもそもAVX2がプログラム内で有効であるか検出する機構がないので、とりあえずそれを書いたという流れだったと思う。
コミットログを眺めてみたけどぱっと見AVX2コードは入ってなさそう。今はあんまりやる気がない..。
もう書くことがないので、あとはつらつらメモの残りを羅列することにする。
プロジェクトを何も知らない人が読んでも意味がわからず、メモ同士の関連性も低く
本当に有用にするには補助的な文章が必要だろうけど面倒くさいのでしない。
あと、ここに書いてあることの正しさは保証してませんので予めご了承。
OpenH264の解析
ISVCEncoder
空っぽのクラス。抽象クラス。
具象クラスは CWelsH264SVCEncoder
になる。メインのでかいクラス。
WelsCreateSVCEncoder() は encoder/plus/src/welsEncoderExt.cpp にある。
エントリーポイント
codec/console/enc/src/welsenc.cpp にコンソール版の main() がある。
- ISVCEncoder を初期化
- ProcessEncoding でエンコードを発火
- ISVCEncoder, SEncParamExt, SFrameBSInfo
- SFrameBSInfo の実際のデータを持ってるのは SLayerBSInfo
- SSourcePicture
- ffmpeg の AVFrame とほぼ同じ
- iStride = { luma_stride, chroma_stride }
- pData = { luma_ptr, chroma_ptr }
- fread で1枚ずつ読む
- ISVCEncoder->EncodeFrame(SSourcePicture, SFrameBSInfo)
EncodeFrame
EncodeFrameInternalを呼び出す。入力がI420じゃない場合は中断。
EncodeFrameInternal
- WelsEncoderEncodeExt() // 1枚のフレームのエンコード発火
- UpdateStatistics() // 統計情報更新
WelsEncoderEncodeExt
メインな関数。色々呼び出す。分かったものだけ記述。
- DecideFrameType()
- BuildSpatialPicList()
- AnalyzePictureComplexity()
- iEncodeNal()
DecideFrameType
フレームタイプを決定する。x264にも似た名前の関数があるけど、x264と違ってここから探索が発生することはなさそう
- イントラ周期ならIDRにするとか
- InterだけどSKIPフラグがあればSKIPにするとか
- シーンチェンジ検出 (IDRにする)
Intra予測の関数
- WelsMdIntraMb
- WelsMdI16x16
- WelsMdI4x4
- WelsMdI4x4Fast
- WelsMdIntraChroma
- WelsMdIntraSecondaryModesEnc
MEはどこ/MEはどこから発火する?
svc_motion_estimate.cpp:WelsMotionEstimateSearch[Static|Scrolled]
encoder_ext において pFuncList->pfMotionSearch に代入されている。
代入後 core/src/svc_base_layer_md
で利用されている。
- 4種類 (16x16, 8x16, 16x8, 8x8) 存在
- どれも SATD を返す
WelsMotionEstimateSearch は内部で pFuncList->pfCalculateSatd を呼び出す
- pfCalculateSatd の実行時引数には sSampleDealingFuncs.pfSampleSatd[BlockSize] を指定して呼び出す
- sSampleDealingFuncs.pfSampleSatd はどこで関数が設定されるのか
core/src/sample:WelsInitSampleSadFunc
で設定
- Sad という名前だけど Satd の関数も設定されうる
- cpuid で分岐し SIMD 版も設定している
WelsMdP16x16 はどこから呼ばれる
- svc_base_layer_md
- WelsMdInterMb (mode-decision inter macroblock)
- WelsMdInterSecondaryModesEnc (refinement)
- ちゃんと他の予測タイプも試す
- core/src/md
- MeRefineFracPixel (hpel)
- MeRefineQuarPixel (qpel)
- WelsMdInterEncode
- WelsInterMbEncode
- WelsPMbChromaEncode
- pFunc->pfDctFourT4() (CbCrのために2回呼ぶ)
- WelsEncRecUV() (CbCrbのために2回呼ぶ)
WelsMdInterMb と WelsMdInterMbEnhancelayer の違い
- Enhancelayer
- step(3) とあるので 流れは通常の WelsMdInterMb と同じものを汲んでいるぽい
- 1, まずSKIPにできるか試す
- WelsMdInterJudgePskip の結果を見ている (boolean)
- できそうならば WelsMdInterDecidedPskip をコールして終了
- 2, Intraが選択できない場合
- とりあえず WelsMdInterP16x16 にしてしまう.
- WelsMdInterSecondaryModesEnc();
- 3, 2でIntraが選択できた場合
- とりあえず I_16x16 にしてしまう
- スキップが有効ならばスキップに
- WelsMdInterDecidedPskip をコールして終了
- そうでない場合は WeldMdIntraSecondaryModesEnc
- 分岐条件
- svc_encode_slice.cpp: 670 の (kbBaseAvail && kbHighestSpatial) で分岐
アセンブラ
読む上で気をつけたこと。
- 各命令実行後のレジスタの状態
- フラグレジスタなど
- SIMD命令の場合は無視して良い?
- 呼出し規約
データの処理単位
- b: byte(8bit)
- w: word(16bit)
- d: double word(32bit)
- q: quad word(64bit)
- dq: double quad word(128bit)
セミコロンの前の文字はよく命令の suffix についている。
メモってあった頻出命令
mov[d|q|dqa|dqu|sxd]
- movd
- move double word : オペランドに xmmレジスタなども取れるが 32bit転送命令
- movq
- move quad word : 64bit 転送命令
- movdqa
- movdqu
- movsxd
- double word(32bit)を quad word(64bit)に符号拡張して転送
lea
アドレス計算命令。フラグレジスタが変更されない。アドレス自体の演算に利用。
pxor
ビット単位の xor を実行。オペランドには 64bit同士、128bit同士を指定することができる。
psub[b|w|d]
オペランドに64bit同士、128bit同士をとれる。
suffix がそれぞれ byte, word, double-word を指すので 8bit/16bit/32bit スロットでの演算となる。
pmax[s|u][|b|w|d]
レジスタを2つとり、スロット毎に比較して最大値を格納する。
値を評価する場合に符号あり・なしとスロットの大きさで命令が分かれる。
psadbw
符号なし8bit整数で絶対値誤差の合計値を計算して格納レジスタの下位16bitに書き込む。
オペランドに64bit/128bitレジスタ同士を取る。
shufps
名前的にはシャッフルを押すほどのシャッフルでもないけど確かにシャッフル可能な命令。
オペランドを3つとり(うちSIMDレジスタが2つ)、スロットを32bitで評価してそれぞれの
SIMDレジスタから2つのスロットを格納先へ書き込む。
pmaddubsw
Multiply-ADD Unsigned Byte, Signed Word
乗算はオペランドレジスタを符号なし8bit整数スロットで評価し、
次のオペランドレジスタは符号あり8bitで評価。
乗算の結果を2つずつ和にして16bitスロットとして格納先レジスタへ書き込む。
movehl_ps
各 hi-64bit を2つのSIMDレジスタからとって merge。
YASM/NASM
用いられているアセンブラ。
codec/common/x86/inc_asm.asm に共通マクロがある。
わかったものだけ。
- WELS_EXTERN
- SIGN_EXTENSION
- WELSEMMS
- PUSH_XMM
- POP_XMM
- retrd
- 戻り値レジスタの alias で主に eax
- retrq なんてのもあった
AVX2の検出
- eax=0 で cpuid 命令を実行後に eax に入る値(基本機能の最大機能番号)が7以上
- eax=7 で cpuid 命令を実行後に ebx & 0x20 が 1 である時AVX2が有効
AVXの検出
- eax=1 で cpuid 命令を実行後に (ecx & 0x18000000) == 0x18000000 である
- xgetbv が有効で xgetbv を実行後に (eax & 0x6) == 0x6 である時AVXが有効
OpenH264のアセンブラ関数
あんまり調べてない。
WelsSampleSadFourWxH
4つ分計算する。この4つ分は上下左右の1pixelだけずれたものを計算している。
WelsSampleSad4x4_mmx
ナイーブに実装されている。mmxなのでレジスタ長が64bitなので8bit*8で2line分まで入るので
よしなにロードして2回 psadbw するだけ。
WelsSampleSad16x16_sse2
128bitSAD命令で 4x16 を4回呼ぶ構成。
場所
codec/common/x86/satd_sad.asm