Chiharu の日記

絵描き C/C++ プログラマーの日記です。

描画処理のまとめ 〜アセンブラとの戦い

先ほどまで Parallel force - パラレル・フォースの描画処理をまとめてました。具体的にはコンパイラの出力するアセンブラ コードとにらめっこしてました。見直していると、画素あたりの処理が複雑なようで、下記の問題が発生していました。

  • 画素あたりの処理が複雑すぎて、自動変数のスタック退避が多発している
  • 画素あたりの処理をファンクタとして定義したが、処理が複雑すぎて十分にインライン展開されないケースがある

前者は自動変数の使用量を減らすことで多少改善しました。あと、そもそも x64 だと汎用レジスタ数および XMM レジスタ数がそれぞれ 16 本あるので x86 ほどは問題にならないようでした。
問題は後者。ファンクタは下記のような感じで定義していました。
まずは画素アクセサ(Gray-8bit の例)と、

///
/// #imageG8 の画素アクセサ
///
struct PixElemG8 {
 std::uint32_t pack32(std::uint8_t* iPix) {
  auto aLevel = iPix[0];
  return (aLevel << 16) | (aLevel << 8) | aLevel;
 }
 std::uint8_t getAlpha(std::uint32_t iPixPacked32) {
  return 0xff;
 }
#ifdef PFORCE_USE_SIMD
 __mm128i pack32to128(std::uint8_t* iPix, PixSimdSSE2) {
  return _mm_cvtsi32_si128(pack32(iPix));
 }
 __mm128i pack32to128(std::uint8_t* iPix, PixSimdSSSE3) {
  __mm128i aSrc = _mm_loadl_epi64(reinterpret_cast<__mm128i*>(iPix));
  __mm128i aFlags = _mm_setr_epi8(0, 0, 0, -1, -1, -1, -1, -1,
   -1, -1, -1, -1, -1, -1, -1, -1);
  return _mm_shuffle_epi8(aSrc, aFlags); // SSSE3
 }
 __mm128i pack64to128(std::uint8_t* iPix, PixSimdSSE2) {
  __mm128i aSrc = _mm_loadl_epi64(reinterpret_cast<__mm128i*>(iPix));
  aSrc = _mm_unpacklo_epi8(aSrc, aSrc);
  return _mm_unpacklo_epi16(aSrc, aSrc);
 }
 __mm128i pack64to128(std::uint8_t* iPix, PixSimdSSSE3) {
  __mm128i aSrc = _mm_loadl_epi64(reinterpret_cast<__mm128i*>(iPix));
  __mm128i aFlags = _mm_setr_epi8(0, 0, 0, -1, 1, 1, 1, -1,
   -1, -1, -1, -1, -1, -1, -1, -1);
  return _mm_shuffle_epi8(aSrc, aFlags); // SSSE3
 }
 __mm128i pack128(std::uint8_t* iPix, PixSimdSSE2) {
  __mm128i aSrc = _mm_loadl_epi64(reinterpret_cast<__mm128i*>(iPix));
  aSrc = _mm_unpacklo_epi8(aSrc, aSrc);
  return _mm_unpacklo_epi16(aSrc, aSrc);
 }
 __mm128i pack128(std::uint8_t* iPix, PixSimdSSSE3) {
  __mm128i aSrc = _mm_loadl_epi64(reinterpret_cast<__mm128i*>(iPix));
  __mm128i aFlags = _mm_setr_epi8(0, 0, 0, -1, 1, 1, 1, -1,
  2, 2, 2, -1, 3, 3, 3, -1);
  return _mm_shuffle_epi8(aSrc, aFlags); // SSSE3
 }
 __mm128i pack128A(std::uint8_t* iPix, PixSimdSSE2) { // α値としてロード
  return _mm_setr_epi32(iPix[0], iPix[1], iPix[2], iPix[3]);
 }
 __mm128i pack128A(std::uint8_t* iPix, PixSimdSSSE3) { // α値としてロード
  __mm128i aSrc = _mm_loadu_si128(reinterpret_cast<__mm128i*>(iPix));
  __mm128i aFlags = _mm_setr_epi8(0, -1, -1, -1, 1, -1, -1, -1,
   2, -1, -1, -1, 3, -1, -1, -1);
  return _mm_shuffle_epi8(aSrc, aFlags); // SSSE3
 }
 __mm128i getAlpha(__mm128i iPixPacked128) {
  return _mm_set1_epi32(255);
 }
#endif // PFORCE_USE_SIMD
 static std::uint8_t length = 1;
};

