自从今年 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 上却有两点制约了它的运用:
CPU 的寄存器不够用, 这个方法光保存数据就需要 4 个 32 位的寄存器, 虽然 EAX,EBX,ECX,EDX 刚够用, 但是这就使得 Alpha 混合函数不能直接写在 Blit 操作里面. 必须单写个子程序调用. (不过也值得写尝试一下, 不是吗? 如果有朋友写好了, 希望能给我拜读一下,我在风魂游戏程序库里留了接口, 并在注释里提到了函数的具体写法)
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 值来调用相应的函数了 :-)
在风魂 0.07 里, Alpha 混合又一次修改了算法, (0.06 使用的上述算法, 0.07 则没有) 这里要感谢网友 T&P ([email protected]) 的新思路. 针对分级数比较少的 Alpha 混合, 比如 8 级, 可以用更简单的方法. 大家可以注意到, 50% 的 Alpha 时, R=(r1+r2)/2, 也可以近似的等于 r1/2+r2/2. 那么 RGB 可以方便的同时运算. 只需要在移位后做一次简单的与操作即可 (0RRRRRGGGGGGBBBB & 011110111101111=0RRRR0GGGGG0BBBB) 然后, 将两个移位后的数据相加就完成了 Alpha=50% 的混合. 这个方法避免了切分和还原数据, 所以速度更快. 风魂的早期版本, 对 50% 的 Alpha 度就做了此种特殊处理. 但是, 它是有误差的, 误差在于移位造成的每色素上 1/32 或 1/64 的偏差.
下一步我们可以将 50% 的 Alpha 值推广到 25% 12.5% 甚至更小. 现在来看一下完成 R1*25%+R2*75%, 它等于 R2+R1*25%-R2*25%=R2+R1/4+R2/4. 这里除 4 的操作和除 2 原理是一样的即: (RRRRRGGGGGGBBBBB >> 2) & 0011100111100111. 依次类推, X * 37.5% + Y * 62.5% = (X+Y)/2 + Y/8 - X/8 等等. 我们就只需要利用移位和加减法就可以同时完成 N 个色素的混合了.
再来看看这个方法的缺陷. 首先是误差问题, 每一组移位取与都会造成最大为 1/32 的误差, 而多次运算有可能使误差累计, 所以 alpha 级别不能分的太多. 而且 alpha 级别分的太细后, 使得运算步骤变的很多, 不切分直接运算的优势有可能损失掉. 而且更致命的一点是, 如果想用 MMX 加速, 那么通常 AND 运算用的掩码应该放在寄存器中 (如果放在内存, 而 MMX 不能立即寻址, 间接寻址取内存可能不能命中 CACHE 速度变慢, 大规模的混合运算速度损失太多) MMX 的寄存器却只有 8 个. 那么多个掩码会使明显的感觉寄存器不够用, 但这不失为一种好的方法. 风魂 0.07 中新的 alpha 精灵, 这一步的算法更改带来了 10% 左右的速度提升, 而画质的损失却几乎没有体现 :-)
最后对关于带 Alpha 通道的位图的做一点探讨, 这里每一个点将带有不同的 Alpha 值, 我们应该合理的协调位图的结构. 将 Alpha 值和颜色信息放在一起是不合算的. 这样不利于高速处理。我们可以将所有点的 Alpha 值提出来放在一起, 对于 16bit 的颜色, 合理的 Alpha 级别应该在 16级以下。这样可以每一个字节存放两个 Alpha 值. 用一个寄存器作为指向 Alpha 值区域的指针, 读入对应点的 Alpha 值, 调用相应的混合函数运算。但是, 这种位图每个点都有可能是不同的 alpha 值, 如此就不能多点同时运算, 云风找到了另外的加速方法, 要知详情, 且看下文分解 ^_^