8.1 はじめに

第8章では、いよいよMMX命令の使って画像処理を高速化する方法について説明したいと思います。 7.6節で述べたように、7.4節で説明したフェードアウトのプログラムと、7.5節で説明したフェードインのプログラムを高速化してみました。 いきなりプログラムの説明をすると大変なので、まずはMMXテクノロジとはどんな技術なのかという事から説明していきます。 既にMMXテクノロジについて理解している方は、8.2〜8.5節は読み飛ばして構いません。

8.2 MMXテクノロジとは

MMXテクノロジは、画像や音声などのマルチメディア関係の処理を高速化するために生まれた技術です。 加算や減算などの演算を一つずつ行うのではなく、一度に複数の値同士を加算したり減算したりする事で、高速に処理をする事ができます。 この考え方は、Pentium3から搭載されたSSE命令(ストリーミングSIMD拡張命令)でも同じです。

8.3 MMXレジスタのデータ型

MMX命令を利用する場合は、今まで利用してきたeaxレジスタやebxレジスタなどの他に、MMXレジスタと呼ばれるレジスタを利用します。 MMXレジスタにはmm0, mm1, mm2, mm3, mm4, mm5, mm6, mm7の8つがあり、どのMMXレジスタも64ビットのレジスタです。 MMX命令で扱うデータの型には、「パックド・バイト」、「パックド・ワード」、「パックド・ダブルワード」、「クワッドワード」の4種類があります。

「パックド・バイト」は、8ビット(1バイト)で表された整数を8つで1組にしたものです。 パックド・バイト同士の加算や減算を行う命令を利用すれば、8ビット(1バイト)で表された整数同士の加算や減算を一度に8つ行う事ができます。



「パックド・ワード」は、16ビット(2バイト)で表された整数を4つで1組にしたものです。 パックド・ワード同士の加算や減算を行う命令を利用すれば、16ビット(2バイト)で表された整数同士の加算や減算を一度に4つ行う事ができます。



「パックド・ダブルワード」は、32ビット(4バイト)で表された整数を2つで1組にしたものです。 パックド・ダブルワード同士の加算や減算を行う命令を利用すれば、32ビット(4バイト)で表された整数同士の加算や減算を一度に2つ行う事ができます。



「クワッドワード」は64ビット(8バイト)で表された整数なのですが、残念ながらクワッドワード同士の加算や減算を行う命令はありません。 ビットシフトなどの論理演算命令はあります。

8.4 飽和加算と飽和減算

通常の加算命令や減算命令では、演算の結果がオーバーフローしたりアンダーフローしたりすると、溢れた桁は無視されてしまいます。 その結果、正しい値を得る事ができません。 しかし、7.5節で説明したフェードインのプログラムのように、加算結果がオーバーフローしてしまう場合は最大値に丸め込みたい場合があります。 そこでMMX命令には、加算した結果がオーバーフローした場合最大値に丸め込んだり、アンダーフローした場合最小値に丸め込んでくれる命令が用意されています。 それが飽和加算命令です。

符号付きパックド・バイトの飽和加算を行うpaddsb命令では、加算結果が127を上回る場合は127に、-128を下回る場合は-128に丸め込んでくれます。 符号付きパックド・ワードの飽和加算を行うpaddsw命令では、加算結果が32767を上回る場合は32767に、-32768を下回る場合は-32768に丸め込んでくれます。 符号なしパックド・バイトの飽和加算を行うpaddusb命令では、加算結果が255を上回る場合は255に丸め込んでくれます。 符号なしパックド・ワードの飽和加算を行うpaddusw命令では、加算結果が65535を上回る場合は65535に丸め込んでくれます。

7.4節で説明したフェードアウトのプログラムのように、減算結果がアンダーフローしてしまう場合は最小値に丸め込みたい場合があります。 そこでMMX命令には、減算した結果がオーバーフローした場合最大値に丸め込んだり、アンダーフローした場合最小値に丸め込んでくれる命令が用意されています。 それが飽和減算命令です。

符号付きパックド・バイトの飽和減算を行うpsubsb命令では、減算結果が127を上回る場合は127に、-128を下回る場合は-128に丸め込んでくれます。 符号付きパックド・ワードの飽和減算を行うpsubsw命令では、減算結果が32767を上回る場合は32767に、-32768を下回る場合は-32768に丸め込んでくれます。 符号なしパックド・バイトの飽和減算を行うpsubusb命令、符号なしパックド・ワードの飽和減算を行うpsubusw命令では、減算結果が0を下回る場合は0に丸め込んでくれます。

残念ながら、飽和加算や飽和減算ができるのはパックド・バイト同士かパックド・ワード同士のみです。 パックド・ダブルワードやクワッドワードではできません。

8.5 MMX命令の有無の調べ方

MMX命令はMMX Pentium以降のCPUには搭載されていますが、Pentium以前のCPUには搭載されていません。 そのため、プログラムを動作させるマシンのCPUにMMX命令が搭載されているかどうか、MMX命令を利用する前にチェックする必要があります。

Pentium以降のCPUにはcpuidという命令があり、cpuid命令を利用する事でMMX命令の有無や、Pentium3以降のストリーミングSIMD命令の有無などをチェックする事ができます。 しかし、初期の頃の80486にはcpuid命令がないため、cpuid命令を利用する前にcpuid命令の存在もチェックしなければなりません。 cpuid命令の有無は、EFLAGSレジスタの21ビット目が書き換え可能であるかをチェックする事で確認できます。 それには、pushfd命令、popfd命令を利用します。

pushf命令はFLAGSレジスタ(EFLAGSレジスタの下位16ビット)をスタックに退避する命令ですが、pushfd命令はEFLAGSレジスタの32ビット全てをスタックへ退避する命令です。 popf命令はスタックへ退避しておいた値をFLAGSレジスタ(EFLAGSレジスタの下位16ビット)へ取り出す命令ですが、popfd命令はスタックへ退避しておいた値をEFLAGSレジスタへ取り出す命令です。

以上のことから、cpuid命令の有無は以下の手順でチェックする事ができます。

  1. まず、pushfd命令でEFLAGSレジスタの値をスタックへpushし、その値をeaxレジスタへpopします。
  2. 次に、eaxレジスタに取りだした値をebxレジスタにも代入しておきます。
  3. xor命令を利用してeaxレジスタの値の21ビット目(0から数えた場合)の値を反転します。
  4. eaxレジスタの値をスタックへpushし、その値をpopfd命令でEFLAGSレジスタへpopします。
  5. 再びpushfd命令でEFLAGSレジスタの値をスタックへpushし、その値をeaxレジスタへpopします。
  6. eaxレジスタの値とebxレジスタの値が等しければcpuid命令が搭載されていないと判断し、等しくなければcpuid命令が搭載されていると判断します。

cpuid命令の存在をチェックできたら、次はMMX命令の有無をチェックします。 eaxレジスタに1を代入してcpuid命令を実行すると、edxレジスタに機能情報が入ります。 この機能情報の特定のビットを調べる事で、様々な機能の有無を調べる事ができます。 この機能情報の23ビット目が1であればMMX命令が存在することを示しており、0であればMMX命令が存在しない事を示しています。

MMXCHECK.ASM
.586                                    ;.486では、cpuid命令は使えない
.model          flat,stdcall
.code
option          casemap :none

DLLMain         proc    p1,p2,p3
                mov     eax,1
                ret
DLLMain         endp

mmxcheck        proc    export uses ebx ecx edx dummy1,dummy2,dummy3,dummy4
                pushfd                  ;CPUID命令の存在チェック
                pop     eax
                mov     ebx,eax
                xor     eax,00200000h
                push    eax
                popfd
                pushfd
                pop     eax
                cmp     eax,ebx
                jz      no

                mov     eax,1           ;MMX命令の存在チェック
                cpuid
                test    edx,00800000h
                jz      no

                mov     eax,0
                ret
        no:     mov     eax,1
                ret
mmxcheck        endp

