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

Last updated: October 9, 2026

asar 1.91 Quirks

Surprises in how asar 1.91 picks an instruction's encoding. Future contributors: read this before spending an hour debugging a byte offset.

The short version: asar does not track the M and X flags, and it picks an operand's width from how the operand is written. The project's style rule (STYLE.md) is the fix for all of it: every data instruction whose operand is a define or struct field says .b, .w or .l. With that, every encoding in the matched code is written as an instruction; no opcode bytes are hand-written (db $A9,$7F), and the few encodings that could not be written as an instruction would get a named macro in asm/include/macros.inc (it is empty so far).

Earlier versions of this page recommended raw db bytes, raw BRL offsets and raw hex operands. Those workarounds were wrong or no longer needed (the code repo's STATUS.md records two of them under "Known issues"); the notes below are what the current source does.

1. A label or equate operand assembles as a long address

Symptom: STA INIDISP, where INIDISP = $2100 is an equate (as in asm/hardware.inc), emits opcode 8F (STA long, 4 bytes) instead of 8D (STA absolute, 3 bytes). Everything after it shifts by a byte.

Cause: asar treats a label or equate as a 24-bit address.

Fix: give the width: STA.w INIDISP (8D 00 21). The same goes for STX/STY, which have no long form: plain STX A1T7L is an error ("Unknown command"), while STX.w A1T7L assembles to 8E 72 43. The matched code writes, for example, STX.w A1T7L and STY.w VMADDL.

Defines (!Name = $2100) are textual, so STA !Name takes the width of the number as written ($2100: absolute). The style rule asks for an explicit width on those too, so the encoding cannot drift when a define's value is written differently.

2. Immediate width follows the digits, not the M flag

Symptom: in a 16-bit accumulator section (after REP #$20), LDA #$7F assembles to the 2-byte A9 7F, and the CPU then reads the next byte as the operand's high byte.

Cause: asar doesn't know that M=0. It sizes an immediate by how it is written: #$7F is one byte, #$007F or #$0040 two (SBC #$0040 gives E9 40 00 whatever M is).

Fix: write the width the CPU will use: LDA.w #$007F in 16-bit code, LDA.b #!Bank7F in 8-bit code. The battle number formatters, entered with M=0 by their callers, carry an explicit .w on every SBC/ADC immediate for this reason. (An earlier note blamed asar for "not tracking REP #$20" in SBC/ADC and fixed it with db bytes; the .w suffix alone reproduces those encodings.)

3. Direct page or absolute is chosen the same way

STA $01,X assembles as direct page, X (95 01) and STA $0001,X as absolute, X (9D 01 00), again by the digits written. With named operands, use .b for direct page and .w for absolute; code that relocates the direct page onto a register block writes the register relative to its base, as in STA.b INIDISP-!DP_PPU.

4. BRL with a number stores the number as the offset

BRL SomeLabel works: asar computes the 16-bit offset from the next instruction. But BRL $B193 stores $B193 itself as the offset, not as a target address. Write the label.

When the original's offset wraps around the 64 KB bank boundary, the matched code writes the target minus one bank, BRL Target-!BankWrap (!BankWrap = $10000 in asm/include/constants_engine.inc), so asar computes the same wrapped offset; Field_EndOfFrame's tail jump to Oam_BuildShadow is one.

5. Calls to code that is not matched yet

A JSR, JSL or JML to an address outside the matched code needs a name. The project gives each such target a label-only stub in asm/include/unmatched.asm or unmatched_battle.asm: an org to the address and a label, emitting no bytes. When the routine is matched its stub is deleted and the real label takes over. Bank files never hold stubs, and call sites never use raw addresses.

65C816 Assembly
org $C0F05E
Irq_UnkF05E:        ; label only: no bytes emitted

Quick reference

Situation Wrong Right
Store to a named register STA INIDISP (long) STA.w INIDISP
STX/STY to a named register STX A1T7L (error) STX.w A1T7L
16-bit immediate LDA #$7F (1-byte operand) LDA.w #$007F
8-bit immediate from a define LDA #!Name LDA.b #!Name
Long branch BRL $B193 (raw offset) BRL Label (or Label-!BankWrap across the bank edge)
Call into unmatched code JSR $9E29 a stub label in asm/include/unmatched*.asm