327. Ppu bus horizontal vertical mirroring

Apply horizontal or vertical nametable mirroring in PpuBus.

Lesson 327 of 356 · tests/chapter_12_mirroring/test_327_ppu_bus_horizontal_vertical_mirroring.py

File to update

emulator/bus/ppu_bus.py

Why this step exists

The mirroring bit now travels through

INesHeader
    -> Cartridge
    -> Mapper000
    -> PpuBus.mapper

PpuBus can finally use that metadata when mapping four logical nametables onto two physical nametable RAM regions.

Logical nametables

table 0: $2000-$23FF
table 1: $2400-$27FF
table 2: $2800-$2BFF
table 3: $2C00-$2FFF

Each table is $400 bytes. The complete logical window is $1000 bytes, while the current physical backing window is $800 bytes.

Vertical mirroring

logical:  0 1 2 3
physical: 0 1 0 1

[A B]
[A B]

Horizontal mirroring

logical:  0 1 2 3
physical: 0 0 1 1

[A A]
[B B]

Suggested implementation example

# emulator/bus/ppu_bus.py

NAMETABLE_SIZE = 0x800
NAMETABLE_BYTES_PER_TABLE = 0x400
NAMETABLE_LOGICAL_SIZE = 0x1000


def normalize_nametable_addr(self, addr: int) -> int:
    if 0x3000 <= addr <= 0x3EFF:
        addr -= 0x1000

    logical_offset = (
        addr - NAMETABLE_START
    ) % NAMETABLE_LOGICAL_SIZE

    logical_table = logical_offset // NAMETABLE_BYTES_PER_TABLE
    offset_inside_table = logical_offset % NAMETABLE_BYTES_PER_TABLE

    is_vertical_mirroring = (
        True
        if self.mapper is None
        else self.mapper.is_vertical_mirroring
    )

    if is_vertical_mirroring:
        physical_table = logical_table % 2
    else:
        physical_table = logical_table // 2

    return (
        NAMETABLE_START
        + physical_table * NAMETABLE_BYTES_PER_TABLE
        + offset_inside_table
    )

Why preserve vertical behavior without a mapper? Historical tutorial tests construct PpuBus directly and introduced the old fixed $800 wrapping behavior before cartridge mirroring existed. Mapper-backed buses use real cartridge metadata; mapper-less buses preserve that historical test model.

Important distinction

Mirroring determines memory aliases. It does not move the visible viewport. The current renderer still draws a fixed nametable region, so horizontal scrolling is a separate future chapter.

Out of scope

  • scroll-position extraction
  • viewport cropping across adjacent nametables
  • four-screen mirroring
  • mapper-controlled dynamic mirroring
  • commercial ROM fixtures

Run this lesson

uv run pytest tests/chapter_12_mirroring/test_327_ppu_bus_horizontal_vertical_mirroring.py -v