野うさぎ亭

FCEUXへの独自マッパーの追加

FCEUXへ独自マッパーを追加する方法を解説します。

利用可能なマッパー番号

nesdev wikiのmapperページのdiscussionのページ?に 私的利用の番号についての記述があります。

Presently, 3840-4095 are reserved for private use, as well as 100 and 248. 
現在、3840-4095は私的使用のために予約されており、100と248も同様です。

本ドキュメントでは、上記ルールに従い、マッパー番号 4064 を使用することとします。

正式な番号の取得については、まだ情報を持ち合わせていないため、判明次第追記する予定です。

FCEUXへの独自マッパーのソースコードの追加

マッパーのソースコードは、FCEUXのプロジェクト内の下記のディレクトリ配下に配置しますj。

src/boards

ファイル名は特に規定はありません。マッパーの名前をそのまま使用するとよいでしょう。

ファイルを格納したら、Visual studioでFCEUXのプロジェクトを開きます。
ソリューションエクスプローラーでboardsのディレクトリを選択した後、 [プロジェクト][既存の項目の追加]メニューを選択します。
ファイルダイアログから、追加するマッパーのソースコードのファイルを選択して プロジェクトに追加します。

次に、src/ines.cpp ファイルをエディタで開いて、追加するマッパーの初期化関数を登録します。
ines.cppの452行目付近にines番号とマッパーの初期化関数を紐づける定義があります。

static BMAPPINGLocal bmap[] = {
	{"NROM",				  0, NROM_Init},
	{"MMC1",				  1, Mapper1_Init},
	{"UNROM",				  2, UNROM_Init},
(略)

	{"",					0, NULL}
};

このリストの末尾に追加するマッパーの定義を記述します。

	{"MCaFE-e1",           4064, MCAFEe1_Init},

1番目の項目にはマッパーの名前を記述します。これはFCEUXの[Help][Message Log]で開いたメッセージログに表示されます。
2番目の項目にはマッパーの番号を記述します。
3番目の項目にはマッパーの初期化関数を記述します。

最後に、src/ines.h ファイルをエディタで開いて、マッパーの初期化関数のプロトタイプ宣言を記述します。

void MCAFEe1_Init(CartInfo *);

マッパーのソースコードの書き方

本ドキュメントで追加する独自マッパーの仕様

追加するマッパーの仕様は次の通りです。

name MCaFE-e1
PRG ROM capacity 128K
PRG ROM window 8K + 8K + 16K fixed
PRG RAM capacity 8K
PRG RAM window 8K
CHR capacity 128K
CHR window 4K + 4K
Nametable mirroring H or V, switchable
Bus conflicts No
IRQ Yes
Audio No
Registers

CHR Bank 0 ($8000-$8FFF)

7  bit  0
---D DDDD
   | ||||
   +-++++- New bank value for 4KB CHR Bank at PPU $0000-$0FFF.
           PPU $0000-$0FFFの範囲の4KB CHRバンクに設定する新しい値

CHR Bank 1 ($9000-$9FFF)

7  bit  0
---D DDDD
   | ||||
   +-++++- New bank value for 4KB CHR Bank at CPU $1000-$1FFF.
           PPU $1000-$1FFFの範囲の4KB CHRバンクに設定する新しい値

PRG Bank 0 ($A000-$AFFF)

7  bit  0
---- DDDD
     ||||
     ++++- New bank value for 8KB PRG Bank at CPU $8000-$9FFF.
           CPU $8000-$9FFFの範囲の8KB PRGバンクに設定する新しい値

PRG Bank 1 ($B000-$BFFF)

7  bit  0
---- DDDD
     ||||
     ++++- New bank value for 8KB PRG Bank at CPU $A000-$BFFF.
           CPU $A000-$BFFFの範囲の8KB PRGバンクに設定する新しい値

IRQ Counter ($E000-$EFFF)

7  bit  0
DDDD DDDD
|||| ||||
++++-++++- New IRQ counter value.
           IRQ is a raster interrupt based on PPU A12 with the same mechanism as MMC3.
           Assign BG to pattern table 0 and OBJ to pattern table to use.
           If the value to write is non-zero, IRQ enable, and  cancel the IRQ trigger.
           If the value to write is zero, IRQ disable, and  cancel the IRQ trigger.
           When the counter value is non-zero, the counter value decreases with each the Horizontal Blank.
           When IRQ is enable and  the value of the counter reaches zero, the IRQ is triggered.
           新しいIRQカウンター値。
           IRQは、MMC3と同じ仕組みのPPU A12に基づくラスター割り込みです。
           BGをパターンテーブル0に割り当て、OBJをパターンテーブルに割り当てて使用します。
           書き込む値がゼロ以外の場合、IRQを有効にして、IRQトリガーをキャンセルします。
           書き込む値がゼロの場合、IRQは無効になり、IRQトリガーをキャンセルします。
           カウンタ値がゼロ以外の場合、カウンタ値は水平ブランクごとに減少します。
           IRQが有効で、カウンターの値がゼロに達すると、IRQがトリガーされます。