end             DLLMain
MMXCHECK.AS
#uselib "mmxcheck.dll"
#func mmxcheck   mmxcheck   0

mmxcheck
if stat=1 : dialog "MMX命令に対応していません。",1 : end
dialog "MMX命令に対応しています。",0

8.6 フェードインの高速化

8.4節の説明から、符号なしパックド・バイトの飽和加算を行うpaddusb命令を利用すると、7.5節で説明したフェードインのプログラムが簡単に高速化できそうな事が分かります。 そこで、paddusb命令を利用してフェードインを行うように7.5節のプログラムを修正してみましょう。

movd命令は、第一オペランドの下位32ビットに第二オペランドの値の下位32ビットを代入する命令です。 第一オペランドがmmxレジスタの場合、上位32ビットには0が代入されます。 第一オペランド及び第二オペランドには、MMXレジスタ(64ビット)、通常の32ビットレジスタ、メモリ(32ビット)の何れかが指定できますが、第一オペランドと第二オペランドの両方をメモリにする事はできません。

punpcklbw命令は、第一オペランドの下位4バイトと第二オペランドの下位4バイトを、第二オペランドへ交互に代入する命令です。 下のプログラムでは、punpcklbw命令を3回実行する事でp2の値をmm0レジスタの8つのバイトへ格納しています。



movq命令は、第一オペランドに第二オペランドの値(64ビット)を代入する命令です。 第一オペランド及び第二オペランドには、MMXレジスタ、メモリの何れかが指定できますが、第一オペランドと第二オペランドの両方をメモリにする事はできません。

paddusb命令では、第二オペランドにはMMXレジスタ又はメモリを指定する事ができます。 しかし、第一オペランドにはMMXレジスタしか指定する事ができません。 そこで、movq命令を利用してVRAM上のデータを一旦mm1レジスタに代入し、paddusb命令で飽和加算を行ってからmovq命令でVRAMを書き換えています。

MMXレジスタは、FPU命令を利用する際に利用されるFPUレジスタと共有されています。 このため、MMX命令を使い終わった後はFPUレジスタを利用できる状態に戻さなければなりません。 そのための命令がemms命令です。

7.3節の説明から分かるように、1行あたりのバイト数は4の倍数です。 従って、1行あたりのバイト数に行数をかけた全体のバイト数も4の倍数です。 しかし、全体のバイト数が8の倍数であるという保証はありません。 全体のバイト数が8で割り切れる場合と、4バイトってしまう場合があります。 全体のバイト数が8で割り切れず4バイト余ってしまう場合、余った4バイトはmovq命令の代わりにmovd命令を使って処理します。 movd mm1,[edi]で、ediレジスタが指しているメモリから32ビット分がmm1の下位32ビットに代入され、mm1の上位32ビットには0が代入されます。 movd [edi],mm1で、ediレジスタが指しているメモリへmm1の下位32ビットが代入されます。

MMXFADEIN.ASM
.586
.model          flat,stdcall
.code
option          casemap :none
.MMX                                    ;MMX命令を利用する事を宣言する
                                        ;optionより後に書かないとエラーになる

include         \masm32\include\windows.inc
include         \masm32\include\gdi32.inc
include         \masm32\include\user32.inc
include         hspdll.inc
includelib      \masm32\lib\gdi32.lib
includelib      \masm32\lib\user32.lib

bm              equ     [esi].BMSCR

DLLMain         proc    p1,p2,p3
                mov     eax,1
                ret
DLLMain         endp

bmspnt          proc    uses ebx esi hdc,bmscr:ptr
                mov     esi,bmscr
                cmp     bm.hpal,NULL
                jz      @f
                invoke  SelectPalette,hdc,bm.hpal,0
                mov     ebx,eax
                invoke  RealizePalette,hdc
        @@:     cmp     bm._type,2
                jz      @f
                invoke  BitBlt,hdc,0,0,bm.wx,bm.wy,bm.hdc,bm.xx,bm.yy,SRCCOPY
        @@:     cmp     bm.hpal,NULL
                jz      @f
                invoke  SelectPalette,hdc,ebx,0
        @@:     ret
