ファミコンの FF3 のメニューの描画を早くした

先日別の方が普通にプレイしている姿をみてて、スタートボタンを押してからメニューがでるまでに毎度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 並になりました. この動画は他にも修正入れまくってますがメニュー表示に関しては大体同じです.

www.youtube.com

PC:$a25f 開始から画面描画完了までは 8 frame でできるようになります. 8 frame のうち約半分の時間はキャラクタRAMへの転送(jsr $dd06)です. 残り半分はnametable への転送ですが、もともとの設計が 64 byte 程度のバッファ経由で少しずつ動かすようになっていてこれ以上の短縮は難しいです.

キャラクタRAMへの転送のループも 1 byte 転送で分岐命令が入っていたので 1 byte から 8 byte にかえて画面描画時間を 7 frame に一応は削れはしました. ただし ROM の空きの確保に苦労に対して効果は見合っていませんでした.