見出し画像

C-BIOSオンリーでPSET処理を実装

久しぶりの投稿です。暑いですね・・。
気温も暑けりゃMSX界隈も熱いです。
さて、今回はPSET実装してみました。
男気あふれるC-BIOSオンリーでの点描です。
まずはMSXのGRAPHIC2モードでどのように描画が行われているのかについて再確認から。

GRAPHIC2モードでの点描処理

GRAPHIC2モードでは画面が上段中段下段の3つに分かれていることはご理解いただけてるかと思います。
GRAPHIC2モードでの点描は上段中段下段に表示しているキャラクタパターンのドットをONにすることで点描を表現しています。
下の図ではキャラクターがある状態でわざと表していますが、上段中段下段にそれぞれ文字が256個埋まっていまして、それぞれのキャラクタパターンをいったんまっさらにして特定のキャラクタの特定のビットだけを1にすることで点の描画を行なっている。そんな感じです。

上段、中段、下段にみっちりと文字が256個ずつ並んでる

これを応用して点を描画します。
「ふぅーーん???うううーーーんん??」ってなりますね。
文章だけで説明するとわかりづらいので図でまとめてみました。

まあコードでも眺めてみようじゃないですか

ということでさっそくサンプルコードをゲットだぜ!
https://github.com/sailorman-msx/games/tree/main/src/sample025

今回のサンプルコードでは仮想VRAMを使っています。
キャラクターパターンの上段中段下段のデータをまるごとC100HからD8FFHまで保持していて、点を書くときはそのデータを書き換えています。
DotPlotサブルーチンがその部分です。

;--------------------------------------------
; SUB-ROUTINE: DotPlot
; ドットをXY座標にプロットする
; 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 ; E1ビット右にシフト
    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での処理の速さを体感してみてください。

高速に表示される満天の星の数々・・・

今回はこれまで!

点が描けるのであれば線も描けますし、塗りつぶすことだってできるようになります。夢は広がりますね、ワイヤフレームのゲームとか作れそうな気がしませんか?(筆者は数学が苦手)

それでは、また!!ノシ

2024.09.14追記
今回のサンプルでは点描をとにかく高速化させたいために、仮想VRAMを上段中段下段でRAMに保持して処理していますが、これ6144バイトもあります。また、今回は文字色は白だけです。今回は速度を優先してカラーはなしにしました。
なぜなら、カラーパターンテーブルも6144バイトあるためです。そのため、カラーパターンテーブルも仮想VRAMにしてしまうとRAMが完全に足りない状態になるので工夫が必要になります。
次回の記事では、カラフルな星空にチャレンジしようと思います。

いいなと思ったら応援しよう!

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