色を正しく混ぜるか、AlphaBlendを最適化する

私はマルチプロトコル(しかし、マルチプラットフォームではなく、悲しいことに、現在はWindowsのみ)メッセンジャーを書いています。 しかし、メッセンジャーについてではなく、そのインターフェースについて、そしてより正確には、その主な機能であるAlphaBlendについて話しています。 はい、 自転車の GUIを作成することにしました。 さて、半透明の要素と滑らかな曲線のない現代のGUIは何ですか? そのため、半透明性を考慮して画像をミックスする必要が急務でした。 アルファブレンディングまたはアルファブレンディング。 幸いなことに、Windows GDIにはそのような機能-AlphaBlendがあります。 必要に応じて機能し、必要なことを行います。 しかし、私はまだ自転車メーカーであり、同じ機能をより速く書けるかどうか疑問に思いました。 カットの下での私の労働の結果。



アルファ混合理論



ほとんどの場合、この理論を知っているので、私はそれを詳細に説明せず、主なポイントのみを述べます。



したがって、ソースピクセルと宛先ピクセルの2つのピクセルがあります。 それらを混合して、新しい宛先ピクセルを取得する必要があります。 各ピクセルは4バイトのA、R、G、Bで表されます。Aはピクセルの(非)透明度の値(0は完全に透明、255は完全に不透明)、RGBは色成分です。 従来の混合式は次のとおりです。



TGT_COLOR = TGT_COLOR * (1 - SRC_ALPHA) + SRC_COLOR * SRC_ALPHA
      
      





重要なポイント! 単位は式にあります。 私たちの生活では、値255は単一性を表します。つまり、 数式を適用するには、まず各バイトの値を255で除算する必要があります。ご覧のとおり、255と256は非常に近い値であり、256で除算することは8ビットの右シフトです。 したがって、このような単純化が頻繁に発生します:手術の代わりに



 (X) * (A/255.0)
      
      



以下を実行します。
  (X * A) >> 8
      
      





これはうまく機能します(そして最も重要なのは、正直な除算よりもはるかに高速です)が、アルファミキシングの場合、結果はまったく正しくありません。つまり、結果のピクセルは少し暗くなります。 次に、速度を落とさずに正確に計算を実行する方法を示します。



もう一つの重要なポイント! 式を見てください。 2番目の部分はSRC_COLOR * SRC_ALPHAです。 3Dアクセラレータは、瞬きすることなく、数百万から数十億もの乗算を実行します。 しかし、中央処理装置を使用して問題を解決しようとしていますが、各ピクセルによる過剰な乗算(正確には4つの追加の乗算)はあまり良くありません。 なぜ多すぎるのですか? はい。元の画像を変換することで、この乗算を事前に実行できるためです。 そのような画像には名前があります: premultiplied 。 ロシア語の用語はわかりませんが、文字通り翻訳すると「乗算」されます。 そして確かに、GDI AlphaBlend関数はソースイメージとして厳密に事前乗算されている必要があります。 これは合理的です。



さて、理論が終了しました。 実際には、32ビットカラーを使用します。 1ピクセルは32ビットの数値で表されます。最小値から始まる4バイトは、B(lue)、G(reen)、R(ed)、A(lpha)を意味します。 行こう



最初の実装



