Zelda Screen Transitions are Undefined Behaviour
The vertical scrolling effect in the original “The Legend of Zelda” relies on manipulating the NES graphics hardware in a manner that was likely unintended by its designers.
Since I don’t have access to any official documentation for the NES Picture Processing Unit (PPU - the graphics chip), my claim of “undefined behaviour” is somewhat speculative. I’ve been relying on the NesDev Wiki for a specification of how the graphics hardware behaves. The PPU is controlled by writing to memory-mapped registers. Using these registers for their (seemingly!) intended purposes, the following effect should not be possible:
When scrolling the screen vertically, the entire screen has to scroll together. The above video is an example of Partial Vertical Scrolling. Part of the screen remains stationary (the Heads Up Display) while another part (the game area) scrolls vertically. Partial vertical scrolling, can’t be done by interacting with the PPU in the “normal” way.
Partial horizontal scrolling, on the other hand, is completely well-defined.
Writing to a particular PPU register while a frame is being drawn can result in graphical artefacts. The Legend of Zelda intentionally causes an artefact which manifests itself as partial vertical scrolling. This post gives some background on NES graphics hardware, and explains how the partial vertical scrolling trick works.
Types of Graphics
The NES has 2 types of graphics:
- sprites, which are tiles that can be placed at arbitrary positions on the screen and independently move around
- the background, which is a grid of tiles that can be scrolled smoothly as a single image
To highlight the difference, here’s a scene made up of sprites and background:
Here’s the same scene with only the sprites visible:
And here’s the scene with only the background visible:
Scrolling
The NES Picture Processor supports scrolling background graphics. In video memory, the background graphics are stored as a 2D grid of tiles, covering an area twice the width and twice the height of the screen. The screen displays a screen-sized window into this grid, and the position of the window can be finely controlled. Gradually moving the visible window within the grid produces a smooth scrolling effect.
The video output from the NES is 256x240 pixels. The in-memory tile grid represents a 512x480 pixel area, and is broken up into 4 screen-sized quadrants called “name tables”. Games can configure the Picture Processing Unit (PPU), to specify the position of the visible screen-sized window by selecting a pixel coordinate within the grid of name tables.
Choosing the coordinate (0, 0) will display the entire top-left name table:
Scrolling to (125, 181) shows a bit of each name table:
The visible window wraps around to the far side of the in-memory tile grid. Scrolling to (342, 290) will place the top-left corner of the visible screen inside the bottom-right name table, and parts of each name table will be visible due to wrapping:
Not Enough Memory!
Each name table is 1kb in size, but the NES only dedicates 2kb of its video memory to name tables, so only 2 name tables can fit in memory.
How can there be 4 name tables?
Name Table Mirroring
Video memory is connected to the PPU in such a way that when the PPU renders a tile from one of the 4 apparent name tables, one of the 2 real name tables is selected, and read from instead. This effectively means that the 4 apparent name tables are made up of 2 identical pairs of name tables.
This image shows a snapshot of the contents of all 4 name tables. The top-left and top-right are identical, as are the bottom-left and bottom-right.
Why not just have 2 name tables then?
Fortunately, the precise mapping between apparent name table and real name table can be configured at runtime. If a game wants to scroll horizontally, it will configure graphics hardware such that the top-left and top-right name tables are different, so it can scroll between them without any visible duplication. In this configuration, the top-left and bottom-left name tables will refer to the same real name table, and likewise the top-right and bottom-right. This configuration is named “Vertical Mirroring”.
The other possible configuration is “Horizontal Mirroring”, which games use when they want to scroll vertically.
Games usually don’t scroll diagonally, as it produces artifacts around the edge of the screen due to name table mirroring.
Cartridges
Each game’s cartridge contains hardware which allows name table mirroring to be configured.
Some games don’t ever need to change mirroring, so their cartridges are hardwired to either horizontal or vertical mirroring. Other games need to dynamically switch between the two modes, so their cartridge can be configured by software to mirror horizontally or vertically. The Legend of Zelda falls into this category. Finally, some really fancy games come with extra video memory in the cartridge, which means they don’t need to mirror at all, and can scroll horizontally and vertically at the same time without any visible duplication.
A real example
On the left is an example of vertical scrolling as it would appear on the screen. On the right is a recording of the name tables, with horizontal mirroring, and the currently-visible window highlighted.
Remember, vertical scrolling itself isn’t unusual at all - just split screen vertical scrolling.
Screen Splitting
Each frame of video produced by the NES is drawn from top to bottom, one row of pixels at a time. Within each row, pixels are drawn one at a time, left to right. Mid way through drawing a frame, the game can reconfigure the PPU, which effects how the yet-to-be drawn pixels will be displayed. One common mid-frame change is to update the horizontal scroll position.
When scrolling horizontally between rooms, The Legend of Zelda always starts with scroll set to (0, 0), and renders the heads up display at the top of the screen. After the final row of pixels of the heads up display has been drawn to the screen, the horizontal scroll is changed by a value which increases slightly each frame, causing the camera to pan smoothly.
The name table view shows how the game is changing from horizontal mirroring to vertical mirroring before it starts scrolling, then back to horizontal mirroring once the transition is complete. Also, while the scroll is in progress, the top-left (and bottom-left) name table is updated to contain a copy of the room being entered. Once the scroll is finished, the game stops splitting the screen, and renders entirely from the top-left name table again.
Measuring Draw Progress
In order to split the screen at the correct position, the game needs a way of finding out how much of the current frame has been drawn. Pixel rows are rendered at a known rate, so it’s possible to tell which row of pixels is currently being drawn by counting the number of CPU cycles that have passed since the start of the frame.
There is another, more accurate technique, called “Sprite Zero Hit”.
The NES can draw 64 sprites at a time. The first sprite in video memory is referred to as “Sprite Zero”. Each frame, the first time an opaque pixel of sprite zero overlaps with an opaque pixel of the background, an event called “Sprite Zero Hit” occurs. This has the effect of setting a bit in one of the memory-mapped PPU registers, which can be checked by the CPU.
To use Sprite Zero Hit to split the screen, games place sprite zero at a vertical position near the boundary of the split, and during rendering, repeatedly check whether a Sprite Zero Hit has occurred. When Sprite Zero Hit occurs, the game changes the horizontal scroll to effect the split.
This shows a horizontal room transition with and without the background.
The brown circle which appears at the start of the transition, and vanishes at the end, is sprite zero. Looking closer at the HUD with and without the background:
Sprite zero is a discoloured bomb sprite, lined up exactly with the regular bomb sprite in the game’s HUD. Sprite zero is configured to appear behind the background, but since the black pixels in the HUD are considered transparent, the sprite zero bomb would be visible if it wasn’t strategically positioned behind the HUD bomb.
Note that the sprite zero hit occurs several pixel rows before the bottom row of the HUD. It occurs at the top pixel of the fuse of the bomb, which is 16 pixels from the bottom of the HUD. When sprite zero hit happens, the game starts counting CPU cycles, and sets the horizontal scroll after a specific number of cycles have passed.
Vertical Blanking
The majority of the time, the NES PPU is drawing pixels to the screen. There is a brief period of “downtime” in between frames during which no drawing is taking place. This is known as the “Vertical Blank” or “vblank”. Some types of PPU configuration changes can only be made during vblank.
The Scroll Register
Games change the scroll position by writing to a PPU register named PPUSCROLL
,
which is mapped at address 0x2005
. The first write to PPUSCROLL
sets the X component
of the scroll position, and the second write sets the Y component. Writes continue to
alternate in this fashion.
This lists all the non-zero writes to PPUSCROLL
during this (slow motion) 16 frame recording of
the story screen. The Y component of the scroll position is incremented once every 2 frames.
All PPUSCROLL
writes occur during vblank in this example, which causes the entire background
to scroll together.
Frame | Sub-Frame | Component | Value |
---|---|---|---|
0 | VBlank | Y | 110 |
1 | VBlank | Y | 110 |
2 | VBlank | Y | 111 |
3 | VBlank | Y | 111 |
4 | VBlank | Y | 112 |
5 | VBlank | Y | 112 |
6 | VBlank | Y | 113 |
7 | VBlank | Y | 113 |
8 | VBlank | Y | 114 |
9 | VBlank | Y | 114 |
10 | VBlank | Y | 115 |
11 | VBlank | Y | 115 |
12 | VBlank | Y | 116 |
13 | VBlank | Y | 116 |
14 | VBlank | Y | 117 |
15 | VBlank | Y | 117 |
Split Screen Scrolling
Writes to PPUSCROLL
during vblank take effect at the beginning of frame drawn immediately
after the vblank.
If the scroll position is changed while a frame is being drawn (ie. outside of vblank), it takes effect when drawing
reaches the next row of pixels. Partial horizontal scrolling works by writing to PPUSCROLL
while the PPU is drawing the last line of pixels before the scroll should happen.
Frame | Sub-Frame | Component | Value |
---|---|---|---|
0 | Pixel Row 63 | X | 72 |
1 | Pixel Row 63 | X | 76 |
2 | Pixel Row 63 | X | 80 |
3 | Pixel Row 63 | X | 84 |
4 | Pixel Row 63 | X | 88 |
5 | Pixel Row 63 | X | 92 |
6 | Pixel Row 63 | X | 96 |
7 | Pixel Row 63 | X | 100 |
8 | Pixel Row 63 | X | 104 |
9 | Pixel Row 63 | X | 108 |
10 | Pixel Row 63 | X | 112 |
11 | Pixel Row 63 | X | 116 |
12 | Pixel Row 63 | X | 120 |
13 | Pixel Row 63 | X | 124 |
14 | Pixel Row 63 | X | 128 |
15 | Pixel Row 63 | X | 132 |
When the scroll position is updated mid-frame, only the X component of the scroll position is applied. That is, the Y component of scroll positions set mid-frame are ignored. Thus, if a game wants to split the screen, and change the scroll position of part of the frame, it may only scroll horizontally.
And yet:
Believe it or not, the PPUSCROLL
register is not changed during this transition.
You may notice a 1-pixel high graphical artefact just below the HUD. This is a bug in my emulator caused by not synchronising CPU clock cycles with per-pixel rendering.
Interference with Other Registers
A second register, named PPUADDR
, mapped to 0x2006
, is used to set the current video memory
address. When the game wants to change, for example,
one of the tiles in a name table, it first writes the video memory address of the tile to PPUADDR
,
then writes the new value of the tile to PPUDATA
- a third register mapped to 0x2007
.
Writing to PPUADDR
outside of vblank (ie. while the frame is drawing) can cause graphical
artefacts. This is because the PPU circuitry affected by writing PPUADDR
is also manipulated
directly by the PPU as it retrieves tiles from video memory for the purposes of drawing them. As
drawing proceeds from the top to the bottom of the screen, and left to right within each
pixel row, the PPU effectively sets PPUADDR
to the address of the tile containing the pixel
currently being drawn. When drawing moves from one tile to another, the PPUADDR
is changed
by incrementing its current value.
Thus writing to PPUADDR
mid-frame can alter the tiles which the PPU would fetch from memory
for the duration of the current frame.
Let’s log writes to PPUADDR
during the vertical transition. Since the name table is also being
updated during the transition, logging all writes to PPUADDR
would be noisy. In the horizontal
transition, the scroll was set while drawing pixel row 63, so we’ll just look at PPUADDR
writes
during this row.
Frame | Sub-Frame | Address |
---|---|---|
0 | Pixel Row 63 | 0x2280 |
1 | Pixel Row 63 | 0x2280 |
2 | Pixel Row 63 | 0x2260 |
3 | Pixel Row 63 | 0x2260 |
4 | Pixel Row 63 | 0x2240 |
5 | Pixel Row 63 | 0x2240 |
6 | Pixel Row 63 | 0x2220 |
7 | Pixel Row 63 | 0x2220 |
8 | Pixel Row 63 | 0x2200 |
9 | Pixel Row 63 | 0x2200 |
10 | Pixel Row 63 | 0x21E0 |
11 | Pixel Row 63 | 0x21E0 |
12 | Pixel Row 63 | 0x21C0 |
13 | Pixel Row 63 | 0x21C0 |
14 | Pixel Row 63 | 0x21A0 |
15 | Pixel Row 63 | 0x21A0 |
There’s a clear pattern. Every 2 frames, the address written on pixel row 63 is decreased by 32 (0x20). But how does this translate into updating the effective scroll position?
The Real Scroll Register
Internal to the PPU, and not mapped into the CPU’s memory, is a 15 bit register which is used as both the current video memory address to access, and background scroll configuration.
When treating this value as an address, bit 14 is ignored, and bits 0-13 are treated as an address in video memory.
When treating the register as scroll configuration, different parts of its contents have different meanings, according to this table.
Bit | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Meaning | Fine Y Scroll | Name Table Select | Coarse Y Scroll | Coarse X Scroll |
Name Table Select is a value from 0 to 3, and selects the name table currently being drawn from.
Coarse X Scroll and Coarse Y Scroll give the coordinate of a tile within the selected name table. This is the tile currently being drawn.
Fine Y Scroll contains a value from 0 to 7, and specifies the current vertical offset of the row of pixels within the current tile. Tiles are 8 pixels square.
Fine X Scroll is absent from this register. There is a separate register which just contains the horizontal offset of the current pixel, but it won’t be relevant for explaining how The Legend of Zelda performs vertical scrolling.
What happens to this register when the game writes PPUADDR
? Here are the first 3 writes from the demo above.
Bit | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Meaning | Fine Y Scroll | Name Table Select | Coarse Y Scroll | Coarse X Scroll | |||||||||||
0x2280 Bits | 0 | 1 | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
0x2280 Parts | 2 | 0 | 20 | 0 | |||||||||||
0x2260 Bits | 0 | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 0 |
0x2260 Parts | 2 | 0 | 19 | 0 | |||||||||||
0x2240 Bits | 0 | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
0x2240 Parts | 2 | 0 | 18 | 0 |
Breaking the address writes into their scroll components, it’s clear what’s going on here. Every 2 frames, the Coarse Y Scroll is decremented, effecting a vertical scroll of 1 tile or 8 pixels.
The initial scroll is 0,0 during each frame of the vertical transition, and then the address is written on pixel row 63. This means the first 63 rows of pixels are drawn from the top of the selected name table, which contains the HUD background. The 64th pixel row and onwards however, are drawn with the vertical scroll applied from this address. As that vertical scroll is decremented every second frame, this gives the impression of vertical scrolling of part of the screen.
Scroll Down to Scroll Up
The Legend of Zelda can’t completely hide this trick from players. It produces a visible artefact on vertical screen transitions which you can see if you look closely. When moving up between rooms, the first frame of the scroll animation scrolls down instead. Here’s the animation in extreme slow motion.
The name table view shows what’s going on. While to players it may look like the visible area is smoothly scrolling up, the scroll transition begins by moving the visible area from the top-left name table to the bottom-left name table, which contains a copy of the room background. This is necessary, as the HUD at the top of the screen is also part of the name table, so if the visible area was to scroll up from its original position, it would scroll past the HUD.
The vertical scrolling is implemented by writing to the PPUADDR
register mid-frame, and the very
first value written is 0x2800
. 2 frames later, 0x23A0
is written, and then starts decrementing
the value by 32 every other frame.
Bit | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Meaning | Fine Y Scroll | Name Table Select | Coarse Y Scroll | Coarse X Scroll | |||||||||||
0x2800 Bits | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
0x2800 Parts | 2 | 2 | 0 | 0 | |||||||||||
0x23A0 Bits | 0 | 1 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
0x23A0 Parts | 2 | 0 | 29 | 0 |
Writing 0x2800
to PPUADDR
sets the
Name Table Select
to 2, which will render out of the bottom-left name table.
Since both scroll values are 0, it will start with the top-left
tile of this name table. However, the
Fine Y Scroll is 2, so there
is a 2-pixel vertical offset from the top of the bottom-left
name table, which is why on the very first frame of the transition,
you see a 2-pixel high black bar at the bottom of the screen.
The initial scroll setting for the transition animation is 2-pixels
below where it would need to be for the transition to be seamless.
2 frames later, 0x23A0
is written to PPUADDR
. This brings
us back to the top-left name table, and we’re rendering from the 29th
row of tiles, which is the very bottom row. Still, the
Fine Y Scroll contains a 2.
Why is it necessary to set the
Fine Y Scroll to 2?
Why couldn’t the game just write 0x0800
and 0x03A0
and not have to suffer the 2-pixel offset?
The 4 name tables occupy a 4kb region of the PPU’s address space
from 0x2000
to 0x2FFF
.
Each tile in a name table occupies a single byte of video memory (they’re really just indices into another table),
and the order of tiles and name tables
in video memory is such that the
Name Table Select,
Coarse Y Scroll and
Coarse X Scroll
comprise the offset of a tile within the name table region of memory.
That is, taking the low 12 bits of the internal PPU register,
and adding it to 0x2000
, you can find the video memory address
of a tile. This is no coincidence! This is precisely what allows
this register to be treated as both an address register, and
a scroll register.
With one caveat.
When treating it as an address register, bits 12 and 13 are treated
as part of the address. During rendering, the PPU is constantly updating
this register with the address of the tile it’s currently drawing.
As tiles located in name tables, and name tables are in the region
of memory from 0x2000
to 0x2FFF
, the PPU will be setting the register
to values within this range.
When a game writes to PPUADDR
mid frame, if it doesn’t write the address
of a tile in a name table, the PPU will attempt to read from somewhere else
in video memory. Whatever bytes it happens to read will be treated as tiles,
which will likely lead to undesirable outcomes. So every mid-frame write to
PPUADDR
must lie between 0x2000
and 0x2FFF
. Taking every number
in that range, and considering its scroll components, the value of
Fine Y Scroll will always be 2.
This limitation means that you can’t change the Fine Y Scroll mid-frame, which means that when using this trick to implement split-screen vertical scrolling, you’re constrained to scroll 8 pixels at a time, and always with a 2-pixel vertical offset from a tile boundary. The Legend of Zelda scrolls 4 pixels per frame when scrolling horizontally, but scrolls 8 pixels every 2 frames when scrolling vertically, and this explains why.
The artefact is also visible when scrolling down between rooms, but it occurs at the end of the animation instead.
Further Reading
- The NesDev Wiki is an invaluable resource for learning about NES hardware. Specifically relevant to this post are the pages about PPU Scrolling and PPU Registers.
- My still very much incomplete NES emulator is here.
Notes
Before I learnt about the internal PPU register, my emulator would display a wipe effect on vertical screen transitions in The Legend of Zelda.
The Link sprite would slide down the screen as intended, but the background would not scroll. The wipe is caused by the game gradually updating the name table to contain the new room’s graphics, but not scrolling to keep the updates off-screen.