VRAM転送のキュー

メインの処理でVRAMへの転送を予約しておき、NMI処理ルーチンでまとめて実行します。

考え方

VRAMに書き込めるのはVブランク中だけですが、「どのデータを、どこへ、どれだけ転送するか」が決まるのはメインの処理の中です。キャラクターのアニメーションでタイルを差し替える、スクロールで新しい列を書き足す、文字を書き換える、といった転送が、フレームごとに違う数だけ発生します。

そこで、メインの処理では転送の内容(転送元・転送先・バイト数)を表に書き込むだけにしておきます。この表を予約の列(キュー)として使い、NMI処理ルーチンは表に並んだ転送を先頭から順に実行して、最後に表を空にします。

こうしておくと、転送を必要とする処理はNMI処理ルーチンの中身を知らなくてもよく、NMI処理ルーチンも転送の理由を知らなくて済みます。

予約の表

1件の予約を8バイトとし、WRAMに16件分の表を置きます。

アドレス大きさ内容
$04802バイト次に書き込む位置(予約の件数 × 8)
$04822バイトこのフレームに予約したバイト数の合計
$0500~$057F8バイト × 16予約の表

1件の内容は次のとおりです。

オフセット大きさ内容
+03バイト転送元のアドレス
+32バイト転送先(VRAMのワードアドレス)
+52バイト転送するバイト数
+71バイトVMAINCに書き込む値

VMAINCの値も予約に含めておくと、タイルマップの縦の列を書き込む転送(アドレスの増分32)も、同じ表で扱えます。

転送を予約する

入力の並びを、表の1件と同じ並びにしておくと、ダイレクトページから表へ4ワードをそのまま写すだけで済みます。入力の並びは、VRAMへの転送のDmaToVramと同じ並びに、VMAINCの値を足したものです。

; VRAMへの転送を予約する
; 入力: $00~$02 転送元のアドレス
;       $03~$04 転送先(VRAMのワードアドレス)
;       $05~$06 転送するバイト数
;       $07      VMAINCの値(ふつうは$80)
; 予約できればキャリーをセット、できなければクリアして返る
; Aは8ビット、X・Yは16ビットで呼ぶこと
QueueVram:
    REP #$20
    LDX $0480
    CPX.w #$0080
    BCS .full       ; 16件たまっている
    LDA $0482
    CLC
    ADC $05
    CMP.w #$1001
    BCS .full       ; 1フレームの上限($1000バイト)を超える
    STA $0482
    LDA $00
    STA $0500,X
    LDA $02
    STA $0502,X
    LDA $04
    STA $0504,X
    LDA $06
    STA $0506,X
    TXA
    CLC
    ADC.w #$0008
    STA $0480       ; 件数は最後に増やす
    SEP #$20
    SEC
    RTS
.full
    SEP #$20
    CLC
    RTS

1フレームに転送する量の上限は、ここでは仮に$1000バイト(4KB)にしています。VRAMへの転送のとおり、1回のVブランクで転送できる量は理論上でも約6KBで、OAMやCGRAMへの転送もそこから時間を使います。実際の上限は、NMI処理ルーチンのほかの処理と合わせて決めてください。

予約できなかったときは、次のフレームでもう一度予約します。予約に失敗しても困らないように、予約する側で「まだ転送していない」ことを覚えておくのが確実です。

予約した転送を実行する

NMI処理ルーチンの中で呼び出します。DMAPとBBADは転送が終わっても値が残るので、ループの前に1回だけ設定します。

; 予約された転送をすべて実行する(NMI処理ルーチンの中で呼ぶ)
; Aは8ビット、X・Yは16ビットで呼ぶこと
RunVramQueue:
    LDA.b #$01
    STA $4300       ; DMAP0: A→B、アドレス増加、モード1(2レジスタ)
    LDA.b #$18
    STA $4301       ; BBAD0: VMDATAL($2118)
    LDX.w #$0000
.loop
    CPX $0480
    BEQ .done       ; 予約をすべて実行した
    LDA $0507,X
    STA $2115       ; VMAINC
    LDY $0503,X
    STY $2116       ; VMADD: 転送先
    LDY $0500,X
    STY $4302       ; A1T0: 転送元のアドレス
    LDA $0502,X
    STA $4304       ; A1B0: 転送元のバンク
    LDY $0505,X
    STY $4305       ; DAS0: バイト数
    LDA.b #$01
    STA $420B       ; MDMAEN: チャネル0で転送開始
    REP #$20
    TXA
    CLC
    ADC.w #$0008
    TAX             ; 次の予約へ
    SEP #$20
    BRA .loop
.done
    LDX.w #$0000
    STX $0480       ; 表を空にする
    STX $0482
    RTS

注意

  • 予約の表を書きかけている途中にNMIが発生すると、NMI処理ルーチンが表を空にしたあとで、メインの処理が古い件数のまま続きを書き込んでしまいます。NMI処理ルーチンは、メインの処理が1フレーム分の処理を終えて「転送してよい」という印を付けたときだけRunVramQueueを呼ぶようにしてください(NMIとフレームの処理の「処理落ち」)。
  • 予約するのは転送元のアドレスだけで、データそのものは写していません。転送元がWRAMの作業用の領域なら、転送が終わるまで(次のNMIまで)その内容を書き換えないでください。同じ領域を毎フレーム作り直す場合は、領域を2つ用意して交互に使います。
  • DMAは1回の転送でバンクの境界をまたげません(BGを表示するの「データの置き場所」)。大きなデータは、境界で分けて2件の予約にしてください。
  • 同じフレームの中では、予約した順に転送されます。同じVRAMの場所に2回予約すると、あとから予約したほうの内容が残ります。

応用

同じ形の表は、VRAM以外の転送にも使えます。CGRAMへの転送(パレットの書き換え)用に別の表を用意したり、1件に「転送先の種類」を持たせて1つの表でVRAM・CGRAMの両方を扱ったりする方法があります。

コードから構造を読み取る

既存のプログラムでも、NMI処理ルーチンの中に次のようなループがあれば、この形の転送の予約を使っていると考えられます。

  • WRAMの表から値を読み出して、$2116(VMADD)・$4302~$4306(A1Tn・A1Bn・DASn)に書き込み、$420B(MDMAEN)に書き込むことを繰り返している。
  • インデクスを一定の値ずつ増やしながら、件数やバイト数を表す変数と比べてループを終え、最後にその変数を0に戻している。

インデクスを増やす値が1件の大きさです。表を書き込んでいる側(メインの処理の中で、同じ表に書き込み、件数の変数を増やしているところ)をたどると、どの処理がどんなデータを転送しているかが分かります。

参照

VRAMへの転送
NMIとフレームの処理
DMAおよびHDMA
VMAINC レジスタ
大きなマップのスクロール