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

Last updated: October 9, 2026

A running glossary for terms I keep using in the devlog. Bookmark this if you're new to SNES dev.


Byte-exact / matching

A function is "matched" when the assembly source I've written can be run through the assembler and produces output identical to the original ROM — byte for byte. Not just "equivalent behavior", literally the same bytes. This matters because the goal of a matching decompilation is a reproducible reconstruction of the original binary, not a reimplementation.

Matched, readable, verified

The three levels a routine goes through, each including the one before. Matched: every byte from its label to the next is emitted by the source and equals the ROM (make diff, make verify). Readable: it also passes the readability lint (named RAM and constants, explicit widths, a header with its CPU state on entry and exit). Verified: an independent reviewer approved its current source; the approval is tied to a hash of that source, so any edit sends it back for review. The Progress page counts all three.

ROM bank

The 65816's 24-bit address space is split into 256 "banks" of 64KB each. The Chrono Trigger ROM is 4MB total, which works out to 64 banks of 64KB each. When I write $C0:B271, that means bank $C0, address $B271 within that bank.

HiROM

One of two common SNES ROM mapping layouts (the other is LoROM). In HiROM, the full 64KB of each bank is available as ROM — bank $C0 maps directly to the first 64KB of the ROM file, bank $C1 to the next 64KB, etc. This is different from LoROM where only the upper 32KB of each bank is ROM. Chrono Trigger uses HiROM.

65816

The CPU inside the SNES. It's an extended version of the classic 6502 (the same chip as in the NES, Commodore 64, Apple II). The "816" part means it can run in two modes: an 8-bit emulation mode (compatible with old 6502 software) and a full 16-bit native mode. Chrono Trigger switches to native mode almost immediately at boot.

Opcode

The first byte of a CPU instruction that tells the CPU what operation to do. For example, opcode $78 means "SEI" (disable interrupts), and opcode $60 means "RTS" (return from subroutine). The bytes that follow (0 to 3 more) are the operand — the data the instruction works on.

M flag and X flag

Two bits inside the 65816 processor status register (P) that control the width of certain operations:

  • M=0 means the accumulator (A) is 16-bit wide. M=1 means A is 8-bit.
  • X=0 means the index registers (X and Y) are 16-bit. X=1 means they're 8-bit.

You change these with SEP #$20 (set M=1, A→8-bit), REP #$20 (clear M=0, A→16-bit), and similarly SEP #$10 / REP #$10 for X/Y.

These flags change the size of instructions mid-code, which is one of the things that makes 65816 assembly tricky to disassemble without knowing the mode at every point.

DP (Direct Page)

Also called "zero page" in 6502 land. The 65816 has a special register called the Direct Page pointer (D). When you write a "dp mode" instruction like LDA $6D, it doesn't load from address $006D — it loads from D + $6D. The field engine runs with D = $0100, so there LDA $6D actually reads WRAM address $016D (Obj_Cur). Other code uses other pages: the map code $1D00, the battle and the scene mode $0000, and init code points D at a register block ($2100, $4200) so every register write is a 2-byte instruction.

This is a common source of confusion when reading disassembly. If you see small two-digit hex addresses used in LDA, STA, etc., they're almost certainly dp-relative.

WRAM

The SNES has 128KB of work RAM (WRAM), mapped at $7E:0000–$7F:FFFF. This is where the game stores everything that changes at runtime: game mode, sprite positions, input state, audio queues, etc. (The ROM itself is read-only.)

DMA

Direct Memory Access. A hardware mechanism that lets the SNES copy blocks of memory without the CPU doing it one byte at a time. Very fast. Chrono Trigger uses DMA for:

  • Copying graphics data to VRAM
  • Clearing WRAM (by reading from a register that always returns 0 and writing to WRAM)
  • Uploading OAM (sprite attribute) data to the PPU

NMI / VBlank

NMI stands for Non-Maskable Interrupt. On the SNES, it fires at the start of VBlank — the ~1.2ms window each frame (at 60fps, every ~16.7ms) when the PPU is not drawing to the screen. This is the only safe time to update PPU registers and upload data to VRAM/OAM. On the field, NmiHandler ($C0:EA63) does the frame's uploads (palette, OAM, queued graphics, map edges) once the game has finished the frame; see Frame cycle and NMI.

VBlank

The vertical blank period — the gap between the last visible scanline and the next frame starting. See NMI above.

OAM

Object Attribute Memory. The PPU's internal table that defines up to 128 sprites: their position, tile, palette, flip flags, and size. The game builds OAM data in WRAM during the frame, then DMA-uploads it to the PPU during VBlank.

PPU

Picture Processing Unit. The SNES graphics chip. Handles backgrounds (BGs), sprites, color palettes, windows, and compositing. Distinct from the CPU.

JSR / JSL / JML / RTS

Jump instructions:

  • JSR addr — Jump to subroutine (within same bank, stores return address on stack)
  • JSL addr — Jump to subroutine long (24-bit address, crosses banks)
  • JML addr — Jump long (no return address, like a goto)
  • RTS — Return from subroutine (pops return address from stack, jumps back)

BRL

Branch Long. Like a BRA (unconditional branch) but with a 16-bit relative offset instead of 8-bit, so it can jump further. The offset is signed and relative to the next instruction's address. asar 1.91 computes it for you from a label (BRL Oam_BuildShadow), but stores a number as the raw offset (BRL $B193), so always write the label; see asar Quirks.

asar

The assembler used for this project. It takes .asm source files and patches them into a copy of the ROM. Works in "patch mode" — you give it the assembly source and the ROM to patch, and it overwrites just the bytes you've specified. Version 1.91 is what we're using.

Reassembly

Running the matched assembly through the assembler and comparing the output to the original ROM (make diff). If the diff shows 0 bytes differ, the match is byte-exact. make verify goes one step further: it assembles onto blank bases, so a routine can't hide behind bytes the original ROM already had.