次いで画素合成の基本処理と、

template <class BaseBlender, class AlphaBlender>
struct PixBlender {
 PixBlender(BaseBlender iBaseBlender, AlphaBlender iAlphaBlender):
  mBaseBlender(iBaseBlender), mAlphaBlender(iAlphaBlender) { }
 std::uint32_t operator()(std::uint32_t iDst, std::uint32_t iSrc, PixLevel iAlpha)
 {
  return mAlphaBlender(iDst, mBaseBlender(iDst, iSrc), iAlpha);
 }
#ifdef PFORCE_USE_SIMD
 template <class PixSimd>
 __mm128i operator()(__mm128i iDst, __mm128i iSrc, __mm128i iAlpha, PixSimd iSimd)
 {
  return mAlphaBlender(iDst, mBaseBlender(iDst, iSrc), iAlpha, iSimd);
 }
#endif // PFORCE_USE_SIMD
private:
 BaseBlender mBaseBlender; ///< 画素データ合成処理 (基本合成)
 AlphaBlender mAlphaBlender; ///< 画素データ合成処理 (不透明度)
};

template <class BaseBlender, class AlphaBlender>
PixBlender<BaseBlender, AlphaBlender> createBlender(BaseBlender iBaseBlender,
 AlphaBlender iAlphaBlender)
{
 return PixBlender<BaseBlender, AlphaBlender>(iBaseBlender, iAlphaBlender);
}

基本合成処理(通常合成の例)と、

struct PixBaseBlenderNormal {
 std::uint32_t operator()(std::uint32_t iDst, std::uint32_t iSrc)
 {
  return iSrc;
 }
#ifdef PFORCE_USE_SIMD
 __mm128i operator()(__mm128i iDst, __mm128i iSrc)
 {
  return iSrc;
 }
#endif // PFORCE_USE_SIMD
};

不透明度処理(α値なしで不透明度が奇数値のフェードの例)を定義。

struct PixAlphaBlenderNonAlphaFadeEven {
 explicit PixAlphaBlenderNonAlphaFadeEven(PixLevel iFade):mFade256(convertRange255to256(iFade))
 {
#ifdef PFORCE_USE_SIMD
  assert((mFade256 & 0x01) == 0);
  
  auto aAlphaAddr = reinterpret_cast<__mm128i*>(
   (reinterpret_cast<std::size_t>(mFade256_16x8Data) + 15) & ~15);
  auto aSrcAlpha = _mm_set1_epi8(mFade256 / 2);
  auto aDstAlpha = _mm_set1_epi8(128 - mFade256 / 2);
  auto aAlpha = _mm_unpacklo_epi8(aDstAlpha, aSrcAlpha);
  _mm_store_si128(aAlphaAddr, aAlpha);
#endif // PFORCE_USE_SIMD
 }
 PixAlphaBlenderNonAlphaFadeEven(PixAlphaBlenderNonAlphaFadeEven& iObj):mFade256(iObj.mFade256)
 {
#ifdef PFORCE_USE_SIMD
  assert((mFade256 & 0x01) == 0);
  
  auto aAlphaAddr = reinterpret_cast<__mm128i*>(
   (reinterpret_cast<std::size_t>(mFade256_16x8Data) + 15) & ~15);
  auto aAlphaAddr2 = reinterpret_cast<__mm128i*>(
   (reinterpret_cast<std::size_t>(iObj.mFade256_16x8Data) + 15) & ~15);
  _mm_store_si128(aAlphaAddr, _mm_load_si128(aAlphaAddr2));
#endif // PFORCE_USE_SIMD
 }
 std::uint32_t operator()(std::uint32_t iDst, std::uint32_t iSrc, PixLevel iAlpha)
 {
  auto aSrcAlpha = mFade256;
  auto aDstAlpha = 256 - aSrcAlpha;
  auto aDst13 = (iDst & 0xff00ff) * aDstAlpha + (iSrc & 0xff00ff) * aSrcAlpha;
  auto aDst02 = (iDst & 0x00ff00) * aDstAlpha + (iSrc & 0x00ff00) * aSrcAlpha;
  return ((aDst13 & (0xff00ff << 8)) | (aDst02 & (0x00ff00 << 8))) >> 8;
 }
#ifdef PFORCE_USE_SIMD
 __mm128i operator()(__mm128i iDst, __mm128i iSrc, __mm128i iAlpha32x4, PixSimdSSE2)
 {
  auto aZero = _mm_setzero_si128();
  auto aSrcAlpha = _mm_set1_epi16(mFade256);
  auto aDstAlpha = _mm_sub_epi16(_mm_set1_epi16(256), aSrcAlpha);
  
  // 下位 64-bit
  auto aSrc = _mm_unpacklo_epi8(aZero, iSrc);
  auto aDst = _mm_unpacklo_epi8(aZero, iDst);
  aSrc = _mm_mulhi_epu16(aSrc, aSrcAlpha);
  aDst = _mm_mulhi_epu16(aDst, aDstAlpha);
  auto aLo = _mm_add_epi16(aSrc, aDst);
  
  // 上位 64-bit
  aSrc = _mm_unpackhi_epi8(aZero, iSrc);
  aDst = _mm_unpackhi_epi8(aZero, iDst);
  aSrc = _mm_mulhi_epu16(aSrc, aSrcAlpha);
  aDst = _mm_mulhi_epu16(aDst, aDstAlpha);
  auto aHi = _mm_add_epi16(aSrc, aDst);
  
  return _mm_packus_epi16(aLo, aHi);
 }
 __mm128i operator()(__mm128i iDst, __mm128i iSrc, __mm128i iAlpha32x4, PixSimdSSSE3)
 {
  auto aAlphaAddr = reinterpret_cast<__mm128i*>(
   (reinterpret_cast<std::size_t>(mFade256_16x8Data) + 15) & ~15);
  auto aAlpha = _mm_load_si128(aAlphaAddr);
  
  // 下位 64-bit
  auto aSrc = _mm_unpacklo_epi8(iDst, iSrc);
  auto aLo = _mm_maddubs_epi16(aSrc, aAlpha);
  aLo = _mm_srli_epi16(aLo, 7);
  
  // 上位 64-bit
  aSrc = _mm_unpackhi_epi8(iDst, iSrc);
  auto aHi = _mm_maddubs_epi16(aSrc, aAlpha);
  aHi = _mm_srli_epi16(aHi, 7);
  
  return _mm_packus_epi16(aLo, aHi);
 }
#endif // PFORCE_USE_SIMD
private:
 std::uint32_t mFade256;
#ifdef PFORCE_USE_SIMD
 std::uint8_t mFade256_16x8Data[sizeof(__mm128i) * 2];
#endif // PFORCE_USE_SIMD
};

これらのファンクタを下記のようなループに対して、

