如何快速alpha混合RGBA无符号字节颜色?

use*_*749 20 c++ performance

我正在使用c ++,我想使用以下代码进行alpha混合.

#define CLAMPTOBYTE(color) \
    if ((color) & (~255)) { \
        color = (BYTE)((-(color)) >> 31); \
    } else { \
        color = (BYTE)(color); \
    }
#define GET_BYTE(accessPixel, x, y, scanline, bpp) \
    ((BYTE*)((accessPixel) + (y) * (scanline) + (x) * (bpp))) 

    for (int y = top ; y < bottom; ++y)
    {
        BYTE* resultByte = GET_BYTE(resultBits, left, y, stride, bytepp);
        BYTE* srcByte = GET_BYTE(srcBits, left, y, stride, bytepp);
        BYTE* srcByteTop = GET_BYTE(srcBitsTop, left, y, stride, bytepp);
        BYTE* maskCurrent = GET_GREY(maskSrc, left, y, width);
        int alpha = 0;
        int red = 0;
        int green = 0;
        int blue = 0;
        for (int x = left; x < right; ++x)
        {
            alpha = *maskCurrent;
            red = (srcByteTop[R] * alpha + srcByte[R] * (255 - alpha)) / 255;
            green = (srcByteTop[G] * alpha + srcByte[G] * (255 - alpha)) / 255;
            blue = (srcByteTop[B] * alpha + srcByte[B] * (255 - alpha)) / 255;
            CLAMPTOBYTE(red);
            CLAMPTOBYTE(green);
            CLAMPTOBYTE(blue);
            resultByte[R] = red;
            resultByte[G] = green;
            resultByte[B] = blue;
            srcByte += bytepp;
            srcByteTop += bytepp;
            resultByte += bytepp;
            ++maskCurrent;
        }
    }
Run Code Online (Sandbox Code Playgroud)

然而,我发现它仍然很慢,当编写两个600*600图像时需要大约40-60毫秒.有没有什么方法可以将速度提高到不到16毫秒?

任何人都可以帮我加速这段代码吗?非常感谢!

Tom*_*eys 26

使用SSE - 从第131页开始.

基本工作流程

  1. 从src加载4个像素(16个1字节数)RGBA RGBA RGBA RGBA(流加载)

  2. 加载另外4个要与srcbytetop RGBx RGBx RGBx RGBx混合的区域

  3. 做一些调整,以便1中的A项填充每个插槽即

    xxxA xxxB xxxC xxxD - > AAAA BBBB CCCC DDDD

    在我的解决方案下面我选择,而不是重新使用现有的"maskcurrent"阵列,但具有α-集成到1中的"A"字段将需要更少的存储器量,因而更快.在这种情况下混合可能是:并使用掩码选择A,B,C,D.右移8,或原始,右移16,或再次.

  4. 将上面的内容添加到每个插槽中全部为-255的向量中

  5. 乘以1*4(具有255-alpha的源)和2*3(具有alpha的结果).

    您应该可以使用"乘法和丢弃底部8位"SSE2指令.

  6. 将这两个(4和5)加在一起

  7. 将它们存放在其他地方(如果可能)或存放在目的地之上(如果必须)

