2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PC-8001 1ドットスクロールゲーム 解説(その2)

2
Last updated at Posted at 2026-08-22

PC-8001 1ドットスクロールゲーム 解説(その2)

前回 は、PC-8001で1ドットスクロールを実現するためのマップ表示方法を書きました。

その後、VSync待ちの時間を利用して表示処理を高速化する方法を思いついたのでその方法をまとめます。

前回の記事で説明した方式を前提にしているため、マップデータやアトリビュートの基本的な構造については、必要に応じて前回の記事を参照してください。

メインループ(再掲)

以下のような順序で行っています。

  1. シフト無しマップデータをMapVram1からVramへ転送
  2. アトリビュート圧縮データをAttrVramに展開
  3. キャラクタ描画(VramとAttrVramへ書き込み)
  4. AttrVramからVramのアトリビュート領域を作成
  5. VSync待ちしてダブルバッファリングの切り替え
  6. マップ圧縮データを、各行展開して1キャラ分をW_NextMapByteDataに設置
  7. MapVram1の末尾データとW_NextMapByteDataからMapVram2の末尾データをシフトして生成
  8. シフトあるマップデータをMapVram2からVramへ転送
  9. アトリビュート圧縮データをAttrVramに展開
  10. キャラクタ描画(VramとAttrVramへ書き込み)
  11. AttrVramからVramのアトリビュート領域を作成
  12. VSync待ちしてダブルバッファリングの切り替え
  13. MapVram1の各行の末尾データをW_NextMapByteDataのデータで埋める

改善方針

上の処理の5と12にあるVSync待ちでは、処理がVSyncに入った直後に到達すると、次のVSyncまで待つことになります。最悪の場合、約1/60秒の待ち時間が発生します。
今回、この待ち時間を有効に使えないかと考えました。

表示中のVRAMを書き換えると画面に影響する可能性があります。一方、アトリビュート圧縮データを仮想エリアであるAttrVramへ展開する処理(2と9)は、まだ表示に使っていないバッファを準備する処理です。そのため、この処理ならVSync待ちの間に実行できます。

前回説明したアトリビュート圧縮データとその展開を、VSync待ちの間に先頭行から可能な限り処理します。

変更内容は次の3点です。

  1. 前回の処理を、全行を繰り返す部分と1行だけ処理する部分に分離する。
  2. VSync待ち(5と12)で1行処理を呼び出し、次のVSyncが始まるまで可能な限り行を進める。VRAMを切り替えられるようになったら、残りの処理を待たずに切り替える。
  3. 本来のAttrVram展開処理(2と9)では、VSync待ちで処理できなかった残りの行を続きから処理する。

VSync待ちでどこまで処理したかは、ATR_setAttrDataToAttrMapOne.rowDone に記録します。通常の展開処理は、この値を読み取って続きの行から再開します。

この処理では、AttrVramへの書き込みにSPを使っています。そのため、通常のcall/retは使えません。VSync待ちから呼び出す場合と、通常の展開処理から呼び出す場合のそれぞれで戻り先アドレスを設定し、retの代わりにjpで戻るようにしています。

実装

VSync待ち中の前処理

次のルーチンは、アトリビュートを1行ずつ処理しながらVSyncの状態を監視します。

  • すでにVSync中なら、VSyncが終わるまで処理を続ける。
  • VSyncが終わったら、次のVSyncが始まるまで処理を続ける。
  • VSyncが始まった時点で処理を中断し、VRAMを切り替える。
                        ; ========================
                         ; アトリビュートデータを一部処理する
                         ; VSync待ち中に呼びだしてVsyncが始まるまで処理することで
                         ; 続く setAttrDataToAttrMapの処理を軽くする
                         ; W_AttrRowsDoneで処理済み行数を管理する
8ECD                     ATR_changeVramAndPrecompute:
                                 ; 前処理カウントをリセット
8ECD AF               4          xor a
8ECE 320B8F          13          ld (ATR_setAttrDataToAttrMapOne.rowDone), a
8ED1 DD213E84        14          ld ix, NextMapAttr.workTop
                         
                                 ; SP保存、AttrVramへ向ける
