Skip to content

perf(cpu): code-generate the opcode dispatch table - #67

Open
eduardovra wants to merge 1 commit into
mainfrom
perf/opcode-codegen
Open

perf(cpu): code-generate the opcode dispatch table#67
eduardovra wants to merge 1 commit into
mainfrom
perf/opcode-codegen

Conversation

@eduardovra

Copy link
Copy Markdown
Owner

Summary

Replaces the WDC 65816 decorator + string-arg instruction table with per-opcode handlers generated from a compact spec. All 256 opcodes now dispatch through generated op_NN(cpu) handlers; the old table is retired.

  • Memory/modify families (187 opcodes) — emitted as inlined literal handlers. The index register, width flag, and operation/source are baked in as literals, so PyPy folds them into the per-opcode JIT trace. The previous shared decorator + string-arg dispatch couldn't (the index/flag arrived as runtime args shared across opcodes). ~2× faster dispatch on hot read/write instructions.
  • Irregular ops (69) — branches, jumps, calls/returns, flags, transfers, push/pull, block move, interrupt, exchanges — emitted as thin handlers that delegate to the existing, tested addressing functions (lambdas hoisted to module level). No risky re-implementation of BlockMove/Interrupt/control-flow.

Layout

file role
opcodes_spec.py compact spec — source of truth (SPEC + MISC_SPEC)
scripts/gen_opcodes.py generator: edit spec → uv run scripts/gen_opcodes.py → commit
opcodes_generated.py committed generated handlers + HANDLERS
instructions.py 3-line adapter building INSTRUCTIONS from HANDLERS
scripts/bench_cpu.py deterministic dispatch micro-benchmark

Removes the now-inlined read.py / write.py / modify.py addressing-mode functions. cpu.py is untouched → existing save states are unaffected.

Verification

  • 6144 Tom Harte SingleStepTest cases pass (all opcodes × both M/X widths).
  • Full unit suite passes.
  • Generator output is reproducible (regenerate → no diff).
  • Dispatch ~2× faster in bench_cpu.py (≈125 ns → ≈45 ns/instruction on hot indexed ops).

Honest scope note

The dispatch speedup is real on the micro-benchmark, but the in-game effect is within measurement noise — dispatch is a small slice of total frame time (the rest is the operations, register access, APU, PPU). The primary value here is architectural uniformity (one spec-driven dispatch system, retiring the decorator+string table and ~570 lines of hand-written addressing-mode functions), not felt FPS.

🤖 Generated with Claude Code

Replace the decorator + string-arg instruction table with per-opcode handlers
generated from a compact spec.

Memory/modify families (187 opcodes) are emitted as inlined literal handlers —
index register, width flag, and operation/source baked in, so PyPy folds them
per-opcode trace (~2x faster dispatch on hot read/write instructions; the shared
decorator+string dispatch could not). The irregular ops (69: branches, jumps,
calls/returns, flags, transfers, push/pull, block move, interrupt, exchanges)
are emitted as thin handlers that delegate to the existing addressing functions.

  opcodes_spec.py        compact spec (source of truth)
  scripts/gen_opcodes.py generator (edit spec -> regenerate -> commit)
  opcodes_generated.py   committed generated handlers + HANDLERS
  instructions.py        3-line adapter over HANDLERS

Removes the now-inlined read.py/write.py/modify.py addressing-mode functions.
cpu.py is untouched, so existing save states are unaffected.

Verified: 6144 Tom Harte SingleStepTest cases (all opcodes x both M/X widths)
and the full unit suite pass; regenerator output is reproducible. Dispatch ~2x
faster in a micro-benchmark; in-game effect is within noise (dispatch is a small
slice of frame time) — the win here is architectural uniformity.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant