PC-98のプログラム作成に関するエトセトラ - VSYNCをフックする

概要

約1/60秒間隔で発生するVSYNC割り込みは、簡易タイマーや画面表示などで使われることもあります。
常駐ソフトの類では頻出ですが、今回はソフト内の割り込み処理ルーチンでのマウス表示をやってみます。
あえて切り分けない方が、本体から直接制御しやすく、また切り取って常駐させるルーチンも不要なので楽かなと。

最低限必要な要素の流れ

・初期化
cli
古い割り込みアドレスを保存(csに。offset segmestの順で)
新しい割り込みアドレスを設定(同様)
stl

・割り込みマスクレジスタをVSYNC許可に
in al,02h
(必要ならalを保存)
and al,0fbh
out 02h,al

・割り込みを発生させる
out 64h,al
で任意のalを出力すると、次に1回発生。
必要なら割り込みの最後で発生を予約すると良いか。


・割り込み処理ルーチン
pushf
使うレジスタをpushで退避、又はpusha
処理
popで戻す
jmp cs:dword ptr [アドレス]

この項はトラ技コンピュータ1990/5 MS-DOSと割り込み を参考しました。

その他

csに変数領域を確保してやれば、割り込みのon-offなどのモード指定が簡単に出来る、と思う。

マウスを表示してみた例

マクロ定義などを抜いてますので、このままだと動きません。要望があれば完全なソースを上げますが、EGCの画像表示サンプルを見ればいいので必要ない筈です。

初期化とData Segment

.model small
.286;面倒なのでshl reg,nを使うのと、EGCを使うので286以上に指定。shlをcl挟むか並べればコード上は8086用でもOK
INCLUDE <04G.h>;グラフィック関係のマクロを入れたり、Libraryを呼び出すのに使用。内容は画像表示のサンプルに近いので略



.stack

.data
;マウス画像(クリアで0にする部分32byteと色番号1にする部分32byteの構成)
GRA db 7FH,0FFH,3FH,0FFH,1FH,0FFH,0FH,0FFH,07H,0FFH,03H,0FFH,01H,0FFH,00H,0FFH,
07H,0FFH,03H,0FFH,43H,0FFH,0E1H,0FFH,0E1H,0FFH,0F0H,0FFH,0F0H,0FFH,0F8H,0FFH,
00H,00H,00H,00H,40H,00H,60H,00H,70H,00H,78H,00H,7CH,00H,70H,00H,
70H,00H,18H,00H,18H,00H,0CH,00H,0CH,00H,06H,00H,06H,00H,00H,00H

SYNC_RSET EQU 64h;VSYNCのリセット(再描画)

SYNC_OFS EQU 28h;割り込みのoffset
SYNC_SEG EQU 2Ah;割り込みのSEGMENT

IO_MOTO db 0;IOの元のデータ

PLpass db 'chara1.pal',0;読み込むファイル名
PLDATA db 32 dup(0EEh);4bitずつでダミー+brg

マクロとPROCの定義

.code
;VSYNCを待つマクロ。詳しくはスーパーテクニックを参照
V_WAIT MACRO
LOCAL V_lp,V_NOT,V_GO
V_lp:jmp $+2
jmp $+2
in al,0A0h
test al,020h
jnz V_lp
;VSYNCがオフだったら開始まで以下で待つ
pushf
V_NOT: cli
in al,0A0h
test al,20h
jnz V_GO
popf
pushf
jmp V_NOT
V_GO:
popf
sti
ENDM

_MO_MES1 PROC C ,POS:word,Fl:word;マウス画像表示のサブルーチン
OUT dx,ax
mov di,POS
MOV cx,16;ここで縦
SMLP: pushcx
push di
mov cx,1
add cx,Fl
rep MOVSW
pop di
pop cx
add di,80
cmp Fl,0
je @f
sub si,2
@@: LOOP SMLP
ROL ax,1
RET
_MO_MES1 ENDP

;マウス画像の表示本体。まえのヤツと一緒
_MO_MES PROC C ,D_Ofs:ptr byte,MX1:word,MY1:word
local Flag:word
mov Flag,0
OUT2 4A2H,0FFH
OUT2 4A4H,0CC0H;抜きアリ
OUT2 4A8H,0FFFFH
OUT2 4AEH,7;までマウス透過の初期化

mov si,0
mov bx,MX1
mov ax,bx
and ax,15
cmp ax,9
jb @f
mov Flag,1
@@: SHL ax,4
mov dx,04ACH
OUT dx,ax;シフト量を指定


SHR bx,4
shl bx,1
add si,bx
mov bx,MY1
SHL bx,4
add si,bx
SHL bx,2
add bx,si;bx=先頭byte

MOV SI,D_Ofs
MOV AX,0FFF0H
MOV DX,4A0H
INVOKE _MO_MES1,bx,Flag;パレット0(=黒に)
OUT2 4A4H,0CFCH

