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.

title

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:

example

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.

horizontal-scrolling

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:

To highlight the difference, here’s a scene made up of sprites and background:

sprites-and-background

Here’s the same scene with only the sprites visible:

only-sprites

And here’s the scene with only the background visible:

only-background

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:

0,0

Scrolling to (125, 181) shows a bit of each name table:

125,181

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:

342,290

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.

name-table-mirroring

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”.

vertical-mirroring

The other possible configuration is “Horizontal Mirroring”, which games use when they want to scroll vertically.

horizontal-mirroring

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.

cart

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.

scroll-demo scroll-demo-name-table

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.

horizontal-scroll-demo horizontal-scroll-demo-name-table

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.

horizontal-scroll-demo-fast horizontal-scroll-demo-sprites

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:

hud hud-sprites

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.

short-text-scroll short-text-scroll-name-table
FrameSub-FrameComponentValue
0VBlankY110
1VBlankY110
2VBlankY111
3VBlankY111
4VBlankY112
5VBlankY112
6VBlankY113
7VBlankY113
8VBlankY114
9VBlankY114
10VBlankY115
11VBlankY115
12VBlankY116
13VBlankY116
14VBlankY117
15VBlankY117

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.

short-horizontal-scroll short-horizontal-scroll-name-table
FrameSub-FrameComponentValue
0Pixel Row 63X72
1Pixel Row 63X76
2Pixel Row 63X80
3Pixel Row 63X84
4Pixel Row 63X88
5Pixel Row 63X92
6Pixel Row 63X96
7Pixel Row 63X100
8Pixel Row 63X104
9Pixel Row 63X108
10Pixel Row 63X112
11Pixel Row 63X116
12Pixel Row 63X120
13Pixel Row 63X124
14Pixel Row 63X128
15Pixel Row 63X132

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:

short-vertical-scroll short-vertical-scroll-name-table

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.

short-vertical-scroll short-vertical-scroll-name-table
FrameSub-FrameAddress
0Pixel Row 630x2280
1Pixel Row 630x2280
2Pixel Row 630x2260
3Pixel Row 630x2260
4Pixel Row 630x2240
5Pixel Row 630x2240
6Pixel Row 630x2220
7Pixel Row 630x2220
8Pixel Row 630x2200
9Pixel Row 630x2200
10Pixel Row 630x21E0
11Pixel Row 630x21E0
12Pixel Row 630x21C0
13Pixel Row 630x21C0
14Pixel Row 630x21A0
15Pixel Row 630x21A0

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.

brief-scroll-down brief-scroll-down-name-table

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.

both-scroll-vertical both-scroll-vertical-name-table

Further Reading

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.

vertical-wipe vertical-wipe-name-table

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.