私の最初の実装は次のようなものでした:



 uint32 ALPHABLEND_PM(uint32 dst, uint32 src) { uint8 ba = ALPHA(src); //  ALPHA    32-  if (ba == 0) return dst; //  == 0,     == 0,      float a = (float)((double)(ba)* (1.0 / 255.0)); //        :) float not_a = 1.0f - a; //   :   uint B = lround(float(BLUE(dst)) * not_a) + BLUE(src); //  BLUE  0-  32-  uint G = lround(float(GREEN(dst)) * not_a) + GREEN(src); //  GREEN  1-  32-  uint R = lround(float(RED(dst)) * not_a) + RED(src); //  RED  2-  32-  uint A = lround(float(ALPHA(dst)) * not_a) + ALPHA(src); return B | (G << 8) | (R << 16) | (A << 24); //  32-    }
      
      





私は同意します、それは非常によく見えません。 4つの実際の(または5つの)乗算とピクセルあたり4ラウンドが多すぎます。 速度の面で、このモンスターがAlphaBlendに約7回負けたことは驚くことではありません。



改善してみましょう。 実際の乗算を取り除きます。



 uint32 ALPHABLEND_PM(uint32 dst, uint32 src) { uint not_a = 256 - ALPHA(src); return = src + (((not_a * BLUEx256(dst))>>16) | (((not_a * GREENx256(dst))>>8) & 0xff00) | (((not_a * REDx256(dst))) & 0xff0000) | (((not_a * ALPHAx256(dst))<<8) & 0xff000000)); }
      
      





ここでは、関数はBLUEx256、GREENx256などです。 8ビット左にシフトされた対応するコンポーネントを返します。 256回。



この関数は、255による除算を8ビット右シフトで置き換えることに対する補償があるという点で注目に値します。 気づいた? そうでない場合は、しばらくお待ちください。以下でこの点について詳しく説明します。



速度では、この実装はAlphaBlendに約3倍劣っています。 すでに優れていますが、それでも理想からはほど遠いです。



予期しない結果



前の機能をどのように改善できますか? できることはすべてやり終えたようです。 しかし、私は驚いたことにこの機能を改善することができました。 何も機能しないことを確認するためだけに試しました。 しかし、判明した。

テーブル内のバイトごとの乗算演算を実行するとどうなりますか。 65536バイトしかありません。 ペニー。



このようなタブレットを作成します。



 uint8 __declspec(align(256)) multbl[256][256];
      
      





以下を記入します。



 for (int i = 0; i < 256; ++i) for (int j = 0; j < 256; ++j) { int k = i * j / 255; multbl[i][j] = (uint8)k; }
      
      





私達は試みます:



 uint32 ALPHABLEND_PM(uint32 dst, uint32 src) { uint8 not_a = 255 - ALPHA(src); return src + ((multbl[not_a][dst & 0xff]) | (((uint)multbl[not_a][(dst >> 8) & 0xff]) << 8) | (((uint)multbl[not_a][(dst >> 16) & 0xff]) << 16) | (((uint)multbl[not_a][(dst >> 24) & 0xff]) << 24)); }
      
      





驚くべきことに、この関数は以前の実装よりも1.5倍速く動作しました。 確かに、微妙な点が1つあります-コンパイラー(私の場合はmsvc 2013)は、メモリー操作で非常に有能に機能しました。 この関数をむき出しのアセンブラで記述しようとすると、どうやらオプティマイザよりもはるかに優れているように見えますが、この関数の2倍の速度で動作します。 失敗でした。 間違いを正確に理解できませんでした-明らかに、すべての操作を適切に並列化することができませんでした-この関数はオプティマイザーに任せました。



だから。 最適化するものはこれ以上ありません。 私には他に何も起こりません。 しかし、AlphaBlendは2倍高速です。 彼らはどのようにしてこれを達成しましたか? 引退する時ですか?



シフトによる255による除算の置換の補償について



255ですばやく分割する方法は多数あります。これに遭遇しました。



 X/255 == (X+1+(X>>8)) >> 8
      
      





これは悪くありません。 これは、正直な除算よりも255速くなります。しかし、それでも面倒です。 私は長い間、255で素早く分割し、品質や速度を落とさない方法を考えていました。 シフト使用時に色の劣化を補正する方法は?



0xff (255)に等しい色成分があり、 0xff (255)に等しい別の成分があるとします。 それらを掛けると、次のようになります。



0xff * 0xff = 0xfe01 。 8ビットを右にシフトすると、 0xfeが得られます-コンポーネントの輝度が低下します。 残念だ。

しかし、乗算の前にコンポーネントの1つを1つ増やすとどうなりますか?

0xff * 0x100 = 0xff00 うーん、そうですね。 コンポーネントの1つが0の場合を確認しましょう。

0xff * 1 = 0x00ff 、8ビット右にシフトすると、0が得られます。 他のコンポーネント値の場合、結果もtrueになります。

これで、2番目の関数で補正の場所を簡単に見つけることができます。uint not_a = 256-ALPHA(src);

255-Aではなく、256-A、つまり 乗算前のコンポーネントに+1。 乗算の表形式の方法では、補正は必要ありません。 表では、すべての値が必要に応じて計算されます。



重砲-SSSE3の手順



simdを使用した最適化について考える時が来ました。 彼らは、インテルのコンパイラーは人間の介入なしでこれを行うことができると言っています。 おそらく。 しかし、疑いはIntelがAlphaBlend'omに対処していることを私にかじります。 まあ、最大-彼女に等しい。 しかし、もっと速くする必要があります。 ディレクトリを開いて行きます。



最初に尋ねる質問は、最適化が実行する命令は何ですか? AlphaBlendはMMX向けに最適化されているのではないかと疑っています。そうでない場合は、純粋なx86実装に対する優位性を説明できません。 MMXは優れていますが、前世紀です。 現在、SSE4がサポートされていないコンピューターを見つけるのは困難です。 また、これらの命令のサポートを確認することさえせずに、SSE向けに最適化できます。Pentium3の下でプログラムが実行される可能性はゼロに近いです。 もちろん、私はデスクトップアプリケーションについて話しています。 エキゾチックはこの記事の範囲外です。



SSSE3を選択しました。 この一連の命令は、非常に便利な命令が存在することを考えると、そのためだけに最適化することで混乱するほど広く普及しています。



すべての最適化の基礎を形成する最も有用な命令はpshufb (intrinsic _mm_shuffle_epi8 )です。 SSSE3が選ばれたのは彼女のためです。 彼女の強さは? この命令を使用すると、元の16バイトレジスタのバイトを任意の順序で分散したり、これらのバイトを不要なものとして廃棄したりすることができます。 つまり 必要な計算に必要なすべてを準備するために、この命令を1つのモーションで使用できます。 もう1つの重要な命令-pmulhuw (intrinsic _mm_mulhi_epu16 )-は、16ビットずつ8回の乗算と8回の右シフトです。 特にアルファブレンディング操作用。 つまり このコマンドだけで、実際に一度に2ピクセルを計算します。



さあ、行こう:



Asmコードシート
  lddqu xmm5, [eax] ;   xmm5 16 ,  4   premultiplied  movdqa xmm6, xmm5 ;   xmm6     2-  ;  :  ;        16    pshufb xmm5, preparesrcs_1 pshufb xmm6, preparesrcs_2 ;  ; xmm5   2 ,     16  ; xmm6     2  ;  :  2  4  ;      8 16-  (256-A) ;    xmm7 movdqa xmm2, xmm5 ;   2   xmm2 pshufb xmm2, preparealphas ;         : A0 A0 A0 A0 A1 A1 A1 A1 movdqa xmm7, sub256 ;  xmm7  8 16-  256 psubw xmm7, xmm2 ;    movdqu xmm0, [edx] ; 4   movdqa xmm1, xmm0 ;   xmm1   3 pshufb xmm0, preparetgtc_1 ;   2   16- ,    8 pmulhuw xmm0, xmm7 ;     2   16-  paddw xmm0, xmm5 ;         pshufb xmm0, packcback_1 ;     8  xmm0 ;   -  ,    2,      movdqa xmm2, xmm6 pshufb xmm2, xmm3 movdqa xmm7, xmm4 psubw xmm7, xmm2 pshufb xmm1, preparetgtc_2 pmulhuw xmm1, xmm7 paddw xmm1, xmm6 pshufb xmm1, packcback_2 por xmm0, xmm1 ;   xmm0 4   movdqu [edx], xmm0 ;       
      
      







ご覧のとおり、simdの実装では、4つのソースピクセルと4つのデスティネーションピクセルが直ちに混合されます。 じゃあ、彼女とシムド。 この記事の範囲外で、4番目のピクセルの倍数を混合する必要がある場合の問題の解決策の説明は残しておきます。 個人的には、このためにc ++実装の「シングルピクセル」呼び出しを使用します。



まとめ



その結果、このssse3の実装は、AlphaBlend関数のほぼ4倍(ハードウェアでは3.78)速く動作します。 これは非常に良い結果です。 多くのプログラマー(私を含む)は、このような「自転車」に懐疑的です。 原則として、結果は明らかに有能な専門家のチームの仕事よりも悪いです。 私はAlphaBlend関数の実装を書くことを始めましたが、Microsoftの仲間を倒すことができるとは信じていませんでした。 それはただのスポーツの興味でしたが、それでも結果が出ました。



しかし、それだけではありません。 実際、この記事では、単純なケースのコードを示しました。元の画像が結果の画像とそのまま混合される場合です。 ただし、 AlphaBlend関数のドキュメントを読むと、この関数が定数アルファ(パラメーターを通過)による追加の乗算を実行できることに気付くかもしれません。 この場合のssse3実装も作成しました。 興味深い結果:定数アルファが255に等しくない場合、AlphaBlendはほぼ2倍遅くなります。 追加の色の乗算が必要です。 私の実装の速度は4%しか低下していません。これは、Microsoftの作成とも区別しています。



参照資料



この記事のコードは、ssse3最適化の原理を理解するためにのみ提供されています。 ここで使用する定数の値は示しませんでした。 プロジェクトで最適化されたAlphaBlendを使用する場合は、Isotoxinのソースから作業コードを直接取得する必要があります(私の開発が呼ばれているため)。



githubの Isotoxinリポジトリ。

目的の機能が存在するファイルをここに直接指定します。



実例を用意しておらず、すべてを別のライブラリに入れていないことをおpoびします。 この機能が本当に必要で、自分のソースから自分で入手できない場合は、個人的なメッセージを書いてください。その方法を詳しく説明します。



All Articles