328. Ppuctrl temp nametable bits

Copy PPUCTRL base-nametable selection into temp_vram_addr.

Lesson 328 of 356 · tests/chapter_13_scrolling/test_328_ppuctrl_temp_nametable_bits.py

File to update

emulator/ppu/ppu.py

Reference documentation

https://www.nesdev.org/wiki/PPU_scrolling#$2000_(PPUCTRL)_write
https://www.nesdev.org/wiki/PPU_registers

Why this step exists

The PPU scrolling address is assembled from several CPU-visible register writes. The project already handles most of the $2005 PPUSCROLL pieces:

coarse X -> temp_vram_addr bits 0-4
coarse Y -> temp_vram_addr bits 5-9
fine Y   -> temp_vram_addr bits 12-14
fine X   -> separate fine_x field

The missing piece is the logical base-nametable selection

PPUCTRL bits 0-1 -> temp_vram_addr bits 10-11

Without this connection, temp_vram_addr behaves as if scrolling always starts from logical nametable 0 ($2000), even when game software selected $2400, $2800, or $2C00. A future viewport renderer would therefore cross the wrong logical boundary.

Internal scrolling layout

temp_vram_addr (t): yyy NN YYYYY XXXXX

XXXXX -> coarse X tile position
YYYYY -> coarse Y tile position
NN    -> logical nametable selection
yyy   -> fine Y pixel position

fine_x (x) is stored separately

PPUCTRL mapping

bits 0-1 = 00 -> logical nametable $2000
bits 0-1 = 01 -> logical nametable $2400
bits 0-1 = 10 -> logical nametable $2800
bits 0-1 = 11 -> logical nametable $2C00

Suggested implementation

# emulator/ppu/ppu.py

case 0x2000:
    self.ctrl = value

    # --- NEW LINE: COPY BASE NAMETABLE INTO temp_vram_addr: ...GH.. ........ <- value: ......GH
    self.temp_vram_addr = (self.temp_vram_addr & 0b1111_0011_1111_1111) | ((value & CTRL_BASE_NAMETABLE_MASK) << 10)
    # --- END NEW LINE ---

Why clear bits 10-11 first? A game may change from any logical nametable to any other logical nametable. Using only OR would set new bits but could not clear old bits. The clear-then-insert operation replaces the previous selection while preserving coarse X, coarse Y, and fine Y.

Why not use the old scroll field? $2005 receives two writes, but the compatibility scroll field stores only the most recent byte. temp_vram_addr plus fine_x preserve the complete hardware-style state.

Important distinction

This step records logical nametable selection. Cartridge mirroring then maps that logical selection onto physical nametable RAM. Neither operation moves the visible viewport by itself; viewport extraction/rendering comes in later steps.

Out of scope

  • deriving viewport pixel X/Y
  • rendering adjacent nametables
  • transferring t into v at exact hardware dots
  • changing PpuBus mirroring
  • commercial ROM fixtures

Run this lesson

uv run pytest tests/chapter_13_scrolling/test_328_ppuctrl_temp_nametable_bits.py -v