C-BIOSオンリーでPSET処理を実装
久しぶりの投稿です。暑いですね・・。
気温も暑けりゃMSX界隈も熱いです。
さて、今回はPSET実装してみました。
男気あふれるC-BIOSオンリーでの点描です。
まずはMSXのGRAPHIC2モードでどのように描画が行われているのかについて再確認から。
GRAPHIC2モードでの点描処理
GRAPHIC2モードでは画面が上段中段下段の3つに分かれていることはご理解いただけてるかと思います。
GRAPHIC2モードでの点描は上段中段下段に表示しているキャラクタパターンのドットをONにすることで点描を表現しています。
下の図ではキャラクターがある状態でわざと表していますが、上段中段下段にそれぞれ文字が256個埋まっていまして、それぞれのキャラクタパターンをいったんまっさらにして特定のキャラクタの特定のビットだけを1にすることで点の描画を行なっている。そんな感じです。
これを応用して点を描画します。
「ふぅーーん???うううーーーんん??」ってなりますね。
文章だけで説明するとわかりづらいので図でまとめてみました。
まあコードでも眺めてみようじゃないですか
ということでさっそくサンプルコードをゲットだぜ!
https://github.com/sailorman-msx/games/tree/main/src/sample025
今回のサンプルコードでは仮想VRAMを使っています。
キャラクターパターンの上段中段下段のデータをまるごとC100HからD8FFHまで保持していて、点を書くときはそのデータを書き換えています。
DotPlotサブルーチンがその部分です。
;--------------------------------------------
; SUB-ROUTINE: DotPlot
; ドットをX、Y座標にプロットする
; X座標: WK_PLOT_X
; Y座標: WK_PLOT_Y
;--------------------------------------------
DotPlot:
push af
push bc
push de
push hl
; X座標を8で割り、商をBレジスタにセットする
ld a, (WK_PLOT_X)
srl a ; / 2
srl a ; / 4
srl a ; / 8
ld b, a
; Y座標を8で割り、商をeレジスタにセットする
ld a, (WK_PLOT_Y)
srl a ; / 2
srl a ; / 4
srl a ; / 8
ld e, a
; WK_PLOT_X - B*8
sla b ; B = B * 2
sla b ; B = B * 4
sla b ; B = B * 8
; B*8の値をLレジスタにセットする
ld l, b
ld a, (WK_PLOT_X)
sub b ; A = WK_PLOT_X - B*8
sub 8 ; A = ABS(A - 8)
neg
ld d, a ; Dはプロット対象ビット番号
;
; パターンジェネレータテーブルの先頭アドレス
; をBCレジスタにセットする
; Eレジスタ=Y座標/8の値
; Lレジスタ=X座標*8の値
;
ld b, e ; B=E*256
; C = WK_POS_Y - E*8
sla e ; E = E * 2
sla e ; E = E * 4
sla e ; E = E * 8
ld c, e
ld a, (WK_PLOT_Y)
sub c
ld c, a
add hl, bc
ld b, h
ld c, l
push bc
; 7回右にビットシフトさせながら
; 対象ビットを特定する
ld b, 7
ld e, 10000000B
SetBitLoop:
ld a, d
cp b
jr z, SetBitLoopEnd
srl e ; Eを1ビット右にシフト
djnz SetBitLoop
SetBitLoopEnd:
pop bc
; Eレジスタにドットパターンがセットされている
; 仮想パターンジェネレータテーブルの先頭アドレスに
; BCCレジスタの値を加算したアドレスの
; キャラクタパターンを取得する
ld hl, WK_VIRT_VRAM_UP
add hl, bc
ld a, (hl)
; 読み込んだキャラクタパターンの値に
; 今回プロットする情報をORする
or e
ld (hl), a
; 再描画フラグをONにする
ld a, 1
ld (WK_REDRAW_FINE), a
pop hl
pop de
pop bc
pop af
ret
このサブルーチンではWK_PLOT_Xという変数にX座標(0-255)が、WK_PLOT_Yという変数にY座標(0-191)がそれぞれセットされた状態で呼び出されると、最初にその座標から算出したパターンジェネレータテーブルのアドレスを特定します。その後、その座標に点を描画するわけですがすでに点が描画されていることも考えられるため、ORでデータを作成したうえでデータをそのアドレスに書き込んでいます。
H.TIMIのタイミングでどかっとVRAM転送
つどつどVRAMに直接書き込むと遅くなったりテアリング懸念があったりなんたり面倒なので、今回のサンプルではH.TIMIのタイミングで仮想VRAMのパターンジェネレータテーブルのデータを1024バイトずつどかどかとVRAMに転送しています。
その後、画面を再描画しなおしています(=パターンネームテーブルの上段中段下段にそれぞれ文字を表示しなおしている)
; 仮想VRAMの情報を反映させる
ld a, (WK_REDRAW_FINE)
cp 1
jr c, ApplyVirtVRAMEnd
; パターンジェネレータテーブルを転写する
; 仮想VRAMは6KBもある
; VBLANKのタイミングで6KBの転写はリスクありすぎるので
; 1KBずつの転写にする
; パターンジェネレータテーブルの転写
ld hl, (WK_CHRPTN_VRAM_ADR)
ld de, hl
ld hl, (WK_VIRT_VRAM_ADR)
ld bc, 1024
call WRTVRMSERIAL
; 仮想VRAMの最後まで転写したら
; いちばん最初の転写に戻す
or a ; CY reset
ld bc, $D500
ld hl, (WK_VIRT_VRAM_ADR)
sbc hl, bc
jr nz, ApplyVirtVRAMSetNext
; VRAM転送元アドレスとVRAM転送先アドレスを初期化する
ld hl, WK_VIRT_VRAM_UP
ld (WK_VIRT_VRAM_ADR), hl
xor a
ld (WK_CHRPTN_VRAM_ADR), a
ld (WK_CHRPTN_VRAM_ADR+1), a
jr ExecRedraw
ApplyVirtVRAMSetNext:
; 次に転写する仮想VRAMのアドレスをセットする
ld bc, 1024
ld hl, (WK_VIRT_VRAM_ADR)
add hl, bc
ld (WK_VIRT_VRAM_ADR), hl
ld hl, (WK_CHRPTN_VRAM_ADR)
add hl, bc
ld (WK_CHRPTN_VRAM_ADR), hl
ExecRedraw:
; 画面全体を再描画する
call RedrawScreen
さすがにVRAM転送は遅いので6KB全部をこのタイミングで収めるのは不可能。でも1KBくらいならだいじょうぶなので、そのくらいの単位でどかどかVRAMにデータを放り投げています。1KBずつの転送なので1画面分を転送するのに必要な時間は6/60秒ですね。10ぶんの1秒。まああっという間ではあります。
今回のサンプルでは乱数でXY座標を特定して点を描画しているのですが、8ビット乱数だといまいち分布が固定されてしまうので、今回は16ビット関数にチャレンジしてみましたよ。
ちなみにどっかのサイトから拾ってきたコードなので何してるのかは筆者にもよくわかっていません(汗)
;--------------------------------------------
; SUB-ROUTINE: RandomValue
; 乱数を取得する
; 16ビットの乱数を取得して、その値の上位8ビットを取得する
; WK_RANDOM_VALUEには16ビットの乱数をセットして返却する
;--------------------------------------------
InitRandom:
xor a
ld (WK_RANDOM_VALUE+1), a ; 乱数のシード値を設定
ld (WK_RANDOM_VALUE), a
ret
RandomValue:
push bc
push de
push hl
ld de, (WK_RANDOM_VALUE) ; 乱数のシード値を乱数ワークエリアから取得
ld a, d
ld h, e
ld l, 253
or a
sbc hl, de
sbc a, 0
sbc hl, de
ld d, 0
sbc a, d
ld e, a
sbc hl, de
jr nc, RandomValueEnd
inc hl
RandomValueEnd:
ld (WK_RANDOM_VALUE), hl
ld a, h ; 乱数の上位8ビットを乱数として採用する
pop hl
pop de
pop bc
ret
ただ、0-255の範囲での値しか得られない乱数よりも0-65535までの範囲での値が拾えるほうが分布はより乱数っぽくなります。
今回は生成された乱数の下位8ビットの値を採用しています。
この乱数生成器、けっこういい感じなのでこれからも使っていこうと思います。
union(合同)という考え方
MSXの場合、RAMには限りがあります。
そこで登場する考え方が「Union(合同)」です。
例えばC000H-C100Hまでをとある変数のエリアにしてしまったけれど、そのアドレスを違う名前で使いたい。この考え方がunionです。
今回のサンプルコードでは以下のように変数を定義してある箇所があります。
; C100H - D8FFH まではパターンテーブルの仮想領域
DEFVARS $C100
{
WK_VIRT_VRAM_UP ds.b 256 * 8
WK_VIRT_VRAM_MD ds.b 256 * 8
WK_VIRT_VRAM_DW ds.b 256 * 8
}
; C100H - C8FFHまでは共用エリアとする
DEFVARS $C100
{
WK_UNIONRAM ds.b $800
}
DEFVARS $1C00 の中でWK_VIRT_VRAM_UPという変数で256*8バイト(800Hバイト)使う。と宣言していますが
同じアドレスを WK_UNIONRAM という変数でも使用しています。
これ何が便利かっていうと、いったん使ってもうその目的では使わないっていうのが特定できているならその名前は捨てて他の名前で使ってしまおう!ということを意味しています。
最初はとっつきにくい考え方かもしれませんが、マシン語プログラミングに慣れてくると「メモリが足りない!」という地獄にまず落ちます。そのあとで「このアドレス、この瞬間にしか使わないじゃん!後続処理では全然使わないじゃん!」っていうのが見えてくるようになります。その「全然使わない」なら「別名で使ってしまえばいい」という発想の転換。です。
今回のサンプルではC100Hを最初、WK_UNIONRAMという変数で使っています。
; VRAMのキャラクタパターン上中下の情報を作成する
;----------------------------------------
; パターンジェネレータテーブルの初期化
;----------------------------------------
; パターンジェネレータテーブルの
; 上段の情報をVRAMからRAMにコピーする
; 転送サイズは0800Hバイト
ld hl, WK_UNIONRAM
ld de, $0000
ld bc, $0800
call REDVRMSERIAL
; パターンジェネレータテーブルの
; 上段の情報をVRAM(中段)にコピーする
; 転送サイズは0800Hバイト
ld hl, WK_UNIONRAM
ld de, $0800
ld bc, $0800
call WRTVRMSERIAL
; パターンジェネレータテーブルの
; 上段の情報をVRAM(下段)にコピーする
; 転送サイズは0800Hバイト
ld hl, WK_UNIONRAM
ld de, $1000
ld bc, $0800
call WRTVRMSERIAL
これはVRAMのパターンジェネレータテーブルの上段の内容を中段下段それぞれのパターンジェネレータテーブルにコピーするために使ってるだけで、コピーが終わったあとは他の目的で使える。他の目的で使うんなら名前も変えちゃえ!っていう感じで使っているわけです。
ついてこれますか・・?(汗)
ちなみに今回のサンプルではわざわざコピーしていますが、必要のない処理です。。。このUNIONという考え方を紹介したかっただけです。ハイ。
速度の違いを体感せよ!!
今回のサンプルではSAMPL025.BASというファイルで同じ処理をBASICで記述したものも含まれています。マシン語でのPSET処理の速さとBASICでの処理の速さを体感してみてください。
今回はこれまで!
点が描けるのであれば線も描けますし、塗りつぶすことだってできるようになります。夢は広がりますね、ワイヤフレームのゲームとか作れそうな気がしませんか?(筆者は数学が苦手)
それでは、また!!ノシ