Famicom cartridge technical information - FF1 のビデオエミュレーション

概要

ファイナルファンタジー(初代)ではクリスタルの輝きを取り戻すと斜めに灰色のオーラのようなものが出る演出があるが、大半のエミュレータでは正しく表示できない。



Nestopia は正常

解析と解説

原因

CPU address $2001 (write) では画面をモノクロかカラーを選択するレジスタがあり、オーラに見えるものはそのレジスタを描画中に頻繁に書き換えることによって実現している。
正しく表示されないエミュレータでは対象レジスタの取り込み頻度が実機と異なるために発生する。

プログラム解析

Program ROM は SQF-FF-0 で、別のバージョンではアドレスがずれている可能性がある。各命令の後のコメントの先頭の数値は実行サイクル数。
loop のメイン処理
DA7F: 
	jsr  $DAD2 ;sequece end, wait vblank interrupt

;wait renderring start line
	ldx  $10 ;3, skip line count
DA84: ;uses 114 cycle (last 113 cycle) / loop
	jsr  $DAF4 ;6 + 103 cycle
	dex  ;2
	bne  $DA84 ;3 or 2

;color -> monochrome renderring line
	ldx  $11 ;3, effective line count
DA8C: ;uses 116 cycle (last 115 cycle) / loop
	jsr  $DAFF; 6+ 57 + 42 (monochorme) + 6 cycle
	dex  ;2
	bne  $DA8C ;3 or 2
loop 開始
$dad2 は renderring frame の終了処理, vblank interrupt の割り込み待ちが入っている。スクロールレジスタの bit0 を毎フレーム 0 か 1 に切り換えているのは、画面をぶるぶるさせているものと思われる。

;renderring squence end, wait vblank
DAD2: 
;generate noice sound?
	lda  $11
	lsr  a
	lsr  a
	lsr  a
	ora  #$30
	sta  $400C
	sta  $4004
;update scroll register
	and  #$01
	eor  $1F
	sta  $2005 ;scroll register port
;wait next vblank interrupt
	jsr  $FE00
	lda  #$02
	sta  $4014 ;sprite dma register
	ldy  #$06
DAF0: 
	dey  
	bne  $DAF0
	rts  
割り込み処理
jsr $fe00 の先は、無限ループ -> 割り込み発生 -> jsr $fe00 から戻ってくる。3回の pla はステータスレジスタと割り込み発生アドレス ($fe00) を破棄するために pop を 3回やってから、 rti ではなく rts を使用している。

FEEE: 
	jmp  $FEEE

FECF:
	lda  $FF
	sta  $2000
	lda  $2002
	pla  ;status
	pla  ;pc low
	pla  ;pc high
	rts  
表示調整待ち

jsr $DAF4 を伴う最初の小ループはライン単位の時間つぶしと思われる。PC:$da84 から次の $da84 までの消費サイクル数は手動で数えたら 114 だった。解析文書によると 1 line 当たりの CPU 消費サイクル数は 113.66 (1364/12) らしいので、CPU レベルの精度では適切な設定値だと思われる。

;2+ (2+3) * 0x11 + 2+2 = 91
DAF4: 
	ldy  #$12 ;2
DAF6: 
	dey  ;2
	bne  $DAF6 ;3 or 2
;2 + 2 + 2 + 6
	nop  ;2
	nop  ;2
	nop  ;2
	rts  ;6
$2001 への頻繁な書き込み
jsr $DAFF を伴う2つ目の小ループは color/monochrome レジスタを切り換えるループで、モノクロになってる期間は 36 cycle。この小ループの消費サイクル数は 116 で、ライン単位待ちより 2 cycle 多い。このため、発生する時間が1ラインあたり 2 cycle ずれるので、1ラインあたりで右に約 1 pixel ずれたモノクロ領域が出せるものと思われる。

このループで消費サイクルを 114 にしたら長方形のモノクロ領域が表示できるかもしれないが、CPU から pixel 単位の計測は精度が粗いので、途中でずれる可能も多々あり、斜めにする方が違和感がないと思われる。

DAFD: ;adjust cycle count for 'bne' opcode
	nop
	nop

;switch monochrome/color register
;2+ (2+3) * 9 + 2+2 = 51
DAFF: 
	ldy  #$0A; 2
DB01: 
	dey  ;2
	bne  $DB01 ;3 or 2
;2+4 = 6
	lda  #$1F ;2
	sta  $2001 ;4, monochrome, display sprite and tile layer
;2*3 = 6
	ldy  #$1E ;2
	nop  ;2
	nop  ;2
;6*4+2*3+6 = 36
	jsr  $DB19 ;6+6
	jsr  $DB19 ;6+6
	nop  ;2
	nop  ;2
	nop  ;2
	sty  $2001 ;6 color, display sprite and tile layer
DB19:
	rts ;6 

6502 のブランチ命令 (ここでは bne) は分岐発生時に 3 cycle, 分岐なしで 2 cycle となるが、分岐先のアドレス bit15:8 が異なると 4 cycle かかる。このため、 $DAFD での2つの nop はこれが発生しないように意図的に入れた命令である。これがないと 分岐先が $DAFF, 分岐元が $DB01 になるので、プログラマ(おそらく Nasir 氏)がCPU の命令実行カウントに気を配っていたと思われる。

プログラムの解説

描画位置の特定および推測
PPU からの描画位置を CPU から推測することは、スキャンライン単位ではよく行われていて、初期のソフトウェアでは sprite の当たり判定から得ることが出来る。スコアの位置は動いてないものの、メインでは横スクロールするのはよく使われていて、表示途中に描画制御のレジスタを書き換えている。

今回の場合は特殊で、スキャンラインと横座標も計測していることになる。解析の部分にも記載したが、PPU と CPU ののクロックの扱いは異なるので厳密な位置は得られないので推測になってしまう。
パレットに関するレジスタは書き込んだ直後に反映される
スクロールレジスタの場合は描画途中とはいえども、取り込む頻度が1スキャンラインに1度だったりするので、時間的な余裕はあるのに対して、カラーレジスタはパレットの address を制御するレジスタなので描画中は常に参照する。

VirtuaNES では本来の動作では左隅からモノクロの場合はそのスキャンライン全てがモノクロというのはカラーレジスタの反映頻度をスクロールレジスタと同じにしているからだと思われる。

エミュレータでの修正方法はこのレジスタを書き換えた場合は PPU と同期を強制的にとるか、根本的に CPU と PPU の同期頻度を上げればよい。そもそもこのカラーレジスタ自体を活用しているソフトが少ないのかモノクロレジスタ自体を実装していないエミュレータも結構ある。即反映の描画関連レジスタは他にキャラクタバンクがある。

対応しているエミュレータ(Nestopia や Nintedulator)ではキャラクタバンクの書き換えと PPU の同期を実装したついでにモノクロレジスタもやっておいた、ぐらいかもしれない。

プログラムの転用の可能性

この方法は下記の理由で使用しづらいものと思われる。
  • 常にCPUで監視するので他のことができない
    • 描画領域上半分の時間で計測するので下半分で残りの簡単な処理をするくらい
    • 下半分にこれを出すことが出来ない
  • 座標の判定精度が粗いので細かい描画が出来ない
  • モノクロとはいっても color ROM の参照アドレス 3:0 を 0 に固定しているだけらしいので使える色は4色だけ