SearchA-ZP › Punch-Out!!

Punch-Out!!

1984 Arcade Proprietary · grey Online

Punch-Out!! (Nintendo, 1984) is the famous two-monitor boxing arcade game in which the player, as Little Mac, weaves and dodges past a roster of larger-than-life opponents. The upper monitor frames the opponent while the lower monitor shows Mac from behind with the health bars and round clock. This build runs the original Nintendo board in the browser, a pure-JavaScript Zilog Z80 core plus a hand-written model of the tile-and-big-sprite video hardware ported from MAME, and boots straight into the game’s attract mode. It renders the bottom (main game) monitor and is wired to the emulators.org in-frame debugger so you can single-step the Z80, read and write the registers and the full 64K memory, set execution breakpoints and set write watchpoints.

View the MAME driver ↗

Visit the official site ↗

Runs on: Web browser

Punch-Out!! Online Emulator

Play Punch-Out!! using JavaScript directly in your browser.

Configurations

ConfigurationEmulatorMachineOSLegal
Punch-Out!!Punch-Out!!Punch-Out!!greyOpen ⛶

Machine emulated

The Punch-Out!! arcade board (Nintendo, 1984) - a Zilog Z80 at 4 MHz (an 8 MHz crystal divided by two) with 48 KB of program ROM driving Nintendo’s tile-and-big-sprite video across two stacked monitors: two 8×8 tile background layers plus two "big sprites" (the boxers) that the hardware scales and positions with a zoom/roz unit, coloured through six PROMs. A second RP2A03 CPU and a VLM5030 speech synth handle sound on the real cabinet. This build models the main (bottom) monitor.

Chips

Notes

Embedding

Punch-Out!! is Nintendo's 1984 arcade boxing machine, famous for its two stacked monitors: the top screen shows the opponent up close, the bottom screen shows Little Mac from behind together with the health bars and round clock. Both are driven by a single Zilog Z80 at 4 MHz. This build runs the original program ROMs on DrGoldfire/Z80.js (MIT), an instruction-at-a-time Z80 interpreter, wrapped in a hand-written model of the Punch-Out!! board (memory map, tile + big-sprite video and input latches) ported from MAME's punchout.cpp. It renders the bottom (main game) monitor.

Boot. The program ROMs are loaded into 0x0000–0xBFFF and the Z80 starts at 0x0000, dropping straight into the attract-mode demo:

var po = new PunchOut();   // decodes gfx ROMs + PROMs, wires the Z80
po.reset();                  // PC = 0x0000 -> attract mode

The machine object. PunchOut exposes the cabinet as ordinary methods, so the host page can render, step and inject input:

MemberKindWhat it does
stepInsn()methodFetch, decode and run one Z80 instruction; returns its T-cycle count.
frameInterrupt()methodPulse the vertical-blank NMI (gated by the mainlatch Q0 mask the game writes to I/O port 0x08) once per field.
render(buf32)methodPaint the 256×224 bottom monitor: the scrolling background tilemap plus the two zoomable big sprites (the boxers).
peek(a) / poke(a,v)methodSide-effect-free access to the address space - what the debugger's memory views read and poke.
setInput(id,down)methodDrive the joystick / punch / coin latches read on I/O ports 0x00 (IN0) and 0x01 (IN1).
reset()methodClear RAM and reset the Z80 - the cabinet's power-cycle.

Video. Each animation frame the loop runs one field's worth of Z80 cycles, pulses the NMI, then re-renders. The bottom monitor is a 64×32 tilemap of 8×8 characters (video RAM at 0xF000, with per-row horizontal scroll RAM in the same window), over which two "big sprites" are drawn: sprite #1 (the 3bpp boxer, video RAM 0xE000) is scaled through a zoom/roz blit, and sprite #2 (the 2bpp boxer, video RAM 0xE800) is drawn 1:1. Colours come from the bottom-monitor colour PROMs (white-labelled set), banked by 0xDFFD.

Debugger integration

The debugger drives a host-owned run loop: because the Z80 interpreter runs one instruction at a time in JavaScript, the loop can pause, single-step and check breakpoints between any two instructions - no changes to the CPU core are needed.

window.EMU_BOOT.transport exposes the controls the shared debugger calls:

  • pause / resume / isPaused - stop or restart the requestAnimationFrame loop.
  • stepInsn(n) - call the core's stepInsn() exactly n times and redraw, so a single step advances the PC by one Z80 instruction.
  • step(n) - advance n whole 60 Hz fields (each with its NMI).
  • breakpoints - a Set of PC values. When it is non-empty the loop reads the Z80's PC (via getState()) before each instruction and pauses before executing an address in the set; when empty it runs a full field at speed, so an idle debugger costs nothing.
  • watchpoints - the Z80 core's memory-write callback is wrapped, so a write to a watched RAM address sets a flag that pauses the loop after that instruction.

The plug-in (punchout-debug.js) reads these hooks and calls EmuKit.defineMachine with the full Z80 register file (AF/BC/DE/HL and their bytes, IX/IY, SP, PC, I, R and the S Z H P/V N C flags), each read live from getState() and written back through setState(). It reuses the shared z80 disassembler (/debugger/src/cpus/z80.js) for the hex/disasm views over the 64 KB bus, the 48 KB program ROM, the background tile RAM and the two big-sprite RAMs.

