My advice:
Every Read/Write should be done through a function (with the acception of zero page reads/writes, which can always be done with RAM directly, as Zero page will always be RAM).
Set up several Read and Write functions which cover different areas of addressing space. For example:
Read_RAM (for $0000-1FFF)
Read_2xxx (for $2000-3FFF)
Read_4xxx (for $4000-4FFF)
Read_OpenBus (for $5000-5FFF)
Read_SRAM (for $6000-7FFF on supported games)
Read_PRG (for $8000-FFFF)
Keep 16 function pointers for reading, and 16 function pointers for writing, each which covers a 4k page of addressing space.
Code:
// make the pointers
ReadProc ReadMemory[0x10];
// set up the pointers
ReadMemory[0x0] = Read_RAM;
ReadMemory[0x1] = Read_RAM;
ReadMemory[0x2] = Read_2xxx;
...
ReadMemory[0xF] = Read_PRG;
When the game performs a Read/Write, simply call the function which represents that area of addressing space. You can do this easily by pulling the high digit of the address and using it as an index (simple right shift by 12)
Code:
#define CPU_READ(adr) ReadMemory[(adr) >> 12](adr)
This provides many benefits:
1) You can avoid doing several if-else chains for every read
2) When games adjust addressing space (by disabling WRAM, or putting RAM @ $8000 and up, or other weird things), this can easily be accomidated by changing the function pointer.
3) Mappers can easily catch their register writes by having their own write function and changing function pointers so that it gets called whenever the game writes there.
PRG/CHR swapping can be done easily by keeping one large buffer which holds ALL the game's PRG/CHR data.. and by keeping several pointers to represent PRG/CHR banks.
Code:
u8 nPRGBuffer[ size_of_games_prg ]; /* this should be allocated dynamically on ROM load with malloc() or new[] or whatever */
u8* pPRG[8]; // 8 pointers, each represents 4k of PRG space
With that above code -- each of the 8 pPRG pointers represents a 4k page of PRG. pPRG[0] would be cpu$8000, pPRG[1] would be cpu$9000, etc. To work this into the above mentioned Read_PRG function:
Code:
u8 Read_PRG(u16 adr)
{
return pPRG[(adr >> 12) - 8][adr & 0x0FFF];
}
That will return the appropriate byte from the appropriate bank which was swapped in.
With this method, bankswapping can be done VERY easily by just changing a few pointers:
Code:
void Swap8kPRG(int where, int page)
{
page *= 0x2000;
pPRG[where] = &nPRGBuffer[ page ];
pPRG[where + 1] = &nPRGBuffer[ page + 0x1000 ];
}
This allows you to swap PRG without having to copy large chunks of memory. 4k banks are the max size you should go with, as the smallest swap size is 4k (NSFs -- I don't know of any actual ROMs which swap any less than 8k).
CHR can be done the same way -- only you should go with 1k or smaller banks (many games have 1k banks -- I think that's the smallest any game swaps -- although I heard rumors that the mapper being used for Grandtheftendo will have 512 byte swapping, so you may want to prepare for it).
This exact same logic can be applied to Nametable mirroring. Simply use 4 pointers for each nametable, and when the game changes mirroring modes, simply change your pointers to accomidate the new mode.
That's what I'd recommend. I'd be happy to answer Qs or clarify if needed.