8ED5 ED73FE8E        20          ld (.spOne), sp
8ED9                     .waitEnd
8ED9 DB40            11          in a, ($40)
8EDB E620             7          and $20
8EDD 280F            12          jr z, .waitStart        ; VSync中でないなら次の処理
8EDF 21E88E          10          ld hl, .ret1            ; 戻り先
8EE2 22728F          16          ld (ATR_setAttrDataToAttrMapOne.retAddr), hl
8EE5                     .inVSync
                                 ; すでにVSync中ならまずVSyncが終わるまで処理を続ける
                                 ; call ATR_setAttrDataToAttrMapOne
8EE5 C30A8F          10          jp ATR_setAttrDataToAttrMapOne
8EE8                     .ret1
8EE8 DB40            11          in a, ($40)
8EEA E620             7          and $20
8EEC 20F7            12          jr nz, .inVSync
8EEE                     .waitStart
8EEE 21F78E          10          ld hl, .ret2            ; 戻り先
8EF1 22728F          16          ld (ATR_setAttrDataToAttrMapOne.retAddr), hl
8EF4                     .waitVSync
                                 ; call ATR_setAttrDataToAttrMapOne
8EF4 C30A8F          10          jp ATR_setAttrDataToAttrMapOne
8EF7                     .ret2
8EF7 DB40            11          in a, ($40)
8EF9 E620             7          and $20
8EFB 28F7            12          jr z, .waitVSync
                         
8EFE                     .spOne  equ $ + 1
8EFD 310000          10          ld sp, 0
                         
8F09 C9              10          ret

アトリビュートデータを1行だけ展開する処理

変なところは、SPを使ってデータ書き込みしているのに call/ret を利用しているところです。普通は許されませんが、callで書かれた戻りアドレスはretで戻った後に別のデータで必ず上書きされるので問題が発生しません。

2026/11/15 コード編集: ixを使うのは遅いと分かっていながら面倒で手を付けていなかったのですが、AIにix使わないように高速化してと頼んだらまったくミスなく修正してくれスピードアップが出来ました。以下を修正後のコードに変更しました。


; =================================================
; アトリビュートデータを1行だけ展開する
; SPも利用するので注意 (行ワークの読み書きとバッファ書き込みの両方に使う)
; Input:
;   hl = 戻り先アドレス (SPを操作するためcall/retが使えないのでhlで戻す)
;   .workPtr = 当該行の NextMapAttr 行ワークアドレス
; =================================================
ATR_setAttrDataToAttrMapOne:
.rowDone        equ $ + 1
        ld a, 0
        cp AttrVram.lines
        jr c, .@@F
        ld hl, (.retAddr)
        jp (hl)                 ; ret 全行完了済み cy = 0
        ; ret                   ; ret 全行完了済み cy = 0
.@@
        
        ; a = 何行目か
.workPtr        equ $ + 1
        ld sp, 0                ; sp = 当該行ワーク (color, length, nextAddrL, nextAddrH)
        pop bc                  ; c = color, b = length
        pop hl                  ; hl = 圧縮データアドレス
        ld (.workPtr), sp       ; 次行のワーク

        inc b
        dec b                   ; aを壊さずに length == 0 判定
        jr nz, .decoded
        ; 前のデータが終わっていたら次のデータを読む
        ; callの戻りアドレスは当該行の nextAddr に書かれるが直後のpushで上書きする
        call decodeAttrData2
        push hl                 ; nextAddr 更新
        ld c, e
        push bc                 ; color, length 更新
        ld a, (.rowDone)        ; decodeAttrData2でaが壊れるので行番号を再取得
