|
|
自從今年 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 (tapu@371.net) 的新思路. 針對分級數比較少的 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 值, 如此就不能多點同時運算, 雲風找到了另外的加速方法, 要知詳情, 且看下文分解 ^_^ | | | |