7.1 はじめに

一口に画像処理と言っても、明るさやコントラストを調整したり、ぼかしたり、鮮明にしたりするなど、様々な画像処理があります。 これらの画像処理は、HSPのPSET命令とPGET命令を駆使すれば、プラグインを使わずに実現する事も不可能ではありません。 しかし、プラグインを使わずにHSPだけで実現しようとすると、処理に時間がかかってしまいます。 そこで、VRAMを直接操作する事で高速に画像処理を行うプラグインを作ってみましょう。

7.2 VRAMについて

VRAMを直接操作する事で画像処理を行うためには、まずVRAMとはどういうものなのかについて知っておかなければなりません。 これについては、「HSPからのDLL呼び出し方法リファレンスマニュアル」に書かれているのでそちらを読んでください。

7.3 画像のスクロール

簡単な画像処理の例として、ウインドウ内の画像を上にスクロールさせるrollup命令を作ってみましょう。 これを作っておけば、ゲームのエンディングなどに利用できそうです。 何ドット上にスクロールさせるかは、p2で指定する事にしたいと思います。 (p1はBMSCR構造体へのポインタです。) まずは、どのようにすれば良いか考えてみましょう。

仮に、p2の値を2として考えたのが下の図です。 横のドット数はsx、縦のドット数はsyです。 これを上に2ドットスクロールさせるということは、太枠の部分を移動する事になります。 太枠の部分をどのようにして移動するかですが、点A、点B、点C、…の順に下から移動していくのは誤りです。 一番下の2行の移動が終わって3行目の移動を始める時、3行目にあるのは元々3行目にあった画像ではなく、1行目から移動されてきた画像だからです。 従って、点D、点E、点F、…の順に上から移動していかなければなりません。 逆に、上にスクロールするのではなく下にスクロールする場合は、上からではなく下から移動していかなければなりません。

移動する順番が分かったら、次は移動するバイト数を考えてみましょう。 ここでは、パレットモードではなくフルカラーモードで初期化されているものとします。 フルカラーモードの場合、1ドットにつきR,G,Bの3バイトで1ドットの色を表しますから、横のドット数がsx、縦のドット数がsyであれば3*sx*(sy-p2)になりますね。

次に、一番最初に移動する点Dのアドレスを考えてみましょう。 画面上の任意の点(x,y)の色が入っているアドレスは、

青の濃度値のアドレス = pBit + { ( sy - 1 - y ) * sx * 3 } + ( x * 3 )
緑の濃度値のアドレス = pBit + { ( sy - 1 - y ) * sx * 3 } + ( x * 3 ) + 1
赤の濃度値のアドレス = pBit + { ( sy - 1 - y ) * sx * 3 } + ( x * 3 ) + 2
ですから、点D(sx-1,p2)のアドレスは
青の濃度値のアドレス = pBit + { ( sy - p2 ) * sx * 3 } - 3
緑の濃度値のアドレス = pBit + { ( sy - p2 ) * sx * 3 } - 2
赤の濃度値のアドレス = pBit + { ( sy - p2 ) * sx * 3 } - 1

となります。 従って、pBit + { ( sy - p2 ) * sx * 3 } - 1から順に移動していけばいい事が分かります。

次に、一番最初に移動する点の移動先のアドレスを考えてみましょう。 一番最初に移動する点の移動先は点G(sx-1,0)ですから、

青の濃度値のアドレス = pBit + ( 3 * sx * sy ) - 3
緑の濃度値のアドレス = pBit + ( 3 * sx * sy ) - 2
赤の濃度値のアドレス = pBit + ( 3 * sx * sy ) - 1

となります。 従って、pBit + ( 3 * sx * sy ) - 1から順に移動していけばいい事が分かります。

今計算した値をそれぞれecxレジスタ、esiレジスタ、ediレジスタに格納し、std命令を実行した上で「rep movsb」を実行すれば、ウインドウ内の画像を上にスクロールさせる事ができます。 但し、VRAM上の画像データを操作しただけでは実際のウインドウには反映されないため、実際のウインドウに反映させなければなりません。 実際のウインドウに反映させるにはHSPのredraw命令を使用するのが簡単ですが、ここでは第4章でbms_sendを移植したようにbms_updateを移植してみました。 なお、移動後の図の灰色の部分はそのままにしてありますので、必要に応じてboxf命令で塗り潰す等してください。

今説明した事を元に作成したのが下のプログラムです。 スクロールさせる手順については今説明したので、まだ説明していない命令だけを説明します。