.decoded
        ; c = color, b = length, hl = 圧縮データアドレス, a = 何行目か

        ; SP = AttrVram row write pointer
        ;   H = (AttrVram.top + AttrVram.lineSize) / 256 + rows_done
        ;   L = (AttrVram.top + AttrVram.lineSize) % 256 = 0
        add a, (AttrVram.top + AttrVram.lineSize) / 256
        ex de, hl
        ld h, a
        ld l, (AttrVram.top + AttrVram.lineSize) % 256
        ld sp, hl
        ex de, hl               ; hl = 圧縮データアドレス

        ; 1行分の書き込み
        ; spのメモリ領域は後で上書きするのでcallしてもよい
        ld e, c                 ; e = color
        ld c, ScrWByte / 2      ; c = 行の残り語数 (2バイト単位)
.nextColor
        ld d, e                 ; d = e = color
        srl b                   ; b = 語数, cy = 奇数フラグ
        jr z, .oneLength        ; 元の差が差が 0 or 1
        ex af, af'              ; 奇数フラグ保存
        ld a, c
        sub b                   ; a = 残り語数 - 語数
        jr nc, .fits
        ld b, c                 ; 行末を超える分は切り捨て
        xor a
.fits
        ld c, a
        ; PUSH列の途中に飛び込み b 回だけ push de する
        ld a, .pushMax
        sub b
        ld (.pushOfs), a        ; jrの飛び先 = 先頭から (.pushMax - b) バイト後
.pushOfs        equ $ + 1
        jr .pushTop
.pushTop
        REPT .pushMax
        push de
        ENDM
        inc c
        dec c
        jr z, .lastNoPush       ; 行末まで書いた
        call decodeAttrData2
        ex af, af'
        jr nc, .nextColor
.oddColor                       ; 奇数の1バイト分を処理
        push de
        dec c
        jr z, .lastNoPush
        dec b
        call z, decodeAttrData2
        jr .nextColor
.oneLength
        ld d, e
        call decodeAttrData2
        jr .oddColor
.lastNoPush
        ; 進捗インクリメント
        ld hl, ATR_setAttrDataToAttrMapOne.rowDone
        inc (hl)

        scf
.retAddr equ $ + 1
        jp $0000        ; retの代わり cy = 1

.pushMax        equ ScrWByte / 2    ; 1行の最大PUSH回数

続きの行からの処理

                         =================================================
                         ; アトリビュートデータを展開してアトリビュートバッファに書き込む
                         ; W_AttrRowsDone > 0 の場合はその行から再開する
                         ; ATR_setAttrDataToAttrMapOne を全行完了するまで繰り返すだけ
                         ; =================================================
8F74                     ATR_setAttrDataToAttrMap:
8F74 3A0B8F          13          ld a, (ATR_setAttrDataToAttrMapOne.rowDone)
8F77 FE19             7          cp AttrVram.lines
8F79 D0              11          ret nc
                         
                                 ; ix = NextMapAttr.workTop + rows_done
8F7A DD213E84        14          ld ix, NextMapAttr.workTop
8F7E 5F               4          ld e, a
8F7F 1600             7          ld d, 0
8F81 DD19            15          add ix, de
                         
8F83 21908F          10          ld hl, .ret     ; 戻り先アドレス
8F86 22728F          16          ld (ATR_setAttrDataToAttrMapOne.retAddr), hl
8F89 ED73938F        20          ld (.spOne), sp
8F8D                     .loop
                                 ; call ATR_setAttrDataToAttrMapOne
8F8D C30A8F          10          jp ATR_setAttrDataToAttrMapOne
8F90                     .ret
8F90 38FB            12          jr c, .loop
                         
8F93                     .spOne equ $ + 1
8F92 310000          10          ld sp, 0
                         
8F95 C9              10          ret

結果

各VSync待ちに何行処理しているかを表示したところ、VSync待ちがどれくらい発生しているか異なるため各回でかなりばらついていますが、最大では25行全部を処理しているようです。

AREA16全体をスクロールする時間を録画した動画で確認したところ、17分30秒程度から16分20秒程度にスピードアップすることが出来ました。機能を増やして処理が増えていることを考えると、(アバウトな計測ですが)10%弱スピードアップしたことになります。

謝辞

アセンブラはAILZ80ASM を利用させていただいています。ありがとうございます。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?