On this page
  1. Parameters
  2. Related
  3. Games
  4. References
  5. Sources

The PPU takes one background position per frame from the scroll registers1, and a program that rewrites them while the frame is drawn gives the lines below the write a new position. A status bar split is the two-band case; a chain of interrupts gives several bands.

Interactive: raster-split (needs JavaScript)

Bands that scroll on their own
  • Two bandsSuper Mario Bros. (1985). One split a frame, between the status bar and the level.2The Legend of Zelda (1986). A PPUADDR rewrite at the split gives the room below its own vertical position.3
  • Several bandsSuper Mario Bros. 3 (1988). The N-Spade game scrolls three rows independently with three IRQs.4
  • Per lineSuper Metroid (1994). HDMA writes BG2's scroll on every line under the water.5Pokémon Crystal (2000). An interrupt after each line copies the next line's byte from a 144-entry table into one video register; the Magnet Train scrolls three bands.6,7

#Parameters

Super Mario Bros. 3’s card game fires its interrupts at line 32, then every 52 lines4; each one writes the next row’s horizontal scroll and sets the next count. A vertical change mid-frame needs PPUADDR writes rather than PPUSCROLL, as The Legend of Zelda does between rooms1,3.

The Game Boy’s PPU re-reads its scroll registers during each line8, and the LCD interrupt can fire in every horizontal blank9. Pokémon Crystal’s Magnet Train scrolls three bands of 47, 48, and 49 lines that way, the scenery at 4 pixels a frame while the middle band holds the train7.

Games

GameYearHowNote
Super Mario Bros.1985Two bandsone split a frame, between the status bar and the level
The Legend of Zelda1986Two bandsa PPUADDR rewrite at the split gives the room below its own vertical position
Super Mario Bros. 31988Several bandsthe N-Spade game scrolls three rows independently with three IRQs
Super Metroid1994Per lineHDMA writes BG2's scroll on every line under the water
Pokémon Crystal2000Per linean interrupt after each line copies the next line's byte from a 144-entry table into one video register; the Magnet Train scrolls three bands
Every claim about this mechanic, by game (7)
Super Mario Bros. 3 1988In the N-Spade card game three IRQs, at scanlines 32, 84, and 136, each give one row its own horizontal scroll and set the next IRQ 52 lines on, and a fourth restores the status bar, so three bands of the screen slide independently.Code
Pokémon Crystal 2000The game sets the STAT interrupt to fire at every horizontal blank, and the handler copies the byte for the current line from a 144-entry table, wLYOverrides, into whichever video register hLCDCPointer names; while that pointer is 0 the handler does nothing. The handler runs in the horizontal blank after line LY, so entry n takes effect from line n + 1, and line 0 shows the register's value from before the frame (the Magnet Train writes its top band's scroll to hSCX for that line).Code
The Magnet Train ride fills the per-line table with three bands each frame, 47 lines, 48 lines, and 49 lines, and points the handler at SCX; the top and bottom bands take twice a running offset that moves 2 a frame, so the scenery scrolls 4 pixels a frame, while the middle band holds the train's own position.Code
generalTwo registers, SCY and SCX ($FF42-$FF43), place the 160 by 144 view in the 256 by 256 background map, and the PPU re-reads them during the line, so a write between lines moves the lines below it.Wiki
LY ($FF44) counts the line being drawn, 0 to 153, with 144 to 153 in vertical blank. The STAT register ($FF41) can raise the LCD interrupt when LY equals LYC ($FF45) or when the PPU enters horizontal blank (mode 0), vertical blank (mode 1), or the OAM scan (mode 2).Wiki
A nametable is 1,024 bytes, 32 by 30 tiles of 8 by 8 pixels plus a 64-byte attribute table, and the PPU addresses four logical nametables in a 2 by 2 grid from $2000.Wiki
Without a split, a program sets the background's position once per frame by writing X and Y to $2005 and the coordinates' high bits to $2000; a split changes the scroll position while the frame is drawn, for instance by writing $2006 twice.Wiki