以下是您的出发点:

    //Define your image with __declspec(align(16)) i.e char __declspec(align(16)) image[640*480]
    // so the first byte is aligned correctly for SIMD.
    // Stride must be a multiple of 16.

    for (int y = top ; y < bottom; ++y)
    {
        BYTE* resultByte = GET_BYTE(resultBits, left, y, stride, bytepp);
        BYTE* srcByte = GET_BYTE(srcBits, left, y, stride, bytepp);
        BYTE* srcByteTop = GET_BYTE(srcBitsTop, left, y, stride, bytepp);
        BYTE* maskCurrent = GET_GREY(maskSrc, left, y, width);
        for (int x = left; x < right; x += 4)
        {
            //If you can't align, use _mm_loadu_si128()
            // Step 1
            __mm128i src = _mm_load_si128(reinterpret_cast<__mm128i*>(srcByte)) 
            // Step 2
            __mm128i srcTop = _mm_load_si128(reinterpret_cast<__mm128i*>(srcByteTop)) 

            // Step 3
            // Fill the 4 positions for the first pixel with maskCurrent[0], etc
            // Could do better with shifts and so on, but this is clear
            __mm128i mask = _mm_set_epi8(maskCurrent[0],maskCurrent[0],maskCurrent[0],maskCurrent[0],
                                        maskCurrent[1],maskCurrent[1],maskCurrent[1],maskCurrent[1],
                                        maskCurrent[2],maskCurrent[2],maskCurrent[2],maskCurrent[2],
                                        maskCurrent[3],maskCurrent[3],maskCurrent[3],maskCurrent[3],
                                        ) 

            // step 4
            __mm128i maskInv = _mm_subs_epu8(_mm_set1_epu8(255), mask) 

            //Todo : Multiply, with saturate - find correct instructions for 4..6
            //note you can use Multiply and add _mm_madd_epi16

            alpha = *maskCurrent;
            red = (srcByteTop[R] * alpha + srcByte[R] * (255 - alpha)) / 255;
            green = (srcByteTop[G] * alpha + srcByte[G] * (255 - alpha)) / 255;
            blue = (srcByteTop[B] * alpha + srcByte[B] * (255 - alpha)) / 255;
            CLAMPTOBYTE(red);
            CLAMPTOBYTE(green);
            CLAMPTOBYTE(blue);
            resultByte[R] = red;
            resultByte[G] = green;
            resultByte[B] = blue;
            //----

            // Step 7 - store result.
            //Store aligned if output is aligned on 16 byte boundrary
            _mm_store_si128(reinterpret_cast<__mm128i*>(resultByte), result)
            //Slow version if you can't guarantee alignment
            //_mm_storeu_si128(reinterpret_cast<__mm128i*>(resultByte), result)

            //Move pointers forward 4 places
            srcByte += bytepp * 4;
            srcByteTop += bytepp * 4;
            resultByte += bytepp * 4;
            maskCurrent += 4;
        }
    }
Run Code Online (Sandbox Code Playgroud)

要找出哪些AMD处理器将运行此代码(目前它正在使用SSE2指令),请参阅Wikipedia的AMD Turion微处理器列表.您还可以查看维基百科上的其他处理器列表,但我的研究表明,大约4年前的AMD cpus至少都支持SSE2.

您应该期望SSE2的良好运行速度比当前代码快8-16倍.这是因为我们消除了循环中的分支,一次处理4个像素(或12个通道),并通过使用流指令提高缓存性能.作为SSE的替代方案,您可以通过消除用于饱和的if检查来更快地运行现有代码.除此之外,我需要在您的工作负载上运行一个分析器.

当然,最好的解决方案是使用硬件支持(即在DirectX中编写问题代码)并在视频卡上完成.


Jas*_*ers 21

您始终可以同时计算红色和蓝色的alpha值.您也可以将此技巧与前面提到的SIMD实现一起使用.

unsigned int blendPreMulAlpha(unsigned int colora, unsigned int colorb, unsigned int alpha)
{
    unsigned int rb = (colora & 0xFF00FF) + ( (alpha * (colorb & 0xFF00FF)) >> 8 );
    unsigned int g = (colora & 0x00FF00) + ( (alpha * (colorb & 0x00FF00)) >> 8 );
    return (rb & 0xFF00FF) + (g & 0x00FF00);
}


unsigned int blendAlpha(unsigned int colora, unsigned int colorb, unsigned int alpha)
{
    unsigned int rb1 = ((0x100 - alpha) * (colora & 0xFF00FF)) >> 8;
    unsigned int rb2 = (alpha * (colorb & 0xFF00FF)) >> 8;
    unsigned int g1  = ((0x100 - alpha) * (colora & 0x00FF00)) >> 8;
    unsigned int g2  = (alpha * (colorb & 0x00FF00)) >> 8;
    return ((rb1 | rb2) & 0xFF00FF) + ((g1 | g2) & 0x00FF00);
}
Run Code Online (Sandbox Code Playgroud)