template <class PixBenderT, class PixElem, class PixSimd>
void blitImageDotByDotImpl(Image& ioDst, PixBenderT iPixBlender, Rect& iArea,
 Image& iSrc, Point& iSrcOffset, PixElem iPixElem, PixSimd iSimd)
{
 assert(ioDst.getFormat() == imageB8G8R8X8);
 
 // 領域決定
 auto aDstMaxArea = RectHelper::set(0, 0, ioDst.getWidth(), ioDst.getHeight());
 auto aArea = RectHelper::intersect(aDstMaxArea, iArea);
 if (RectHelper::isEmpty(aArea)) {
  // 塗りつぶしの必要がない。
  return;
 }
 
 // 転送
 auto aSrcY = iSrcOffset.y;
 auto aSrcRowbytes = iSrc.getRowbytes();
 auto aDstRowbytes = ioDst.getRowbytes();
 auto aBottom = aArea.bottom;
 for (auto aY = aArea.top; aY < aBottom; ++aY, ++aSrcY) {
  auto aDstS = reinterpret_cast<std::uint32_t*>(ioDst.getPixelData() + aY * aDstRowbytes);
  auto aDstE = aDstS + aArea.right;
  auto aDst = aDstS + aArea.left;
  auto aSrc = iSrc.getPixelData() + aSrcY * aSrcRowbytes + iSrcOffset.x * iPixElem.length;
#ifdef PFORCE_USE_SIMD
  if ((aDstE - aDst) > 7) {
   auto aDst8S = reinterpret_cast<decltype(aDst)>((reinterpret_cast<std::size_t>(aDst) + 15) & ~15);
   while (aDst != aDst8S) {
    auto aSrcElem = iPixElem.pack32(aSrc);
    *aDst++ = iPixBlender(*aDst, aSrcElem, iPixElem.getAlpha(aSrcElem));
    aSrc += iPixElem.length;
   }
   auto aDst8E = aDst + ((aDstE - aDst) & ~7);
   while (aDst != aDst8E) {
    auto aDst128 = reinterpret_cast<__mm128i*>(aDst);
    
    auto aSrcElem = iPixElem.pack128(aSrc + iPixElem.length * 0, iSimd);
    _mm_store_si128(aDst128 + 0, iPixBlender(_mm_load_si128(aDst128 + 0), aSrcElem,
     iPixElem.getAlpha(aSrcElem), iSimd));
    
    aSrcElem = iPixElem.pack128(aSrc + iPixElem.length * 4, iSimd);
    _mm_store_si128(aDst128 + 1, iPixBlender(_mm_load_si128(aDst128 + 1), aSrcElem,
     iPixElem.getAlpha(aSrcElem), iSimd));
    
    aSrc += iPixElem.length * 8;
    aDst += 8;
   }
  }
#else
  auto aDst8E = aDst + ((aDstE - aDst) & ~7);
  while (aDst != aDst8E) {
   auto aSrcElem = iPixElem.pack32(aSrc + iPixElem.length * 0);
   aDst[0] = iPixBlender(aDst[0], aSrcElem, iPixElem.getAlpha(aSrcElem));
   
   aSrcElem = iPixElem.pack32(aSrc + iPixElem.length * 1);
   aDst[1] = iPixBlender(aDst[1], aSrcElem, iPixElem.getAlpha(aSrcElem));
   
   aSrcElem = iPixElem.pack32(aSrc + iPixElem.length * 2);
   aDst[2] = iPixBlender(aDst[2], aSrcElem, iPixElem.getAlpha(aSrcElem));
   
   aSrcElem = iPixElem.pack32(aSrc + iPixElem.length * 3);
   aDst[3] = iPixBlender(aDst[3], aSrcElem, iPixElem.getAlpha(aSrcElem));
   
   aSrcElem = iPixElem.pack32(aSrc + iPixElem.length * 4);
   aDst[4] = iPixBlender(aDst[4], aSrcElem, iPixElem.getAlpha(aSrcElem));
   
   aSrcElem = iPixElem.pack32(aSrc + iPixElem.length * 5);
   aDst[5] = iPixBlender(aDst[5], aSrcElem, iPixElem.getAlpha(aSrcElem));
   
   aSrcElem = iPixElem.pack32(aSrc + iPixElem.length * 6);
   aDst[6] = iPixBlender(aDst[6], aSrcElem, iPixElem.getAlpha(aSrcElem));
   
   aSrcElem = iPixElem.pack32(aSrc + iPixElem.length * 7);
   aDst[7] = iPixBlender(aDst[7], aSrcElem, iPixElem.getAlpha(aSrcElem));
   
    aSrc += iPixElem.length * 8;
    aDst += 8;
  }
#endif // PFORCE_USE_SIMD
  while (aDst != aDstE) {
   auto aSrcElem = iPixElem.pack32(aSrc);
   *aDst++ = iPixBlender(*aDst, aSrcElem, iPixElem.getAlpha(aSrcElem));
   aSrc += iPixElem.length;
  }
 }
}

下記のように使います。

blitImageDotByDotImpl(ioDst, createBlender(PixBaseBlenderNormal(),
 PixAlphaBlenderNonAlphaFadeEven(iFade)), aArea, iSrc, aSrcOffset,
 PixElemG8(), iSimd);

まぁ、ファンクタの実装が十分簡単であればインライン展開されるのですが、アセンブラ出力を確認した限りにおいて、今回の場合はいくつかのケースでしっかりとファンクタのメソッドを call されていました。すんごく残念な気分になったので、ファンクタに対して __forceinline キーワードを使用して強制的にインライン展開するようにしました。結果、アセンブラ出力はこんな感じになりました。ファンクタ起因の call はなくなったようで、すっきりしました。