Architecture

The Punch-Out!! board is a single Zilog Z80 at 4 MHz (an 8 MHz crystal divided by two) with 48 KB of program ROM and Nintendo's tile-and-big-sprite video across two monitors, all modelled here as plain JavaScript hanging off PunchOut:

  • CPU - a Z80 interpreter. 0x0000–0xBFFF is program ROM; 0xC000–0xFFFF is work RAM plus video RAM: the top-monitor tilemap at 0xD800, the big-sprite control latches at 0xDFF0, big-sprite #1 RAM at 0xE000, big-sprite #2 RAM at 0xE800 and the bottom-monitor tilemap at 0xF000.
  • Video - two 8×8 tile background layers (2bpp), plus two "big sprites" made of 16×32 arrangements of 8×8 tiles that the hardware scales and positions with a zoom/roz unit - this is how the boxers grow and shrink. Sprite #1 is 3bpp, sprite #2 is 2bpp. Six 256-nibble colour PROMs give the two monitors' palettes; the 0xDFFD latch selects the colour bank per monitor.
  • Input - read on Z80 I/O ports: the two punch buttons on port 0x00 (IN0), the 4-way joystick plus coin and service on port 0x01 (IN1); the DIP switches on ports 0x02/0x03.
  • Interrupt - a single NMI per field at vertical blank, gated by the mainlatch Q0 mask (I/O port 0x08), is the only timing the game depends on.
  • Sound - a separate sound board carrying a Ricoh RP2A03 (NES 2A03) sound CPU plus a VLM5030 speech synth. Both run from their genuine ROMs: the real 2A03 program on a 6502 core drives the emulated APU, and the VLM5030 replays its speech-data ROM, driven from the Z80's sound latches and LS259 control lines (see the Sound note).

Sound

The real sound hardware, driven by the real ROMs. Punch-Out!!'s audio is a separate sound board carrying one populated Ricoh RP2A03 (the NES 2A03: a 6502 with an integrated APU - two pulse channels, a triangle, a 15-bit LFSR noise channel and a direct DAC; the second 2A03 socket is unpopulated) plus a Sanyo VLM5030 LPC speech synthesiser (“Body blow!”, the referee's count, the crowd). Both are emulated here from their genuine ROMs - nothing is synthesised. The main Z80 never touches the APU directly; it writes command bytes to two sound latches (OUT 0x02 → soundlatch, OUT 0x03 → soundlatch2) and drives the VLM's data bus (OUT 0x04) and control lines, and the RP2A03 runs its own program to turn a command into APU register writes.

The RP2A03. A 6502 core (punchout-6502.js: documented opcode set, decimal disabled as on the 2A03, per-opcode cycle table, NMI/IRQ) runs the genuine sound program ROM (chp1-c.4k, mapped at $E000–$FFFF). It reads the two latches at $4016/$4017 and writes the APU registers $4000–$4017. The 2A03 APU is emulated faithfully in punchout-apu.js: duty-cycle pulses with envelope/sweep/length, the 32-step triangle with its linear counter, the 15-bit LFSR noise, the $4011 7-bit direct DAC the program streams the crowd noise through, a cycle-driven 240/120 Hz frame sequencer and the NES non-linear mixer.

The VLM5030 speech. punchout-vlm.js is a port of MAME's sound/vlm5030.cpp with the decapped coefficient table from sound/tms5110r.hxx - a 10-tap lattice filter with energy/pitch/reflection-coefficient interpolation and silent/voiced/unvoiced excitation - replaying the real speech-data ROM (chp1-c.6p). It is fed from the Z80 through punchout.js setSoundHook: OUT 0x04 is the data bus and the LS259 bits OUT 0x0c/0x0d/0x0e are the RST/ST/VCU pins (OUT 0x0b is the sound-CPU reset). The VLM's BSY pin is read back by the Z80 as DSW1 bit 4.

Sample rate & frame. Each video field the sound CPU takes one NMI (top-monitor vblank) and runs a field's worth of cycles; the APU channels are generated at EmuAudio.sampleRate (pulse period → frequency via CPU/(16·(t+1)), triangle via CPU/(32·(t+1))) and the VLM5030 (native rate clock/440 ≈ 8135 Hz) is resampled, the two mixed, and exactly Math.round(EmuAudio.sampleRate/60) interleaved-stereo Int16 samples pushed to the shared sink once per field. A one-pole DC blocker centres the APU's unipolar output.

Mute contract. window.EMU_BOOT.transport exposes isMuted()/setMute(m), delegated to EmuAudio; audio starts muted (browsers block audio before a gesture) and the shell's Sound button resumes the AudioContext and unmutes from a real click. The attract demo drives the real 2A03 (the “Demo Sounds” DIP defaults on); press coin then Start to begin a match and hear the in-match tunes and the announcer.

Limitations (honest). The 6502 is instruction-stepped with base-cycle timing (no page-cross/branch penalty or sub-instruction bus timing) and the APU channels are phase-accumulated at the output rate rather than per-CPU-cycle, so timing is field-accurate rather than cycle-exact. The VLM5030 follows MAME's integer lattice-filter model, so its timbre matches MAME rather than an analog-perfect chip. Everything you hear - the music, effects, crowd DAC and speech - comes from the genuine sound program and speech ROM, not from any hand-written content.