266. Opcode cycles table

Validate opcode cycle metadata before changing CPU.step().

Lesson 266 of 356 · tests/chapter_04_ppu_timing_and_vblank/test_266_opcode_cycles_table.py

Reference

https://www.nesdev.org/wiki/Visual6502wiki/6502_all_256_Opcodes

File to update

emulator/cpu/opcodes.py

Why this intermediate step exists

The emulator already has working opcode dispatch through

OPCODE_TABLE[opcode] -> handler(cpu)

We do not want to refactor that table yet, because old CPU chapter tests already depend on the current instruction execution path.

Instead, this step introduces timing as separate metadata

OPCODE_CYCLES[opcode] -> base CPU cycle count

This lets a future CPU.step() return cycles without changing every instruction handler or replacing the existing opcode dispatch table.

Suggested implementation example

Copy this table into emulator/cpu/opcodes.py. It was created from the opcode timing reference linked above:

OPCODE_CYCLES = [

7,6,0,8,3,3,5,5,3,2,2,2,4,4,6,6,
3,5,0,8,4,4,6,6,2,4,2,7,4,4,7,7,
6,6,0,8,3,3,5,5,4,2,2,2,4,4,6,6,
2,5,0,8,4,4,6,6,2,4,2,7,4,4,7,7,
6,6,0,8,3,3,5,5,3,2,2,2,3,4,6,6,
3,5,0,8,4,4,6,6,2,4,2,7,4,4,7,7,
6,6,0,8,3,3,5,5,4,2,2,2,5,4,6,6,
2,5,0,8,4,4,6,6,2,4,2,7,4,4,7,7,
2,6,2,6,3,3,3,3,2,2,2,2,4,4,4,4,
3,6,0,6,4,4,4,4,2,5,2,5,5,5,5,5,
2,6,2,6,3,3,3,3,2,2,2,2,4,4,4,4,
2,5,0,5,4,4,4,4,2,4,2,4,4,4,4,4,
2,6,2,8,3,3,5,5,2,2,2,2,4,4,6,6,
3,5,0,8,4,4,6,6,2,4,2,7,4,4,7,7,
2,6,2,8,3,3,5,5,2,2,2,2,4,4,6,6,
2,5,0,8,4,4,6,6,2,4,2,7,4,4,7,7

]

Important correction

The first value must be 7 because opcode $00 is BRK, and BRK takes 7 CPU cycles. If your copied table has 0 at index $00, fix it to 7 because this emulator already implements BRK.

What is an opcode cycle table? An opcode cycle table is a 256-entry lookup table where each index is an opcode byte and each value is the base number of CPU cycles for that opcode.

Minimal example

OPCODE_CYCLES[0xEA] == 2   # NOP implied
OPCODE_CYCLES[0xA9] == 2   # LDA immediate
OPCODE_CYCLES[0x20] == 6   # JSR absolute

Common misconception

Cycle count is not the same thing as instruction length or number of fetches. For example, JSR is 3 bytes but takes 6 CPU cycles.

How this appears in the emulator

Current step shape:
    opcode = cpu.fetch_byte()
    handler = OPCODE_TABLE[opcode]
    handler(cpu)

Future step shape, without refactoring OPCODE_TABLE:
    opcode = cpu.fetch_byte()
    handler = OPCODE_TABLE[opcode]
    handler(cpu)
    return OPCODE_CYCLES[opcode]

Important limitation

These are base cycle counts. Some instructions later need dynamic extra cycles, for example:

branch taken
branch crosses page
indexed load crosses page

Do not solve those dynamic penalties in this step. First make the base timing metadata explicit and testable.

Out of scope

  • changing CPU.step() to return cycles
  • refactoring OPCODE_TABLE entries into dataclasses
  • modifying old CPU instruction tests
  • dynamic page-crossing or branch cycle penalties
  • Console.step() advancing PPU by CPU cycles * 3

Run this lesson

uv run pytest tests/chapter_04_ppu_timing_and_vblank/test_266_opcode_cycles_table.py -v