⭅ Previous (Sprites) | Next (System Timing) ⭆ |
Last time we took a look at how the NES handles highly flexible graphics using sprites. Now we’ll see how the NES generates background images.
Also on Youtube.
Before diving in and learning how backgrounds work, we can compare the functionality of background graphics against the sprites we looked at last time. This will help us better understand why the system uses two different techniques for rendering.
Feature | Foreground (Sprites) | Background (Nametables) |
---|---|---|
Max Drawable tiles | 64 | 32*30 = 960 |
Tiles per line | 8 | 30 |
Positioning | Arbitrary | Grid aligned |
Coloring | Any fg palette | Palette shared across cell |
Bytes per tile | 4 | About 1 |
Just by looking at the features, we can see that these two rendering systems are optimized for different purposes. Sprite rendering is very flexible but takes more space. Background rendering is more space efficient but comes with limitations.
With that context, lets take a look at how the system actually manages background graphics.
Nametables are arrays stored in VRAM, that the system uses to render the background. There are logically 4 nametables, arranged in a grid. The first nametable starts at VRAM address 0x2000, and corresponds with pixels (0,0) in the upper right to (256,240) in the lower right.
(0,0) (256,0) (511,0)
+-----------+-----------+
| | |
| | |
| $2000 | $2400 |
| | |
| | |
(0,240)+-----------+-----------+(511,240)
| | |
| | |
| $2800 | $2C00 |
| | |
| | |
+-----------+-----------+
(0,479) (256,479) (511,479)
(Diagram from nesdev)
Lets look at the first nametable in detail. From the layout above, we can see each nametable is 0x400 or 1024 bytes. The nametable consists of tile data, followed by attribute data. The tile data contains one byte per tile. Since each tile is 8x8, and a nametable spans 256x240 pixels, we can see that the tile data will take 32x30=960 bytes. This leaves 64 bytes per nametable for attributes.
Recall that when we looked at the CHR rom, we saw that tiles are organized into two tables, each with 256 tiles. This gives the system 512 tiles, but a byte can only select from 256. The system can be configured to use either the left or the right pattern table for background rendering, so that one byte is sufficient. The pattern table used for backgrounds is configured via the PPUCTRL MMIO register at 0x2000.
Tile data is stored row-major, so the second tile byte corresponds with the the one drawn just to the right of the first one.
After all the tile data for the nametable comes the attribute data. Though most documentation refers to this as the attribute table, the only “attribute” it represents is which palette will be used for each tile.
There are 4 background palettes, which would require 2 bits to represent. With 960 tiles per nametable, this would require 1920 bits or 240 bytes. Yet the attribute data is only 64 bytes (about 27% of the size needed for this naive representation).
The NES packs this data tighly by sharing palette information for chunks of the nametable. Chunks of 2x2 tiles share the same palette. With 2 bits per palette, each byte in the attribute table represents 4 2x2 tile chunks, or 4x4 tiles.
For each byte, the bits map to:
BL UL
76543210
BR UR
Each pair of bits select which of the palettes are used for the bottom-right, bottom-left, upper-right, or upper-left 2x2 tile block.
To help explain this multi-level blocking, lets work through an example by hand. We’ll try to make sense of the MegaMan(tm) title screen. Megaman is owned by Capcom. We present the graphics here only for the purpose of explanining the system, which we believe is covered under Fair Use.
There are no sprites on screen, so the entire title screen is generated using the background rendering system.
The menu comes completely from the first nametable. Most of the screen is actually black, so lets try to skip ahead and find some interesting data to look at.
The upper left of the “M” in “Mega” starts at pixel (x,y) = 108, 78. We care about the tile offsets, so
(tx,ty) = 10,7. Since row has 32 tiles, this tile is 32*7 + 10 = 234 or 0xEA
tiles/bytes into the nametable. So it should be at
0x2000 + 0xEA = 0x20EA
Below is a snippet of the vram dump when the title is displayed:
20D0: 7F 7F 7F 7F 7F 7F 7F 7F 7F 7F 7F 7F 7F 7F 7F 7F
20E0: 7F 7F 7F 7F 7F 7F 7F 7F 7F 7F 00 01 02 03 04 05
20F0: 06 07 08 09 0A 0B 7F 7F 7F 7F 7F 7F 7F 7F 7F 7F
2100: 7F 7F 7F 7F 7F 7F 7F 7F 7F 0C 0D 0E 0F 10 11 12
20EA is the byte with value 00. Selecting these tiles from the right pattern table, which gives us the upper corner of the “M” we expected. The next tile would be just to the right in the tile array, which gives 01 and is thus also right next to it in the pattern table.
So far we have the tile. Now lets find the palettes used for these tiles.
Attributes start at 0x2000 + 960 = 0x23c0 for the first nametable. To find the relevant palette, we need to find which 4x4 tile chunk it is within, then which quadrant of that tile. We can divide the tile numbers by 4:
tile numbers:
tx,ty = 10,7
4x4 tile chunk numbers:
cx, cy = 2, 1
and we'll make note of the remainders, used to find the quadrants
rx, ry = 2, 3
Visualizing the 4x4 chunk:
01 23
0 ab cd
1 ef gh
2 ij lm
3 mn op
2,3 would be 'o', in the bottom right quadrant of this chunk.
One byte per 4x4 chunk, and 8 chunks per line means chunk (2,1) is 2*8+1 = 9 bytes
into the attribute section.
This means address 0x23c9, which has value 0x55, or 0b01010101.
23B0: 7F 7F 7F 7F 7F 7F 7F 7F 7F 7F 7F 7F 7F 7F 7F 7F
23C0: 00 00 00 00 00 00 00 00 55 55 55 55 55 55 55 55
23D0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 A0 00
Since this was in the bottom right quadrant of its chunk, the palette comes from the first two bits, or 0b01 for palette 1. This gives it the red palette. Another interesting detail here is that the red tops of the otherwise blue lettering was only possible because these were positioned on different 2x2 tile chunks, so a different palette could be used.
Finally giving us:
Now that we know how everything looks internally, lets take a broader view and see how this fits together. We saw that backgrounds are logically split into 4 nametables, yet each one can fill the full screen. So why does the system need four? The answer is that this enables smooth scrolling.
In order to smoothly scroll the background, the system keeps more than one screen full in memory. Having four logical nametables means no matter which way the window scrolls, there is another nametable up,down,left,and right of the current window.
The current scroll position can be thought of as an offset from the first nametable, and is tracked in pixels. This can be set by writing to the PPUSCROLL register at 0x2005.
Smooth scrolling of 4 screens worth of color graphics, with only 4K of video memory. Quite efficient, especially considering that a standard PNG graphic for the same size would be 3 bytes per pixel * 512 * 480 = 7.3 M bytes, roughly 180 times as large.
But there’s one more trick the NES has in store. It actually only used 2K bytes to store all four nametables.
Most games only needed to scroll in one direction. Cartridge hardware has the ability to “mirror” nametables either horizontally or vertically. This means the 2 unique nametables are either “duplicated” left and right, or up and down. If the system tried to read from a mirrored nametable, it would instead read from the one of the two nametables present on the system.
Games that really want 4 unique nametables could accomplish this by adding vram to the cartridge, and avoiding this mirroring hardware. But this came at a cost, so most games used one of those two mirroring modes.
Since we looked at Megaman already, here you can see the mirroring at work in the nametable viewer. There are only two unique nametables, and the ones below are really just mirroring the ones above. This one is called “vertical mirroring” since the true nametables are “mirrored” below. Vertical mirroring is typically used in games that want to scroll horizontally, since it presents two unique nametables left and right.
You can also see a white border around the upper left nametable, which is how I show the current window selected in my debugger.
And with that, we are very nearly done covering the graphics features of the NES. Next up we’ll have a short discussion about how the CPU and PPU manage synchronization.
⭅ Previous (Sprites) | Next (System Timing) ⭆ |