References

  1. ^ abWithout a split, a program sets the background's position once per frame by writing X and Y to $2005 and the coordinates' high bits to $2000; a split changes the scroll position while the frame is drawn, for instance by writing $2006 twice.WikiNESdev Wiki, “PPU scrolling”revised
    How we know
    If the screen does not use split-scrolling, setting the position of the background requires only writing the X and Y coordinates to $2005 and the high bit of both coordinates to $2000.
    Frequent pitfalls · archived copy ↗
    By writing twice to $2006, the second write causes an immediate full reload of v from t, allowing you to update the vertical scroll in the middle of the screen.
    Split X/Y scroll · archived copy ↗

    Checked against the evidence by its author, an AI model, 2026-10-21.

    Revised after a check by a second AI model, 2026-10-21.

    In the claim ledger

  2. ^Each frame the NMI handler waits for the sprite 0 flag to clear, spins until the sprite 0 hit, waits a short loop for horizontal blank, and only then writes the level's scroll, so the status bar above the hit stays still.CodeXkeeper0/smb1
    How we know
    159	LDA Sprite0HitDetectFlag                     ; check for flag here
    ⋮
    162	LDA PPU_STATUS                               ; wait for sprite 0 flag to clear, which will
    163	AND #%01000000                               ; not happen until vblank has ended
    ⋮
    170Sprite0Hit:
    171	LDA PPU_STATUS                               ; do sprite #0 hit detection
    172	AND #%01000000
    173	BEQ Sprite0Hit
    174	LDY #$14                                     ; small delay, to wait until we hit horizontal blank time
    ⋮
    179	LDA HorizontalScroll                         ; set scroll registers from variables
    180	STA PPU_SCROLL_REG
    ⋮
    182	STA PPU_SCROLL_REG
    
    src/prg.asm lines 159–183 at 1cc3c92 · upstream ↗

    Checked against the evidence by its author, an AI model, 2026-10-21.

    Checked by a second AI model, 2026-10-21.

    In the claim ledger

  3. ^ abWhen a mode needs the split, the NMI handler waits for the sprite 0 hit under the status bar and then sets the scroll for the room below; while the screen scrolls vertically between rooms it rewrites PPUADDR ($2006) mid-frame instead of PPUSCROLL.Codealdonunez/zelda1-disassembly
    How we know
    447    ; Wait for Sprite 0 hit, if needed.
    448    LDA IsSprite0CheckActive
    449    BEQ @CheckScroll
    450    LDA #$05
    451    JSR SwitchBank
    452    JSR WaitAndScrollToSplitBottom
    
    src/Z_07.asm lines 447–452 at 50a1c86 · upstream ↗
    1263WaitAndScrollToSplitBottom:
    1264    LDA PpuStatus_2002          ; Wait for Sprite 0 Hit.
    1265    AND #$40
    1266    BEQ WaitAndScrollToSplitBottom
    
    src/Z_05.asm lines 1263–1266 at 50a1c86 · upstream ↗
    1299    ; Scrolling vertically, will set PPUADDR instead of PPUSCROLL.
    1300    ; Wait about 666 cycles.
    ⋮
    1313    ; Set PPUADDR mid-frame to 
    1314    ; achieve split-frame scrolling.
    1315    LDA VScrollAddrHi
    1316    LDY VScrollAddrLo
    1317    STA PpuAddr_2006
    1318    STY PpuAddr_2006
    
    src/Z_05.asm lines 1299–1320 at 50a1c86 · upstream ↗

    Checked against the evidence by its author, an AI model, 2026-10-21.

    Checked by a second AI model, 2026-10-21.

    In the claim ledger

  4. ^ abIn the N-Spade card game three IRQs, at scanlines 32, 84, and 136, each give one row its own horizontal scroll and set the next IRQ 52 lines on, and a fourth restores the status bar, so three bands of the screen slide independently.CodeSouthbird's SMB3 disassemblyrevised
    How we know
    2602	; Fire on scanline 32
    2603	LDA #$20
    2604	STA MMC3_IRQCNT
    2605	STA MMC3_IRQLATCH
    2606	STA MMC3_IRQENABLE
    
    PRG/prg022.asm lines 2602–2606 at 09b1bd8 · upstream ↗
    2493	; Based on the current Raster_State, set next scanline delay
    2494	LDA Roulette_RasterDiv,Y
    2495	STA MMC3_IRQCNT
    
    PRG/prg031.asm lines 2491–2496 at 09b1bd8 · upstream ↗
    2474	; This table tells how many lines to skip to the next row.
    ⋮
    2477Roulette_RasterDiv:	; Raster_State = 0,1,2
    2478	.byte $34, $34, $34
    
    PRG/prg031.asm lines 2474–2478 at 09b1bd8 · upstream ↗
    2550	LDA Roulette_PosHi,Y	 ; Get position for this row
    ⋮
    2554	LDA Roulette_Pos,Y	 ; Get horizontal scroll position for this row
    2555	STA PPU_SCROLL	 ; Store the horizontal
    2556	LDA #$00	 ; 
    2557	STA PPU_SCROLL	 ; Vertical = 0
    2558	INY		 ; 
    2559	STY Raster_State ; Raster_State++
    
    PRG/prg031.asm lines 2550–2559 at 09b1bd8 · upstream ↗

    Checked against the evidence by its author, an AI model, 2026-10-21.

    Revised after a check by a second AI model, 2026-10-21.

    In the claim ledger

  5. ^In rooms whose water is set to wave, an indirect HDMA channel writes BG2's X scroll on every line. Above the water each line points at the plain scroll; below it the lines walk through a ring of 16 values, the scroll plus a displacement of 0, 1, 1, 0, 0, −1, −1, 0 repeating, so the background ripples a pixel either way in an 8-line pattern that shifts one line every 6 frames.CodeInsaneFirebat/sm_disassembly
    How we know
    LDA.W FX_LiquidOptions … AND.W #$0002 … BEQ .notWavy … JSL.L Spawn_HDMAObject … db $42,$0F … dw InstList_Water_BG2_Xscroll
    src/bank_88.asm#L7229-L7234 · upstream ↗
    982;;         $0F: BG2 X scroll (16-bit, write-twice)
    
    src/bank_88.asm lines 980–982 at 7af131e · upstream ↗
    7303;;; $C46E: Wave displacement table - water ;;;
    7304WaveDisplacementTable_Water:
    
    src/bank_88.asm lines 7303–7306 at 7af131e · upstream ↗
    7524Setup_Water_BG2_Xscroll_DataTable_Wavy:
    ⋮
    7554  .loop:
    
    src/bank_88.asm lines 7523–7570 at 7af131e · upstream ↗
    7468; the calculation 1FFh - [A] at $C5CE is equivalent to FFh + (Y position of screen on FX)
    
    src/bank_88.asm lines 7465–7472 at 7af131e · upstream ↗
    8365;;; $CF46: Indirect HDMA table - water BG2 X scroll ;;;
    8366IndirectHDMATable_WaterBG2XScroll:
    ⋮
    8368    db $81 : dw WaterBG2XScrollHDMADataTable-4
    
    src/bank_88.asm lines 8365–8368 at 7af131e · upstream ↗
    8623    db $81 : dw WaterBG2XScrollHDMADataTable
    ⋮
    8638    db $81 : dw WaterBG2XScrollHDMADataTable+$1E
    
    src/bank_88.asm lines 8623–8638 at 7af131e · upstream ↗

    Checked against the evidence by its author, an AI model, 2026-10-21.

    Checked by a second AI model, 2026-10-21.

    In the claim ledger

  6. ^The game sets the STAT interrupt to fire at every horizontal blank, and the handler copies the byte for the current line from a 144-entry table, wLYOverrides, into whichever video register hLCDCPointer names; while that pointer is 0 the handler does nothing. The handler runs in the horizontal blank after line LY, so entry n takes effect from line n + 1, and line 0 shows the register's value from before the frame (the Magnet Train writes its top band's scroll to hSCX for that line).Codepret/pokecrystalrevised
    How we know
    111	ld a, STAT_MODE_0
    112	ldh [rSTAT], a
    
    home/init.asm lines 111–112 at 97187cd · upstream ↗
    46SECTION "lcd", ROM0[$0048]
    47	jp LCD
    
    home/header.asm lines 46–47 at 97187cd · upstream ↗
    12LCD::
    13	push af
    14	ldh a, [hLCDCPointer]
    15	and a
    16	jr z, .done
    ⋮
    20	ldh a, [rLY]
    21	ld c, a
    22	ld b, HIGH(wLYOverrides)
    23	ld a, [bc]
    24	ld b, a
    25	ldh a, [hLCDCPointer]
    26	ld c, a
    ⋮
    
    home/lcd.asm lines 12–31 at 97187cd · upstream ↗
    3628wLYOverrides:: ds SCREEN_HEIGHT_PX
    
    ram/wram.asm line 3628 at 97187cd · upstream ↗
    997def SCREEN_HEIGHT_PX equ 144 ; height of screen in pixels
    
    constants/hardware.inc line 997 at 97187cd · upstream ↗

    Checked against the evidence by its author, an AI model, 2026-10-21.

    Revised after a check by a second AI model, 2026-10-21.

    In the claim ledger

  7. ^ abThe Magnet Train ride fills the per-line table with three bands each frame, 47 lines, 48 lines, and 49 lines, and points the handler at SCX; the top and bottom bands take twice a running offset that moves 2 a frame, so the scenery scrolls 4 pixels a frame, while the middle band holds the train's own position.Codepret/pokecrystal
    How we know
    86MagnetTrain_UpdateLYOverrides:
    87	ld hl, wLYOverridesBackup
    88	ld c, 6 * TILE_WIDTH - 1
    89	ld a, [wMagnetTrainOffset]
    90	add a
    91	ldh [hSCX], a
    92	call .loadloop
    93	ld c, 6 * TILE_WIDTH
    94	ld a, [wMagnetTrainPosition]
    95	call .loadloop
    96	ld c, 6 * TILE_WIDTH + 1
    97	ld a, [wMagnetTrainOffset]
    ⋮
    
    engine/events/magnet_train.asm lines 86–114 at 97187cd · upstream ↗
    229MagnetTrain_InitLYOverrides:
    ⋮
    238	ld a, LOW(rSCX)
    239	ldh [hLCDCPointer], a
    
    engine/events/magnet_train.asm lines 229–240 at 97187cd · upstream ↗
    50	call MagnetTrain_UpdateLYOverrides
    51	call PushLYOverrides
    52	call DelayFrame
    53	jr .loop
    
    engine/events/magnet_train.asm lines 50–53 at 97187cd · upstream ↗
    1008def TILE_WIDTH  equ  8 ; width of tile in pixels
    
    constants/hardware.inc line 1008 at 97187cd · upstream ↗

    Checked against the evidence by its author, an AI model, 2026-10-21.

    Checked by a second AI model, 2026-10-21.

    In the claim ledger

  8. ^Two registers, SCY and SCX ($FF42-$FF43), place the 160 by 144 view in the 256 by 256 background map, and the PPU re-reads them during the line, so a write between lines moves the lines below it.WikiPan Docs, “Pan Docs: LCD Position and Scrolling”
    How we know
    These two registers specify the top-left coordinates of the visible 160×144 pixel area within the 256×256 pixels BG map.
    FF42–FF43 — SCY, SCX · archived copy ↗
    The scroll registers are re-read on each tile fetch, except for the low 3 bits of SCX, which are only read at the beginning of the scanline
    Mid-frame behavior · archived copy ↗

    Checked against the evidence by its author, an AI model, 2026-10-21.

    Checked by a second AI model, 2026-10-21.

    In the claim ledger

  9. ^LY ($FF44) counts the line being drawn, 0 to 153, with 144 to 153 in vertical blank. The STAT register ($FF41) can raise the LCD interrupt when LY equals LYC ($FF45) or when the PPU enters horizontal blank (mode 0), vertical blank (mode 1), or the OAM scan (mode 2).WikiPan Docs, “Pan Docs: LCD Status Registers” · Pan Docs, “Pan Docs: Rendering Overview”revised
    How we know
    LY can hold any value from 0 to 153, with values from 144 to 153 indicating the VBlank period.
    FF44 — LY · archived copy ↗
    When both values are identical, the “LYC=LY” flag in the STAT register is set, and (if enabled) a STAT interrupt is requested.
    FF45 — LYC · archived copy ↗
    Mode 2 OAM scan … Mode 0 Horizontal blank … Mode 1 Vertical blank
    PPU modes (diagram) · archived copy ↗
    LYC int select (Read/Write): If set, selects the LYC == LY condition for the STAT interrupt. Mode 2 int select (Read/Write): If set, selects the Mode 2 condition for the STAT interrupt. Mode 1 int select (Read/Write): If set, selects the Mode 1 condition for the STAT interrupt. Mode 0 int select (Read/Write): If set, selects the Mode 0 condition for the STAT interrupt.
    FF41 — STAT · archived copy ↗

    Checked against the evidence by its author, an AI model, 2026-10-21.

    Revised after a check by a second AI model, 2026-10-21.

    In the claim ledger

Sources

Code

Secondary