289. Oamdma 4014
Implement OAMDMA at CPU address $4014.
Lesson 289 of 356 · tests/chapter_06_rom_startup_preparation/test_289_oamdma_4014.py
File to update
emulator/bus/cpu_bus.pyWhy this step exists
Real NES games usually prepare sprite bytes in CPU memory and then trigger OAMDMA to copy those bytes into PPU OAM. Even before sprite rendering exists, this copy mechanism matters because it lets real-ROM startup code keep running and gives the future sprite renderer real OAM data to consume.
What is OAM? OAM means Object Attribute Memory. It is the PPU's 256-byte sprite memory:
64 sprites * 4 bytes each = 256 bytesEach sprite entry is shaped like
byte 0: Y position
byte 1: tile index
byte 2: attributes
byte 3: X positionWhat is OAMDMA? OAMDMA is a CPU-bus write mechanism. Writing one byte to $4014 selects a CPU memory page and copies all 256 bytes from that page into PPU OAM.
Minimal example
CPU writes $02 to $4014This means
copy CPU $0200-$02FF -> PPU.oam[0x00-0xFF]Suggested implementation example
def write(self, addr: int, value: int) -> None:
...
# OAMDMA: copy one CPU page into PPU OAM.
if addr == 0x4014:
page_start = (value & 0xFF) << 8
for offset in range(256):
self.ppu.oam[offset] = self.read(page_start + offset)
return
...Important detail
OAMDMA should read through CpuBus.read(), not directly from raw RAM. The source is CPU address space. Most games use pages like $0200-$02FF, but using bus reads keeps the mechanism correct and avoids future refactors.
Common misconception
"OAMDMA means sprites are rendered."No. This step only copies bytes into PPU.oam. Sprite decoding/rendering comes later.
Read behavior
$4014 is a write-triggered register for this tutorial. CpuBus.read($4014) should remain unsupported for now.
Out of scope
- sprite rendering
- sprite priority
- sprite transparency
- sprite 0 hit
- sprite overflow
- 513/514 CPU cycle DMA stall timing
- controller $4016
Run this lesson
uv run pytest tests/chapter_06_rom_startup_preparation/test_289_oamdma_4014.py -v