利用MMX优化64K色Alpha混合算法


  自从今年 3 月云风开始使用 Pentium 200MMX CPU 后, 一直在考虑如何用 MMX 技术加快 Alpha 混合的操作, 尤其是针对目前常用的高彩模式. 而早先在国外一个有关游戏编程的 MailList 讨论的结果是 MMX 不利于对 16位色进行 Alpha 混合操作. 让我们先来看看 MMX 技术相对于普通指令集的更新,来了解一下这个论点的立论.

  MMX 技术的优势在于, 它的寄存器是 64 位的, 而提供了分组模式, 可以将寄存器内的数据 按 8 个字节, 或 4 个字, 或 2 个双字同时进行同一操作, 方便了大数据量的数据处理; 可以成组数据同时作比较操作, 这为透明色点的批量判断带来好处; MMX 的 CPU 拥有 8 个 MMX 寄存器, 在一定程度上缓解了 80x86 CPU 寄存器数量不足的缺陷.

  但是它也有诸多不足, 比如算术指令不能对四字节字操作; 指令结构都不影响标志位; 不能对常数立即寻址; MMX 系统指令集的指令相当贫乏(连 NOT 操作也不能直接实现);

  当颜色深度是 24/32 位时, RGB 都占 8 位, 这样可以巧妙的利用 MMX 里的分组乘法指令达到 做 Alpha 混合运算的效果(MMX 的乘法相关指令只有对字操作的 PMULHW/PMULLW 两条, 分别是成组数据的乘后取高位和取低位) 本文旨在探讨 16bit 色的快速 Alpha 混合运算, 所以此处略去不提.

  而 16bit 色, 红绿蓝各占 5 或 6 位, 难以被分组分开, 所以不利于运用 MMX 的这些特性. 当然另外的解决方法是采用 aRGB 4444 的结构, 其中 4 位是 Alpha 通道, 每个色素占半个字节, 再采用类似的方法.

  看过云风去年提出的16bit Alpha 混合优化算法的朋友, 应该会联想到这个算法向 MMX 的引申, OK, 也许你已经明白了大概, 本文的理论基本点就在此, 唯一的问题是, 我们需要面对的是 MMX 指令集的种种缺陷, 这些在实际的程序设计中会逐步的体现出来, 下面, 云风将在介绍算法的同时, 附带的提出一些运用 MMX 的技巧(随后将有专文介绍 MMX 编程技术)

  先来看看上次的算法有无可进一步优化的可能:

  16bit 下 Alpha 混合的关键在于如何将 RGB 分离, 让随后的乘法结果不至于相互干扰.

