Skip to content

Feeding New Columns

As the NES scrolls sideways, write a fresh column of tiles into the offscreen nametable each time the view crosses a tile boundary — so a level far wider than two screens rolls past seamlessly.

Code status: illustrative code. Adapt the listing to your program and check it on your target machine. This page links no assembly or execution record for the listing itself.

Overview

Two nametables give 512 pixels of scrolling — but a real level is thousands of pixels wide, so the PPU has to recycle that memory. The trick is to run just ahead of the view: every time scrolling crosses an 8-pixel tile boundary, one new column of tiles has appeared at the leading edge, and you write it into the nametable the view is about to reach — the one currently offscreen behind the player. Do it a column at a time, in vblank, and the seam is invisible: the world scrolls forever through two nametables’ worth of memory that never stops being rewritten.

Code

6502 assembly
; =============================================================================
; FEEDING NEW COLUMNS - NES
; On each 8-px scroll step, write one 30-tile column into the offscreen nametable
; Runs in the NMI (vblank); the column data is prepared during the frame
; =============================================================================

PPUCTRL   = $2000
PPUSTATUS = $2002
PPUADDR   = $2006
PPUDATA   = $2007

.segment "ZEROPAGE"
col_addr_hi: .res 1        ; $2006 target for the top of the column
col_addr_lo: .res 1
col_buf:     .res 30       ; the 30 tiles, prepared in the main loop

.segment "CODE"

; --- Call in the NMI when a new column is due ---
feed_column:
    lda ppu_ctrl_shadow
    ora #%00000100         ; preserve other settings; increment32
    sta PPUCTRL

    bit PPUSTATUS          ; reset the write latch
    lda col_addr_hi
    sta PPUADDR            ; point at the top tile of the target column...
    lda col_addr_lo
    sta PPUADDR

    ldx #0
:   lda col_buf,x
    sta PPUDATA            ; each write steps 32 bytes -> straight down the column
    inx
    cpx #30
    bne :-

    ; ... write this column's attribute bytes too (each attribute byte covers4x4 tiles; each quadrant covers2x2) ...

    lda ppu_ctrl_shadow    ; restore the caller's original increment and modes
    sta PPUCTRL
    rts

Trade-offs

Aspect Cost
CPU 30 PPUDATA writes per tile-column crossed (plus attributes) — sized to fit vblank
Memory A column buffer, and the level map to source tiles from
Limitation ~2270 vblank cycles cap what you can write; fast scroll may need two columns a frame

When to use: Any horizontally scrolling level wider than two screens.

When to avoid: A world that fits in the two nametables outright — then just scroll, no feeding.

The +32 increment trick

Tiles in a nametable are stored row by row, so consecutive addresses run across the screen — wrong for writing a vertical column. The fix is PPUCTRL bit 2: set it and every PPUDATA write advances the VRAM address by 32 instead of 1, stepping straight down a column. Point $2006 at the top tile, blast 30 bytes, and you’ve drawn a full-height column with a plain loop. Set the increment back to 1 afterwards, or your next normal write walks down the screen too.

Where the column goes

The address is the leading edge translated into nametable space. As the view scrolls right, the column entering from the right belongs 32 tiles (one screen) ahead of the left edge — which, once you pass the nametable’s width, wraps into the other nametable. So the target address cycles through both nametables’ column slots as you travel, always writing into the strip that’s offscreen. Getting this wrap right — and matching the attribute writes, which cover a coarser 32×32-pixel grid with2×2-tile quadrants inside each byte; a newly exposed quadrant can need an update before four columns have passed — is the fiddly heart of a scroll engine.

Prepare in the frame, write in vblank

There isn’t time to decide what to draw during vblank — only to draw it. So the pattern splits in two: during the visible frame the main loop reads the level map, works out the next column’s 30 tiles and its attributes, and stashes them in a buffer; then the NMI does nothing but stream that buffer to the PPU. Vblank is roughly 2270 cycles, enough for a column and its attributes before accounting for OAM DMA, audio, the NMI wrapper and other queued writes — but decode work done there instead of in the buffer is how a scroll engine runs out of vblank and tears.

Patterns: Nametables and Scrolling, Sprite 0 Split

Vault: PPU | Nintendo Entertainment System

The caller supplies one shared ppu_ctrl_shadow, a valid top-of-column VRAM address and a complete30-byte buffer. Main prepares data and address first, then publishes a ready byte last; NMI consumes only a published buffer and acknowledges it. Reuse or double-buffer it after that acknowledgement. The tile loop does not supply attribute updates; merge each changed2×2 quadrant without destroying the other bits. Restore scroll after all $2006/$2007 writes. Measure the whole NMI budget rather than assuming this loop can be added for free.

Explore the cross-platform scrolling family for camera coordinates, seams and implementations on other machines.