見出し画像

VDPを直接操作してみる 処理説明

謝辞:今回の処理についてはHRA!さんに多大なアドバイスをもらいました。感謝感謝でございます。今回の記事は処理説明ではありますが、ほぼ、HRA!さんの説明ママになっています。

出典:https://github.com/hra1129

前回記事についての処理説明です。
https://github.com/sailorman-msx/games/tree/main/src/sample016
スプライトが消えないようにするためには、高速にスプライトアトリビュートを入れ替えてスプライト番号をシャッフルする必要があります。
スプライト番号はVRAMの1B00Hから4バイトずつでスプライト番号#0-スプライト番号#31までの32枚があります。以下のような感じですね。

並び替えに必要な変数を用意する

素数を使った並びかえですが、これは誰が考えたのか、とてもアタマが良い方法だと思います。かなり綺麗にシャッフルされるし、一定回数で元の状態に循環します。並べ替えの方法は以下のとおりです。
・スプライト32枚分の仮想アトリビュートを格納するワークテーブル(WK_VIRT_SPR_ATTR_TBL)を用意する。形はVRAMの1B00Hと同じとする。


・並び替え用のアトリビュートテーブルを用意する(今回のサンプルではVRAMの1C00H以降をそのテーブルとして使用)
・VRAM:1B00Hに並び替え用アトリビュートワークテーブルを転送するためのワークテーブル(WK_DISP_SPR_ATTR_TBL)を用意する
・並べ替えの基準となる数値を決める変数(WK_SPRITE0_NUM)を用意する。この変数には最初は0をセットしておく。

並べ替え処理概要

1. 並べ替えの基準となる数値にWK_SPRITE0_NUMの値をセットする。
2. 並べ替えの基準となる数値に素数(例えば7)を加算して、31でAND演算を行う。(31でAND演算するのは32以上にしないため)
3. 演算結果を4倍した数値に1C00Hを足したVRAMアドレスに仮想アトリビュートテーブルの4バイトぶんを転送する
4. 32枚分、上記2から3を繰り返す。(これでVRAMの1C00Hにシャッフルされた32枚分のスプライトアトリビュートテーブルが出来上がる)
5. WK_SPRITE0_NUMに2とは異なる素数(例えば19)を加算して、31でAND演算を行う。
6. H.TIMIのタイミングでVRAMの1C00Hから128バイトぶんをVRAMの1B00Hに転送する。

「うーーん??」と私も思いましたがこれ表にしてみたので、見てみましょう。

S#と書いてある列が1/60秒ごとにシャッフルする単位です。AT#0にはWK_SPRITE0_NUMの値に素数7を足して、31でAND演算した数値になっています。これを繰り返すと32回目(AT#31の次)でWK_SPRITE0_NUMの値に戻ります。
処理では32回繰り返したあとでWK_SPRITE0_NUMの値に素数19を加算し、31でAND演算を行なっています。こうすることで次回のシャッフル時には前とは違った値でシャッフルが行われるようになります。
表をみるとAT#0-AT#31では重複する数字が存在しないこと、S#31の次でWK_SPRITE0_NUMが初期値の0に戻っていることに着目してください。

ソースコードも見てくださいね

ソースコードではsprite.asmで以下のようなサブルーチンで並べ替え処理を行なっています。

;--------------------------------------------
; SUB-ROUTINE: ShuffleSprite
; ワーク用スプライトアトリビュートテーブルの内容を
; 素数を使った形式(KONAMI方式)でシャッフルし
; その後、VDPポートを直接叩いて
; アトリビュートテーブルの内容をVRAMに転送する。
;--------------------------------------------
ShuffleSprite:

    ; VRAM書込み事前準備
    ; VDP書き込み用ポートに書き込みたいVRAMアドレスを
    ; セットする

    ld a, (WK_VDPPORT1)
    ld c, a
    inc c         ; 0007Hの値に1を加算するとWRITEモードのポート番号になる

    ; 直接、スプライトアトリビュートのVRAMアドレスを
    ; 書き換えるとテアリングが発生する可能性があるため
    ; 1C00Hにシャッフルした仮想アトリビュートテーブルを
    ; セットする
    ; シャッフルが終わったら1C00Hから128バイトぶんを
    ; 1B00Hに書き込む

    ld hl, $1C00
    ld a, l
    out (c), a    ; 12ステート
    nop           ;  4ステート
    nop           ;  4ステート
    nop           ;  4ステート

    ld a, h
    and $3F
    or $40
    out (c), a    ; 12ステート
    nop           ;  4ステート
    nop           ;  4ステート
    nop           ;  4ステート

    ld b, 32

    ; VRAM書込み事前準備
    ; HLレジスタにVDPに転送する先頭アドレスを
    ; セットする

    ld a, (WK_VDPPORT1)
    ld c, a
    ld h, WK_VIRT_SPR_ATTR_TBL >> 8

    ld a, (WK_SPRITE0_NUM)

