ファミコンの FF3 のメニューの描画に IRQ を当ててみた

疑似スクロール

疑似スクロールはアイテム欄が32個(=16行)あるのに対し1画面で16個(=8行)の表示をしています. よろしくないのは1つのnametableでスクロールしているようにみせているので、上下端でカーソルを動かすとアイテム欄の nametable をすべて描画して、これが遅いです.

nametable は1画面分のみ使っているのでもう1画面はまったくつかっていません. これを1度に描画しておき、疑似ではなく本当にスクロールができるかためしました.

ウインドウシステム

ウインドウは文字列がはみ出ないような基本設計がされています.

ウインドウは x, y, width, height の4つを 8x8 pixel (=1 name) 単位で指定して、対象の文字列のポインタを設定することができます. その後、中身が空のウインドウを描画し、その中に文字列をいれていきます. (一旦空のウインドウを描画するのは別の問題があり後述します.)

アイテム欄は中身 8 行の大きさが定義されていますが、これを中身 16+1 (+1 はゴミ箱)にすると一度にすべて描画してくれましたが、Address bit 10 (別仮想画面)をまたぐ nametable の address 更新が想定外だったのでパッチを当てました.

さらに VRAM mirroring を当てるとこの表示になりました.
f:id:na6ko:20211017225539p:plain

ウインドウシステムではさらにカーソルの位置やアイテムの情報も管理できるようになっていて設計は素晴らしいとは思いましたが、 inc 1 つですむような命令を lda ; clc; adc; sta として何箇所も書いているので実装は.... パッチをいれる隙間が多くて助かりました.

スキャンラインカウンタ IRQ を発生してスクロールレジスタを更新する

IRQレジスタ管理はそんなに難しくありませんでした. アイテム欄は縦につながっているので、スクロール管理が少し難しいです. やってみてわかったのは、 CPU address $2005 での横スクロール切り替えは制約が少ないものの、CPU address $2006 での縦スクロールは切り替えは制約が多いです.

IRQ handler を作るために固定領域(CPU address $c000-$ffff)の空きを作ること、IRQ 発生途中でのバンク切り替えに対応することなど ROM 領域の空きを捻出することも求められます.

今回の IRQ handler では CPU address $2001 も hblank 中に更新できるような仕組みをいれてみました. 今のところは空き領域確保の都合で実用化できてないのですが、描画途中に CPU address $2001 bit 3 の nametable rendering を替えることによってアイテム欄を隠すことも mesen などエミュレータではできてます. というのも、 MMC3 の scanline counter は PPU Address bit 12 の立ち上がりを数えているだけなので nametable rendering だけを止めたらちゃんと動くのかは(私の知識がないので)自信がないためです.

試行錯誤はありましたが、疑似スクロールだった処理は16 pixel単位ではあるもの本物のスクロールを利用して再描画時間がなくなり待ち時間は解消できました.

アイテム欄では 1 frame あたり表示期間中に 2 度 IRQ を発生して nametable を本当にスクロールさせています. 装備,預ける,売るは下がアイテム欄だけだったのでそのまま見えるようにしています.
f:id:na6ko:20211017224751p:plain

これは作り途中だった動画です.
www.youtube.com

本当に必要な描画のみにする

nametable の再描画を大きい単位でやっているためで更新の必要がないものも再度書き込みを行っているのが原因です.

アイテム欄で消費や場所の移動をすると従来のプログラムではアイテム欄を更新が必要ない部分も含めて見えている部分を再描画します. アイテム欄のウインドウの面積を2倍にすると再描画時間も2倍になりよくありません. アイテム欄を途中から描画してもらうのは既存のプログラムをみたところ、難解でした.
先述の xywh と一緒にポインタがでてくる内容を完全に理解できないので、 x = 2 (ウインドウの中身), y = 更新対象, w = xx, h = 2 (濁音もいれる) としつつ、ポインタだけを同じ感じにしてなんとかしています. このため、アイテム1つだけ再描画はできずアイテム2つ(=1行)再描画となりました.

