One way of looking at mirroring is that it's a way of remapping the two highest bits of the address.
Code: Select all
15 bit 8 7 bit 0
--------- ---------
0010 nnaa aaaa aaaa < nametable read/write address
The bottom 10 bits are used as is for accessing nametables, but the top 2 bits are used to select which nametable to use. The practice of "mirroring" remaps the 4 possible combinations of those 2 bits to the internal VRAM. For example, if you used those two bits for a lookup table, some common mirroring modes might look like:
Code: Select all
mirroring[4] = { 0, 1, 0, 1 }; // vertical mirroring
mirroring[4] = { 0, 0, 1, 1 }; // horizontal mirroring
mirroring[4] = { 0, 0, 0, 0 }; // single screen 0 mirroring
mirroring[4] = { 1, 1, 1, 1 }; // single screen 1 mirroring
You can replace those two bits "nn" with the value looked up from your mirroring table, before using the address to make a read or write. You don't necessarily have to use a lookup table, but any equivalent implementation should work.
It's a little more complicated with mappers that have 4-screen mirroring, or can otherwise map CHR-ROM or CHR-RAM into the nametable space, but this is a little more obscure, and I'm sure you can figure it out when you get there.
On the cartridge, simple mirroring is implemented by physically connecting the 10th or 11th CHR address bit to bit 10 of the VRAM address. The 10th CHR address gives a 0,1,0,1 pattern (vertical mirroring), and the 11th CHR address line gives a 0,0,1,1 pattern (horizontal mirroring), and connecting that output directly to VRAM's 10th bit address directly selects the 1k page. (Reference:
Wiki: Mirroring)