VRAM転送のキュー
メインの処理でVRAMへの転送を予約しておき、NMI処理ルーチンでまとめて実行します。
考え方
VRAMに書き込めるのはVブランク中だけですが、「どのデータを、どこへ、どれだけ転送するか」が決まるのはメインの処理の中です。キャラクターのアニメーションでタイルを差し替える、スクロールで新しい列を書き足す、文字を書き換える、といった転送が、フレームごとに違う数だけ発生します。
そこで、メインの処理では転送の内容(転送元・転送先・バイト数)を表に書き込むだけにしておきます。この表を予約の列(キュー)として使い、NMI処理ルーチンは表に並んだ転送を先頭から順に実行して、最後に表を空にします。
こうしておくと、転送を必要とする処理はNMI処理ルーチンの中身を知らなくてもよく、NMI処理ルーチンも転送の理由を知らなくて済みます。
予約の表
1件の予約を8バイトとし、WRAMに16件分の表を置きます。
| アドレス | 大きさ | 内容 |
|---|---|---|
| $0480 | 2バイト | 次に書き込む位置(予約の件数 × 8) |
| $0482 | 2バイト | このフレームに予約したバイト数の合計 |
| $0500~$057F | 8バイト × 16 | 予約の表 |
1件の内容は次のとおりです。
| オフセット | 大きさ | 内容 |
|---|---|---|
| +0 | 3バイト | 転送元のアドレス |
| +3 | 2バイト | 転送先(VRAMのワードアドレス) |
| +5 | 2バイト | 転送するバイト数 |
| +7 | 1バイト | 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件の大きさです。表を書き込んでいる側(メインの処理の中で、同じ表に書き込み、件数の変数を増やしているところ)をたどると、どの処理がどんなデータを転送しているかが分かります。