I'll be calling the game metatiles (16x16) "Squares" just to simplify the names.
Let's suppose you have a big map. It could be as big as 4096x4096 squares large.
Variables you may have:
Camera Coordinates in pixels (16 bits)
CameraX (unit: pixels)
Tile Coordinates of upper-left corner in VRAM
TileX (unit: tiles, 8px) Range is either 0-31 (single screen or horizontal mirroring), or 0-63 (vertical mirroring or four screen)
TileY (unit: tiles, 8px) Range is either 0-29 (single screen or vertical mirroring), or 0-29/32-61 (horizontal mirroring or four screen)
You can get TileX just by dividing CameraX by 8. But TileY needs to be tracked separately, because of wrapping after 30 tiles.
Size of scrolling region that will contain valid tiles at any given point
I suggest a 272x240 scrolling region (17x15 squares) if you are using vertical mirroring, but you might be using a 256x240 region instead (16x15 squares)
An in-RAM copy of the attribute tables, so you don't need to read it back out of VRAM.
This consumes 64 bytes of RAM per nametable, and is much simpler than trying to read bytes out of video memory.
So the problem is, given an Absolute Map Square Coordinate (Target), and Camera Coordinates, update the video memory for a new square.
First, you need Map Square coordinates of Upper-Left corner.
Just divide camera X/Y by 16.
MapUpperLeftX = CameraX / 16 (units: map squares, 16px)
MapUpperLeftY = CameraY / 16
Then you can get a relative position from the Target coordinate
RelativeX = TargetX - MapUpperLeftX (units: map squares, 16px)
RelativeY = TargetY - MapUpperLeftY
Check if your RelativeX, RelativeY is in bounds (for example, the 17x15 square region, but you might be using something else), sometimes a game can try to update a tile after it has scrolled out of bounds, and you get a graphical glitch
Address in video RAM:
VramAddress = 0x2000
AttributeTableAddress = 0x23C0
AttributeIndexRam = 0 (range: 0-63 per nametable)
TargetTileX = RelativeX * 2 + TileX (unit: tiles 8px)
Mask it to 0-63 for vertical mirroring or 4 screen, 0-31 for single screen or horizontal mirroring
If TargetTileX >= 32, subtract 32
- If it exceeded 32, For Vertical Mirroring or 4 screen: add 0x400 to VramAddress and AttributeTableAddress, and add 0x40 to AttributeIndexInRam
VramAddress += TargetTileX
AttributeTableAddress += TargetTileX / 4
AttributeIndexInRam += TargetTileX / 4
TargetTileY = RelativeY * 2 + TileY (unit: tiles 8px)
(horizontal mirroring or 4 screen only): If TargetTileY >= 62, subtract 62
If TargetTileY >= 30, subtract 30
- If it exceeded 30, For Horizontal Mirroring or 4 screen: add 0x800 to VramAddress and AttributeTableAddress, and add 0x40 to AttributeIndexInRam (add 0x80 for 4 screen)
VramAddress += TargetTileY * 32
AttributeTableAddress += (TargetTileY / 4) * 8
AttributeIndexInRam += (TargetTileY / 4) * 8
Now we have an address in VRAM for the target tile, and target attribute byte address.
Need to set a new value for the attribute byte.
Easiest way to do this is to replicate the new color value four times within a byte, then apply a mask to the old data, and new data.
NewValue = color + color * 4 + color * 16 + color + 64 (Use a tiny lookup table for this, 4 entries)
Determine a mask for which bits we want from the old data, and which bits we want from the new data.
Mask: 1 = bit comes from old data, 0 = bit comes from new data
if TargetTileX & 2, Mask |= 00110011, otherwise Mask |= 11001100
if TargetTileY & 2, Mask |= 00001111, otherwise Mask |= 11110000
NewByte = (OldByte & Mask) | (NewValue & (Mask xor FF))
I hope I did this correctly, and I hope it helps with the math side of things.
Here come the fortune cookies! Here come the fortune cookies! They're wearing paper hats!