ShuffleSpriteLoop:

    ld l, a

    ; HLレジスタの指すメモリの4バイトぶんの内容を
    ; 事前準備でセットしたVRAMアドレスに対して書き込む
    ; OUTIを実行するとBレジスタがデクリメントされるので
    ; OUTIのつどBレジスタをインクリメント補正する
    ; VRAMへの書き換えを行ったら最低21ステート間隔を
    ; あける必要があるらしい

    outi  ; 16ステート
    inc b ;  4ステート
    nop   ;  4ステート

    outi
    inc b
    nop

    outi
    inc b
    nop

    outi
    inc b
    nop

    add a, 28   ; A = A + 7 * 4

    ; Aレジスタの値はスプライト番号*7のアドレス
    ; スプライトの最大数は32なので32*4-1で
    ; 128以上にならないようマスクする
    and a, 7FH

    ; 32回分ループする
    djnz ShuffleSpriteLoop

    ; 次のシャッフル値の基準値(アドレス)を決める

    add a, 76   ; A = A + 19 * 4
    and a, 7FH  ; Aレジスタの値が128以上にならないようマスク

    ; 次のシャッフルの基準値を変更する
    ld (WK_SPRITE0_NUM), a

    ret

H.TIMIでは、PutSpriteサブルーチンを呼び出しています。PutSprite内部ではVRAMの1C00Hの内容をVDPを使って128バイト読み込んで、メモリ内部のWK_DISP_SPR_ATTR_TBLに格納し、さらにそのWK_DISP_SPR_ATTR_TBLの内容をVDPを使って、VRAMの1B00Hに転送しています。

interval.asm 抜粋

; ====================================================================================================
; H.TIMI割り込み処理
; ====================================================================================================
H_TIMI_HANDLER:

    ;---------------------------------------------------
    ; H.TIMI割込みが発生した=V-BLANKING期間開始となる
    ; 画面書き換え時にVRAMを書き換えるとテアリングが発生
    ; するためV-BLANKING期間の開始時点で
    ; スプライトアトリビュートテーブルを書き換える
    ; 当処理ではシャッフルされた仮想スプライトアトリビュート
    ; テーブルの
    ; VRAM 1C00Hから128バイトぶんを
    ; VRAM 1B00Hに転送する
    ;---------------------------------------------------
    call PutSprite
 
sprite.asm 抜粋
 
;--------------------------------------------
; SUB-ROUTINE: PutSprite
; VRAM 1C00Hから128バイト分を
; VRAM 1B00Hに転送する
;--------------------------------------------
PutSprite:

    ; VRAMの1C00Hから128バイトぶんを
    ; データ表示用のワークテーブルに格納する
    ld a, (WK_VDPPORT1)
    ld c, a
    inc c
    ld hl, $1C00
    ld a, l
    out (c), a
    nop
    nop
    nop
    ld a, h
    out (c), a
    nop
    nop
    nop

    ld a, (WK_VDPPORT0)
    ld c, a
    ld b, 128
    ld hl, WK_DISP_SPR_ATTR_TBL
    inir
    nop
    nop

    ld a, (WK_VDPPORT1)
    ld c, a
    inc c
    ld hl, $1B00
    ld a, l
    out (c), a
    nop
    nop
    nop
    ld a, h
    and $3F
    or $40
    out (c), a
    nop
    nop
    nop

    ld a, (WK_VDPPORT0)
    ld c, a
    ld b, 128
    ld hl, WK_DISP_SPR_ATTR_TBL
    otir
    nop
    nop

    ret

INIR命令はVDPポートからデータを入力する命令です。Bレジスタに格納されている数ぶん、VDPからデータを読み込み、HLレジスタに格納されたメモリアドレスに転送を行う命令になっています。

そんなに並び替えても大丈夫??

「消える」という仕様はあくまでも横に5枚以上並んだ場合のみに発生する仕様です。スプライトの順番を並び替えたからといって、縦に並ぶぶんには消えることはありません。今回の並び替え処理は「もし横に5枚以上ならんだらどうやって並び替えよう?」という問題に対するソリューションです。

また、この並び替え処理をサブルーチン化しておけば、プログラムするときは並び替えのことを考えなくて良くなります。今回の例でいえばWK_VIRT_SPR_ATTR_TBLを操作するだけですみます。
あとは、常にスプライト番号を並び替えた状態で常に表示が行われるようになります。
こういうのは大事です。
余計なことをプログラミングで並行して考えるのはバグのもとです。

次回以降

今回のサンプルでVDPを直接操作することができるようになったかと思います。次回以降はVRAM操作はBIOSを介さず、VDPを直接操作する自作サブルーチンを作って、それを使うようにしようと思います。
ゲームのアイデアが浮かばないですが・・(汗)

・・。
では、また!!ノシ

セーラー服が似合うおじさんです。猫好き、酒好き、ガジェット好き、楽しいことならなんでも好き。そんな「好き」をつらつらと書き留めていきます。