;          pt        X Y  x y  w h  pt
;0040 0000-8486 001B-0205 0006-1C10 FC85
;0040 0000-D881 001B-020C 000D-1C10 5081

item_redraw_line:
	ldx	#0
	beq	irl_main
equip_redraw_line:
	ldx	#5
irl_main:
	stx	<$81
	and	#$fe
	sta	<$80
	lsr	<$80
	ldx	#2
	clc
	adc	ic_scroll_offset
	cmp	#$1e
	bcc	irl_0
	sbc	#$1e
	ldx	#$22
irl_0:
	sta	<$39 ;y
	tay
	dey
	sty	<$97
	stx	<$38 ;x
	dex
	stx	<$98
	lda	#$1c
	sta	<$3c ;w
	lda	#2
	sta	<$3d ;h

	ldx	#0
irl_push:
	lda	$7a00,x
	pha
	inx
	cpx	#8
	bne	irl_push
;先頭のアイテムはポインタ算出がわからなかったので正式なアイテム描画をやる
	ldx	<$81
	lda	<$80
	bne	irl_1
	lda	irl_parameter,x
	jsr	$a678
	jmp	irl_cusor_position_restore
irl_1
	lda	#$1b
	sta	<$93
	lda	irl_parameter+1,x
	ldy	irl_parameter+2,x
	jsr	item_pointer_calc
	sta	<$1c
	sty	<$1d
	ldx	<$81
	lda	irl_parameter+3,x
	ldy	irl_parameter+4,x
	jsr	item_pointer_calc
	sta	<$3e
	sty	<$3f
	jsr	$eb2d
irl_cusor_position_restore:
	ldx	#7
irl_pop:
	pla
	sta	$7a00,x
	dex
	bpl	irl_pop
	rts

irl_parameter:
;通常
	byt	$3d
	adr	$85fc-$11
	adr	$8684-$11
;装備
	byt	$2f
	adr	$8150-$11
	adr	$81d8-$11

item_pointer_calc:
	ldx	<$80
;	beq	ipc1
item_pointer_next:
	clc
	adc	#$11
	bcc	ipc0
	iny
ipc0:
	dex
	bne	item_pointer_next
ipc1:
	rts

本当に更新が必要なアイテムの位置を見つける

  • 消費、装備装着、売却はアイテム1個単位
  • 交換はアイテム2個単位
  • 装備解除はアイテム最大5個

苦労したのは装備解除です.外れるアイテムが最大5個で描画の重複を避けるようにソートするのも手間で ROM 領域も消費します. またすべて外すの内部処理中に装備品を1つずつ外すので、キャラクタのステータスを外すたびに再計算しているようでこれがわりと遅い(80 scanlineぐらい)です. このため IRQ 管理に矛盾が生じ画面が乱れました.

ステータス再計算中だけに RTI だけしている NMI handler を勝手に更新して MMC3 の IRQ counter を初期化することで IRQ もちゃんと発生させるようにしました. RTI だけしている NMI handler も基本設計が割り切ってる感じがあってそのデメリットを見ている感じです.

ウインドウの再描画もいらない

アイテム欄以外のメッセージや金額そして装備ウインドウでさえ外枠->中身埋め->文字列入れ直しをやっています. 体感として待ってる感じはないのですが文字列がちらついているのはよくないです.

原因は window の xywh の算出と描画と文字列の描画が1組になるようなしくみだったため、wyxh の算出, x+=1; y+=1; w-=2; h-=2, 文字列の描画だけにしたところほとんど改善しました.

予想と違うのはセリフのような可変長文字列の 0 終端以降は前の文字が残るはずなのに残らなったことです.

唯一都合が悪かったのは装備品を外したときのウインドウのゴミが残るところで、連続しない3つの name(tile) をきれいに直す方法を探る必要があります.