MOV AX,0FFFEH
MOV DX,4A0H
INVOKE _MO_MES1,bx,Flag;パレット1(=白に)
;他の色は使わないので、ここまでがマウスの描画
RET
_MO_MES ENDP

_MO_MES2 PROC ;こっちはOLD用サブルーチン
;si,diは指定済みとする
MOV cx,16;ここで縦
SMLP: push cx
push di
mov cx,2
rep MOVSW
pop di
pop cx
add di,80
LOOP SMLP
RET
_MO_MES2 ENDP

;1F前の部分を復帰
;幅2wordコピーすれば1word幅までの画像は100%その範囲内なのでシフト無しでOK
;EGCを使わない場合は今回のマウス画像は8bit幅なので1word分でOK
_OLD_MES PROC C ,MX1:word,MY1:word
OUT2 04A2H,000FFH
OUT2 04A4H,028F0h;ここでマスクとかの設定
OUT2 0A8H,0FFFFH
OUT2 04ACH,0
OUT2 04A0H,0FFF0h;全プレーン制御
OUT2 04AEh,31

mov si,0
mov di,MX1

SHR di,4
shl di,1
add si,di
mov di,MY1
SHL di,4
add si,di
SHL di,2
add di,si;di=先頭byte

mov si,7FC0h;VRAMの余りの一番最後の4byte×縦16=64byte分を使用、だからコレで良いはず・・・。
push ds;ds(@DATA)をes(VRAM開始アドレス)に変更
push es
pop ds
call _MO_MES2
pop ds
ret
_OLD_MES ENDP


_MO_MES3 PROC ;こっちはバックコピー用サブルーチン
;si,diは指定済みとする
MOV cx,16;ここで縦
SMLP: push cx
push si
mov cx,2
rep MOVSW
pop si
pop cx
add si,80
LOOP SMLP
RET
_MO_MES3 ENDP

;書き込み前のVRAMを退避
;siとdiが逆なだけで基本は上と一緒
_BCK_CPY PROC C ,MX1:word,MY1:word
OUT2 04A2H,000FFH
OUT2 04A4H,028F0h;ここでマスクとかの設定
OUT2 04A8H,0FFFFH
OUT2 04ACH,0
OUT2 04A0H,0FFF0h;全プレーン制御
OUT2 04AEh,31

mov si,0
mov bx,MX1

SHR bx,4
shl bx,1
add si,bx
mov bx,MY1
SHL bx,4
add si,bx
SHL bx,2
add si,bx;si=先頭byte

mov di,7FC0H;di;コピー先


push ds
push es
pop ds
call _MO_MES3
pop ds
ret
_BCK_CPY ENDP

メインルーチン

;メインルーチン

.startup
;初期化
CALL _USE_ALL
call _CLR_ALL;テキスト関係を消去してるだけ
call _CUR_NOT

CLD
CALL _GINIT;16色初期化
CALL _EGC_ON;EGCを使用


FLD_DS <offset PLpass>,32,<offset PLDATA>;パレットをメモリにロード


mov cx,15
@@: PALSET <offset PLDATA>,cl,cx;パレットに設定
loop @b

mov ax,0A800H
mov es,ax

BACK 3;背景をパレット3で塗りつぶすマクロ

;ここまでは本質には関わらないですし、以前のサンプルを改造したり出来るので詳しくは述べません。

push es
;割り込み初期化開始
push ds
xor ax,ax
mov ds,ax;割り込み指定用にdsを0に
pop es;es=@data
CLI
mov ax,ds:[SYNC_OFS];割り込み先の退避
mov cs:[SYNC_M_OFS],ax
mov ax,ds:[SYNC_SEG]
mov cs:[SYNC_M_SEG],ax

mov ax,offset MOU_CH;割り込みの書き換え
mov ds:[SYNC_OFS],ax
mov ax,cs
mov ds:[SYNC_SEG],ax
STI
push es
pop ds
pop es;セグメントを元に戻しただけ

mov ax,cs:[OLD_X]
mov bx,cs:[OLD_Y]
invoke _BCK_CPY,ax,bx;マウス描画が始まる前に画像をコピーしておく


in al,02h;割り込みの開始
mov IO_MOTO,al
and al,0FBh
out 02h,al

MLOOP:
V_WAIT;VSYNCを待つ
;マウス座標に表示とか
mov cs:[mode],0;座標更新停止
mov ax,cs:[OLD_X]
mov bx,cs:[OLD_Y]
invoke _OLD_MES,ax,bx
mov ax,cs:[MOU_X]
mov bx,cs:[MOU_Y]
push ax
push bx
invoke _BCK_CPY,ax,bx
pop bx
pop ax
invoke _MO_MES,ADDR GRA,ax,bx
mov cs:[mode],1;座標更新再開

mov AX,400h;ESCを押すと終了
int 18h
test AH,1
jne ENDING
jmp MLOOP;ここまでメインループ

