Kajar notes of ChronoRET, rebuilding Chrono Trigger one matched routine at a time

20 July 2026: setting up, and the last page of bank $00

The project starts. I set up the build, found the reset vector, and matched the whole last page of bank $00: 256 bytes of boot stubs, tables, ROM header and vectors.

I have wanted to do a proper SNES decompilation for years. Chrono Trigger is the obvious choice: well documented, much loved, and an engine worth understanding. If you don't read 65816 assembly, the Glossary explains the terms used here.

The reset vector

Every decomp starts with the question "what am I looking at?". I have a legally dumped ROM of the US 1.0 release, and the obvious entry point is the reset vector.

At power-on the SNES CPU starts in 6502-emulation mode and jumps to the address stored at $FFFC–$FFFD. Chrono Trigger is HiROM, so that is file offset $FFFC. The bytes there are $FF00, so the first instruction runs at $FF00.

That is the reset stub, and it is 7 bytes:

65C816 Assembly
SEI          ; disable interrupts while we set up
CLC
XCE          ; switch from 6502-emulation mode to native 65816 mode
JML $FDC000  ; jump to MainInit in bank $FD

The rest of the page $FF00–$FFFF (the last page of bank $00) holds:

  • the reset stub
  • NMI and IRQ stubs, which jump to RAM-resident handlers (not installed yet at boot)
  • a BRK handler, an infinite loop that acts as a crash handler
  • two 8-byte lookup tables, BitSet and BitClear: precomputed (1<<N) and ~(1<<N) for N=0..7
  • two 64-byte sine-wave tables, ScrollWaveA and ScrollWaveB
  • the 80-byte SNES ROM header (title, memory map type, checksum and so on)
  • the native-mode interrupt vectors

All 256 bytes fit in one page, and all of them match.

A 16 by 16 grid of the 256 bytes from $00:FF00 to $00:FFFF, coloured by region

The last page of bank $00, one cell per byte: the reset stub, the interrupt stubs, the BRK handler, the two 8-byte bit tables, the two 64-byte wave tables, the cartridge header and the vectors. The RESET vector at $FFFC points back at the stub in the top-left corner.

The toolchain

The build uses:

  • asar 1.91, the standard SNES assembler used by nearly every SNES decomp
  • Python 3 for tooling (diff scripts, progress tracking)
  • make to tie it together

The build copies the original ROM, runs asar to patch in the matched assembly, and compares the result with the original byte for byte. "0 bytes differ" means a good match.

Flow diagram: the original ROM is copied, asar writes the matched source over the copy, and diff_rom.py compares the result with the original

How the July build checked a match: copy the ROM, let asar write the matched source over the copy, then compare every byte with the original. (The build stopped copying the ROM in October; see 15 October.)

One flag matters: --fix-checksum=off. Without it asar recalculates the ROM checksum, and the output differs from the original by 4 bytes. Always pass it.

What I learned

HiROM addressing. Bank $C0 maps directly to the first 64 KB of the ROM file. Bank $FD is at file offset ($FD - $C0) × $10000 = $3D0000. The reset vector at $FFFC is in bank $00, which mirrors bank $C0 for addresses $8000–$FFFF in HiROM. Since $FF00 is in the upper half, bank $00 works as expected here.

Diagram of HiROM mapping: CPU banks $C0 to $FF map one to one onto the 64 banks of the ROM file

HiROM in one picture: each CPU bank from $C0 to $FF is a whole 64 KB bank of the file, and banks $00-$3F show the upper half of the same banks, which is how the reset vector at $00:FFFC lands at file offset $00FFFC.

The ROM header. The 80-byte header at $00:FFB0 holds the title ("CHRONO TRIGGER"), the memory map type ($31 = HiROM FastROM), ROM size, SRAM size and the checksum that asar would otherwise overwrite.

Wave tables. The two 64-byte sine-wave tables at $FF30 and $FF70 are not audio. They are scroll-effect tables that make backgrounds wobble. Each is a 32-entry signed 16-bit table with values in the range ±6.

Next

The reset stub jumps straight to $FD:C000, which is MainInit. Matching it is the way into the engine's startup sequence.

← Back to devlog