Status bar split
Keeping a status bar fixed while the playfield scrolls, by changing the display partway through a frame or using a separate layer.
Games keep a status bar fixed over a scrolling playfield in several ways. On the NES, the program can change scroll after a sprite 0 hit1, wait for a mapper’s scanline IRQ2, or draw the meters as sprites over the playfield3. The Game Boy’s window layer has its own tile map and does not scroll with the background4.
#Implementations
#NES
Super Mario Bros. places sprite 0 behind the background under the status bar, then its NMI handler waits for the hit before writing the level scroll5,6. The Legend of Zelda also waits for sprite 0 beneath the map and item bar; during vertical room scrolling, it writes PPUADDR mid-frame to set the lower area’s position7.
Super Mario Bros. 3 loads the MMC3 scanline counter with 192 scanlines8 each frame. Its IRQ selects the status bar’s nametable rows and tile banks8.
Super Mario World uses a different SNES interrupt setup in Mode 7 boss rooms. The top of the frame uses Mode 1 for the status bar; an IRQ changes to Mode 7 and writes its scroll below the bar9. The bar ends at line 36 in most of those rooms and line 45 in Morton and Roy’s rooms, and Bowser’s room skips the bar IRQ9.
#Game Boy
The Legend of Zelda: Link’s Awakening places its inventory bar in the window at line 128, leaving the bottom 16 lines for it10. The 1993 release sets window X to 6; the DX disassembly sets it to 710. Metroid II: Return of Samus sets the window to line 136 for the HUD’s bottom 8 lines and raises it to line 128 for save and item messages11. In the Queen’s room, the HUD moves to the background because her head uses the window11.
#Meters without a split
Mega Man 2 draws each energy meter as a column of 7 sprites, so it does not change the playfield scroll3.
- Sprite overlayNESMega Man 2 (1988). No split screen; the energy meters are drawn as sprites.3
- Sprite 0 hitNESSuper Mario Bros. (1985). The status bar holds still while the level scrolls under it.6,5The Legend of Zelda (1986). Waits for sprite 0 under the map and item bar; scrolls vertically between rooms by rewriting PPUADDR mid-frame.7
- Scanline IRQNES, SNESSuper Mario Bros. 3 (1988). The MMC3's scanline counter fires an IRQ above the status bar.8Super Mario World (1990). In the Mode 7 boss rooms but Bowser's an interrupt at line 36 switches from Mode 1 to Mode 7 below the bar.9
- Window layerGame Boy familyMetroid II: Return of Samus (1991). The HUD is the window's bottom 8 lines, raised to 16 for save and item messages; the Queen's head takes the window in her room.11The Legend of Zelda: Link's Awakening (1993). The inventory bar is the window at line 128, the bottom 16 lines.10
#Parameters
Super Mario Bros. sets sprite 0 to Y $18, tile $FF, attributes $23, X $585, behind the background, so the hit can fire without the sprite showing5. The handler first waits for the previous frame’s flag to clear, then spins on bit 6 of $2002, then runs a short delay before writing the scroll6.
Games
| Game | Year | How | Note |
|---|---|---|---|
| Mega Man 2 | 1988 | Sprite overlay NES | no split screen; the energy meters are drawn as sprites |
| Super Mario Bros. | 1985 | Sprite 0 hit NES | the status bar holds still while the level scrolls under it |
| The Legend of Zelda | 1986 | Sprite 0 hit NES | waits for sprite 0 under the map and item bar; scrolls vertically between rooms by rewriting PPUADDR mid-frame |
| Super Mario Bros. 3 | 1988 | Scanline IRQ NES, SNES | the MMC3's scanline counter fires an IRQ above the status bar |
| Super Mario World | 1990 | Scanline IRQ NES, SNES | in the Mode 7 boss rooms but Bowser's an interrupt at line 36 switches from Mode 1 to Mode 7 below the bar |
| Metroid II: Return of Samus | 1991 | Window layer Game Boy family | the HUD is the window's bottom 8 lines, raised to 16 for save and item messages; the Queen's head takes the window in her room |
| The Legend of Zelda: Link's Awakening | 1993 | Window layer Game Boy family | the inventory bar is the window at line 128, the bottom 16 lines |
Every claim about this mechanic, by game (13)
References
- ^A sprite 0 hit happens when an opaque pixel of sprite 0 overlaps an opaque background pixel; it sets bit 6 of
PPUSTATUS($2002), which stays set until the pre-render line, so a frame reports only its first hit.WikiNESdev Wiki, “PPU OAM”How we know
While the PPU is drawing the picture, when an opaque pixel of sprite 0 overlaps an opaque pixel of the background, this is a sprite 0 hit. … Bit 6 of PPUSTATUS ($2002) is cleared to 0 at dot 1 of the pre-render line. This means only the first sprite 0 hit in a frame can be detected.
Sprite 0 hits · archived copy ↗ Checked against the evidence by its author, an AI model, 2026-10-21.
Checked by a second AI model, 2026-10-21.
- ^The MMC3 mapper counts scanlines by watching the PPU's address line A12, reloads its counter from a latch at zero, and raises an IRQ when the counter reaches zero with IRQs enabled.WikiNESdev Wiki, “MMC3”
How we know
The MMC3 scanline counter is based entirely on PPU A12 … When the IRQ is clocked (filtered A12 0→1), the counter value is checked - if zero or the reload flag is true, it's reloaded with the IRQ latched value at $C000; otherwise, it decrements. … If the IRQ counter is zero and IRQs are enabled ($E001), an IRQ is triggered.
IRQ Specifics · archived copy ↗ Checked against the evidence by its author, an AI model, 2026-10-21.
Checked by a second AI model, 2026-10-21.
- ^ abcMega Man 2 draws its energy meters as sprites, not a split screen; each meter is a column of 7 sprites from Y
$18down to$48, each showing 0-4 units, at X$10for the weapon,$18for life, and$28for a boss.Codelsmmega/mm2How we know
473 vertical_bar_base_ycoord = $18 474 hp_bar_xcoord = $18 475 weapon_bar_xcoord = $10 476 boss_bar_xcoord = $28
home/sprites.asm lines 473–476 at 94f0bf3 · upstream ↗ 530_draw_bar_tile: 531 LDY #6 ⋮ 533@loop: 534 LDA @vertical_bar_ycoord_table, Y 535 STA aoam_y, X 536 SEC 537 LDA z:z00 538 SBC #4
home/sprites.asm lines 530–545 at 94f0bf3 · upstream ↗ 572@vertical_bar_ycoord_table: 573 .BYTE vertical_bar_base_ycoord + 0 ⋮ 579 .BYTE vertical_bar_base_ycoord + 48
home/sprites.asm lines 572–579 at 94f0bf3 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-21.
Checked by a second AI model, 2026-10-21.
- ^The window is a second background layer whose top-left corner sits at screen position (WX − 7, WY), set by WY and WX (
$FF4A-$FF4B); it can't be scrolled and always starts from its tile map's top-left tile. From the line where LY equals WY, the PPU switches from the background to the window when the line reaches WX, drawing the window's current row, and that row advances once for each line the window is drawn on; at WX 7 and WY 0 it covers the whole background.WikiPan Docs, “Pan Docs: Tile Maps” · Pan Docs, “Pan Docs: Window”revisedHow we know
The content of the Window is not scrollable; it is always displayed starting at the top left tile of its tile map. The only way to adjust the Window is by modifying its position on the screen, which is done via the WX and WY registers. The screen coordinates of the top left corner of the Window are (WX-7,WY).
Window · archived copy ↗ These two registers specify the on-screen coordinates of the Window’s top-left pixel. The Window is visible (if enabled) when WX and WY are in the range [0; 166] and [0; 143] respectively. Values WX=7, WY=0 place the Window at the top left of the screen, completely covering the background.
FF4A–FF4B — WY, WX · archived copy ↗ At the beginning of each scanline, if the value of WY is equal to LY, the Y condition becomes true (and remains so for subsequent scanlines). … When this counter is equal to WX, if the Y condition is true and the Window enable bit is set in LCDC, background rendering is reset, beginning anew from the active row of the Window’s tilemap. The coordinate of the active Window row is then incremented.
Window rendering criteria · archived copy ↗ If intending to hide the Window for part of the screen (e.g. to have a status bar at the top of the screen instead of the bottom), hide it by setting WX to a high value rather than writing to LCDC.
Window mid-frame behavior · 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.
- ^ abcdThe game sets sprite 0 to OAM bytes
$18,$FF,$23,$58, so tile$FFsits at Y$18and X$58behind the background in sprite palette 3.CodeXkeeper0/smb1How we know
2118Sprite0Data: 2119 .db $18, $ff, $23, $58
src/prg.asm lines 2118–2119 at 1cc3c92 · upstream ↗ 2237 LDY #$03 ; set up sprite #0 2238ISpr0Loop: 2239 LDA Sprite0Data,y 2240 STA Sprite_Data,y ⋮ 2245 INC Sprite0HitDetectFlag ; set sprite #0 check flag
src/prg.asm lines 2236–2245 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.
- ^ abcEach 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.
- ^ 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 ofPPUSCROLL.Codealdonunez/zelda1-disassemblyHow 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.
- ^ abcEach frame the NMI handler loads the MMC3's scanline counter with 192, and the IRQ it raises points the PPU at the status bar's nametable rows and swaps in the status bar's tile banks.CodeSouthbird's SMB3 disassembly
How we know
1595 ; This sets the status bar scroll fix for everything after the title screen! 1596 ; At scanline 192, the name table scroll is fixed to always display the status bar 1597 LDA #192 ; A = 192 1598 STA MMC3_IRQCNT ; Store 192 into the IRQ count 1599 STA MMC3_IRQLATCH ; Store it into the latch (will be used later) 1600 STA MMC3_IRQENABLE ; Start the IRQ counter 1601 CLI ; Enable maskable interrupts
PRG/prg031.asm lines 1595–1601 at 09b1bd8 · upstream ↗ 2197 ; Because vertical scroll will not change after frame begins (second write to 2198 ; PPU_SCROLL will always be unused until next frame), the hack for MMC3 split 2199 ; vertical scrolling is to change the nametable address that the PPU is reading 2200 ; at to where we would like it to be... 2201 LDY #$07 2202 STY PPU_VRAM_ADDR 2203 STX PPU_VRAM_ADDR ; ... so we're now reading at $0700
PRG/prg031.asm lines 2197–2203 at 09b1bd8 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-21.
Checked by a second AI model, 2026-10-21.
- ^ abcIn the Mode 7 boss rooms other than Bowser's, the NMI sets Mode 1 for the status bar at the top of the frame, and an interrupt at the line where the bar ends switches to Mode 7 and writes the Mode 7 layer's scroll; the bar ends at line 36, or 45 in Morton's and Roy's rooms. Bowser's room skips the status bar interrupt and goes straight to Mode 7.CodeSMWDisXrevised
How we know
379Mode7Lagging: ; Transfer various RAM mirrors to the registers 380 LDA.B #!HW_BG_BG3Pri|!HW_BG_Mode1 381 STA.W HW_BGMODE
bank_00.asm lines 379–381 at 30643c7 · upstream ↗ 413 LDA.W IRQNMICommand ;\ 414 LSR A ;| Branch if not in Bowser's room. 415 BCC + ;/ ⋮ 420 LDA.B #!HW_TIMEN_NMI|!HW_TIMEN_JoyRead ;\ Skip the status bar IRQ and immediately prepare the registers after. 421 JMP CODE_0083F3 ;/
bank_00.asm lines 413–421 at 30643c7 · upstream ↗ 432 LDY.B #!StatusBarHeight+9 ;/ Scanline the status bar ends at in Morton/Roy's rooms. 433 + JMP CODE_008294 ; Prepare IRQ, and set up a couple more registers.
bank_00.asm lines 423–433 at 30643c7 · upstream ↗ 505CODE_0083F3: ; IRQ done; wait for H-blank, then update registers. ⋮ 510 LDA.B #!HW_BG_Mode7 511 STA.W HW_BGMODE 512 LDA.B Mode7XPos 513 STA.W HW_BG1HOFS ⋮ 515 STA.W HW_BG1HOFS ⋮ 517 STA.W HW_BG1VOFS ⋮ 519 STA.W HW_BG1VOFS
bank_00.asm lines 505–519 at 30643c7 · upstream ↗ 57!StatusBarHeight = 36constants.asm line 57 at 30643c7 · 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.
- ^ abcThe inventory bar along the bottom of the screen is the window layer. Setting it up stores window Y
$80(line 128), which leaves the bar the bottom 16 lines, and window X 7 in DX (6 in the 1993 release, one pixel further left); each gameplay frame copies the stored Y to the window register.Codezladx/LADX-Disassembly · “Legend of Zelda, The - Link's Awakening (USA, Europe) (Rev 2)”How we know
825InitializeInventoryBar:: 826 ; Set the window to the bottom of the screen
src/code/bank1.asm lines 825–830 at 2a5c2c0 · upstream ↗ 238.renderInteractiveFrame ⋮ 240 ; Update graphics registers from game values
src/code/home/render_loop.asm lines 238–242 at 2a5c2c0 · upstream ↗ In the cartridge (“Legend of Zelda, The - Link's Awakening (USA, Europe) (Rev 2)”): the 1993 release's setup, bank 1: ld a,$80; ld [$DB9A],a; ld a,$06; ldh [$4B],a; the 1993 release's per-frame copy, bank 0: ld a,[$DB9A]; ldh [$4A],a. We don't reproduce the bytes.
Checked against the evidence by its author, an AI model, 2026-10-21.
Checked by a second AI model, 2026-10-21.
- ^ abcThe HUD is the window layer, set to X 7. The main game mode's handler sets window Y to
$88(line 136, the bottom 8 lines), or$80(16 lines) to show a message while Samus touches a save point or collects a major item. In the Queen's room the HUD moves to the background layer, because her head uses the window.Codealex-west/M2RoSHow we know
2946; Handle window height, save text, earthquake, low heath beep, fade in, and Metroid Queen cry ⋮ 2970 ld a, $88 ; Default window position 2971 ldh [rWY], a ⋮ 2981 cp $0b ; Check if not a common item or refill ⋮ 2983 ld a, $80 ; Higher window position 2984 ldh [rWY], a ⋮ 2987 ; Touching a save point 2988 ld a, $80 ; Higher window position 2989 ldh [rWY], a
SRC/bank_001.asm lines 2946–2989 at 031aea3 · upstream ↗ 72 ; (the HUD in the Queen's room is on the normal BG layer, since her head uses the window layer) ⋮ 77 ld a, $07 78 ldh [rWX], a 79 ld hl, vramDest_statusBar
SRC/bank_001.asm lines 71–79 at 031aea3 · upstream ↗ 689 ; Handle window height, save sprite text, earthquake, low heath beep, fade in, and Metroid Queen cry 690 call miscIngameTasks_longJump
SRC/bank_000.asm lines 683–690 at 031aea3 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-21.
Checked by a second AI model, 2026-10-21.
Sources
Code
- lsmmega. 2026. “mm2: Mega Man 2 disassembly (lsmmega/mm2).” Commit 94f0bf3. lsmmega/mm2 @ 94f0bf3 · licence, all uses
- doppelganger and Xkeeper. 2022. “smb1: Super Mario Bros. disassembly for asm6f (Xkeeper0/smb1).” Commit 1cc3c92. Xkeeper0/smb1 @ 1cc3c92 · licence, all uses
- Núñez, Aldo. 2021. “zelda1-disassembly: A complete disassembly of The Legend of Zelda.” Commit 50a1c86. aldonunez/zelda1-disassembly @ 50a1c86 · licence, all uses
- Southbird. 2025. “smb3: Super Mario Bros. 3 disassembly for NESASM (captainsouthbird/smb3).” Commit 09b1bd8. captainsouthbird/smb3 @ 09b1bd8 · licence, all uses
- IsoFrieze, kaizoman666, mikeyk, and loveemu. 2026. “SMWDisX: Super Mario World disassembly (IsoFrieze/SMWDisX).” Commit 30643c7. IsoFrieze/SMWDisX @ 30643c7 · licence, all uses
- zladx contributors. 2026. “LADX-Disassembly: Disassembly of Link's Awakening DX.” zladx. Commit 2a5c2c0. zladx/LADX-Disassembly @ 2a5c2c0 · licence, all uses
- n.d. “Legend of Zelda, The - Link's Awakening (USA, Europe) (Rev 2).” · licence, all uses
- alex-west. 2026. “M2RoS: Return of Samus Disassembly.” Commit 031aea3. alex-west/M2RoS @ 031aea3 · licence, all uses
Secondary
- NESdev Wiki contributors. n.d. “PPU OAM.” NESdev Wiki. Version oldid 23161. original · archived · licence, all uses
- NESdev Wiki contributors. n.d. “MMC3.” NESdev Wiki. Version oldid 23702. original · archived · licence, all uses
- Pan Docs contributors. n.d. “Pan Docs: Tile Maps.” Pan Docs (gbdev.io). original · archived · licence, all uses
- Pan Docs contributors. n.d. “Pan Docs: Window.” Pan Docs (gbdev.io). original · archived · licence, all uses