bmspnt          endp

bms_update      proc    uses ebx esi bmscr:ptr
                mov     esi,bmscr
                cmp     bm.fl_udraw,0
                jz      @f
                invoke  GetDC,bm.hwnd
                mov     ebx,eax
                invoke  bmspnt,ebx,esi
                invoke  ReleaseDC,bm.hwnd,ebx
        @@:     ret
bms_update      endp

mmxcheck        proc    export uses ebx ecx edx dummy1,dummy2,dummy3,dummy4
                pushfd                  ;CPUID命令の存在チェック
                pop     eax
                mov     ebx,eax
                xor     eax,00200000h
                push    eax
                popfd
                pushfd
                pop     eax
                cmp     eax,ebx
                jz      no

                mov     eax,1           ;MMX命令の存在チェック
                cpuid
                test    edx,00800000h
                jz      no

                mov     eax,0
                ret
        no:     mov     eax,1
                ret
mmxcheck        endp

mmxfadein       proc    export uses ebx ecx edx esi edi bmscr:ptr,p2,dummy1,dummy2
                mov     esi,bmscr
                cmp     bm.palmode,1
                jz      err1            ;パレットモードならerr1へ
                cmp     p2,255
                ja      err2            ;p2>255ならerr2へ

                mov     eax,3
                mul     bm.sx
                test    eax,3
                jz      @f
                and     eax,0fffffffch
                add     eax,4
        @@:     mul     bm.sy
                mov     ecx,eax
                shr     ecx,3           ;ecx=バイト数/8
                mov     edx,eax
                and     edx,7           ;edx=バイト数 mod 8
                mov     edi,bm.pBit

                movd    mm0,p2          ;p2の値をmm0の各バイトにセット
                punpcklbw mm0,mm0
                punpcklbw mm0,mm0
                punpcklbw mm0,mm0

        next:   movq    mm1,[edi]       ;MMX命令を用いて、8バイトずつ飽和加算
                paddusb mm1,mm0
                movq    [edi],mm1
                add     edi,8
                loop    next

                test    edx,edx         ;余ったデータがなければokへ
                jz      ok

                movd    mm1,[edi]       ;余ったデータを飽和加算
                paddusb mm1,mm0
                movd    [edi],mm1

        ok:     emms
                invoke  bms_update,esi
                mov     eax,0
                ret
        err1:   mov     eax,-1
                ret
        err2:   mov     eax,-2
                ret
mmxfadein       endp

end             DLLMain
MMXFADEIN.AS
#uselib "mmxfadein.dll"
#func mmxcheck   mmxcheck   0
#func mmxfadein  mmxfadein  2

mmxcheck
if stat=1 : dialog "MMX命令に対応していません。",1 : end

screen 0,640,480,0,0,0,640,480
color   0,  0,255 : boxf 100,100,200,200
color   0,255,  0 : boxf 300,100,400,200
color   0,255,255 : boxf 500,100,600,200
color 255,  0,  0 : boxf 100,300,200,400
color 255,  0,255 : boxf 300,300,400,400
color 255,255,  0 : boxf 500,300,600,400
repeat 128,0
        mmxfadein 2
loop

8.7 フェードアウトの高速化

フェードアウトの場合も高速化の手順はフェードインの場合と殆ど同じですので、mmxfadeoutの説明は省略します。 fadeout、mmxfadein、mmxfadeoutの3つを見比べてみてください。

MMXFADEOUT.ASM
.586
.model          flat,stdcall
.code
option          casemap :none
.MMX

include         \masm32\include\windows.inc
include         \masm32\include\gdi32.inc
include         \masm32\include\user32.inc
include         hspdll.inc
includelib      \masm32\lib\gdi32.lib
includelib      \masm32\lib\user32.lib

bm              equ     [esi].BMSCR

DLLMain         proc    p1,p2,p3
                mov     eax,1
                ret
DLLMain         endp

