先日別の方が普通にプレイしている姿をみてて、スタートボタンを押してからメニューがでるまでに毎度1度秒以上かかるのが気になったので改修してみました.
ROM の CRC32 は 0x57E220D0 のものです.
遅い原因
- 画面描画を止めずに vblank を待って限られた時間に最大で 64 byte 書いている
- 移動画面でのセリフに使うウインドウのシステムを何故か流用している
- FF2 からの仕様らしく FF3 でもそのまま使ってしまっている
客観的事実としまして、FF1 はメニュー描画は続編と比較して早く(ファミコンソフト一般として普通)で、FF2 と FF3 はものすごく遅いです.
プログラムをしらべる
対策は画面描画を止めて、vblank を待つことなく1度に VRAM へデータを書き込み、終わったら画面描画を再開することです.
直接の原因
PC:$edcd と PC:$f693 付近の命令が問題となっています.
EDCD: jsr $ff00 ;wait a vblank EDD0: lda #$02 ;DMA to OAM EDD2: sta $4014 EDD5: jsr $f6aa ;store to nametable from buffer EDD8: jsr $c9a9 EDDB: jsr $ede1 EDDE: jmp $c750 ;sound task
F692: pha F693: jsr $ff00 F696: inc $f0 F698: pla F699: jsr $f6aa ;store to nametable from buffer F69C: jsr $ede1 F69F: jsr $c750 F6A2: lda $93 F6A4: jsr $ff03 ;switch a bank for address $8000-$9fff F6A7: jmp $f683
メニューのプログラム
スタートボタンを押すと画面暗転後に PC:0x3d:$a52f から始まります. MMC3 では CPU ROM bank は address $8000-$9fff, $a000-$bfff で分割していますが、このプログラムでは page は 0x3c と 0x3d をつなげて利用しています.
a52f: lda #$00 a531: sta $78f0 ;カーソルに関する変数 a534: lda #$00 a536: sta <$25 a538: sta $2001 ;PPU 描画停止 a53b: sta $79f0 ;カーソルに関する変数 a53e: sta $7af0 ;同上 a541: jsr $dd06 ;CPU の ROM からキャラデータの転送 a544: jsr $f7bb ;画面外枠だけの nametable を data 0 で埋める a547: jsr $c486 a54a: jsr $ff00 ;wait a VBLANK (いらない) a54d: lda #$02 a54f: sta $4014 ;update OAM (いらない) a555: lda #$88 a557: sta <$fd a559: sta <$ff a55b: jsr $959f ;PPU 描画再開
メニューのための VRAM 転送命令が続きますが中略します. その転送命令のなかでは直接の原因が利用されています.
下記は描画完了後のユーザーからのボタン入力待ちに関連するループです.
a5a9: jsr $a7cd a5ac: lda #$00 a5ae: jsr $8000 ;sprite 管理で OAM buffer を更新 a5b1: lda #$00 a5b3: jsr $80cb ;同上 a5b6: lda #$04 a5b8: jsr $91a3 ;同上 a5bb: lda <$25 a5bd: beq $a5c2 ;Bボタンが押されたか a5bf: jmp $a646 ;メニュー終了 a5c2: lda <$24 a5c4: beq $a5a9 ;Aボタンが押されたか a646: jsr $a685 ;画面全体の nametable を data 0 で埋める (いらない) a649: lda #$00 a64b: sta $2001 ;PPU 描画停止 a64e: jsr $a654 a651: jmp $8f58 ;移動画面に戻る
PC:$a646 は本当に無駄な処理で単純にこの jsr のみをなくしても早く移動画面に戻ります.
PC:$a5c4 以降はボタンが押されたかで分岐が入り、アイテムや装備の画面切り替えに飛べます.
下記はアイテムのメニューのプログラムの抜粋です.
9ec2: jsr $9592 9ec5: lda #$80 9ec7: sta <$b4 9ec9: jsr $a685 9ecc: jsr $956f 9ecf: jsr $a328 (中略) 9efe: jsr $a7cd 9f01: lda #$08 9f03: jsr $920d 9f06: lda #$03 9f08: jsr $8000 9f0b: lda <$25 9f0d: bne $9ebc 9f0f: lda <$24 9f11: beq $9efe
アイテム以外のメニュー画面も流れは大体同じで、 PC:$a685 で nametable を data 0 で埋めて、 PC:$956f で PPU を描画にして(すでに描画になってるのですが)、VRAM と変数の初期化命令が入り、 PC:$a7cd が呼ばれた後ボタン入力でループを回しています.
プログラムを直す
直接の原因
命令が増えることもあり、空き領域の確保が求められますがここでは省略します.
変数を追加して vblank 待ちを行わないフラグによって分岐するようにしました.
org $edcd jsr edcd_new org $f693 jsr f693_new org xxxx ;PC: $c000-$ffff のどこか ;skip waiting vblank (jsr $ff00) edcd_new: lda <render_count bmi edcd_skip to_vblank_wait: jmp $ff00 edcd_skip: pla pla jsr $f6aa jmp $c9a9 f693_new: lda <render_count bpl to_vblank_wait rept 3 pla endm inc <$f0 jsr $f6aa jsr $ede1; update scroll register jmp $f683
変数 render_count は address:$00dc 付近を使ってます. address: $00dc-$00e0 あたりは未使用の zeropage のようでした. この領域は reset のあと data 0 で埋めています. (address: $00f0-$00ff 付近は 0 クリアをやってない)
edcd_skip は無理やりやってるので rts 経由せずに pla x2 をいれています(あまりよくないです). PC:$ff00 以外にもサウンドドライバやいらなそうなのは削ってあります.
メニューのプログラム
修正すべてをここには記載できませんが、下記の方針で該当部分を修正します.
- 初期化
- jsr/jmp $a646 と jsr $956f は消してかわりに jsr display_off をいれる
- jsr $ff00 とか OAM への DMA request も消す
- その他 address $2001 で描画を有効にする命令があれば消す
- jmp $a646 で省いて VRAM で data 0 にすべきところがでてくるので直す
- キー入力待ちのループ
- jsr $a7cd を jsr a7cd_new にする
- メニューの終了 (PC:$a646)
- 変数 render_count = 0 にする
display_off: lda #0 sta $2001 lda #$80 sta <render_count jmp $c486 ;clear OAM buffer a7cd_new: lda <render_count lsr a bcs render_temp_0 ;----1度目---- ;nametable attribute を書いた後,一旦戻る ;subroutine の外で OAM buffer を書いてもらう lsr a bcc ppu_attr_fill_ff jsr $95b2 ;draw nametable attribute jmp a7cd_new_noattr ppu_attr_fill_ff: ldx #4 jsr ppuram_fill a7cd_new_noattr: lda #%0001 | $80 sta <render_count jmp $A7D7 render_temp_0: lsr a lsr a bcs render_temp_1 ;----2度目---- ;renderer on, OAM buffer -> OAM jsr $ff00 lda #%0101 sta <render_count jsr $959f lda #0 sta $2003 lda #2 sta $4014 jmp $d308 ;----3度目---- ;通常処理 render_temp_1: jmp $a7cd
a7cd_new について. jsr $a7cd は共通で呼ばれていることと、OAM の初期化が 描画開始から 1 frame ずれてしまう対策として作りました. メインループに入ってから 2 度は初期化が続きます.
- 1度目はその他足りてない VRAM の初期化をいれて一度抜けて、 OAM buffer を更新してもらいます. VBLANK は待ちません.
- 2度目は VBLANK を待ち, PPU を画面描画を再開、直後に OAM buffer を DMA で OAM に転送し、さらにパレットも更新します.
- 3度目は通常通りです.
このため FF1 と違って nametable と object が同時にすべて出るようになります.
対策後
メニュー表示が FF1 並になりました. この動画は他にも修正入れまくってますがメニュー表示に関しては大体同じです.
PC:$a25f 開始から画面描画完了までは 8 frame でできるようになります. 8 frame のうち約半分の時間はキャラクタRAMへの転送(jsr $dd06)です. 残り半分はnametable への転送ですが、もともとの設計が 64 byte 程度のバッファ経由で少しずつ動かすようになっていてこれ以上の短縮は難しいです.
キャラクタRAMへの転送のループも 1 byte 転送で分岐命令が入っていたので 1 byte から 8 byte にかえて画面描画時間を 7 frame に一応は削れはしました. ただし ROM の空きの確保に苦労に対して効果は見合っていませんでした.