ENDING:V_WAIT;終了処理
CALL _EGC_OFF
mov al,IO_MOTO;以下復帰処理
out 02h,al
xor ax,ax
mov ds,ax
CLI
mov ax,cs:[SYNC_M_OFS]
mov ds:[SYNC_OFS],ax
mov ax,cs:[SYNC_M_SEG]
mov ds:[SYNC_SEG],ax
STI

mov ah,4Ch;終了
int 21h

割り込み処理とデータ部

MOU_CH:pusha;割り込みルーチン
pushf
mov cl,cs:[mode]
cmp cl,0
je MOU_EN
cmp cl,2
je @f
mov ax,cs:[MOU_X]
mov cs:[OLD_X],ax
mov ax,cs:[MOU_Y]
mov cs:[OLD_Y],ax


@@: OUT2 7FDDh,80h;X下位4bitを入手&バッファ固定
mov dx,7FD9h
in ax,dx
and ax,0Fh;不要な情報をカット
mov bx,ax

OUT2 7FDDh,0A0h;X上位4bitを入手
mov dx,7FD9h
in ax,dx
and ax,0Fh;不要な情報をカット

SHL ax,4;シフトして足し合わせ
add al,bl;al=カウンタ

cbw ;alを符号付きでaxに
add cs:MOU_X,ax
test cs:MOU_X,8000h
jz @f
mov cs:MOU_X,0

@@: cmp word ptr cs:MOU_X,632
jb @f
mov cs:MOU_X,631;以上X

@@: OUT2 7FDDh,0C0h;Y下位4bitを入手
mov dx,7FD9h
in ax,dx
and ax,0Fh;不要な情報をカット
mov bx,ax

OUT2 7FDDh,0E0h;Y上位4bitを入手
mov dx,7FD9h
in ax,dx
and ax,0Fh;不要な情報をカット
SHL ax,4
add al,bl;al=カウンタ

cbw
add cs:MOU_Y,ax
test cs:MOU_Y,8000h
jz @f
mov cs:MOU_Y,0

@@:
cmp word ptr cs:MOU_Y,385
jb @f
mov word ptr cs:MOU_Y,384
@@:
OUT2 7FDDh,00h;バッファ固定解除
out SYNC_RSET,al
popf
popa
MOU_EN: jmp dword ptr SYNC_M_OFS;前の割り込みにジャンプ
iret;ジャンプするのでこの行は必要ないです
;割り込みルーチンここまで


;割り込み用データ(CS上に格納)
MOU_X dw 320
MOU_Y dw 200

OLD_X dw 320
OLD_Y dw 200

MODE byte1;0完全無効 1通常 2OLDを更新せずに運用(スリープ的な)

SYNC_M_OFS dw 0
SYNC_M_SEG dw 0
END

作成の意図と注意点

割り込み上で画像を表示させたりしないのは、メインルーチンでやった方が小細工が効くと考えたためです。
マウスドライバとして公開するでもなし、全部マシン語で書くんだから、細かくモード分けをして割り込み部分が煩雑になるより、メインの方で必要に応じて描けばいいかなと。
あと、下手にドライバ化すると、処理が重くなった場合に削るのに困る気もしたので。

VRAM間コピーでのEGCの設定が特殊です。
04A4hに028F0hを出力すると指定したプレーン全てのVRAM間コピーが可能なので重ね合わせなどにとても便利です。
ぜひ覚えておくといいでしょう。
そのうちEGCの基本事項をまとめたいですね。

画像表示系を一般化したPROCを作って呼び出すとコード容量的にかなり削減できますし、今回みたいに個別に用意すると速度上ちょっと有利になります。
お好みでどうぞ。

多重割り込みの注意。
vsyncを発生させている最中にint 21h等を使うと、多重割り込みが発生してバグったりします。
割り込みを呼び出す前に終了処理のmov al,IO_MOTO out02h,alを呼び出してvsyncを止めてください。
音楽演奏でタイマー割り込みなどを使っている場合は、それぞれの割り込み中は他の割り込みを許可しないようにcli,stiを使って下さい。

VRAMの余りを半分に切り詰める場合は、EGCの設定でコピー元をシフトしたりが、ちょっと面倒ですが、多分出来るはずです。
(コピー元のシフト周りのループ回数指定にちょっと癖があるので注意してください。)
byte幅まで切り詰めるのも可能なはずですが、コピー先までシフトしたりすることになるので速度が心配です。
この辺はマシン語ゲームグラフィックス辺りを参考にやってみてもいいかもしれません。

あと、VRAMの余り部分のアドレス指定は好みで変えてOKです。(本当にコレであってるかちょっと不安もありますし)
クリック判定は今回のメインじゃないですし、簡単なので略します。
一応IOからINした値の特定ビットが1or0で判別するだけですので、UNDOCUMENTED9801vol.2かプログラマーズバイブル辺りを見ればいいと思います。

本編で解説を入れ忘れましたが、OUT2はOUT dx,axをラップしただけのマクロです。

完全なソースが必要ならコメントでもして下さい。
気付いたらUPします。