bmspnt          proc    uses ebx esi hdc,bmscr:ptr
                mov     esi,bmscr
                cmp     bm.hpal,NULL
                jz      @f
                invoke  SelectPalette,hdc,bm.hpal,0
                mov     ebx,eax
                invoke  RealizePalette,hdc
        @@:     cmp     bm._type,2
                jz      @f
                invoke  BitBlt,hdc,0,0,bm.wx,bm.wy,bm.hdc,bm.xx,bm.yy,SRCCOPY
        @@:     cmp     bm.hpal,NULL
                jz      @f
                invoke  SelectPalette,hdc,ebx,0
        @@:     ret
bmspnt          endp

bms_update      proc    uses ebx esi bmscr:ptr
                mov     esi,bmscr
                cmp     bm.fl_udraw,0
                jz      @f
                invoke  GetDC,bm.hwnd
                mov     ebx,eax
                invoke  bmspnt,ebx,esi
                invoke  ReleaseDC,bm.hwnd,ebx
        @@:     ret
bms_update      endp

mmxcheck        proc    export uses ebx ecx edx dummy1,dummy2,dummy3,dummy4
                pushfd                  ;CPUID命令の存在チェック
                pop     eax
                mov     ebx,eax
                xor     eax,00200000h
                push    eax
                popfd
                pushfd
                pop     eax
                cmp     eax,ebx
                jz      no

                mov     eax,1           ;MMX命令の存在チェック
                cpuid
                test    edx,00800000h
                jz      no

                mov     eax,0
                ret
        no:     mov     eax,1
                ret
mmxcheck        endp

mmxfadeout      proc    export uses ebx ecx edx esi edi bmscr:ptr,p2,dummy1,dummy2
                mov     esi,bmscr
                cmp     bm.palmode,1
                jz      err1            ;パレットモードならerr1へ
                cmp     p2,255
                ja      err2            ;p2>255ならerr2へ

                mov     eax,3
                mul     bm.sx
                test    eax,3
                jz      @f
                and     eax,0fffffffch
                add     eax,4
        @@:     mul     bm.sy
                mov     ecx,eax
                shr     ecx,3           ;ecx=バイト数/8
                mov     edx,eax
                and     edx,7           ;edx=バイト数 mod 8
                mov     edi,bm.pBit

                movd    mm0,p2          ;p2の値をmm0の各バイトにセット
                punpcklbw mm0,mm0
                punpcklbw mm0,mm0
                punpcklbw mm0,mm0

        next:   movq    mm1,[edi]       ;MMX命令を用いて、8バイトずつ飽和減算
                psubusb mm1,mm0
                movq    [edi],mm1
                add     edi,8
                loop    next

                test    edx,edx
                jz      ok              ;余ったデータがなければokへ

                movd    mm1,[edi]       ;余ったデータを飽和減算
                psubusb mm1,mm0
                movd    [edi],mm1

        ok:     emms
                invoke  bms_update,esi
                mov     eax,0
                ret
        err1:   mov     eax,-1
                ret
        err2:   mov     eax,-2
                ret
mmxfadeout      endp

end             DLLMain
MMXFADEOUT.AS
#uselib "mmxfadeout.dll"
#func mmxcheck   mmxcheck   0
#func mmxfadeout mmxfadeout 2

mmxcheck
if stat=1 : dialog "MMX命令に対応していません。",1 : end

screen 0,640,480,0,0,0,640,480
color   0,  0,255 : boxf 100,100,200,200
color   0,255,  0 : boxf 300,100,400,200
color   0,255,255 : boxf 500,100,600,200
color 255,  0,  0 : boxf 100,300,200,400
color 255,  0,255 : boxf 300,300,400,400
color 255,255,  0 : boxf 500,300,600,400
repeat 128,0
        mmxfadeout 2
loop

8.8 おわりに

MMX命令の使い方の説明はいかがでしたでしょうか? 実を言うと、第7章のプログラムを書き始めるまでは僕自身もMMX命令を使ったことがありませんでした。 MMX命令についていろいろ調べながら第7章と第8章を書いたため、誤っている点や改良すべき点があるかもしれません。 何かご意見がありましたら掲示板かE-mailでお知らせください。