Mode ($F000-$FFFF)

7  bit  0
---- ---M
        |
        +- Nametable mirroring (0: vertical; 1: horizontal)
           ネームテーブルミラーリング (0:垂直 1:水平)

上記仕様のマッパーのFCEUX用のソースコードを下記に示します。

mcafe-e1.cpp

グローバル変数

FCEUXでは、主要な情報をグローバル変数を通じて伝達します。
マッパーのプログラムで参照するグローバル変数を次に示します。

変数名 内容
A 読み書き中のCPUアドレス
V 書き込み中のCPUデータ
GameHBIRQHook 水平復帰期間のタイミングで呼び出されるフック関数の関数ポインタ

Init関数

マッパーの初期化関数です。
ines.cppのソース内のマッパーとines番号を紐づけるリストに記述することで、 nesファイルを実行したときに最初に呼び出される関数です。

初期化関数では、拡張メモリ領域に使用するメモリの確保と各コールバック関数とフック関数を登録します。

void MCAFEe1_Init(CartInfo *info) {

	// Allocate extended memory.
	WRAM = (uint8*)FCEU_gmalloc(WRAMSIZE);
	SetupCartPRGMapping(0x10, WRAM, WRAMSIZE, 1);
	AddExState(WRAM, WRAMSIZE, 0, "WRAM");

	// Register memory for save.
	info->SaveGame[0] = WRAM;
	info->SaveGameLen[0] = WRAMSIZE;

	// Register internal register state
	AddExState(MCAFEe1_StateRegs, 0, 0, 0);

	// Register callback.
	info->Power = MCAFEe1_Power;
	info->Reset = MCAFEe1_Reset;
	info->Close = MCAFEe1_Close;

	// register HBlank hook.
	GameHBIRQHook = MCAFEe1_HBlank;
}

FCEU_gmalloc()はメモリを確保する関数です。WRAM用のメモリ領域を確保しています。

SetupCartPRGMapping()は、NESのPRGメモリ空間にエミュレータのメモリ領域を割り当てる関数です。
割り当て領域は全部で32個あり、0番目はNESファイルのPRG-ROMが割り当て済みです。
上記プログラム例では、WRAM用に確保したメモリ領域を、16番目の割り当て領域に登録しています。

登録するメモリ領域が電源を切っても保存可能な領域であれば、info->SaveGame[], info->SaveGameLen[] に情報を設定します。

AddExState()は、メモリ領域の状態を参照するために登録する処理のようなのですが、どこに使用されているのか、まだわかっていません。
詳細が分かりましたら、追記します。

info->Power, info->Reset, info->Close は、それぞれ、電源投入時、リセット時、nesファイルを閉じる時に呼び出される コールバック関数を設定します。
各関数の詳細は後述します。

GameHBIRQHook は、水平復帰期間開始時に呼び出されるフック関数を登録するグローバル変数です。
水平復帰期間割り込み関数を設定します。関数の詳細は後述します。

Power関数

電源を入れたタイミングで呼び出されるコールバック関数です。

void MCAFEe1_Power(void) {

	// Register a handler for memory access.
	SetWriteHandler(0x8000, 0xffff, MCAFEe1_MapperWrite);
	SetReadHandler(0x8000, 0xffff, CartBR);
	SetWriteHandler(0x6000, 0x7fff, CartBW);
	SetReadHandler(0x6000, 0x7fff, CartBR);

	FCEU_CheatAddRAM(WRAMSIZE>>10, 0x6000, WRAM);

	// Bank settings for fixed area.
	setprg8r(0x10, 0x6000, 0);
	setprg8(0xc000, 0x0e);
	setprg8(0xe000, 0x0f);

	// initialize a inner registers.
	IRQ_CNT = 0;

	// Nametable milloring settings at startup.
	setmirror(MI_V);

	MCAFEe1_Reset();
}

SetReadHandler()、SetWriteHandler()は、CPUメモリ領域に対するメモリアクセス関数を登録します。
上記プログラムでは、$8000-$FFFFのメモリ領域の書き込みのメモリアクセス関数としてMCAFEe1_MapperWrite()を設定しています。
また、$8000-$FFFFのメモリ領域と$6000-$7FFFのメモリ領域の読み込みにFCEUXが標準で用意しているメモリの読み込み関数 CartBR()、 $6000-$7FFFのメモリ領域の書き込みにFCEUXが標準で用意しているメモリの書き込み関数 CartBW()を設定しています。