我提出的是将 16bit 的 rrrrrggggggbbbbb 扩展到 32bit 变形成 00000gggggg00000rrrrr000000bbbbb, 即将中间的绿色提到高 16 位, 而使色素间隔都有 5 到 6 位, 而 对于 5 位的颜色, 超过 5 位的 Alpha 级别是没有意义的, 所以只要设定 Alpha 值在 0~31 间, 同时算这 3 个色素的乘法是不会因为 进位造成干扰的. 而这里需要多操作一次移位扩展 16 位到 32 位, 然后需要一次与操作, 将中间间隔位置0, 而且结果需要同样复杂的逆操作从 32 位还原到 16 位.

  改进的思路是直接将两个点交错分离, 即 rrrrrggggggbbbbbRRRRRGGGGGGBBBBB 分离成 rrrrr000000bbbbb00000GGGGGG00000 和 00000gggggg00000RRRRR000000BBBBB 两部分, 前一部分右移 5 位后变成 00000rrrrr000000bbbbb00000GGGGGG, 两个数字就都可以同时运算 3 个色素, 其结果后一组右移 5 位后可以与前一组合并. 这样就省去了好几次移位操作, 并且数据可以 4 字节读入, 和四字节写, 粗看真的效率很高. 但是在传统的 80x86 上却有两点制约了它的运用:

  1. CPU 的寄存器不够用, 这个方法光保存数据就需要 4 个 32 位的寄存器, 虽然 EAX,EBX,ECX,EDX 刚够用, 但是这就使得 Alpha 混合函数不能直接写在 Blit 操作里面. 必须单写个子程序调用. (不过也值得写尝试一下, 不是吗? 如果有朋友写好了, 希望能给我拜读一下,我在风魂游戏程序库里留了接口, 并在注释里提到了函数的具体写法)
  2. 2D 游戏中, 一般都是利用 Alpha 混合绘制精灵而不是规则的矩形位图, 所以这里面还存在着 透明色的判断, 如果是双点处理, 这一步不易实现. (不过也不是没有好的方法, 就是代码的长度就长而复杂了:-( )
而 MMX 却提供了 8 个寄存器, 同时有分组比较的指令, 正好弥补了这两点不足, 而且利用寄存器有 64 位的优势可以同时运算 4 个点. 所以我们暂且只用 MMX 来实现新的想法.(如果你对这个方法用在 传统指令集上有兴趣, 希望同时操作 2 个点进行 Alpha 混合, 并写出实际的代码, 请和我联系, 我非常希望看到风魂的非 MMX Alpha 混合版本能够进一步优化)

  用 MMX 来做这项工作, 原理差不多(相当简单不是?), 也是读入源点和目标点后分离成 4 个数据放在 4 个寄存器中. 两对间进行 Alpha 混合, (这样一对数据间就同时运算了 6 个色素) 最后就两对数据混合的结果合并。 不过从现在开始我们就要面对 MMX 8 个寄存器不够用的困境了 :-( MMX 指令不能和 64 位立即常数一起使用, 所以在进行分裂操作的时候用到的掩码要常驻在寄存器内. 如果寄存器主够多的话, 可以连掩码的反值也放一个, 可惜现在不能这么浪费 :-( 处理透明色问题方面, 可以先将点和透明色比较得到一个掩码, 我们再将混合后的点,及原来的目标图上的点 (这个点应当保留一个备份, 哎, 又去了一个寄存器) 分别与掩码逻辑运算合并得到最终的数据写入目标图. 这里, 需要大量运用的 NOT 操作, Intel 竟然没有在 MMX 指令集中提供 @#$%^&! 我们只好用 PANDN (取反再与操作) 间接完成. (例:可以先用 PCMPEQW mm0,mm0 (自己和自己比较当然全相等了 ;-) 生成常数 FFFFFFFFFFFFFFFF, 用 PANDN mm1,mm0 就可以将 mm1 取反.) 这里, 不再可以利用 MMX 的分组乘法, (MMX 不能对 32 位数进行乘法操作) 所以我们应该用移位和加减法来实现. 这样, 如果有几级 Alpha 值, 就应该写几个混合函数. 最后建立一个函数指针数组, 将每级 Alpha 混合函数依次放入数组. 我们在调用时就可以根据需要的 Alpha 值来调用相应的函数了 :-)

  最后对关于带 Alpha 通道的位图的做一点探讨, 这里每一个点将带有不同的 Alpha 值, 我们应该合理的协调位图的结构. 将 Alpha 值和颜色信息放在一起是不合算的. 这样不利于高速处理。 我们可以将所有点的 Alpha 值提出来放在一起, 对于 16bit 的颜色, 合理的 Alpha 级别应该在 16级以下。 这样可以每一个字节存放两个 Alpha 值. 用一个寄存器作为指向 Alpha 值区域的指针, 读入对应点的 Alpha 值, 调用相应的混合函数运算。

  本文提出的方法, 都被云风实践证明可行, 请参阅风魂游戏程序库 的源代码. 你会发现速度相当的快. 测试表明, MMX 下带 Alpha 混合的位图操作, 仅仅比普通的检查 透明色的位图操作慢 20%. 比不用 MMX, 逐点做 Alpha 混合快 2.7 倍. 如果采用 RLE 压缩掉透明色点, 去掉对透明色的特殊处理, 速度还会有很大的提高. (达到 DirectDraw 里内存 Surface 间key-color检查的 blit 操作的速度) 这个算法的意义在于, 16bit 色下, 软件 Alpha 混合的速度已经足够快, 这使游戏中大量运用光影效果不再有速度上的顾虑 ^_^