0 <= alpha <= 0x100


Gui*_*zan 17

对于想要除以255的人,我发现了一个完美的公式:

pt->r = (r+1 + (r >> 8)) >> 8; // fast way to divide by 255
Run Code Online (Sandbox Code Playgroud)

  • +1但请注意,它仅对r <65535有效 (5认同)
  • 这可以扩展到两个16位字:((r + 0x10001 +((r >> 8)&0xFF00FF))>> 8)&0xFF00FF这允许在ARGB中复用xRxB和AxGx操作,类似于RGBA和其他变体 (2认同)
  • (((x + 1)* 257)&gt;&gt; 16 // [0..65790)的整数div 255-替代公式在某些平台上可能更快-有趣的注释:[通过乘法除法](http: //research.swtch.com/divmult) (2认同)
  • @nobar:用乘法逆进行除法的标准编译器技巧也值得考虑:[`n/255` 编译为 = asm 执行 `(n*0x8081) &gt;&gt; 23`](https://godbolt.org /g/GUaezV)。这也适用于所有 16 位 `n`。(我刚刚注意到您的上限高于 65536)。对于 x86 SSE2,这是一个 `_mm_mulhi_epu16` 和一个 `_mm_srli_epu16(mul, 23-16)`。`x+1 * 257` 是一个 paddw 和一个 pmulhuw,所以实际上更好(因为 mul 和 shift 可能会竞争同一个端口)。 (2认同)

Rod*_*ddy 6

这里有一些指示.

考虑使用Porter和Duff描述的预乘的前景图像.除了可能更快,您还可以避免许多潜在的色边效果.

合成方程由...改变

r =  kA + (1-k)B
Run Code Online (Sandbox Code Playgroud)

... 至 ...

r =  A + (1-k)B
Run Code Online (Sandbox Code Playgroud)

或者,您可以重新设计标准公式以删除一个乘法.

r =  kA + (1-k)B
==  kA + B - kB
== k(A-B) + B
Run Code Online (Sandbox Code Playgroud)

我可能错了,但我认为你不应该需要夹紧......


nfr*_*s88 6

我无法发表评论,因为我没有足够的声誉,但我想说 Jasper 的版本不会因为有效输入溢出。屏蔽乘法结果是必要的,因为否则红色+蓝色乘法会在绿色通道中留下位(如果您分别将红色和蓝色相乘,这也是正确的,您仍然需要屏蔽掉蓝色通道中的位)和绿色乘法将在蓝色通道中留下位。如果将组件分离出来,这些位会因右移而丢失,这在 alpha 混合中经常出现。所以它们不会上溢或下溢。它们只是需要屏蔽以达到预期结果的无用位。

也就是说,Jasper 的版本是不正确的。它应该是 0xFF-alpha (255-alpha),而不是 0x100-alpha (256-alpha)。这可能不会产生可见的错误。会产生可见错误的是他对 | 的使用。而不是 + 合并乘法结果时。

我发现对 Jasper 代码的改编比我旧的 alpha 混合代码更快,这已经很不错了,并且目前正在我的软件渲染器项目中使用它。我使用 32 位 ARGB 像素:

Pixel AlphaBlendPixels(Pixel p1, Pixel p2)
{
    static const int AMASK = 0xFF000000;
    static const int RBMASK = 0x00FF00FF;
    static const int GMASK = 0x0000FF00;
    static const int AGMASK = AMASK | GMASK;
    static const int ONEALPHA = 0x01000000;
    unsigned int a = (p2 & AMASK) >> 24;
    unsigned int na = 255 - a;
    unsigned int rb = ((na * (p1 & RBMASK)) + (a * (p2 & RBMASK))) >> 8;
    unsigned int ag = (na * ((p1 & AGMASK) >> 8)) + (a * (ONEALPHA | ((p2 & GMASK) >> 8)));
    return ((rb & RBMASK) | (ag & AGMASK));
}
Run Code Online (Sandbox Code Playgroud)