まず、前の方に「bm equ [esi].BMSCR」というのが出てきます。 このequというのはHSPの#define命令と全く同じ働きをするもので、「bm」という名前が出てきたら「[esi].BMSCR」に置き換えてアセンブルしなさい、とアセンブラに命令しているのです。 このようにする事で、BMSCR構造体を参照するたびに「[esi].BMSCR」と書く手間が省けます。

rollupの中で使用しているmul命令は掛け算をする命令なのですが、これが少し厄介です。 足し算を行うadd命令や引き算を行うsub命令にはオペランドが2つありますが、このmul命令にはオペランドが1つしかありません。 では、オペランドの値と何の値を掛けるのかというと、オペランドが8ビットの場合はalレジスタ、16ビットの場合はaxレジスタ、32ビットの場合はeaxレジスタとの掛け算が行われます。 掛け算をした結果は、オペランドが8ビットの場合alレジスタに入るわけでもオペランドに入るわけでもなく、axレジスタに入ります。 また、オペランドが16ビットの場合は、掛け算をした結果の下位16ビットがaxレジスタに、上位16ビットがdxレジスタに入ります。 オペランドが32ビットの場合は、掛け算をした結果の下位32ビットがeaxレジスタに、上位32ビットがedxレジスタに入ります。 下のプログラムでは大きな値をかけているわけではなく、上位32ビットが0になる事は明らかであることから、掛け算をした後のedxレジスタの値は無視しています。

sub命令は、第一オペランドの値から第二オペランドの値を引いた値を、第一オペランドに代入する命令です。 dec命令は、オペランドの値を1だけ減らす命令です。

ROLLUP.ASM
.486
.model          flat,stdcall
.code
option          casemap :none

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

rollup          proc    export uses ebx ecx edx esi edi bmscr:ptr,p2,dummy1,dummy2
                mov     esi,bmscr
                cmp     bm.palmode,1
                jz      err1            ;パレットモードならerr1へ
                mov     eax,p2
                cmp     eax,bm.sy
                jae     err2            ;p2≧syならerr2へ

                mov     eax,3
                mul     bm.sx
                mov     ebx,eax         ;ebx=1行のバイト数

                mov     eax,bm.sy
                sub     eax,p2
                mul     ebx
                mov     ecx,eax         ;ecx=(sy-p2)*ebx(移動するバイト数)

                mov     eax,bm.sy
                mul     ebx
                mov     edi,bm.pBit
                add     edi,eax
                dec     edi             ;edi=pBit+sy*ebx-1(移動先のアドレス)

                mov     esi,bm.pBit
                add     esi,ecx
                dec     esi             ;esi=pBit+ecx-1(移動元のアドレス)

                pushf                   ;移動
                std
                rep     movsb
                popf

                invoke  bms_update,bmscr
                mov     eax,0
                ret
        err1:   mov     eax,-1
                ret
        err2:   mov     eax,-2
                ret
rollup          endp

end             DLLMain
ROLLUP.AS
#uselib "rollup.dll"
#func rollup rollup 2

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 240,0
	rollup 2
loop

さて、上のサンプルを実行すると、意図した通りウインドウ内の図形が上へスクロールしていきますね。 めでたしめでたし・・・ と言いたいところなのですが、screen命令のパラメータを「0,639,480,0.0,0,639,480」に変えてみると・・・ あれれ?左斜め上にスクロールしてしまいますね。 また、screen命令のパラメータを「0,638,480,0.0,0,638,480」に変えてみると、色が変化しながら左斜め上にスクロールしてしまいます。 一体何が間違っているのでしょうか?

HSPのプラグインの作成について解説しているサイトを検索してみたところ、「HSPからのDLL呼び出し方法リファレンスマニュアル」に記載されている「pBit + { ( sy - 1 - y ) * sx * 3 } + ( x * 3 )」という式は、横のドット数(sxの値)が4の倍数の時にしか成り立たない事が分かりました。 今まで、1行のバイト数は3*sxであるという前提で解説しましたが、この3*sxという値が4の倍数ではない場合、1行のバイト数が4の倍数になるようにダミーのバイトが挿入されているのです。


と言っても分かりにくいでしょうから、具体的な例で説明しましょう。 仮にsx=6、sy=6だったとします。 点(0,5)の色はpBit、pBit+1、pBit+2に入っています。 点(1,5)の色はpBit+3、pBit+4、pBit+5に入っています。 …… 点(5,5)の色はpBit+15、pBit+16、pBit+17に入っています。