setprg8()、setprg8r()は、CPUメモリ領域のバンク設定を行う関数です。
関数名に含まれる8は、8KBのバンクウィンドウを示しています。
setprg8()はPRG-ROMのメモリ用で、setprg8r()はその他のメモリ用です。setprg8(a,b)は、setprg8r(0x00,a,b)と等価です。
上記プログラムでは、バンク切り替えが行われないCPUメモリ領域の設定を行っています。

バンク設定の関数の一覧を以下に示します。

setprg2r setprg2 CPUメモリ領域 2KBウィンドウ
setprg4r setprg4 CPUメモリ領域 4KBウィンドウ
setprg8r setprg8 CPUメモリ領域 8KBウィンドウ
setprg16r setprg16 CPUメモリ領域 16KBウィンドウ
setprg32r setprg32 CPUメモリ領域 32KBウィンドウ
setchr1r setchr1 PPUメモリ領域 1KBウィンドウ
setchr2r setchr2 PPUメモリ領域 2KBウィンドウ
setchr4r setchr4 PPUメモリ領域 4KBウィンドウ
setchr8r setchr8 PPUメモリ領域 8KBウィンドウ

IRQ_CNTは、本マッパープログラムが用意したグローバル変数です。電源投入時の初期値として0に設定しています。

setmirror()は、ネームテーブルのミラーリング設定を行う関数です。電源投入時の初期値として垂直ミラーリングに設定しています。

Reset関数

リセットしたタイミングで呼び出されるコールバック関数です。

void MCAFEe1_Reset(void) {
	;
}

本マッパーでは、リセット時の動作として特に何も行わないので、何も行わない関数を設定しています。

Close関数

nesファイルを閉じて実行を止めたタイミングで呼び出されるコールバック関数です。

void MCAFEe1_Close(void) {
	// Free extended memory.
	if (WRAM) FCEU_gfree(WRAM);
	WRAM = NULL;
}

初期化関数で確保したWRAM用のメモリ領域を解放しています。

メモリアクセス関数

メモリアクセスの処理を行うコールバック関数です。

DECLFW(MCAFEe1_MapperWrite) {
	switch (A & 0xf000) {
	case 0x8000:
		setchr4(0x0000, V & 0x1f);
		break;
	case 0x9000:
		setchr4(0x1000, V & 0x1f);
		break;
	case 0xa000:
		setprg8(0x8000, V & 0x0f);
		break;
	case 0xb000:
		setprg8(0xa000, V & 0x0f);
		break;
	case 0xe000:
		IRQ_CNT = V;
		X6502_IRQEnd(FCEU_IQEXT);
		break;
	case 0xf000:
		setmirror((V & 1) ? MI_H : MI_V);
		break;
	}
}

本マッパープログラムでは、CPUメモリ領域 $8000-$FFFF への書き込みでマッパーのレジスタへ値を設定するため、 レジスタ書き込みを行うメモリアクセス関数を定義してPower関数で登録しています。

メモリアクセス関数内では、グローバル変数のAを評価して各レジスタへの書き込み処理を行っています。

$8000-$BFFFへの書き込みでは、setprg8()、setchr4()を使用してバンク切り替えを行っています。

$E000-$EFFFへの書き込みは、IRQレジスタへ値を設定します。
X6502_IRQEnd()は、CPUへのIRQのトリガーをキャンセルする関数です。

$F000-$FFFFへの書き込みは、モード設定のレジスタへの書き込みです。
書き込むデータの0bit目に従ってネームテーブルのミラーリングを設定しています。
CHR ROMのフラッシュプログラミングを支援するモード(bit7)の処理は未実装です。

水平復帰期間割込み関数

水平復帰期間に入ったタイミングで呼び出されるフック関数です。

void MCAFEe1_HBlank(void) {
	if (IRQ_CNT > 0) {
		IRQ_CNT--;
		if (IRQ_CNT == 0) {
			X6502_IRQBegin(FCEU_IQEXT);
		}
	}
}

本マッパーの仕様に沿ってIRQ_CNTの値を操作しています。

X6502_IRQBegin()はIRQをトリガーする関数です。

NESファイルの作成

nesasmはnes2.0形式のヘッダーをサポートしていません。 そこでnesファイルのヘッダーを書き換えるツールを用意しました。

inesAlterer v1.01

v1.00 初版
v1.01 mirroringの書き込みの不具合を修正

nesasmでのアセンブルでは、マッパー番号100で作成し、生成されたnesファイルに対して下記のコマンドを実行します。

	inesAlterer -num 4064 -prg_ram 8k <nesファイル>

-num オプションでマッパー番号を指定します。
-prg_ram オプションでWRAMのメモリサイズを指定します。

makefileに組み込んで使用するとよいでしょう。