と、ここまでは良いのですが、点(0,4)の色はpBit+18、pBit+19、pBit+20に入っているわけではありません。 3*sxは18であり、4の倍数ではないため、4の倍数になるようにダミーのバイトが2バイト入っています。 この例の場合、pBit+18とpBit+19がダミーです。 従って、点(0,4)の色はpBit+20、pBit+21、pBit+22に入っています。


うまくスクロールされなかった原因が分かったので、うまくスクロールされるように修正しましょう。 まず、3*sxの値が4の倍数かどうかを調べます。 下のプログラムでは、3*sxの値(eaxの値)と3との論理和が0かどうかを調べる事で、3*sxの値が4の倍数かどうかを調べています。 論理和を求めるにはand命令を使っても良いのですが、この例のように特定のビットが0か1かを調べる目的で論理和を求める場合には、and命令ではなくtest命令を使います。 and命令では、第一オペランドと第二オペランドの論理和が計算され、その値が第一オペランドに代入されます。 それに対してtest命令では、第一オペランドと第二オペランドの論理和が計算されますが、その値はどこにも代入されません。 論理和が0かどうかによって分岐したい時に、jz命令やjnz命令と共に用います。

4の倍数かどうかを調べた結果4の倍数だと分かったら、ダミーのバイト数を加えます。 下のプログラムでは、下位2ビットを0にしてから4を足しています。 なぜ下位2ビットを0にしてから4を足すことでダミーのバイト数を加えた事になるかは、自分で考えてみてください。 2進数について良く理解していないと難しいと思いますが、2進数について良く理解していれば分かると思います。

修正前
                mov     eax,3
                mul     bm.sx
                mov     ebx,eax         ;ebx=1行のバイト数
修正後
                mov     eax,3
                mul     bm.sx
                test    eax,3
                jz      @f
                and     eax,0ffffffch
                add     eax,4
        @@:     mov     ebx,eax         ;ebx=1行のバイト数

ところで、一体なぜこんなややこしい構造になっているのでしょうか? なぜ1行のバイト数が4の倍数になるようにわざわざダミーのバイトが加えられているのでしょうか? それには、2つの理由が考えられます。

一つは、movsbを使う変わりにmovsdを使う事ができるという事です。 movsbを4回実行しなくても、movsdを1回実行するだけで4バイト移動する事ができるので、移動の回数を減らす事ができます。 移動の回数を減らす事ができるため、若干高速化する事ができます。

もう一つは、4バイト境界をまたぐ事が避けられるという事です。 せっかくmovsdを使って4バイト単位で移動しても、movsdを実行するたびに4バイト境界をまたいでいては、それだけ速度が低下してしまいます。 そこで、1行のバイト数が4の倍数になるようにすることで、4バイト境界をまたぐのを防いでいると考えられます。

今述べた二つの理由は、あくまでも僕の推測にすぎません。 従って、必ずしもこの二つの理由が本当の理由であるという保証はありませんのでご了承ください。 また、「4バイト境界」という聞き慣れない言葉が出てきましたが、これについてはAce.K氏のHPにある「偽マシン語講座」を参照してください。

さて、今述べた推測が正しければ、movsbを使う変わりにmovsdを使う事で高速化する事ができますね。 そこで、movsbの代わりにmovsdを使うようにプログラムを修正してみましょう。 ここでshrという命令を使っていますが、これは第一オペランドの値を右にシフトする命令です。 何ビットシフトするかは、第二オペランドで指定します。

movsbの代わりにmovsdを使う事でどれだけ高速化したか、実行して比較してみました。 殆ど差はありませんでしたが、repeat命令のループ回数を増やしてみると、ほんの少しだけ高速化したようです。

修正前
mov     eax,bm.sy
mul     ebx
mov     edi,bm.pBit
add     edi,eax
dec     edi             ;edi=pBit+sy*ebx-1(移動先のアドレス)

mov     esi,bm.pBit
add     esi,ecx
dec     esi             ;esi=pBit+ecx-1(移動元のアドレス)

pushf                   ;移動
std
rep     movsb
popf
修正後
mov     eax,bm.sy
mul     ebx
mov     edi,bm.pBit
add     edi,eax
sub     edi,4           ;edi=pBit+sy*ebx-4(移動先のアドレス)

mov     esi,bm.pBit
add     esi,ecx
sub     esi,4           ;esi=pBit+ecx-4(移動元のアドレス)

pushf                   ;移動
std
shr     ecx,2
rep     movsd
popf